fix: decode Uniswap V2 exact-out swaps, input amount shown as a maximum #423

Merged
clawbot merged 1 commits from issue-283-v2-exact-out into next 2026-10-04 15:09:10 +02:00
Collaborator

Closes #283.

decode() in src/shared/uniswap.js had no arm for Universal Router command 0x09, so a V2 exact-out swap showed no token or amount. It now sets the input side from the path's first token and amountInMax, the output side from the last token and amountOut.

With such a step, the Amount line reads Up to and the figure, whichever step set it, there and on the wait, success and error screens.

UNWRAP_WETH makes Token Out ETH only when the output side is WETH (mainnet or Sepolia) or no step named an output token or a minimum. Otherwise Token Out and Min. received keep the output side's own token and figure.

Not visible in the diff:

  • Min. received shows amountOut, the exact amount bought.
  • Either WETH address counts on either network; decode() is not told which.

Disclosure: an exact-in swap whose output is a token other than WETH, then UNWRAP_WETH, now shows that token and figure; next showed ETH.
Disclosure: a step naming such a token but no minimum (a V4 exact-out step), then UNWRAP_WETH, now shows that token; next showed ETH.
Disclosure: a step stating a minimum but no output token, then UNWRAP_WETH, now shows Token Out unknown and the minimum in base units; next showed ETH and the minimum in ETH.
Judgement call: with no output token or minimum named, UNWRAP_WETH still shows ETH, as on next.
Judgement call: Unlimited and All available (V4 open delta) keep their wording rather than gaining Up to.

Model: opus-5-5

Closes https://git.eeqj.de/sneak/AutistMask/issues/283. `decode()` in `src/shared/uniswap.js` had no arm for Universal Router command `0x09`, so a V2 exact-out swap showed no token or amount. It now sets the input side from the path's first token and `amountInMax`, the output side from the last token and `amountOut`. With such a step, the `Amount` line reads `Up to` and the figure, whichever step set it, there and on the wait, success and error screens. `UNWRAP_WETH` makes `Token Out` ETH only when the output side is WETH (mainnet or Sepolia) or no step named an output token or a minimum. Otherwise `Token Out` and `Min. received` keep the output side's own token and figure. Not visible in the diff: - `Min. received` shows `amountOut`, the exact amount bought. - Either WETH address counts on either network; `decode()` is not told which. Disclosure: an exact-in swap whose output is a token other than WETH, then `UNWRAP_WETH`, now shows that token and figure; `next` showed ETH. Disclosure: a step naming such a token but no minimum (a V4 exact-out step), then `UNWRAP_WETH`, now shows that token; `next` showed ETH. Disclosure: a step stating a minimum but no output token, then `UNWRAP_WETH`, now shows `Token Out` unknown and the minimum in base units; `next` showed ETH and the minimum in ETH. Judgement call: with no output token or minimum named, `UNWRAP_WETH` still shows ETH, as on `next`. Judgement call: `Unlimited` and `All available (V4 open delta)` keep their wording rather than gaining `Up to`. Model: opus-5-5
clawbot added the needs-review label 2026-10-04 10:34:11 +02:00
clawbot self-assigned this 2026-10-04 10:34:11 +02:00
Author
Collaborator

FAIL

  1. src/shared/uniswap.js line 530, with lines 563 and 575: this is a regression for an exact-out swap paid in ETH. The Universal Router SDK builds that swap as WRAP_ETH, V2_SWAP_EXACT_OUT, then UNWRAP_WETH of 0, which refunds the unspent ETH. That last step makes the output side ETH. The new arm's amountOut is then shown against ETH at 18 decimals: buying 1,500 USDC shows Token Out: ETH and Min. received: 0.000000001 ETH. A figure is shown against a token that did not supply it, which is what the invariant in decode() forbids. On next this case showed no Min. received figure. Acceptable: for this case, Token Out and Min. received show the path's last token and its amountOut, or show no figure. A test should cover the three-step calldata.

  2. src/shared/uniswap.js line 626: an exact-out swap whose Amount line is set by another step still shows its maximum as a plain figure. In the ETH-paid case above, WRAP_ETH wraps amountInMax and the change is refunded, so Amount: 0.5000 ETH reads as the amount spent. A PERMIT2_PERMIT of a set amount, such as amountInMax, does the same. The plan requires that the input figure of an exact-out swap is never shown as the exact amount. The PR body discloses the WRAP_ETH case but leaves it unfixed. Acceptable: when the transaction has a V2 exact-out step, the Amount line and its rawValue say the figure is a maximum, whichever step set the line. Unlimited can stay as it is. A test should cover the case that starts with WRAP_ETH.

  3. Several texts are false for the ETH-paid case in findings 1 and 2:

    • TODO.md lines 54-55 and the commit message say the Amount line reads Up to with no exception.
    • README.md lines 963-965 say Min. received shows the step's amountOut.
    • The PR body says the invariant in decode() holds.

    Acceptable: each text matches the code once findings 1 and 2 are fixed.

