feat(BOP-491): add SEIZE_RECEIVER_POLICY seize destination gate - #188
Conversation
Adds a receiver-side seize policy mirroring MINT_RECEIVER_POLICY: seizeWithMemo now gates the destination "to" under SEIZE_RECEIVER_POLICY (unset = ALWAYS_ALLOW, so a treasury need not be allowlisted by default). Lets issuers constrain where seized funds may land, derisking a runaway SEIZE_ROLE. Cobalt (V2) surface. - B20Constants + IB20: SEIZE_RECEIVER_POLICY constant and getter; seize natspec. - MockB20: _requireSeizeReceiver gate after the seizable check; read/write arms. - MockB20Storage: pack the receiver id as lane 1 of the seize slot (slot 14, bits 64..127); add codec + extractor. - Tests: receiver gate unit + revert-order (blocked>receiver, receiver>balance), layout pin, slot-helper roundtrip, updatePolicy scope. Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Interface Coverage✅ All interface functions have test coverage. |
📊 Forge Coverage (
|
| 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.79% | 97.29% | 98.14% | 96.49% |
Full report: download artifact. To browse locally: make coverage (runs forge coverage + genhtml + opens the HTML report).
| /// @dev Mirrors `MINT_RECEIVER_POLICY`: always enforced on the seize destination. An unset slot reads | ||
| /// as `0` (always-allow), so seize may send anywhere until an issuer configures the slot — a | ||
| /// treasury need not be allowlisted by default. Setting it lets an issuer constrain where seized | ||
| /// funds may land, derisking a runaway `SEIZE_ROLE`. |
There was a problem hiding this comment.
| /// @dev Mirrors `MINT_RECEIVER_POLICY`: always enforced on the seize destination. An unset slot reads | |
| /// as `0` (always-allow), so seize may send anywhere until an issuer configures the slot — a | |
| /// treasury need not be allowlisted by default. Setting it lets an issuer constrain where seized | |
| /// funds may land, derisking a runaway `SEIZE_ROLE`. | |
| /// @dev Mirrors `MINT_RECEIVER_POLICY`: always enforced on the seize destination. An unset slot reads | |
| /// as `0` (always-allow), so seize may send anywhere until an issuer configures the slot. |
| // receiver policy); every skip here is explicit. | ||
| // non-zero (otherwise this would be a burn), 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. |
There was a problem hiding this comment.
This should all be natspec not inline.
|
… getter doc - Move the seizeWithMemo admin-seize explanation from an inline // block into /// natspec on the function (stevieraykatz). - Trim the SEIZE_RECEIVER_POLICY() getter natspec to match SEIZE_HOLDER_POLICY's concise form (stevieraykatz suggestion). Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
| emit BurnedBlocked(msg.sender, from, amount); | ||
| } | ||
|
|
||
| /// @dev Admin seize: reassign a blocked account's balance. `to` must be non-zero (otherwise this |
There was a problem hiding this comment.
Missing @notice, @params and @returns.
There was a problem hiding this comment.
hmm i'm seeing that we weren't consistent with our natspecs in this contract generally. i'd be ok deferring this for a separate doc cleanup pass
| emit BurnedBlocked(msg.sender, from, amount); | ||
| } | ||
|
|
||
| /// @dev Admin seize: reassign a blocked account's balance. `to` must be non-zero (otherwise this |
There was a problem hiding this comment.
hmm i'm seeing that we weren't consistent with our natspecs in this contract generally. i'd be ok deferring this for a separate doc cleanup pass
What
Adds a receiver-side seize policy,
SEIZE_RECEIVER_POLICY, mirroringMINT_RECEIVER_POLICYexactly.seizeWithMemonow gates the destinationtounder this policy, in addition to the existingSEIZE_HOLDER_POLICYcheck onfrom.An unset slot reads as
ALWAYS_ALLOW(0), so an unconfigured token can still seize to any destination — a treasury need not be allowlisted, preserving the merged default. Issuers who configure it can constrain where seized funds land, derisking a runawaySEIZE_ROLE(symmetric to howMINT_RECEIVER_POLICYconstrains mint destinations).This is a scoped reversal of the original "seize enforces no policy on
to" decision —tois now policy-gated but defaults open. Cobalt (V2) surface; base-std is the source of truth for the base/base mirror (BOP-492).The new receiver id packs into the same seize slot (slot 14) as the seize-holder id — lane 1, bits 64..127 — so no new slot and no offset drift.
Changes
B20Constants/IB20:SEIZE_RECEIVER_POLICYconstant + getter;seizeWithMemonatspec updated for the new gate/revert.MockB20:_requireSeizeReceiver(to)enforced after_requireSeizable(from);_readPolicyId/_writePolicyIdarms.MockB20Storage:SeizePolicyIds.receiverlane +seizeReceiverPolicyIdextractor and updatedpackSeizePolicyIds.Guard order
SEIZEpause ->SEIZE_ROLE->to != 0->AccountNotSeizable(from)->PolicyForbids(SEIZE_RECEIVER_POLICY, to)->InsufficientBalance.Testing
forge test— 719 passed, 4 skipped (fork). New/updated:seizeWithMemo.t.sol: receiver forbids revert, unset-allows-any default, configured-allow success.seizeWithMemo_revertOrder.t.sol:blocked_beats_receiver,receiver_beats_balance.B20FullLayout.t.sol+MockB20SlotHelpers.t.sol: slot-14 lane-1 pin + codec roundtrip.updatePolicy.t.sol+B20Test: new scope routed through the supported-type set.forge fmt --checkclean.Ticket: https://linear.app/coinbase/issue/BOP-491