1 Commits
Author SHA1 Message Date
sneak 88cee51f5e fix: the swap decoder names where a WRAP_ETH pays and counts the router's ETH in a V4 settle (closes #665)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
A WRAP_ETH that sets the input side and pays its WETH to anyone but
the router now names that recipient on its Moved to line, or reads
The sender for address(1).

V4Router pays ETH from the router's own ETH, whoever a SETTLE names as
payer. ETH the router already holds now pays first for the ETH a V4
step would take from the user, and only the rest is moved from the
sender. The decoder subtracts only when it knows both amounts;
otherwise the sender's ETH reads as unknown. An UNWRAP_WETH into the
router keeps a known WETH amount as the ETH the router then holds. A
step that spends an unstated amount of a token from the router makes
the router's amount of that token unknown.

Model: opus-5-5
2026-10-27 01:01:21 +00:00
3 changed files with 277 additions and 128 deletions
+99 -97
View File
@@ -3412,47 +3412,48 @@ Currently supported:
as a swap paid in ETH does, but only when that amount is not as a swap paid in ETH does, but only when that amount is not
`CONTRACT_BALANCE` and the router held no WETH from an earlier step when the `CONTRACT_BALANCE` and the router held no WETH from an earlier step when the
`WRAP_ETH` ran: after two `WRAP_ETH`s of 0.5 ETH into the router, a swap of 1 `WRAP_ETH` ran: after two `WRAP_ETH`s of 0.5 ETH into the router, a swap of 1
WETH from it reads as leaving the WETH there. An `UNWRAP_WETH` into the router WETH from it reads as leaving the WETH there. A V2 or V3 exact-out step paid
turns all the router's WETH into as much ETH. The decoder then knows how much from the router spends an amount the calldata does not state, at most its
ETH the router holds only when it knew the WETH's amount and the router held `amountInMax`. The decoder then no longer knows how much of the step's input
no ETH before. A V4 `SETTLE` of ETH spends the ETH the router holds, whoever token the router holds. A V4 `SETTLE` from the router has the same effect when
pays it (see below). A V2 or V3 exact-out step paid from the router spends an the decoder does not know what that `SETTLE` pays (see below). An
amount the calldata does not state, at most its `amountInMax`, so the decoder `UNWRAP_WETH` into the router turns all the router's WETH into the same amount
counts it as spending nothing the router holds. A `SETTLE` of `OPEN_DELTA` of ETH. The decoder knows how much ETH that is only when it knew how much WETH
(zero), and a `SETTLE_ALL`, pay only what the step still owes of their token the router held and the router held no ETH before. A V4 `SETTLE` of ETH always
when they run. The decoder takes that to be the amounts the step's swap spends the router's own ETH, whichever payer it names (see below). A `SETTLE`
actions state they spend of that token only when every action in the step of `OPEN_DELTA` (zero), and a `SETTLE_ALL`, pay only what the step still owes
could be read, every swap action names the token it spends and the token it of their token when they run. The decoder takes that to be the amounts the
buys (a swap action with an empty path swaps nothing), the `SETTLE` or step's swap actions state they spend of that token only when every action in
`SETTLE_ALL` is the step's only `SETTLE` or `SETTLE_ALL` of that token and the step could be read, every swap action names the token it spends and the
token it buys (a swap action with an empty path swaps nothing), the `SETTLE`
or `SETTLE_ALL` is the step's only `SETTLE` or `SETTLE_ALL` of that token and
comes after every swap action, no swap action buys that token, no `TAKE`, comes after every swap action, no swap action buys that token, no `TAKE`,
`TAKE_ALL` or `TAKE_PORTION` in the step takes it (a `TAKE` of a stated amount `TAKE_ALL` or `TAKE_PORTION` in the step takes it (a `TAKE` of a stated amount
adds to what the step owes), and the swap actions that spend it state a adds to what the step owes), and the swap actions that spend it state a
quantity together; otherwise it does not know the amount, and such a `SETTLE` quantity together; otherwise it does not know the amount. So the rest of a V4
from the router counts as spending nothing. So the rest of a V4 `SETTLE` of `SETTLE` of more than its swap spends, taken to the router, the WETH of a
more than its swap spends, taken to the router, the WETH of a `WRAP_ETH`, and `WRAP_ETH`, and a token a swap pays the router on the way to the output, each
a token a swap pays the router on the way to the output, each stays in the stays in the router when later steps do not spend all of it, where anyone can
router when later steps do not spend all of it, where anyone can take it in a take it in a later transaction. The router sends ETH as written, so a
later transaction. The router sends ETH as written, so a `TRANSFER` of `TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A
`CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A step paying a step paying a token to the V2 pair that the very next step spends, as a V2
token to the V2 pair that the very next step spends, as a V2 exact-in of that exact-in of that token whose `amountIn` is `ALREADY_PAID` (zero) or
token whose `amountIn` is `ALREADY_PAID` (zero) or `CONTRACT_BALANCE`, is a `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes pay the
hop, not a payout: Uniswap's mixed routes pay the next V2 pair directly, and next V2 pair directly, and the router enforces that swap's own minimum. The
the router enforces that swap's own minimum. The decoder checks that the decoder checks that the address paid is that pair, deriving it from the V2
address paid is that pair, deriving it from the V2 factory and pair init code factory and pair init code hash the network's routers were deployed with and
hash the network's routers were deployed with and the first two tokens of that the first two tokens of that step's path; a payment to any other address is
step's path; a payment to any other address is named. (Sepolia's named. (Sepolia's `UniversalRouterV1_2_V2Support` was deployed with
`UniversalRouterV1_2_V2Support` was deployed with placeholders for both placeholders for both values, so no V2 swap step can succeed on that router.)
values, so no V2 swap step can succeed on that router.) Every other payment Every other payment counts, in whichever step and of whatever token, a share
counts, in whichever step and of whatever token, a share of the output of the output included: an interface fee paid to another address by
included: an interface fee paid to another address by `PAY_PORTION`, `PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the minimum reads
`TRANSFER` or `TAKE_PORTION` is named, and the minimum reads `Min. paid out`, `Min. paid out`, because the swap's minimum includes the fee. The minimum
because the swap's minimum includes the fee. The minimum reads `Min. received` reads `Min. received` only when some step pays the transaction's sender (the
only when some step pays the transaction's sender (the From address on From address on TxApproval, the sender the explorer reports on
TxApproval, the sender the explorer reports on TransactionDetail), no TransactionDetail), no `Paid to` line is shown, and no permit or batch grants
`Paid to` line is shown, and no permit or batch grants an allowance to a an allowance to a spender other than the router (see below). `Paid to` lines
spender other than the router (see below). `Paid to` lines after `Token Out` after `Token Out` name each other address paid; one reads
name each other address paid; one reads
`The bridge, which pays the deposit's recipient on another network` for a `The bridge, which pays the deposit's recipient on another network` for a
bridge deposit; one reads, for example, bridge deposit; one reads, for example,
`The router, 2000.0000 USDT from the sender (later steps may not spend all of it)` `The router, 2000.0000 USDT from the sender (later steps may not spend all of it)`
@@ -3485,65 +3486,66 @@ Currently supported:
router's own ETH, whoever a `SETTLE` names as payer. So when a V4 step would router's own ETH, whoever a `SETTLE` names as payer. So when a V4 step would
take ETH from the user's wallet, ETH the router already holds pays first. The take ETH from the user's wallet, ETH the router already holds pays first. The
wallet pays only the rest. For example, an `UNWRAP_WETH` into the router wallet pays only the rest. For example, an `UNWRAP_WETH` into the router
leaves ETH there. When the decoder does not know how much ETH the router leaves ETH there. The decoder knows what the wallet pays only when it knows
holds, or how much the step would take, it counts the router as paying all of how much ETH the router holds and how much the step would take. Otherwise that
that ETH. A later step paid from the router spends what the router holds and payment reads as unknown, like any amount the decoder does not know. A later
takes nothing, and so does a V2 exact-in whose `amountIn` is `ALREADY_PAID`. A step paid from the router spends what the router holds and takes nothing, and
later take of `Token In` is added to the total while both are quantities: a V3 so does a V2 exact-in whose `amountIn` is `ALREADY_PAID`. A later take of
exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user, `Token In` is added to the total while both are quantities: a V3 exact-in of
read `1000.0000 USDT`. When the decoder does not know a take's amount, the 600 USDT and a V2 exact-in of 400 USDT, both paid by the user, read
total is not known and there is no `Amount` line. A `PERMIT2_PERMIT` names `1000.0000 USDT`. When the decoder does not know a take's amount, the total is
`Token In` only when its amount, `Unlimited` included, stands as the `Amount` not known and there is no `Amount` line. A `PERMIT2_PERMIT` names `Token In`
figure in place of the total, which it does only in the plain shape of only when its amount, `Unlimited` included, stands as the `Amount` figure in
Uniswap's own routes: it comes before the step that sets the input side, it is place of the total, which it does only in the plain shape of Uniswap's own
the only `PERMIT2_PERMIT` in the transaction and there is no routes: it comes before the step that sets the input side, it is the only
`PERMIT2_PERMIT_BATCH`, its spender is the router, the router may not let it `PERMIT2_PERMIT` in the transaction and there is no `PERMIT2_PERMIT_BATCH`,
fail, no step before it takes its token, a `PERMIT2_TRANSFER_FROM` of it its spender is the router, the router may not let it fail, no step before it
included, or a token the decoder does not know, and the decoder reads every takes its token, a `PERMIT2_TRANSFER_FROM` of it included, or a token the
step after it in full. Permit2 then lets the router take no more of the token decoder does not know, and the decoder reads every step after it in full.
than that amount, so the permit covers every take of it. A step before the Permit2 then lets the router take no more of the token than that amount, so
permit takes under whatever allowance the router already had, so it may take the permit covers every take of it. A step before the permit takes under
more than the permit's amount, which would then read lower than what leaves whatever allowance the router already had, so it may take more than the
the user's wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the router, permit's amount, which would then read lower than what leaves the user's
then a permit of 600 USDT to the router and a V3 exact-in of 2,000 USDT paid wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the router, then a permit
from the router, read `Amount 2000.0000 USDT`. A later permit or batch may of 600 USDT to the router and a V3 exact-in of 2,000 USDT paid from the
raise what Permit2 lets the router take, and so may a step the decoder does router, read `Amount 2000.0000 USDT`. A later permit or batch may raise what
not read in full: a command the decoder does not read (listed at the end of Permit2 lets the router take, and so may a step the decoder does not read in
this section), a step whose input it cannot read, in whole or in part, and a full: a command the decoder does not read (listed at the end of this section),
step it fails on. Every other `PERMIT2_PERMIT`, before the step that sets the a step whose input it cannot read, in whole or in part, and a step it fails
input side or after it, leaves `Token In` and the `Amount` line as they read on. Every other `PERMIT2_PERMIT`, before the step that sets the input side or
with no permit, unless it lets a spender other than the router take `Token In` after it, leaves `Token In` and the `Amount` line as they read with no permit,
(see below): a permit of 5,000 DAI to another address and then a V3 exact-in unless it lets a spender other than the router take `Token In` (see below): a
of 600 USDT read `Swap USDT → USDC` with `Amount 600.0000 USDT`, and so do permit of 5,000 DAI to another address and then a V3 exact-in of 600 USDT read
that exact-in and then a permit of 5,000 DAI to the router. When a permit's `Swap USDT → USDC` with `Amount 600.0000 USDT`, and so do that exact-in and
amount stands and the step that sets the input side spends a token other than then a permit of 5,000 DAI to the router. When a permit's amount stands and
the permit's, or one the decoder does not know, that step's amount gets lines the step that sets the input side spends a token other than the permit's, or
of its own, unless it states no amount and the user pays it none of its input one the decoder does not know, that step's amount gets lines of its own,
token. Each take the total does not count gives three lines after the `Amount` unless it states no amount and the user pays it none of its input token. Each
line (after `Token In` when there is none): one of a token other than take the total does not count gives three lines after the `Amount` line (after
`Token In`; one of `Token In` after an amount that is not a quantity (a V4 `Token In` when there is none): one of a token other than `Token In`; one of
open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line, or `Token In` after an amount that is not a quantity (a V4 open delta,
no stated amount, as of a V4 exact-out step); and one whose amount the decoder `ALREADY_PAID`, which keep their wording on the `Amount` line, or no stated
does not know. When there is no `Amount` line, each take the total counts gets amount, as of a V4 exact-out step); and one whose amount the decoder does not
them too, so that every amount the decoder reads is shown: the amount the step know. When there is no `Amount` line, each take the total counts gets them
that sets the input side states, when it is a quantity and the user pays that too, so that every amount the decoder reads is shown: the amount the step that
step (a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays sets the input side states, when it is a quantity and the user pays that step
some of its input token, or a `WRAP_ETH`, whose ETH comes from the (a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays some of
transaction's value), and each later take of `Token In` added to it. A step its input token, or a `WRAP_ETH`, whose ETH comes from the transaction's
that sets the input side and is paid from the router takes nothing from the value), and each later take of `Token In` added to it. A step that sets the
user's wallet and gets none. A `PERMIT2_PERMIT` the decoder cannot read, then input side and is paid from the router takes nothing from the user's wallet
V3 exact-ins of 600 USDT and 400 DAI paid by the user, give lines for both and gets none. A `PERMIT2_PERMIT` the decoder cannot read, then V3 exact-ins
amounts. When the step that sets the input side states no amount, the user of 600 USDT and 400 DAI paid by the user, give lines for both amounts. When
pays it some of its input token, and there is no `Amount` line, that step gets the step that sets the input side states no amount, the user pays it some of
them too whenever another take's lines give a figure, or a its input token, and there is no `Amount` line, that step gets them too
`PERMIT2_TRANSFER_FROM` of its token gives one, on the lines below or, into whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM` of
the router, on its `Paid to` line, so that figure cannot read as all that its token gives one, on the lines below or, into the router, on its `Paid to`
leaves. When it states no amount and the user pays it none of its input token, line, so that figure cannot read as all that leaves. When it states no amount
as when the router pays it, that step takes none of that token from the user's and the user pays it none of its input token, as when the router pays it, that
wallet and never gets them, with a permit or without. They come in step order, step takes none of that token from the user's wallet and never gets them, with
except that the step that sets the input side comes first when it has them, a permit or without. They come in step order, except that the step that sets
followed, when there is no `Amount` line, by the other takes the total counts: the input side comes first when it has them, followed, when there is no
`Moved from the sender`, its amount, worded as the `Amount` line words one, or `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; `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 `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 a `WRAP_ETH` that `Moved to`, which reads `The swap step that spends it`. For a `WRAP_ETH` that
+26 -16
View File
@@ -1406,8 +1406,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// from the router (see spendFromRouter()). A V4 SETTLE of ETH paid by // from the router (see spendFromRouter()). A V4 SETTLE of ETH paid by
// the user spends the router's ETH too (see the V4_SWAP branch). The // the user spends the router's ETH too (see the V4_SWAP branch). The
// value is the amount the router holds, in base units, when a // value is the amount the router holds, in base units, when a
// WRAP_ETH stated it, otherwise null. An UNWRAP_WETH into the router // WRAP_ETH stated it, otherwise null. A step that spends an unstated
// turns that amount of WETH into the same amount of ETH. // amount of the token from the router makes the value null. An
// UNWRAP_WETH into the router turns that amount of WETH into the same
// amount of ETH.
const heldByRouter = new Map(); const heldByRouter = new Map();
const isRouter = (recipient) => { const isRouter = (recipient) => {
const r = recipient.toLowerCase(); const r = recipient.toLowerCase();
@@ -1417,15 +1419,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity // own balance. CONTRACT_BALANCE pays all of it. Any other quantity
// counts against a known amount the router holds, and the steps pay // counts against a known amount the router holds, and the steps pay
// all of it once their amounts reach that one: a swap paid in ETH wraps // all of it once their amounts reach that one: a swap paid in ETH wraps
// what its swap steps then spend. An amount that is not a quantity, // what its swap steps then spend. A null amount is one the calldata
// such as the null decodeV4Swap() gives a SETTLE whose payment it // does not state, as for a V2 or V3 exact-out or a SETTLE whose
// cannot tell, or one of a token whose amount is not known, pays out // payment decodeV4Swap() cannot tell. A null amount makes the
// nothing this decoder can see. // router's amount of the token unknown. ALREADY_PAID spends nothing.
const spendFromRouter = (token, amount) => { const spendFromRouter = (token, amount) => {
const t = token.toLowerCase(); const t = token.toLowerCase();
const held = heldByRouter.get(t); const held = heldByRouter.get(t);
if (amount === CONTRACT_BALANCE) { if (amount === CONTRACT_BALANCE) {
heldByRouter.delete(t); heldByRouter.delete(t);
} else if (amount === null) {
if (heldByRouter.has(t)) heldByRouter.set(t, null);
} else if (typeof held === "bigint" && typeof amount === "bigint") { } else if (typeof held === "bigint" && typeof amount === "bigint") {
if (amount >= held) { if (amount >= held) {
heldByRouter.delete(t); heldByRouter.delete(t);
@@ -1681,8 +1685,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
) { ) {
// Buys exactly amountOut and spends at most amountInMax. // Buys exactly amountOut and spends at most amountInMax.
// Paid from the router, it spends an amount the calldata // Paid from the router, it spends an amount the calldata
// does not state, so it spends nothing spendFromRouter() // does not state (see spendFromRouter()).
// could count.
hasV2OrV3ExactOut = true; hasV2OrV3ExactOut = true;
const s = const s =
command === "V2_SWAP_EXACT_OUT" command === "V2_SWAP_EXACT_OUT"
@@ -1696,6 +1699,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
addUserPaid(s.tokenIn, s.amountInMax); addUserPaid(s.tokenIn, s.amountInMax);
} }
setOutput(i, s.tokenOut, s.amountOut, [s.recipient]); setOutput(i, s.tokenOut, s.amountOut, [s.recipient]);
if (!s.payerIsUser) {
spendFromRouter(s.tokenIn, null);
}
if (!isHop(i, s.tokenOut, s.recipient)) { if (!isHop(i, s.tokenOut, s.recipient)) {
addPayee(s.recipient, s.tokenOut); addPayee(s.recipient, s.tokenOut);
} }
@@ -1838,9 +1844,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// whoever a SETTLE names as payer. So ETH the router // whoever a SETTLE names as payer. So ETH the router
// holds from an earlier step pays first for the ETH // holds from an earlier step pays first for the ETH
// the user would pay the step. The user pays only the // the user would pay the step. The user pays only the
// rest. When this decoder does not know how much ETH // rest. This decoder knows the rest only when it knows
// the router holds, or how much the user would pay, // how much ETH the router holds and how much the user
// it counts the router as paying all of that ETH. // would pay. Otherwise the rest is unknown.
for (const p of v4.paidByUser) { for (const p of v4.paidByUser) {
if ( if (
p.currency !== p.currency !==
@@ -1852,10 +1858,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} }
const held = heldByRouter.get(p.currency); const held = heldByRouter.get(p.currency);
if ( if (
typeof held === "bigint" && typeof held !== "bigint" ||
typeof p.amount === "bigint" && typeof p.amount !== "bigint"
p.amount > held
) { ) {
paidByUser.push({
currency: p.currency,
amount: null,
});
} else if (p.amount > held) {
paidByUser.push({ paidByUser.push({
currency: p.currency, currency: p.currency,
amount: p.amount - held, amount: p.amount - held,
@@ -2320,8 +2330,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 there, and MSG_SENDER // that spends it. A take with a recipient goes to that recipient;
// there is the sender. // `MSG_SENDER` means the sender.
for (const s of spentFromSender) { for (const s of spentFromSender) {
details.push({ details.push({
label: "Moved from the sender", label: "Moved from the sender",
+152 -15
View File
@@ -2474,8 +2474,8 @@ describe("uniswap decoder", () => {
// from the router's own ETH, whoever a SETTLE names as payer. ETH the // from the router's own ETH, whoever a SETTLE names as payer. ETH the
// router holds from an UNWRAP_WETH into it pays first, so the sender // router holds from an UNWRAP_WETH into it pays first, so the sender
// pays only the ETH beyond it. On next each row also took the V4 // pays only the ETH beyond it. On next each row also took the V4
// step's whole ETH from the sender: 1 ETH more in the first two, and // step's whole ETH from the sender: 1 ETH more in the first, and 1.5
// 1.5 ETH in the last. // ETH in the last.
const unwrapIntoRouter = encodeWrapEth(ROUTER_ADDR, 0n); // UNWRAP_WETH, same encoding const unwrapIntoRouter = encodeWrapEth(ROUTER_ADDR, 0n); // UNWRAP_WETH, same encoding
// A V4 step swapping `amount` of ETH for USDC, with a SETTLE of // A V4 step swapping `amount` of ETH for USDC, with a SETTLE of
// OPEN_DELTA by the user. // OPEN_DELTA by the user.
@@ -2493,19 +2493,6 @@ describe("uniswap decoder", () => {
], ],
); );
test.each([ test.each([
{
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(ROUTER_ADDR, USDT_600, 1n, [
USDT_ADDR,
WETH_ADDR,
]),
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [["Amount", "600.0000 USDT"]],
},
{ {
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", 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], commands: [0x0b, 0x0c, 0x10],
@@ -2531,6 +2518,156 @@ describe("uniswap decoder", () => {
expect(linesBetweenTokens(result)).toEqual(lines); expect(linesBetweenTokens(result)).toEqual(lines);
}); });
// The lines for ETH the sender pays a V4 step, of an amount this
// decoder does not know.
const unknownEthLines = [
["Moved from the sender", UNDECODED],
["Token moved", "ETH"],
["Moved to", "The swap step that spends it"],
];
// https://git.eeqj.de/sneak/AutistMask/issues/665 — this decoder may
// not know how much ETH the router holds, or how much ETH the user
// pays a V4 step. The ETH the sender pays that step then reads as
// unknown. Counting the router as paying all of it would hide ETH
// that can leave the wallet.
test.each([
{
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(ROUTER_ADDR, USDT_600, 1n, [
USDT_ADDR,
WETH_ADDR,
]),
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [["Amount", "600.0000 USDT"], ...unknownEthLines],
},
{
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, ROUTER_ADDR, 0n),
],
),
v4EthForUsdc(5n * ONE_ETH),
],
lines: [["Amount", "600.0000 USDT"], ...unknownEthLines],
},
{
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],
inputs: [
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
unwrapIntoRouter,
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"),
...unknownEthLines,
],
},
])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
});
// https://git.eeqj.de/sneak/AutistMask/issues/665 — a step that spends
// an unstated amount of a token from the router makes the router's
// amount of that token unknown. In each row such a step spends the
// WETH of a WRAP_ETH of 1 ETH into the router. The UNWRAP_WETH then
// leaves an unknown amount of ETH there, so the ETH the sender pays
// the last step reads as unknown.
test.each([
{
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: [
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
encodeV3SwapExactOut(
USER_ADDR,
390000000n,
ONE_ETH,
[USDC_ADDR, WETH_ADDR],
false,
),
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [
["Moved from the sender", "Up to 1.0000 ETH"],
["Token moved", "ETH"],
["Moved to", "The swap step that spends it"],
...unknownEthLines,
],
},
{
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: [
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
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),
],
),
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [
...wrapLines("The swap step that spends it"),
...unknownEthLines,
],
},
])("$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) =>