Skip to content

fix(BOP-510): reject from == to in seizeWithMemo (self-seize) - #195

Open
rayyan224 wants to merge 3 commits into
mainfrom
rayyanalam/bop-510-self-seize-from-to-produces-misleading-events
Open

fix(BOP-510): reject from == to in seizeWithMemo (self-seize)#195
rayyan224 wants to merge 3 commits into
mainfrom
rayyanalam/bop-510-self-seize-from-to-produces-misleading-events

Conversation

@rayyan224

Copy link
Copy Markdown
Collaborator

Summary

  • seizeWithMemo's balance move is a no-op when from == to, but the call still emitted Transfer, Memo, and Seized with no balance change once the address passed both the SeizeHolder and SeizeReceiver checks — letting a SEIZE_ROLE holder pollute the compliance trail with misleading events. No fund loss.
  • Rejects from == to with InvalidReceiver in MockB20.seizeWithMemo (shared by the asset and stablecoin mocks), right after the existing zero-receiver check.
  • Updates IB20.seizeWithMemo natspec with the new revert condition.
  • Adds test_seizeWithMemo_revert_selfSeize and two revert-order tests (role_beats_selfSeize, selfSeize_beats_blocked); fixes two existing fuzz tests (revert_accountNotBlocked, revert_receiverPolicyForbids) that lacked a from != to assumption and would occasionally hit the new guard instead of their intended revert.
  • Extends the seize smoke journey (script/smoke/journeys/seize.py) with a self-seize edge case.
  • Mirrors the companion fix in base (Rust precompile).

Linear

BOP-510 — Cantina finding L-04.

Test plan

  • forge test — all 724 tests pass (0 failed).
  • forge test --match-contract B20SeizeWithMemo — 22/22 pass, including the new self-seize and revert-order tests.
  • forge fmt --check clean on all changed files.

seizeWithMemo's balance move is a no-op when from == to, but the call
still emitted Transfer, Memo, and Seized with no balance change once
the address passed both the SeizeHolder and SeizeReceiver checks,
letting a SEIZE_ROLE holder pollute the compliance trail. Reject
from == to with InvalidReceiver before either policy check, mirroring
the zero-receiver guard.

Two existing fuzz tests (revert_accountNotBlocked, revert_
receiverPolicyForbids) lacked a from != to assumption and would have
occasionally hit the new guard instead of their intended revert;
added vm.assume(from != to) to keep them scoped to their own check.

Extends the seize smoke journey with a self-seize edge case.

Co-Authored-By: Claude <noreply@anthropic.com>
@linear

linear Bot commented Aug 7, 2026

Copy link
Copy Markdown

BOP-510

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Interface Coverage

✅ All interface functions have test coverage.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

📊 Forge Coverage (src/lib/)

🟡 ≥95% across all metrics — some metrics below 99%.

File Lines Stmts Branches Funcs
🔴 B20FactoryLib.sol 95.40% 96.00% 100.00% 90.00%
🔴 test/lib/ForceFeeder.sol 0.00% 0.00% 100.00% 0.00%
🔴 test/lib/PrecompileProbe.sol 0.00% 0.00% 0.00% 0.00%
🟢 MockActivationRegistry.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockActivationRegistryStorage.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20Asset.sol 100.00% 100.00% 100.00% 100.00%
🟡 MockB20Factory.sol 98.96% 99.10% 100.00% 100.00%
🟢 MockB20Stablecoin.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20Storage.sol 100.00% 100.00% 100.00% 100.00%
🟡 MockPolicyRegistry.sol 100.00% 99.54% 97.67% 100.00%
🟢 MockPolicyRegistryStorage.sol 100.00% 100.00% 100.00% 100.00%
Total 96.80% 97.29% 98.15% 96.49%

Full report: download artifact. To browse locally: make coverage (runs forge coverage + genhtml + opens the HTML report).

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

⚠️ Fork tests: 4 failed, 712 passed

These failures indicate divergences where base/base needs to catch up to the base-std spec. This check is advisory and does not block merging.

Failing tests
  • test_seizeWithMemo_revertOrder_selfSeize_beats_blocked(address): Error != expected error: AccountNotSeizable(0xd0CA30A71FD35894Ec18aa30242E8AdbC392c95E) != InvalidReceiver(0xd0CA30A71FD35894Ec18aa30242E8AdbC392c95E); counterexample: calldata=0xbff2d70b000000000000000000000000d0ca30a71fd35894ec18aa30242e8adbc392c95e args=[0xd0CA30A71FD35894Ec18aa30242E8AdbC392c95E]
  • test_seizeWithMemo_revert_selfSeize(address,uint256): next call did not revert as expected; counterexample: calldata=0xffbeec22000000000000000000000000cb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f4240 args=[0xCB00000000000000000000000000000000000000, 1000000 [1e6]]

Comment thread src/interfaces/IB20.sol Outdated
Comment thread test/lib/mocks/MockB20.sol Outdated
Comment on lines +335 to +343
/// would be a burn), `from != to` (otherwise `_moveBalance` is a no-op that would still emit a
/// misleading `Transfer`/`Memo`/`Seized`), and — unlike a normal transfer — no sender/receiver/
/// executor transfer policy is consulted, no allowance is spent, and `from` is not zero-checked
/// (consistent with the burn-blocked family; a zero/empty `from` fails the seizable or balance
/// check anyway). The membership checks are that `from` is blocked under `SEIZE_HOLDER_POLICY`
/// and `to` is authorized under `SEIZE_RECEIVER_POLICY` (mirroring `MINT_RECEIVER_POLICY`; an
/// unset slot is always-allow, so a treasury need not be allowlisted by default). Deliberately
/// does NOT reuse the factory-bootstrap privileged path (which would silently skip the receiver
/// policy); every skip here is explicit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a cleanup pass on this block. I know it's mostly legacy and not in scope for this PR but it could be slimmed down to about half the length imo.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High level, i think can be simplifed to:

Reassigns the balance of a user to a different user. Seized users are those whose SEIZE_HOLDER_POLICY alllowed policy returns true, SEIZE_RECEIVER_POLICY are those who can receive.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed it down per both suggestions — dropped the factory-bootstrap-path aside and folded the rest into a 3-line @dev.

Comment on lines +64 to +65
// `account` is seizable (blocked) and SEIZE_RECEIVER_POLICY is unset (always-allow), so both
// membership checks would pass — the self-seize guard must fire before either is consulted.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusing inline honestly... consider removing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed — agreed, the @notice above already covers it.

rayyan224 and others added 2 commits August 7, 2026 13:57
Co-authored-by: katzman <steve.katzman@coinbase.com>
- IB20.sol: collapse the two-line InvalidReceiver @dev back to one
  line (accepted suggestion).
- MockB20.sol: cut the seizeWithMemo @dev block roughly in half per
  review feedback; legacy verbosity, not specific to this change.
- seizeWithMemo.t.sol: drop the inline comment in
  test_seizeWithMemo_revert_selfSeize; the @notice above the function
  already covers it.

Co-Authored-By: Claude <noreply@anthropic.com>

@robriks robriks left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good stuff!

surfaced a side question on the rust PR about 0-value seizures, I'll run it by Roger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants