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