fix: show the true token amount on the approval screen, or none at all (closes #306) #321
Reference in New Issue
Block a user
Delete Branch "issue-306-approval-decimals"
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 #306.
What changed
decodeCalldatainsrc/popup/views/approval.jsresolved an ERC-20 amount's scale from the 512-entry bundled token list alone and fell back to 18 decimals for everything else, so atransferof5000000000units of a 6-decimal token — 5,000 tokens — was drawn asAmount 0.0000.src/shared/approvalAmount.js.resolveTokenDecimals()consults the bundled list, thenstate.trackedTokens, then the decimals the block explorer already reported inaddr.tokenBalances, accepting only a real uint8 from any of them, and returningnullwhen no source knows. Explorer entries for one contract that disagree with each other also givenull: a disputed scale is not a known one, and this screen has no way to pick the true member.unknownDecimalsAmount()is the refusal rendering. When the scale isnullnoformatUnitscall is reached at all, and the line reads5000000000 base units (decimals unknown).approveis covered alongsidetransfer. An unbounded allowance still readsUnlimited, which needs no scale to describe. The same string goes intopendingTxDetails.amount, so no formatted figure reappears on the status screens.decodeCalldatais also whatsrc/popup/views/transactionDetail.jsrenders history entries with, so that screen gets the same correction; no other caller exists.On reusing
src/shared/transferAmount.jsIt does not fit, and the parts of it that are exported are not the parts this needs. It converts a decimal string the user typed into base units at signing time and refuses when the contract's
decimals()disagrees with the scale the screen displayed; this converts base units the page supplied into something displayable, before anything is signed and with no second scale to compare against. Its two entry points both go the wrong direction, and its messages are transaction-error copy ("The transfer was not sent"), which is not what an amount line says.The one piece that is genuinely shared is the uint8 bound, so
MAX_DECIMALSis imported from it rather than restated. ItstoDecimals()validator is private and the module is another unit's freshly reviewed work, so it was not modified to export it; the equivalent validator here is written out, accepting the same three types for the same reason (Number([])is0, so a coercing check would read an empty array as a scale of zero). If a reviewer would rather have one shared validator, exportingtoDecimalsfromtransferAmount.jsand deleting this copy is a small follow-up — it is deliberately not done in this PR.Verification
make checkgreen on the branch at0710de9, rebased onnextat6350aad:Lint ran in the pinned container, not on the host, and executed rather than hitting cache —
#11 [lint 1/1] RUN make lint/DONE 4.7s, witheslint . && prettier --check .andAll matched files use Prettier code style!in its output. No container survives the run;docker ps -ais clean and no image tag was produced (--output=type=cacheonly).Mutation used to prove the new tests are not vacuous: the fix's lookup was replaced with the original
const tokenDecimals = token ? token.decimals : 18;, nothing else touched. 6 of the 15 new tests fail, and the rest of the suite stays green (782 passed), so no existing test was relying on the 18 default. The unknown-decimals transfer case reported:and the tracked 6-decimal transfer case reported
Expected: "5000.0000"/Received: "0.0000"— the issue's screen output, reproduced by the mutation. The two tests that still pass under it are regression guards (Unlimited, and a bundled token keeping its scale), which is what they are for.New tests in
tests/approvalAmount.test.js: a 6-decimal non-bundled token renders5000.0000fromtrackedTokensand again from the explorer'stokenBalances(where decimals arrive as the string"6"); an unknown-decimals token renders the raw units and the warning for bothtransferandapprove, asserted also to not match0.0000; plus the precedence order, the disagreeing-explorer refusal, and the non-uint8 rejections.Not in scope
The
Tokenline still readsUnknown tokenfor anything outside the bundled list even whentrackedTokensor the explorer has the symbol — symbol resolution is not part of this issue's definition of done and is untouched.Independent review of
0710de9: PASS. Meets every box of #306's definition of done; CI green on head, fast-forward onnextat6350aad, tree clean, 4 files, commit authoredclawbotand ending(closes #306), no attribution trailers.Probes run (all in a throwaway clone, all mutations reverted,
git statusclean at0710de9afterwards):transferandapprove— andrawValue, which is whatpendingTxDetails.amountcarries to the status screens — all end in... base units (decimals unknown)with no digit-dot-digit anywhere, for amounts0,1and5000000000.Unlimitedstill renders for an unbounded allowance of an unknown-scale token.resolveTokenDecimals()held under:decimalsof0fromtrackedTokensand from the explorer (returns0, formats, does not refuse);255accepted,256/-1/1.5/255.5/"1e1"/" 6"/"6 "/"-1"/"256"/256n/-1n/undefined/Infinity/[]/{}/trueall rejected;6naccepted; case-mismatched addresses match in every source; agreeing explorer entries across two addresses are not read as a dispute; an unusable entry beside a good one does not manufacture one; disagreement refuses in either entry order. A hostile explorer cannot invert precedence — it loses to both the bundled list andtrackedTokens, including when it disagrees. Malformedwallets/trackedTokensshapes do not throw.const tokenDecimals = token ? token.decimals : 18;reproduces exactly 6 failed / 782 passed with the unknown-decimals case receiving"0.0000". Confirmed as stated.18instead ofnullon the unknown path also fails 6 tests, a different set (resolver-level and render-level both fire). The suite catches both ways this breaks.make checkre-run here: 36 suites / 788 tests,test-verify-build18 cases,check-censored144 files, and lint genuinely executed in the container —#11 [lint 1/1] RUN make lint/DONE 4.8s,eslint . && prettier --check .,All matched files use Prettier code style!. No container or image left behind.One gap, not blocking: the committed suite has no
decimals: 0case. Changing eitherif (d !== null) return d;inresolveTokenDecimals()to a truthyif (d)— the same falsy-zero trap already filed as #318 — leaves all 788 tests green while a 0-decimal token silently drops to the refusal rendering. The code is correct today; only the guard against regressing it is missing. Worth one assertion in a follow-up.On the open question about
toDecimals: export it fromsrc/shared/transferAmount.jsand delete the copy, as a follow-up, not here. The two bodies are byte-identical, and it is a pure uint8 parser — it carries no refusal semantics, so sharing it does not couple the two screens' behaviour when a scale is unknown, which does legitimately differ and should stay in each module.MAX_DECIMALSalready crosses that boundary, so the dependency exists either way; what the duplicate adds is only the chance of one copy drifting (dropping thebigintbranch, say) and quietly changing which scales one of the two screens accepts. Not touching another unit's in-flight module in this PR was the right call.