fix: a swap split across steps the user pays for shows every amount it spends (closes #625)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

The swap's Amount line gave only the first swap step's amount. It now
gives the total the steps take of Token In: the amount of the step that
sets the input side plus each later take of it by a step the user pays.
A take of another token, one after an amount that is not a quantity, or
one of unknown amount gets lines of its own. A step that may take what
the decoder did not read, such as a V3_SWAP_EXACT_OUT or an
EXECUTE_SUB_PLAN, gets one line saying so and leaves no figure. A
PERMIT2_PERMIT names Token In; its amount stands only in the plain shape
of Uniswap's own routes.

Model: opus-5-5
This commit is contained in:
2026-10-20 04:15:32 +00:00
committed by sneak
parent ca9cd438d9
commit cad2d262ce
4 changed files with 1364 additions and 130 deletions
+102 -45
View File
@@ -1190,14 +1190,8 @@ number. They can read:
`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. The line shows the
amount of the step that sets the input side: the last `PERMIT2_PERMIT` step if
there is one, otherwise the first V2 or V3 exact-in, V2 exact-out, `WRAP_ETH`
or V4 swap step that names an input token or amount. A V4 step with a swap
action sets the input side when no earlier step set it, even when it names
neither, as described below. A V2 exact-out step gives its `amountInMax`, and
a V4 swap step the `amountIn` of the swap action that spends its input
currency.
whole open delta", so the calldata states no quantity. Transaction Decoding
below gives the steps whose amounts the line shows.
- `Whatever an earlier step sent to the pair (V2 already paid)`: the swap's
`Amount` line, when the amount it shows is a V2 exact-in `amountIn` of zero.
The router reads that zero as "the pair already holds the input tokens": the
@@ -2379,10 +2373,11 @@ view would leave a wallet one click from deletion.
- Site origin (bold, scheme and port included) + "wants to send a
transaction"
- Decoded action (if calldata is recognized): action name, token details,
amounts, what each `PERMIT2_TRANSFER_FROM` moves and where to, steps,
deadline, and unless a swap pays only the From address, a `Paid to` line
for each other address it pays, or saying that a token stays in the router
or who it pays is unknown (see Transaction Decoding)
amounts, what each swap step spends that the `Amount` line does not give,
what each `PERMIT2_TRANSFER_FROM` moves and where to, steps, deadline, and
unless a swap pays only the From address, a `Paid to` line for each other
address it pays, or saying that a token stays in the router or who it pays
is unknown (see Transaction Decoding)
- From: color dot + full address + etherscan link
- Contract: color dot + full address + etherscan link, token symbol label if
known; for a contract creation, the same sentence as on WaitTx
@@ -3090,37 +3085,40 @@ Currently supported:
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
`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. The router pays a `SETTLE` of
`OPEN_DELTA` (zero) only what its step still owes of that token when the
`SETTLE` runs, so one from the router counts as spending 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`
is the step's only `SETTLE` or `SETTLE_ALL` of that token and comes after
every swap action, no swap action buys that token, and the swap actions that
spend it state a quantity together; otherwise it counts as spending nothing.
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 take it in a later transaction. The router sends ETH
as written, so a `TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and
pays out nothing. A step paying a token to the V2 pair that the very next step
spends, as a V2 exact-in of that token whose `amountIn` is `ALREADY_PAID`
(zero) or `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes
pay the next V2 pair directly, and the router enforces that swap's own
minimum. The decoder checks that the address paid is that pair, deriving it
from the V2 factory and pair init code hash the network's routers were
deployed with and the first two tokens of that step's path; a payment to any
other address is named. (Sepolia's `UniversalRouterV1_2_V2Support` was
deployed with placeholders for both values, so no V2 swap step can succeed on
that router.) Every other payment counts, in whichever step and of whatever
token, a share of the output included: an interface fee paid to another
address by `PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the
minimum reads `Min. paid out`, because the swap's minimum includes the fee.
The minimum reads `Min. received` only when some step pays the transaction's
sender (the From address on TxApproval, the sender the explorer reports on
TransactionDetail) and no `Paid to` line is shown. `Paid to` lines after
`Token Out` name each other address paid; one reads
WETH from it reads as leaving the WETH there. 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 `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 take it in a
later transaction. The router sends ETH as written, so a `TRANSFER` of
`CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A step paying a
token to the V2 pair that the very next step spends, as a V2 exact-in of that
token whose `amountIn` is `ALREADY_PAID` (zero) or `CONTRACT_BALANCE`, is a
hop, not a payout: Uniswap's mixed routes pay the next V2 pair directly, and
the router enforces that swap's own minimum. The decoder checks that the
address paid is that pair, deriving it from the V2 factory and pair init code
hash the network's routers were deployed with and the first two tokens of that
step's path; a payment to any other address is named. (Sepolia's
`UniversalRouterV1_2_V2Support` was deployed with placeholders for both
values, so no V2 swap step can succeed on that router.) Every other payment
counts, in whichever step and of whatever token, a share of the output
included: an interface fee paid to another address by `PAY_PORTION`,
`TRANSFER` or `TAKE_PORTION` is named, and the minimum reads `Min. paid out`,
because the swap's minimum includes the fee. The minimum reads `Min. received`
only when some step pays the transaction's sender (the From address on
TxApproval, the sender the explorer reports on TransactionDetail) and no
`Paid to` line is shown. `Paid to` lines after `Token Out` name each other
address paid; one reads
`The bridge, which pays the deposit's recipient on another network` for a
bridge deposit; one reads, for example,
`The router, 2000.0000 USDT from the sender (later steps may not spend all of it)`
@@ -3132,8 +3130,67 @@ Currently supported:
name); and one more reads `Unknown (could not be decoded)` when a step that
may pay an address went unread. When no step pays anyone, a single line reads
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
minimum reads `Min. paid out`. After the `Amount` line (after `Token In` when
there is none), each `PERMIT2_TRANSFER_FROM` but one into the router, in step
minimum reads `Min. paid out`. The `Amount` line gives the total the steps
take of `Token In`, from the step that sets the input side on: the first V2 or
V3 exact-in, V2 exact-out, `WRAP_ETH` or V4 swap step that names an input
token or amount. A V4 step with a swap action sets the input side when no
earlier step set it, even when it names neither (see Display Consistency).
That step counts its own amount, whoever pays it: a V2 exact-out step its
`amountInMax`, and a V4 swap step the `amountIn` of the swap action that
spends its input currency. Each later V2 or V3 exact-in or V2 exact-out step
whose `payerIsUser` is true takes its amount, found in the same way, from the
user's wallet. So does every V4 step but the one that sets the input side,
with a swap action or without: it takes what its `SETTLE`s paid by the user
(`payerIsUser` true) and its `SETTLE_ALL`s pay, for each token on its own. A
`SETTLE` 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. 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. `Token In` is the token of the last
`PERMIT2_PERMIT` the decoder reads, when there is one. Its amount, `Unlimited`
included, stands as the `Amount` figure in place of the total only in the
plain shape of Uniswap's own routes: it is the only `PERMIT2_PERMIT`, its
spender is the router, the router may not let it fail, no step before it takes
its token 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 the decoder
does not read in full may raise what Permit2 lets the router take, as a
`PERMIT2_PERMIT_BATCH` can: 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. In any other case the line gives the total, as with no
permit. When the step that sets the input side spends a token other than the
permit's, or one the decoder does not know, its amount gets lines of its own
and the total has no figure to start from: unless the permit's amount stands
there is no `Amount` line, and each take of the permit's token gets lines of
its own. 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 the step that sets the input side states no amount and
there is no `Amount` line, that step gets them too whenever another take's
lines give a figure, so that figure cannot read as all that leaves. They come
in step order, the step that sets the input side first when it has them:
`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, other
than the one that sets the input side, 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 V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input it
cannot read; every `PERMIT2_TRANSFER_FROM_BATCH`; every `V3_SWAP_EXACT_OUT`,
which the decoder does not read, wherever it stands; 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