fix: filter a fake ETH token from the balance list too (closes #235)
All checks were successful
check / check (push) Successful in 30s
All checks were successful
check / check (push) Successful in 30s
This commit was merged in pull request #257.
This commit is contained in:
@@ -12,8 +12,9 @@ const {
|
||||
const { state, currentAddress } = require("../../shared/state");
|
||||
let ctx;
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { KNOWN_SYMBOLS, resolveSymbol } = require("../../shared/tokenList");
|
||||
const { resolveSymbol } = require("../../shared/tokenList");
|
||||
const { isLowHolderCount } = require("../../shared/holders");
|
||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
||||
const { getAddress } = require("ethers");
|
||||
|
||||
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
||||
@@ -116,14 +117,6 @@ function updateToValidation() {
|
||||
}
|
||||
}
|
||||
|
||||
function isSpoofedToken(t) {
|
||||
const upper = (t.symbol || "").toUpperCase();
|
||||
if (!KNOWN_SYMBOLS.has(upper)) return false;
|
||||
const legit = KNOWN_SYMBOLS.get(upper);
|
||||
if (legit === null) return true;
|
||||
return t.address.toLowerCase() !== legit;
|
||||
}
|
||||
|
||||
function renderSendTokenSelect(addr) {
|
||||
const sel = $("send-token");
|
||||
sel.innerHTML = '<option value="ETH">ETH</option>';
|
||||
@@ -131,7 +124,7 @@ function renderSendTokenSelect(addr) {
|
||||
(state.fraudContracts || []).map((a) => a.toLowerCase()),
|
||||
);
|
||||
for (const t of addr.tokenBalances || []) {
|
||||
if (isSpoofedToken(t)) continue;
|
||||
if (isSpoofedSymbol(t.symbol, t.address)) continue;
|
||||
if (fraudSet.has(t.address.toLowerCase())) continue;
|
||||
// An unknown holder count does not withhold a token the user holds:
|
||||
// only a count the explorer actually reported as below the threshold
|
||||
|
||||
@@ -11,8 +11,9 @@ const {
|
||||
const { ERC20_ABI } = require("./constants");
|
||||
const { log, debugFetch } = require("./log");
|
||||
const { deriveAddressFromXpub } = require("./wallet");
|
||||
const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders");
|
||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||
|
||||
// Use a static network to skip auto-detection (which can fail and cause
|
||||
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
||||
@@ -89,15 +90,11 @@ async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
|
||||
// Skip spam tokens the user never asked to see
|
||||
if (!isKnown && !isTracked && !hasEnoughHolders) continue;
|
||||
|
||||
// Skip tokens spoofing a known symbol from a different address
|
||||
const sym = (item.token.symbol || "").toUpperCase();
|
||||
const legitAddr = KNOWN_SYMBOLS.get(sym);
|
||||
if (
|
||||
legitAddr !== undefined &&
|
||||
legitAddr !== null &&
|
||||
tokenAddr !== legitAddr
|
||||
)
|
||||
continue;
|
||||
// Skip tokens spoofing a known symbol from a different address.
|
||||
// Every row here is an ERC-20 the explorer reported, so it has a
|
||||
// contract address; the native ETH balance is fetched over RPC in
|
||||
// refreshBalances and never passes through this loop.
|
||||
if (isSpoofedSymbol(item.token.symbol, tokenAddr)) continue;
|
||||
|
||||
balances.push({
|
||||
address: item.token.address_hash,
|
||||
|
||||
43
src/shared/symbolSpoof.js
Normal file
43
src/shared/symbolSpoof.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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.
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// 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 = (symbol || "").toUpperCase();
|
||||
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,
|
||||
};
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
const { formatEther, formatUnits } = require("ethers");
|
||||
const { log, debugFetch } = require("./log");
|
||||
const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { parseHoldersCount, isLowHolderCount } = require("./holders");
|
||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||
|
||||
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
|
||||
// over the address, not part of its identity. Every address comparison in
|
||||
@@ -245,18 +246,6 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if a token transfer is spoofing a known symbol.
|
||||
// Returns true if the symbol matches a known token but the contract
|
||||
// address doesn't match the legitimate one.
|
||||
function isSpoofedSymbol(tx) {
|
||||
if (!tx.contractAddress) return false;
|
||||
const symbol = (tx.symbol || "").toUpperCase();
|
||||
if (!KNOWN_SYMBOLS.has(symbol)) return false;
|
||||
const legit = KNOWN_SYMBOLS.get(symbol);
|
||||
if (legit === null) return true; // "ETH" as ERC-20 is always fake
|
||||
return normalizeAddress(tx.contractAddress) !== normalizeAddress(legit);
|
||||
}
|
||||
|
||||
// Pure filter function. Takes raw transactions and filter settings,
|
||||
// returns { transactions, newFraudContracts }.
|
||||
function filterTransactions(txs, filters = {}) {
|
||||
@@ -283,7 +272,7 @@ function filterTransactions(txs, filters = {}) {
|
||||
const contract = normalizeAddress(tx.contractAddress);
|
||||
|
||||
// Filter spoofed known symbols and record the fraud contract
|
||||
if (hideSpoofed && isSpoofedSymbol(tx)) {
|
||||
if (hideSpoofed && isSpoofedSymbol(tx.symbol, tx.contractAddress)) {
|
||||
if (contract && !fraudSet.has(contract)) {
|
||||
fraudSet.add(contract);
|
||||
newFraud.push(contract);
|
||||
|
||||
Reference in New Issue
Block a user