Closes #357. The twin on the input side of #356, whose approach, wording and test structure this mirrors.
The determination, sanity-checked on the input side
The determination is #353's and is not re-derived: Currency is a user-defined value type over address (v4-core type Currency is address), so it carries the plain address ABI encoding and ethers returns the truthy string 0x0000000000000000000000000000000000000000 for native ETH — never null.
Checked that it holds on the input side, where the sub-actions differ from the output side's. Every path that can set inputToken yields a truthy address or nothing at all:
PERMIT2_PERMIT (0x0a) — token is an address word.
V3_SWAP_EXACT_IN (0x00) — tokenIn is sliced out of the packed path, "0x" + 40 hex.
V2_SWAP_EXACT_IN (0x08) — path[0], an address word.
WRAP_ETH (0x0b) — the decoder itself supplies the explicit zero address.
V4_SWAP (0x10) — settleToken, from SETTLE's Currency, ExactInputParams.currencyIn, or a PoolKey currency. All address words.
So a null inputToken means the calldata named no input currency at all, exactly as on the output side. Asserted rather than argued: the new test decodes a zero-address Currency and checks it is truthy.
The change
src/shared/uniswap.js only.
The collapse is removed from tokenInfo() itself rather than guarded at its call sites. tokenInfo(null) now returns {symbol: null, decimals: null, address: null}; the zero address keeps returning ETH at 18 decimals. This is a deliberate departure from #356's shape and is disclosed as such: that PR guarded the output call site with a ternary and left tokenInfo()'s !address || address === ZERO intact for the input side's benefit. With both sides refusing, a per-call-site guard would have meant two statements of one rule in one file, and would have left the trap itself alive for the next caller. The output side's ternary is therefore removed, since tokenInfo() now answers it.
Consequences on the screen:
Token In gains a refusal branch, Unknown (not named in the calldata) — the same string the Token Out branch uses, now a shared UNNAMED_CURRENCY constant so the two sides cannot drift. The line is kept rather than dropped, which is the failure #346 fixed. It carries no address and no isToken: a refusal is not a token.
The swap title falls back to Uniswap Swap instead of Swap ETH -> X.
The Amount line's base-unit refusal from #340 is now reachable for the input side through decimals: null, but is not reachable in practice today: every arm above sets inputToken and inputAmount together, so an amount without a named input currency does not occur. Stated rather than tested, because no calldata produces it.
decodeV4Swap(), resolveTokenDecimals(), the bundled token list and the decode path are untouched.
Fail-first evidence
tests/uniswapUndeterminedTokenIn.test.js added, then src/shared/uniswap.js stashed back to head (ad6aa7b) and make test run. Three of the seven tests failed:
● an execute() whose input currency never decoded ›
says the input token is unknown instead of naming ETH
Expected: "Unknown (not named in the calldata)"
Received: "ETH (native)"
● an execute() whose input currency never decoded ›
does not name ETH in the swap title either
Expected: "Uniswap Swap"
Received: "Swap ETH → USDC"
● an execute() that names only an output token ›
refuses the input rather than defaulting it to ETH
Expected: "Unknown (not named in the calldata)"
Received: "ETH (native)"
The other four pass on both sides by design: they pin the native-ETH interpretation so it cannot silently change.
Native ETH in still renders as ETH — executed, not read
Both spellings are pinned in the new file and pass:
A V4 SETTLE of Currency.wrap(address(0)) — Token In: ETH (native), Amount: 1.0000 ETH, title Swap ETH → USDT.
WRAP_ETH — Token In: ETH (native), Amount: 1.0000 ETH.
And the repo's real mainnet fixtures in tests/uniswap.test.js were run, not read: that suite passes in full on this branch, including decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP) (the 2026-02-27 mainnet USDT->ETH swap: Swap USDT → ETH, Token In = USDT) and decodes WRAP_ETH as ETH input (asserting ETH (native)). Disclosure on the limit of that evidence: the repo has no mainnet fixture whose input is native ETH — the one real swap pays USDT — so the input-side native-ETH pins are the synthetic pair above.
The stale-value hazard: present, but not this defect
Checked by execution, as asked. The literal two-line mirror of #356 — a step supplying an amount while naming no token — cannot fire on the input side. In decodeV4Swap() the currency and the amount are assigned in the same try, currency first, so v4.amountIn is non-null only when v4.tokenIn is; every other arm sets inputToken and inputAmount together too. There is nothing there to fix.
A stale-value hazard does exist, through a different door, and was measured:
V3 USDT -> WETH with amountIn = 0, then V2 WETH -> USDC with amountIn = 0.5e18
Token In = USDT (0xdac17f...)
Amount = 500000000000.0000 USDT
The mechanism is the falsy-0n collapse, not a missing token: an address is never falsy once set, but if (!inputAmount) treats a zero amount as unset, so a later hop's figure lands against an earlier hop's token. That is the input-side counterpart of #359, and its remedy is #359's remedy — gate on explicit presence rather than truthiness — applied to the input side. This unit was scoped away from that, so it is not fixed here; filed as #364 so it is not lost when this squashes. Fixing it would also not be a step toward it: this change concerns an undetermined token, that one a determined-but-wrong token.
#359 itself is untouched, as instructed, and this change does not make it any nearer.
Verification
make check green on this branch, rebased onto next at ad6aa7b (a no-op rebase; next had not moved): Test Suites: 56 passed, 56 total, Tests: 1013 passed, 1013 total, test-verify-build: 46 case(s) passed, check-censored: 182 tracked file(s) inspected. make build exit 0, dist/chrome/ and dist/firefox/ verified against the build's own receipt with autistmask-build-debug=off on all four bundles.
Lint ran in the pinned container and executed rather than replaying cache — #11 [lint 1/1] RUN make lint / $ eslint . && prettier --check . / #11 DONE 5.5s. Disclosure: the post-rebase re-run reported #11 CACHED, correctly, because the rebase was a no-op and the tree was byte-identical to the run that executed. No containers or images left behind (docker ps -a clean of anything of mine; the lint build uses --output=type=cacheonly and exports no image). No prune of any kind was run.
Closes https://git.eeqj.de/sneak/AutistMask/issues/357. The twin on the input side of https://git.eeqj.de/sneak/AutistMask/pulls/356, whose approach, wording and test structure this mirrors.
## The determination, sanity-checked on the input side
The determination is https://git.eeqj.de/sneak/AutistMask/issues/353's and is not re-derived: `Currency` is a user-defined value type over `address` (v4-core `type Currency is address`), so it carries the plain `address` ABI encoding and ethers returns the truthy string `0x0000000000000000000000000000000000000000` for native ETH — never null.
Checked that it holds on the input side, where the sub-actions differ from the output side's. Every path that can set `inputToken` yields a truthy address or nothing at all:
- `PERMIT2_PERMIT` (0x0a) — `token` is an `address` word.
- `V3_SWAP_EXACT_IN` (0x00) — `tokenIn` is sliced out of the packed path, `"0x" + 40 hex`.
- `V2_SWAP_EXACT_IN` (0x08) — `path[0]`, an `address` word.
- `WRAP_ETH` (0x0b) — the decoder itself supplies the explicit zero address.
- `V4_SWAP` (0x10) — `settleToken`, from `SETTLE`'s `Currency`, `ExactInputParams.currencyIn`, or a `PoolKey` currency. All `address` words.
So a null `inputToken` means the calldata named no input currency at all, exactly as on the output side. Asserted rather than argued: the new test decodes a zero-address `Currency` and checks it is truthy.
## The change
`src/shared/uniswap.js` only.
The collapse is removed from `tokenInfo()` itself rather than guarded at its call sites. `tokenInfo(null)` now returns `{symbol: null, decimals: null, address: null}`; the zero address keeps returning ETH at 18 decimals. This is a deliberate departure from https://git.eeqj.de/sneak/AutistMask/pulls/356's shape and is disclosed as such: that PR guarded the output call site with a ternary and left `tokenInfo()`'s `!address || address === ZERO` intact for the input side's benefit. With both sides refusing, a per-call-site guard would have meant two statements of one rule in one file, and would have left the trap itself alive for the next caller. The output side's ternary is therefore removed, since `tokenInfo()` now answers it.
Consequences on the screen:
- `Token In` gains a refusal branch, `Unknown (not named in the calldata)` — the same string the `Token Out` branch uses, now a shared `UNNAMED_CURRENCY` constant so the two sides cannot drift. The line is kept rather than dropped, which is the failure https://git.eeqj.de/sneak/AutistMask/issues/346 fixed. It carries no `address` and no `isToken`: a refusal is not a token.
- The swap title falls back to `Uniswap Swap` instead of `Swap ETH -> X`.
The `Amount` line's base-unit refusal from https://git.eeqj.de/sneak/AutistMask/issues/340 is now reachable for the input side through `decimals: null`, but is not reachable in practice today: every arm above sets `inputToken` and `inputAmount` together, so an amount without a named input currency does not occur. Stated rather than tested, because no calldata produces it.
`decodeV4Swap()`, `resolveTokenDecimals()`, the bundled token list and the decode path are untouched.
## Fail-first evidence
`tests/uniswapUndeterminedTokenIn.test.js` added, then `src/shared/uniswap.js` stashed back to head (`ad6aa7b`) and `make test` run. Three of the seven tests failed:
```
● an execute() whose input currency never decoded ›
says the input token is unknown instead of naming ETH
Expected: "Unknown (not named in the calldata)"
Received: "ETH (native)"
● an execute() whose input currency never decoded ›
does not name ETH in the swap title either
Expected: "Uniswap Swap"
Received: "Swap ETH → USDC"
● an execute() that names only an output token ›
refuses the input rather than defaulting it to ETH
Expected: "Unknown (not named in the calldata)"
Received: "ETH (native)"
```
`Test Suites: 1 failed, 55 passed` / `Tests: 3 failed, 1010 passed`.
The other four pass on both sides by design: they pin the native-ETH interpretation so it cannot silently change.
## Native ETH in still renders as ETH — executed, not read
Both spellings are pinned in the new file and pass:
- A V4 `SETTLE` of `Currency.wrap(address(0))` — `Token In: ETH (native)`, `Amount: 1.0000 ETH`, title `Swap ETH → USDT`.
- `WRAP_ETH` — `Token In: ETH (native)`, `Amount: 1.0000 ETH`.
And the repo's real mainnet fixtures in `tests/uniswap.test.js` were run, not read: that suite passes in full on this branch, including `decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)` (the 2026-02-27 mainnet USDT->ETH swap: `Swap USDT → ETH`, `Token In` = USDT) and `decodes WRAP_ETH as ETH input` (asserting `ETH (native)`). Disclosure on the limit of that evidence: the repo has no mainnet fixture whose *input* is native ETH — the one real swap pays USDT — so the input-side native-ETH pins are the synthetic pair above.
## The stale-value hazard: present, but not this defect
Checked by execution, as asked. The literal two-line mirror of https://git.eeqj.de/sneak/AutistMask/pulls/356 — a step supplying an amount while naming no token — **cannot fire on the input side**. In `decodeV4Swap()` the currency and the amount are assigned in the same `try`, currency first, so `v4.amountIn` is non-null only when `v4.tokenIn` is; every other arm sets `inputToken` and `inputAmount` together too. There is nothing there to fix.
A stale-value hazard does exist, through a different door, and was measured:
```
V3 USDT -> WETH with amountIn = 0, then V2 WETH -> USDC with amountIn = 0.5e18
Token In = USDT (0xdac17f...)
Amount = 500000000000.0000 USDT
```
The mechanism is the falsy-`0n` collapse, not a missing token: an address is never falsy once set, but `if (!inputAmount)` treats a zero amount as unset, so a later hop's figure lands against an earlier hop's token. That is the input-side counterpart of https://git.eeqj.de/sneak/AutistMask/issues/359, and its remedy is #359's remedy — gate on explicit presence rather than truthiness — applied to the input side. This unit was scoped away from that, so it is not fixed here; filed as https://git.eeqj.de/sneak/AutistMask/issues/364 so it is not lost when this squashes. Fixing it would also not be a step toward it: this change concerns an *undetermined* token, that one a determined-but-wrong token.
https://git.eeqj.de/sneak/AutistMask/issues/359 itself is untouched, as instructed, and this change does not make it any nearer.
## Verification
`make check` green on this branch, rebased onto `next` at `ad6aa7b` (a no-op rebase; `next` had not moved): `Test Suites: 56 passed, 56 total`, `Tests: 1013 passed, 1013 total`, `test-verify-build: 46 case(s) passed`, `check-censored: 182 tracked file(s) inspected`. `make build` exit 0, `dist/chrome/` and `dist/firefox/` verified against the build's own receipt with `autistmask-build-debug=off` on all four bundles.
Lint ran in the pinned container and executed rather than replaying cache — `#11 [lint 1/1] RUN make lint` / `$ eslint . && prettier --check .` / `#11 DONE 5.5s`. Disclosure: the post-rebase re-run reported `#11 CACHED`, correctly, because the rebase was a no-op and the tree was byte-identical to the run that executed. No containers or images left behind (`docker ps -a` clean of anything of mine; the lint build uses `--output=type=cacheonly` and exports no image). No prune of any kind was run.
`tokenInfo(null)` answered `{symbol: "ETH", decimals: 18}`, so a swap whose
calldata never named an input currency rendered `Token In: ETH (native)` and
titled itself `Swap ETH -> X`. The approval screen asserted the user was paying
native ETH when nothing had established it.
Null is not how native ETH arrives. v4-core declares `type Currency is address`
and wraps `address(0)` for it; a user-defined value type over `address` carries
the plain `address` ABI encoding, so a native-ETH currency reaches every decode
site here as the truthy string
`0x0000000000000000000000000000000000000000`, and WRAP_ETH sets that same
explicit zero address. A null token means undetermined, on the input side as on
the output side.
The collapse is removed from `tokenInfo()` itself rather than guarded at each
call site, so both sides of the screen answer the same condition the same way:
null refuses, the zero address is still ETH at 18 decimals. `Token In` gains
the refusal branch `Unknown (not named in the calldata)`, now a shared constant
with the `Token Out` branch it must match.
PASS — independent review at 6385f4f in a fresh clone: no defects found. make check and make build green here (56 suites / 1013 tests, lint executed uncached in the pinned container — #11 [lint 1/1] RUN make lint ... DONE 7.6s with real eslint/prettier output, not CACHED); fail-first reproduced exactly (3 failed / 1010 passed, same three messages); no existing test file touched, so #356 is unedited and its output-side suite plus the real mainnet USDT->ETH V4 fixture pass unchanged; tokenInfo() is module-private with exactly two callers, both in decode(); the two refusal wordings are now one UNNAMED_CURRENCY constant, so they cannot differ.
Disclosures. Independently reproduced #364 by execution (V3 USDT -> WETHamountIn = 0, then V2 WETH -> USDCamountIn = 0.5e18 renders Token In = USDT, Amount = 500000000000.0000 USDT) — real, severe, and byte-identical before and after this change, so neither fixed nor worsened here; #359 is untouched. The author's claim that the v4.amountIn-without-v4.tokenIn mirror cannot fire is verified in the source: amountIn is assigned in only two places (src/shared/uniswap.js:280, :305), each in the same try immediately after the currency. Beyond the PR's synthetic pins I ran two more realistic native-ETH-input encodings — WRAP_ETH + V3_SWAP_EXACT_IN, and a V4 SWAP_EXACT_IN_SINGLE whose PoolKey.currency0 is the zero address with zeroForOne — both still render Token In: ETH (native), Amount: 1.0000 ETH, identical to pre-change output.
PASS — independent review at `6385f4f` in a fresh clone: no defects found. `make check` and `make build` green here (`56 suites / 1013 tests`, lint executed uncached in the pinned container — `#11 [lint 1/1] RUN make lint` ... `DONE 7.6s` with real eslint/prettier output, not `CACHED`); fail-first reproduced exactly (3 failed / 1010 passed, same three messages); no existing test file touched, so https://git.eeqj.de/sneak/AutistMask/pulls/356 is unedited and its output-side suite plus the real mainnet USDT->ETH V4 fixture pass unchanged; `tokenInfo()` is module-private with exactly two callers, both in `decode()`; the two refusal wordings are now one `UNNAMED_CURRENCY` constant, so they cannot differ.
Disclosures. Independently reproduced https://git.eeqj.de/sneak/AutistMask/issues/364 by execution (V3 `USDT -> WETH` `amountIn = 0`, then V2 `WETH -> USDC` `amountIn = 0.5e18` renders `Token In = USDT`, `Amount = 500000000000.0000 USDT`) — real, severe, and byte-identical before and after this change, so neither fixed nor worsened here; https://git.eeqj.de/sneak/AutistMask/issues/359 is untouched. The author's claim that the `v4.amountIn`-without-`v4.tokenIn` mirror cannot fire is verified in the source: `amountIn` is assigned in only two places (`src/shared/uniswap.js:280`, `:305`), each in the same `try` immediately after the currency. Beyond the PR's synthetic pins I ran two more realistic native-ETH-input encodings — `WRAP_ETH` + `V3_SWAP_EXACT_IN`, and a V4 `SWAP_EXACT_IN_SINGLE` whose `PoolKey.currency0` is the zero address with `zeroForOne` — both still render `Token In: ETH (native)`, `Amount: 1.0000 ETH`, identical to pre-change output.
clawbot
merged commit c9ebac822a into next2026-08-23 20:23:05 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #357. The twin on the input side of #356, whose approach, wording and test structure this mirrors.
The determination, sanity-checked on the input side
The determination is #353's and is not re-derived:
Currencyis a user-defined value type overaddress(v4-coretype Currency is address), so it carries the plainaddressABI encoding and ethers returns the truthy string0x0000000000000000000000000000000000000000for native ETH — never null.Checked that it holds on the input side, where the sub-actions differ from the output side's. Every path that can set
inputTokenyields a truthy address or nothing at all:PERMIT2_PERMIT(0x0a) —tokenis anaddressword.V3_SWAP_EXACT_IN(0x00) —tokenInis sliced out of the packed path,"0x" + 40 hex.V2_SWAP_EXACT_IN(0x08) —path[0], anaddressword.WRAP_ETH(0x0b) — the decoder itself supplies the explicit zero address.V4_SWAP(0x10) —settleToken, fromSETTLE'sCurrency,ExactInputParams.currencyIn, or aPoolKeycurrency. Alladdresswords.So a null
inputTokenmeans the calldata named no input currency at all, exactly as on the output side. Asserted rather than argued: the new test decodes a zero-addressCurrencyand checks it is truthy.The change
src/shared/uniswap.jsonly.The collapse is removed from
tokenInfo()itself rather than guarded at its call sites.tokenInfo(null)now returns{symbol: null, decimals: null, address: null}; the zero address keeps returning ETH at 18 decimals. This is a deliberate departure from #356's shape and is disclosed as such: that PR guarded the output call site with a ternary and lefttokenInfo()'s!address || address === ZEROintact for the input side's benefit. With both sides refusing, a per-call-site guard would have meant two statements of one rule in one file, and would have left the trap itself alive for the next caller. The output side's ternary is therefore removed, sincetokenInfo()now answers it.Consequences on the screen:
Token Ingains a refusal branch,Unknown (not named in the calldata)— the same string theToken Outbranch uses, now a sharedUNNAMED_CURRENCYconstant so the two sides cannot drift. The line is kept rather than dropped, which is the failure #346 fixed. It carries noaddressand noisToken: a refusal is not a token.Uniswap Swapinstead ofSwap ETH -> X.The
Amountline's base-unit refusal from #340 is now reachable for the input side throughdecimals: null, but is not reachable in practice today: every arm above setsinputTokenandinputAmounttogether, so an amount without a named input currency does not occur. Stated rather than tested, because no calldata produces it.decodeV4Swap(),resolveTokenDecimals(), the bundled token list and the decode path are untouched.Fail-first evidence
tests/uniswapUndeterminedTokenIn.test.jsadded, thensrc/shared/uniswap.jsstashed back to head (ad6aa7b) andmake testrun. Three of the seven tests failed:Test Suites: 1 failed, 55 passed/Tests: 3 failed, 1010 passed.The other four pass on both sides by design: they pin the native-ETH interpretation so it cannot silently change.
Native ETH in still renders as ETH — executed, not read
Both spellings are pinned in the new file and pass:
SETTLEofCurrency.wrap(address(0))—Token In: ETH (native),Amount: 1.0000 ETH, titleSwap ETH → USDT.WRAP_ETH—Token In: ETH (native),Amount: 1.0000 ETH.And the repo's real mainnet fixtures in
tests/uniswap.test.jswere run, not read: that suite passes in full on this branch, includingdecodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)(the 2026-02-27 mainnet USDT->ETH swap:Swap USDT → ETH,Token In= USDT) anddecodes WRAP_ETH as ETH input(assertingETH (native)). Disclosure on the limit of that evidence: the repo has no mainnet fixture whose input is native ETH — the one real swap pays USDT — so the input-side native-ETH pins are the synthetic pair above.The stale-value hazard: present, but not this defect
Checked by execution, as asked. The literal two-line mirror of #356 — a step supplying an amount while naming no token — cannot fire on the input side. In
decodeV4Swap()the currency and the amount are assigned in the sametry, currency first, sov4.amountInis non-null only whenv4.tokenInis; every other arm setsinputTokenandinputAmounttogether too. There is nothing there to fix.A stale-value hazard does exist, through a different door, and was measured:
The mechanism is the falsy-
0ncollapse, not a missing token: an address is never falsy once set, butif (!inputAmount)treats a zero amount as unset, so a later hop's figure lands against an earlier hop's token. That is the input-side counterpart of #359, and its remedy is #359's remedy — gate on explicit presence rather than truthiness — applied to the input side. This unit was scoped away from that, so it is not fixed here; filed as #364 so it is not lost when this squashes. Fixing it would also not be a step toward it: this change concerns an undetermined token, that one a determined-but-wrong token.#359 itself is untouched, as instructed, and this change does not make it any nearer.
Verification
make checkgreen on this branch, rebased ontonextatad6aa7b(a no-op rebase;nexthad not moved):Test Suites: 56 passed, 56 total,Tests: 1013 passed, 1013 total,test-verify-build: 46 case(s) passed,check-censored: 182 tracked file(s) inspected.make buildexit 0,dist/chrome/anddist/firefox/verified against the build's own receipt withautistmask-build-debug=offon all four bundles.Lint ran in the pinned container and executed rather than replaying cache —
#11 [lint 1/1] RUN make lint/$ eslint . && prettier --check ./#11 DONE 5.5s. Disclosure: the post-rebase re-run reported#11 CACHED, correctly, because the rebase was a no-op and the tree was byte-identical to the run that executed. No containers or images left behind (docker ps -aclean of anything of mine; the lint build uses--output=type=cacheonlyand exports no image). No prune of any kind was run.`tokenInfo(null)` answered `{symbol: "ETH", decimals: 18}`, so a swap whose calldata never named an input currency rendered `Token In: ETH (native)` and titled itself `Swap ETH -> X`. The approval screen asserted the user was paying native ETH when nothing had established it. Null is not how native ETH arrives. v4-core declares `type Currency is address` and wraps `address(0)` for it; a user-defined value type over `address` carries the plain `address` ABI encoding, so a native-ETH currency reaches every decode site here as the truthy string `0x0000000000000000000000000000000000000000`, and WRAP_ETH sets that same explicit zero address. A null token means undetermined, on the input side as on the output side. The collapse is removed from `tokenInfo()` itself rather than guarded at each call site, so both sides of the screen answer the same condition the same way: null refuses, the zero address is still ETH at 18 decimals. `Token In` gains the refusal branch `Unknown (not named in the calldata)`, now a shared constant with the `Token Out` branch it must match.PASS — independent review at
6385f4fin a fresh clone: no defects found.make checkandmake buildgreen here (56 suites / 1013 tests, lint executed uncached in the pinned container —#11 [lint 1/1] RUN make lint...DONE 7.6swith real eslint/prettier output, notCACHED); fail-first reproduced exactly (3 failed / 1010 passed, same three messages); no existing test file touched, so #356 is unedited and its output-side suite plus the real mainnet USDT->ETH V4 fixture pass unchanged;tokenInfo()is module-private with exactly two callers, both indecode(); the two refusal wordings are now oneUNNAMED_CURRENCYconstant, so they cannot differ.Disclosures. Independently reproduced #364 by execution (V3
USDT -> WETHamountIn = 0, then V2WETH -> USDCamountIn = 0.5e18rendersToken In = USDT,Amount = 500000000000.0000 USDT) — real, severe, and byte-identical before and after this change, so neither fixed nor worsened here; #359 is untouched. The author's claim that thev4.amountIn-without-v4.tokenInmirror cannot fire is verified in the source:amountInis assigned in only two places (src/shared/uniswap.js:280,:305), each in the sametryimmediately after the currency. Beyond the PR's synthetic pins I ran two more realistic native-ETH-input encodings —WRAP_ETH+V3_SWAP_EXACT_IN, and a V4SWAP_EXACT_IN_SINGLEwhosePoolKey.currency0is the zero address withzeroForOne— both still renderToken In: ETH (native),Amount: 1.0000 ETH, identical to pre-change output.