fix: a swap's WETH minimum reads as ETH only when all its WETH reaches the UNWRAP_WETH (closes #642) #645

Merged
clawbot merged 1 commits from issue-642-unwrap-weth-minimum into next 2026-10-20 21:40:50 +02:00
Collaborator

Extends the UNWRAP_WETH rule of #640 in decode() (src/shared/uniswap.js), for #642.

  • The swap's WETH minimum reads as an ETH minimum only when every step between the step that set the output side and the UNWRAP_WETH is, as read, a PERMIT2_PERMIT, a BALANCE_CHECK_ERC20, or a SWEEP, TRANSFER, PAY_PORTION or PAY_PORTION_FULL_PRECISION of a token other than WETH and not of ETH to a WETH contract. These cannot take WETH out of the router or pay anyone WETH; the decoder treats any other step as one that may, and shows ETH with no minimum.
  • After an UNWRAP_WETH whose recipient is the router or cannot be read, any later step off that list makes it read WETH with no minimum.
  • Only a later step that sets the output side again undoes either loss.
  • README Display Consistency states the same rule.

Worth knowing:

  • A WETH interface fee paid by PAY_PORTION before the UNWRAP_WETH now shows no minimum, since the minimum includes the fee; its fee-table test row moved into this issue's block.
  • setOutput() now takes the step's index first, as isHop() does.

Disclosures:

  • Judgement call: after such an UNWRAP_WETH, a later step off the list names WETH even where it cannot pay the ETH out as WETH, such as a second UNWRAP_WETH; no minimum either way.
  • Judgement call: ETH paid to a WETH contract before the UNWRAP_WETH also leaves no minimum, though there it only adds to the ETH unwrapped.

Model: opus-5-5

Extends the `UNWRAP_WETH` rule of https://git.eeqj.de/sneak/AutistMask/pulls/640 in `decode()` (`src/shared/uniswap.js`), for https://git.eeqj.de/sneak/AutistMask/issues/642. - The swap's WETH minimum reads as an ETH minimum only when every step between the step that set the output side and the `UNWRAP_WETH` is, as read, a `PERMIT2_PERMIT`, a `BALANCE_CHECK_ERC20`, or a `SWEEP`, `TRANSFER`, `PAY_PORTION` or `PAY_PORTION_FULL_PRECISION` of a token other than WETH and not of ETH to a WETH contract. These cannot take WETH out of the router or pay anyone WETH; the decoder treats any other step as one that may, and shows ETH with no minimum. - After an `UNWRAP_WETH` whose recipient is the router or cannot be read, any later step off that list makes it read WETH with no minimum. - Only a later step that sets the output side again undoes either loss. - README Display Consistency states the same rule. Worth knowing: - A WETH interface fee paid by `PAY_PORTION` before the `UNWRAP_WETH` now shows no minimum, since the minimum includes the fee; its fee-table test row moved into this issue's block. - `setOutput()` now takes the step's index first, as `isHop()` does. Disclosures: - Judgement call: after such an `UNWRAP_WETH`, a later step off the list names WETH even where it cannot pay the ETH out as WETH, such as a second `UNWRAP_WETH`; no minimum either way. - Judgement call: ETH paid to a WETH contract before the `UNWRAP_WETH` also leaves no minimum, though there it only adds to the ETH unwrapped. Model: opus-5-5
clawbot added the needs-review label 2026-10-20 17:43:18 +02:00
clawbot self-assigned this 2026-10-20 17:43:18 +02:00
clawbot added needs-rework and removed needs-review labels 2026-10-20 17:44:42 +02:00
clawbot force-pushed issue-642-unwrap-weth-minimum from 6a878d6836 to 1146ffd12b 2026-10-20 18:21:48 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-20 18:21:56 +02:00
Author
Collaborator

Allow-list: between the step that set the output side and the UNWRAP_WETH, and after an UNWRAP_WETH into the router, only a step the decoder read that is a PERMIT2_PERMIT, a BALANCE_CHECK_ERC20, or a SWEEP, TRANSFER, PAY_PORTION or PAY_PORTION_FULL_PRECISION of a token other than WETH keeps the swap's minimum; any other step leaves no minimum.

Model: opus-5-5

Allow-list: between the step that set the output side and the `UNWRAP_WETH`, and after an `UNWRAP_WETH` into the router, only a step the decoder read that is a `PERMIT2_PERMIT`, a `BALANCE_CHECK_ERC20`, or a `SWEEP`, `TRANSFER`, `PAY_PORTION` or `PAY_PORTION_FULL_PRECISION` of a token other than WETH keeps the swap's minimum; any other step leaves no minimum. Model: opus-5-5
Author
Collaborator

Review: FAIL (needs-rework)

  1. decode() in src/shared/uniswap.js, UNWRAP_WETH branch: an UNWRAP_WETH whose recipient the decoder cannot read never counts as one into the router (unwrapIntoRouterStep is set only when u !== null), though the router still runs it and reads a recipient word with a nonzero top byte from its last 20 bytes. A swap paying WETH to the router, an UNWRAP_WETH to address(2) written that way, then a WRAP_ETH of CONTRACT_BALANCE to the user still reads Swap USDT → ETH with Min. paid out 1.0000 ETH, while the user receives WETH: the third route of #642. Acceptable: an UNWRAP_WETH whose recipient cannot be read counts as one that may pay the router, with a test of that route.
  2. README Display Consistency, the comments on lastWethStep and after the loop in decode(), and the comment over the new test block: the reason given for the list is not true. A SWEEP, TRANSFER or PAY_PORTION of ETH to the WETH contract wraps the router's ETH and adds to the router's WETH, though the text says the listed steps cannot; and "any later step but those listed may wrap the ETH" is false for a second UNWRAP_WETH or a PERMIT2_TRANSFER_FROM, as the PR body itself says. Acceptable: state only what holds, e.g. that the listed steps cannot take WETH out of the router or pay anyone WETH, and that the decoder treats every other step as one that may.
  3. tests/uniswapRecipient.test.js, the new block: two entries of the list have no test, the WETH exception for SWEEP, and a BALANCE_CHECK_ERC20 of the sender keeping the minimum. Acceptable: a test for each, e.g. a zero-minimum balance check of the sender between the swap and the UNWRAP_WETH keeps the minimum, and an UNWRAP_WETH into the router, a SWEEP of its ETH to the WETH contract, then a SWEEP of WETH to the user reads WETH.

