fix: name a tracked or explorer-known token instead of "Unknown token" (closes #323)
The approval and transaction-status screens read a token's scale from the bundled list, the tokens the user tracks, then the block explorer, but read its symbol from the bundled list alone. A token the user added by hand was scaled correctly yet labelled "Unknown token", and a non-bundled ERC-20 was carried onto the wait screen as ETH. resolveTokenSymbol() now draws the symbol through the same sources and precedence as the scale, and the ERC-20 and Uniswap swap lines both use it. A tracked or explorer-reported name stays subject to the spoof rule, so it cannot claim a bundled or native ticker. Folds in #354. Model: opus-4-8 Co-authored-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #394.
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
// enumerated rather than coerced.
|
||||
const { toDecimals } = require("./transferAmount");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||
|
||||
// Every decimals the explorer reported for this contract, across all the
|
||||
// addresses whose balances have been fetched. They describe one contract, so
|
||||
@@ -74,6 +75,59 @@ function resolveTokenDecimals(tokenAddress, sources) {
|
||||
return explorerDecimals(lower, sources && sources.wallets);
|
||||
}
|
||||
|
||||
// Every symbol the explorer reported for this contract, across the addresses
|
||||
// whose balances have been fetched. The counterpart to explorerDecimals(): one
|
||||
// contract, so the reports should agree, and a set that does not agree is a
|
||||
// name this screen has no way to choose between.
|
||||
function explorerSymbol(lower, wallets) {
|
||||
let found = null;
|
||||
for (const wallet of wallets || []) {
|
||||
for (const addr of wallet.addresses || []) {
|
||||
for (const tb of addr.tokenBalances || []) {
|
||||
if ((tb.address || "").toLowerCase() !== lower) continue;
|
||||
if (!tb.symbol) continue;
|
||||
if (found !== null && found !== tb.symbol) return null;
|
||||
found = tb.symbol;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// The symbol to label a token with, or null when no source the wallet trusts
|
||||
// names one — in which case the screen keeps saying `Unknown token` rather than
|
||||
// guessing. The bundled list, then the tokens the user tracks, then what the
|
||||
// explorer reported: the same sources and the same precedence
|
||||
// resolveTokenDecimals() uses, so a token's name and its scale are drawn from
|
||||
// the same place and the two can no longer disagree about which sources they
|
||||
// trust. `sources` is { trackedTokens, wallets }, shaped as on `state`.
|
||||
//
|
||||
// A tracked or explorer-reported symbol is attacker-influenced text, so it is
|
||||
// held to the spoof rule (symbolSpoof.js): a candidate that wears a bundled or
|
||||
// native ticker from a contract not entitled to it is refused and the next
|
||||
// source tried, so resolving a symbol never becomes a new way to claim a known
|
||||
// ticker. The bundled list is the wallet's own data and is trusted as it is.
|
||||
function resolveTokenSymbol(tokenAddress, sources) {
|
||||
const lower = (tokenAddress || "").toLowerCase();
|
||||
if (!lower) return null;
|
||||
|
||||
const bundled = TOKEN_BY_ADDRESS.get(lower);
|
||||
if (bundled && bundled.symbol) return bundled.symbol;
|
||||
|
||||
const tracked = ((sources && sources.trackedTokens) || []).find(
|
||||
(t) => (t.address || "").toLowerCase() === lower,
|
||||
);
|
||||
const candidates = [];
|
||||
if (tracked && tracked.symbol) candidates.push(tracked.symbol);
|
||||
const reported = explorerSymbol(lower, sources && sources.wallets);
|
||||
if (reported) candidates.push(reported);
|
||||
|
||||
for (const symbol of candidates) {
|
||||
if (!isSpoofedSymbol(symbol, tokenAddress)) return symbol;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// What the amount line reads when the scale is unknown. The base units are
|
||||
// exact and the caveat is part of the same string, so the number on the screen
|
||||
// cannot be mistaken for a token quantity, and it can never read as zero for a
|
||||
@@ -84,5 +138,6 @@ function unknownDecimalsAmount(rawAmount) {
|
||||
|
||||
module.exports = {
|
||||
resolveTokenDecimals,
|
||||
resolveTokenSymbol,
|
||||
unknownDecimalsAmount,
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// swap details. Designed to be extended with other DEX decoders later.
|
||||
|
||||
const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
||||
const {
|
||||
resolveTokenDecimals,
|
||||
resolveTokenSymbol,
|
||||
unknownDecimalsAmount,
|
||||
} = require("./approvalAmount");
|
||||
|
||||
@@ -123,9 +123,8 @@ function tokenInfo(address, sources) {
|
||||
if (address === "0x0000000000000000000000000000000000000000") {
|
||||
return { symbol: "ETH", decimals: 18, address: null };
|
||||
}
|
||||
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
||||
return {
|
||||
symbol: t ? t.symbol : null,
|
||||
symbol: resolveTokenSymbol(address, sources),
|
||||
decimals: resolveTokenDecimals(address, sources),
|
||||
address,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user