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 took only the first swap step's amount. Each later
V2, V3 or V4 swap step the user pays for now adds what it spends of
Token In to that line, and one that spends another token gets Moved from
the sender, Token moved and Moved to lines of its own. Steps paid from
the router add nothing, and a PERMIT2_PERMIT keeps its own amount.

Model: opus-5-5
This commit is contained in:
2026-10-19 22:53:31 +00:00
committed by sneak
parent 5376723c41
commit 8382fab3b6
4 changed files with 365 additions and 64 deletions
+49 -30
View File
@@ -1189,14 +1189,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
@@ -2374,10 +2368,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 the user pays for spends of a token other
than `Token In`, 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
@@ -3127,24 +3122,48 @@ 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
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
`Token moved into the router` line already give the amount, the token and
where it goes, and so its amount is shown once. These lines change neither the
`Paid to` lines nor the minimum's label. A step goes unread when its recipient
does not decode as an address, or when it is a command the decoder does not
read: every command other than `PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`,
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`,
`SWEEP`, `TRANSFER`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`,
`ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
(`V3_SWAP_EXACT_OUT`, `PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and
the NFT commands among them), or a type its release names no command for.
minimum reads `Min. paid out`. The `Amount` line gives 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 (see
Display Consistency). A V2 exact-out step gives its `amountInMax`, and a V4
swap step the `amountIn` of the swap action that spends its input currency.
Each later swap step that the user pays for also takes its amount, found in
the same way, from the user's wallet: a V2 or V3 exact-in or V2 exact-out step
whose `payerIsUser` is true, or a V4 step with a swap action whose input
currency a `SETTLE` paid by the user (`payerIsUser` true) or a `SETTLE_ALL`
names. When it spends `Token In`, its amount is added to the line's, unless a
`PERMIT2_PERMIT` set the input side, whose amount the line keeps. So 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 either amount is not a quantity (a V4 open delta,
`ALREADY_PAID`, or an amount the decoder did not read), the total is not known
and there is no `Amount` line. A step paid from the router spends what the
router holds and adds nothing, and so does a V2 exact-in whose `amountIn` is
`ALREADY_PAID`, which takes nothing from the user. A step the user pays for
that spends a token other than `Token In` gives three lines after the `Amount`
line (after `Token In` when there is none), in step order:
`Moved from the sender`, its amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder read none; `Token moved`,
giving the token's address as the `Token In` line does; and `Moved to`, which
reads `The swap step that spends it`. 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 `Token moved into the router` line already give
the amount, the token and where it goes, and so its amount is shown once.
These lines change neither the `Paid to` lines nor the minimum's label. A step
goes unread when its recipient does not decode as an address, or when it is a
command the decoder does not read: every command other than `PERMIT2_PERMIT`,
`PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`,
`PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`,
`UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`,
`PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and the NFT commands among
them), or a type its release names no command for.
### Non-Goals Forever