fix: filter a fake ETH token from the balance list too (closes #235) #257
Reference in New Issue
Block a user
Delete Branch "fix/issue-235-fake-eth-balance-list"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #235.
The disagreement
KNOWN_SYMBOLSmaps"ETH"tonull. The transaction history and the Sendtoken selector read that as "no contract may bear this symbol" and filtered a
fake
ETHERC-20. The balance list's guard waslegitAddr !== undefined && legitAddr !== null && tokenAddr !== legitAddr, so anullmapping failed the guard and the same token was listed as a holding namedETH, next to the user's real ETH.
Verdict: oversight, not a deliberate native-token exemption. The commit that
added the balance-list check,
5af8a78, says in its own message: "Also rejectstokens spoofing a known symbol from a different contract address (same check
used for transaction filtering)". The check it names had shipped the opposite
reading a day earlier in
b5b4f75(
if (legit === null) return true; // "ETH" as ERC-20 is always fake). Theexemption is also uncommented, in a file that comments its holder-count
reasoning at length, and it protects nothing: the native ETH balance never
enters that loop.
The fix
The rule moves to
src/shared/symbolSpoof.jsand all three sites call it, theway #230 extracted
src/shared/holders.js. A fourth reading is no longer available to a futurecall site.
nullbelongs to the native asset, so no contract maybear it and every contract bearing it is a spoof.
Correct by construction for a future null-mapped symbol: the native exemption is
"has no contract address", not "the symbol is ETH", so a second
nullentryinherits both halves of the rule with no call-site change. A test adds one and
asserts it.
src/shared/transactions.js,src/popup/views/send.jsandsrc/shared/balances.jslose their local copies; behaviour on the first two isunchanged, which the existing suites confirm.
The real ETH balance is unaffected
It is not an ERC-20.
refreshBalancesreads it over RPC withprovider.getBalance();fetchTokenBalancesiterates explorer rows and dropsanything that is not
item.token.type === "ERC-20", so the rule never sees it.tests/symbolSpoof.test.jsdrivesrefreshBalancesend to end with a stubbedprovider against a balance list whose only row is a fake ETH: the native balance
comes back exactly as the node reported it and the fake token is gone.
Tests
tests/symbolSpoof.test.jsruns the same fakeETHtoken — the contract fromthe attack documented in the README, given 900,000 holders so no other filter can
catch it — through all three surfaces, plus unit coverage of the rule itself.
Demonstrated failing first: with the module added but the three call sites
untouched, exactly the two balance-list cases failed while the history and
selector cases passed — the disagreement itself, reproduced:
Docs
The two
README.mdpassages describing the balance list'snullexemption nowstate that all three surfaces apply the check identically and name
src/shared/symbolSpoof.jsas the single source; the Data Model paragraph onfetchTokenBalances()gains the null-mapped case; the source tree gains the newmodule.
TODO.mdupdated in the same commit.Verification
make checkgreen, run in the container viascript/cibuildafter the rebaseonto
nextatbd4bdca:An uncached
docker build --no-cacherun before the rebase showed the samelayer executing rather than replaying cache.
Review: PASS
Independent review of
88a5c92against #235: definition of done met, archaeology verified (b5b4f752026-02-26 precedes5af8a782026-02-27 and contains the quotedlegit === nullline), all six new guards mutation-tested and each kills tests, no local copy of the rule remains, fast-forwardable ontoorigin/nextatbd4bdca, one commit, correct author/committer, TODO.md one bullet at top with no landed entry lost, README passages match behaviour, no forbidden references.Money check, done independently rather than taken on the author's word: with a balance list holding a fake
ETH, real WETH (both checksummed and lowercase forms) and USDC, exactly the fakeETHdisappears;addr.balancefromprovider.getBalance()is unchanged. AWETH-symbol token from a foreign contract is still caught. No input toisSpoofedSymbolwas found that hides real ETH or a legitimate token.Anomalies and disclosures:
a fake ETH token clearing the holder floor is filtered,tracking the fake token manually does not admit it either, andthe real native ETH balance survives a fake ETH airdrop(the last on itstokenBalancesassertion only; the native balance itself is correct in both directions). History and Send-selector cases pass alongside, which is the disagreement. Substance unaffected; the count in the PR body is wrong." ETH ") missesKNOWN_SYMBOLSon all three surfaces and is not a spoof by this rule, while HTML collapses the whitespace on display. Unchanged by this PR, in place before it; worth its own issue.script/cibuildexecutedmake checkin the container (layerDONE 17.2s, notCACHED) reporting 20 suites / 434 tests and Prettier clean, matching the PR body.88a5c92547tob75bd197b2clawbot referenced this pull request2026-08-12 11:49:19 +02:00