diff --git a/README.md b/README.md index 58eb59a..cf9638f 100644 --- a/README.md +++ b/README.md @@ -3564,10 +3564,14 @@ Currently supported: on every router but `UniversalRouterV1`, and otherwise gives the address. A move into the router needs none of them, because its `Paid to` line and `Token moved into the router` line already give the amount, the token and - where it goes, and so its amount is shown once. Every `PERMIT2_PERMIT` whose - amount does not stand as the `Amount` figure, before the step that sets the - input side or after it, gives three lines, in step order, after those for each - `PERMIT2_TRANSFER_FROM`: `Permit2 allowance`, its amount, for example + where it goes, and so its amount is shown once. For a move of the zero + address, `Moved from the sender` gives the amount in base units and + `Token moved` gives the zero address. For such a move into the router, the + `Paid to` line gives the amount in base units and + `Token moved into the router` gives the zero address. Every `PERMIT2_PERMIT` + whose amount does not stand as the `Amount` figure, before the step that sets + the input side or after it, gives three lines, in step order, after those for + each `PERMIT2_TRANSFER_FROM`: `Permit2 allowance`, its amount, for example `5000.0000 DAI`, or `Unlimited` for Permit2's maximum; `Allowance token`, giving the token's address as the `Token In` line does; and `Allowance spender`, the address it lets take the token. Only a permit whose diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index 68b8d1e..5e98da6 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -479,6 +479,17 @@ function tokenInfo(address, sources) { }; } +// The token of a Permit2 allowance or of a PERMIT2_TRANSFER_FROM, in the +// shape tokenInfo() gives. Permit2 has no allowance of ETH and moves no ETH. +// The zero address there reads as a token nothing knows: its amount in base +// units, and the zero address as its token. +function permit2TokenInfo(address, sources) { + if (address === "0x0000000000000000000000000000000000000000") { + return { symbol: null, decimals: null, address }; + } + return tokenInfo(address, sources); +} + // A swap amount line. With a scale it is the token quantity; with none it is // the base-unit integer with the unknown scale stated, the same refusal the // ERC-20 amount line makes, so the screen has one way of saying it. `display` @@ -497,16 +508,18 @@ function amountText(raw, info) { // The line giving a moved token, or the token of a permit, under `label`, in // the shape of the Token In line, so the screens show it as they show that -// one. ETH has no address. -function tokenMovedLine(label, token, sources) { - const info = tokenInfo(token, sources); +// one. `info` is what tokenInfo() or permit2TokenInfo() gives for the token. +// ETH has no address. +function tokenMovedLine(label, info) { if (!present(info.address)) { return { label, value: info.symbol }; } return { label, - value: info.symbol ? info.symbol + " (" + token + ")" : token, - address: token, + value: info.symbol + ? info.symbol + " (" + info.address + ")" + : info.address, + address: info.address, isToken: true, }; } @@ -2329,7 +2342,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) { : UNDECODED_CURRENCY, }); if (present(s.token)) { - details.push(tokenMovedLine("Token moved", s.token, sources)); + details.push( + tokenMovedLine("Token moved", tokenInfo(s.token, sources)), + ); details.push({ label: "Moved to", value: "The swap step that spends it", @@ -2340,12 +2355,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // What each PERMIT2_TRANSFER_FROM moves: its amount, its token and // where it goes, an address on a line of its own. for (const m of movedFromSender) { + const info = permit2TokenInfo(m.token, sources); details.push({ label: "Moved from the sender", - value: amountText(m.amount, tokenInfo(m.token, sources)) - .display, + value: amountText(m.amount, info).display, }); - details.push(tokenMovedLine("Token moved", m.token, sources)); + details.push(tokenMovedLine("Token moved", info)); details.push( present(m.to) ? { label: "Moved to", value: m.to } @@ -2375,36 +2390,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) { }); continue; } - // Permit2 has no allowance of ETH. An allowance of the zero - // address reads as one of a token nothing knows: its amount in - // base units, and the zero address as its token. - if (p.token === "0x0000000000000000000000000000000000000000") { - details.push({ - label: "Permit2 allowance", - value: - p.amount >= MAX_UINT160 - ? "Unlimited" - : unknownDecimalsAmount(p.amount), - }); - details.push({ - label: "Allowance token", - value: p.token, - address: p.token, - isToken: true, - }); - } else { - details.push({ - label: "Permit2 allowance", - value: - p.amount >= MAX_UINT160 - ? "Unlimited" - : amountText(p.amount, tokenInfo(p.token, sources)) - .display, - }); - details.push( - tokenMovedLine("Allowance token", p.token, sources), - ); - } + const info = permit2TokenInfo(p.token, sources); + details.push({ + label: "Permit2 allowance", + value: + p.amount >= MAX_UINT160 + ? "Unlimited" + : amountText(p.amount, info).display, + }); + details.push(tokenMovedLine("Allowance token", info)); details.push({ label: "Allowance spender", value: p.spender, @@ -2462,7 +2456,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) { paidTo.push({ label: "Paid to", value: PAID_TO_BRIDGE }); } for (const m of movedIntoRouter) { - const info = tokenInfo(m.token, sources); + const info = permit2TokenInfo(m.token, sources); paidTo.push({ label: "Paid to", value: @@ -2470,9 +2464,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) { amountText(m.amount, info).display + " from the sender (later steps may not spend all of it)", }); - paidTo.push( - tokenMovedLine("Token moved into the router", m.token, sources), - ); + paidTo.push(tokenMovedLine("Token moved into the router", info)); } if (outputHeld) { paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER }); @@ -2489,8 +2481,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) { paidTo.push( tokenMovedLine( "Token moved into the router", - getAddress(token), - sources, + tokenInfo(getAddress(token), sources), ), ); } diff --git a/tests/uniswap.test.js b/tests/uniswap.test.js index d6e365b..5a0d0d0 100644 --- a/tests/uniswap.test.js +++ b/tests/uniswap.test.js @@ -3440,6 +3440,82 @@ describe("uniswap decoder", () => { }); }); + // https://git.eeqj.de/sneak/AutistMask/issues/685 — Permit2 moves no + // ETH, so a PERMIT2_TRANSFER_FROM of the zero address reads as a move + // of a token nothing knows. On next at ec6b6b5 its lines read ETH, + // with an ETH figure. + describe("a PERMIT2_TRANSFER_FROM of the zero address", () => { + const MOVED_AMOUNT = + "1000000000000000000 base units (decimals unknown)"; + + test("to another address names the address", () => { + const result = decodeSteps( + [0x00, 0x02], + [V3_600, transfer(ZERO_ADDR, OTHER_ADDR, ONE_WETH)], + ); + expect(linesBetweenTokens(result)).toEqual([ + ["Amount", "600.0000 USDT"], + ["Moved from the sender", MOVED_AMOUNT], + ["Token moved", ZERO_ADDR], + ["Moved to", OTHER_ADDR], + ]); + expect(detail(result, "Token moved").address).toBe(ZERO_ADDR); + }); + + test("into the router names the address", () => { + const result = decodeSteps( + [0x02, 0x00], + [transfer(ZERO_ADDR, ROUTER_ADDR, ONE_WETH), V3_600], + ); + expect(detail(result, "Paid to").value).toBe( + "The router, " + + MOVED_AMOUNT + + " from the sender (later steps may not spend all of it)", + ); + expect(detail(result, "Token moved into the router")).toEqual({ + label: "Token moved into the router", + value: ZERO_ADDR, + address: ZERO_ADDR, + isToken: true, + }); + }); + + // A V4 exact-out step of ETH whose SETTLE the user pays states no + // amount. The step's lines say that the ETH amount is not known. + test("after a V4 exact-out step of ETH, also shows that the ETH amount is not known", () => { + const v4EthExactOut = encodeV4Swap( + new Uint8Array([ + V4_SWAP_EXACT_OUT_SINGLE_ID, + V4_SETTLE_ID, + V4_TAKE_ALL_ID, + ]), + [ + encodeV4ExactOutSingle( + ZERO_ADDR, + USDC_ADDR, + 590000000n, + ONE_WETH, + ), + encodeV4Settle(ZERO_ADDR), + encodeV4TakeAll(USDC_ADDR, 0n), + ], + ); + const result = decodeSteps( + [0x10, 0x02], + [v4EthExactOut, transfer(ZERO_ADDR, OTHER_ADDR, ONE_WETH)], + ); + expect(detail(result, "Token In").value).toBe("ETH (native)"); + expect(linesBetweenTokens(result)).toEqual([ + ["Moved from the sender", UNDECODED], + ["Token moved", "ETH"], + ["Moved to", "The swap step that spends it"], + ["Moved from the sender", MOVED_AMOUNT], + ["Token moved", ZERO_ADDR], + ["Moved to", OTHER_ADDR], + ]); + }); + }); + // https://git.eeqj.de/sneak/AutistMask/issues/682 — V4 pays a SETTLE // of ETH from the router's own ETH, whichever payer the SETTLE names, // and the router holds the transaction's value. On next at 36b2002 a