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