Skip to content

feat: composite policies (UNION/INTERSECT) — interface, mock, and tests#174

Merged
rayyan224 merged 24 commits into
mainfrom
composite-policy-interface-proposal
Jul 24, 2026
Merged

feat: composite policies (UNION/INTERSECT) — interface, mock, and tests#174
rayyan224 merged 24 commits into
mainfrom
composite-policy-interface-proposal

Conversation

@rayyan224

@rayyan224 rayyan224 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds composite-policy support to the policy registry: the IPolicyRegistry interface surface, the MockPolicyRegistry reference implementation, and the full unit-test suite. (Includes the mock implementation merged via #175.)

A composite policy combines existing simple policies (ALLOWLIST/BLOCKLIST) under a logic gate. The gate rides the policy ID's top byte, so composites need no separate type storage.

Interface (IPolicyRegistry)

  • PolicyType gains UNION (2) and INTERSECT (3). Append-only: leaf discriminants (BLOCKLIST=0, ALLOWLIST=1) are unchanged.
  • createCompositePolicy(address admin, PolicyType policyType, uint64[] childPolicyIds) — creates a UNION/INTERSECT over existing simple child policies. Children must exist and be simple (never a composite or a built-in sentinel); the child count must be in [2, 4].
  • updateComposite(uint64 policyId, uint64[] childPolicyIds) — full replace of the child set, re-validated exactly as at creation. The gate is fixed in the ID and cannot change.
  • isAuthorized — composites evaluate live over current child membership: UNION is OR (any child authorizes; empty → false), INTERSECT is AND (every child authorizes; empty → true).
  • Event: CompositePolicyUpdated(policyId, updater, childPolicyIds) — emitted on creation and every update, carrying the full post-update child set for single-log indexing. (Creation also emits the canonical PolicyCreated + PolicyAdminUpdated.)
  • Errors: ChildPoliciesOutsideOfRange(min, max) when the child count is outside [2, 4]; InvalidChildPolicy(childPolicyId) when a child is a built-in sentinel or a composite. Reuses PolicyNotFound / IncompatiblePolicyType / Unauthorized / ZeroAddress.

Mock implementation

  • MockPolicyRegistryStorage: adds a children mapping (uint64 => uint64[]) at layout offset 4 (offsets 0–3 unchanged) plus a childrenSlot helper for Rust slot-math parity.
  • MockPolicyRegistry: implements createCompositePolicy / updateComposite with the canonical revert precedence, and splits composite evaluation into _isAuthorizedUnion / _isAuthorizedIntersect. Child validation (_requireCreatedSimplePolicies) is two-pass so PolicyNotFound precedes InvalidChildPolicy.

Tests

test/unit/PolicyRegistry/: composite create/update (reverts + success), sequential revert-order walks, isAuthorized UNION/INTERSECT/mixed-child/live-membership semantics, and built-in-sentinel-child rejection.

Testing

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

Generated with Claude Code

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>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Interface Coverage

✅ All interface functions have test coverage.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

⚠️ Fork tests: 66 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_childrenSlot_success_locatesChildArray(uint8): custom error 0x6fdd1491; counterexample: calldata=0x69fce77600000000000000000000000000000000000000000000000000000000000000fc args=[252]
  • test_createCompositePolicy_revertOrder(uint8): custom error 0x6fdd1491; counterexample: calldata=0x5fafe4f70000000000000000000000000000000000000000000000000000000000000005 args=[5]
  • test_createCompositePolicy_revert_builtinChild(address,address,uint8): Error != expected error: custom error 0x6fdd1491 != InvalidChildPolicy(0); counterexample: calldata=0x87108a4b0000000000000000000000000000000000000000000000000000000000002a79000000000000000000000000cb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b args=[0x0000000000000000000000000000000000002A79, 0xCB00000000000000000000000000000000000000, 11]
  • test_createCompositePolicy_revert_childPoliciesOutsideOfRange(address,address,uint8,uint8): Error != expected error: custom error 0x6fdd1491 != ChildPoliciesOutsideOfRange(2, 4); counterexample: calldata=0xb84664360000000000000000000000008e680535dcf931f77699bd3b6153673c2ac1c6c200000000000000000000000055fcdaf4af29a37f81449755c352a9da32806a6400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009 args=[0x8E680535dCf931F77699BD3b6153673c2Ac1C6c2, 0x55fCdAF4Af29A37f81449755c352a9dA32806a64, 0, 9]
  • test_createCompositePolicy_revert_incompatiblePolicyType(address,address,uint8): Error != expected error: custom error 0x6fdd1491 != IncompatiblePolicyType(); counterexample: calldata=0x97326925000000000000000000000000000000000000000000000000000000000000064e000000000000000000000000cb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2 args=[0x000000000000000000000000000000000000064e, 0xCB00000000000000000000000000000000000000, 178]
  • test_createCompositePolicy_revert_invalidChildPolicy(address,address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x7416f45900000000000000000000000089e3498e886338c48ad5cbd349154366e287dab20000000000000000000000003442c83de28e0ad8edc573189fcfcd89ec8f00680000000000000000000000000000000000000000000000000000000000000029 args=[0x89e3498e886338C48ad5cBd349154366e287DAb2, 0x3442c83dE28E0ad8eDC573189FCfCD89ec8f0068, 41]
  • test_createCompositePolicy_revert_policyNotFound(address,address,uint8,uint64): Error != expected error: custom error 0x6fdd1491 != PolicyNotFound(); counterexample: calldata=0x1803f6050000000000000000000000008d3bba4001b2945da7f411851983a0409f6592fb0000000000000000000000001673c0d17190c95ef0e26ea23719d072ffb2188600000000000000000000000000000000000000000000000000000000000000fc0000000000000000000000000000000000000000000000000c47744128ec688d args=[0x8d3BBa4001B2945da7F411851983A0409F6592Fb, 0x1673C0d17190C95Ef0e26eA23719D072Ffb21886, 252, 884803675009869965 [8.848e17]]
  • test_createCompositePolicy_revert_zeroAdmin(address,uint8): Error != expected error: custom error 0x6fdd1491 != ZeroAddress(); counterexample: calldata=0xb18b5cb1000000000000000000000000517bfc853b94ea7e74a8b11f567348b4d953e4ee0000000000000000000000000000000000000000000000000000000000000053 args=[0x517bfC853b94EA7e74a8B11f567348B4d953E4ee, 83]
  • test_createCompositePolicy_success_advancesNextPolicyId(address,uint8,uint8): custom error 0x6fdd1491; counterexample: calldata=0xf9a1ca7700000000000000000000000000000000000000000000000000000000000000f200000000000000000000000000000000000000000000000000000000000000670000000000000000000000000000000000000000000000000000000000000010 args=[0x00000000000000000000000000000000000000F2, 103, 16]
  • test_createCompositePolicy_success_atMaxChildren(address,address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x296e3341000000000000000000000000ae50efe37da2c2f8e2d92323e58a38aa4b33604e0000000000000000000000000980c79bd18fedf7e9ef0e503d1806f72ec972c70000000000000000000000000000000000000000000000000000000000000000 args=[0xae50eFE37Da2C2F8e2D92323E58a38AA4b33604e, 0x0980c79BD18FEdf7e9ef0e503D1806F72ec972C7, 0]
  • test_createCompositePolicy_success_emitsCompositePolicyUpdated(address,address,uint8): log != expected log; counterexample: calldata=0x1694cfe0000000000000000000000000b69e1fa1b3cb46f475a82ac1e266309ce2ccea4f000000000000000000000000c9da5453b191acdf54e6e5f7e4f52962085e2f9a0000000000000000000000000000000000000000000000000000000000000003 args=[0xB69e1FA1b3cb46F475A82Ac1e266309CE2CCea4f, 0xC9dA5453B191Acdf54E6e5F7E4F52962085e2F9A, 3]
  • test_createCompositePolicy_success_emitsInitialPolicyAdminUpdated(address,address,uint8): log != expected log; counterexample: calldata=0xd6a6c2b2000000000000000000000000000000000000000000000000000000000000dec700000000000000000000000000000000000000000000000000000000000042250000000000000000000000000000000000000000000000000000000000000004 args=[0x000000000000000000000000000000000000deC7, 0x0000000000000000000000000000000000004225, 4]
  • test_createCompositePolicy_success_emitsPolicyCreated(address,address,uint8): log != expected log; counterexample: calldata=0x8903116500000000000000000000000000000000000000000000000000000000000007e200000000000000000000000000000000000000000000000000000000000043fc000000000000000000000000000000000000000000000000000000000000005f args=[0x00000000000000000000000000000000000007E2, 0x00000000000000000000000000000000000043Fc, 95]
  • test_createCompositePolicy_success_intersect(address,address): custom error 0x6fdd1491; counterexample: calldata=0xccbe0bd400000000000000000000000000000000000000000000000000000000000044690000000000000000000000008453000000000000000000000000000000000002 args=[0x0000000000000000000000000000000000004469, 0x8453000000000000000000000000000000000002]
  • test_createCompositePolicy_success_union(address,address): custom error 0x6fdd1491; counterexample: calldata=0xd748b1fe00000000000000000000000000000000000000000000000000000000000040f700000000000000000000000000000000000000000000000000000000000040f6 args=[0x00000000000000000000000000000000000040F7, 0x00000000000000000000000000000000000040F6]
  • test_isAuthorized_success_composite_mixedChildTypes(address): custom error 0x6fdd1491; counterexample: calldata=0xca56e88d00000000000000000000000062a9494189e58fa5d7c2c3c0dd976a56e96fc006 args=[0x62a9494189e58Fa5d7c2C3c0DD976a56e96fC006]
  • test_isAuthorized_success_composite_reflectsChildMembershipChange(address): custom error 0x6fdd1491; counterexample: calldata=0x9154ecaf000000000000000000000000d154c028cc9bf8a153680968595ec2b1b24d853e args=[0xd154C028cC9bF8a153680968595EC2b1b24D853E]
  • test_isAuthorized_success_intersect_allChildrenAuthorize(address): custom error 0x6fdd1491; counterexample: calldata=0x12f37cef00000000000000000000000000000000000000000000000000000000000010d4 args=[0x00000000000000000000000000000000000010D4]
  • test_isAuthorized_success_intersect_oneChildDenies(address): custom error 0x6fdd1491; counterexample: calldata=0x70fa07e6000000000000000000000000a5a4e4e727605a42e44b91bf0256cd22eb1138b4 args=[0xA5A4e4E727605A42e44b91bf0256CD22Eb1138B4]
  • test_isAuthorized_success_union_anyChildAuthorizes(address): custom error 0x6fdd1491; counterexample: calldata=0x1ab541130000000000000000000000000000000000000000000000000000000000001896 args=[0x0000000000000000000000000000000000001896]
  • test_isAuthorized_success_union_noChildAuthorizes(address): custom error 0x6fdd1491; counterexample: calldata=0x18ea2b87000000000000000000000000e396a2c5a7f126f03954a9029fd63aadf7789fd8 args=[0xe396a2C5A7F126f03954A9029Fd63AaDf7789Fd8]
  • test_updateComposite_revertOrder(uint8): custom error 0x6fdd1491; counterexample: calldata=0x46b240f900000000000000000000000000000000000000000000000000000000000000e4 args=[228]
  • test_updateComposite_revert_builtinChild(uint8): custom error 0x6fdd1491; counterexample: calldata=0x44e9729e0000000000000000000000000000000000000000000000000000000000000000 args=[0]
  • test_updateComposite_revert_childPoliciesOutsideOfRange(uint8,uint8): custom error 0x6fdd1491; counterexample: calldata=0x25c4476000000000000000000000000000000000000000000000000000000000000000fd0000000000000000000000000000000000000000000000000000000000000000 args=[253, 0]
  • test_updateComposite_revert_incompatiblePolicyType(uint8): Error != expected error: custom error 0xbfe142c0 != IncompatiblePolicyType(); counterexample: calldata=0xbe9d9a400000000000000000000000000000000000000000000000000000000000000002 args=[2]
  • test_updateComposite_revert_invalidChildPolicy(uint8): custom error 0x6fdd1491; counterexample: calldata=0xfc6d02c6000000000000000000000000000000000000000000000000000000000000003e args=[62]
  • test_updateComposite_revert_policyNotFound(address,uint64): Error != expected error: custom error 0xbfe142c0 != PolicyNotFound(); counterexample: calldata=0xbc492f7a0000000000000000000000009c7a02bdec4cd520f1ce24ab64984454d6433c1c000000000000000000000000000000000000000000000000fffffffffffffffc args=[0x9c7A02BdeC4cd520F1cE24AB64984454d6433C1c, 18446744073709551612 [1.844e19]]
  • test_updateComposite_revert_policyNotFoundChild(uint8,uint64): custom error 0x6fdd1491; counterexample: calldata=0x5d96e02c00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000075258c82 args=[4, 1965395074 [1.965e9]]
  • test_updateComposite_revert_renouncedComposite(uint8): custom error 0x6fdd1491; counterexample: calldata=0x9c795a420000000000000000000000000000000000000000000000000000000000000001 args=[1]
  • test_updateComposite_revert_unauthorized(address,uint8): custom error 0x6fdd1491; counterexample: calldata=0x13f7deab000000000000000000000000de35ade08cbdb6092540953156d3a51345cfb73d0000000000000000000000000000000000000000000000000000000000000025 args=[0xDe35AdE08cbdb6092540953156d3a51345CfB73D, 37]
  • test_updateComposite_success_atMaxChildren(uint8): custom error 0x6fdd1491; counterexample: calldata=0x9b8e06050000000000000000000000000000000000000000000000000000000000000000 args=[0]
  • test_updateComposite_success_emitsCompositePolicyUpdated(uint8): custom error 0x6fdd1491; counterexample: calldata=0xc975d54c0000000000000000000000000000000000000000000000000000000000000049 args=[73]
  • test_updateComposite_success_replacesChildSet(address): custom error 0x6fdd1491; counterexample: calldata=0xbb149933000000000000000000000000e27eac788a7e2e24a763d70aa00be46fcd3b1445 args=[0xe27EaC788A7e2E24a763D70aa00Be46FCD3b1445]

Comment thread src/interfaces/IPolicyRegistry.sol Outdated
function createPolicy(address admin, PolicyType policyType) external returns (uint64 newPolicyId);

/// @notice Creates a new policy seeded with `accounts` as initial members. Permissionless.
/// @notice Creates a new leaf policy seeded with `accounts` as initial members. Permissionless.

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.

Remove leaf here

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>
Comment thread src/interfaces/IPolicyRegistry.sol Outdated

/// @notice A composite policy (UNION or INTERSECT) was created over `operands`.
event CompositePolicyCreated(
uint64 indexed policyId, address indexed creator, PolicyType policyType, uint64[] operands

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.

feel like I want to see the clarification of "policy" in there

Suggested change
uint64 indexed policyId, address indexed creator, PolicyType policyType, uint64[] operands
uint64 indexed policyId, address indexed creator, PolicyType policyType, uint64[] operandPolicies

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.

Do we even need this event? Why not just use PolicyCreated as-is and then combine with the event below for declaring operand policies updated?

@rayyan224 rayyan224 Jul 23, 2026

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.

I think we can use policy created. Only thing is I would assume a user want's an event that lists the new childPolicyIds for the composite, when a policy is created / updated.

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.

The existing PolicyCreated event does declare the policy type so I think your goal is met as-is

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.

But it wouldn't solve the event that the user can see the new childPoloicyId's

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.

Updated to PolicyCreated, and a UpdateCompositePolicyEvent both being emitted

Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
rayyan224 and others added 8 commits July 23, 2026 10:08
Co-authored-by: katzman <steve.katzman@coinbase.com>
Co-authored-by: Conner Swenberg <conner.swenberg@coinbase.com>
Co-authored-by: katzman <steve.katzman@coinbase.com>
Co-authored-by: Conner Swenberg <conner.swenberg@coinbase.com>
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment on lines +93 to +96
/// @notice A composite policy (UNION or INTERSECT) was created over `childPolicyIds`.
event CompositePolicyCreated(
uint64 indexed policyId, address indexed creator, PolicyType policyType, uint64[] childPolicyIds
);

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.

Seems to ignore discussion here: #174 (comment).

As we keep adding policy types with more nuance, we'll continually run up against the desire to have custom created events that have all of the content of PolicyCreated, but with a new field(s). I think we should take the opinionated stance that we have one canonical creation event (PolicyCreated) and we use *Updated events for the type-specific field(s). This gives indexers the cleanest experience where this is only one path for creation to look at ever and only one path for field updates to look at (regardless of creation/update).

Suggested change
/// @notice A composite policy (UNION or INTERSECT) was created over `childPolicyIds`.
event CompositePolicyCreated(
uint64 indexed policyId, address indexed creator, PolicyType policyType, uint64[] childPolicyIds
);

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.

So the create function fires two events fires two events, Created and UpdatePolicy ?

Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
rayyan224 and others added 3 commits July 23, 2026 15:24
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>
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol
/// @dev Reverts with `PolicyNotFound` when any child policy does not exist.
/// @dev Reverts with `InvalidChildPolicy` when any child policy is itself a composite
/// (not a simple policy).
/// @dev Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value.

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.

Aren't we going to explicitly check that 2 < childPolicyIds <= 4? What risk is there of overflow?

@rayyan224 rayyan224 Jul 23, 2026

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.

They share the create counter which can overflow

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.

oooh for the global policy id. i don't think this needs explicit callout here...

Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
rayyan224 and others added 6 commits July 23, 2026 16:28
Co-authored-by: katzman <steve.katzman@coinbase.com>
Co-authored-by: katzman <steve.katzman@coinbase.com>
Co-authored-by: katzman <steve.katzman@coinbase.com>
Co-authored-by: katzman <steve.katzman@coinbase.com>
@rayyan224
rayyan224 requested a review from stevieraykatz July 23, 2026 20:37
Comment thread src/interfaces/IPolicyRegistry.sol Outdated
Comment on lines +207 to +208
/// @dev Reverts with `TooFewChildPolicies` when `childPolicyIds.length < 2`; there is no clear-the-list path.
/// @dev Reverts with `BatchSizeTooLarge(4)` when `childPolicyIds.length > 4`

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.

asymmetry of these events feels weird, feels like we should do one OutsideAllowedChildPoliciesRange(received, min, max) or similar

* 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>
@rayyan224
rayyan224 requested a review from ilikesymmetry July 24, 2026 19:54
@rayyan224
rayyan224 marked this pull request as ready for review July 24, 2026 19:55
@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 100.00% 100.00% 100.00% 100.00%
Total 96.90% 97.48% 98.53% 97.04%

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

@rayyan224 rayyan224 changed the title Propose composite policy interface (UNION/INTERSECT) feat: composite policies (UNION/INTERSECT) — interface, mock, and tests Jul 24, 2026
@rayyan224
rayyan224 requested review from robriks and stephancill July 24, 2026 20:31
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>
@rayyan224
rayyan224 merged commit 6fc07aa into main Jul 24, 2026
10 checks passed
@rayyan224
rayyan224 deleted the composite-policy-interface-proposal branch July 24, 2026 20:46
///
/// @dev Reverts with `ZeroAddress` when `admin` is `address(0)`.
/// @dev Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value.
/// @dev Reverts with `IncompatiblePolicyType` when `policyType` is a composite gate.

@robriks robriks Jul 24, 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.

createPolicy and createPolicyWithAccounts have natspec mentioning that they revert for composite policy types, but it looks like

policyRegistry.createPolicy(admin, PolicyType.UNION);      // does not revert, creating a UNION with 0 children
policyRegistry.createPolicy(admin, PolicyType.INTERSECT);  // isAuthorized(id, anyAddr) == true 
policyRegistry.createPolicyWithAccounts(admin, PolicyType.UNION, accts); // does not revert

don't revert as documented

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.

Oh yep nice catch

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.

6 participants