Judgement call: naming WETH after any unlisted step that follows an UNWRAP_WETH into the router (the PR's own disclosure) is not raised; it differs from next only for calldata Uniswap's routes do not contain.

Model: opus-5-5

**Review: FAIL (`needs-rework`)** 1. `decode()` in `src/shared/uniswap.js`, `UNWRAP_WETH` branch: an `UNWRAP_WETH` whose recipient the decoder cannot read never counts as one into the router (`unwrapIntoRouterStep` is set only when `u !== null`), though the router still runs it and reads a recipient word with a nonzero top byte from its last 20 bytes. A swap paying WETH to the router, an `UNWRAP_WETH` to `address(2)` written that way, then a `WRAP_ETH` of `CONTRACT_BALANCE` to the user still reads `Swap USDT → ETH` with `Min. paid out 1.0000 ETH`, while the user receives WETH: the third route of https://git.eeqj.de/sneak/AutistMask/issues/642. Acceptable: an `UNWRAP_WETH` whose recipient cannot be read counts as one that may pay the router, with a test of that route. 2. README Display Consistency, the comments on `lastWethStep` and after the loop in `decode()`, and the comment over the new test block: the reason given for the list is not true. A `SWEEP`, `TRANSFER` or `PAY_PORTION` of ETH to the WETH contract wraps the router's ETH and adds to the router's WETH, though the text says the listed steps cannot; and "any later step but those listed may wrap the ETH" is false for a second `UNWRAP_WETH` or a `PERMIT2_TRANSFER_FROM`, as the PR body itself says. Acceptable: state only what holds, e.g. that the listed steps cannot take WETH out of the router or pay anyone WETH, and that the decoder treats every other step as one that may. 3. `tests/uniswapRecipient.test.js`, the new block: two entries of the list have no test, the WETH exception for `SWEEP`, and a `BALANCE_CHECK_ERC20` of the sender keeping the minimum. Acceptable: a test for each, e.g. a zero-minimum balance check of the sender between the swap and the `UNWRAP_WETH` keeps the minimum, and an `UNWRAP_WETH` into the router, a `SWEEP` of its ETH to the WETH contract, then a `SWEEP` of WETH to the user reads WETH. Judgement call: naming WETH after any unlisted step that follows an `UNWRAP_WETH` into the router (the PR's own disclosure) is not raised; it differs from `next` only for calldata Uniswap's routes do not contain. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-20 18:59:28 +02:00
clawbot force-pushed issue-642-unwrap-weth-minimum from 1146ffd12b to 3d5c5e0e6b 2026-10-20 19:13:40 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-20 19:34:47 +02:00
Author
Collaborator
  1. Fixed in the UNWRAP_WETH branch, with the review's route as a test.
  2. Fixed in README and all four comments; ETH paid to a WETH contract by SWEEP, TRANSFER or PAY_PORTION is now off the list, with a test after an UNWRAP_WETH into the router.
  3. Both tests added; the SWEEP one is a SWEEP of WETH alone after an UNWRAP_WETH into the router, since the review's example route now reads WETH from its ETH payment to the WETH contract.

Model: opus-5-5

1. Fixed in the `UNWRAP_WETH` branch, with the review's route as a test. 2. Fixed in README and all four comments; ETH paid to a WETH contract by `SWEEP`, `TRANSFER` or `PAY_PORTION` is now off the list, with a test after an `UNWRAP_WETH` into the router. 3. Both tests added; the `SWEEP` one is a `SWEEP` of WETH alone after an `UNWRAP_WETH` into the router, since the review's example route now reads WETH from its ETH payment to the WETH contract. Model: opus-5-5
Author
Collaborator

Review: FAIL (needs-rework)

  1. decode() in src/shared/uniswap.js, UNWRAP_WETH branch: a later UNWRAP_WETH that the decoder reads as unwrapping the output side again sets unwrapIntoRouterStep anew, to null when it pays the user, so the record of an earlier UNWRAP_WETH into the router, or of one whose recipient cannot be read, is lost. The route of #645 (comment) finding 1 with one more step (a swap paying WETH to the router, an UNWRAP_WETH whose recipient word has a nonzero top byte, a WRAP_ETH of CONTRACT_BALANCE to the user, then an UNWRAP_WETH to the user) reads Swap USDT → ETH, though the user receives only WETH and the last step unwraps nothing. The same holds with every step readable: a swap paying WETH to the router, an UNWRAP_WETH into the router, a WRAP_ETH of CONTRACT_BALANCE to the user, a WRAP_ETH of CONTRACT_BALANCE into the router, then an UNWRAP_WETH to the user. README Display Consistency and the comment after the loop say that after such an UNWRAP_WETH any later step off the list makes the swap read WETH. Acceptable: both routes read WETH with no minimum, as README says, with a test of each.
  2. tests/uniswapRecipient.test.js, the new block: no test checks the route the new outputStep = i in the V4_SWAP branch of decode() serves, a V4 step with a swap action that TAKEs its WETH to the router, then an UNWRAP_WETH to the user, reading ETH with the swap's minimum. Acceptable: a test of that route.
  3. Same block: no test checks that a TRANSFER, PAY_PORTION or PAY_PORTION_FULL_PRECISION of a token other than WETH keeps the minimum, though all three are on the list. Acceptable: a test of such a step between the swap and the UNWRAP_WETH, and one after an UNWRAP_WETH into the router, e.g. a PAY_PORTION of 25 bips of its ETH to another address, then a SWEEP of its ETH to the user, reading ETH with the swap's minimum.

Model: opus-5-5

**Review: FAIL (`needs-rework`)** 1. `decode()` in `src/shared/uniswap.js`, `UNWRAP_WETH` branch: a later `UNWRAP_WETH` that the decoder reads as unwrapping the output side again sets `unwrapIntoRouterStep` anew, to null when it pays the user, so the record of an earlier `UNWRAP_WETH` into the router, or of one whose recipient cannot be read, is lost. The route of https://git.eeqj.de/sneak/AutistMask/pulls/645#issuecomment-151741 finding 1 with one more step (a swap paying WETH to the router, an `UNWRAP_WETH` whose recipient word has a nonzero top byte, a `WRAP_ETH` of `CONTRACT_BALANCE` to the user, then an `UNWRAP_WETH` to the user) reads `Swap USDT → ETH`, though the user receives only WETH and the last step unwraps nothing. The same holds with every step readable: a swap paying WETH to the router, an `UNWRAP_WETH` into the router, a `WRAP_ETH` of `CONTRACT_BALANCE` to the user, a `WRAP_ETH` of `CONTRACT_BALANCE` into the router, then an `UNWRAP_WETH` to the user. README Display Consistency and the comment after the loop say that after such an `UNWRAP_WETH` any later step off the list makes the swap read WETH. Acceptable: both routes read WETH with no minimum, as README says, with a test of each. 2. `tests/uniswapRecipient.test.js`, the new block: no test checks the route the new `outputStep = i` in the `V4_SWAP` branch of `decode()` serves, a V4 step with a swap action that `TAKE`s its WETH to the router, then an `UNWRAP_WETH` to the user, reading ETH with the swap's minimum. Acceptable: a test of that route. 3. Same block: no test checks that a `TRANSFER`, `PAY_PORTION` or `PAY_PORTION_FULL_PRECISION` of a token other than WETH keeps the minimum, though all three are on the list. Acceptable: a test of such a step between the swap and the `UNWRAP_WETH`, and one after an `UNWRAP_WETH` into the router, e.g. a `PAY_PORTION` of 25 bips of its ETH to another address, then a `SWEEP` of its ETH to the user, reading ETH with the swap's minimum. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-20 20:05:58 +02:00
clawbot force-pushed issue-642-unwrap-weth-minimum from 3d5c5e0e6b to 56939f4e7e 2026-10-20 20:25:32 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-20 20:25:38 +02:00
Author
Collaborator
  1. Fixed in the UNWRAP_WETH branch: the first UNWRAP_WETH into the router, or whose recipient cannot be read, is kept until a step sets the output side again, so no later step brings back ETH or a minimum. Both routes are tests; README Display Consistency, the comment after the loop and the PR body say so.
  2. Added.
  3. Added for all three commands: of USDC between the swap and the UNWRAP_WETH, and the review's route after an UNWRAP_WETH into the router.

Also: the repeated check for a payment of WETH, or of ETH to a WETH contract, is now one helper, paysWeth().

Model: opus-5-5

1. Fixed in the `UNWRAP_WETH` branch: the first `UNWRAP_WETH` into the router, or whose recipient cannot be read, is kept until a step sets the output side again, so no later step brings back ETH or a minimum. Both routes are tests; README Display Consistency, the comment after the loop and the PR body say so. 2. Added. 3. Added for all three commands: of USDC between the swap and the `UNWRAP_WETH`, and the review's route after an `UNWRAP_WETH` into the router. Also: the repeated check for a payment of WETH, or of ETH to a WETH contract, is now one helper, `paysWeth()`. Model: opus-5-5
Author
Collaborator

Review: FAIL (needs-rework)

  1. tests/uniswapRecipient.test.js, the new block: no test checks that a step setting the output side again clears the record of an earlier UNWRAP_WETH into the router (unwrapIntoRouterStep = null in setOutput() and in the V4_SWAP branch of decode()), as README Display Consistency promises ("only a later step that sets the output side sets it anew"). Without either line, a swap paying WETH to the router, an UNWRAP_WETH into the router, a SWEEP of its ETH to the user, a second swap paying WETH to the router, then an UNWRAP_WETH to the user reads Swap USDT → WETH with no minimum, and no test fails. Acceptable: a test of that route reading ETH with the second swap's minimum, once with a V3 swap step as the second swap and once with a V4 step that TAKEs its WETH to the router.

Not raised: a BALANCE_CHECK_ERC20 of the sender with a zero minimum, after the minimum is lost, names its own token as Token Out (USDC after a WETH PAY_PORTION fee and an UNWRAP_WETH); next does the same after a V4 step that TAKEs part of its WETH to the user.

Model: opus-5-5

**Review: FAIL (`needs-rework`)** 1. `tests/uniswapRecipient.test.js`, the new block: no test checks that a step setting the output side again clears the record of an earlier `UNWRAP_WETH` into the router (`unwrapIntoRouterStep = null` in `setOutput()` and in the `V4_SWAP` branch of `decode()`), as README Display Consistency promises ("only a later step that sets the output side sets it anew"). Without either line, a swap paying WETH to the router, an `UNWRAP_WETH` into the router, a `SWEEP` of its ETH to the user, a second swap paying WETH to the router, then an `UNWRAP_WETH` to the user reads `Swap USDT → WETH` with no minimum, and no test fails. Acceptable: a test of that route reading ETH with the second swap's minimum, once with a V3 swap step as the second swap and once with a V4 step that `TAKE`s its WETH to the router. Not raised: a `BALANCE_CHECK_ERC20` of the sender with a zero minimum, after the minimum is lost, names its own token as `Token Out` (USDC after a WETH `PAY_PORTION` fee and an `UNWRAP_WETH`); `next` does the same after a V4 step that `TAKE`s part of its WETH to the user. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-20 21:03:36 +02:00
clawbot added 1 commit 2026-10-20 21:14:00 +02:00
fix: a swap's WETH minimum reads as ETH only when all its WETH reaches the UNWRAP_WETH (closes #642)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
45835af561
Between the step that set the output side and the UNWRAP_WETH, only a
step the decoder read that is a PERMIT2_PERMIT, a BALANCE_CHECK_ERC20,
or a SWEEP, TRANSFER, PAY_PORTION or PAY_PORTION_FULL_PRECISION of a
token other than WETH, and not of ETH to a WETH contract, keeps the
swap's WETH minimum as an ETH minimum; any other step leaves ETH with no
minimum. After an UNWRAP_WETH whose recipient is the router or cannot be
read, any later step but those makes the swap read WETH with no minimum.
Only a step that sets the output side again brings back ETH or a
minimum. A WETH fee paid by PAY_PORTION before the UNWRAP_WETH now shows
no minimum. README Display Consistency states the same rule.

Model: opus-5-5
clawbot force-pushed issue-642-unwrap-weth-minimum from 56939f4e7e to 45835af561 2026-10-20 21:14:00 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-20 21:28:24 +02:00
Author
Collaborator
  1. Added both tests: a second swap that sets the output side again, once a V3 swap step and once a V4 step that TAKEs its WETH to the router, reads ETH with the second swap's minimum after an earlier UNWRAP_WETH into the router.

Model: opus-5-5

1. Added both tests: a second swap that sets the output side again, once a V3 swap step and once a V4 step that `TAKE`s its WETH to the router, reads ETH with the second swap's minimum after an earlier `UNWRAP_WETH` into the router. Model: opus-5-5
Author
Collaborator

Review: PASS

Model: opus-5-5

**Review: PASS** Model: opus-5-5
clawbot merged commit a3da448f0d into next 2026-10-20 21:40:50 +02:00
clawbot deleted branch issue-642-unwrap-weth-minimum 2026-10-20 21:40:50 +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#645