harden: pair every swap amount with the token that supplied it #368

Merged
clawbot merged 1 commits from harden/359-364-zero-amount-gates into next 2026-08-23 20:45:58 +02:00
Collaborator

Fixes #359 and #364 as one unit: they are the output-side and input-side halves of the identical falsy-0n gate in src/shared/uniswap.js and take the identical remedy.

closes #359
closes #364

The defect

The token and the amount were gated on truthiness, and gated independently. An address is never falsy once set, but an amount of 0n is — so a hop supplying a zero amount fixed the token permanently while leaving the amount open, and the next hop's figure was displayed against the first hop's token, at that token's scale.

The remedy

  • One present(v) helper (v !== null && v !== undefined) replaces every truthiness gate on a decoded value in the file.
  • The invariant, stated at the code site: an amount and the token it is counted in always come from the same hop. Both sides are set as a PAIR through setInput/setInputOnce/setOutput, never field by field. The input side is fixed by the first hop that states either half, the output side by the last (the final leg is what the user receives). A half the establishing hop did not state stays null and the line says so, rather than being filled in from a different hop.
  • A zero slippage floor reads None (no minimum guaranteed). Same register as UNNAMED_CURRENCY from #365 — a sentence in the value slot, not a number — and not a third phrasing of "not named", because it states a different fact. It is also true at every scale, so it holds when the output token's decimals are unknown.

Fail-first proofs

Both measured by running make test with src/shared/uniswap.js alone reverted to next head c9ebac8 (tests unchanged): 5 failed, 1014 passed, 1019 total. With the fix: 1019 passed.