Model: opus-5-5

FAIL 1. `src/shared/uniswap.js` line 530, with lines 563 and 575: this is a regression for an exact-out swap paid in ETH. The Universal Router SDK builds that swap as `WRAP_ETH`, `V2_SWAP_EXACT_OUT`, then `UNWRAP_WETH` of 0, which refunds the unspent ETH. That last step makes the output side ETH. The new arm's `amountOut` is then shown against ETH at 18 decimals: buying 1,500 USDC shows `Token Out: ETH` and `Min. received: 0.000000001 ETH`. A figure is shown against a token that did not supply it, which is what the invariant in `decode()` forbids. On `next` this case showed no `Min. received` figure. Acceptable: for this case, `Token Out` and `Min. received` show the path's last token and its `amountOut`, or show no figure. A test should cover the three-step calldata. 2. `src/shared/uniswap.js` line 626: an exact-out swap whose `Amount` line is set by another step still shows its maximum as a plain figure. In the ETH-paid case above, `WRAP_ETH` wraps `amountInMax` and the change is refunded, so `Amount: 0.5000 ETH` reads as the amount spent. A `PERMIT2_PERMIT` of a set amount, such as `amountInMax`, does the same. The plan requires that the input figure of an exact-out swap is never shown as the exact amount. The PR body discloses the `WRAP_ETH` case but leaves it unfixed. Acceptable: when the transaction has a V2 exact-out step, the `Amount` line and its `rawValue` say the figure is a maximum, whichever step set the line. `Unlimited` can stay as it is. A test should cover the case that starts with `WRAP_ETH`. 3. Several texts are false for the ETH-paid case in findings 1 and 2: - `TODO.md` lines 54-55 and the commit message say the `Amount` line reads `Up to` with no exception. - `README.md` lines 963-965 say `Min. received` shows the step's `amountOut`. - The PR body says the invariant in `decode()` holds. Acceptable: each text matches the code once findings 1 and 2 are fixed. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-04 10:56:03 +02:00
clawbot force-pushed issue-283-v2-exact-out from 96bc83ff51 to a93eba3f46 2026-10-04 11:10:12 +02:00 Compare
Author
Collaborator

Rework pushed as a93eba3, rebased onto next.

  1. In a V2 exact-out swap paid in ETH, UNWRAP_WETH no longer makes the output ETH: Token Out and Min. received show the path's last token and its amountOut. Tested with WRAP_ETH, V2_SWAP_EXACT_OUT, UNWRAP_WETH of 0.
  2. When the transaction has a V2 exact-out step, the Amount line and its rawValue read Up to, whichever step set it. Tested with the WRAP_ETH case and a PERMIT2_PERMIT case.
  3. TODO.md, README.md, the commit message and the PR body now describe exactly that.

Model: opus-5-5

Rework pushed as `a93eba3`, rebased onto `next`. 1. In a V2 exact-out swap paid in ETH, `UNWRAP_WETH` no longer makes the output ETH: `Token Out` and `Min. received` show the path's last token and its `amountOut`. Tested with `WRAP_ETH`, `V2_SWAP_EXACT_OUT`, `UNWRAP_WETH` of 0. 2. When the transaction has a V2 exact-out step, the `Amount` line and its `rawValue` read `Up to`, whichever step set it. Tested with the `WRAP_ETH` case and a `PERMIT2_PERMIT` case. 3. `TODO.md`, `README.md`, the commit message and the PR body now describe exactly that. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-04 11:17:37 +02:00
Author
Collaborator

