fix: a shared ticker no longer hides one of its two real tokens (closes #276)
All checks were successful
check / check (push) Successful in 33s
All checks were successful
check / check (push) Successful in 33s
Seven bundled tokens were filtered as spoofs at their own address, so a user holding FRAX, TON, REUSD, EURE, MSUSD, MUSD or JPYC could not see or spend the one the wallet happened not to pick. The known-symbol table is derived from the bundled token list, first-wins in market-cap order, so a symbol that appears twice silently condemned its second contract. Both are real tokens from the same fetch and neither is stale -- three pairs are one issuer's old and new contract, four are unrelated issuers sharing a ticker. Picking a winner would have been guessing, and dropping the ambiguous symbols would have ended spoof filtering for those tickers entirely. The table now maps a symbol to the set of addresses that legitimately bear it. A contract outside the set is still a spoof, so the check is not weakened: a third contract bearing any of the seven shared tickers is refused, and that is tested. The filter decides what is fake, not what is worth holding, so a legacy contract stays in the set -- it still holds real balances. A test walks the whole bundled list asserting no token is filtered at its own address, which is the guard whose absence let this ship.
This commit was merged in pull request #277.
This commit is contained in:
@@ -8,12 +8,19 @@
|
||||
// either verdict alone, because the balance list is where the user forms
|
||||
// their belief about what they own (issue #235).
|
||||
//
|
||||
// KNOWN_SYMBOLS maps a symbol to the lowercased contract address that may
|
||||
// bear it, or to null. Null means the symbol belongs to the native asset,
|
||||
// which has no contract at all, so no contract may bear it and every one
|
||||
// that does is a spoof. "ETH" is the only such entry today; the rule is
|
||||
// KNOWN_SYMBOLS maps a symbol to the set of lowercased contract addresses
|
||||
// that may bear it, or to null. Null means the symbol belongs to the native
|
||||
// asset, which has no contract at all, so no contract may bear it and every
|
||||
// one that does is a spoof. "ETH" is the only such entry today; the rule is
|
||||
// written so that a second one needs no change here or at any call site.
|
||||
//
|
||||
// The value is a set because a ticker is not unique: seven symbols in the
|
||||
// bundled list belong to two real contracts each, and answering with one of
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// question is whether the symbol reaches the user's eye as a known one,
|
||||
@@ -93,7 +100,7 @@ function isSpoofedSymbol(symbol, contractAddress) {
|
||||
if (!KNOWN_SYMBOLS.has(sym)) return false;
|
||||
const legit = KNOWN_SYMBOLS.get(sym);
|
||||
if (legit === null) return true;
|
||||
return contract !== normalizeAddress(legit);
|
||||
return !legit.has(contract);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user