Skip to content

feat(BOP-440): add transferFromBlockedWithMemo and burnBlockedWithMemo seize surface - #177

Open
stephancill wants to merge 8 commits into
mainfrom
stephancilliers/bop-440-base-std-seize
Open

feat(BOP-440): add transferFromBlockedWithMemo and burnBlockedWithMemo seize surface#177
stephancill wants to merge 8 commits into
mainfrom
stephancilliers/bop-440-base-std-seize

Conversation

@stephancill

@stephancill stephancill commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Adds the seize operation class to the base-std reference + ABI (the source of truth the fork harness cross-validates against base/base), per the BOP-439 PPS. Implements BOP-440 and, per review, also folds in the BOP-471 burnBlocked repoint (base-std portion).

New surface:

  • transferFromBlockedWithMemo(address from, address to, uint256 amount, bytes32 memo) — admin seize that reassigns a blocked account's balance in one call. Skips allowance and the transfer policies; the only membership check is that from is blocked under the new SEIZABLE_POLICY. to is not policy-checked (admin op). Emits Transfer + TransferredFromBlocked + Memo.
  • burnBlockedWithMemo(address from, uint256 amount, bytes32 memo) — memo variant of burn-based seizure.
  • New SEIZABLE_POLICY scope, TRANSFER_FROM_BLOCKED_ROLE, and SEIZE PausableFeature.

Seize operation class (all three gate uniformly on SEIZABLE_POLICY + the SEIZE pause vector):

  • burnBlocked repointed from TRANSFER_SENDER_POLICY/BURN pause to SEIZABLE_POLICY/SEIZE pause. This is a breaking change, acceptable because burnBlocked is not yet in use; intent is a single uniform seize gate rather than a transient split.

Design notes

  • No storage-offset disruption. SEIZABLE_POLICY's id is packed into the previously-reserved lane 3 (bits 192..255) of the TransferPolicyIds slot. No existing field offsets move.
  • Explicit guards, no privileged reuse. Seize applies its own guard sequence (SEIZE pause → role → SEIZABLE_POLICY block check) and moves balance via a shared _moveBalance primitive; it does not reuse the factory-bootstrap privileged path.

Testing

  • forge test (non-fork): 684 passed, 0 failed. forge fmt --check clean.
  • Existing burnBlocked tests updated to the seize gating; new happy-path + revert-order suites for both new functions.

…o seize surface

Adds the additive seize operation class to the base-std reference and ABI
(source of truth for fork tests), per the BOP-439 PPS:

- transferFromBlockedWithMemo(from, to, amount, memo): admin seize that
  reassigns a blocked account's balance in one call. Skips allowance and
  the sender/receiver/executor transfer policies; the only membership check
  is that `from` is blocked under the new SEIZABLE_POLICY. `to` is not
  policy-checked (admin op). Emits Transfer + TransferredFromBlocked + Memo.
- burnBlockedWithMemo(from, amount, memo): memo variant of burn-based
  seizure, gated on SEIZABLE_POLICY and the new SEIZE pause vector.
- New SEIZABLE_POLICY scope (packed into the transfer slot's reserved
  lane 3, no storage-offset disruption), TRANSFER_FROM_BLOCKED_ROLE, and
  SEIZE PausableFeature.

Reference impl uses an explicit guard sequence and a shared _moveBalance
primitive; it does NOT reuse the factory-bootstrap privileged path.

Out of scope: repointing the legacy burnBlocked to SEIZABLE_POLICY (BOP-471).

Tests: 25 new (happy + revert-order for both functions); layout-pin, slot
codec, and removals regression tests updated for the new lane/feature.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
@github-actions

Copy link
Copy Markdown

Interface Coverage

✅ All interface functions have test coverage.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

📊 Forge Coverage (src/lib/)

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

File Lines Stmts Branches Funcs
🟢 B20FactoryLib.sol 100.00% 100.00% 100.00% 100.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% 100.00% 100.00% 100.00%
🟢 MockPolicyRegistryStorage.sol 100.00% 100.00% 100.00% 100.00%
Total 96.98% 97.53% 98.57% 97.12%

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

Comment thread src/interfaces/IB20.sol Outdated
Comment thread src/interfaces/IB20.sol Outdated
Comment thread src/interfaces/IB20.sol Outdated
Comment thread src/lib/B20Constants.sol Outdated
Comment thread test/lib/mocks/MockB20.sol
Comment on lines -58 to +60
/// bits 192..255 : reserved (implicit, no field declared)
/// bits 192..255 : seizable (`SEIZABLE_POLICY`)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

does adding seizable here use up the reserved bits? no more reserved bits after adding it?

@stephancill stephancill Jul 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

AI: Yes — this fills the slot. TransferPolicyIds is one 256-bit slot = 4 × uint64 lanes; sender/receiver/executor used lanes 0–2, and seizable takes lane 3 (bits 192..255), which was the last reserved lane. No reserved bits remain in this slot. A future transfer-side policy would need a new appended slot. I picked this lane specifically because it avoids shifting any existing field offset on the already-shipped layout.

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.

These bits were reserved for exactly this kind of purpose. Good to flag that we won't have any headroom after this lands, but it's correct to consume them like this.

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.

@rayyan224 another candidate for automated agent fuzzing would be any changes to storage layout (including appending)

@ilikesymmetry ilikesymmetry Jul 27, 2026

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.

I'm a bit concerned to take the last space here. Main thing on my mind is if we have some other idea that definitely needs to be on the hotpath transfer. The seizing operations are cold-path and we won't be using any of the other policies here in its execution, so I'm not seeing as clear of a benefit tbh. I'd opt to make a new slot for seize policy entirely by default.

We also recognize seize as an operation applies to burning too. Placing this policy in the "transfer policies" group is not consistent with our separation of "mint policies" as a distinct group by operation.

@stephancill
stephancill marked this pull request as draft July 27, 2026 15:57
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

