fix: the swap decoder reads who the router's transfer commands pay (closes #599)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION (0x07, on the routers with
the 0x7f mask), PERMIT2_TRANSFER_FROM and V4 TAKE_PORTION were payouts the
decoder did not read, so a swap using one read Min. paid out with an unknown
payee. Each recipient is now read under the rule swap steps follow: the
sender, the router, a hop into the next V2 pair, or another address, which is
named. Paying all the router holds of a token pays it out as SWEEP does; a
part leaves the rest in the router. UniversalRouterV1 pays a
PERMIT2_TRANSFER_FROM to address(1) or address(2) as written. 0x07 and
ACROSS_V4_DEPOSIT_V3 (0x40) are named in Steps; the deposit stays unread.

Model: opus-5-5
This commit is contained in:
2026-10-19 09:56:19 +00:00
parent 9c67e87ae5
commit 46a39fd96b
4 changed files with 539 additions and 101 deletions
+48 -35
View File
@@ -2955,45 +2955,58 @@ Currently supported:
address uses: `0x1f` for `UniversalRouterV1`, `0x3f` for
`UniversalRouterV1_2_V2Support` and `UniversalRouterV2`, and `0x7f` for
`UniversalRouterV2_1_1` and `UniversalRouterV2_1_2`. The allow-revert flag
(`0x80`) therefore does not change a step's name. The decoder does not read
the commands inside an `EXECUTE_SUB_PLAN` (`0x21`) step, and wherever that
step stands they may spend the user's tokens or pay the user. That step is
listed as `Execute Sub-Plan (its steps are not shown)`, and whatever the other
steps state, the summary names no swap direction, `Token In` and `Token Out`
read `Unknown (could not be decoded)`, and there is no `Amount` or
`Min. received` line. Each swap step, `SWEEP`, `WRAP_ETH`, `UNWRAP_WETH` and
V4 `TAKE` names the address it pays, and `TAKE_ALL` pays the transaction's
sender. The router reads `address(1)` as the transaction's sender, and
`address(2)` as the router itself, which holds what it is paid until a later
step pays it out; a payment to the router, as `address(2)` or by its own
address, is not counted as paid out. When a swap step or `TAKE` pays the
router the swap's output token, the output stays in the router unless a later
`SWEEP` of that token, or a later `UNWRAP_WETH` when it is WETH, pays it out.
A swap step or `TAKE` 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. 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), no step pays any other address, no step that
may pay an address went unread, and the output does not stay in the router.
Otherwise it reads `Min. paid out`, a `Paid to` line after `Token Out` names
each other address paid, one reads
(`0x80`) therefore does not change a step's name. Only the routers with the
`0x7f` mask have `PAY_PORTION_FULL_PRECISION` (`0x07`), listed as
`Pay Portion (Full Precision)`, and `ACROSS_V4_DEPOSIT_V3` (`0x40`), listed as
`Across Bridge Deposit`; the other routers revert on `0x07`, and it is listed
there as `Command 0x07`. The decoder does not read the commands inside an
`EXECUTE_SUB_PLAN` (`0x21`) step, and wherever that step stands they may spend
the user's tokens or pay the user. That step is listed as
`Execute Sub-Plan (its steps are not shown)`, and whatever the other steps
state, the summary names no swap direction, `Token In` and `Token Out` read
`Unknown (could not be decoded)`, and there is no `Amount` or `Min. received`
line. Each swap step, `SWEEP`, `WRAP_ETH`, `UNWRAP_WETH`, `TRANSFER`,
`PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`, `PERMIT2_TRANSFER_FROM` (which
moves the user's own tokens) and V4 `TAKE` and `TAKE_PORTION` names the
address it pays, and `TAKE_ALL` pays the transaction's sender. The router
reads `address(1)` as the transaction's sender, and `address(2)` as the router
itself, which holds what it is paid until a later step pays it out; a payment
to the router, as `address(2)` or by its own address, is not counted as paid
out. `UniversalRouterV1` alone pays a `PERMIT2_TRANSFER_FROM` to `address(1)`
or `address(2)` as those addresses, and the decoder names them. When a swap
step, `TAKE` or `TAKE_PORTION` pays the router the swap's output token, the
output stays in the router unless a later step pays all the router holds of it
to anyone but the router: a `SWEEP` of that token, an `UNWRAP_WETH` when it is
WETH, or a `TRANSFER` of `CONTRACT_BALANCE`, a `PAY_PORTION` of 10,000 bips or
a `PAY_PORTION_FULL_PRECISION` of 1e18 of that token. 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), no step
pays any other address, no step that may pay an address went unread, and the
output does not stay in the router. Otherwise it reads `Min. paid out`, a
`Paid to` line after `Token Out` names each other address paid, one reads
`The router, where it stays (no later step pays it out)` when the output stays
in the router, and one more reads `Unknown (could not be decoded)` when such a
step went unread, or `Unknown (not named in the calldata)` when no step pays
anyone. A step goes unread when its recipient does not decode as an address,
or when it is a command or V4 action the decoder does not read: every command
other than `PERMIT2_PERMIT`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `WRAP_ETH`, `UNWRAP_WETH` and
`BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`, `TRANSFER`, `PAY_PORTION` and
`EXECUTE_SUB_PLAN` among them), and V4 `TAKE_PORTION`.
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`, `WRAP_ETH`, `UNWRAP_WETH` and
`BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`, `PERMIT2_TRANSFER_FROM_BATCH`,
`EXECUTE_SUB_PLAN` and `ACROSS_V4_DEPOSIT_V3` among them).
### Non-Goals Forever