diff --git a/README.md b/README.md index dbba5db..7f5ea3b 100644 --- a/README.md +++ b/README.md @@ -3195,9 +3195,10 @@ Currently supported: amount, as of a V4 exact-out step); and one whose amount the decoder does not know. When the step that sets the input side states no amount and there is no `Amount` line, that step gets them too whenever another take's lines give a - figure, so that figure cannot read as all that leaves. They come in step - order, the step that sets the input side first when it has them: - `Moved from the sender`, its amount, worded as the `Amount` line words one, or + figure, or a `PERMIT2_TRANSFER_FROM` of its token gets the lines below, so + that figure cannot read as all that leaves. They come in step order, the step + that sets the input side first when it has them: `Moved from the sender`, its + amount, worded as the `Amount` line words one, or `Unknown (could not be decoded)` when the decoder does not know it; `Token moved`, giving the token's address as the `Token In` line does; and `Moved to`, which reads `The swap step that spends it`. A token the decoder diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index 6563f62..cc1bd83 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -1873,14 +1873,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) { if (ofTokenIn && !present(t.amount)) total = null; } inputAmount = permitStands ? permit.amount : total; - // With no Amount figure, a later take's own figure could read as all - // that leaves, so the step that set the input side, when it stated - // no amount, then gets lines of its own too, first, saying its amount + // With no Amount figure, a later take's own figure, or that of a + // PERMIT2_TRANSFER_FROM of the same token, could read as all that + // leaves, so the step that set the input side, when it stated no + // amount, then gets lines of its own too, first, saying its amount // is not known. if ( !present(inputAmount) && firstTakeUnstated !== null && - spentFromSender.some((s) => present(s.amount)) + (spentFromSender.some((s) => present(s.amount)) || + movedFromSender.some((m) => + sameAddress(m.token, firstTakeUnstated.token), + )) ) { spentFromSender.unshift(firstTakeUnstated); } diff --git a/tests/uniswap.test.js b/tests/uniswap.test.js index 1ae22e6..f896178 100644 --- a/tests/uniswap.test.js +++ b/tests/uniswap.test.js @@ -2423,6 +2423,56 @@ describe("uniswap decoder", () => { }, ); + // https://git.eeqj.de/sneak/AutistMask/issues/634 — a + // PERMIT2_TRANSFER_FROM of the token the V4 exact-out step spends, + // before or after it, shows its own figure, so the step's amount is + // shown as not known beside it, and the 400 USDT cannot read as all + // that leaves. On next only the 400 USDT was shown. + const transferTo = (token, amount) => + coder.encode( + ["address", "address", "uint160"], + [token, OTHER_ADDR, amount], + ); + const MOVED_400_USDT = [ + ["Moved from the sender", "400.0000 USDT"], + ["Token moved", "USDT (" + USDT_ADDR + ")"], + ["Moved to", OTHER_ADDR], + ]; + test.each([ + { + kind: "a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 USDT to another address", + commands: [0x10, 0x02], + inputs: [V4_EXACT_OUT_600, transferTo(USDT_ADDR, USDT_400)], + }, + { + kind: "a PERMIT2_TRANSFER_FROM of 400 USDT to another address, then a V4 exact-out step of at most 600 USDT", + commands: [0x02, 0x10], + inputs: [transferTo(USDT_ADDR, USDT_400), V4_EXACT_OUT_600], + }, + ])( + "$kind, paid by the user, shows that the step's amount is not known", + ({ commands, inputs }) => { + const result = decodeSteps(commands, inputs); + expect(linesBetweenTokens(result)).toEqual([ + ...UNKNOWN_USDT, + ...MOVED_400_USDT, + ]); + }, + ); + + // A move of another token says nothing of how much USDT leaves. + test("a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 DAI, shows only the DAI moved", () => { + const result = decodeSteps( + [0x10, 0x02], + [V4_EXACT_OUT_600, transferTo(DAI_ADDR, DAI_400)], + ); + expect(linesBetweenTokens(result)).toEqual([ + ["Moved from the sender", "400.0000 DAI"], + ["Token moved", "DAI (" + DAI_ADDR + ")"], + ["Moved to", OTHER_ADDR], + ]); + }); + // A V2 exact-out step makes every amount the swap steps spend a maximum, // the total included. test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {