1 Commits
Author SHA1 Message Date
clawbot a72f301fe1 fix: the bundled token list names tokens on Ethereum Mainnet only (closes #632)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
bundledToken(address, chainId) in tokenList.js answers only for Ethereum
Mainnet's chain id. Every reader of TOKEN_BY_ADDRESS now goes through it,
with the chain id of the transaction, balance or history entry it draws.
The token sources the screens build carry that chain id. The Uniswap
decoder sets the transaction's chain id on them. fetchTokenBalances()
takes the chain id of the network it asks about.

isSpoofedSymbol() takes the chain id of the token's network. It checks the
bundled list's symbols only on Ethereum Mainnet. The native asset's labels
stay reserved on every network.

Eighteen existing tests and three shared test helpers read the bundled
list without naming a network. Each now names Ethereum Mainnet.

Model: opus-5-5
2026-10-27 20:14:25 +00:00
2 changed files with 39 additions and 39 deletions
+37 -38
View File
@@ -1474,23 +1474,24 @@ tracked.
A tracked token is shown when its balance is nonzero, and any other ERC-20 when A tracked token is shown when its balance is nonzero, and any other ERC-20 when
its balance is nonzero and it is in the bundled known-token list or has 1,000 or its balance is nonzero and it is in the bundled known-token list or has 1,000 or
more holders. On Ethereum Mainnet, a token claiming a symbol from the bundled more holders. On Ethereum Mainnet, a token that claims a bundled token's symbol
list from any other contract address is always dropped, tracked or not. On every from another contract is always dropped, tracked or not. On every network, a
network, a token claiming a label of the native asset is always dropped. Those token claiming a label of the native asset is always dropped. Those labels are
labels are `"ETH"` and each network's `nativeCurrency`, such as `"SepoliaETH"`. `"ETH"` and each network's `nativeCurrency`, such as `"SepoliaETH"`. The balance
That filter is unconditional — the "Hide tokens with fewer than 1,000 holders" list applies all three rules whatever the settings say. The "Hide tokens with
setting governs the transaction history and the send-screen token selector, not fewer than 1,000 holders" setting governs only the transaction history and the
this list. A token's holder count is unknown when the explorer reports none, or send-screen token selector. A token's holder count is unknown when the explorer
reports anything other than a whole number written in digits alone, such as reports none, or reports anything other than a whole number written in digits
`1,000` or `1e3` (`parseHoldersCount()` in `src/shared/holders.js`). This list alone, such as `1,000` or `1e3` (`parseHoldersCount()` in
does not take an unknown count as 1,000 or more, so such a token is shown only `src/shared/holders.js`). This list does not take an unknown count as 1,000 or
when it is on the bundled list or tracked. Every nonzero holding of a token more, so such a token is shown only when it is on the bundled list or tracked.
shown is stored exactly, however small, but a holding below 0.000001 is left out Every nonzero holding of a token shown is stored exactly, however small, but a
of the balance lists, the send-screen token selector, the address total and the holding below 0.000001 is left out of the balance lists, the send-screen token
remove-address warning (`isBelowOneMillionth()` in selector, the address total and the remove-address warning
`src/shared/amountDisplay.js`). The Send and confirmation screens show it when (`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
its token is the one being sent. Tracked tokens with a zero balance are listed confirmation screens show it when its token is the one being sent. Tracked
as well while "Show tracked tokens with zero balance" is on. tokens with a zero balance are listed as well while "Show tracked tokens with
zero balance" is on.
#### Stored state and its version #### Stored state and its version
@@ -3198,27 +3199,25 @@ indexes it as a real token transfer.
Ethereum mainnet ERC-20s taken from the CoinGecko API, with decimals verified Ethereum mainnet ERC-20s taken from the CoinGecko API, with decimals verified
on-chain and addresses EIP-55 checksummed; `TOKENS` in on-chain and addresses EIP-55 checksummed; `TOKENS` in
`src/shared/tokenList.js` is the authoritative set. It is bundled at build `src/shared/tokenList.js` is the authoritative set. It is bundled at build
time and only changes when that file is regenerated. On Ethereum Mainnet, any time and only changes when that file is regenerated. On Ethereum Mainnet, a
token transfer claiming a symbol from this list (e.g. "ETH", "USDT", "USDC") token that claims a symbol from this list, such as "USDT" or "USDC", is a
but originating from an unrecognized contract address is identified as a spoof spoof unless its contract is the listed one. The wallet hides it. The fake
and filtered from display. On another network, the list's symbols are not "Ethereum" token in the attack above used symbol "ETH" from contract
checked, because the list holds mainnet contracts. The fake "Ethereum" token `0xD05339f9Ea5ab9d9F03B9d57F671d2abD1F55c82`. No contract may bear "ETH", so
in the attack above used symbol "ETH" from contract that token is a spoof. Detecting a spoof is also what adds a contract to the
`0xD05339f9Ea5ab9d9F03B9d57F671d2abD1F55c82`, which does not match the known fraud contract blocklist below; that is the only thing that populates it. In
WETH contract — so it would be caught by this check. Detecting a spoof is also the transaction history the check is the "Hide fake tokens impersonating a
what adds a contract to the fraud contract blocklist below; that is the only known symbol" setting, on by default; with it off, spoofed transfers are shown
thing that populates it. In the transaction history the check is the "Hide and no new blocklist entries are learned from them. The send-screen token
fake tokens impersonating a known symbol" setting, on by default; with it off, selector and the balance list apply the same check unconditionally, because
spoofed transfers are shown and no new blocklist entries are learned from they decide which tokens the user can act on and what the user believes they
them. The send-screen token selector and the balance list apply the same check own rather than what the history displays. All three surfaces read the rule
unconditionally, because they decide which tokens the user can act on and what from `src/shared/symbolSpoof.js`, so they cannot answer the question
the user believes they own rather than what the history displays. All three differently. A symbol the list maps to no contract at all — the native asset's
surfaces read the rule from `src/shared/symbolSpoof.js`, so they cannot answer labels: `"ETH"` and every network's `nativeCurrency`, such as `"SepoliaETH"`,
the question differently. A symbol the list maps to no contract at all — the on every network — may be borne by no contract, so every ERC-20 claiming it is
native asset's labels: `"ETH"` and every network's `nativeCurrency`, such as a spoof on all three. The user's real ETH balance is not an ERC-20 and is read
`"SepoliaETH"`, on every network — may be borne by no contract, so every over RPC, so the rule never sees it.
ERC-20 claiming it is a spoof on all three. The user's real ETH balance is not
an ERC-20 and is read over RPC, so the rule never sees it.
- **Low-holder token filtering**: Token transfers from ERC-20 contracts with - **Low-holder token filtering**: Token transfers from ERC-20 contracts with
fewer than 1,000 holders are hidden from transaction history by default. fewer than 1,000 holders are hidden from transaction history by default.
+2 -1
View File
@@ -3978,7 +3978,8 @@ function bundledToken(address, chainId) {
// hides from the balance list, the history and the send selector at its own // hides from the balance list, the history and the send selector at its own
// address, so the user cannot spend it (issue #276). Naming every address // address, so the user cannot spend it (issue #276). Naming every address
// that bears the symbol is the only shape that says what is true; it does not // that bears the symbol is the only shape that says what is true; it does not
// loosen the rule, because a contract outside the set is still a spoof. // loosen the rule. On Ethereum Mainnet, a contract outside the set is still a
// spoof.
const KNOWN_SYMBOLS = new Map(); const KNOWN_SYMBOLS = new Map();
KNOWN_SYMBOLS.set("ETH", null); KNOWN_SYMBOLS.set("ETH", null);
for (const network of Object.values(NETWORKS)) { for (const network of Object.values(NETWORKS)) {