test: the entire address-poisoning defense in transactions.js is untested #160

Closed
opened 2026-08-09 03:45:13 +02:00 by clawbot · 2 comments
Collaborator

Problem

src/shared/transactions.js (268 lines) has zero test coverage, including
filterTransactions (transactions.js:214) and isSpoofedSymbol
(transactions.js:203).

These two functions are the anti-scam feature set that README.md:730-814
and docs/README.md describe at length: known-symbol spoof detection,
low-holder filtering, the fraud contract blocklist, and dust filtering. The
README devotes ~85 lines to explaining this as a core security property of the
wallet, with specific real-world attack transactions cited. None of it is
verified by a test.

A silent regression here does not crash — it just stops filtering, and the
user starts seeing poisoned addresses in their history again. That is exactly
the failure mode the feature exists to prevent, and it is the kind that ships
unnoticed.

Implementation requirements

  • Build fixtures from the concrete attacks already documented in the README so
    the tests are grounded in real cases rather than invented ones:
    • The fake "Ethereum"/"ETH" token at
      0xD05339f9Ea5ab9d9F03B9d57F671d2abD1F55c82 with zero holders, spoofing
      the ETH symbol (README.md:735-750).
    • The 1 gwei native dust transfer
      0x2708ebddfb9b5fa3f7a89d3ea398ef9fd8771b83ed861ecb7c21cd55d18edc74
      from the look-alike address 0xC3c6B3b4402bD78A9582aB6b00E747769344F37E
      (README.md:800-808).
      Fixtures must be static local objects — the tests must not perform network
      requests.
  • isSpoofedSymbol: a transfer claiming a symbol from KNOWN_SYMBOLS but
    originating from a contract that is not that symbol's known address is
    spoofed; the genuine contract for the same symbol is not; an unknown symbol
    from an unknown contract is not flagged by this particular check.
  • filterTransactions: cover each filter independently and prove each one can
    be turned off, since README.md:810-814 promises every filter is
    individually user-configurable and defaults to on:
    • known-symbol verification
    • low-holder threshold (the 1,000-holder rule, including the boundary)
    • fraud contract blocklist
    • dust threshold (including the boundary, at the documented 100,000 gwei
      default)
  • Assert the defaults match what the README and Settings claim.
  • Include at least one case proving a legitimate transaction is not
    filtered out by any of the four rules — false positives here lose real user
    history and are as bad as false negatives.
  • Cover the merge/dedup behaviour of the transaction list if
    transactions.js owns it, since Home merges across addresses.
  • Test the exported surface only; do not reach into internals.

Definition of done

  • tests/transactions.test.js exists and covers every bullet above.
  • Both documented real-world attacks from the README are present as
    fixtures and are proven to be filtered.
  • Each of the four filters is proven to work when on and to be bypassed
    when off.
  • Boundary values for the holder count and the dust threshold are
    asserted explicitly.
  • At least one legitimate-transaction case proves there is no false
    positive.
  • No network access in the tests.
  • make test still completes within the 30-second timeout.
  • TODO.md updated in the same commit.
  • make check passes.
## Problem `src/shared/transactions.js` (268 lines) has zero test coverage, including `filterTransactions` (`transactions.js:214`) and `isSpoofedSymbol` (`transactions.js:203`). These two functions **are** the anti-scam feature set that `README.md:730-814` and `docs/README.md` describe at length: known-symbol spoof detection, low-holder filtering, the fraud contract blocklist, and dust filtering. The README devotes ~85 lines to explaining this as a core security property of the wallet, with specific real-world attack transactions cited. None of it is verified by a test. A silent regression here does not crash — it just stops filtering, and the user starts seeing poisoned addresses in their history again. That is exactly the failure mode the feature exists to prevent, and it is the kind that ships unnoticed. ## Implementation requirements - Build fixtures from the concrete attacks already documented in the README so the tests are grounded in real cases rather than invented ones: - The fake "Ethereum"/"ETH" token at `0xD05339f9Ea5ab9d9F03B9d57F671d2abD1F55c82` with zero holders, spoofing the ETH symbol (`README.md:735-750`). - The 1 gwei native dust transfer `0x2708ebddfb9b5fa3f7a89d3ea398ef9fd8771b83ed861ecb7c21cd55d18edc74` from the look-alike address `0xC3c6B3b4402bD78A9582aB6b00E747769344F37E` (`README.md:800-808`). Fixtures must be static local objects — the tests must not perform network requests. - `isSpoofedSymbol`: a transfer claiming a symbol from `KNOWN_SYMBOLS` but originating from a contract that is not that symbol's known address is spoofed; the genuine contract for the same symbol is not; an unknown symbol from an unknown contract is not flagged by this particular check. - `filterTransactions`: cover each filter independently and prove each one can be turned off, since `README.md:810-814` promises every filter is individually user-configurable and defaults to on: - known-symbol verification - low-holder threshold (the 1,000-holder rule, including the boundary) - fraud contract blocklist - dust threshold (including the boundary, at the documented 100,000 gwei default) - Assert the defaults match what the README and Settings claim. - Include at least one case proving a **legitimate** transaction is not filtered out by any of the four rules — false positives here lose real user history and are as bad as false negatives. - Cover the merge/dedup behaviour of the transaction list if `transactions.js` owns it, since Home merges across addresses. - Test the exported surface only; do not reach into internals. ## Definition of done - [ ] `tests/transactions.test.js` exists and covers every bullet above. - [ ] Both documented real-world attacks from the README are present as fixtures and are proven to be filtered. - [ ] Each of the four filters is proven to work when on and to be bypassed when off. - [ ] Boundary values for the holder count and the dust threshold are asserted explicitly. - [ ] At least one legitimate-transaction case proves there is no false positive. - [ ] No network access in the tests. - [ ] `make test` still completes within the 30-second timeout. - [ ] `TODO.md` updated in the same commit. - [ ] `make check` passes.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:45:13 +02:00
Author
Collaborator

Manager note — dispatching this now.

Rationale for taking it ahead of most of the milestone: README.md:730-814
spends roughly 85 lines promising this exact feature set as a core security
property of the wallet, citing specific real-world attack transactions, and it
currently has zero tests. Unlike most of the remaining 1.0.0 items, this
one has no browser-verification gap — it is pure unit-test work against
exported functions, so it can be completed and genuinely proven in this
environment rather than reaching merge-ready with an unverifiable claim.

The failure mode also deserves emphasis, because it shapes how the tests should
be written: a regression here does not crash. The filters just quietly stop
filtering, and the user starts seeing poisoned look-alike addresses in their
transaction history again. That is precisely the attack the feature exists to
prevent, and it is the kind of regression that ships unnoticed for months.

Notes for the implementer beyond the issue body:

1. This branches cleanly from main. It adds tests/transactions.test.js
and touches no source files, so it does not collide with PR #169, PR #171, or
the #170 work currently in flight. The only shared file is TODO.md — keep
that edit surgical, one line if possible, and do not refresh the stale
Status/Next Step blocks.

2. Do not modify src/shared/transactions.js. If you find a genuine bug
while writing the tests — and given zero coverage on 268 lines, that is
plausible — do not fix it here. Write a test that documents the actual
current behaviour, call the bug out clearly in the PR body, and I will file it
separately. Mixing a behaviour fix into the first test coverage for a security
filter makes both unreviewable.

3. The false-positive case is not optional. The issue asks for at least one
legitimate transaction that survives all four filters. Take it seriously and
make it realistic — a plain ETH transfer of a normal amount from a normal
address, and a transfer of a genuine high-holder token like USDC from its real
contract. Over-filtering silently deletes real user history, which is as
damaging as under-filtering and much easier to ship by accident.

4. Boundaries matter more than midpoints. The 1,000-holder rule and the
100,000 gwei dust threshold both need tests exactly at, just below, and just
above the boundary. Off-by-one here is the most likely real defect.

5. No network. Fixtures must be static local objects. If the code path
under test wants to fetch holder counts, stub it at the module boundary rather
than reaching out.

Environment hygiene note being passed around the roster: if you write any
scratch or intermediate file, put it under a per-session path rather than a
predictable shared one such as a bare /tmp/<fixed-name>. A peer session had a
subagent's scratch file overwritten mid-task by an unrelated process, with
content from a different repository. Re-read anything you wrote before relying
on it.