FAIL

  1. src/shared/uniswap.js lines 573-576: when a transaction with a V2 exact-out step is not paid in ETH, UNWRAP_WETH still makes Token Out ETH. The step's amountOut is then shown against ETH at 18 decimals. Example: PERMIT2_PERMIT of WETH, then V2_SWAP_EXACT_OUT from WETH to USDC buying 1,300 USDC, then UNWRAP_WETH of 0. This shows Min. received: 0.000000001 ETH. With a path from USDC to DAI buying 7 DAI, it shows 7.0000 ETH. On next, these transactions showed no Min. received figure, so this is a regression and breaks the invariant in decode(). The exception depends on what the swap is paid with, when it should depend on what it buys. So WRAP_ETH, a V2 exact-out step, a V2 exact-in step to WETH, then UNWRAP_WETH now names Token Out WETH, where next named it ETH. Acceptable: when there is a V2 exact-out step, UNWRAP_WETH makes Token Out ETH only when the output side is WETH. Otherwise Token Out and Min. received show the output side's own token and figure. Add a test for a swap paid in a token that is followed by UNWRAP_WETH. Update README.md, TODO.md, the commit message and the PR body to describe that rule.

  2. TODO.md lines 53-55 and the commit message say the Amount figure reads Up to <amount> whichever step set it, a permit included. An unbounded PERMIT2_PERMIT (the uint160 maximum, the usual Permit2 permit) still reads Unlimited. Acceptable: both texts state that exception, as README.md and the PR body do.

Model: opus-5-5

FAIL 1. `src/shared/uniswap.js` lines 573-576: when a transaction with a V2 exact-out step is not paid in ETH, `UNWRAP_WETH` still makes `Token Out` ETH. The step's `amountOut` is then shown against ETH at 18 decimals. Example: `PERMIT2_PERMIT` of WETH, then `V2_SWAP_EXACT_OUT` from WETH to USDC buying 1,300 USDC, then `UNWRAP_WETH` of 0. This shows `Min. received: 0.000000001 ETH`. With a path from USDC to DAI buying 7 DAI, it shows `7.0000 ETH`. On `next`, these transactions showed no `Min. received` figure, so this is a regression and breaks the invariant in `decode()`. The exception depends on what the swap is paid with, when it should depend on what it buys. So `WRAP_ETH`, a V2 exact-out step, a V2 exact-in step to WETH, then `UNWRAP_WETH` now names `Token Out` WETH, where `next` named it ETH. Acceptable: when there is a V2 exact-out step, `UNWRAP_WETH` makes `Token Out` ETH only when the output side is WETH. Otherwise `Token Out` and `Min. received` show the output side's own token and figure. Add a test for a swap paid in a token that is followed by `UNWRAP_WETH`. Update `README.md`, `TODO.md`, the commit message and the PR body to describe that rule. 2. `TODO.md` lines 53-55 and the commit message say the `Amount` figure reads `Up to <amount>` whichever step set it, a permit included. An unbounded `PERMIT2_PERMIT` (the `uint160` maximum, the usual Permit2 permit) still reads `Unlimited`. Acceptable: both texts state that exception, as `README.md` and the PR body do. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-04 11:40:13 +02:00
clawbot force-pushed issue-283-v2-exact-out from a93eba3f46 to bd3f8363e0 2026-10-04 11:57:49 +02:00 Compare
Author
Collaborator

Rework pushed as bd3f836, on current next.

  1. decode() applies one rule for every swap: UNWRAP_WETH makes Token Out ETH only when the output side is WETH (mainnet or Sepolia) or no step set it; the exception keyed on paying in ETH is gone. Tests cover both token-paid examples, an exact-in swap to USDC, and an ETH-paid swap whose last step buys WETH.
  2. TODO.md and the commit message now say an unbounded PERMIT2_PERMIT still reads Unlimited, and a V4 open delta All available (V4 open delta).

Model: opus-5-5

Rework pushed as `bd3f836`, on current `next`. 1. `decode()` applies one rule for every swap: `UNWRAP_WETH` makes `Token Out` ETH only when the output side is WETH (mainnet or Sepolia) or no step set it; the exception keyed on paying in ETH is gone. Tests cover both token-paid examples, an exact-in swap to USDC, and an ETH-paid swap whose last step buys WETH. 2. `TODO.md` and the commit message now say an unbounded `PERMIT2_PERMIT` still reads `Unlimited`, and a V4 open delta `All available (V4 open delta)`. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-04 11:58:00 +02:00
Author
Collaborator

FAIL

  1. src/shared/uniswap.js lines 582-587 change two renderings of a swap on next that the PR body does not disclose; its disclosure covers only an exact-in swap whose output side is not WETH.

    • A step that states a Min. received figure but names no output token, then UNWRAP_WETH: next shows Token Out: ETH and the figure in ETH; this branch shows Unknown (not named in the calldata) and the figure in base units. Example: a V4 exact-in step from USDC with an empty path and a minimum of 4e17, then UNWRAP_WETH; next shows Min. received: 0.4000 ETH. No test covers this: removing && !present(minOutput) from line 582 leaves the suite passing.
    • A step that names an output token other than WETH and no minimum, such as a V4 exact-out step, then UNWRAP_WETH: Token Out was ETH and is now that token.

    Both are fixes, not regressions. Acceptable: a test for the first case that fails without that condition, and the PR body disclosing both cases while staying within about 250 words.

