Skip to content

feat: composite-policy support in MockPolicyRegistry#175

Merged
rayyan224 merged 5 commits into
composite-policy-interface-proposalfrom
composite-policy-tests
Jul 24, 2026
Merged

feat: composite-policy support in MockPolicyRegistry#175
rayyan224 merged 5 commits into
composite-policy-interface-proposalfrom
composite-policy-tests

Conversation

@rayyan224

Copy link
Copy Markdown
Collaborator

Summary

Implements composite-policy support in the MockPolicyRegistry reference mock, filling the gap where IPolicyRegistry already declared composites (createCompositePolicy, updateComposite, CompositePolicyUpdated, TooFewChildPolicies, InvalidChildPolicy) but the mock only handled simple BLOCKLIST/ALLOWLIST policies.

Storage (MockPolicyRegistryStorage.sol)

  • Append a children mapping (uint64 => uint64[]) to Layout at offset 4 (offsets 0–3 unchanged).
  • Add CHILDREN_OFFSET, childrenBaseSlot(), and a documented childrenSlot(policyId) helper for slot-math parity with the Rust precompile.

Implementation (MockPolicyRegistry.sol)

  • MAX_CHILD_POLICIES = 4.
  • createCompositePolicy — canonical precedence: ZeroAddressIncompatiblePolicyTypeTooFewChildPoliciesBatchSizeTooLarge(4) → child validation → create + store children + CompositePolicyUpdated.
  • updateComposite — precedence: PolicyNotFoundIncompatiblePolicyTypeUnauthorized → count/size → child validation → full child-set replacement + CompositePolicyUpdated.
  • isAuthorized extended with live composite evaluation, split into _isAuthorizedUnion (OR, empty → false) and _isAuthorizedIntersect (AND, empty → true).
  • _requireCreatedSimplePolicies — two-pass child validation: existence first (PolicyNotFound), then rejects built-in sentinels and composites (InvalidChildPolicy). Built-in sentinels (ALWAYS_ALLOW_ID / ALWAYS_BLOCK_ID) may not be composed.

Tests

  • Full composite suite is in place (create/update reverts + success, revert-order walks, isAuthorized UNION/INTERSECT/mixed/live-membership cases).
  • Added test_createCompositePolicy_revert_builtinChild and test_updateComposite_revert_builtinChild covering the built-in-sentinel rejection.

Testing

forge test660 passed, 0 failed, 4 skipped (the 4 skipped are the mock-only counter-overflow tests guarded by vm.skip(livePrecompiles)). forge fmt --check clean.

rayyan224 and others added 3 commits July 24, 2026 12:27
Add tests asserting createCompositePolicy and updateComposite revert
InvalidChildPolicy when a child is a built-in sentinel (ALWAYS_ALLOW_ID
/ ALWAYS_BLOCK_ID), and align the IPolicyRegistry natspec accordingly.

Generated with Claude Code

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

Copy link
Copy Markdown

Interface Coverage

❌ Interface functions with no test coverage found.


  test/lib/mocks/MockPolicyRegistryStorage.sol: MockPolicyRegistryStorage.childrenBaseSlot
  test/lib/mocks/MockPolicyRegistryStorage.sol: MockPolicyRegistryStorage.childrenSlot\n```

@github-actions

github-actions Bot commented Jul 24, 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 90.62% 88.89% 100.00% 88.89%
Total 96.51% 97.04% 98.53% 96.06%

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

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

⚠️ Fork tests: 64 failed, 616 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_createCompositePolicy_revertOrder(uint8): custom error 0x6fdd1491; counterexample: calldata=0x5fafe4f70000000000000000000000000000000000000000000000000000000000000000 args=[0]
  • test_createCompositePolicy_revert_builtinChild(address,address,uint8): Error != expected error: custom error 0x6fdd1491 != InvalidChildPolicy(0); counterexample: calldata=0x87108a4b0000000000000000000000008f529896394bf3ad9d0a5af800290f2799a9c4fe000000000000000000000000013400f7fbd1eaf0b2172625e82c388e57cc18430000000000000000000000000000000000000000000000000000000000000000 args=[0x8F529896394bF3AD9D0A5Af800290F2799a9c4Fe, 0x013400F7fbD1eAf0B2172625E82C388e57Cc1843, 0]
  • test_createCompositePolicy_revert_childPoliciesOutsideOfRange(address,address,uint8,uint8): Error != expected error: custom error 0x6fdd1491 != ChildPoliciesOutsideOfRange(2, 4); counterexample: calldata=0xb8466436000000000000000000000000cb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070ca10bc00000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000012 args=[0xCB00000000000000000000000000000000000000, 0x0000000000000000000000000000000070ca10BC, 33, 18]
  • test_createCompositePolicy_revert_incompatiblePolicyType(address,address,uint8): Error != expected error: custom error 0x6fdd1491 != IncompatiblePolicyType(); counterexample: calldata=0x97326925000000000000000000000000ba53c2a94672984f6fde51cb8c6f0f9295a7b632000000000000000000000000c361863a8d619d757395ebb4dbb2b949fb1b0eba000000000000000000000000000000000000000000000000000000000000009e args=[0xBa53C2a94672984f6fde51cb8c6f0f9295A7B632, 0xc361863A8D619d757395eBB4DBb2b949fB1B0eba, 158]
  • test_createCompositePolicy_revert_invalidChildPolicy(address,address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x7416f459000000000000000000000000f3c67f93215b020936304a31bfdcabe69e335c560000000000000000000000005d2e4d3db17867452fc56cc578c27efe4dd9517e000000000000000000000000000000000000000000000000000000000000001e args=[0xF3C67f93215b020936304A31BfDCaBE69e335C56, 0x5D2e4D3Db17867452fC56cC578C27EfE4dD9517E, 30]
  • test_createCompositePolicy_revert_policyNotFound(address,address,uint8,uint64): Error != expected error: custom error 0x6fdd1491 != PolicyNotFound(); counterexample: calldata=0x1803f605000000000000000000000000101600fa201b27aee2253f0c09645f159f73571800000000000000000000000068fa9f6aada6dc7a74c85ba33d439d25e875ddc60000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000137e616058479 args=[0x101600Fa201b27aEe2253f0C09645F159F735718, 0x68fa9f6AAda6Dc7a74C85BA33d439d25e875DDc6, 4, 342936328176761 [3.429e14]]
  • test_createCompositePolicy_revert_zeroAdmin(address,uint8): Error != expected error: custom error 0x6fdd1491 != ZeroAddress(); counterexample: calldata=0xb18b5cb1000000000000000000000000e34d600616ebd720f887e3dfe46feb5daeab8ff00000000000000000000000000000000000000000000000000000000000000006 args=[0xe34D600616EBD720f887E3DFE46fEb5dAeab8Ff0, 6]
  • test_createCompositePolicy_success_advancesNextPolicyId(address,uint8,uint8): custom error 0x6fdd1491; counterexample: calldata=0xf9a1ca77000000000000000000000000e043aa0bad42aff1b8167cb197340bb4884d826700000000000000000000000000000000000000000000000000000000000000fc0000000000000000000000000000000000000000000000000000000000000001 args=[0xE043aa0Bad42aFf1B8167cb197340BB4884D8267, 252, 1]
  • test_createCompositePolicy_success_atMaxChildren(address,address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x296e334100000000000000000000000002d4a1798b224e29f372650f1d87b953af688cfc000000000000000000000000845d3d2304aac26758896b3f2a62fb46c3d00be70000000000000000000000000000000000000000000000000000000000000000 args=[0x02d4A1798b224E29F372650f1d87B953aF688cFC, 0x845D3d2304Aac26758896B3F2a62fB46C3d00bE7, 0]
  • test_createCompositePolicy_success_emitsCompositePolicyUpdated(address,address,uint8): log != expected log; counterexample: calldata=0x1694cfe00000000000000000000000000000000000000000000000000000000000000eaf00000000000000000000000000000000000000000000000000000000000041d30000000000000000000000000000000000000000000000000000000000000080 args=[0x0000000000000000000000000000000000000eAf, 0x00000000000000000000000000000000000041d3, 128]
  • test_createCompositePolicy_success_emitsInitialPolicyAdminUpdated(address,address,uint8): log != expected log; counterexample: calldata=0xd6a6c2b20000000000000000000000000b52d963fd15fa189f588e59238124e6b3a9e8e30000000000000000000000001f6a6260e98325af9507dfd83fb8d31fb601466400000000000000000000000000000000000000000000000000000000000000ff args=[0x0B52D963FD15fa189f588e59238124e6B3A9E8E3, 0x1f6a6260e98325AF9507dfd83fb8d31fB6014664, 255]
  • test_createCompositePolicy_success_emitsPolicyCreated(address,address,uint8): log != expected log; counterexample: calldata=0x89031165000000000000000000000000cb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015ef3a580000000000000000000000000000000000000000000000000000000000000057 args=[0xCB00000000000000000000000000000000000000, 0x0000000000000000000000000000000015eF3a58, 87]
  • test_createCompositePolicy_success_intersect(address,address): custom error 0x6fdd1491; counterexample: calldata=0xccbe0bd4000000000000000000000000e5b3c72580669643034b0778e50e416b4a1d4ef3000000000000000000000000cae2355578d02c5ac8393a6604ce5f75545c163e args=[0xE5B3c72580669643034B0778E50E416B4A1d4ef3, 0xcAE2355578D02c5Ac8393A6604Ce5F75545c163e]
  • test_createCompositePolicy_success_union(address,address): custom error 0x6fdd1491; counterexample: calldata=0xd748b1fe0000000000000000000000005527a54d684d9bdd0db1856e5f98def291fa49a80000000000000000000000004cc1d2857db341cff2a19b45559a36b50d1c8234 args=[0x5527a54D684d9BdD0db1856E5F98dEf291fA49a8, 0x4CC1d2857Db341cFF2A19B45559a36B50d1c8234]
  • test_isAuthorized_success_composite_mixedChildTypes(address): custom error 0x6fdd1491; counterexample: calldata=0xca56e88d000000000000000000000000eb2388224102e8062c8a8197199ccd8e8a7702f4 args=[0xEB2388224102e8062C8A8197199cCD8E8a7702F4]
  • test_isAuthorized_success_composite_reflectsChildMembershipChange(address): custom error 0x6fdd1491; counterexample: calldata=0x9154ecaf0000000000000000000000008fab9f184eec6e9c55b4ad0e1c47ae3b2a5cd11a args=[0x8faB9f184eec6e9C55b4AD0E1c47aE3B2A5CD11A]
  • test_isAuthorized_success_intersect_allChildrenAuthorize(address): custom error 0x6fdd1491; counterexample: calldata=0x12f37cef0000000000000000000000008453000000000000000000000000000000000001 args=[0x8453000000000000000000000000000000000001]
  • test_isAuthorized_success_intersect_oneChildDenies(address): custom error 0x6fdd1491; counterexample: calldata=0x70fa07e600000000000000000000000006ceb8b50458706bc62b88f46a54ec14e8df2acb args=[0x06CEB8B50458706bc62b88f46a54EC14e8Df2Acb]
  • test_isAuthorized_success_union_anyChildAuthorizes(address): custom error 0x6fdd1491; counterexample: calldata=0x1ab54113000000000000000000000000385fd4c59e9486b6478a3dd19b70e09c482e1807 args=[0x385FD4C59e9486b6478A3Dd19b70E09C482e1807]
  • test_isAuthorized_success_union_noChildAuthorizes(address): custom error 0x6fdd1491; counterexample: calldata=0x18ea2b87000000000000000000000000ad891a4bee98cfa5f06361acf4877c634c052083 args=[0xaD891A4Bee98cfA5f06361aCF4877c634c052083]
  • test_updateComposite_revertOrder(uint8): custom error 0x6fdd1491; counterexample: calldata=0x46b240f90000000000000000000000000000000000000000000000000000000000000004 args=[4]
  • test_updateComposite_revert_builtinChild(uint8): custom error 0x6fdd1491; counterexample: calldata=0x44e9729e0000000000000000000000000000000000000000000000000000000000000094 args=[148]
  • test_updateComposite_revert_childPoliciesOutsideOfRange(uint8,uint8): custom error 0x6fdd1491; counterexample: calldata=0x25c44760000000000000000000000000000000000000000000000000000000000000006200000000000000000000000000000000000000000000000000000000000000f8 args=[98, 248]
  • test_updateComposite_revert_incompatiblePolicyType(uint8): Error != expected error: custom error 0xbfe142c0 != IncompatiblePolicyType(); counterexample: calldata=0xbe9d9a400000000000000000000000000000000000000000000000000000000000000098 args=[152]
  • test_updateComposite_revert_invalidChildPolicy(uint8): custom error 0x6fdd1491; counterexample: calldata=0xfc6d02c600000000000000000000000000000000000000000000000000000000000000b9 args=[185]
  • test_updateComposite_revert_policyNotFound(address,uint64): Error != expected error: custom error 0xbfe142c0 != PolicyNotFound(); counterexample: calldata=0xbc492f7a000000000000000000000000594a4129ad5c53ea4794388fb9b418557473c56e000000000000000000000000000000000000000000000000000000000002fc71 args=[0x594A4129aD5c53eA4794388Fb9B418557473c56E, 195697 [1.956e5]]
  • test_updateComposite_revert_policyNotFoundChild(uint8,uint64): custom error 0x6fdd1491; counterexample: calldata=0x5d96e02c00000000000000000000000000000000000000000000000000000000000000cf0000000000000000000000000000000000000000000000000000000000000461 args=[207, 1121]
  • test_updateComposite_revert_renouncedComposite(uint8): custom error 0x6fdd1491; counterexample: calldata=0x9c795a420000000000000000000000000000000000000000000000000000000000000000 args=[0]
  • test_updateComposite_revert_unauthorized(address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x13f7deab00000000000000000000000036698de8127195f613a85b48314370a7d3d4f18e0000000000000000000000000000000000000000000000000000000000000013 args=[0x36698De8127195F613a85b48314370a7d3d4F18e, 19]
  • test_updateComposite_success_atMaxChildren(uint8): custom error 0x6fdd1491; counterexample: calldata=0x9b8e06050000000000000000000000000000000000000000000000000000000000000000 args=[0]
  • test_updateComposite_success_emitsCompositePolicyUpdated(uint8): custom error 0x6fdd1491; counterexample: calldata=0xc975d54c0000000000000000000000000000000000000000000000000000000000000000 args=[0]
  • test_updateComposite_success_replacesChildSet(address): custom error 0x6fdd1491; counterexample: calldata=0xbb149933000000000000000000000000cb00000000000000000000000000000000000000 args=[0xCB00000000000000000000000000000000000000]

rayyan224 and others added 2 commits July 24, 2026 13:41
Generated with Claude Code

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

Collapse the TooFewChildPolicies (< 2) and BatchSizeTooLarge(4) (> 4)
composite child-count checks into a single ChildPoliciesOutsideOfRange(min, max)
error reverting with (2, 4). Remove the now-unused TooFewChildPolicies error;
BatchSizeTooLarge is retained for account-membership batches. Add MIN_CHILD_POLICIES
and update the composite tests + revert-order walks accordingly.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
@rayyan224
rayyan224 merged commit 09a8792 into composite-policy-interface-proposal Jul 24, 2026
6 of 7 checks passed
@rayyan224
rayyan224 deleted the composite-policy-tests branch July 24, 2026 19:54
rayyan224 added a commit that referenced this pull request Jul 24, 2026
…ts (#174)

* Propose composite policy interface (UNION/INTERSECT)

Interface-only proposal for composite policies on IPolicyRegistry:

- Extend PolicyType with UNION and INTERSECT (append-only; leaf
  discriminants 0/1 unchanged, gate rides the policy ID top byte).
- Add createCompositePolicy(admin, gate, operands) and
  updateCompositeOperands(policyId, operands). Update is a full-set
  overwrite (replace-all), re-validated as at creation.
- Add CompositePolicyCreated and CompositeOperandsUpdated events, and
  EmptyOperandSet / InvalidOperand errors.
- Note on createPolicy/createPolicyWithAccounts that composite gates
  revert IncompatiblePolicyType.

Operands are flat-only (leaf ALLOWLIST/BLOCKLIST), capped at the
composite operand limit, and the set may not be empty. This changes
only the interface; MockPolicyRegistry and the Rust precompile
implementation follow in a separate PR, so the mock will not compile
against this interface until then.

Generated with Claude Code

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

* Tighten composite NatSpec wording

Drop the "not an incremental edit" binary contrast and the "silently"
adverb; make the flat-only note active. Wording only, no signature or
behavior change.

Generated with Claude Code

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

* Apply suggestion from @stevieraykatz

Co-authored-by: katzman <steve.katzman@coinbase.com>

* Apply suggestion from @ilikesymmetry

Co-authored-by: Conner Swenberg <conner.swenberg@coinbase.com>

* Apply suggestion from @stevieraykatz

Co-authored-by: katzman <steve.katzman@coinbase.com>

* Apply suggestion from @ilikesymmetry

Co-authored-by: Conner Swenberg <conner.swenberg@coinbase.com>

* feat: poloicy registry

* feaet: more clean up

* chore: clean up

* feat: rename to simple

* chore: clean up

* chore: update error types

* feat: update policy registry

* refactor: remove CompositePolicyCreated event

Use the canonical PolicyCreated event for every policy type and surface
composite child policies via CompositePolicyUpdated (now emitted on both
creation and update). Gives indexers one creation path and one per-field
update path. Addresses PR #174 review.

Generated with Claude Code

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

* chore: update dev docs

* Apply suggestion from @stevieraykatz

Co-authored-by: katzman <steve.katzman@coinbase.com>

* Apply suggestion from @stevieraykatz

Co-authored-by: katzman <steve.katzman@coinbase.com>

* Apply suggestion from @stevieraykatz

Co-authored-by: katzman <steve.katzman@coinbase.com>

* Apply suggestion from @stevieraykatz

Co-authored-by: katzman <steve.katzman@coinbase.com>

* feat: policy registry

* chore: clean up

* feat: composite-policy support in MockPolicyRegistry (#175)

* feat: create composite polocies

* feat: create mock polocy

* test: reject built-in sentinels as composite children

Add tests asserting createCompositePolicy and updateComposite revert
InvalidChildPolicy when a child is a built-in sentinel (ALWAYS_ALLOW_ID
/ ALWAYS_BLOCK_ID), and align the IPolicyRegistry natspec accordingly.

Generated with Claude Code

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

* style: forge fmt

Generated with Claude Code

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

* refactor: replace composite count errors with ChildPoliciesOutsideOfRange

Collapse the TooFewChildPolicies (< 2) and BatchSizeTooLarge(4) (> 4)
composite child-count checks into a single ChildPoliciesOutsideOfRange(min, max)
error reverting with (2, 4). Remove the now-unused TooFewChildPolicies error;
BatchSizeTooLarge is retained for account-membership batches. Add MIN_CHILD_POLICIES
and update the composite tests + revert-order walks accordingly.

Generated with Claude Code

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

---------

Co-authored-by: Claude <noreply@anthropic.com>

* test: cover childrenSlot storage helper

The Interface Coverage CI check flags any test/lib/mocks function with zero
test hits. childrenSlot / childrenBaseSlot were added for the composite child
set but never exercised. Add a slot-helper test that reads a composite's child
array via childrenSlot, matching the existing slot-helper test pattern.

Generated with Claude Code

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: katzman <steve.katzman@coinbase.com>
Co-authored-by: Conner Swenberg <conner.swenberg@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.

1 participant