From dcf52853d75ec88af8272abcd1b2733720a6af8e Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 3 Nov 2026 01:28:25 +0000 Subject: [PATCH] fix: the swap decoder names where a WRAP_ETH pays and shows a V4 step's ETH as a maximum when the router may hold ETH (closes #665) A `WRAP_ETH` that sets the input side and pays its WETH to anyone but the router names that recipient on its `Moved to` line. V4 pays a `SETTLE` of ETH from the router's own ETH, whichever payer it names. When an earlier step may leave ETH in the router, the ETH that the user pays a later V4 step gets lines of its own. Their `Moved from the sender` line reads `Up to `, or `Unknown (could not be decoded)` when the amount is not known. The decoder never subtracts the router's ETH from that amount. Without such an earlier step, the lines read as before. Model: opus-5-5 --- README.md | 101 +++++++------ src/shared/uniswap.js | 78 +++++++--- tests/uniswap.test.js | 335 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 447 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index de33fb1..444c4f2 100644 --- a/README.md +++ b/README.md @@ -1268,6 +1268,9 @@ see Transaction Decoding), do not always read as a number. They can read: wording. When a V2 or V3 exact-out step sets `Min. received`, that line shows its `amountOut`, the exact amount it buys. When earlier swap steps also pay that token to the user, the line shows the sum (see Transaction Decoding). + When the router may hold ETH from an earlier step, the `Moved from the sender` + line of ETH that the user pays a V4 step also reads `Up to ` (see + Transaction Decoding). - `All available (V4 open delta)`: the swap's `Amount` line, when the amount it shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the whole open delta", so the calldata states no quantity. Transaction Decoding @@ -3524,23 +3527,26 @@ Currently supported: of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of `OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the - rest taken to another address, reads `5000.0000 USDT`. A later step that pays - an ERC-20 token from the router takes none of that token from the user's - wallet. A V2 exact-in whose `amountIn` is `ALREADY_PAID` takes nothing either. - A V4 `SETTLE` of ETH paid from the router still takes from the user's wallet, - since the router holds the transaction's value. When the decoder does not know - the amount of such a `SETTLE`, as for `CONTRACT_BALANCE`, that `SETTLE` counts - as a later step's take. When an earlier step left ETH in the router, that ETH - may pay part of a `SETTLE` of ETH paid from the router. That `SETTLE` then - gets lines of its own, with `Up to` before its amount. A later take of - `Token In` that only the user's wallet pays is added to the total while both - are quantities: a V3 exact-in of 600 USDT and a V2 exact-in of 400 USDT, both - paid by the user, read `1000.0000 USDT`. When the decoder does not know a - take's amount, the total is not known and there is no `Amount` line. A - `PERMIT2_PERMIT` names `Token In` only when its amount, `Unlimited` included, - stands as the `Amount` figure in place of the total, which it does only in the - plain shape of Uniswap's own routes: it comes before the step that sets the - input side, it is the only `PERMIT2_PERMIT` in the transaction and there is no + rest taken to another address, reads `5000.0000 USDT`. V4 pays a `SETTLE` of + ETH from the router's own ETH, whichever payer it names. So when the router + may hold ETH from an earlier step (see above), the ETH that the user pays a V4 + step gets lines of its own (see below). A later step that pays an ERC-20 token + from the router takes none of that token from the user's wallet. A V2 exact-in + whose `amountIn` is `ALREADY_PAID` takes nothing either. A V4 `SETTLE` of ETH + paid from the router still takes from the user's wallet, since the router + holds the transaction's value. When the decoder does not know the amount of + such a `SETTLE`, as for `CONTRACT_BALANCE`, that `SETTLE` counts as a later + step's take. When an earlier step left ETH in the router, that ETH may pay + part of a `SETTLE` of ETH paid from the router. That `SETTLE` then gets lines + of its own, with `Up to` before its amount. A later take of `Token In` that + only the user's wallet pays is added to the total while both are quantities: a + V3 exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user, + read `1000.0000 USDT`. When the decoder does not know a take's amount, the + total is not known and there is no `Amount` line. A `PERMIT2_PERMIT` names + `Token In` only when its amount, `Unlimited` included, stands as the `Amount` + figure in place of the total, which it does only in the plain shape of + Uniswap's own routes: it comes before the step that sets the input side, it is + the only `PERMIT2_PERMIT` in the transaction and there is no `PERMIT2_PERMIT_BATCH`, its spender is the router, the router may not let it fail, no step before it takes its token, a `PERMIT2_TRANSFER_FROM` of it included, or a token the decoder does not know, and the decoder reads every @@ -3570,34 +3576,39 @@ Currently supported: 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); an ETH take whose line reads `Up to` (see above); and one whose amount the - decoder does not know. When there is no `Amount` line, each take the total - counts gets them too, so that every amount the decoder reads is shown: the - amount the step that sets the input side states, when it is a quantity and the - user pays that step (a V2 or V3 step whose `payerIsUser` is true, a V4 step - the user pays some of its input token, or a `WRAP_ETH`, whose ETH comes from - the transaction's value), and each later take of `Token In` added to it. A - step that sets the input side gets none when the user pays it none of its - input token. A `PERMIT2_PERMIT` the decoder cannot read, then V3 exact-ins of - 600 USDT and 400 DAI paid by the user, give lines for both amounts. 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. When it states no amount and the - user pays it none of its input token, that step never gets them, with a permit - or without. They come in step order, except that the step that sets the input - side comes first when it has them, followed, when there is no `Amount` line, - by the other takes the total counts: `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 take from the user's - wallet what the decoder did not read: a V4 step whose input or one of whose - `SETTLE`s or `SETTLE_ALL`s it cannot read, which the router still runs; a V2 - or V3 exact-in or exact-out step or a `PERMIT2_TRANSFER_FROM` whose input it - cannot read, such as a V3 step whose path is shorter than one token address or - has bytes after its last hop; every `PERMIT2_TRANSFER_FROM_BATCH`; every + decoder does not know. ETH that the user pays a V4 step when the router may + hold ETH gets these three lines too. When there is no `Amount` line, each take + the total counts gets them too, so that every amount the decoder reads is + shown: the amount the step that sets the input side states, when it is a + quantity and the user pays that step (a V2 or V3 step whose `payerIsUser` is + true, a V4 step the user pays some of its input token, or a `WRAP_ETH`, whose + ETH comes from the transaction's value), and each later take of `Token In` + added to it. A step that sets the input side gets none when the user pays it + none of its input token. A `PERMIT2_PERMIT` the decoder cannot read, then V3 + exact-ins of 600 USDT and 400 DAI paid by the user, give lines for both + amounts. 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. When it states no amount and the user pays it none of its input token, + that step never gets them, with a permit or without. They come in step order, + except that the step that sets the input side comes first when it has them, + followed, when there is no `Amount` line, by the other takes the total counts: + `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`. For ETH that the user + pays a V4 step when the router may hold ETH, `Moved from the sender` reads + `Up to ` for a known amount. For a `WRAP_ETH` that pays its WETH to + anyone but the router, `Moved to` names that recipient instead: `The sender` + for `address(1)`, and otherwise the address. A token the decoder does not know + gives the `Moved from the sender` line alone. A step may also take from the + user's wallet what the decoder did not read: a V4 step whose input or one of + whose `SETTLE`s or `SETTLE_ALL`s it cannot read, which the router still runs; + a V2 or V3 exact-in or exact-out step or a `PERMIT2_TRANSFER_FROM` whose input + it cannot read, such as a V3 step whose path is shorter than one token address + or has bytes after its last hop; every `PERMIT2_TRANSFER_FROM_BATCH`; every `EXECUTE_SUB_PLAN`, wherever it stands; and every step it fails on. Each such step gives one `Moved from the sender` line among those, reading `Unknown (could not be decoded)`. That line, or any line for a token the diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index d0a2814..5e5d107 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -1264,6 +1264,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // ERC-20 token from the router takes none of that token from the // sender. let inputPaidByUser = false; + // The recipient of a WRAP_ETH that set the input side, as written, + // when it is not the router; otherwise null. That WRAP_ETH pays its + // WETH there, not to a swap step (see the Moved to lines). + let wrapEthRecipient = null; // The last PERMIT2_PERMIT this decoder read before a step set the // input side, or null (see permitStands after the loop). let permit = null; @@ -1364,10 +1368,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // "Up to" and the amount. const userPaid = []; // A swap step the user pays for (`payerIsUser` true) takes `amount` - // of `token` from the sender. A V2 exact-in of ALREADY_PAID takes - // nothing. - const addUserPaid = (token, amount) => { - if (amount !== ALREADY_PAID) userPaid.push({ token, amount }); + // of `token` from the sender, or at most that when `atMost` is set. A + // V2 exact-in of ALREADY_PAID takes nothing. + const addUserPaid = (token, amount, atMost = false) => { + if (amount !== ALREADY_PAID) { + userPaid.push({ token, amount, atMost }); + } }; // A step may take from the sender what this decoder did not read: a // V4 step whose input or one of whose SETTLEs or SETTLE_ALLs it @@ -1895,6 +1901,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) { w.amount, ); inputPaidByUser = true; + if (!isRouter(w.recipient)) { + wrapEthRecipient = w.recipient; + } } addPayee(w.recipient); // CONTRACT_BALANCE wraps all the router's ETH. @@ -2055,9 +2064,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // router still runs it. const setInputHere = inputEstablished && !inputSetBefore; if (setInputHere) inputPaidByUser = paidIn !== undefined; + // ETH an earlier step left in the router may pay part of + // the ETH that the user pays this step. The sender then + // pays at most that amount. for (const p of paidByUser) { if (setInputHere && p === paidIn) continue; - addUserPaid(p.currency, p.amount); + addUserPaid( + p.currency, + p.amount, + routerHeldEth && + p.currency === + "0x0000000000000000000000000000000000000000", + ); } // A SETTLE of ETH from the router that `paidByUser` leaves // out is a take of its own, even on the step that sets @@ -2221,15 +2239,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) { permit.beforeEveryTake && !stepsUnread.some((step) => step > permit.step); // What the steps take that the Amount line does not give, as - // { token, amount }. A null token is a step this decoder did not read - // (see spentUnread()), or a token it does not know. + // { token, amount, atMost, recipient }. A null token is a step this + // decoder did not read (see spentUnread()), or a token it does not + // know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that + // pays elsewhere has a recipient (see wrapEthRecipient). const spentFromSender = []; let total = null; - // The takes from the sender that `total` adds up, each a quantity, as - // { token, amount }. When there is no Amount line, each gets lines of - // its own (see below). The amount of the step that set the input side - // counts in `total` whoever pays that step. That amount is among them - // only when the user pays the step (see inputPaidByUser). + // The takes from the sender that `total` adds up, each a quantity, in + // the shape of `spentFromSender`. When there is no Amount line, each + // gets lines of its own (see below). The amount of the step that set + // the input side counts in `total` whoever pays that step. That amount + // is among them only when the user pays the step (see inputPaidByUser). const inTotal = []; // Whether the step that set the input side counts as a take from the // sender, which may get lines of its own. The amount it stated counts @@ -2243,7 +2263,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) { if (!permitStands || sameAddress(inputToken, permit.token)) { total = inputAmount; if (typeof inputAmount === "bigint" && inputPaidByUser) { - inTotal.push({ token: inputToken, amount: inputAmount }); + inTotal.push({ + token: inputToken, + amount: inputAmount, + recipient: wrapEthRecipient, + }); } if (hasFirstTake && !present(inputAmount)) { firstTakeUnstated = { token: inputToken, amount: null }; @@ -2251,7 +2275,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) { } else if (hasFirstTake) { // The step that set the input side spent a token other than the // permit's, or one not known, so its amount gets lines of its own. - spentFromSender.push({ token: inputToken, amount: inputAmount }); + spentFromSender.push({ + token: inputToken, + amount: inputAmount, + recipient: wrapEthRecipient, + }); } if (permitStands) inputToken = permit.token; for (const t of userPaid) { @@ -2434,7 +2462,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // What steps take from the sender beyond the Amount line, in the shape // of the lines below for a PERMIT2_TRANSFER_FROM. An amount this // decoder does not know is said so, and a step it did not read gets - // that one line alone. + // that one line alone. A take with no recipient goes to the swap step + // that spends it. A take with a recipient goes to that recipient; + // `MSG_SENDER` means the sender. for (const s of spentFromSender) { details.push({ label: "Moved from the sender", @@ -2447,10 +2477,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) { details.push( tokenMovedLine("Token moved", tokenInfo(s.token, sources)), ); - details.push({ - label: "Moved to", - value: "The swap step that spends it", - }); + if (!present(s.recipient)) { + details.push({ + label: "Moved to", + value: "The swap step that spends it", + }); + } else if (s.recipient.toLowerCase() === MSG_SENDER) { + details.push({ label: "Moved to", value: "The sender" }); + } else { + details.push({ + label: "Moved to", + value: s.recipient, + address: s.recipient, + }); + } } } diff --git a/tests/uniswap.test.js b/tests/uniswap.test.js index 5e70a1d..76d9308 100644 --- a/tests/uniswap.test.js +++ b/tests/uniswap.test.js @@ -1428,8 +1428,8 @@ describe("uniswap decoder", () => { ]; // execute() of these command bytes and inputs, decoded as sent by - // the user. - function decodeSteps(commands, inputs) { + // the user to `router`. + function decodeSteps(commands, inputs, router = ROUTER_ADDR) { const data = buildExecute( solidityPacked( commands.map(() => "uint8"), @@ -1438,7 +1438,7 @@ describe("uniswap decoder", () => { inputs, 9999999999n, ); - return uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR); + return uniswap.decode(data, router, "0x1", {}, USER_ADDR); } // The first step: a V3 exact-in of 600 USDT for USDC, paid by the @@ -2424,6 +2424,335 @@ describe("uniswap decoder", () => { expect(linesBetweenTokens(result)).toEqual(lines); }); + // https://git.eeqj.de/sneak/AutistMask/issues/665 — a WRAP_ETH that + // sets the input side and pays its WETH to anyone but the router + // names that recipient on its Moved to line. On next each row read + // The swap step that spends it. + const ONE_ETH = 1000000000000000000n; + const MSG_SENDER = "0x0000000000000000000000000000000000000001"; + // The lines of a WRAP_ETH of 1 ETH whose Moved to line reads `to`. + const wrapLines = (to) => [ + ["Moved from the sender", "1.0000 ETH"], + ["Token moved", "ETH"], + ["Moved to", to], + ]; + test.each([ + { + kind: "a WRAP_ETH of 1 ETH to another address, then an EXECUTE_SUB_PLAN", + commands: [0x0b, 0x21], + inputs: [encodeWrapEth(OTHER_ADDR, ONE_ETH), SUB_PLAN], + lines: [ + ...wrapLines(OTHER_ADDR), + ["Moved from the sender", UNDECODED], + ], + }, + { + kind: "a WRAP_ETH of 1 ETH to address(1), then an EXECUTE_SUB_PLAN", + commands: [0x0b, 0x21], + inputs: [encodeWrapEth(MSG_SENDER, ONE_ETH), SUB_PLAN], + lines: [ + ...wrapLines("The sender"), + ["Moved from the sender", UNDECODED], + ], + }, + { + kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a WRAP_ETH of 1 ETH to another address, then a V3 exact-in of 600 USDT", + commands: [0x0a, 0x0b, 0x00], + inputs: [ + encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR), + encodeWrapEth(OTHER_ADDR, ONE_ETH), + V3_600, + ], + lines: [["Amount", "5000.0000 USDT"], ...wrapLines(OTHER_ADDR)], + }, + ])("$kind", ({ commands, inputs, lines }) => { + const result = decodeSteps(commands, inputs); + expect(linesBetweenTokens(result)).toEqual(lines); + }); + + // https://git.eeqj.de/sneak/AutistMask/issues/665 — V4Router pays a + // SETTLE of ETH from the router's own ETH, whichever payer it names. + // After an earlier step that may leave ETH in the router, the ETH that + // the user pays a V4 step reads as a maximum, on lines of its own. + // Every row reads at least the ETH that can leave the wallet. The two + // rows marked "reads as on next" read the same on next. On next every + // other row added that ETH to the Amount line, or read it as an exact + // amount. + // + // ADDRESS_THIS is the router on every release. + const ADDRESS_THIS = "0x0000000000000000000000000000000000000002"; + const WRAP_INTO_ROUTER = encodeWrapEth(ADDRESS_THIS, ONE_ETH); + // UNWRAP_WETH has the layout of WRAP_ETH. + const UNWRAP_INTO_ROUTER = encodeWrapEth(ADDRESS_THIS, 0n); + // A V4 step swapping `amount` of ETH for USDC, with a SETTLE of + // OPEN_DELTA by the user. + const v4EthForUsdc = (amount) => + encodeV4Swap( + new Uint8Array([ + V4_SWAP_EXACT_IN_SINGLE_ID, + V4_SETTLE_ID, + V4_TAKE_ALL_ID, + ]), + [ + encodeV4ExactInSingle(ZERO_ADDR, USDC_ADDR, amount, 1n), + settle(ZERO_ADDR, 0n, true), + encodeV4TakeAll(USDC_ADDR, 0n), + ], + ); + const V4_ONE_ETH = v4EthForUsdc(ONE_ETH); + // A TRANSFER or PAY_PORTION of `amount` of `token` to another + // address. + const payOut = (token, amount) => + coder.encode( + ["address", "address", "uint256"], + [token, OTHER_ADDR, amount], + ); + // An ACROSS_V4_DEPOSIT_V3 of `amount` of ETH (`useNative` set), from + // the sender to the sender on chain id 8453. + const acrossEth = (amount) => + coder.encode( + [ + "tuple(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes,bool)", + ], + [ + [ + USER_ADDR, + USER_ADDR, + WETH_ADDR, + WETH_ADDR, + amount, + 1n, + 8453n, + ZERO_ADDR, + 0, + 0, + 0, + "0x", + true, + ], + ], + ); + // The lines of ETH the sender pays a V4 step, of `amount`. + const ethLines = (amount) => [ + ["Moved from the sender", amount], + ["Token moved", "ETH"], + ["Moved to", "The swap step that spends it"], + ]; + test.each([ + { + kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 ETH that the user settles, reads as on next", + commands: [0x0b, 0x10], + inputs: [WRAP_INTO_ROUTER, V4_ONE_ETH], + lines: [["Amount", "2.0000 ETH"]], + }, + { + kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles", + commands: [0x0b, 0x0c, 0x10], + inputs: [WRAP_INTO_ROUTER, UNWRAP_INTO_ROUTER, V4_ONE_ETH], + lines: [ + ["Amount", "1.0000 ETH"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "the same with a TRANSFER of 1 ETH to another address after the UNWRAP_WETH", + commands: [0x0b, 0x0c, 0x05, 0x10], + inputs: [ + WRAP_INTO_ROUTER, + UNWRAP_INTO_ROUTER, + payOut(ZERO_ADDR, ONE_ETH), + V4_ONE_ETH, + ], + lines: [ + ["Amount", "1.0000 ETH"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "the same with a PAY_PORTION of 5,000 bips of WETH to another address before the UNWRAP_WETH", + commands: [0x0b, 0x06, 0x0c, 0x10], + inputs: [ + WRAP_INTO_ROUTER, + payOut(WETH_ADDR, 5000n), + UNWRAP_INTO_ROUTER, + V4_ONE_ETH, + ], + lines: [ + ["Amount", "1.0000 ETH"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "the same with a WRAP_ETH of 1 ETH to another address after the UNWRAP_WETH", + commands: [0x0b, 0x0c, 0x0b, 0x10], + inputs: [ + WRAP_INTO_ROUTER, + UNWRAP_INTO_ROUTER, + encodeWrapEth(OTHER_ADDR, ONE_ETH), + V4_ONE_ETH, + ], + lines: [ + ["Amount", "1.0000 ETH"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "the same on UniversalRouterV2_1_2 with an ACROSS_V4_DEPOSIT_V3 of 1 ETH after the UNWRAP_WETH", + router: ROUTER_V2_1_2_ADDR, + commands: [0x0b, 0x0c, 0x40, 0x10], + inputs: [ + WRAP_INTO_ROUTER, + UNWRAP_INTO_ROUTER, + acrossEth(ONE_ETH), + V4_ONE_ETH, + ], + lines: [ + ["Amount", "1.0000 ETH"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "a V3 exact-in of 600 USDT that the user pays, a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles", + commands: [0x00, 0x0b, 0x0c, 0x10], + inputs: [ + V3_600, + WRAP_INTO_ROUTER, + UNWRAP_INTO_ROUTER, + V4_ONE_ETH, + ], + lines: [ + ["Amount", "600.0000 USDT"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "a V3 exact-in of 600 USDT for WETH paid to the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles", + commands: [0x00, 0x0c, 0x10], + inputs: [ + encodeV3SwapExactIn(ADDRESS_THIS, USDT_600, 1n, [ + USDT_ADDR, + WETH_ADDR, + ]), + UNWRAP_INTO_ROUTER, + V4_ONE_ETH, + ], + lines: [ + ["Amount", "600.0000 USDT"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "a V4 swap of 600 USDT that takes ETH into the router, then a V4 swap of 5 ETH that the user settles", + commands: [0x10, 0x10], + inputs: [ + encodeV4Swap( + new Uint8Array([ + V4_SWAP_EXACT_IN_SINGLE_ID, + V4_SETTLE_ID, + V4_TAKE_ID, + ]), + [ + encodeV4ExactInSingle( + USDT_ADDR, + ZERO_ADDR, + USDT_600, + 1n, + ), + settle(USDT_ADDR, 0n, true), + take(ZERO_ADDR, ADDRESS_THIS, 0n), + ], + ), + v4EthForUsdc(5n * ONE_ETH), + ], + lines: [ + ["Amount", "600.0000 USDT"], + ...ethLines("Up to 5.0000 ETH"), + ], + }, + { + kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 exact-out paid in ETH with a SETTLE_ALL of at most 3 ETH, reads as on next", + commands: [0x0b, 0x0c, 0x10], + inputs: [ + WRAP_INTO_ROUTER, + UNWRAP_INTO_ROUTER, + encodeV4Swap( + new Uint8Array([ + V4_SWAP_EXACT_OUT_SINGLE_ID, + V4_SETTLE_ALL_ID, + V4_TAKE_ALL_ID, + ]), + [ + encodeV4ExactOutSingle( + ZERO_ADDR, + USDC_ADDR, + 390000000n, + 3n * ONE_ETH, + ), + encodeV4SettleAll(ZERO_ADDR, 3n * ONE_ETH), + encodeV4TakeAll(USDC_ADDR, 0n), + ], + ), + ], + lines: [ + ...wrapLines("The swap step that spends it"), + ...ethLines(UNDECODED), + ], + }, + { + kind: "a WRAP_ETH of 1 ETH into the router, a V3 exact-out of at most 1 WETH paid from the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles", + commands: [0x0b, 0x01, 0x0c, 0x10], + inputs: [ + WRAP_INTO_ROUTER, + encodeV3SwapExactOut( + USER_ADDR, + 390000000n, + ONE_ETH, + [USDC_ADDR, WETH_ADDR], + false, + ), + UNWRAP_INTO_ROUTER, + V4_ONE_ETH, + ], + lines: [ + ["Amount", "Up to 1.0000 ETH"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + { + kind: "a WRAP_ETH of 1 ETH into the router, a V4 exact-out paid from the router's WETH, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles", + commands: [0x0b, 0x10, 0x0c, 0x10], + inputs: [ + WRAP_INTO_ROUTER, + encodeV4Swap( + new Uint8Array([ + V4_SWAP_EXACT_OUT_SINGLE_ID, + V4_SETTLE_ID, + V4_TAKE_ALL_ID, + ]), + [ + encodeV4ExactOutSingle( + WETH_ADDR, + USDC_ADDR, + 390000000n, + ONE_ETH, + ), + settle(WETH_ADDR, 0n, false), + encodeV4TakeAll(USDC_ADDR, 0n), + ], + ), + UNWRAP_INTO_ROUTER, + V4_ONE_ETH, + ], + lines: [ + ["Amount", "1.0000 ETH"], + ...ethLines("Up to 1.0000 ETH"), + ], + }, + ])("$kind", ({ router, commands, inputs, lines }) => { + const result = decodeSteps(commands, inputs, router); + expect(linesBetweenTokens(result)).toEqual(lines); + }); + // A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax` // USDT for USDC. const v3ExactOut = (amountInMax) =>