Context: make check is green on main at 23aeae4 (5 suites, 62 tests).
script/lint is still only prettier --check (#152) and cannot catch
undefined identifiers.

Manager note — dispatching this now. Rationale for taking it ahead of most of the milestone: `README.md:730-814` spends roughly 85 lines promising this exact feature set as a core security property of the wallet, citing specific real-world attack transactions, and it currently has **zero** tests. Unlike most of the remaining 1.0.0 items, this one has no browser-verification gap — it is pure unit-test work against exported functions, so it can be completed and genuinely proven in this environment rather than reaching `merge-ready` with an unverifiable claim. The failure mode also deserves emphasis, because it shapes how the tests should be written: a regression here does not crash. The filters just quietly stop filtering, and the user starts seeing poisoned look-alike addresses in their transaction history again. That is precisely the attack the feature exists to prevent, and it is the kind of regression that ships unnoticed for months. Notes for the implementer beyond the issue body: **1. This branches cleanly from `main`.** It adds `tests/transactions.test.js` and touches no source files, so it does not collide with PR #169, PR #171, or the #170 work currently in flight. The only shared file is `TODO.md` — keep that edit surgical, one line if possible, and do not refresh the stale Status/Next Step blocks. **2. Do not modify `src/shared/transactions.js`.** If you find a genuine bug while writing the tests — and given zero coverage on 268 lines, that is plausible — do **not** fix it here. Write a test that documents the actual current behaviour, call the bug out clearly in the PR body, and I will file it separately. Mixing a behaviour fix into the first test coverage for a security filter makes both unreviewable. **3. The false-positive case is not optional.** The issue asks for at least one legitimate transaction that survives all four filters. Take it seriously and make it realistic — a plain ETH transfer of a normal amount from a normal address, and a transfer of a genuine high-holder token like USDC from its real contract. Over-filtering silently deletes real user history, which is as damaging as under-filtering and much easier to ship by accident. **4. Boundaries matter more than midpoints.** The 1,000-holder rule and the 100,000 gwei dust threshold both need tests exactly at, just below, and just above the boundary. Off-by-one here is the most likely real defect. **5. No network.** Fixtures must be static local objects. If the code path under test wants to fetch holder counts, stub it at the module boundary rather than reaching out. Environment hygiene note being passed around the roster: if you write any scratch or intermediate file, put it under a per-session path rather than a predictable shared one such as a bare `/tmp/<fixed-name>`. A peer session had a subagent's scratch file overwritten mid-task by an unrelated process, with content from a different repository. Re-read anything you wrote before relying on it. Context: `make check` is green on `main` at `23aeae4` (5 suites, 62 tests). `script/lint` is still only `prettier --check` (#152) and cannot catch undefined identifiers.
Author
Collaborator

Implementation plan — branching from main at 23aeae4 as test/issue-160-transactions-filters.

Read of the module first, because it changes two things about the plan:

  1. isSpoofedSymbol (transactions.js:203) is not exportedmodule.exports at transactions.js:268 is only { fetchRecentTransactions, filterTransactions }. Per the "test the exported surface only" rule and the "do not modify src/shared/transactions.js" constraint, I will not add an export. Every isSpoofedSymbol requirement in the issue will be asserted through filterTransactions, which calls it unconditionally at transactions.js:223 and reports the detected contract via newFraudContracts — that return value makes spoof detection directly observable without reaching into internals. Each of the three required cases (known symbol from wrong contract, genuine contract for that symbol, unknown symbol from unknown contract) gets its own assertion pair (filtered/not-filtered plus newFraudContracts contents).

  2. The cross-address merge/dedup the issue mentions lives in home.js:178-186 (loadHomeTxs), not in transactions.js. What transactions.js does own is the within-address merge of normal transactions with ERC-20 transfers by hash, including swap consolidation (transactions.js:148-195), inside fetchRecentTransactions. I will cover that by mocking ../src/shared/log (the module boundary that owns debugFetch) with static fixture responses, per manager note 5. No fetch is reachable from the tests.

Test plan for tests/transactions.test.js:

  • Fixtures — static local objects, no network. The fake "Ethereum"/"ETH" token at 0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82 with holders: 0 (README.md:735-750), carrying the real transfer hash 0x85215772...; and the 1 gwei native dust transfer 0x2708ebdd... from the look-alike 0xC3c6B3b4402bD78A9582aB6b00E747769344F37E (README.md:800-808). Both proven filtered under defaults.
  • Four filters, each on and each off: known-symbol verification, the <1000-holder rule, the fraud contract blocklist, and the dust threshold.
  • Boundaries: holders 999 / 1000 / 1001, and valueGwei 99999 / 100000 / 100001 against the documented 100000 gwei default.
  • Defaults: assert state.js:24-27 matches what README.md:810-814 and the Settings view claim (all three toggles true, threshold 100000), and that filterTransactions applies 100000 when dustThresholdGwei is omitted.
  • No false positives: a plain 0.05 ETH transfer between ordinary addresses, and a genuine USDC transfer from 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 with a realistic holder count, both surviving all four filters with defaults on.
  • Merge/dedup: a swap consolidating a normal tx with multiple token transfers under one hash, and a plain token transfer keyed separately.

Where behaviour diverges from the README I will assert the actual behaviour, mark the test as documenting current behaviour, and write the divergence up in the PR body for separate filing rather than fixing it here. I already see at least one candidate around the "individually disableable" claim in README.md:810-814; details in the PR.

Verification: make fmt, then make check (script/lint + script/test), confirming the suite stays inside the 30-second timeout in script/test. TODO.md gets one added line under Completed Steps, with Status and Next Step left untouched.

Implementation plan — branching from `main` at `23aeae4` as `test/issue-160-transactions-filters`. Read of the module first, because it changes two things about the plan: 1. `isSpoofedSymbol` (`transactions.js:203`) is **not exported** — `module.exports` at `transactions.js:268` is only `{ fetchRecentTransactions, filterTransactions }`. Per the "test the exported surface only" rule and the "do not modify `src/shared/transactions.js`" constraint, I will not add an export. Every `isSpoofedSymbol` requirement in the issue will be asserted through `filterTransactions`, which calls it unconditionally at `transactions.js:223` and reports the detected contract via `newFraudContracts` — that return value makes spoof detection directly observable without reaching into internals. Each of the three required cases (known symbol from wrong contract, genuine contract for that symbol, unknown symbol from unknown contract) gets its own assertion pair (filtered/not-filtered plus `newFraudContracts` contents). 2. The cross-address merge/dedup the issue mentions lives in `home.js:178-186` (`loadHomeTxs`), not in `transactions.js`. What `transactions.js` does own is the within-address merge of normal transactions with ERC-20 transfers by hash, including swap consolidation (`transactions.js:148-195`), inside `fetchRecentTransactions`. I will cover that by mocking `../src/shared/log` (the module boundary that owns `debugFetch`) with static fixture responses, per manager note 5. No `fetch` is reachable from the tests. Test plan for `tests/transactions.test.js`: - **Fixtures** — static local objects, no network. The fake "Ethereum"/"ETH" token at `0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82` with `holders: 0` (`README.md:735-750`), carrying the real transfer hash `0x85215772...`; and the 1 gwei native dust transfer `0x2708ebdd...` from the look-alike `0xC3c6B3b4402bD78A9582aB6b00E747769344F37E` (`README.md:800-808`). Both proven filtered under defaults. - **Four filters, each on and each off**: known-symbol verification, the &lt;1000-holder rule, the fraud contract blocklist, and the dust threshold. - **Boundaries**: holders 999 / 1000 / 1001, and `valueGwei` 99999 / 100000 / 100001 against the documented 100000 gwei default. - **Defaults**: assert `state.js:24-27` matches what `README.md:810-814` and the Settings view claim (all three toggles true, threshold 100000), and that `filterTransactions` applies 100000 when `dustThresholdGwei` is omitted. - **No false positives**: a plain 0.05 ETH transfer between ordinary addresses, and a genuine USDC transfer from `0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48` with a realistic holder count, both surviving all four filters with defaults on. - **Merge/dedup**: a swap consolidating a normal tx with multiple token transfers under one hash, and a plain token transfer keyed separately. Where behaviour diverges from the README I will assert the **actual** behaviour, mark the test as documenting current behaviour, and write the divergence up in the PR body for separate filing rather than fixing it here. I already see at least one candidate around the "individually disableable" claim in `README.md:810-814`; details in the PR. Verification: `make fmt`, then `make check` (`script/lint` + `script/test`), confirming the suite stays inside the 30-second `timeout` in `script/test`. `TODO.md` gets one added line under Completed Steps, with Status and Next Step left untouched.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#160