Two small defects found while writing the first test coverage for the
anti-poisoning filters (#160 / PR #175), deliberately not fixed there. Both are
in src/shared/transactions.js and both are a few lines, so grouping them.
1. A dust threshold of 0 is silently replaced by the default
src/shared/transactions.js:257:
filters.dustThresholdGwei||100000
0 is falsy, so a user who sets the threshold to 0 — the obvious way to say
"do not hide anything as dust" — gets the 100,000 gwei default applied instead.
The wallet then hides transactions the user explicitly asked to see, with no
indication why.
This is reachable: the threshold is a free-text editable field at src/popup/views/settings.js:305-310. It also directly contradicts README.md:810-814, which promises the user can "configure the wallet to show
everything unfiltered, unix-style" — 0 is exactly how a user would express
that, and it is the one value that does not work.
Requirements
Distinguish "unset" from "zero". Use a nullish check (??) or an explicit undefined/null test rather than ||.
Decide and document what 0 means: almost certainly "disable dust
filtering", which is also what the separate enable/disable checkbox does.
Make sure the two controls agree and neither silently overrides the other.
Check the input validation path in settings.js:305-310 — confirm what
happens for empty string, negative numbers, and non-numeric input, and make
each behave sensibly rather than coercing to a surprising default. Say in
the PR what each now does.
Audit src/shared/ for the same ||-as-default pattern applied to any other
numeric setting; this class of bug rarely appears once. Report what you find
even if you do not fix it here.
2. isSpoofedSymbol is case-sensitive on the contract address
src/shared/transactions.js:209 compares the contract address without
normalising case, unlike the blocklist check at transactions.js:215-217,
which lowercases correctly.
Ethereum addresses are routinely rendered in EIP-55 mixed-case checksum form.
A checksummed address for a genuine, known contract will therefore fail the
equality test and be misclassified as a spoof — a false positive that hides
a legitimate token transfer from the user's history.
The reporter notes this is currently unreachable in-app, because the addresses
reaching this function happen to already be lowercased upstream. That makes it
latent rather than live: isSpoofedSymbol is an exported function, the
inconsistency with the adjacent blocklist check is exactly the kind of thing a
later refactor trips over, and the failure direction is silent data loss from
the history view.
Requirements
Normalise case on both sides of the comparison at transactions.js:209, the
same way :215-217 already does.
Check every other address comparison in transactions.js for the same
inconsistency and make them uniform. Prefer one shared normalisation helper
over repeating .toLowerCase() at each site, so the next comparison added
cannot get it wrong.
Add a test passing a checksummed genuine contract address and asserting it is not flagged as a spoof. tests/transactions.test.js already has a current behaviour: test pinning the case-sensitive behaviour — invert it
rather than deleting it.
Definition of done
A dust threshold of 0 disables dust filtering rather than applying the
100,000 gwei default.
Empty, negative and non-numeric threshold input each behave sensibly, and
the PR states what each does.
Any other ||-as-numeric-default in src/shared/ is reported.
isSpoofedSymbol treats a checksummed genuine contract as genuine.
Address comparisons in transactions.js are consistently normalised,
ideally through one helper.
Both current behaviour: tests are inverted into regression guards.
All other tests in tests/transactions.test.js still pass unmodified.
TODO.md updated in the same commit.
make check passes.
Two small defects found while writing the first test coverage for the
anti-poisoning filters (#160 / PR #175), deliberately not fixed there. Both are
in `src/shared/transactions.js` and both are a few lines, so grouping them.
## 1. A dust threshold of 0 is silently replaced by the default
`src/shared/transactions.js:257`:
```js
filters.dustThresholdGwei || 100000
```
`0` is falsy, so a user who sets the threshold to `0` — the obvious way to say
"do not hide anything as dust" — gets the 100,000 gwei default applied instead.
The wallet then hides transactions the user explicitly asked to see, with no
indication why.
This is reachable: the threshold is a free-text editable field at
`src/popup/views/settings.js:305-310`. It also directly contradicts
`README.md:810-814`, which promises the user can "configure the wallet to show
everything unfiltered, unix-style" — `0` is exactly how a user would express
that, and it is the one value that does not work.
**Requirements**
- Distinguish "unset" from "zero". Use a nullish check (`??`) or an explicit
`undefined`/`null` test rather than `||`.
- Decide and document what `0` means: almost certainly "disable dust
filtering", which is also what the separate enable/disable checkbox does.
Make sure the two controls agree and neither silently overrides the other.
- Check the input validation path in `settings.js:305-310` — confirm what
happens for empty string, negative numbers, and non-numeric input, and make
each behave sensibly rather than coercing to a surprising default. Say in
the PR what each now does.
- Audit `src/shared/` for the same `||`-as-default pattern applied to any other
numeric setting; this class of bug rarely appears once. Report what you find
even if you do not fix it here.
## 2. `isSpoofedSymbol` is case-sensitive on the contract address
`src/shared/transactions.js:209` compares the contract address without
normalising case, unlike the blocklist check at `transactions.js:215-217`,
which lowercases correctly.
Ethereum addresses are routinely rendered in EIP-55 mixed-case checksum form.
A checksummed address for a genuine, known contract will therefore fail the
equality test and be misclassified as a **spoof** — a false positive that hides
a legitimate token transfer from the user's history.
The reporter notes this is currently unreachable in-app, because the addresses
reaching this function happen to already be lowercased upstream. That makes it
latent rather than live: `isSpoofedSymbol` is an exported function, the
inconsistency with the adjacent blocklist check is exactly the kind of thing a
later refactor trips over, and the failure direction is silent data loss from
the history view.
**Requirements**
- Normalise case on both sides of the comparison at `transactions.js:209`, the
same way `:215-217` already does.
- Check every other address comparison in `transactions.js` for the same
inconsistency and make them uniform. Prefer one shared normalisation helper
over repeating `.toLowerCase()` at each site, so the next comparison added
cannot get it wrong.
- Add a test passing a checksummed genuine contract address and asserting it is
**not** flagged as a spoof. `tests/transactions.test.js` already has a
`current behaviour:` test pinning the case-sensitive behaviour — invert it
rather than deleting it.
## Definition of done
- [ ] A dust threshold of `0` disables dust filtering rather than applying the
100,000 gwei default.
- [ ] Empty, negative and non-numeric threshold input each behave sensibly, and
the PR states what each does.
- [ ] Any other `||`-as-numeric-default in `src/shared/` is reported.
- [ ] `isSpoofedSymbol` treats a checksummed genuine contract as genuine.
- [ ] Address comparisons in `transactions.js` are consistently normalised,
ideally through one helper.
- [ ] Both `current behaviour:` tests are inverted into regression guards.
- [ ] All other tests in `tests/transactions.test.js` still pass unmodified.
- [ ] `TODO.md` updated in the same commit.
- [ ] `make check` passes.
clawbot
added this to the 1.0.0 milestone 2026-08-09 07:08:34 +02:00
Additional requirement, from the mutation testing done during the review of
PR #175.
The reviewer applied 15 mutations to src/shared/transactions.js; 13 were
killed by the new suite. One of the two survivors falls in the filter logic
this issue touches:
src/shared/transactions.js:244 — removing the tx.holders !== null guard
survives undetected. No fixture pairs a non-null contractAddress with a
null holders value, so the guard that stops the low-holder rule from acting
on an unknown holder count is unpinned.
That is worth closing here rather than leaving it, because the failure
direction is bad: without the guard, a token whose holder count could not be
fetched (a Blockscout hiccup, a rate limit, a self-hosted instance that does
not return the field) would be treated as having zero holders and silently
filtered out of the user's history. That is over-filtering — the same class of
harm as the zero-threshold bug in part 1 of this issue, where the user asks to
see everything and the wallet hides things anyway.
Please add:
A fixture with a real contractAddress and holders: null, asserting the
transaction is not filtered by the low-holder rule.
The same for holders: undefined if that is reachable from the Blockscout
response shape — check rather than assume.
Verification that the mutation is now caught: delete the !== null guard,
confirm at least one test fails, revert, and state the result in the PR as
PR #175 did.
Note this pairs naturally with part 1 of this issue. Both are cases where a
missing or falsy value is silently coerced into filtering behaviour the user
did not ask for — || 100000 turning "show everything" into the default
threshold, and a null holder count turning "unknown" into "suspicious". Worth
checking whether there are other instances of the same shape in transactions.js while you are there, and reporting them even if you do not
fix them.
Additional requirement, from the mutation testing done during the review of
PR #175.
The reviewer applied 15 mutations to `src/shared/transactions.js`; 13 were
killed by the new suite. One of the two survivors falls in the filter logic
this issue touches:
**`src/shared/transactions.js:244` — removing the `tx.holders !== null` guard
survives undetected.** No fixture pairs a non-null `contractAddress` with a
null `holders` value, so the guard that stops the low-holder rule from acting
on an unknown holder count is unpinned.
That is worth closing here rather than leaving it, because the failure
direction is bad: without the guard, a token whose holder count could not be
fetched (a Blockscout hiccup, a rate limit, a self-hosted instance that does
not return the field) would be treated as having zero holders and silently
filtered out of the user's history. That is over-filtering — the same class of
harm as the zero-threshold bug in part 1 of this issue, where the user asks to
see everything and the wallet hides things anyway.
Please add:
- A fixture with a real `contractAddress` and `holders: null`, asserting the
transaction is **not** filtered by the low-holder rule.
- The same for `holders: undefined` if that is reachable from the Blockscout
response shape — check rather than assume.
- Verification that the mutation is now caught: delete the `!== null` guard,
confirm at least one test fails, revert, and state the result in the PR as
PR #175 did.
Note this pairs naturally with part 1 of this issue. Both are cases where a
missing or falsy value is silently coerced into filtering behaviour the user
did not ask for — `|| 100000` turning "show everything" into the default
threshold, and a null holder count turning "unknown" into "suspicious". Worth
checking whether there are other instances of the same shape in
`transactions.js` while you are there, and reporting them even if you do not
fix them.
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.
Two small defects found while writing the first test coverage for the
anti-poisoning filters (#160 / PR #175), deliberately not fixed there. Both are
in
src/shared/transactions.jsand both are a few lines, so grouping them.1. A dust threshold of 0 is silently replaced by the default
src/shared/transactions.js:257:0is falsy, so a user who sets the threshold to0— the obvious way to say"do not hide anything as dust" — gets the 100,000 gwei default applied instead.
The wallet then hides transactions the user explicitly asked to see, with no
indication why.
This is reachable: the threshold is a free-text editable field at
src/popup/views/settings.js:305-310. It also directly contradictsREADME.md:810-814, which promises the user can "configure the wallet to showeverything unfiltered, unix-style" —
0is exactly how a user would expressthat, and it is the one value that does not work.
Requirements
??) or an explicitundefined/nulltest rather than||.0means: almost certainly "disable dustfiltering", which is also what the separate enable/disable checkbox does.
Make sure the two controls agree and neither silently overrides the other.
settings.js:305-310— confirm whathappens for empty string, negative numbers, and non-numeric input, and make
each behave sensibly rather than coercing to a surprising default. Say in
the PR what each now does.
src/shared/for the same||-as-default pattern applied to any othernumeric setting; this class of bug rarely appears once. Report what you find
even if you do not fix it here.
2.
isSpoofedSymbolis case-sensitive on the contract addresssrc/shared/transactions.js:209compares the contract address withoutnormalising case, unlike the blocklist check at
transactions.js:215-217,which lowercases correctly.
Ethereum addresses are routinely rendered in EIP-55 mixed-case checksum form.
A checksummed address for a genuine, known contract will therefore fail the
equality test and be misclassified as a spoof — a false positive that hides
a legitimate token transfer from the user's history.
The reporter notes this is currently unreachable in-app, because the addresses
reaching this function happen to already be lowercased upstream. That makes it
latent rather than live:
isSpoofedSymbolis an exported function, theinconsistency with the adjacent blocklist check is exactly the kind of thing a
later refactor trips over, and the failure direction is silent data loss from
the history view.
Requirements
transactions.js:209, thesame way
:215-217already does.transactions.jsfor the sameinconsistency and make them uniform. Prefer one shared normalisation helper
over repeating
.toLowerCase()at each site, so the next comparison addedcannot get it wrong.
not flagged as a spoof.
tests/transactions.test.jsalready has acurrent behaviour:test pinning the case-sensitive behaviour — invert itrather than deleting it.
Definition of done
0disables dust filtering rather than applying the100,000 gwei default.
the PR states what each does.
||-as-numeric-default insrc/shared/is reported.isSpoofedSymboltreats a checksummed genuine contract as genuine.transactions.jsare consistently normalised,ideally through one helper.
current behaviour:tests are inverted into regression guards.tests/transactions.test.jsstill pass unmodified.TODO.mdupdated in the same commit.make checkpasses.Additional requirement, from the mutation testing done during the review of
PR #175.
The reviewer applied 15 mutations to
src/shared/transactions.js; 13 werekilled by the new suite. One of the two survivors falls in the filter logic
this issue touches:
src/shared/transactions.js:244— removing thetx.holders !== nullguardsurvives undetected. No fixture pairs a non-null
contractAddresswith anull
holdersvalue, so the guard that stops the low-holder rule from actingon an unknown holder count is unpinned.
That is worth closing here rather than leaving it, because the failure
direction is bad: without the guard, a token whose holder count could not be
fetched (a Blockscout hiccup, a rate limit, a self-hosted instance that does
not return the field) would be treated as having zero holders and silently
filtered out of the user's history. That is over-filtering — the same class of
harm as the zero-threshold bug in part 1 of this issue, where the user asks to
see everything and the wallet hides things anyway.
Please add:
contractAddressandholders: null, asserting thetransaction is not filtered by the low-holder rule.
holders: undefinedif that is reachable from the Blockscoutresponse shape — check rather than assume.
!== nullguard,confirm at least one test fails, revert, and state the result in the PR as
PR #175 did.
Note this pairs naturally with part 1 of this issue. Both are cases where a
missing or falsy value is silently coerced into filtering behaviour the user
did not ask for —
|| 100000turning "show everything" into the defaultthreshold, and a null holder count turning "unknown" into "suspicious". Worth
checking whether there are other instances of the same shape in
transactions.jswhile you are there, and reporting them even if you do notfix them.