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) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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 249 additions and 300 deletions
+81 -77
View File
@@ -1265,9 +1265,7 @@ 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. 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 <amount>` (see Transaction Decoding).
its `amountOut`, the exact amount it buys.
- `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
@@ -3416,18 +3414,23 @@ Currently supported:
`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. A V2 or V3 exact-out step paid
from the router spends an amount the calldata does not state, at most its
`amountInMax`, so the decoder counts it as spending nothing the router holds.
A `SETTLE` of `OPEN_DELTA` (zero), and a `SETTLE_ALL`, pay only what the step
still owes of their token when they run. The decoder takes that to be the
amounts the step's swap actions state they spend of that token only when every
action in 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`, `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 quantity together; otherwise it does not know the amount, and such a
`SETTLE` from the router counts as spending nothing. So the rest of a V4
`amountInMax`. The decoder then no longer knows how much of the step's input
token the router holds. A V4 `SETTLE` from the router has the same effect when
the decoder does not know what that `SETTLE` pays (see below). An
`UNWRAP_WETH` into the router turns all the router's WETH into the same amount
of ETH. The decoder knows how much ETH that is only when it knew how much WETH
the router held and the router held no ETH before. A V4 `SETTLE` of ETH always
spends the router's own ETH, whichever payer it names (see below). A `SETTLE`
of `OPEN_DELTA` (zero), and a `SETTLE_ALL`, pay only what the step still owes
of their token when they run. The decoder takes that to be the amounts the
step's swap actions state they spend of that token only when every action in
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`,
`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
quantity together; otherwise it does not know the amount. So the rest of a V4
`SETTLE` of more than its swap spends, taken to the router, the WETH of a
`WRAP_ETH`, and a token a swap pays the router on the way to the output, each
stays in the router when later steps do not spend all of it, where anyone can
@@ -3479,48 +3482,51 @@ 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`. 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
rest taken to another address, reads `5000.0000 USDT`. V4 pays ETH from the
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
wallet pays only the rest. For example, an `UNWRAP_WETH` into the router
leaves ETH there. The decoder knows what the wallet pays only when it knows
how much ETH the router holds and how much the step would take. Otherwise that
payment reads as unknown, like any amount the decoder does not know. 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
@@ -3542,26 +3548,24 @@ Currently supported:
its amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder does not know it;
`Token moved`, giving the token's address as the `Token In` line does; and
`Moved to`, which reads `The swap step that spends it`. For ETH that the user
pays a V4 step when the router may hold ETH, `Moved from the sender` reads
`Up to <amount>` 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
`Moved to`, which reads `The swap step that spends it`. 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
+92 -62
View File
@@ -1340,18 +1340,15 @@ 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, 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.
// 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).
const userPaid = [];
// 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
// V2 exact-in of ALREADY_PAID takes nothing.
const addUserPaid = (token, amount, atMost = false) => {
if (amount !== ALREADY_PAID) {
userPaid.push({ token, amount, atMost });
}
// 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 });
};
// 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
@@ -1406,9 +1403,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// token, an UNWRAP_WETH of WETH, a WRAP_ETH of CONTRACT_BALANCE of
// ETH, a TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION or
// ACROSS_V4_DEPOSIT_V3 of all of it, or a swap step or V4 SETTLE paid
// from the router (see spendFromRouter()). The value is the amount the
// router holds, in base units, when a WRAP_ETH stated it, otherwise
// null.
// 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
// value is the amount the router holds, in base units, when a
// WRAP_ETH stated it, otherwise null. A step that spends an unstated
// 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 isRouter = (recipient) => {
const r = recipient.toLowerCase();
@@ -1416,17 +1417,19 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
};
// A swap step or V4 SETTLE pays `amount` of `token` from the router's
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity
// counts against the amount a WRAP_ETH stated, and the steps pay 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,
// such as the null decodeV4Swap() gives a SETTLE whose payment it
// cannot tell, or one of a token whose amount is not known, pays out
// nothing this decoder can see.
// 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
// what its swap steps then spend. A null amount is one the calldata
// does not state, as for a V2 or V3 exact-out or a SETTLE whose
// payment decodeV4Swap() cannot tell. A null amount makes the
// router's amount of the token unknown. ALREADY_PAID spends nothing.
const spendFromRouter = (token, amount) => {
const t = token.toLowerCase();
const held = heldByRouter.get(t);
if (amount === CONTRACT_BALANCE) {
heldByRouter.delete(t);
} else if (amount === null) {
if (heldByRouter.has(t)) heldByRouter.set(t, null);
} else if (typeof held === "bigint" && typeof amount === "bigint") {
if (amount >= held) {
heldByRouter.delete(t);
@@ -1682,8 +1685,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
) {
// Buys exactly amountOut and spends at most amountInMax.
// Paid from the router, it spends an amount the calldata
// does not state, so it spends nothing spendFromRouter()
// could count.
// does not state (see spendFromRouter()).
hasV2OrV3ExactOut = true;
const s =
command === "V2_SWAP_EXACT_OUT"
@@ -1697,6 +1699,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
addUserPaid(s.tokenIn, s.amountInMax);
}
setOutput(i, s.tokenOut, s.amountOut, [s.recipient]);
if (!s.payerIsUser) {
spendFromRouter(s.tokenIn, null);
}
if (!isHop(i, s.tokenOut, s.recipient)) {
addPayee(s.recipient, s.tokenOut);
}
@@ -1825,20 +1830,49 @@ 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",
);
// What the user pays the step, as decodeV4Swap() gives
// it, less the ETH the router already holds (below).
// Empty when the step cannot be read.
const paidByUser = [];
if (v4) {
// Each SETTLE paid from the router spends the amount
// decodeV4Swap() says it pays.
for (const s of v4.settledFromRouter) {
spendFromRouter(s.currency, s.amount);
}
// V4Router pays ETH from the router's own ETH,
// whoever a SETTLE names as payer. So ETH the router
// holds from an earlier step pays first for the ETH
// the user would pay the step. The user pays only the
// rest. This decoder knows the rest only when it knows
// how much ETH the router holds and how much the user
// would pay. Otherwise the rest is unknown.
for (const p of v4.paidByUser) {
if (
p.currency !==
"0x0000000000000000000000000000000000000000" ||
!heldByRouter.has(p.currency)
) {
paidByUser.push(p);
continue;
}
const held = heldByRouter.get(p.currency);
if (
typeof held !== "bigint" ||
typeof p.amount !== "bigint"
) {
paidByUser.push({
currency: p.currency,
amount: null,
});
} else if (p.amount > held) {
paidByUser.push({
currency: p.currency,
amount: p.amount - held,
});
}
spendFromRouter(p.currency, p.amount);
}
for (const t of v4.takes) {
if (!isHop(i, t.currency, t.recipient)) {
addPayee(t.recipient, t.currency);
@@ -1863,10 +1897,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
commandNames[i] += " (could not be decoded)";
}
// The step's amount, if it sets the input side: what the
// user pays it of its input currency (see
// decodeV4Swap()), or, when the user pays none of it,
// what its swap actions spend of it, whoever pays that.
const paidIn = v4?.paidByUser.find(
// user pays it of its input currency (see paidByUser),
// or, when the user pays none of it, what its swap
// actions spend of it, whoever pays that.
const paidIn = paidByUser.find(
(p) => p.currency === v4.tokenIn,
);
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
@@ -1920,17 +1954,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// router still runs it.
const setInputHere = inputEstablished && !inputSetBefore;
if (setInputHere) inputPaidByUser = paidIn !== undefined;
if (v4) {
for (const p of v4.paidByUser) {
if (setInputHere && p === paidIn) continue;
addUserPaid(
p.currency,
p.amount,
routerMayHoldEth &&
p.currency ===
"0x0000000000000000000000000000000000000000",
);
}
for (const p of paidByUser) {
if (setInputHere && p === paidIn) continue;
addUserPaid(p.currency, p.amount);
}
if (!v4 || v4.settleUnread) {
spentUnread();
@@ -1978,15 +2004,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
addPayee(u.recipient);
// It pays out all the router's WETH, as ETH. Into the
// router, the router then holds as ETH the WETH a
// step paid it.
const heldWeth = WETH_ADDRESSES.some((w) =>
// step paid it. That amount is known when the router
// held a known amount of WETH and no ETH before.
const heldWeth = WETH_ADDRESSES.filter((w) =>
heldByRouter.has(w),
);
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
if (isRouter(u.recipient) && heldWeth) {
const ethAmount =
heldWeth.length === 1 &&
!heldByRouter.has(
"0x0000000000000000000000000000000000000000",
)
? heldByRouter.get(heldWeth[0])
: null;
heldWeth.forEach((w) => heldByRouter.delete(w));
if (isRouter(u.recipient) && heldWeth.length > 0) {
heldByRouter.set(
"0x0000000000000000000000000000000000000000",
null,
ethAmount,
);
}
} else {
@@ -2040,8 +2074,7 @@ 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. A take of at most its amount (see
// userPaid) is never added, and gets lines of its own too.
// total unknown, with no figure.
//
// 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
@@ -2076,10 +2109,10 @@ 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, 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).
// { token, amount, recipient }. A null token is a step this decoder
// did not read (see spentUnread()), or a token it does not know. 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, in
@@ -2125,7 +2158,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (ofTokenIn && permitStands) continue;
if (
ofTokenIn &&
!t.atMost &&
typeof total === "bigint" &&
typeof t.amount === "bigint"
) {
@@ -2230,9 +2262,8 @@ 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. `atMost` says the amount is a maximum (see
// userPaid).
const spentText = (amount, info, atMost = false) => {
// compared with one.
const spentText = (amount, info) => {
if (amount === OPEN_DELTA) {
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
}
@@ -2245,7 +2276,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (amount >= MAX_UINT160) {
return { raw: "Unlimited", display: "Unlimited" };
}
if (hasV2OrV3ExactOut || atMost) {
if (hasV2OrV3ExactOut) {
// 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
@@ -2305,8 +2336,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
details.push({
label: "Moved from the sender",
value: present(s.amount)
? spentText(s.amount, tokenInfo(s.token, sources), s.atMost)
.display
? spentText(s.amount, tokenInfo(s.token, sources)).display
: UNDECODED_CURRENCY,
});
if (present(s.token)) {
+76 -161
View File
@@ -1428,8 +1428,8 @@ describe("uniswap decoder", () => {
];
// execute() of these command bytes and inputs, decoded as sent by
// the user to `router`.
function decodeSteps(commands, inputs, router = ROUTER_ADDR) {
// the user.
function decodeSteps(commands, inputs) {
const data = buildExecute(
solidityPacked(
commands.map(() => "uint8"),
@@ -1438,7 +1438,7 @@ describe("uniswap decoder", () => {
inputs,
9999999999n,
);
return uniswap.decode(data, router, "0x1", {}, USER_ADDR);
return uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
}
// The first step: a V3 exact-in of 600 USDT for USDC, paid by the
@@ -2470,20 +2470,13 @@ describe("uniswap decoder", () => {
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);
// https://git.eeqj.de/sneak/AutistMask/issues/665 — V4Router pays ETH
// 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
// 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, and 1.5
// ETH in the last.
const unwrapIntoRouter = encodeWrapEth(ROUTER_ADDR, 0n); // UNWRAP_WETH, same encoding
// A V4 step swapping `amount` of ETH for USDC, with a SETTLE of
// OPEN_DELTA by the user.
const v4EthForUsdc = (amount) =>
@@ -2499,147 +2492,58 @@ describe("uniswap decoder", () => {
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"),
inputs: [
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [["Amount", "1.0000 ETH"]],
},
{
kind: "the same with a TRANSFER of 1 ETH to another address after the UNWRAP_WETH",
commands: [0x0b, 0x0c, 0x05, 0x10],
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1.5 ETH that the user settles",
commands: [0x0b, 0x0c, 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"),
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
unwrapIntoRouter,
v4EthForUsdc((ONE_ETH * 3n) / 2n),
],
lines: [["Amount", "1.5000 ETH"]],
},
])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
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(ADDRESS_THIS, USDT_600, 1n, [
encodeV3SwapExactIn(ROUTER_ADDR, USDT_600, 1n, [
USDT_ADDR,
WETH_ADDR,
]),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 1.0000 ETH"),
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",
@@ -2659,22 +2563,19 @@ describe("uniswap decoder", () => {
1n,
),
settle(USDT_ADDR, 0n, true),
take(ZERO_ADDR, ADDRESS_THIS, 0n),
take(ZERO_ADDR, ROUTER_ADDR, 0n),
],
),
v4EthForUsdc(5n * ONE_ETH),
],
lines: [
["Amount", "600.0000 USDT"],
...ethLines("Up to 5.0000 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, 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],
inputs: [
WRAP_INTO_ROUTER,
UNWRAP_INTO_ROUTER,
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
unwrapIntoRouter,
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
@@ -2695,14 +2596,26 @@ describe("uniswap decoder", () => {
],
lines: [
...wrapLines("The swap step that spends it"),
...ethLines(UNDECODED),
...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: [
WRAP_INTO_ROUTER,
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
encodeV3SwapExactOut(
USER_ADDR,
390000000n,
@@ -2710,19 +2623,21 @@ describe("uniswap decoder", () => {
[USDC_ADDR, WETH_ADDR],
false,
),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [
["Amount", "Up to 1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
["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: [
WRAP_INTO_ROUTER,
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
@@ -2740,16 +2655,16 @@ describe("uniswap decoder", () => {
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
UNWRAP_INTO_ROUTER,
V4_ONE_ETH,
unwrapIntoRouter,
v4EthForUsdc(ONE_ETH),
],
lines: [
["Amount", "1.0000 ETH"],
...ethLines("Up to 1.0000 ETH"),
...wrapLines("The swap step that spends it"),
...unknownEthLines,
],
},
])("$kind", ({ router, commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs, router);
])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
});