Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1a391f4
Propose composite policy interface (UNION/INTERSECT)
rayyan224 Jul 22, 2026
e685897
Tighten composite NatSpec wording
rayyan224 Jul 22, 2026
0c29a7e
Apply suggestion from @stevieraykatz
rayyan224 Jul 23, 2026
5c14492
Apply suggestion from @ilikesymmetry
rayyan224 Jul 23, 2026
0f599ad
Apply suggestion from @stevieraykatz
rayyan224 Jul 23, 2026
57e6caa
Apply suggestion from @ilikesymmetry
rayyan224 Jul 23, 2026
2594d62
feat: poloicy registry
rayyan224 Jul 23, 2026
6f30915
feaet: more clean up
rayyan224 Jul 23, 2026
bc683b1
chore: clean up
rayyan224 Jul 23, 2026
35e816a
feat: rename to simple
rayyan224 Jul 23, 2026
fe6710a
chore: clean up
rayyan224 Jul 23, 2026
0682ec3
chore: update error types
rayyan224 Jul 23, 2026
050a4b0
feat: update policy registry
rayyan224 Jul 23, 2026
02e8f22
refactor: remove CompositePolicyCreated event
rayyan224 Jul 23, 2026
4d27af7
chore: update dev docs
rayyan224 Jul 23, 2026
2bd24c2
Apply suggestion from @stevieraykatz
rayyan224 Jul 23, 2026
03230f2
Apply suggestion from @stevieraykatz
rayyan224 Jul 23, 2026
500e269
Apply suggestion from @stevieraykatz
rayyan224 Jul 23, 2026
14c9bb5
Apply suggestion from @stevieraykatz
rayyan224 Jul 23, 2026
aa0dc53
feat: policy registry
rayyan224 Jul 23, 2026
ffc3105
Merge branch 'composite-policy-interface-proposal' of https://github.…
rayyan224 Jul 23, 2026
fb594b2
chore: clean up
rayyan224 Jul 24, 2026
09a8792
feat: composite-policy support in MockPolicyRegistry (#175)
rayyan224 Jul 24, 2026
f6f2825
test: cover childrenSlot storage helper
rayyan224 Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 69 additions & 7 deletions src/interfaces/IPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity >=0.8.20 <0.9.0;

/// @title IPolicyRegistry
///
/// @notice Singleton registry of address-membership policies. Policies are referenced by
/// @notice Singleton registry of simple and composite policies. Policies are referenced by
/// `uint64 policyId` and queried via `isAuthorized(policyId, account)`.
interface IPolicyRegistry {
/*//////////////////////////////////////////////////////////////
Expand All @@ -12,11 +12,19 @@ interface IPolicyRegistry {

/// @notice Policy type discriminator.
///
/// @param BLOCKLIST Authorized unless in the policy's set.
/// @param ALLOWLIST Authorized only if in the policy's set.
/// @dev Two kinds of policy:
/// - Simple (BLOCKLIST, ALLOWLIST): decide from an address set.
/// - Composite (UNION, INTERSECT): decide by combining child simple policies.
///
/// @param BLOCKLIST Account is authorized unless it is in the set.
/// @param ALLOWLIST Account is authorized only if it is in the set.
/// @param UNION (OR). Account is authorized if any child policy authorizes it.
/// @param INTERSECT (AND). Account is authorized only if every child policy authorizes it.
enum PolicyType {
BLOCKLIST,
ALLOWLIST
ALLOWLIST,
UNION,
INTERSECT
}

/*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -48,6 +56,18 @@ interface IPolicyRegistry {
/// @notice `finalizeUpdateAdmin` was called with no pending admin staged.
error NoPendingAdmin();

/// @notice A composite policy was created or updated with a child-policy count outside the
/// permitted range. A composite must reference between `min` and `max` simple policies,
/// inclusive.
/// @param min Minimum number of child policies permitted.
/// @param max Maximum number of child policies permitted.
error ChildPoliciesOutsideOfRange(uint256 min, uint256 max);

/// @notice Composite policies are not simple policies. Child policies must be existing
/// ALLOWLIST or BLOCKLIST policies;
/// @param childPolicyId The offending child policy ID.
error InvalidChildPolicy(uint64 childPolicyId);

/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
Expand All @@ -68,24 +88,29 @@ interface IPolicyRegistry {
/// @notice One or more accounts had their BLOCKLIST membership set to `blocked` in a single batch.
event BlocklistUpdated(uint64 indexed policyId, address indexed updater, bool blocked, address[] accounts);

/// @notice A composite policy's child set was set or replaced in full with `childPolicyIds`. Emitted
/// on composite creation and on every subsequent update; carries the complete post-update set.
event CompositePolicyUpdated(uint64 indexed policyId, address indexed updater, uint64[] childPolicyIds);

/*//////////////////////////////////////////////////////////////
POLICY CREATION
//////////////////////////////////////////////////////////////*/

/// @notice Creates a new policy with no initial members. Permissionless.
/// @notice Creates a new simple policy with no initial members. Permissionless.
///
/// @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

///
/// @param admin Initial admin authorized to modify membership and transfer or renounce administration.
/// @param policyType BLOCKLIST or ALLOWLIST.
///
/// @return newPolicyId The newly assigned policy ID.
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 simple policy seeded with `accounts` as initial members. Permissionless.
///
/// @dev Reverts with `ZeroAddress` when `admin` is `address(0)`. Takes precedence over `BatchSizeTooLarge`.
/// @dev Reverts with `IncompatiblePolicyType` when `policyType` is a composite policyType.
/// @dev Reverts with `BatchSizeTooLarge` when `accounts.length` exceeds the registry limit.
/// @dev Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value.
///
Expand All @@ -98,6 +123,28 @@ interface IPolicyRegistry {
external
returns (uint64 newPolicyId);

/// @notice Creates a new composite policy that combines existing simple policies under a logic
/// gate.
///
/// @dev Child policies must be simple policies (ALLOWLIST or BLOCKLIST), never another composite.
Comment thread
rayyan224 marked this conversation as resolved.
/// The child-policy set is capped at 4.
/// @dev Reverts with `IncompatiblePolicyType` when `policyType` is not UNION or INTERSECT.
/// @dev Reverts with `ZeroAddress` when `admin` is `address(0)`.
/// @dev Reverts with `ChildPoliciesOutsideOfRange(2, 4)` when `childPolicyIds.length` is not in `[2, 4]`.
/// @dev Reverts with `PolicyNotFound` when any child policy does not exist.
/// @dev Reverts with `InvalidChildPolicy` when any child policy is not a simple policy or a built-in 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...

///
/// @param admin Initial admin authorized to update child policies and transfer or renounce
/// administration.
/// @param policyType UNION or INTERSECT.
/// @param childPolicyIds Existing simple policy IDs to combine.
///
/// @return newPolicyId The newly assigned composite policy ID.
function createCompositePolicy(address admin, PolicyType policyType, uint64[] calldata childPolicyIds)
external
returns (uint64 newPolicyId);

/*//////////////////////////////////////////////////////////////
POLICY ADMINISTRATION
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -154,6 +201,21 @@ interface IPolicyRegistry {
/// @param accounts Accounts to update.
function updateBlocklist(uint64 policyId, bool blocked, address[] calldata accounts) external;

/// @notice Replaces a composite policy's child-policy set in full with `childPolicyIds`.
/// @dev Reverts with `PolicyNotFound` when `policyId` does not exist.
/// @dev Reverts with `IncompatiblePolicyType` when `policyId` is not a composite (UNION or INTERSECT).
/// @dev Reverts with `Unauthorized` when the caller is not the current admin. A renounced composite
/// (admin `address(0)`) can never be updated.
/// @dev Reverts with `ChildPoliciesOutsideOfRange(2, 4)` when `childPolicyIds.length` is not in `[2, 4]`;
/// there is no clear-the-list path (the composite child-policy range, not the 64-account batch limit).
/// @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).
Comment thread
ilikesymmetry marked this conversation as resolved.
///
/// @param policyId Composite policy to update.
/// @param childPolicyIds Complete new set of existing simple policy IDs.
function updateComposite(uint64 policyId, uint64[] calldata childPolicyIds) external;

/*//////////////////////////////////////////////////////////////
AUTHORIZATION QUERIES
//////////////////////////////////////////////////////////////*/
Expand Down
68 changes: 66 additions & 2 deletions test/lib/PolicyRegistryTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,20 @@ contract PolicyRegistryTest is BaseTest {
// ============================================================
// POLICY-TYPE FUZZ HELPERS
// ============================================================
// The `PolicyType` enum has two values (BLOCKLIST = 0, ALLOWLIST = 1),
// both creatable. The helper picks one from a fuzz seed.
// The `PolicyType` enum has two SIMPLE values (BLOCKLIST = 0,
// ALLOWLIST = 1) and two COMPOSITE gates (UNION = 2, INTERSECT = 3).
// The helpers below pick one from a fuzz seed.

/// @notice Maps a fuzz seed to ALLOWLIST or BLOCKLIST.
function _creatablePolicyType(uint8 idx) internal pure returns (IPolicyRegistry.PolicyType) {
return idx % 2 == 0 ? IPolicyRegistry.PolicyType.ALLOWLIST : IPolicyRegistry.PolicyType.BLOCKLIST;
}

/// @notice Maps a fuzz seed to a composite gate: UNION or INTERSECT.
function _creatableCompositeType(uint8 idx) internal pure returns (IPolicyRegistry.PolicyType) {
return idx % 2 == 0 ? IPolicyRegistry.PolicyType.UNION : IPolicyRegistry.PolicyType.INTERSECT;
}

/// @notice Predict the ID the next `createPolicy(_, policyType)` would assign.
/// @dev Reads `nextCounter` directly via `vm.load`. When the registry
/// has not yet been initialized (counter == 0), the next
Expand Down Expand Up @@ -107,4 +113,62 @@ contract PolicyRegistryTest is BaseTest {
accounts[i] = address(uint160(0x1000 + i));
}
}

// ============================================================
// COMPOSITE-POLICY HELPERS
// ============================================================

/// @notice Minimum number of child policies a composite must reference.
/// @dev Mirrors the registry's composite child-policy floor. Kept as a
/// test-side literal so fork tests against the real precompile use
/// the same compile-time constant.
uint256 internal constant MIN_CHILD_POLICIES = 2;

/// @notice Maximum number of child policies a composite may reference.
/// @dev Mirrors the registry's composite child-policy cap. Distinct
/// from `MAX_BATCH_SIZE` (64), which caps account membership
/// batches; a composite created or updated with a child count
/// outside `[MIN_CHILD_POLICIES, MAX_CHILD_POLICIES]` reverts with
/// `ChildPoliciesOutsideOfRange(MIN_CHILD_POLICIES, MAX_CHILD_POLICIES)`.
/// Kept as a test-side literal so fork tests against the real
/// precompile use the same compile-time constant.
uint256 internal constant MAX_CHILD_POLICIES = 4;

/// @notice Create `n` simple ALLOWLIST policies (admin = `admin`) and
/// return their IDs. Used to seed valid child sets for
/// composite creation / update tests.
function _makeSimpleChildren(uint256 n) internal returns (uint64[] memory childPolicyIds) {
childPolicyIds = new uint64[](n);
for (uint256 i = 0; i < n; ++i) {
childPolicyIds[i] = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST);
}
}

/// @notice Create a composite policy with explicit caller, admin, gate
/// type, and child set.
function _createComposite(
address caller,
address policyAdmin,
IPolicyRegistry.PolicyType policyType,
uint64[] memory childPolicyIds
) internal returns (uint64 policyId) {
vm.prank(caller);
policyId = policyRegistry.createCompositePolicy(policyAdmin, policyType, childPolicyIds);
}

/// @notice Create a composite policy as the default admin over a fresh
/// set of `n` simple ALLOWLIST children. Returns the composite
/// ID; use `_makeSimpleChildren` directly when the child IDs
/// are needed at the call site.
function _createComposite(IPolicyRegistry.PolicyType policyType, uint256 n) internal returns (uint64 policyId) {
uint64[] memory childPolicyIds = _makeSimpleChildren(n);
policyId = policyRegistry.createCompositePolicy(admin, policyType, childPolicyIds);
}

/// @notice Pack two policy IDs into a length-2 `uint64[]`.
function _childIds(uint64 a, uint64 b) internal pure returns (uint64[] memory ids) {
ids = new uint64[](2);
ids[0] = a;
ids[1] = b;
}
}
Loading
Loading