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.

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

Model: opus-5-5
This commit was merged in pull request #703.
This commit is contained in:
2026-11-03 03:41:15 +01:00
parent 430ea0166e
commit 754cf9686f
25 changed files with 732 additions and 115 deletions
+11 -3
View File
@@ -19,7 +19,8 @@
// them hid the other one's holders' money (issue #276). Membership, not
// equality, is therefore the question — but it is the same question, asked of
// a table that can now state the truth. Every address in a set is one the
// wallet ships as a real token; a contract outside the set is still a spoof.
// wallet ships as a real token on Ethereum Mainnet. On mainnet, a contract
// outside the set is still a spoof.
//
// The symbol is attacker-controlled — it is whatever the ERC-20 contract
// returns — so the lookup is done on a normalized form (issue #260): the
@@ -27,6 +28,7 @@
// since that is what the user acts on.
const { KNOWN_SYMBOLS } = require("./tokenList");
const { NETWORKS } = require("./networks");
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
// over the address, not part of its identity.
@@ -93,18 +95,24 @@ function normalizeSymbol(symbol) {
}
// True when a token bearing `symbol` from contract `contractAddress` is
// impersonating a known symbol.
// impersonating a known symbol. `chainId` is the chain id of the token's
// network.
//
// The native asset's labels are known symbols on every network. The bundled
// list's symbols are known symbols only on Ethereum Mainnet, because the list
// holds mainnet contracts.
//
// An empty contract address is the native asset, which is never a spoof:
// this is what keeps the user's real ETH out of the rule, and it holds for
// any symbol that becomes null-mapped later, not just for ETH.
function isSpoofedSymbol(symbol, contractAddress) {
function isSpoofedSymbol(symbol, contractAddress, chainId) {
const contract = normalizeAddress(contractAddress);
if (!contract) return false;
const sym = normalizeSymbol(symbol);
if (!KNOWN_SYMBOLS.has(sym)) return false;
const legit = KNOWN_SYMBOLS.get(sym);
if (legit === null) return true;
if (chainId !== NETWORKS.mainnet.chainId) return false;
return !legit.has(contract);
}