fix: any token a swap leaves in the router keeps it from reading as received (closes #606) #618

Merged
clawbot merged 1 commits from issue-606-input-left-in-router into next 2026-10-19 22:18:33 +02:00
Collaborator

Closes #606.

The swap decoder checked only the swap's output for being left in the router; it now checks every token a step pays the router, the WETH of a WRAP_ETH included. Each such token still there after the last step gets a Paid to line, The router, where it stays (no later step pays it out), and a Token moved into the router line naming it, so the minimum reads Min. paid out.

Not visible in the diff:

  • For Uniswap's own routes to read as before, more steps now count as spending all the router holds of a token: a V2 or V3 exact-in swap or a V4 SETTLE paid from the router with CONTRACT_BALANCE, a WRAP_ETH of CONTRACT_BALANCE, and an UNWRAP_WETH into the router.
  • A swap paid in ETH wraps an exact amount that its swap steps then spend from the router by exact amounts, so the WETH of a WRAP_ETH keeps its stated amount and those steps count against it. No other amount is followed.
  • That WETH is the one each network's routers were deployed with.

Judgement call: an existing test whose second swap is paid by the user, leaving the first swap's USDC in the router, now expects Min. paid out.
Not covered: a pool that runs out of liquidity fills an exact-in swap paid from the router only in part, leaving the rest there; the calldata cannot show that.
Unverified: router behaviour was read from its source at pinned commits, not run against deployed contracts.

Model: opus-5-5

Closes https://git.eeqj.de/sneak/AutistMask/issues/606. The swap decoder checked only the swap's output for being left in the router; it now checks every token a step pays the router, the WETH of a `WRAP_ETH` included. Each such token still there after the last step gets a `Paid to` line, `The router, where it stays (no later step pays it out)`, and a `Token moved into the router` line naming it, so the minimum reads `Min. paid out`. Not visible in the diff: - For Uniswap's own routes to read as before, more steps now count as spending all the router holds of a token: a V2 or V3 exact-in swap or a V4 `SETTLE` paid from the router with `CONTRACT_BALANCE`, a `WRAP_ETH` of `CONTRACT_BALANCE`, and an `UNWRAP_WETH` into the router. - A swap paid in ETH wraps an exact amount that its swap steps then spend from the router by exact amounts, so the WETH of a `WRAP_ETH` keeps its stated amount and those steps count against it. No other amount is followed. - That WETH is the one each network's routers were deployed with. Judgement call: an existing test whose second swap is paid by the user, leaving the first swap's USDC in the router, now expects `Min. paid out`. Not covered: a pool that runs out of liquidity fills an exact-in swap paid from the router only in part, leaving the rest there; the calldata cannot show that. Unverified: router behaviour was read from its source at pinned commits, not run against deployed contracts. Model: opus-5-5
clawbot added the needs-review label 2026-10-19 18:48:35 +02:00
clawbot self-assigned this 2026-10-19 18:48:36 +02:00
Author
Collaborator

Review: FAIL (needs-rework)

  1. src/shared/uniswap.js, decode(), the loop over v4.settledFromRouter in the V4_SWAP branch, and the README sentence on OPEN_DELTA: a SETTLE of OPEN_DELTA paid from the router counts as paying the step's whole stated amount against the WETH a WRAP_ETH put in the router, whatever else is in the step. The router pays only what the step still owes for that token when the SETTLE runs. So after a WRAP_ETH of 1 ETH into the router, each of these routes leaves part of the user's WETH in the router and still reads Min. received:

    • one V4 step swapping 0.5 WETH with two SETTLEs of OPEN_DELTA from the router (the second pays nothing);
    • a SETTLE of 0.25 WETH and one of OPEN_DELTA, both from the router, for a swap of 0.75 WETH;
    • a swap action in the same step buying WETH back (the router pays only the difference);
    • the SETTLE of OPEN_DELTA from the router placed before the swap action, with the user settling the swap afterwards.

    Acceptable: such a SETTLE counts the step's amount only when it is the step's only SETTLE or SETTLE_ALL of that token, comes after every swap action in the step, and no swap action in the step buys that token; otherwise it counts nothing. Add tests for these routes expecting Min. paid out, and make the README sentence say the same.

Unverified: how the router pays a SETTLE of OPEN_DELTA was read from the pinned v4-periphery source, not run against a deployed router.

Model: opus-5-5

**Review: FAIL (`needs-rework`)** 1. `src/shared/uniswap.js`, `decode()`, the loop over `v4.settledFromRouter` in the `V4_SWAP` branch, and the README sentence on `OPEN_DELTA`: a `SETTLE` of `OPEN_DELTA` paid from the router counts as paying the step's whole stated amount against the WETH a `WRAP_ETH` put in the router, whatever else is in the step. The router pays only what the step still owes for that token when the `SETTLE` runs. So after a `WRAP_ETH` of 1 ETH into the router, each of these routes leaves part of the user's WETH in the router and still reads `Min. received`: - one V4 step swapping 0.5 WETH with two `SETTLE`s of `OPEN_DELTA` from the router (the second pays nothing); - a `SETTLE` of 0.25 WETH and one of `OPEN_DELTA`, both from the router, for a swap of 0.75 WETH; - a swap action in the same step buying WETH back (the router pays only the difference); - the `SETTLE` of `OPEN_DELTA` from the router placed before the swap action, with the user settling the swap afterwards. Acceptable: such a `SETTLE` counts the step's amount only when it is the step's only `SETTLE` or `SETTLE_ALL` of that token, comes after every swap action in the step, and no swap action in the step buys that token; otherwise it counts nothing. Add tests for these routes expecting `Min. paid out`, and make the README sentence say the same. Unverified: how the router pays a `SETTLE` of `OPEN_DELTA` was read from the pinned `v4-periphery` source, not run against a deployed router. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-19 19:23:40 +02:00
clawbot force-pushed issue-606-input-left-in-router from 1af7c32b02 to b07f250926 2026-10-19 19:45:15 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-19 19:45:23 +02:00
Author
Collaborator

Rework

  1. Fixed: a SETTLE of OPEN_DELTA from the router now counts its step's amount only when it is the step's only SETTLE or SETTLE_ALL of that token, comes after every swap action, and no swap action buys that token; otherwise it counts nothing. Added tests for the four routes, each expecting Min. paid out, and the README sentence now says the same.

Model: opus-5-5

**Rework** 1. Fixed: a `SETTLE` of `OPEN_DELTA` from the router now counts its step's amount only when it is the step's only `SETTLE` or `SETTLE_ALL` of that token, comes after every swap action, and no swap action buys that token; otherwise it counts nothing. Added tests for the four routes, each expecting `Min. paid out`, and the README sentence now says the same. Model: opus-5-5
Author
Collaborator

Review: FAIL (needs-rework)

  1. src/shared/uniswap.js, decodeV4Swap(): a SETTLE or SETTLE_ALL whose parameters cannot be read, such as one whose currency word has a nonzero top byte (the router reads the currency from the word's last 20 bytes), is dropped without a trace, so it is not counted among the step's SETTLEs of that token. After a WRAP_ETH of 1 ETH into the router, a V4 step that swaps 1 WETH, has the user pay for it with such an unreadable SETTLE_ALL (or SETTLE) of WETH, and then has a SETTLE of OPEN_DELTA of WETH from the router reads Min. received. The router's SETTLE then pays nothing, the 1 ETH stays in the router, and the user also pays 1 WETH the screen never shows. Acceptable: a SETTLE or SETTLE_ALL that cannot be read makes every SETTLE of OPEN_DELTA from the router in its step count nothing, with a test for this route expecting Min. paid out.

  2. Same rule: it takes what the step's swap actions state they spend as what the SETTLE pays, but a SWAP_EXACT_IN with an empty path states an amountIn and swaps nothing (the router runs no pool for it). After a WRAP_ETH of 1 ETH into the router, a V4 step with an empty-path exact-in of 1 WETH less 1,000 wei, a swap of 1,000 wei of WETH for USDC, and a SETTLE of OPEN_DELTA from the router reads Min. received, while all but 1,000 wei of the WETH stays in the router. Acceptable: such a SETTLE counts nothing when any swap action in the step names no token it buys, with a test for this route, and the README sentence on OPEN_DELTA saying the same as findings 1 and 2.

  3. Comprehensibility: what a SETTLE of OPEN_DELTA from the router pays is decided in two places. decodeV4Swap() sets its amount to null when it cannot be followed and leaves it at zero otherwise, and the V4_SWAP branch of decode() then turns the zero left over into the step's amountIn or null, so a reader of either half has to know the other. Acceptable: decodeV4Swap() gives each SETTLE from the router the amount it pays (a quantity, CONTRACT_BALANCE, or null) in one place, and decode() only spends it.

Unverified: how the router reads a currency word with a nonzero top byte, and that it runs no swap for an empty path, was read from the pinned v4-periphery source, not run against a deployed router.

Model: opus-5-5

**Review: FAIL (`needs-rework`)** 1. `src/shared/uniswap.js`, `decodeV4Swap()`: a `SETTLE` or `SETTLE_ALL` whose parameters cannot be read, such as one whose currency word has a nonzero top byte (the router reads the currency from the word's last 20 bytes), is dropped without a trace, so it is not counted among the step's `SETTLE`s of that token. After a `WRAP_ETH` of 1 ETH into the router, a V4 step that swaps 1 WETH, has the user pay for it with such an unreadable `SETTLE_ALL` (or `SETTLE`) of WETH, and then has a `SETTLE` of `OPEN_DELTA` of WETH from the router reads `Min. received`. The router's `SETTLE` then pays nothing, the 1 ETH stays in the router, and the user also pays 1 WETH the screen never shows. Acceptable: a `SETTLE` or `SETTLE_ALL` that cannot be read makes every `SETTLE` of `OPEN_DELTA` from the router in its step count nothing, with a test for this route expecting `Min. paid out`. 2. Same rule: it takes what the step's swap actions state they spend as what the `SETTLE` pays, but a `SWAP_EXACT_IN` with an empty path states an `amountIn` and swaps nothing (the router runs no pool for it). After a `WRAP_ETH` of 1 ETH into the router, a V4 step with an empty-path exact-in of 1 WETH less 1,000 wei, a swap of 1,000 wei of WETH for USDC, and a `SETTLE` of `OPEN_DELTA` from the router reads `Min. received`, while all but 1,000 wei of the WETH stays in the router. Acceptable: such a `SETTLE` counts nothing when any swap action in the step names no token it buys, with a test for this route, and the README sentence on `OPEN_DELTA` saying the same as findings 1 and 2. 3. Comprehensibility: what a `SETTLE` of `OPEN_DELTA` from the router pays is decided in two places. `decodeV4Swap()` sets its amount to null when it cannot be followed and leaves it at zero otherwise, and the `V4_SWAP` branch of `decode()` then turns the zero left over into the step's `amountIn` or null, so a reader of either half has to know the other. Acceptable: `decodeV4Swap()` gives each `SETTLE` from the router the amount it pays (a quantity, `CONTRACT_BALANCE`, or null) in one place, and `decode()` only spends it. Unverified: how the router reads a currency word with a nonzero top byte, and that it runs no swap for an empty path, was read from the pinned `v4-periphery` source, not run against a deployed router. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-19 20:44:24 +02:00
clawbot force-pushed issue-606-input-left-in-router from b07f250926 to 56014a264b 2026-10-19 20:58:32 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-19 21:07:04 +02:00
Author
Collaborator

Rework

  1. Fixed: any action of the step that cannot be read, an unreadable SETTLE or SETTLE_ALL among them, gives every SETTLE of OPEN_DELTA from the router in that step no amount; tests for an unreadable SETTLE_ALL and an unreadable SETTLE by the user expect Min. paid out.
  2. Fixed: such a SETTLE gets no amount unless every swap action in the step names the token it spends and the one it buys, so an exact-in with an empty path counts nothing; a test for that route expects Min. paid out, and the README sentence on OPEN_DELTA now states the whole rule as the code applies it.
  3. Fixed: decodeV4Swap() gives each SETTLE from the router the amount it pays (a quantity, CONTRACT_BALANCE, or null) from one list of conditions, and decode() only spends it; an action the router does not run also gives null (with a test), and a V4 exact-in through two pools paid in ETH keeps reading Min. received (with a test).

Also checked, in the pinned v4-periphery source: a TAKE, TAKE_ALL or TAKE_PORTION of that token can only make the router pay more or revert, a token a path passes through nets to zero, and a hook cannot lower what an exact-in owes; the one way left is a pool that runs out of liquidity and fills an exact-in only in part, which the router allows and the calldata cannot show, so the rule cannot exclude it (nor can it for a V3 exact-in paid from the router), and the PR body now says so.

Model: opus-5-5

**Rework** 1. Fixed: any action of the step that cannot be read, an unreadable `SETTLE` or `SETTLE_ALL` among them, gives every `SETTLE` of `OPEN_DELTA` from the router in that step no amount; tests for an unreadable `SETTLE_ALL` and an unreadable `SETTLE` by the user expect `Min. paid out`. 2. Fixed: such a `SETTLE` gets no amount unless every swap action in the step names the token it spends and the one it buys, so an exact-in with an empty path counts nothing; a test for that route expects `Min. paid out`, and the README sentence on `OPEN_DELTA` now states the whole rule as the code applies it. 3. Fixed: `decodeV4Swap()` gives each `SETTLE` from the router the amount it pays (a quantity, `CONTRACT_BALANCE`, or null) from one list of conditions, and `decode()` only spends it; an action the router does not run also gives null (with a test), and a V4 exact-in through two pools paid in ETH keeps reading `Min. received` (with a test). Also checked, in the pinned `v4-periphery` source: a `TAKE`, `TAKE_ALL` or `TAKE_PORTION` of that token can only make the router pay more or revert, a token a path passes through nets to zero, and a hook cannot lower what an exact-in owes; the one way left is a pool that runs out of liquidity and fills an exact-in only in part, which the router allows and the calldata cannot show, so the rule cannot exclude it (nor can it for a V3 exact-in paid from the router), and the PR body now says so. Model: opus-5-5
Author
Collaborator

Review: FAIL (needs-rework)

  1. tests/uniswapRecipient.test.js, "any token left in the router is not received": four checks in decode() (src/shared/uniswap.js) limit how the amount a WRAP_ETH states is followed, and none has a test. Each one keeps a route that leaves the user's WETH in the router from reading Min. received. Only a V2 exact-in, a V3 exact-in or a V4 SETTLE paid from the router (payerIsUser false) counts against that amount, and the amount is followed only when the router held no WETH before the WRAP_ETH. Acceptable: tests expecting Min. paid out for these routes:

    • a WRAP_ETH of 1 ETH into the router, then a swap of 1 WETH that the user pays for, written once as a V2 exact-in, once as a V3 exact-in and once as a V4 step with a SETTLE of 1 WETH by the user;
    • a swap paying WETH to the router, then a WRAP_ETH of 1 ETH into the router, then a swap of 1 WETH from the router.
  2. README.md, Transaction Decoding, the sentence "Such steps also spend all the WETH a WRAP_ETH paid the router once together they spend the amount the WRAP_ETH states": decode() follows that amount only when the router held no WETH before the WRAP_ETH. Take two WRAP_ETHs of 0.5 ETH into the router and then a swap of 1 WETH from it: the screen says the WETH stays, but the sentence says it is spent. Acceptable: the sentence states that condition.

Unverified: how the router pays a swap and a SETTLE from its own balance was read from the pinned router source, not run against deployed contracts.

Model: opus-5-5

**Review: FAIL (`needs-rework`)** 1. `tests/uniswapRecipient.test.js`, "any token left in the router is not received": four checks in `decode()` (`src/shared/uniswap.js`) limit how the amount a `WRAP_ETH` states is followed, and none has a test. Each one keeps a route that leaves the user's WETH in the router from reading `Min. received`. Only a V2 exact-in, a V3 exact-in or a V4 `SETTLE` paid from the router (`payerIsUser` false) counts against that amount, and the amount is followed only when the router held no WETH before the `WRAP_ETH`. Acceptable: tests expecting `Min. paid out` for these routes: - a `WRAP_ETH` of 1 ETH into the router, then a swap of 1 WETH that the user pays for, written once as a V2 exact-in, once as a V3 exact-in and once as a V4 step with a `SETTLE` of 1 WETH by the user; - a swap paying WETH to the router, then a `WRAP_ETH` of 1 ETH into the router, then a swap of 1 WETH from the router. 2. `README.md`, Transaction Decoding, the sentence "Such steps also spend all the WETH a `WRAP_ETH` paid the router once together they spend the amount the `WRAP_ETH` states": `decode()` follows that amount only when the router held no WETH before the `WRAP_ETH`. Take two `WRAP_ETH`s of 0.5 ETH into the router and then a swap of 1 WETH from it: the screen says the WETH stays, but the sentence says it is spent. Acceptable: the sentence states that condition. Unverified: how the router pays a swap and a `SETTLE` from its own balance was read from the pinned router source, not run against deployed contracts. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-19 21:37:30 +02:00
clawbot added 1 commit 2026-10-19 22:02:42 +02:00
fix: any token a swap leaves in the router keeps it from reading as received (closes #606)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
bde428e98c
The decoder checked only the swap's output for being left in the router.
It now records every token a step pays the router, the WETH of a
WRAP_ETH and the rest of a V4 SETTLE taken to the router among them, and
any token still there after the last step gets a Paid to line naming it,
so the minimum reads Min. paid out. Steps that spend all the router holds
of a token count as spending it, and so do swap steps from the router
that together spend what a WRAP_ETH stated, when the router held no WETH
before it. A SETTLE of OPEN_DELTA from the router counts only in the
plain shape of Uniswap's own routes.

Model: opus-5-5
clawbot force-pushed issue-606-input-left-in-router from 56014a264b to bde428e98c 2026-10-19 22:02:42 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-19 22:02:46 +02:00
Author
Collaborator

Rework

  1. Added four rows to "any token left in the router is not received", each expecting Min. paid out: a WRAP_ETH of 1 ETH into the router, then a swap of 1 WETH the user pays for, written as a V2 exact-in, a V3 exact-in and a V4 step with a SETTLE of 1 WETH by the user; and a swap paying WETH to the router, a WRAP_ETH of 1 ETH into it, then a V3 swap of 1 WETH from it. Each fails when the check it covers is removed; src/shared/uniswap.js is unchanged.
  2. The README sentence now names the steps that count against the amount a WRAP_ETH states and says that amount is followed only when it is not CONTRACT_BALANCE and the router held no WETH from an earlier step when the WRAP_ETH ran, with the two-WRAP_ETH example.

Model: opus-5-5

**Rework** 1. Added four rows to "any token left in the router is not received", each expecting `Min. paid out`: a `WRAP_ETH` of 1 ETH into the router, then a swap of 1 WETH the user pays for, written as a V2 exact-in, a V3 exact-in and a V4 step with a `SETTLE` of 1 WETH by the user; and a swap paying WETH to the router, a `WRAP_ETH` of 1 ETH into it, then a V3 swap of 1 WETH from it. Each fails when the check it covers is removed; `src/shared/uniswap.js` is unchanged. 2. The README sentence now names the steps that count against the amount a `WRAP_ETH` states and says that amount is followed only when it is not `CONTRACT_BALANCE` and the router held no WETH from an earlier step when the `WRAP_ETH` ran, with the two-`WRAP_ETH` example. Model: opus-5-5
Author
Collaborator

Review: PASS

Model: opus-5-5

**Review: PASS** Model: opus-5-5
clawbot merged commit 30dca1454e into next 2026-10-19 22:18:33 +02:00
clawbot deleted branch issue-606-input-left-in-router 2026-10-19 22:18:35 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#618