All checks were successful
check / check (push) Successful in 32s
A token whose symbol is " ETH " missed KNOWN_SYMBOLS on all three surfaces while HTML collapsed the padding and painted it as ETH next to the user's real ETH. isSpoofedSymbol() now normalizes before the lookup: NFKC, every Unicode format character removed wherever it sits, then trimmed, then uppercased. All three surfaces inherit it unchanged. Covered: ASCII and Unicode whitespace padding, zero-width and other invisible format characters, and compatibility variants such as fullwidth letters. Knowingly left open, and asserted as open in the suite: confusables that are distinct letters (Cyrillic capital Ie), bidi reordering, and interior whitespace, which renders differently and so is not the confusion. No symbol in KNOWN_SYMBOLS or the bundled token list contains whitespace or a non-ASCII character, so nothing legitimate is newly filtered; a test walks the whole table and asserts it. The balance list's token-type gate is now case-insensitive. It compared exactly, so an explorer writing "erc-20" would silently drop a real holding before any filter ran. Which types are admitted is unchanged.
95 lines
4.7 KiB
JavaScript
95 lines
4.7 KiB
JavaScript
// The known-symbol spoof rule, in one place.
|
|
//
|
|
// A token that borrows a known symbol from a contract that is not the one
|
|
// that symbol belongs to is a spoof, and the wallet hides it. Three surfaces
|
|
// ask that question — the transaction history, the Send token selector and
|
|
// the balance list — and they must answer it identically: a token the history
|
|
// calls fake while the balance list lists it as a holding is worse than
|
|
// 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
|
|
// written so that a second one needs no change here or at any call site.
|
|
//
|
|
// 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,
|
|
// since that is what the user acts on.
|
|
|
|
const { KNOWN_SYMBOLS } = require("./tokenList");
|
|
|
|
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
|
|
// over the address, not part of its identity.
|
|
function normalizeAddress(addr) {
|
|
return (addr || "").toLowerCase();
|
|
}
|
|
|
|
// Fold a symbol onto what a user actually sees, and no further:
|
|
//
|
|
// NFKC collapses compatibility variants that render as the ASCII
|
|
// letters they imitate — fullwidth ETH, styled mathematical
|
|
// letters — and maps the non-ASCII spaces onto U+0020.
|
|
// strip drops \p{Cf} plus \p{Default_Ignorable_Code_Point}: the
|
|
// format characters (zero-width space, joiner and non-joiner,
|
|
// word joiner, soft hyphen, byte-order mark, bidi marks and
|
|
// overrides), the variation selectors, and the Hangul
|
|
// fillers. \p{Cf} alone is not the class of things that
|
|
// paint nothing — a Hangul filler is Lo and a variation
|
|
// selector is Mn, and both are as invisible as a zero-width
|
|
// space. Removed everywhere, not merely at the ends.
|
|
// trim removes surrounding whitespace, which HTML collapses:
|
|
// `" ETH "` is painted next to the user's real ETH as `ETH`.
|
|
// toUpperCase makes the comparison case-insensitive, as before.
|
|
//
|
|
// The class is Unicode's, so what it covers is a definition rather than a
|
|
// measurement; measured in the repo's pinned e2e Chromium (16px sans-serif,
|
|
// plain `ETH` = 32.00px), every stripped character paints nothing except
|
|
// U+1160 and U+FFA0, which font fallback draws as a box. Stripping those
|
|
// two hides a token that does not look like the symbol, which is the
|
|
// harmless direction of the two.
|
|
//
|
|
// Deliberately not folded, and asserted as open in tests/symbolSpoof.test.js:
|
|
// interior whitespace (`E T H` renders as `E T H`, so folding it would filter
|
|
// a token nobody could confuse with the native asset), confusables that are
|
|
// distinct letters rather than compatibility variants (Cyrillic capital Ie,
|
|
// U+0415; Greek capital Epsilon, U+0395), and bidi reordering, which needs
|
|
// the bidi algorithm rather than a character filter. The C0/C1 controls are
|
|
// left alone because they render as a visible box (48.00px) — except U+007F,
|
|
// which measures 32.00px, i.e. invisible and still not caught. That one is
|
|
// a live gap, flagged rather than closed here because it is a control
|
|
// character rather than a default-ignorable one and the class to strip is a
|
|
// decision of its own.
|
|
//
|
|
// This decides only how the question is asked. Nothing here changes what a
|
|
// surface displays; a token still shows the symbol it reports.
|
|
function normalizeSymbol(symbol) {
|
|
return String(symbol || "")
|
|
.normalize("NFKC")
|
|
.replace(/[\p{Cf}\p{Default_Ignorable_Code_Point}]/gu, "")
|
|
.trim()
|
|
.toUpperCase();
|
|
}
|
|
|
|
// True when a token bearing `symbol` from contract `contractAddress` is
|
|
// impersonating a known symbol.
|
|
//
|
|
// 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) {
|
|
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;
|
|
return contract !== normalizeAddress(legit);
|
|
}
|
|
|
|
module.exports = {
|
|
isSpoofedSymbol,
|
|
};
|