The stored ETH and token balances and the send-confirm screen's fee were each
cut to six decimal places, so a value below 0.000001 read as zero. Balances are
now stored exactly; a token declaring more than 18 decimals is cut to 18, the
most the balance check reads. A token holding below 0.000001 is still left off
the lists as dust, except for a token the user tracks, whose holding now
reaches the Send screen and is what the send is checked against. The send and
send-confirm screens' balances, reserve and insufficient-balance messages go
through truncateAmountNeverZero(). The send-confirm and approval screens both
render the fee through formatFee(), which prices the exact fee in USD. The
balance lists still round with toFixed(4).
Model: opus-5-5
parseInt(decimals || "18") ran before writing stored tokenBalances[].decimals, so an explorer reporting no decimals produced a fabricated 18 indistinguishable from a real one at read time. That defeated the resolve-or-refuse guarantees of #306 and #340: their refusal paths were intact but never fired, because the guess was laundered upstream of them.
An absent scale is now stored as unknown, and a holding whose scale nothing knows carries a null balance -- unknown, never zero -- with six reader sites saying so rather than printing 0.0000. The Send screen resolves the display scale rather than reading the stored one, so a bundled token whose explorer row omits decimals still sends; when the scale cannot be resolved the stored quantity is withdrawn too, so the user is told the balance is unknown rather than only that the fee failed.
Existing fabricated 18s cannot be told apart retroactively and are replaced wholesale on the next balance refresh. An explorer-sourced scale stays trusted -- only fabrication is removed; the reasoning is recorded on the issue.