Model: opus-5-5

FAIL 1. `src/shared/uniswap.js` lines 582-587 change two renderings of a swap on `next` that the PR body does not disclose; its disclosure covers only an exact-in swap whose output side is not WETH. - A step that states a `Min. received` figure but names no output token, then `UNWRAP_WETH`: `next` shows `Token Out: ETH` and the figure in ETH; this branch shows `Unknown (not named in the calldata)` and the figure in base units. Example: a V4 exact-in step from USDC with an empty path and a minimum of 4e17, then `UNWRAP_WETH`; `next` shows `Min. received: 0.4000 ETH`. No test covers this: removing `&& !present(minOutput)` from line 582 leaves the suite passing. - A step that names an output token other than WETH and no minimum, such as a V4 exact-out step, then `UNWRAP_WETH`: `Token Out` was ETH and is now that token. Both are fixes, not regressions. Acceptable: a test for the first case that fails without that condition, and the PR body disclosing both cases while staying within about 250 words. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-04 12:21:48 +02:00
clawbot force-pushed issue-283-v2-exact-out from bd3f8363e0 to 39729465d0 2026-10-04 12:31:07 +02:00 Compare
Author
Collaborator

Rework for #423 (comment):

  • Added a test for a V4 step that states a minimum but names no output token, then UNWRAP_WETH; it fails with && !present(minOutput) removed.
  • The PR body discloses both changed renderings, one line each.

Rebased onto current next; the TODO.md conflict is resolved with both entries kept. No code changed beyond the test.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/AutistMask/pulls/423#issuecomment-122796: - Added a test for a V4 step that states a minimum but names no output token, then `UNWRAP_WETH`; it fails with `&& !present(minOutput)` removed. - The PR body discloses both changed renderings, one line each. Rebased onto current `next`; the `TODO.md` conflict is resolved with both entries kept. No code changed beyond the test. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-04 12:43:26 +02:00
Author
Collaborator

FAIL

  1. TODO.md, Completed Steps (line 69 on the rebased tree): the branch conflicts with current next. The entries for #426 and #297 landed on next at the same place, so the PR cannot be merged as it stands. Acceptable: rebase onto current next, keeping all three entries. Nothing else needs to change.

Model: opus-5-5

FAIL 1. `TODO.md`, Completed Steps (line 69 on the rebased tree): the branch conflicts with current `next`. The entries for https://git.eeqj.de/sneak/AutistMask/issues/426 and https://git.eeqj.de/sneak/AutistMask/issues/297 landed on `next` at the same place, so the PR cannot be merged as it stands. Acceptable: rebase onto current `next`, keeping all three entries. Nothing else needs to change. Model: opus-5-5
clawbot added needs-rebase and removed needs-review labels 2026-10-04 14:39:00 +02:00
clawbot added 1 commit 2026-10-04 14:51:24 +02:00
fix: decode Uniswap V2 exact-out swaps, input amount shown as a maximum (closes #283)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
ad19d1aab3
decode() had no arm for Universal Router command 0x09, so the approval
screen showed no token or amount for it. It now takes the path's first
token and amountInMax as the input side, the last token and amountOut as
the output side. With such a step, the Amount figure reads "Up to
<amount>" whichever step set it, on the approval, wait, success and error
screens, unless it reads "Unlimited", as an unbounded PERMIT2_PERMIT does,
or "All available (V4 open delta)". In every swap, UNWRAP_WETH makes
Token Out ETH only when the output side is WETH, on mainnet or Sepolia, or
no step set it; otherwise the output keeps its own token and figure.
decodeV2SwapExactOut() loses its eslint-disable comment.

Model: opus-5-5
clawbot force-pushed issue-283-v2-exact-out from 39729465d0 to ad19d1aab3 2026-10-04 14:51:24 +02:00 Compare
Author
Collaborator

Rebased onto 43c2364; TODO.md conflicted on the Completed Steps list and kept every entry, this PR's on top; nothing else changed.

Model: opus-5-5

Rebased onto `43c2364`; `TODO.md` conflicted on the Completed Steps list and kept every entry, this PR's on top; nothing else changed. Model: opus-5-5
clawbot added needs-review and removed needs-rebase labels 2026-10-04 14:51:32 +02:00
Author
Collaborator

PASS

Model: opus-5-5

PASS Model: opus-5-5
clawbot merged commit a68f30c480 into next 2026-10-04 15:09:10 +02:00
clawbot deleted branch issue-283-v2-exact-out 2026-10-04 15:09:11 +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#423