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
This commit is contained in:
2026-10-27 01:01:21 +00:00
parent 33ab99b7be
commit 88cee51f5e
3 changed files with 451 additions and 112 deletions
+88 -74
View File
@@ -3414,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
@@ -3477,70 +3482,79 @@ Currently supported:
of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the
rest taken to another address, reads `5000.0000 USDT`. A later step paid from
the router spends what the router holds and takes nothing, and so does a V2
exact-in whose `amountIn` is `ALREADY_PAID`. A later take of `Token In` is
added to the total while both are quantities: a V3 exact-in of 600 USDT and a
V2 exact-in of 400 USDT, both paid by the user, read `1000.0000 USDT`. When
the decoder does not know a take's amount, the total is not known and there is
no `Amount` line. A `PERMIT2_PERMIT` names `Token In` only when its amount,
`Unlimited` included, stands as the `Amount` figure in place of the total,
which it does only in the plain shape of Uniswap's own routes: it comes before
the step that sets the input side, it is the only `PERMIT2_PERMIT` in the
transaction and there is no `PERMIT2_PERMIT_BATCH`, its spender is the router,
the router may not let it fail, no step before it takes its token, a
`PERMIT2_TRANSFER_FROM` of it included, or a token the decoder does not know,
and the decoder reads every step after it in full. Permit2 then lets the
router take no more of the token than that amount, so the permit covers every
take of it. A step before the permit takes under whatever allowance the router
already had, so it may take more than the permit's amount, which would then
read lower than what leaves the user's wallet: a `PERMIT2_TRANSFER_FROM` of
2,000 USDT into the router, then a permit of 600 USDT to the router and a V3
exact-in of 2,000 USDT paid from the router, read `Amount 2000.0000 USDT`. A
later permit or batch may raise what Permit2 lets the router take, and so may
a step the decoder does not read in full: a command the decoder does not read
(listed at the end of this section), a step whose input it cannot read, in
whole or in part, and a step it fails on. Every other `PERMIT2_PERMIT`, before
the step that sets the input side or after it, leaves `Token In` and the
`Amount` line as they read with no permit, unless it lets a spender other than
the router take `Token In` (see below): a permit of 5,000 DAI to another
address and then a V3 exact-in of 600 USDT read `Swap USDT → USDC` with
`Amount 600.0000 USDT`, and so do that exact-in and then a permit of 5,000 DAI
to the router. When a permit's amount stands and the step that sets the input
side spends a token other than the permit's, or one the decoder does not know,
that step's amount gets lines of its own, unless it states no amount and the
user pays it none of its input token. Each take the total does not count gives
three lines after the `Amount` line (after `Token In` when there is none): one
of a token other than `Token In`; one of `Token In` after an amount that is
not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on
the `Amount` line, or no stated amount, as of a V4 exact-out step); and one
whose amount the decoder does not know. When there is no `Amount` line, each
take the total counts gets them too, so that every amount the decoder reads is
shown: the amount the step that sets the input side states, when it is a
quantity and the user pays that step (a V2 or V3 step whose `payerIsUser` is
true, a V4 step the user pays some of its input token, or a `WRAP_ETH`, whose
ETH comes from the transaction's value), and each later take of `Token In`
added to it. A step that sets the input side and is paid from the router takes
nothing from the user's wallet and gets none. A `PERMIT2_PERMIT` the decoder
cannot read, then V3 exact-ins of 600 USDT and 400 DAI paid by the user, give
lines for both amounts. When the step that sets the input side states no
amount, the user pays it some of its input token, and there is no `Amount`
line, that step gets them too whenever another take's lines give a figure, or
a `PERMIT2_TRANSFER_FROM` of its token gives one, on the lines below or, into
the router, on its `Paid to` line, so that figure cannot read as all that
leaves. When it states no amount and the user pays it none of its input token,
as when the router pays it, that step takes none of that token from the user's
wallet and never gets them, with a permit or without. They come in step order,
except that the step that sets the input side comes first when it has them,
followed, when there is no `Amount` line, by the other takes the total counts:
`Moved from the sender`, its amount, worded as the `Amount` line words one, or
rest taken to another address, reads `5000.0000 USDT`. V4 pays 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
its input token, or a `WRAP_ETH`, whose ETH comes from the transaction's
value), and each later take of `Token In` added to it. A step that sets the
input side and is paid from the router takes nothing from the user's wallet
and gets none. A `PERMIT2_PERMIT` the decoder cannot read, then V3 exact-ins
of 600 USDT and 400 DAI paid by the user, give lines for both amounts. When
the step that sets the input side states no amount, the user pays it some of
its input token, and there is no `Amount` line, that step gets them too
whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM` of
its token gives one, on the lines below or, into the router, on its `Paid to`
line, so that figure cannot read as all that leaves. When it states no amount
and the user pays it none of its input token, as when the router pays it, that
step takes none of that token from the user's wallet and never gets them, with
a permit or without. They come in step order, except that the step that sets
the input side comes first when it has them, followed, when there is no
`Amount` line, by the other takes the total counts: `Moved from the sender`,
its amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder does not know it;
`Token moved`, giving the token's address as the `Token In` line does; and
`Moved to`, which reads `The swap step that spends it`. A token the decoder
does not know gives the `Moved from the sender` line alone. A step may also
take from the user's wallet what the decoder did not read: a V4 step whose
input or one of whose `SETTLE`s or `SETTLE_ALL`s it cannot read, which the
router still runs; a V2 or V3 exact-in or exact-out step or a
`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;