// The floor of the approval screen's amount line. // // Amounts are truncated to four decimal places for scannability (README.md, // Display Consistency). With the token's true scale resolved, that truncation // can still take a real amount below the floor and print it as `0.0000`: one // base unit of an 18-decimal token, or a few hundred of an 8-decimal one. On // the one screen whose job is to state what is being authorized, a nonzero // transfer or allowance then reads as nothing. // // The invariant asserted here is narrow: a nonzero amount never renders as // zero. The four-decimal rule itself is unchanged, and the string the // confirmation screens carry as `txInfo.amount` is the same one, so it is // asserted on `rawValue` alongside the displayed line. globalThis.chrome = { storage: { local: { get: async () => ({}), set: async () => {} } }, }; const { Interface } = require("ethers"); const { ERC20_ABI } = require("../src/shared/constants"); const { state } = require("../src/shared/state"); const { decodeCalldata } = require("../src/popup/views/approval"); const iface = new Interface(ERC20_ABI); // Bundled tokens, so the scale and the symbol both come from the list. const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // 6 decimals const WBT = "0x925206b8a707096Ed26ae47C84747fE0bb734F59"; // 8 decimals const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; // 18 decimals // Outside the list, so the scale comes from what the user tracks and the line // carries no symbol. const NOVEL = "0xE2E0000000000000000000000000000000000E2e"; const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe"; const SPENDER = "0x1111111111111111111111111111111111111111"; function transferData(amount) { return iface.encodeFunctionData("transfer", [RECIPIENT, amount]); } function approveData(amount) { return iface.encodeFunctionData("approve", [SPENDER, amount]); } // The Amount detail as the approval screen renders it: `value` is the line on // the screen, `rawValue` is what is carried to the wait/success/error screens. function amount(data, token) { const decoded = decodeCalldata(data, token); return decoded.details.find((d) => d.label === "Amount"); } beforeEach(() => { state.trackedTokens = []; state.wallets = []; }); describe("a nonzero amount never renders as zero", () => { test("500 base units of a 6-decimal token", () => { const detail = amount(transferData(500n), USDC); expect(detail.value).toBe("0.0005 USDC"); expect(detail.rawValue).toBe("0.0005"); }); test("1 base unit of an 18-decimal token", () => { const detail = amount(transferData(1n), DAI); expect(detail.value).toBe("0.000000000000000001 DAI"); expect(detail.rawValue).toBe("0.000000000000000001"); }); test("500 base units of an 8-decimal token", () => { expect(amount(transferData(500n), WBT).rawValue).toBe("0.000005"); }); test("an allowance below the floor is not rendered as zero either", () => { expect(amount(approveData(1n), DAI).value).toBe( "0.000000000000000001 DAI", ); }); // The floor holds at any scale, not only the three above: for every // decimals a token can declare, one base unit has to show a digit. test("one base unit shows a significant digit at every scale", () => { for (let decimals = 0; decimals <= 30; decimals++) { state.trackedTokens = [{ address: NOVEL, decimals }]; expect(amount(transferData(1n), NOVEL).rawValue).toMatch(/[1-9]/); } }); }); describe("the four-decimal rule is otherwise unchanged", () => { test("a whole amount keeps exactly four decimals", () => { expect(amount(transferData(5000000000n), USDC).rawValue).toBe( "5000.0000", ); }); test("precision beyond four decimals is still truncated", () => { expect(amount(transferData(1234567890123456789n), DAI).rawValue).toBe( "1.2345", ); }); test("an amount at the floor is not extended", () => { expect(amount(transferData(100000000000000n), DAI).rawValue).toBe( "0.0001", ); }); test("a genuine zero still renders as zero", () => { expect(amount(transferData(0n), DAI).rawValue).toBe("0.0000"); }); });