diff --git a/README.md b/README.md index d5a5221..cb25065 100644 --- a/README.md +++ b/README.md @@ -1265,7 +1265,9 @@ see Transaction Decoding), do not always read as a number. They can read: the same words. `Unlimited`, `All available (V4 open delta)` and `Whatever an earlier step sent to the pair (V2 already paid)` keep their wording. When a V2 or V3 exact-out step sets `Min. received`, that line shows - its `amountOut`, the exact amount it buys. + its `amountOut`, the exact amount it buys. 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 @@ -3477,81 +3479,89 @@ 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 paid from - the router spends what the router holds and takes nothing, and so does a V2 - exact-in whose `amountIn` is `ALREADY_PAID`. A later take of `Token In` 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 step after it in full. Permit2 then lets the - router take no more of the token than that amount, so the permit covers every - take of it. A step before the permit takes under whatever allowance the router - already had, so it may take more than the permit's amount, which would then - read lower than what leaves the user's wallet: a `PERMIT2_TRANSFER_FROM` of - 2,000 USDT into the router, then a permit of 600 USDT to the router and a V3 - exact-in of 2,000 USDT paid from the router, read `Amount 2000.0000 USDT`. A - later permit or batch may raise what Permit2 lets the router take, and so may - a step the decoder does not read in full: a command the decoder does not read - (listed at the end of this section), a step whose input it cannot read, in - whole or in part, and a step it fails on. Every other `PERMIT2_PERMIT`, before - the step that sets the input side or after it, leaves `Token In` and the - `Amount` line as they read with no permit, unless it lets a spender other than - the router take `Token In` (see below): a permit of 5,000 DAI to another - address and then a V3 exact-in of 600 USDT read `Swap USDT → USDC` with - `Amount 600.0000 USDT`, and so do that exact-in and then a permit of 5,000 DAI - to the router. When a permit's amount stands and the step that sets the input - side spends a token other than the permit's, or one the decoder does not know, - that step's amount gets lines of its own, unless it states no amount and the - user pays it none of its input token. Each take the total does not count gives - three lines after the `Amount` line (after `Token In` when there is none): one - of a token other than `Token In`; one of `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 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 and is paid from the router takes - nothing from the user's wallet and gets none. 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, - as when the router pays it, that step takes none of that token from the user's - wallet and 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 + 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 paid from the router + spends what the router holds and takes nothing, and so does a V2 exact-in + whose `amountIn` is `ALREADY_PAID`. A later take of `Token In` 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 step after it in full. Permit2 then lets the router take no more of the + token than that amount, so the permit covers every take of it. A step before + the permit takes under whatever allowance the router already had, so it may + take more than the permit's amount, which would then read lower than what + leaves the user's wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the + router, then a permit of 600 USDT to the router and a V3 exact-in of 2,000 + USDT paid from the router, read `Amount 2000.0000 USDT`. A later permit or + batch may raise what Permit2 lets the router take, and so may a step the + decoder does not read in full: a command the decoder does not read (listed at + the end of this section), a step whose input it cannot read, in whole or in + part, and a step it fails on. Every other `PERMIT2_PERMIT`, before the step + that sets the input side or after it, leaves `Token In` and the `Amount` line + as they read with no permit, unless it lets a spender other than the router + take `Token In` (see below): a permit of 5,000 DAI to another address and then + a V3 exact-in of 600 USDT read `Swap USDT → USDC` with `Amount 600.0000 USDT`, + and so do that exact-in and then a permit of 5,000 DAI to the router. When a + permit's amount stands and the step that sets the input side spends a token + other than the permit's, or one the decoder does not know, that step's amount + gets lines of its own, unless it states no amount and the user pays it none of + its input token. Each take the total does not count gives three lines after + the `Amount` line (after `Token In` when there is none): one of a token other + than `Token In`; one of `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); one whose amount the decoder + does not know; and one of ETH that the user pays a V4 step when the router may + hold ETH. 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 and is paid from the router takes nothing from the user's wallet + and gets none. 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, as when the router pays it, that + step takes none of that token from the user's wallet and 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 `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 decoder does not know, leaves no `Amount` line, whose - figure might be too low, and such a step before a `PERMIT2_PERMIT` keeps the - permit's amount from standing. Then each `PERMIT2_TRANSFER_FROM` but one into - the router, in step order, gives three lines: `Moved from the sender`, the - amount it moves, for example `1000000.0000 USDT`; `Token moved`, giving the - token's address as the `Token In` line does; and `Moved to`, which reads + `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 + decoder does not know, leaves no `Amount` line, whose figure might be too low, + and such a step before a `PERMIT2_PERMIT` keeps the permit's amount from + standing. Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step + order, gives three lines: `Moved from the sender`, the amount it moves, for + example `1000000.0000 USDT`; `Token moved`, giving the token's address as the + `Token In` line does; and `Moved to`, which reads `The V2 pair the next swap spends` for a hop and `The sender` for `address(1)` 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 diff --git a/src/shared/uniswap.js b/src/shared/uniswap.js index 2a00fff..d48440e 100644 --- a/src/shared/uniswap.js +++ b/src/shared/uniswap.js @@ -1244,6 +1244,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // comes from the transaction's value. A step paid from the router // takes nothing 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; @@ -1336,15 +1340,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) { }; // What the steps take from the sender beyond the amount of the step - // that sets the input side, as { token, amount }, in step order. - // After the loop each joins the Amount line or gets lines of its own - // (see permitStands). + // that sets the input side, as { token, amount, atMost }, in step + // order. After the loop each joins the Amount line or gets lines of + // its own (see permitStands). A take with `atMost` set takes at most + // its amount, and always gets lines of its own. 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 @@ -1787,6 +1794,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. @@ -1815,6 +1825,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) { } else if (command === "V4_SWAP") { const v4 = decodeV4Swap(inputs[i]); const inputSetBefore = inputEstablished; + // V4Router pays a SETTLE of ETH from the router's own + // ETH, whichever payer it names. When the router may hold + // ETH from an earlier step, that ETH may pay some of the + // ETH that the user pays this step. The sender then pays + // at most the step's amount. + const routerMayHoldEth = heldByRouter.has( + "0x0000000000000000000000000000000000000000", + ); if (v4) { // Each SETTLE paid from the router spends the amount // decodeV4Swap() says it pays. @@ -1905,7 +1923,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) { if (v4) { for (const p of v4.paidByUser) { if (setInputHere && p === paidIn) continue; - addUserPaid(p.currency, p.amount); + addUserPaid( + p.currency, + p.amount, + routerMayHoldEth && + p.currency === + "0x0000000000000000000000000000000000000000", + ); } } if (!v4 || v4.settleUnread) { @@ -2016,7 +2040,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // being of another token or coming after an amount that is not a // quantity (a V4 open delta, ALREADY_PAID, or none stated), gets // lines of its own. One whose amount is not known also leaves the - // total unknown, with no figure. + // total unknown, with no figure. A take of at most its amount (see + // userPaid) is never added, and gets lines of its own too. // // A PERMIT2_PERMIT names Token In only when its amount stands as the // Amount figure in place of the total, which it does only in the @@ -2051,14 +2076,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 a step that set the input side - // and is paid from the router counts in `total` but is not among them. + // 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 a step that set + // the input side and is paid from the router counts in `total` but is + // not among them. 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 @@ -2073,7 +2101,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 }; @@ -2081,7 +2113,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) { @@ -2089,6 +2125,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) { if (ofTokenIn && permitStands) continue; if ( ofTokenIn && + !t.atMost && typeof total === "bigint" && typeof t.amount === "bigint" ) { @@ -2193,8 +2230,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) { // V2's already-paid zero, neither of which is a quantity at all (see // OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two // tests come first — the sentinels are not bigints and cannot be - // compared with one. - const spentText = (amount, info) => { + // compared with one. `atMost` says the amount is a maximum (see + // userPaid). + const spentText = (amount, info, atMost = false) => { if (amount === OPEN_DELTA) { return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT }; } @@ -2207,7 +2245,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) { if (amount >= MAX_UINT160) { return { raw: "Unlimited", display: "Unlimited" }; } - if (hasV2OrV3ExactOut) { + if (hasV2OrV3ExactOut || atMost) { // A V2 or V3 exact-out swap spends at most this figure, // whichever steps gave it (an amountInMax, the WRAP_ETH of a // swap paid in ETH, a permit, or several steps' total), so it @@ -2260,20 +2298,33 @@ 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", value: present(s.amount) - ? spentText(s.amount, tokenInfo(s.token, sources)).display + ? spentText(s.amount, tokenInfo(s.token, sources), s.atMost) + .display : UNDECODED_CURRENCY, }); if (present(s.token)) { details.push(tokenMovedLine("Token moved", 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 c0e3b64..971fc18 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) =>