diff --git a/README.md b/README.md index 7f5ea3b..b8d9f32 100644 --- a/README.md +++ b/README.md @@ -3193,13 +3193,15 @@ Currently supported: `Token In` after an amount that is not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line, or no stated 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, 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; + know. When the step that sets the input side states no amount, the user pays + it some of its input token, and there is no `Amount` line, that step gets them + too whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM` + of its token gives one, on the lines below or, into the router, on its + `Paid to` line, so that figure cannot read as all that leaves. Paid from the + router, that step takes nothing from the user's wallet and never gets them. + 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 does not know gives the `Moved from the sender` line alone. A step may also diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index cc1bd83..1f8e32e 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -1204,6 +1204,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) { let inputToken = null; let inputAmount = null; let inputEstablished = false; + // Whether the user pays the V4 step that set the input side any of + // its input currency (see decodeV4Swap()). Only a V4 step can set the + // input side with no amount, the one case in which this is read. + let inputPaidByUser = false; // The last PERMIT2_PERMIT this decoder read, or null (see // permitStands after the loop). let permit = null; @@ -1756,6 +1760,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // this decoder could not read may take anything: the // router still runs it. const setInputHere = inputEstablished && !inputSetBefore; + if (setInputHere) inputPaidByUser = paidIn !== undefined; if (v4) { for (const p of v4.paidByUser) { if (setInputHere && p === paidIn) continue; @@ -1844,12 +1849,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // (see spentUnread()), or a token it does not know. const spentFromSender = []; let total = null; - // The take of the step that set the input side, when the total - // starts from it and it stated no amount (see below), or null. + // The take of the step that set the input side, when the user pays + // that step, the total starts from it and it stated no amount (see + // below), or null. Paid from the router, the step takes nothing from + // the sender. let firstTakeUnstated = null; if (permit === null || sameAddress(inputToken, permit.token)) { total = inputAmount; - if (inputEstablished && !present(inputAmount)) { + if (inputEstablished && inputPaidByUser && !present(inputAmount)) { firstTakeUnstated = { token: inputToken, amount: null }; } } else if (inputEstablished) { @@ -1874,15 +1881,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) { } inputAmount = permitStands ? permit.amount : total; // 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. + // PERMIT2_TRANSFER_FROM of the same token, into the router or not, + // could read as all that leaves, so the step that set the input side, + // when the user pays it and 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)) || - movedFromSender.some((m) => + [...movedFromSender, ...movedIntoRouter].some((m) => sameAddress(m.token, firstTakeUnstated.token), )) ) { diff --git a/tests/uniswap.test.js b/tests/uniswap.test.js index f896178..e98617a 100644 --- a/tests/uniswap.test.js +++ b/tests/uniswap.test.js @@ -2318,6 +2318,24 @@ describe("uniswap decoder", () => { encodeV4TakeAll(WETH_ADDR, 0n), ], ); + // The same step with its SETTLE of OPEN_DELTA paid from the router. + const V4_EXACT_OUT_600_FROM_ROUTER = encodeV4Swap( + new Uint8Array([ + V4_SWAP_EXACT_OUT_SINGLE_ID, + V4_SETTLE_ID, + V4_TAKE_ALL_ID, + ]), + [ + encodeV4ExactOutSingle( + USDT_ADDR, + WETH_ADDR, + ONE_WETH, + USDT_600, + ), + settle(USDT_ADDR, 0n, false), + encodeV4TakeAll(WETH_ADDR, 0n), + ], + ); test.each([ { kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user", @@ -2325,6 +2343,13 @@ describe("uniswap decoder", () => { first: V4_EXACT_OUT_600, lines: [...UNKNOWN_USDT, ...UP_TO_400_USDT], }, + // Paid from the router, the step takes nothing from the wallet. + { + kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA from the router", + commands: [0x10, 0x09], + first: V4_EXACT_OUT_600_FROM_ROUTER, + lines: UP_TO_400_USDT, + }, // Settled by the user, the step's amount would be what that // SETTLE pays, which the decoder cannot state here. { @@ -2427,11 +2452,12 @@ describe("uniswap decoder", () => { // 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) => + // that leaves. On next only the 400 USDT was shown. Into the router, + // the 400 USDT is on its Paid to line. + const transfer = (token, recipient, amount) => coder.encode( ["address", "address", "uint160"], - [token, OTHER_ADDR, amount], + [token, recipient, amount], ); const MOVED_400_USDT = [ ["Moved from the sender", "400.0000 USDT"], @@ -2442,29 +2468,70 @@ describe("uniswap decoder", () => { { 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)], + inputs: [ + V4_EXACT_OUT_600, + transfer(USDT_ADDR, OTHER_ADDR, USDT_400), + ], + lines: [...UNKNOWN_USDT, ...MOVED_400_USDT], }, { 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], + inputs: [ + transfer(USDT_ADDR, OTHER_ADDR, USDT_400), + V4_EXACT_OUT_600, + ], + lines: [...UNKNOWN_USDT, ...MOVED_400_USDT], + }, + { + kind: "a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 USDT into the router", + commands: [0x10, 0x02], + inputs: [ + V4_EXACT_OUT_600, + transfer(USDT_ADDR, ROUTER_ADDR, USDT_400), + ], + lines: UNKNOWN_USDT, + }, + { + kind: "a PERMIT2_TRANSFER_FROM of 400 USDT into the router, then a V4 exact-out step of at most 600 USDT", + commands: [0x02, 0x10], + inputs: [ + transfer(USDT_ADDR, ROUTER_ADDR, USDT_400), + V4_EXACT_OUT_600, + ], + lines: UNKNOWN_USDT, }, ])( "$kind, paid by the user, shows that the step's amount is not known", - ({ commands, inputs }) => { + ({ commands, inputs, lines }) => { const result = decodeSteps(commands, inputs); - expect(linesBetweenTokens(result)).toEqual([ - ...UNKNOWN_USDT, - ...MOVED_400_USDT, - ]); + expect(linesBetweenTokens(result)).toEqual(lines); }, ); + // Paid from the router, the V4 exact-out step takes nothing from the + // wallet, so the moves give all that leaves. + test("a PERMIT2_TRANSFER_FROM of 400 USDT into the router, one of 10 USDT to another address, then a V4 exact-out step paid from the router, shows only the moves", () => { + const result = decodeSteps( + [0x02, 0x02, 0x10], + [ + transfer(USDT_ADDR, ROUTER_ADDR, USDT_400), + transfer(USDT_ADDR, OTHER_ADDR, 10000000n), + V4_EXACT_OUT_600_FROM_ROUTER, + ], + ); + expect(linesBetweenTokens(result)).toEqual([ + ["Moved from the sender", "10.0000 USDT"], + ["Token moved", "USDT (" + USDT_ADDR + ")"], + ["Moved to", OTHER_ADDR], + ]); + }); + // 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)], + [V4_EXACT_OUT_600, transfer(DAI_ADDR, OTHER_ADDR, DAI_400)], ); expect(linesBetweenTokens(result)).toEqual([ ["Moved from the sender", "400.0000 DAI"],