fix: the bundled token list names tokens on Ethereum Mainnet only (closes #632)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

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
This commit is contained in:
clawbot
2026-10-27 20:14:25 +00:00
committed by sneak
parent 8998d6c661
commit a72f301fe1
21 changed files with 681 additions and 117 deletions
+39 -39
View File
@@ -1188,11 +1188,12 @@ The wallet takes the `decimals` from these sources, in this order
"Use Blockscout" is on (`tokenSourcesByChainId()` in
`src/popup/views/helpers.js`).
The token's name comes from the same sources. While "Use Blockscout" is off, a
token gets a line under it when the bundled list and the tracked tokens lack its
name or decimals. The line says "Blockscout's report of your balances is not
used to name this token, because "Use Blockscout" is off in Settings." These
screens show the line:
The token's name comes from the same sources. The bundled token list is used
only for a token on Ethereum Mainnet. While "Use Blockscout" is off, a token
gets a line under it when the bundled list and the tracked tokens lack its name
or decimals. The line says "Blockscout's report of your balances is not used to
name this token, because "Use Blockscout" is off in Settings." These screens
show the line:
- TxApproval and SignApproval
- WaitTx, SuccessTx and ErrorTx, which follow TxApproval
@@ -1473,20 +1474,20 @@ 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. A token claiming a symbol from the bundled list from any other
contract address is always dropped, tracked or not, and so is any token claiming
a symbol that belongs to the native asset and therefore has no legitimate
contract at all (`"ETH"`, and every network's `nativeCurrency`, such as
`"SepoliaETH"`, on every network). 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
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
@@ -3198,26 +3199,25 @@ 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. 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. 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.
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.
- **Low-holder token filtering**: Token transfers from ERC-20 contracts with
fewer than 1,000 holders are hidden from transaction history by default.