fix: a balance check of another address sets no swap minimum (closes #600)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s

BALANCE_CHECK_ERC20 takes (owner, token, minBalance), and every Universal
Router release that has it checks the owner's balance as written, without
map(), so address(1) and address(2) there are plain addresses. The decoder
read only the token and the minimum, so a check of another address's balance
read as Min. received. A check whose owner is not the transaction's sender now
sets neither Token Out nor the minimum, and its step reads "Balance Check (of
another address)"; one whose input does not decode reads "Balance Check (could
not be decoded)". A check of the sender keeps its reading, including the zero
reading for one the router lets fail.

Model: opus-5-5
This commit is contained in:
2026-10-19 14:10:00 +00:00
parent c99772a520
commit 165c748641
4 changed files with 176 additions and 38 deletions
+21 -10
View File
@@ -1198,11 +1198,13 @@ number. They can read:
guarantees nothing: it sets `Token Out` and `Min. received` only when the
output side holds no minimum, not even a zero one, at the point the check is
reached, and otherwise leaves the current token and figure in place. A nonzero
`minBalance` sets both lines, as a swap step does. A `BALANCE_CHECK_ERC20`
step that the router lets fail without reverting the transaction (see
Transaction Decoding) guarantees nothing either, whatever its `minBalance`,
and is read as one whose `minBalance` is zero. A minimum stated by a V4 swap
step the router lets fail is likewise read as zero.
`minBalance` sets both lines, as a swap step does. Only a check of the
transaction's sender's own balance sets either line; a check of any other
address sets neither (see Transaction Decoding). A `BALANCE_CHECK_ERC20` step
that the router lets fail without reverting the transaction (see Transaction
Decoding) guarantees nothing either, whatever its `minBalance`, and is read as
one whose `minBalance` is zero. A minimum stated by a V4 swap step the router
lets fail is likewise read as zero.
The swap's `Token In` and `Token Out` lines name a currency, not an amount; each
reads `Unknown (not named in the calldata)` when the decoder found no token for
@@ -3018,11 +3020,20 @@ Currently supported:
`ACROSS_V4_DEPOSIT_V3` pays the bridge, which pays the deposit's recipient on
another network and refunds a deposit nobody fills to its depositor; the
router passes both on as written, so `address(1)` and `address(2)` there are
addresses, and each that is not the transaction's sender is named. 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, a `TRANSFER` of `CONTRACT_BALANCE`, a `PAY_PORTION` of 10,000 bips or a
addresses, and each that is not the transaction's sender is named. A
`BALANCE_CHECK_ERC20` pays nobody: the router checks that its owner holds at
least `minBalance` of its token, and reads the owner as written, so
`address(1)` and `address(2)` there are addresses too. A check of any address
but the transaction's sender's says nothing about what the user receives: it
sets neither `Token Out` nor the minimum, and it is listed as
`Balance Check (of another address)`. A check whose input does not decode,
such as one whose owner word has a nonzero top byte (the router reads the
owner from the word's last 20 bytes), likewise sets neither, and it is listed
as `Balance Check (could not be decoded)`. 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, a
`TRANSFER` of `CONTRACT_BALANCE`, a `PAY_PORTION` of 10,000 bips or a
`PAY_PORTION_FULL_PRECISION` of 1e18 of that token, or an
`ACROSS_V4_DEPOSIT_V3` of `CONTRACT_BALANCE` of that token, or of ETH when it
sets `useNative`. The router sends ETH as written, so a `TRANSFER` of