1 Commits
Author SHA1 Message Date
sneak c56179abff fix: a WRAP_ETH says where its ETH goes (closes #747)
check / check (push) Successful in 5m26s
e2e / e2e-chrome (push) Successful in 14m4s
e2e / e2e-firefox (push) Successful in 2m37s
A WRAP_ETH into the router now always reads `Moved to: The router`. That is
true whichever step then takes the WETH. Other takes keep
`The swap step that spends it`.

A later WRAP_ETH of CONTRACT_BALANCE to anyone but the router now gets a
`Moved from the sender` line reading `All the ETH the transaction carries`,
and `Moved to` names the recipient. It gets none after an UNWRAP_WETH into
the router, or while the router holds ETH from a V4 step.

README states both rules.

Model: opus-5-5
2026-11-03 14:07:59 +00:00
3 changed files with 430 additions and 56 deletions
+13 -9
View File
@@ -1414,15 +1414,19 @@ when both tokens are known. The lines are:
the sender that the `Amount` line does not include. The same three lines show the sender that the `Amount` line does not include. The same three lines show
what a `PERMIT2_TRANSFER_FROM` moves and where. A `WRAP_ETH` can pay its WETH what a `PERMIT2_TRANSFER_FROM` moves and where. A `WRAP_ETH` can pay its WETH
to a recipient other than the router. When its amount gets these lines, its to a recipient other than the router. When its amount gets these lines, its
`Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH` `Moved to` line names that recipient, or reads `The sender`. For a `WRAP_ETH`
after the step that sets `Token In` takes the amount it states from the into the router, `Moved to` reads `The router`. A `WRAP_ETH` after the step
sender. When it pays anyone but the router, that amount gets these lines. When that sets `Token In` takes the amount it states from the sender. When it pays
it pays the router, that amount joins the `Amount` line if `Token In` is ETH, anyone but the router, that amount gets these lines. When it pays the router,
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` states that amount joins the `Amount` line if `Token In` is ETH, and gets these lines
no amount, so it gets no lines. ETH that the sender pays a V4 step or a if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` to anyone but the router gets
`WRAP_ETH` of a stated amount can get these three lines when the router may these lines. It gets none after an `UNWRAP_WETH` into the router, or while the
hold ETH from an earlier step. When the decoder knows the amount, its router holds ETH from a V4 step. When it gets them, its
`Moved from the sender` line reads `Up to` and the amount. `Moved from the sender` line reads `All the ETH the transaction carries`. ETH
that the sender pays a V4 step or a `WRAP_ETH` of a stated amount can get
these three lines when the router may hold ETH from an earlier step. When the
decoder knows the amount, its `Moved from the sender` line reads `Up to` and
the amount.
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance - `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's
amount already stands as the `Amount` figure. amount already stands as the `Amount` figure.
+61 -26
View File
@@ -404,6 +404,11 @@ const OPEN_DELTA = Symbol("v4-open-delta");
// that nothing is swapped. // that nothing is swapped.
const ALREADY_PAID = Symbol("v2-already-paid"); const ALREADY_PAID = Symbol("v2-already-paid");
// A later WRAP_ETH of CONTRACT_BALANCE wraps all the ETH the router holds, and
// so all the ETH the transaction carries. The calldata states no quantity for
// it. The approval screen's Value line gives that ETH.
const TRANSACTION_ETH = Symbol("transaction-eth");
// The router's whole-balance amount, universal-router // The router's whole-balance amount, universal-router
// `ActionConstants.CONTRACT_BALANCE`: a V2 exact-in given it as `amountIn` // `ActionConstants.CONTRACT_BALANCE`: a V2 exact-in given it as `amountIn`
// pays the pair all the router holds of the input token. // pays the pair all the router holds of the input token.
@@ -416,6 +421,7 @@ const CONTRACT_BALANCE = 2n ** 255n;
const OPEN_DELTA_AMOUNT = "All available (V4 open delta)"; const OPEN_DELTA_AMOUNT = "All available (V4 open delta)";
const ALREADY_PAID_AMOUNT = const ALREADY_PAID_AMOUNT =
"Whatever an earlier step sent to the pair (V2 already paid)"; "Whatever an earlier step sent to the pair (V2 already paid)";
const TRANSACTION_ETH_AMOUNT = "All the ETH the transaction carries";
const NO_MINIMUM = "None (no minimum guaranteed)"; const NO_MINIMUM = "None (no minimum guaranteed)";
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded". // Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
@@ -1267,9 +1273,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// ERC-20 token from the router takes none of that token from the // ERC-20 token from the router takes none of that token from the
// sender. // sender.
let inputPaidByUser = false; let inputPaidByUser = false;
// The recipient of a WRAP_ETH that set the input side, as written, // 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 // otherwise null. That WRAP_ETH pays its WETH there (see the Moved to
// WETH there, not to a swap step (see the Moved to lines). // lines).
let wrapEthRecipient = null; let wrapEthRecipient = null;
// The last PERMIT2_PERMIT this decoder read before a step set the // The last PERMIT2_PERMIT this decoder read before a step set the
// input side, or null (see permitStands after the loop). // input side, or null (see permitStands after the loop).
@@ -1289,6 +1295,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
let minOutput = null; let minOutput = null;
let outputEstablished = false; let outputEstablished = false;
let hasUnwrapWeth = false; let hasUnwrapWeth = false;
// Set once an UNWRAP_WETH pays ETH to the router, which a later
// WRAP_ETH of CONTRACT_BALANCE then wraps.
let unwrappedIntoRouter = false;
// Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH, // Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH,
// which the router holds; cleared whenever the output side is set // which the router holds; cleared whenever the output side is set
// again, or a later step may pay that ETH out as WETH (see after the // again, or a later step may pay that ETH out as WETH (see after the
@@ -1368,9 +1377,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// After the loop each joins the Amount line or gets lines of its own // After the loop each joins the Amount line or gets lines of its own
// (see permitStands). A take with `atMost` set may take less than // (see permitStands). A take with `atMost` set may take less than
// `amount`: it never joins the Amount line, and its line reads // `amount`: it never joins the Amount line, and its line reads
// "Up to" and the amount. A take with a `recipient`, the ETH of a // "Up to" and the amount. A take with a `recipient` is the ETH of a
// later WRAP_ETH that pays anyone but the router, never joins the // later WRAP_ETH. If that recipient is not the router, the take never
// Amount line either: its Moved to line names that recipient. // joins the Amount line either: its Moved to line names that recipient.
const userPaid = []; const userPaid = [];
// A swap step the user pays for (`payerIsUser` true) takes `amount` // A swap step the user pays for (`payerIsUser` true) takes `amount`
// of `token` from the sender, or at most that when `atMost` is set. A // of `token` from the sender, or at most that when `atMost` is set. A
@@ -1906,9 +1915,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
w.amount, w.amount,
); );
inputPaidByUser = true; inputPaidByUser = true;
if (!isRouter(w.recipient)) { wrapEthRecipient = w.recipient;
wrapEthRecipient = w.recipient;
}
} else if ( } else if (
w.amount > 0n && w.amount > 0n &&
w.amount !== CONTRACT_BALANCE w.amount !== CONTRACT_BALANCE
@@ -1916,17 +1923,33 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// The ETH of a stated amount comes from the // The ETH of a stated amount comes from the
// transaction's value. ETH an earlier step left in // transaction's value. ETH an earlier step left in
// the router may pay part of it, and the sender // the router may pay part of it, and the sender
// then pays at most that amount. CONTRACT_BALANCE // then pays at most that amount.
// states no amount to take.
userPaid.push({ userPaid.push({
token: "0x0000000000000000000000000000000000000000", token: "0x0000000000000000000000000000000000000000",
amount: w.amount, amount: w.amount,
atMost: heldByRouter.has( atMost: heldByRouter.has(
"0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000",
), ),
recipient: isRouter(w.recipient) recipient: w.recipient,
? null });
: w.recipient, } else if (
w.amount === CONTRACT_BALANCE &&
!isRouter(w.recipient) &&
!unwrappedIntoRouter &&
!heldByRouter.has(
"0x0000000000000000000000000000000000000000",
)
) {
// It pays the recipient all the ETH the transaction
// carries. It gets no lines after an UNWRAP_WETH
// into the router, or while the router holds ETH
// from a V4 step: Uniswap's routes pay back what
// their swap did not spend this way, and carry no
// ETH.
userPaid.push({
token: "0x0000000000000000000000000000000000000000",
amount: TRANSACTION_ETH,
recipient: w.recipient,
}); });
} }
addPayee(w.recipient); addPayee(w.recipient);
@@ -2169,6 +2192,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
heldByRouter.has(w), heldByRouter.has(w),
); );
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w)); WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
if (isRouter(u.recipient)) {
unwrappedIntoRouter = true;
}
if (isRouter(u.recipient) && heldWeth) { if (isRouter(u.recipient) && heldWeth) {
heldByRouter.set( heldByRouter.set(
"0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000",
@@ -2266,8 +2292,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// What the steps take that the Amount line does not give, as // What the steps take that the Amount line does not give, as
// { token, amount, atMost, recipient }. A null token is a step this // { token, amount, atMost, recipient }. A null token is a step this
// decoder did not read (see spentUnread()), or a token it does not // 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 // know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH has
// pays elsewhere has a recipient (see wrapEthRecipient and userPaid). // a recipient (see wrapEthRecipient and userPaid).
const spentFromSender = []; const spentFromSender = [];
let total = null; let total = null;
// The takes from the sender that `total` adds up, each a quantity, in // The takes from the sender that `total` adds up, each a quantity, in
@@ -2315,7 +2341,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
typeof total === "bigint" && typeof total === "bigint" &&
typeof t.amount === "bigint" && typeof t.amount === "bigint" &&
!t.atMost && !t.atMost &&
!present(t.recipient) (!present(t.recipient) || isRouter(t.recipient))
) { ) {
total += t.amount; total += t.amount;
inTotal.push(t); inTotal.push(t);
@@ -2413,13 +2439,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} }
// The text of an amount the swap steps spend, for the Amount line and // The text of an amount the swap steps spend, for the Amount line and
// the lines for other tokens below it. Three amounts need no scale to // the lines for other tokens below it. Four amounts need no scale to
// describe and are named rather than formatted: V4's open delta and // describe and are named rather than formatted: V4's open delta, V2's
// V2's already-paid zero, neither of which is a quantity at all (see // already-paid zero and the ETH the transaction carries, none of which
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two // is a quantity at all (see OPEN_DELTA, ALREADY_PAID and
// tests come first — the sentinels are not bigints and cannot be // TRANSACTION_ETH), and an unbounded permit. Those three tests come
// compared with one. `atMost` marks an amount that is only the most a // first — the sentinels are not bigints and cannot be compared with
// take may be (see userPaid). // one. `atMost` marks an amount that is only the most a take may be
// (see userPaid).
const spentText = (amount, info, atMost) => { const spentText = (amount, info, atMost) => {
if (amount === OPEN_DELTA) { if (amount === OPEN_DELTA) {
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT }; return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
@@ -2430,6 +2457,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
display: ALREADY_PAID_AMOUNT, display: ALREADY_PAID_AMOUNT,
}; };
} }
if (amount === TRANSACTION_ETH) {
return {
raw: TRANSACTION_ETH_AMOUNT,
display: TRANSACTION_ETH_AMOUNT,
};
}
if (amount >= MAX_UINT160) { if (amount >= MAX_UINT160) {
return { raw: "Unlimited", display: "Unlimited" }; return { raw: "Unlimited", display: "Unlimited" };
} }
@@ -2489,8 +2522,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this // 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 // decoder does not know is said so, and a step it did not read gets
// that one line alone. A take with no recipient goes to the swap step // 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; // that spends it. A take with a recipient goes to that recipient:
// `MSG_SENDER` means the sender. // the router, the sender as `MSG_SENDER`, or an address.
for (const s of spentFromSender) { for (const s of spentFromSender) {
details.push({ details.push({
label: "Moved from the sender", label: "Moved from the sender",
@@ -2514,6 +2547,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
label: "Moved to", label: "Moved to",
value: "The swap step that spends it", value: "The swap step that spends it",
}); });
} else if (isRouter(s.recipient)) {
details.push({ label: "Moved to", value: "The router" });
} else if (s.recipient.toLowerCase() === MSG_SENDER) { } else if (s.recipient.toLowerCase() === MSG_SENDER) {
details.push({ label: "Moved to", value: "The sender" }); details.push({ label: "Moved to", value: "The sender" });
} else { } else {
+356 -21
View File
@@ -2474,10 +2474,11 @@ describe("uniswap decoder", () => {
// SETTLE of ETH from the router's own ETH, whichever payer it names. // 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 // 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. // 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 // Every row reads at least the ETH that can leave the wallet. The row
// rows marked "reads as on next" read the same on next. On next every // marked "reads as on next" reads the same on next. The row with a
// other row added that ETH to the Amount line, or read it as an exact // SETTLE_ALL of at most 3 ETH read that ETH as not known on next. On
// amount. // 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. // ADDRESS_THIS is the router on every release.
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002"; const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
@@ -2625,7 +2626,7 @@ describe("uniswap decoder", () => {
], ],
lines: [ lines: [
["Amount", "600.0000 USDT"], ["Amount", "600.0000 USDT"],
...wrapLines("The swap step that spends it"), ...wrapLines("The router"),
...ethLines("Up to 1.0000 ETH"), ...ethLines("Up to 1.0000 ETH"),
], ],
}, },
@@ -2674,7 +2675,7 @@ describe("uniswap decoder", () => {
], ],
}, },
{ {
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", 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",
commands: [0x0b, 0x0c, 0x10], commands: [0x0b, 0x0c, 0x10],
inputs: [ inputs: [
WRAP_INTO_ROUTER, WRAP_INTO_ROUTER,
@@ -2697,10 +2698,7 @@ describe("uniswap decoder", () => {
], ],
), ),
], ],
lines: [ lines: [...wrapLines("The router"), ...ethLines(UNDECODED)],
...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", 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",
@@ -2761,8 +2759,8 @@ describe("uniswap decoder", () => {
// does not set the input side takes the amount it states from the // does not set the input side takes the amount it states from the
// sender. On next no line gave that ETH. The rows marked "reads as on // sender. On next no line gave that ETH. The rows marked "reads as on
// next" state no amount to take from the sender. Uniswap's own routes // next" state no amount to take from the sender. Uniswap's own routes
// wrap CONTRACT_BALANCE after their first step, as the last two rows // wrap CONTRACT_BALANCE into the router after their first step, as the
// do. // last row does.
const CONTRACT_BALANCE = 2n ** 255n; const CONTRACT_BALANCE = 2n ** 255n;
const WRAP_TO_OTHER = encodeWrapEth(OTHER_ADDR, ONE_ETH); const WRAP_TO_OTHER = encodeWrapEth(OTHER_ADDR, ONE_ETH);
test.each([ test.each([
@@ -2787,7 +2785,7 @@ describe("uniswap decoder", () => {
inputs: [V3_600, WRAP_INTO_ROUTER], inputs: [V3_600, WRAP_INTO_ROUTER],
lines: [ lines: [
["Amount", "600.0000 USDT"], ["Amount", "600.0000 USDT"],
...wrapLines("The swap step that spends it"), ...wrapLines("The router"),
], ],
}, },
{ {
@@ -2796,7 +2794,7 @@ describe("uniswap decoder", () => {
inputs: [V3_600, encodeWrapEth(ROUTER_ADDR, ONE_ETH)], inputs: [V3_600, encodeWrapEth(ROUTER_ADDR, ONE_ETH)],
lines: [ lines: [
["Amount", "600.0000 USDT"], ["Amount", "600.0000 USDT"],
...wrapLines("The swap step that spends it"), ...wrapLines("The router"),
], ],
}, },
{ {
@@ -2839,7 +2837,9 @@ describe("uniswap decoder", () => {
], ],
lines: [ lines: [
["Amount", "1.0000 ETH"], ["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"), ["Moved from the sender", "Up to 1.0000 ETH"],
["Token moved", "ETH"],
["Moved to", "The router"],
], ],
}, },
{ {
@@ -2877,12 +2877,6 @@ describe("uniswap decoder", () => {
inputs: [V3_600, encodeWrapEth(OTHER_ADDR, 0n)], inputs: [V3_600, encodeWrapEth(OTHER_ADDR, 0n)],
lines: [["Amount", "600.0000 USDT"]], lines: [["Amount", "600.0000 USDT"]],
}, },
{
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
commands: [0x00, 0x0b],
inputs: [V3_600, encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE)],
lines: [["Amount", "600.0000 USDT"]],
},
{ {
kind: "a V4 swap of 600 USDT that takes ETH into the router, a WRAP_ETH of CONTRACT_BALANCE into the router, then a V3 exact-in of it, reads as on next", kind: "a V4 swap of 600 USDT that takes ETH into the router, a WRAP_ETH of CONTRACT_BALANCE into the router, then a V3 exact-in of it, reads as on next",
commands: [0x10, 0x0b, 0x00], commands: [0x10, 0x0b, 0x00],
@@ -2923,6 +2917,347 @@ describe("uniswap decoder", () => {
expect(linesBetweenTokens(result)).toEqual(lines); expect(linesBetweenTokens(result)).toEqual(lines);
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a WRAP_ETH into the
// router pays its WETH to the router, whichever step then takes it. On
// next that Moved to line read The swap step that spends it in every
// row.
const SWAP_STEP = "The swap step that spends it";
// Each pays 1 WETH from the router, for USDC, to the user.
const V3_WETH_FROM_ROUTER = coder.encode(
["address", "uint256", "uint256", "bytes", "bool"],
[
USER_ADDR,
ONE_ETH,
1n,
encodeV3Path([WETH_ADDR, USDC_ADDR]),
false,
],
);
const V2_WETH_FROM_ROUTER = coder.encode(
["address", "uint256", "uint256", "address[]", "bool"],
[USER_ADDR, ONE_ETH, 1n, [WETH_ADDR, USDC_ADDR], false],
);
const V4_WETH_FROM_ROUTER = encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactInSingle(WETH_ADDR, USDC_ADDR, ONE_ETH, 1n),
settle(WETH_ADDR, 0n, false),
encodeV4TakeAll(USDC_ADDR, 0n),
],
);
// SWEEP has the layout of TRANSFER, and its amount is a minimum.
const SWEEP_WETH = payOut(WETH_ADDR, 0n);
test.each([
{
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a SWEEP of that WETH to another address",
commands: [0x00, 0x0b, 0x04],
inputs: [V3_600, WRAP_INTO_ROUTER, SWEEP_WETH],
lines: [
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
},
{
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, a SWEEP of that WETH to another address, then a V3 exact-in paid from the router's WETH",
commands: [0x00, 0x0b, 0x04, 0x00],
inputs: [
V3_600,
WRAP_INTO_ROUTER,
SWEEP_WETH,
V3_WETH_FROM_ROUTER,
],
lines: [
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
},
{
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in of WETH that the user pays",
commands: [0x00, 0x0b, 0x00],
inputs: [
V3_600,
WRAP_INTO_ROUTER,
encodeV3SwapExactIn(USER_ADDR, ONE_ETH, 1n, [
WETH_ADDR,
USDC_ADDR,
]),
],
lines: [
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
["Moved from the sender", "1.0000 WETH"],
["Token moved", "WETH (" + WETH_ADDR + ")"],
["Moved to", SWAP_STEP],
],
},
{
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, a V3 exact-in paid from the router's WETH, then another WRAP_ETH of 1 ETH into the router",
commands: [0x00, 0x0b, 0x00, 0x0b],
inputs: [
V3_600,
WRAP_INTO_ROUTER,
V3_WETH_FROM_ROUTER,
WRAP_INTO_ROUTER,
],
lines: [
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
...wrapLines("The router"),
],
},
{
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in paid from the router's WETH",
commands: [0x00, 0x0b, 0x00],
inputs: [V3_600, WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER],
lines: [
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
},
{
kind: "the same with a V2 exact-in paid from the router's WETH",
commands: [0x00, 0x0b, 0x08],
inputs: [V3_600, WRAP_INTO_ROUTER, V2_WETH_FROM_ROUTER],
lines: [
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
},
{
kind: "the same with a V3 exact-out paid from the router's WETH",
commands: [0x00, 0x0b, 0x01],
inputs: [
V3_600,
WRAP_INTO_ROUTER,
encodeV3SwapExactOut(
USER_ADDR,
390000000n,
ONE_ETH,
[USDC_ADDR, WETH_ADDR],
false,
),
],
lines: [
["Amount", "Up to 600.0000 USDT"],
["Moved from the sender", "Up to 1.0000 ETH"],
["Token moved", "ETH"],
["Moved to", "The router"],
],
},
{
kind: "the same with a V4 swap that the router's WETH settles",
commands: [0x00, 0x0b, 0x10],
inputs: [V3_600, WRAP_INTO_ROUTER, V4_WETH_FROM_ROUTER],
lines: [
["Amount", "600.0000 USDT"],
...wrapLines("The router"),
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a SWEEP of that WETH to another address, then an EXECUTE_SUB_PLAN",
commands: [0x0b, 0x04, 0x21],
inputs: [WRAP_INTO_ROUTER, SWEEP_WETH, SUB_PLAN],
lines: [
...wrapLines("The router"),
["Moved from the sender", UNDECODED],
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a V3 exact-in paid from the router's WETH, then an EXECUTE_SUB_PLAN",
commands: [0x0b, 0x00, 0x21],
inputs: [WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER, SUB_PLAN],
lines: [
...wrapLines("The router"),
["Moved from the sender", UNDECODED],
],
},
{
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in of 600 USDT",
commands: [0x0a, 0x0b, 0x00],
inputs: [
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
WRAP_INTO_ROUTER,
V3_600,
],
lines: [
["Amount", "5000.0000 USDT"],
...wrapLines("The router"),
],
},
{
kind: "the same with a V3 exact-in paid from the router's WETH after the WRAP_ETH",
commands: [0x0a, 0x0b, 0x00, 0x00],
inputs: [
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
WRAP_INTO_ROUTER,
V3_WETH_FROM_ROUTER,
V3_600,
],
lines: [
["Amount", "5000.0000 USDT"],
...wrapLines("The router"),
],
},
])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
});
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a later WRAP_ETH
// of CONTRACT_BALANCE wraps all the ETH the router holds. Unless an
// earlier step paid ETH to the router, that is the ETH the transaction
// carries. On next no line gave it. Into the router it gets no lines.
// It gets none after an UNWRAP_WETH into the router, or while the
// router holds ETH from a V4 step: Uniswap's own routes pay back what
// their swap did not spend this way, and carry no ETH. In the rows
// marked "reads as on next" that WRAP_ETH gets no lines.
const WRAP_ALL_TO_OTHER = encodeWrapEth(OTHER_ADDR, CONTRACT_BALANCE);
const WRAP_ALL_TO_SENDER = encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE);
// The lines of such a WRAP_ETH whose Moved to line reads `to`.
const allEthLines = (to) => [
["Moved from the sender", "All the ETH the transaction carries"],
["Token moved", "ETH"],
["Moved to", to],
];
// The first steps of a route that spends WETH on a V4 pool of ETH:
// a PERMIT2_TRANSFER_FROM of 1 WETH into the router, an UNWRAP_WETH
// into the router, then a V4 exact-out paid from the router's ETH.
const UNWRAP_ROUTE = [
coder.encode(
["address", "address", "uint160"],
[WETH_ADDR, ADDRESS_THIS, ONE_ETH],
),
UNWRAP_INTO_ROUTER,
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactOutSingle(
ZERO_ADDR,
USDC_ADDR,
390000000n,
ONE_ETH,
),
settle(ZERO_ADDR, 0n, false),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
];
// The lines of that route on next: this decoder cannot tell what the
// V4 step's SETTLE of ETH pays.
const UNWRAP_ROUTE_LINES = [
["Moved from the sender", UNDECODED],
["Token moved", "ETH"],
["Moved to", SWAP_STEP],
];
test.each([
{
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to another address",
commands: [0x00, 0x0b],
inputs: [V3_600, WRAP_ALL_TO_OTHER],
lines: [
["Amount", "600.0000 USDT"],
...allEthLines(OTHER_ADDR),
],
},
{
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to the sender",
commands: [0x00, 0x0b],
inputs: [V3_600, WRAP_ALL_TO_SENDER],
lines: [
["Amount", "600.0000 USDT"],
...allEthLines("The sender"),
],
},
{
kind: "a V3 exact-out of at most 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to another address",
commands: [0x01, 0x0b],
inputs: [
encodeV3SwapExactOut(USER_ADDR, 390000000n, USDT_600, [
USDC_ADDR,
USDT_ADDR,
]),
WRAP_ALL_TO_OTHER,
],
lines: [
["Amount", "Up to 600.0000 USDT"],
...allEthLines(OTHER_ADDR),
],
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a WRAP_ETH of CONTRACT_BALANCE to another address",
commands: [0x0b, 0x0b],
inputs: [WRAP_INTO_ROUTER, WRAP_ALL_TO_OTHER],
lines: [["Amount", "1.0000 ETH"], ...allEthLines(OTHER_ADDR)],
},
{
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE into the router, reads as on next",
commands: [0x00, 0x0b],
inputs: [V3_600, encodeWrapEth(ADDRESS_THIS, CONTRACT_BALANCE)],
lines: [["Amount", "600.0000 USDT"]],
},
{
kind: "a V3 exact-in of 600 USDT, an UNWRAP_WETH to the sender, then a WRAP_ETH of CONTRACT_BALANCE to another address",
commands: [0x00, 0x0c, 0x0b],
inputs: [
V3_600,
encodeWrapEth(MSG_SENDER, 0n),
WRAP_ALL_TO_OTHER,
],
lines: [
["Amount", "600.0000 USDT"],
...allEthLines(OTHER_ADDR),
],
},
{
kind: "a V4 swap of 600 USDT that takes ETH into the router, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
commands: [0x10, 0x0b],
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),
],
),
WRAP_ALL_TO_SENDER,
],
lines: [["Amount", "600.0000 USDT"]],
},
{
kind: "the unwrap route, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
commands: [0x02, 0x0c, 0x10, 0x0b],
inputs: [...UNWRAP_ROUTE, WRAP_ALL_TO_SENDER],
lines: UNWRAP_ROUTE_LINES,
},
{
kind: "the unwrap route, then a WRAP_ETH of CONTRACT_BALANCE to another address, reads as on next",
commands: [0x02, 0x0c, 0x10, 0x0b],
inputs: [...UNWRAP_ROUTE, WRAP_ALL_TO_OTHER],
lines: UNWRAP_ROUTE_LINES,
},
])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
});
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax` // A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
// USDT for USDC. // USDT for USDC.
const v3ExactOut = (amountInMax) => const v3ExactOut = (amountInMax) =>