⚠️ Fork tests: 162 failed, 592 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_burnBlockedWithMemo_revertOrder_blocked_beats_balance(address): Error != expected error: custom error 0x6f872692 != AccountNotBlocked(0xF7DCd4Ea25eF4Da382c475646bB49bc2fa2badB8); counterexample: calldata=0x2cf250e8000000000000000000000000f7dcd4ea25ef4da382c475646bb49bc2fa2badb8 args=[0xF7DCd4Ea25eF4Da382c475646bB49bc2fa2badB8]
  • test_burnBlockedWithMemo_revertOrder_pause_beats_blocked(address): custom error 0xa290249c: type check failed for "(uint8[])" with data: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003; counterexample: calldata=0x26d48986000000000000000000000000aba3bf01deb912586e9fe0cf2cd248e1f159fa00 args=[0xaba3Bf01dEb912586E9fE0cF2cD248E1F159fA00]
  • test_burnBlockedWithMemo_revertOrder_pause_beats_role(address,address): custom error 0xa290249c: type check failed for "(uint8[])" with data: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003; counterexample: calldata=0x53c0e66c000000000000000000000000b20f0000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001256 args=[0xb20f000000000000000000000000000000000001, 0x0000000000000000000000000000000000001256]
  • test_burnBlockedWithMemo_revertOrder_role_beats_blocked(address,address): Error != expected error: custom error 0x6f872692 != AccessControlUnauthorizedAccount(0x08071d75d2b1ce51089D8445D2849b01eA618930, 0x7408fdc0d31c7bcb349eab611f5d1168acd4303574993f8cdc98b1cd18c41cae); counterexample: calldata=0xbd3f8b6b00000000000000000000000008071d75d2b1ce51089d8445d2849b01ea6189300000000000000000000000001c277dec893a32b1dc1c9338b3dc626f4d44ba37 args=[0x08071d75d2b1ce51089D8445D2849b01eA618930, 0x1C277dec893a32B1dc1c9338b3Dc626f4D44Ba37]
  • test_burnBlockedWithMemo_revert_accountNotBlocked(address,uint256): Error != expected error: custom error 0x6f872692 != AccountNotBlocked(0xdB3032371D504228B5B85d90226ac9552c240CA1); counterexample: calldata=0xf0e9ee6a000000000000000000000000db3032371d504228b5b85d90226ac9552c240ca100000000000000000000000000000000000000000000002964aca876fc47282e args=[0xdB3032371D504228B5B85d90226ac9552c240CA1, 763570865350870771758 [7.635e20]]
  • test_burnBlockedWithMemo_revert_insufficientBalance(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xeb275e77000000000000000000000000ee28c5852dc86ffefdf27415ad678e8cec1be48e0000000000e031a49f183e5c1bda08a60a06af51c98a09b5a78204c89b732aab args=[0xEE28c5852Dc86fFefdF27415Ad678e8CEC1BE48e, 92228028519831563145088605584571008137073861029429821041842268843 [9.222e64]]
  • test_burnBlockedWithMemo_revert_unauthorized(address,address,uint256): Error != expected error: custom error 0x6f872692 != AccessControlUnauthorizedAccount(0xc245a3bE59021043C1C001549E2eE8Bdcb87A8bf, 0x7408fdc0d31c7bcb349eab611f5d1168acd4303574993f8cdc98b1cd18c41cae); counterexample: calldata=0xb8e4c85b000000000000000000000000c245a3be59021043c1c001549e2ee8bdcb87a8bf00000000000000000000000077a55237ded17a2be1799a7edd091b137ee3dadf000000000000000000000000000000000000000000000000000eb2b09925521f args=[0xc245a3bE59021043C1C001549E2eE8Bdcb87A8bf, 0x77A55237deD17A2be1799a7Edd091B137EE3DaDF, 4137121227297311 [4.137e15]]
  • test_burnBlockedWithMemo_revert_whenSeizePaused(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xfce2f39a00000000000000000000000000000000000000000000000000000000000011f30000000000000000000000000000000000000000000000000000000000001055 args=[0x00000000000000000000000000000000000011F3, 4181]
  • test_burnBlockedWithMemo_success_debitsAndDecreasesSupply(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x189a0bed000000000000000000000000c44cee8d937dde5f08cd1e1cf9a1d7b9378f1a0800000000000000000000007233926dddc097248f6fc3b1bb7441bd5aae64b752 args=[0xc44CEE8D937DDE5F08cD1E1CF9A1d7b9378f1a08, 166905610664531500166876215383213756476364043892562 [1.669e50]]
  • test_burnBlockedWithMemo_success_emitsTransferBurnedBlockedAndMemo(address,uint256,bytes32): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x25c74b37000000000000000000000000cb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff7ed2de698d9d321278a6388c54d27ac6b0d08e8908897576f03af493f9d5cc69 args=[0xCB00000000000000000000000000000000000000, 18446744073709551615 [1.844e19], 0x7ed2de698d9d321278a6388c54d27ac6b0d08e8908897576f03af493f9d5cc69]
  • test_burnBlocked_revertOrder_pause_beats_balance(address,uint256): custom error 0xa290249c: type check failed for "(uint8[])" with data: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003; counterexample: calldata=0xca82fe43000000000000000000000000c029088ca30e467be9546a35f36d8ba00cbfa96800000000fc9083d6f9fc634c1e13c5e6e96a099a3f2597546729c33b29c289a8 args=[0xc029088cA30E467be9546a35f36d8BA00CBFa968, 26598147522853179956990371914500046998193758818841651139589729061288 [2.659e67]]
  • test_burnBlocked_revertOrder_pause_beats_blocked(address,uint256): custom error 0xa290249c: type check failed for "(uint8[])" with data: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003; counterexample: calldata=0x4c29bb1300000000000000000000000085ced38a7e32be69119852fbead1bcc3fef5314100000000000000000000000000000000000000000000000000000010fc394c7d args=[0x85ceD38a7E32BE69119852Fbead1bCC3FeF53141, 72951090301 [7.295e10]]
  • test_burnBlocked_revertOrder_pause_beats_role(address,address,uint256): custom error 0xa290249c: type check failed for "(uint8[])" with data: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003; counterexample: calldata=0x022471a50000000000000000000000005795071ffb2a4c79fcc9965192f5de1467482345000000000000000000000000d558d7158d5050018f92165f4a23f5d82bb1afdd0000000000000000000000000000000000000000000000000000000000000000 args=[0x5795071FFb2A4c79fCc9965192f5DE1467482345, 0xD558D7158d5050018F92165f4A23f5d82Bb1AfDD, 0]
  • test_burnBlocked_revert_insufficientBalance(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x15f3e92c00000000000000000000000000000000000000000000000000000000000012a30000000000000000000000000000000000000000000000000000000000001949 args=[0x00000000000000000000000000000000000012a3, 6473]
  • test_burnBlocked_revert_whenSeizePaused(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xf55efb32000000000000000000000000cb0000000000000000000000000000000000000000000000000000000000000000000000000000000000021e19e0c9bab2400000 args=[0xCB00000000000000000000000000000000000000, 10000000000000000000000 [1e22]]
  • test_burnBlocked_success_debitsTarget(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x485c308e00000000000000000000000013e2a1f0effce4c1fade2cbc22d0b38076290ddf00000000000000000000000000959e1c425772e97cc2cfa1e373d8e3a2ea0088 args=[0x13E2A1F0effCe4C1FADE2CbC22d0B38076290ddF, 3336584391847662995301278645616880962746581128 [3.336e45]]
  • test_burnBlocked_success_decreasesTotalSupply(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x8bb754f20000000000000000000000000000000000000000000000000000000000002e790000000000000000000000000000000000000000000000000000000000001000 args=[0x0000000000000000000000000000000000002e79, 4096]
  • test_burnBlocked_success_emitsTransferAndBurnedBlocked(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x89de84f8000000000000000000000000000000000000000000000000000000000000015500000000000000000000000000000000000000000000000000000000000000b2 args=[0x0000000000000000000000000000000000000155, 178]
  • test_childrenSlot_success_locatesChildArray(uint8): custom error 0x6fdd1491; counterexample: calldata=0x69fce77600000000000000000000000000000000000000000000000000000000000000cd args=[205]
  • test_createCompositePolicy_revertOrder(uint8): custom error 0x6fdd1491; counterexample: calldata=0x5fafe4f700000000000000000000000000000000000000000000000000000000000000c0 args=[192]
  • test_createCompositePolicy_revert_builtinChild(address,address,uint8): Error != expected error: custom error 0x6fdd1491 != InvalidChildPolicy(0); counterexample: calldata=0x87108a4b0000000000000000000000000fa65bd96a942d9b5e2ee9a86e79f3a7b2c3225c00000000000000000000000079d6fb329077a10ef72a4f3d221207d88ceee5fc0000000000000000000000000000000000000000000000000000000000000002 args=[0x0fA65BD96a942d9b5e2ee9a86e79f3a7B2c3225c, 0x79D6Fb329077A10ef72A4F3D221207d88CEEE5Fc, 2]
  • test_createCompositePolicy_revert_childPoliciesOutsideOfRange(address,address,uint8,uint8): Error != expected error: custom error 0x6fdd1491 != ChildPoliciesOutsideOfRange(2, 4); counterexample: calldata=0xb8466436000000000000000000000000afa36502c2864e18720fdd18fc65edcf3c27199b000000000000000000000000657197f53eba539766b295f612f76b36dbaf44fb000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000034 args=[0xAFa36502C2864E18720FDd18Fc65EdcF3c27199B, 0x657197f53EBA539766b295F612F76b36DbAf44fB, 15, 52]
  • test_createCompositePolicy_revert_incompatiblePolicyType(address,address,uint8): Error != expected error: custom error 0x6fdd1491 != IncompatiblePolicyType(); counterexample: calldata=0x973269250000000000000000000000000000000000000000000000000000000000003203000000000000000000000000000000000000000000000000000000000000140d0000000000000000000000000000000000000000000000000000000000000008 args=[0x0000000000000000000000000000000000003203, 0x000000000000000000000000000000000000140D, 8]
  • test_createCompositePolicy_revert_invalidChildPolicy(address,address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x7416f45900000000000000000000000061b86fa2d2b8835727a7913d4d61d429c57e6ce8000000000000000000000000d70fe969aee813404eaba4abe2f9d7a65fe63f65000000000000000000000000000000000000000000000000000000000000007d args=[0x61b86fA2D2b8835727A7913d4D61D429c57e6CE8, 0xD70fE969Aee813404Eaba4aBe2F9D7a65fe63F65, 125]
  • test_createCompositePolicy_revert_policyNotFound(address,address,uint8,uint64): Error != expected error: custom error 0x6fdd1491 != PolicyNotFound(); counterexample: calldata=0x1803f6050000000000000000000000001b056ebe595809003997a3f4608bb7e4d84a50d1000000000000000000000000310486e48ff2d4ccf5a20c090e1966df651c9cdd00000000000000000000000000000000000000000000000000000000000000a20000000000000000000000000000000000000000000000000000000be995c9a1 args=[0x1B056EBE595809003997a3F4608Bb7e4d84a50d1, 0x310486e48fF2d4CCF5A20c090e1966Df651c9cDd, 162, 51163548065 [5.116e10]]
  • test_createCompositePolicy_revert_zeroAdmin(address,uint8): Error != expected error: custom error 0x6fdd1491 != ZeroAddress(); counterexample: calldata=0xb18b5cb1000000000000000000000000eb142fdbfca43ff409ad474fa96c79d0a7aa573a0000000000000000000000000000000000000000000000000000000000000005 args=[0xEb142fDbfcA43Ff409Ad474Fa96C79d0A7aa573A, 5]
  • test_createCompositePolicy_success_advancesNextPolicyId(address,uint8,uint8): custom error 0x6fdd1491; counterexample: calldata=0xf9a1ca770000000000000000000000000000000000000000000000000000000000000121000000000000000000000000000000000000000000000000000000000000001d0000000000000000000000000000000000000000000000000000000000000098 args=[0x0000000000000000000000000000000000000121, 29, 152]
  • test_createCompositePolicy_success_atMaxChildren(address,address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x296e3341000000000000000000000000148d4510e5fa634f0af24294971d83b50dcd3fc0000000000000000000000000550cac57d9e64bfcc4ab11a9bfe36fa388a4f4f00000000000000000000000000000000000000000000000000000000000000030 args=[0x148D4510e5Fa634f0aF24294971d83b50dcD3fC0, 0x550CAc57D9E64BFCC4ab11A9BfE36Fa388A4f4f0, 48]
  • test_createCompositePolicy_success_emitsCompositePolicyUpdated(address,address,uint8): log != expected log; counterexample: calldata=0x1694cfe00000000000000000000000000000000000000000000000000000000000000e7a000000000000000000000000000000000000000000000000000000000c9fd5810000000000000000000000000000000000000000000000000000000000000050 args=[0x0000000000000000000000000000000000000e7a, 0x000000000000000000000000000000000C9fD581, 80]
  • test_createCompositePolicy_success_emitsInitialPolicyAdminUpdated(address,address,uint8): log != expected log; counterexample: calldata=0xd6a6c2b200000000000000000000000000000000000000000000000000000000000010ff0000000000000000000000000000000000000000000000000000000000002b95000000000000000000000000000000000000000000000000000000000000000c args=[0x00000000000000000000000000000000000010FF, 0x0000000000000000000000000000000000002B95, 12]
  • test_createCompositePolicy_success_emitsPolicyCreated(address,address,uint8): log != expected log; counterexample: calldata=0x89031165000000000000000000000000941b7bae32b03eaf38be8a4850241c4c50af155e000000000000000000000000aceeee4a7804b35ae0da7565812653bb9ac1d2d4000000000000000000000000000000000000000000000000000000000000004b args=[0x941B7bae32b03EAF38BE8a4850241C4C50af155e, 0xaCEEeE4A7804b35AE0DA7565812653Bb9AC1D2d4, 75]
  • test_createCompositePolicy_success_intersect(address,address): custom error 0x6fdd1491; counterexample: calldata=0xccbe0bd4000000000000000000000000000000000000000000000000000000004a43ae88000000000000000000000000cb00000000000000000000000000000000000000 args=[0x000000000000000000000000000000004A43AE88, 0xCB00000000000000000000000000000000000000]
  • test_createCompositePolicy_success_union(address,address): custom error 0x6fdd1491; counterexample: calldata=0xd748b1fe000000000000000000000000319bdeae827bd34f5d464c3a889bdecaaad2a4db0000000000000000000000006b78ff25f76c742b47f239da5a8940f9de2af845 args=[0x319BdeaE827bD34f5D464c3a889bDEcaAAd2A4dB, 0x6B78ff25F76C742B47f239Da5A8940f9dE2Af845]
  • test_isAuthorized_success_composite_mixedChildTypes(address): custom error 0x6fdd1491; counterexample: calldata=0xca56e88d000000000000000000000000566e2e47985fdd22440a24812be2db427de7d120 args=[0x566E2e47985fdd22440a24812be2Db427DE7d120]
  • test_isAuthorized_success_composite_reflectsChildMembershipChange(address): custom error 0x6fdd1491; counterexample: calldata=0x9154ecaf0000000000000000000000008453000000000000000000000000000000000002 args=[0x8453000000000000000000000000000000000002]
  • test_isAuthorized_success_intersect_allChildrenAuthorize(address): custom error 0x6fdd1491; counterexample: calldata=0x12f37cef0000000000000000000000009c608ba1aca469cb07153de333bb28b2d4ededd6 args=[0x9c608BA1acA469Cb07153De333bb28b2d4EDEDD6]
  • test_isAuthorized_success_intersect_oneChildDenies(address): custom error 0x6fdd1491; counterexample: calldata=0x70fa07e600000000000000000000000022f8d0d97fa716f16e3f3abe18f199e9eecafa5b args=[0x22F8d0d97FA716f16e3f3aBE18f199e9eecAFa5B]
  • test_isAuthorized_success_union_anyChildAuthorizes(address): custom error 0x6fdd1491; counterexample: calldata=0x1ab54113000000000000000000000000849db99838842ab11aad6c2468336b8e6ff1cb29 args=[0x849dB99838842ab11aad6c2468336B8E6FF1cB29]
  • test_isAuthorized_success_union_noChildAuthorizes(address): custom error 0x6fdd1491; counterexample: calldata=0x18ea2b870000000000000000000000005f0a0d884e45fbdb62911569959e8448c8bd251a args=[0x5F0a0d884E45fBDb62911569959e8448C8bD251a]
  • test_multiplier_success_returnsStoredValue(uint256): InvalidMultiplier(); counterexample: calldata=0x538a9cdf0000000000000000000000005e1fa03fb540a49a02c10eeb9dae50468ce0ba42 args=[537350415203059475007070180992761528583517682242 [5.373e47]]
  • test_policyId_success_reflectsUpdatePolicy(uint8,uint64): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xa21d943e00000000000000000000000000000000000000000000000000000000000000b200000000000000000000000000000000000000000000000000000000000021fe args=[178, 8702]
  • test_policyId_success_zeroByDefault(uint8): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x11a5c8f40000000000000000000000000000000000000000000000000000000000000021 args=[33]
  • test_scaledBalanceOf_success_zeroForEmptyAccount(address,uint256): InvalidMultiplier(); counterexample: calldata=0x2f24150f000000000000000000000000015c923f270cd4a936f614b21afac1224e4230be0fb20ed0f86e9f67eef04faf74000fbc0f56a5df3954c6ae14c8aaf4c2d73c28 args=[0x015c923F270cd4a936F614b21aFAc1224e4230bE, 7099293764560265936169582393395556258849848704777863850817066419643995339816 [7.099e75]]
  • test_toRawBalance_success_zeroScaledBalance(uint256): InvalidMultiplier(); counterexample: calldata=0x2c97287100000000000000000000001cea99cdc85186c201c22db598c494d8d566950829 args=[42261379625836891909117049673610072030025425946665 [4.226e49]]
  • test_toScaledBalance_revert_arithmeticOverflow(uint256,uint256): InvalidMultiplier(); counterexample: calldata=0xfce6199100000000000000000000000000000000000000000000000000000000001c7dfc000000000000000000107e20d7a9b1bf9d44ca912ce739f2c899c33d33ee9220 args=[1867260 [1.867e6], 1579685736949354795046464160310035059481669545153696288 [1.579e54]]
  • test_toScaledBalance_success_zeroRawBalance(uint256): InvalidMultiplier(); counterexample: calldata=0x26ec913e0000000000000000000000fcc66709a49a25e9f20b632e3cfcccbb85042d7940 args=[369431090596787883799499266388356611058387041220928 [3.694e50]]
  • test_transferFromBlockedWithMemo_revertOrder_blocked_beats_balance(address,address): Error != expected error: custom error 0x6b27a2b2 != AccountNotBlocked(0x4AC8814Dd4DcB84E812A81B746467f84e3dCAdFA); counterexample: calldata=0x2378bb4d0000000000000000000000004ac8814dd4dcb84e812a81b746467f84e3dcadfa00000000000000000000000067f39aeb48335656ccc7e44495178d7d9f86dab9 args=[0x4AC8814Dd4DcB84E812A81B746467f84e3dCAdFA, 0x67f39aeb48335656CCC7E44495178d7d9f86dab9]
  • test_transferFromBlockedWithMemo_revertOrder_pause_beats_blocked(address,address): custom error 0xa290249c: type check failed for "(uint8[])" with data: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003; counterexample: calldata=0x5d169bf1000000000000000000000000a452597c3aec7c9b1b49eca1a2c23b0712a68530000000000000000000000000cf1bc0177187b0c6d90b32b31f0da5f48953f665 args=[0xa452597C3AeC7c9B1B49Eca1A2c23b0712a68530, 0xcF1bC0177187B0c6d90b32B31F0Da5F48953F665]
  • test_transferFromBlockedWithMemo_revertOrder_pause_beats_role(address,address,address): custom error 0xa290249c: type check failed for "(uint8[])" with data: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003; counterexample: calldata=0xa9d671d1000000000000000000000000ebe8e2f779f7f871ca2dce341b7418a11346172f000000000000000000000000c282b769516dd5186c71c92d3ebb027eb5b73dcc00000000000000000000000080fbf90a80099cb99ba603ab9a69b803d8732324 args=[0xebe8e2F779f7f871CA2DCe341B7418A11346172f, 0xC282B769516DD5186C71c92d3ebB027eB5B73dCc, 0x80fbF90a80099Cb99ba603ab9A69B803d8732324]
  • test_transferFromBlockedWithMemo_revertOrder_role_beats_zeroActors(address,address): Error != expected error: custom error 0x6b27a2b2 != AccessControlUnauthorizedAccount(0xC31Db01C9c39ecb0C9E2096fE6F91452aa6B64D5, 0xf16b87e355bcd4200b68b68a5a44873be4281da4d192baf04688c5e3c4d2beb1); counterexample: calldata=0x2d469dae000000000000000000000000c31db01c9c39ecb0c9e2096fe6f91452aa6b64d50000000000000000000000001c36276e4a4fac67e2b8a78a8beb18cc2a545409 args=[0xC31Db01C9c39ecb0C9E2096fE6F91452aa6B64D5, 0x1c36276e4A4FaC67E2b8A78a8beb18CC2A545409]
  • test_transferFromBlockedWithMemo_revertOrder_zeroActors_beats_blocked(address): Error != expected error: custom error 0x6b27a2b2 != InvalidReceiver(0x0000000000000000000000000000000000000000); counterexample: calldata=0xc63d5a350000000000000000000000002ccea2f9b5b91160faf0a67d10e47ebd3a097e19 args=[0x2CCEA2F9b5B91160FAF0A67D10e47eBd3a097e19]
  • test_transferFromBlockedWithMemo_revert_accountNotBlocked(address,address,uint256): Error != expected error: custom error 0x6b27a2b2 != AccountNotBlocked(0x0000000000000000000000000000000000002eAf); counterexample: calldata=0x37127b030000000000000000000000000000000000000000000000000000000000002eaf000000000000000000000000cb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064 args=[0x0000000000000000000000000000000000002eAf, 0xCB00000000000000000000000000000000000000, 100]
  • test_transferFromBlockedWithMemo_revert_insufficientBalance(address,address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x5974c2f20000000000000000000000002aeae4d7fba87f2bb2d633cef3580295096201f6000000000000000000000000926fcd0a3a576afbdd1dad38d6798e2bf74cdce004e3a944d601b81619f768db0d06ed252b0bf8b02f3cda0cd4ecedbe930b47d1 args=[0x2aEAE4D7FBA87f2bB2d633CEf3580295096201F6, 0x926fcD0a3A576aFBdd1Dad38D6798e2bf74cDcE0, 2211493928984277376375933119531109908722614936582170775855176609996676483025 [2.211e75]]
  • test_transferFromBlockedWithMemo_revert_invalidReceiver(address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x7b2563d80000000000000000000000007d00648b3aa847a6ce083eeff6f128de9a046e8c0000000000000000000000000000000000000000000000000000000035785713 args=[0x7d00648b3aA847a6cE083EeFf6f128de9a046e8c, 897079059 [8.97e8]]
  • test_transferFromBlockedWithMemo_revert_unauthorized(address,address,address,uint256): Error != expected error: custom error 0x6b27a2b2 != AccessControlUnauthorizedAccount(0x7e49045F9bcE53f98Cc8e81e98F39B8db4F41a9e, 0xf16b87e355bcd4200b68b68a5a44873be4281da4d192baf04688c5e3c4d2beb1); counterexample: calldata=0x195ea85c0000000000000000000000007e49045f9bce53f98cc8e81e98f39b8db4f41a9e00000000000000000000000007df8c74ea8dc2f6fb35218020de82da0a5b9743000000000000000000000000e56e4b6cdd1cd00b1a7b7f2a273d86ceab1ca1b1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc args=[0x7e49045F9bcE53f98Cc8e81e98F39B8db4F41a9e, 0x07df8c74eA8dC2f6fb35218020de82dA0a5b9743, 0xE56E4b6cDD1Cd00B1A7b7f2A273d86cEAB1Ca1B1, 115792089237316195423570985008687907853269984665640564039457584007913129639932 [1.157e77]]
  • test_transferFromBlockedWithMemo_revert_whenSeizePaused(address,address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xb16d64540000000000000000000000003be17566613fb42b8aa6cf627a0eb203e903a792000000000000000000000000ce9d4d0a2edf784205b760c18d80712949d26fc70000000000000000000000000000000000000000000000000000000000295cc5 args=[0x3Be17566613FB42b8aA6cf627a0EB203e903a792, 0xce9D4D0A2eDf784205B760C18d80712949D26Fc7, 2710725 [2.71e6]]
  • test_transferFromBlockedWithMemo_success_emitsEvents(address,address,uint256,bytes32): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x5f087a7f000000000000000000000000fc4533ebd305dcc1c3769deeb5e6ff4606fcd6570000000000000000000000003b1b784f783b5a2d96103089e9684fe2353b258700000000000000000000000002e384934b70736b70ab15842515025497b17a13f1c57c4bbe65a2299f147f9643fb83d83101e562ea8ff79ee9c3b52dddb8c66b args=[0xfC4533Ebd305DCc1C3769deEb5e6Ff4606fCd657, 0x3B1B784F783B5a2d96103089e9684fE2353B2587, 16491799645241038153517928427335739281767496211 [1.649e46], 0xf1c57c4bbe65a2299f147f9643fb83d83101e562ea8ff79ee9c3b52dddb8c66b]
  • test_transferFromBlockedWithMemo_success_ignoresReceiverPolicy(address,address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x0f0a256e000000000000000000000000a019a9295c5a61cd8bb053398dee05d0545211ae000000000000000000000000d5fd890af36eb83db9dd8ec35e3b6d67f67237660000000000000000000000000000000000000000000000000000000000000001 args=[0xA019a9295C5a61cD8BB053398dee05D0545211ae, 0xd5FD890Af36eb83db9DD8eC35E3B6D67f6723766, 1]
  • test_transferFromBlockedWithMemo_success_movesBalance(address,address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xbb286a34000000000000000000000000dc0f513ac1c5f9fc925bfba08a9027dd1e549915000000000000000000000000eac45a45c48497cb9bc16b092caf157322909d5600000000000000000000000000000000000000001abac3bd3beab7cc73a4faae args=[0xDc0f513Ac1c5F9FC925bfba08A9027dD1e549915, 0xEaC45a45C48497cB9Bc16B092cAF157322909d56, 8272394810019547606791027374 [8.272e27]]
  • test_transferFromBlockedWithMemo_success_noAllowanceRequired(address,address,uint256): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xae5f9a740000000000000000000000000c9ec885c57aba6a592ea9a6a8c007d7ca75cd27000000000000000000000000cd9705df4a3fdccfc8aebd082d0af48c67f3e39f000000000000000000000000002d268f2f6b412fa701afbc1582113faead74ae args=[0x0C9eC885c57aBa6A592Ea9a6A8C007d7CA75CD27, 0xCD9705dF4a3fDCCFc8aeBd082D0aF48c67f3e39f, 1006892524208368322730137854408702998587733166 [1.006e45]]
  • test_updateComposite_revertOrder(uint8): custom error 0x6fdd1491; counterexample: calldata=0x46b240f90000000000000000000000000000000000000000000000000000000000000016 args=[22]
  • test_updateComposite_revert_builtinChild(uint8): custom error 0x6fdd1491; counterexample: calldata=0x44e9729e00000000000000000000000000000000000000000000000000000000000000b3 args=[179]
  • test_updateComposite_revert_childPoliciesOutsideOfRange(uint8,uint8): custom error 0x6fdd1491; counterexample: calldata=0x25c4476000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000092 args=[33, 146]
  • test_updateComposite_revert_incompatiblePolicyType(uint8): Error != expected error: custom error 0xbfe142c0 != IncompatiblePolicyType(); counterexample: calldata=0xbe9d9a400000000000000000000000000000000000000000000000000000000000000003 args=[3]
  • test_updateComposite_revert_invalidChildPolicy(uint8): custom error 0x6fdd1491; counterexample: calldata=0xfc6d02c6000000000000000000000000000000000000000000000000000000000000003c args=[60]
  • test_updateComposite_revert_policyNotFound(address,uint64): Error != expected error: custom error 0xbfe142c0 != PolicyNotFound(); counterexample: calldata=0xbc492f7a0000000000000000000000000b678606af7b4ceda436f7ff2f17c309164b12c40000000000000000000000000000000000000000000000000000000000000002 args=[0x0b678606Af7b4CEdA436F7FF2F17C309164B12C4, 2]
  • test_updateComposite_revert_policyNotFoundChild(uint8,uint64): custom error 0x6fdd1491; counterexample: calldata=0x5d96e02c0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000298201d1a5 args=[6, 178274816421 [1.782e11]]
  • test_updateComposite_revert_renouncedComposite(uint8): custom error 0x6fdd1491; counterexample: calldata=0x9c795a420000000000000000000000000000000000000000000000000000000000000082 args=[130]
  • test_updateComposite_revert_unauthorized(address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x13f7deab000000000000000000000000d967646918a6a3cc8784d6d93c00767f4d954fc30000000000000000000000000000000000000000000000000000000000000029 args=[0xD967646918A6A3CC8784d6D93C00767F4d954fc3, 41]
  • test_updateComposite_success_atMaxChildren(uint8): custom error 0x6fdd1491; counterexample: calldata=0x9b8e060500000000000000000000000000000000000000000000000000000000000000a8 args=[168]
  • test_updateComposite_success_emitsCompositePolicyUpdated(uint8): custom error 0x6fdd1491; counterexample: calldata=0xc975d54c000000000000000000000000000000000000000000000000000000000000002a args=[42]
  • test_updateComposite_success_replacesChildSet(address): custom error 0x6fdd1491; counterexample: calldata=0xbb1499330000000000000000000000000000000000000000000000000000000000001318 args=[0x0000000000000000000000000000000000001318]
  • test_updateMultiplier_success_emitsEvent(uint256): log != expected log; counterexample: calldata=0xfda1a8f90000cb0f3e66c0ac40f8a1fc3883d644292221d1235549a367c632c091b2ef6c args=[1401465479230554729768438035765260637519465505474745737499488344664633196 [1.401e72]]
  • test_updateMultiplier_success_writesSlot(uint256): InvalidMultiplier(); counterexample: calldata=0xff0bf5390000005915c1f568cfdfb25f6291cc973396b1ba6164b9a3bd362d55403ae74b args=[2401726601454073865895447546000615557318125526099714112684083577022283 [2.401e69]]
  • test_updatePolicy_revert_policyNotFound(uint8,uint64): Error != expected error: UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc) != PolicyNotFound(216172782113848247 [2.161e17]); counterexample: calldata=0x74fd7f3900000000000000000000000000000000000000000000000000000000000000fd0000000000000000000000000000000000000000000000000000000000fbb70f args=[253, 16496399 [1.649e7]]
  • test_updatePolicy_success_builtinAllow(uint8): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0xde997e8b0000000000000000000000000000000000000000000000000000000000000003 args=[3]
  • test_updatePolicy_success_builtinReject(uint8): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x56f5b908000000000000000000000000000000000000000000000000000000000000001c args=[28]
  • test_updatePolicy_success_emitsPolicyUpdated(uint8,uint64): log != expected log; counterexample: calldata=0x59afa2fa000000000000000000000000000000000000000000000000000000000000003f00000000000000000000000000000000000000000000000000001446323b5b8a args=[63, 22291723017098 [2.229e13]]
  • test_updatePolicy_success_writesSlot(uint8,uint64): UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc); counterexample: calldata=0x9746e8e10000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000356f args=[8, 13679 [1.367e4]]
    [FAIL: UnsupportedPolicyType(0xa56257498532f11bbd41c0e4915afb29340c1f133679e9cf62f3efc37a0c85fc)] test_b20Layout_success_populatedSnapshotMatchesAllSlots() (gas: 569463)
    [FAIL: isPaused must still accept enum index 3 (SEIZE)] test_isPaused_revert_noFifthFeature() (gas: 9532)

…TURES_PAUSED

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Comment thread src/lib/B20Constants.sol Outdated
stephancill and others added 2 commits July 27, 2026 12:08
Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Comment thread src/interfaces/IB20.sol Outdated
…emo natspec

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Comment thread src/interfaces/IB20.sol Outdated
Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Comment thread src/interfaces/IB20.sol
MINT,
BURN
BURN,
SEIZE

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.

Why do we need to be able to pause seizing? Seems like the kind of admin action you don't want to stop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

the intention here was that you should be able to pause the other functions of the contract without pausing seizing. one option for implementing that is to not make seizing pausable at all, the other one is this one where seizing becomes its own pause vector

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.

given transferFromBlockedWithMemo ignores our other safeguards i think it could make sense that we would also ignore pause state.

I would prefer us to know that we need to be able to pause seizing action rather than implement something to avoid its use.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think that makes sense

@ilikesymmetry mind weighing in here?

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.

@stevieraykatz reference pause options and tradeoffs here

We landed on a new pause option to separate concerns. Including an option to pause seizing operations helps asset issuers reduce risk of a buggy/compromised role-holding account by having a separate system able to killswitch the operation.

Comment thread src/interfaces/IB20.sol Outdated
Comment on lines +109 to +110
/// `burnBlocked` (authorized under `TRANSFER_SENDER_POLICY`), or `transferFromBlockedWithMemo` /
/// `burnBlockedWithMemo` (authorized under `SEIZABLE_POLICY`).

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.

It feels wrong to me that these don't share a policy. I know its because we've got BC to worry about, but it still smells. Is this discussed elsewhere?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i think this is actually a mistake - we are willing to break BC for burnBlocked under the assumption that it isn't being used yet. intention is to migrate it to the same policy

@stephancill stephancill Jul 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ah i see what happened - that breaking change diff was slated for a separate PR. gonna fold it into this one

Comment thread src/lib/B20Constants.sol
bytes32 internal constant TRANSFER_RECEIVER_POLICY = keccak256("TRANSFER_RECEIVER_POLICY");
bytes32 internal constant TRANSFER_EXECUTOR_POLICY = keccak256("TRANSFER_EXECUTOR_POLICY");
bytes32 internal constant MINT_RECEIVER_POLICY = keccak256("MINT_RECEIVER_POLICY");
bytes32 internal constant SEIZABLE_POLICY = keccak256("SEIZABLE_POLICY");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@stevieraykatz thoughts on the naming here? an alternative that was floated was SEIZE_BLOCKED_POLICY

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.

Thanks for drawing my attention. The verbiage is strange but seizable is actually accurately following convention. We want to describe what the policy does by describing who the policy applies to. So since we're talking about the holders who can be seized from, SEIZABLE is fine.

I'm wondering if there's a way to incorporate the holder though? SEIZEABLE_HOLDER_POLICY doesn't feel right but I also don't like that we don't name the account.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

SEIZEABLE_HOLDER_POLICY could work. Another option could be SEIZEABLE_ACCOUNT_POLICY?

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.

Are we intending for this policy to be an allowlist or a blocklist? I think that might change how we frame the policy name.

@ilikesymmetry ilikesymmetry Jul 27, 2026

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.

Are we intending for this policy to be an allowlist or a blocklist? I think that might change how we frame the policy name.

It's most likely going to be a blocklist model, but theoretically we can't guarantee how people will use the scope

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.

I'm wondering if there's a way to incorporate the holder though? SEIZEABLE_HOLDER_POLICY doesn't feel right but I also don't like that we don't name the account.

This was the intent of "SEIZE_BLOCKED" where we describe the entity as being seized from as a "blocked" address. It's a weird word to use as a noun, but it does feel consistent with our naming conventions "transfer from a blocked account" -> "blocked" is the noun

@stephancill stephancill Jul 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's most likely going to be a blocklist model, but theoretically we can't guarantee how people will use the scope

This is my main hesitation against SEIZE_BLOCKED_POLICY. Adding an address to the seizing policy doesn't enforce any blocking logic

@stephancill stephancill Jul 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is also making me second guess the transferFromBlocked function naming. seize or transferFromSeizable is perhaps more accurate

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.

+1 to transferFromSeizable and then naming the policy according to whether the account can be seized from, i.e. SEIZABLE_ACCOUNT_POLICY

Folds the legacy burnBlocked into the seize operation class: its blocked
check now uses SEIZABLE_POLICY (was TRANSFER_SENDER_POLICY) and it gates on
the SEIZE pause vector (was BURN), matching burnBlockedWithMemo and
transferFromBlockedWithMemo. Breaking change, acceptable because burnBlocked
is not yet in use. Simplifies the AccountNotBlocked error to a single
seizable-policy statement.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Comment thread src/interfaces/IB20.sol
/// (mirroring the `burnBlocked` "blocked" semantics). An unset slot reads as `0` (always-allow),
/// which makes no account seizable until an issuer configures the slot.
/// @return Policy scope constant.
function SEIZABLE_POLICY() external view returns (bytes32);

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.

I don't remember aligning on this as the name and recall we had SEIZE_BLOCKED_POLICY. What's the thought process of this new name?

@stephancill stephancill Jul 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

discussed some options with @rayyan224 async and felt SEIZABLE_POLICY was maybe more descriptive so proceeded with it as a placeholder with the intention of discussing here

@rayyan224 rayyan224 Jul 27, 2026

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.

I think we can do as discussed in the P/PS ? Seems' like it's related to this duscusion here
#177 (comment)

Comment thread src/interfaces/IB20.sol Outdated
Comment on lines +456 to +457
/// Emits `Transfer(from, to, amount)`, `TransferredFromBlocked(caller, from, to, amount)`, and
/// `Memo(caller, memo)`. A memo of `bytes32(0)` is permitted.

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.

for clarity, should enumerate events in order of emission, so Memo would immediately follow Transfer

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

AI: Fixed in f0660e2 — natspec now lists emission order Transfer, Memo, TransferredFromBlocked, and the impl emits Memo immediately after Transfer (before TransferredFromBlocked) to satisfy the IB20 Memo invariant.

Comment thread src/interfaces/IB20.sol Outdated
/// @param amount Amount to burn.
function burnBlocked(address from, uint256 amount) external;

/// @notice Same as `burnBlocked`, plus emits `Memo` after the standard `Transfer` and `BurnedBlocked` events.

@ilikesymmetry ilikesymmetry Jul 27, 2026

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.

for clarity, we emit Memo immediately after Transfer and before BurnBlocked

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

AI: Fixed in f0660e2burnBlockedWithMemo now emits Transfer -> Memo -> BurnedBlocked, and the natspec reflects that order.

Comment thread test/lib/mocks/MockB20.sol Outdated
Comment on lines +331 to +332
emit BurnedBlocked(msg.sender, from, amount);
emit Memo(msg.sender, memo);

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.

Memo always needs to follow Transfer per spec. @rayyan224 can we get some automated agent workflow going that consistently looks for accidental divergence from previously defined invariants? This one should be caught before it hits human review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

AI: Fixed in f0660e2 — reordered to Transfer -> Memo -> BurnedBlocked so Memo immediately follows Transfer per the spec invariant.

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.

Yes, agreed @stevieraykatz we had some encoded smart contract skills, that you were working on right ?

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.

Our solidity harness is the shared repo for agent rules. If this skill/rule isn't defined there, it should be added!

https://coinbase.ghe.com/smart-contracts/harness

Comment thread test/lib/mocks/MockB20.sol Outdated
Comment thread test/lib/mocks/MockB20.sol Outdated
Comment on lines +510 to 512
if (policyScope == SEIZABLE_POLICY) return $.transferPolicyIds.seizable;
if (policyScope == MINT_RECEIVER_POLICY) return $.mintPolicyIds.receiver;
revert UnsupportedPolicyType(policyScope);

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.

I'm concerned that our UnsupportedPolicyType revert is overly restrictive. Thinking through the rollout of these new changes, compliant assets live in production will be temporarily stripped of their ability to block users from holding their assets. To re-enable blocking, they'll need to manually queue up a transaction to set the seize policy to whatever blocklist they were previously managing to keep going. Setting ourselves up for a race condition of sequencing Cobalt activation -> update seize policy -> continue blocking+seizing feels like an unnecessary friction.

Proposal: Consider removing restriction of defined policy types and treat policies more like roles (open-ended with B20-aware values). If we had this, we could have enabled teams live in production to queue up their seize policy ahead of the Cobalt upgrade and have zero race conditions or risk of service disturbance. We'll still hit this for Cobalt as we can't change this logic until the upgrade goes live, but I think this sets us up better for changes like this in the future.

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.

Additionally, we should start to temp check this rollout plan with active customers inside Coinbase on stock and wrapped asset products

@ilikesymmetry ilikesymmetry 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.

Seeking alignment on policy name, storage location, and revisiting behavior of only supporting specific policy scopes.

…k on seize

- Reorder seize emissions so Memo immediately follows Transfer (per the IB20
  Memo invariant): Transfer -> Memo -> BurnedBlocked/TransferredFromBlocked.
- transferFromBlockedWithMemo: drop the from==address(0) check (keep to!=0),
  matching the burn-blocked family; a zero/empty from fails the seizable or
  balance check anyway. Removes the now-obsolete InvalidSender test.

Addresses review from @ilikesymmetry.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
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.

4 participants