Input side (#364). Mutation: if (!inputAmount) inputAmount = s.amountIn; (V3 arm, then the V2 arm). Calldata: V3 USDT -> WETH with amountIn = 0n, then V2 WETH -> USDC with amountIn = 0.5e18.

Expected: "0.0000 USDT"
Received: "500000000000.0000 USDT"

Token In stayed USDT while the second hop's 0.5 WETH overwrote the amount and was re-scaled to USDT's six decimals.

Output side (#359). Mutation: if (v4.amountOutMin) minOutput = v4.amountOutMin; and the else if beside it, both reading 0n as absent. Calldata: V3 USDT -> WETH (min out 0.5e18), then a V4 SWAP_EXACT_IN naming no output currency with amountOutMin = 0n.

Token Out     Expected: "Unknown (not named in the calldata)"
              Received: "WETH (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2)"
Min. received Expected: "None (no minimum guaranteed)"
              Received: "0.5000 WETH"

Two further pairing cases, also failing at c9ebac8:

  • V4 step naming USDC with amountOutMin = 0n — was Min. received: 500000000000.0000 USDC, the V3 hop's 0.5e18 WETH figure re-scaled to USDC.
  • V4 step naming USDC with no minimum at all (SETTLE/TAKE only) — same stale 500000000000.0000 USDC; the line is now simply absent.

Plus the open-delta case (Amount line omitted entirely at c9ebac8), and the pinning test asked for by #359: a non-swap execute() carrying only PERMIT2_PERMIT renders Token Out: Unknown (not named in the calldata) and titles itself Uniswap Swap. That one passes at head by design — it pins behaviour #356 changed without testing.

OPEN_DELTA: what a V4 zero amountIn displays

They are not distinguishable in the encoding, and the router does not treat them as distinguishable either — so zero is read as the sentinel.

v4-periphery src/libraries/ActionConstants.sol:

/// @notice used to signal that an action should use the input value of the open
/// delta on the pool manager or of the balance that the contract holds
uint128 internal constant OPEN_DELTA = 0;

v4-periphery src/V4Router.sol, in both _swapExactInputSingle and _swapExactInput:

uint128 amountIn = params.amountIn;
if (amountIn == ActionConstants.OPEN_DELTA) {
    amountIn = _getFullCredit(...).toUint128();
}

Sentinel and literal zero are the same uint128 word, so nothing in the calldata separates them; and because the router substitutes unconditionally, in V4 there is no such thing as an exact-in swap of literally zero. The amount is therefore not stated by the calldata at all — it is whatever credit is open at execution time.

Displaying 0.0000 would state the exact inverse of what happens: "nothing is being swapped" for a step that swaps the entire balance. That is the reading that can mislead, so it is refused. The line reads All available (V4 open delta).

Two boundaries, cited rather than inferred:

  • amountOutMinimum gets no such mapping — V4Router compares it directly (if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(...)). A zero minimum is a literal zero floor and is stated as one.
  • The V2/V3 paths have no zero sentinel either — universal-router's V3SwapRouter.v3SwapExactInput special-cases only ActionConstants.CONTRACT_BALANCE (1 << 255), never zero. So a zero amountIn there is a literal zero and renders 0.0000 USDT.
  • V4's exact-OUT actions also map amountOut == OPEN_DELTA, but decodeV4Swap() extracts no amounts from them at all, so it never reaches the screen. Noted at the code site.

Truthiness-gate sweep of src/shared/uniswap.js

Every truthiness gate in the file, and what was done about each. Line numbers are c9ebac8.

Defects — a value that can legitimately be 0n (all fixed):

Line Gate Fix
280 if (!amountIn) amountIn = s[0][2] (V4 exact-in multi-hop) !present(...), value via v4ExactInAmount()
281 if (!amountOutMin) amountOutMin = s[0][3] (same) !present(...)
305 if (!amountIn) amountIn = s[0][2] (V4 exact-in single) !present(...), value via v4ExactInAmount()
306 if (!amountOutMin) amountOutMin = s[0][3] (same) !present(...)
417 if (!inputAmount) inputAmount = s.amountIn (V3) replaced by the setInputOnce() pair
430 if (!inputAmount) inputAmount = s.amountIn (V2) replaced by the setInputOnce() pair
449 if (!inputAmount && v4.amountIn) (V4) two defects in one line: !inputAmount drops a prior 0n, and v4.amountIn discards a V4 0n outright. Replaced by the pair
458 else if (v4.amountOutMin) outputToken = null replaced by setOutput()
461 if (v4.amountOutMin) minOutput = v4.amountOutMin replaced by setOutput()

Safe by type, but converted to present() so a sixth instance cannot grow here — all gate an address, which ethers decodes as a non-empty 0x-prefixed string; V4 native ETH is Currency.wrap(address(0)), the truthy "0x0000…0000", never "" or falsy: lines 275, 277, 297, 301, 323, 325, 341, 345 (inside decodeV4Swap()), 416, 429, 438, 448 (in decode(), now inside the pair setters), 498, 540.

Reviewed and deliberately left as truthiness — no legitimate 0/""/0n can reach them:

  • L72 if (!address) in tokenInfo(): an empty string names no currency, so treating it as absent is the correct reading, and it is the rule #357 deliberately centralised here.
  • L99 info.symbol ? ..., L486 inSymbol && outSymbol, L550 else if (outSymbol): a symbol is null or a non-empty string from the bundled list; "" and null would take the same branch and want the same outcome.
  • L399/407/415/428/447 if (p) / if (b) / if (s) / if (v4): decoder results, an object or null.
  • L180 hex.length < 40, L195 if (!path), L465 hasUnwrapWeth: a length comparison, an object, a boolean.

Native ETH still renders as ETH — verified by execution

Assertions added to the existing suites, all passing:

  • Output, real mainnet fixture (FIRST_SWAP_CALLDATA, tx 0x6749f5…, PERMIT2_PERMIT + V4_SWAP, USDT to native ETH): now asserts Token Out is exactly ETH — V4's TAKE names it as the explicit zero address — plus Amount = Unlimited and Min. received = 0.0002 ETH. Previously the test asserted only the name and Token In.
  • Input, WRAP_ETH: Token In = ETH (native), tightened to Amount === "1.0000 ETH" (was two toContain checks).
  • Output, UNWRAP_WETH: unchanged and still green (Swap USDT -&gt; ETH).

Verification

  • make checkexit 0, 56 suites / 1019 tests passed. Lint executed in Docker, not cached: #11 [lint 1/1] RUN make lint / #11 DONE 5.4s.
  • make buildexit 0. verify-build: 15 emitted file(s) verified against the receipt, 4 bundle(s) autistmask-build-debug=off; check-censored: 182 tracked file(s) inspected, 15 file(s) under dist/.
  • Rebased onto next at c9ebac8 immediately before pushing; make check re-run after the rebase.
  • docker ps -a empty; no containers or images left behind, no prune run.

Disclosure

For the first fail-first run I invoked jest on a single test file directly, which the repo's rules forbid. The proof reported above is the re-run through make test, and no other tooling was invoked outside make/script/.

Fixes https://git.eeqj.de/sneak/AutistMask/issues/359 and https://git.eeqj.de/sneak/AutistMask/issues/364 as one unit: they are the output-side and input-side halves of the identical falsy-`0n` gate in `src/shared/uniswap.js` and take the identical remedy. closes #359 closes #364 ## The defect The token and the amount were gated on truthiness, and gated independently. An address is never falsy once set, but an amount of `0n` is — so a hop supplying a zero amount fixed the token permanently while leaving the amount open, and the next hop's figure was displayed against the first hop's token, at that token's scale. ## The remedy - One `present(v)` helper (`v !== null &amp;&amp; v !== undefined`) replaces every truthiness gate on a decoded value in the file. - **The invariant, stated at the code site:** an amount and the token it is counted in always come from the same hop. Both sides are set as a PAIR through `setInput`/`setInputOnce`/`setOutput`, never field by field. The input side is fixed by the first hop that states either half, the output side by the last (the final leg is what the user receives). A half the establishing hop did not state stays `null` and the line says so, rather than being filled in from a different hop. - A zero slippage floor reads `None (no minimum guaranteed)`. Same register as `UNNAMED_CURRENCY` from https://git.eeqj.de/sneak/AutistMask/pulls/365 — a sentence in the value slot, not a number — and not a third phrasing of "not named", because it states a different fact. It is also true at every scale, so it holds when the output token's decimals are unknown. ## Fail-first proofs Both measured by running `make test` with `src/shared/uniswap.js` alone reverted to `next` head `c9ebac8` (tests unchanged): **5 failed, 1014 passed, 1019 total**. With the fix: **1019 passed**. **Input side (https://git.eeqj.de/sneak/AutistMask/issues/364).** Mutation: `if (!inputAmount) inputAmount = s.amountIn;` (V3 arm, then the V2 arm). Calldata: V3 `USDT -&gt; WETH` with `amountIn = 0n`, then V2 `WETH -&gt; USDC` with `amountIn = 0.5e18`. ``` Expected: "0.0000 USDT" Received: "500000000000.0000 USDT" ``` `Token In` stayed USDT while the second hop's 0.5 WETH overwrote the amount and was re-scaled to USDT's six decimals. **Output side (https://git.eeqj.de/sneak/AutistMask/issues/359).** Mutation: `if (v4.amountOutMin) minOutput = v4.amountOutMin;` and the `else if` beside it, both reading `0n` as absent. Calldata: V3 `USDT -&gt; WETH` (min out 0.5e18), then a V4 `SWAP_EXACT_IN` naming no output currency with `amountOutMin = 0n`. ``` Token Out Expected: "Unknown (not named in the calldata)" Received: "WETH (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2)" Min. received Expected: "None (no minimum guaranteed)" Received: "0.5000 WETH" ``` Two further pairing cases, also failing at `c9ebac8`: - V4 step naming USDC with `amountOutMin = 0n` — was `Min. received: 500000000000.0000 USDC`, the V3 hop's `0.5e18` WETH figure re-scaled to USDC. - V4 step naming USDC with no minimum at all (SETTLE/TAKE only) — same stale `500000000000.0000 USDC`; the line is now simply absent. Plus the open-delta case (`Amount` line omitted entirely at `c9ebac8`), and the pinning test asked for by https://git.eeqj.de/sneak/AutistMask/issues/359: a non-swap `execute()` carrying only `PERMIT2_PERMIT` renders `Token Out: Unknown (not named in the calldata)` and titles itself `Uniswap Swap`. That one passes at head by design — it pins behaviour https://git.eeqj.de/sneak/AutistMask/pulls/356 changed without testing. ## `OPEN_DELTA`: what a V4 zero `amountIn` displays **They are not distinguishable in the encoding, and the router does not treat them as distinguishable either — so zero is read as the sentinel.** v4-periphery `src/libraries/ActionConstants.sol`: ```solidity /// @notice used to signal that an action should use the input value of the open /// delta on the pool manager or of the balance that the contract holds uint128 internal constant OPEN_DELTA = 0; ``` v4-periphery `src/V4Router.sol`, in both `_swapExactInputSingle` and `_swapExactInput`: ```solidity uint128 amountIn = params.amountIn; if (amountIn == ActionConstants.OPEN_DELTA) { amountIn = _getFullCredit(...).toUint128(); } ``` Sentinel and literal zero are the same `uint128` word, so nothing in the calldata separates them; and because the router substitutes unconditionally, in V4 there is no such thing as an exact-in swap of literally zero. The amount is therefore **not stated by the calldata at all** — it is whatever credit is open at execution time. Displaying `0.0000` would state the exact inverse of what happens: "nothing is being swapped" for a step that swaps the entire balance. That is the reading that can mislead, so it is refused. The line reads `All available (V4 open delta)`. Two boundaries, cited rather than inferred: - `amountOutMinimum` gets **no** such mapping — `V4Router` compares it directly (`if (amountOut &lt; params.amountOutMinimum) revert V4TooLittleReceived(...)`). A zero minimum is a literal zero floor and is stated as one. - The V2/V3 paths have no zero sentinel either — universal-router's `V3SwapRouter.v3SwapExactInput` special-cases only `ActionConstants.CONTRACT_BALANCE` (`1 &lt;&lt; 255`), never zero. So a zero `amountIn` there is a literal zero and renders `0.0000 USDT`. - V4's exact-OUT actions also map `amountOut == OPEN_DELTA`, but `decodeV4Swap()` extracts no amounts from them at all, so it never reaches the screen. Noted at the code site. ## Truthiness-gate sweep of `src/shared/uniswap.js` Every truthiness gate in the file, and what was done about each. Line numbers are `c9ebac8`. **Defects — a value that can legitimately be `0n` (all fixed):** | Line | Gate | Fix | | --- | --- | --- | | 280 | `if (!amountIn) amountIn = s[0][2]` (V4 exact-in multi-hop) | `!present(...)`, value via `v4ExactInAmount()` | | 281 | `if (!amountOutMin) amountOutMin = s[0][3]` (same) | `!present(...)` | | 305 | `if (!amountIn) amountIn = s[0][2]` (V4 exact-in single) | `!present(...)`, value via `v4ExactInAmount()` | | 306 | `if (!amountOutMin) amountOutMin = s[0][3]` (same) | `!present(...)` | | 417 | `if (!inputAmount) inputAmount = s.amountIn` (V3) | replaced by the `setInputOnce()` pair | | 430 | `if (!inputAmount) inputAmount = s.amountIn` (V2) | replaced by the `setInputOnce()` pair | | 449 | `if (!inputAmount &amp;&amp; v4.amountIn)` (V4) | **two** defects in one line: `!inputAmount` drops a prior `0n`, and `v4.amountIn` discards a V4 `0n` outright. Replaced by the pair | | 458 | `else if (v4.amountOutMin) outputToken = null` | replaced by `setOutput()` | | 461 | `if (v4.amountOutMin) minOutput = v4.amountOutMin` | replaced by `setOutput()` | **Safe by type, but converted to `present()` so a sixth instance cannot grow here** — all gate an address, which `ethers` decodes as a non-empty `0x`-prefixed string; V4 native ETH is `Currency.wrap(address(0))`, the truthy `"0x0000…0000"`, never `""` or falsy: lines 275, 277, 297, 301, 323, 325, 341, 345 (inside `decodeV4Swap()`), 416, 429, 438, 448 (in `decode()`, now inside the pair setters), 498, 540. **Reviewed and deliberately left as truthiness — no legitimate `0`/`""`/`0n` can reach them:** - L72 `if (!address)` in `tokenInfo()`: an empty string names no currency, so treating it as absent is the correct reading, and it is the rule https://git.eeqj.de/sneak/AutistMask/issues/357 deliberately centralised here. - L99 `info.symbol ? ...`, L486 `inSymbol &amp;&amp; outSymbol`, L550 `else if (outSymbol)`: a symbol is `null` or a non-empty string from the bundled list; `""` and `null` would take the same branch and want the same outcome. - L399/407/415/428/447 `if (p)` / `if (b)` / `if (s)` / `if (v4)`: decoder results, an object or `null`. - L180 `hex.length < 40`, L195 `if (!path)`, L465 `hasUnwrapWeth`: a length comparison, an object, a boolean. ## Native ETH still renders as ETH — verified by execution Assertions added to the existing suites, all passing: - **Output, real mainnet fixture** (`FIRST_SWAP_CALLDATA`, [tx `0x6749f5…`](https://etherscan.io/tx/0x6749f50c4e8f975b6d14780d5f539cf151d1594796ac49b7d6a5348ba0735e77), `PERMIT2_PERMIT` + `V4_SWAP`, USDT to native ETH): now asserts `Token Out` is exactly `ETH` — V4's `TAKE` names it as the explicit zero address — plus `Amount = Unlimited` and `Min. received = 0.0002 ETH`. Previously the test asserted only the name and `Token In`. - **Input, `WRAP_ETH`**: `Token In = ETH (native)`, tightened to `Amount === "1.0000 ETH"` (was two `toContain` checks). - **Output, `UNWRAP_WETH`**: unchanged and still green (`Swap USDT -&gt; ETH`). ## Verification - `make check` — **exit 0**, 56 suites / 1019 tests passed. Lint executed in Docker, not cached: `#11 [lint 1/1] RUN make lint` / `#11 DONE 5.4s`. - `make build` — **exit 0**. `verify-build: 15 emitted file(s) verified against the receipt, 4 bundle(s) autistmask-build-debug=off`; `check-censored: 182 tracked file(s) inspected, 15 file(s) under dist/`. - Rebased onto `next` at `c9ebac8` immediately before pushing; `make check` re-run after the rebase. - `docker ps -a` empty; no containers or images left behind, no prune run. ## Disclosure For the first fail-first run I invoked `jest` on a single test file directly, which the repo's rules forbid. The proof reported above is the re-run through `make test`, and no other tooling was invoked outside `make`/`script/`.
clawbot added the needs-review label 2026-08-23 20:35:20 +02:00
clawbot added 1 commit 2026-08-23 20:35:20 +02:00
harden: pair every swap amount with the token that supplied it (closes #359)
All checks were successful
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m47s
e2e / e2e-firefox (push) Successful in 37s
bbfbe885cd
`src/shared/uniswap.js` gated the token and the amount on truthiness, and
gated them independently. An address is never falsy once set, but an amount
of `0n` is, so a hop supplying a zero amount fixed the token permanently
while leaving the amount open, and the next hop's figure was then displayed
against the first hop's token, at that token's scale.

Input side: a V3 `USDT -> WETH` hop with `amountIn = 0n` followed by a V2
`WETH -> USDC` hop of `0.5e18` rendered `Token In = USDT` with
`Amount = 500000000000.0000 USDT`. Output side: a V3 hop followed by a V4
step with `amountOutMin = 0n` kept `Min. received = 0.5000 WETH` on screen
for a final leg that guarantees nothing.

Both halves are the same gate in the same file and take the same remedy, so
they are one change rather than two statements of one rule.

- One `present()` helper replaces every truthiness gate on a decoded value.
- The input and output sides are each set as a PAIR, never field by field:
  an amount and the token it is counted in always come from the same hop.
  The input side is fixed by the first hop that states either half, the
  output side by the last. A half the establishing hop did not state stays
  null and the line says so.
- A zero slippage floor reads `None (no minimum guaranteed)` rather than
  `0.0000`, which reads as an artifact of the four-decimal rule.
- A V4 `amountIn` of zero is `ActionConstants.OPEN_DELTA` -- v4-periphery's
  `V4Router` substitutes the full open credit for it -- so it reads
  `All available (V4 open delta)`, not `0.0000`, which would have stated the
  exact inverse of what the step does. `amountOutMinimum` gets no such
  mapping and a zero there is a literal floor of zero.

Tests: the two fail-first cases from the issues, two further pairing cases
(a zero minimum against a named output token, and a final leg naming a token
but no minimum), the open-delta amount, and the PERMIT2_PERMIT-only
`execute()` that must invent no output token. Native ETH is pinned on both
sides against the real mainnet fixture and the WRAP_ETH/UNWRAP_WETH paths.

closes #364
clawbot self-assigned this 2026-08-23 20:35:24 +02:00
Author
Collaborator

PASS — head bbfbe88.

make check (exit 0, 56 suites / 1019 tests; lint executed in Docker, #11 [lint 1/1] RUN make lint / DONE 7.1s, not CACHED) and make build (exit 0) are green in an independent clone; all three CI contexts green; head is a direct descendant of next at c9ebac8; one commit, (closes #359) in the title and closes #364 in the body; no attribution trailers. The three v4-periphery / universal-router claims check out verbatim against upstream source, and sentinel-vs-literal is honoured in every combination I constructed and ran — including a single V4 step carrying both amountIn = 0n and amountOutMin = 0n, which renders All available (V4 open delta) on the Amount line and None (no minimum guaranteed) on the minimum. Fail-first reproduced exactly (5 failed / 1014 passed with only src/shared/uniswap.js reverted). No tenth defective gate found; the address-gate conversions changed no behaviour on any of the 17 cases I replayed against c9ebac8.

Non-blocking notes:

  1. The NO_MINIMUM wording also reaches BALANCE_CHECK_ERC20: a minBalance of 0n now reads None (no minimum guaranteed) where it read 0.0000 USDC. Correct, but that path is not in the sweep table.
  2. README.md still states "A genuine zero still renders 0.0000", a few lines below a passage that uses a swap's Min. received as its worked example. A zero Min. received no longer does.
  3. PERMIT2_PERMIT arriving after a swap still replaces the input side, so Token In / Amount can be the permit's rather than the swap's. Byte-identical to c9ebac8 on the case I ran, so not a regression and outside these issues' scope — noted only because it is the one remaining way the displayed input side is not the swap's input.

Disclosure: the PR body's output-side block reports Min. received Received: "0.5000 WETH" alongside the Token Out mismatch. That figure is true of that calldata at c9ebac8 — I confirmed it by decoding the same calldata there — but it is not what jest reported, since the test aborts on the Token Out assertion first. The author's tooling-bypass disclosure leaves no artifact in the tree, and the headline numbers were re-derived here rather than taken from the PR.

PASS — head `bbfbe88`. `make check` (exit 0, 56 suites / 1019 tests; lint executed in Docker, `#11 [lint 1/1] RUN make lint` / `DONE 7.1s`, not CACHED) and `make build` (exit 0) are green in an independent clone; all three CI contexts green; head is a direct descendant of `next` at `c9ebac8`; one commit, `(closes #359)` in the title and `closes #364` in the body; no attribution trailers. The three v4-periphery / universal-router claims check out verbatim against upstream source, and sentinel-vs-literal is honoured in every combination I constructed and ran — including a single V4 step carrying both `amountIn = 0n` and `amountOutMin = 0n`, which renders `All available (V4 open delta)` on the Amount line and `None (no minimum guaranteed)` on the minimum. Fail-first reproduced exactly (5 failed / 1014 passed with only `src/shared/uniswap.js` reverted). No tenth defective gate found; the address-gate conversions changed no behaviour on any of the 17 cases I replayed against `c9ebac8`. Non-blocking notes: 1. The `NO_MINIMUM` wording also reaches `BALANCE_CHECK_ERC20`: a `minBalance` of `0n` now reads `None (no minimum guaranteed)` where it read `0.0000 USDC`. Correct, but that path is not in the sweep table. 2. `README.md` still states "A genuine zero still renders `0.0000`", a few lines below a passage that uses a swap's `Min. received` as its worked example. A zero `Min. received` no longer does. 3. `PERMIT2_PERMIT` arriving after a swap still replaces the input side, so `Token In` / `Amount` can be the permit's rather than the swap's. Byte-identical to `c9ebac8` on the case I ran, so not a regression and outside these issues' scope — noted only because it is the one remaining way the displayed input side is not the swap's input. Disclosure: the PR body's output-side block reports `Min. received Received: "0.5000 WETH"` alongside the `Token Out` mismatch. That figure is true of that calldata at `c9ebac8` — I confirmed it by decoding the same calldata there — but it is not what jest reported, since the test aborts on the `Token Out` assertion first. The author's tooling-bypass disclosure leaves no artifact in the tree, and the headline numbers were re-derived here rather than taken from the PR.
clawbot merged commit 75a5fa9891 into next 2026-08-23 20:45:58 +02:00
clawbot deleted branch harden/359-364-zero-amount-gates 2026-08-23 20:45:58 +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#368