From 4ff21efd94effa7ae05037fe64f9246c47ea5efc Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 7 Aug 2026 09:52:54 -0400 Subject: [PATCH 1/3] docs(BOP-509): document composite policy support in PolicyRegistry The PolicyRegistry docs still described union/intersect composition as a future hardfork feature, but createCompositePolicy/updateComposite/ compositePolicyChildIds already ship in IPolicyRegistry. Bring the docs back in line with the interface and test suite: new UNION/INTERSECT policy type section, updated activation gating lists, and Create/Update Composite user flows with their revert conditions. Co-Authored-By: Claude --- docs/PolicyRegistry/README.md | 56 +++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/docs/PolicyRegistry/README.md b/docs/PolicyRegistry/README.md index c2475e4..f1e6e52 100644 --- a/docs/PolicyRegistry/README.md +++ b/docs/PolicyRegistry/README.md @@ -1,15 +1,22 @@ # PolicyRegistry -The PolicyRegistry is a singleton precompile for list-based access policies — allowlists and blocklists. Any caller can create a policy and nominate its admin; B20 tokens and other consumers reference policies by `uint64` ID for authorization checks. See [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol) for the full Solidity interface. +The PolicyRegistry is a singleton precompile for list-based and composite access policies. Any caller can create a policy and nominate its admin; B20 tokens and other consumers reference policies by `uint64` ID for authorization checks. See [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol) for the full Solidity interface. ## Policy Types -Two policy types are supported today: +Four policy types are supported, split into two kinds: + +**Simple** policies decide from an address set: - **`BLOCKLIST`** — accounts are authorized by default; the admin maintains a list of accounts to explicitly deny. - **`ALLOWLIST`** — accounts are denied by default; the admin maintains a list of accounts to explicitly authorize. -Additional types (union / intersect composition of existing policies) are planned for a future hardfork via additive `PolicyType` enum values and sibling creator functions. +**Composite** policies decide by combining existing simple policies under a logic gate: + +- **`UNION`** (OR) — authorized if *any* child policy authorizes the account. +- **`INTERSECT`** (AND) — authorized only if *every* child policy authorizes the account. + +A composite's child set is 2–4 existing simple (`ALLOWLIST`/`BLOCKLIST`) policy IDs — never another composite, and never a built-in sentinel (`ALWAYS_ALLOW`/`ALWAYS_BLOCK`). Composites reference their children live: `isAuthorized` reads current child membership on every call, not a snapshot taken at composite-creation time, so updating a child's membership immediately changes what the composite authorizes. ## Policy IDs @@ -36,16 +43,21 @@ The `PolicyRegistry` is gated by the [`ActivationRegistry`](../ActivationRegistr - `policyExists` - `policyAdmin` - `pendingPolicyAdmin` +- `compositePolicyChildIds` +- `MIN_COMPOSITE_CHILD_POLICIES` +- `MAX_COMPOSITE_CHILD_POLICIES` **Gated** — revert with `FeatureNotActivated` while the feature is inactive: - `createPolicy` - `createPolicyWithAccounts` +- `createCompositePolicy` - `stageUpdateAdmin` - `finalizeUpdateAdmin` - `renounceAdmin` - `updateAllowlist` - `updateBlocklist` +- `updateComposite` Because reads are never gated, a consumer — a B20 token calling `isAuthorized` on transfer, or an indexer reading membership and admin state — sees the same behavior whether or not the feature is active. @@ -70,6 +82,26 @@ Use `createPolicyWithAccounts(admin, policyType, accounts)` for the seeded varia Reverts: `ZeroAddress` (if `admin` is `address(0)`), `BatchSizeTooLarge` (seeded variant only). +### Create Composite Policy + +A caller combines 2–4 existing simple policies under a `UNION` or `INTERSECT` gate and nominates an admin for the composite. + +```mermaid +sequenceDiagram + participant Creator + participant PolicyRegistry + + Creator->>PolicyRegistry: createCompositePolicy(admin, policyType, childPolicyIds) + Note over PolicyRegistry: validate children
allocate new policyId
store type, admin, children + PolicyRegistry-->>Creator: emit PolicyCreated(policyId, creator, policyType) + PolicyRegistry-->>Creator: emit PolicyAdminUpdated(policyId, 0, admin) + PolicyRegistry-->>Creator: emit CompositePolicyUpdated(policyId, creator, childPolicyIds) +``` + +Every entry in `childPolicyIds` must be an existing simple (`ALLOWLIST`/`BLOCKLIST`) policy — never another composite and never a built-in sentinel (`ALWAYS_ALLOW`/`ALWAYS_BLOCK`). The set size must fall within `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4, inclusive). + +Reverts: `ZeroAddress` (if `admin` is `address(0)`), `IncompatiblePolicyType` (`policyType` isn't `UNION`/`INTERSECT`), `ChildPoliciesOutsideOfRange` (child count outside `[2, 4]`), `PolicyNotFound` (a child doesn't exist), `InvalidChildPolicy` (a child is a composite or a built-in sentinel). + ### Update Membership The policy admin sets `accounts` to a uniform membership state — all included or all excluded — in a single batch. @@ -88,6 +120,24 @@ sequenceDiagram Reverts: `PolicyNotFound` (unknown `policyId`), `IncompatiblePolicyType` (wrong call for the policy's type), `Unauthorized` (caller isn't current admin), `BatchSizeTooLarge`. +### Update Composite Children + +The composite's admin replaces its child-policy set in full with `updateComposite`. + +```mermaid +sequenceDiagram + participant PolicyAdmin + participant PolicyRegistry + + PolicyAdmin->>PolicyRegistry: updateComposite(policyId, childPolicyIds) + Note over PolicyRegistry: validate children
replace child set in full + PolicyRegistry-->>PolicyAdmin: emit CompositePolicyUpdated(policyId, updater, childPolicyIds) +``` + +`childPolicyIds` is a full replacement, not a merge — a child omitted from the new set no longer governs the composite, even if it governed before. There is no clear-the-list path: the new set must still satisfy the same size and child-validity rules as creation. Because composites read child membership live, `isAuthorized` reflects the new set immediately. + +Reverts: `PolicyNotFound` (unknown `policyId` or a child that doesn't exist), `IncompatiblePolicyType` (`policyId` isn't `UNION`/`INTERSECT`), `Unauthorized` (caller isn't current admin — a renounced composite can never be updated), `ChildPoliciesOutsideOfRange` (child count outside `[2, 4]`), `InvalidChildPolicy` (a child is a composite or a built-in sentinel). + ### Transfer Admin A two-step transfer: the current admin proposes a successor, then the proposed admin accepts. The active admin doesn't change until the second step. From 9d2dcfa1b2ee310150171585bbe0d916761faea6 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 7 Aug 2026 10:07:07 -0400 Subject: [PATCH 2/3] docs(BOP-509): trim composite policy prose per review Co-Authored-By: Claude --- docs/PolicyRegistry/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PolicyRegistry/README.md b/docs/PolicyRegistry/README.md index f1e6e52..bc8e57a 100644 --- a/docs/PolicyRegistry/README.md +++ b/docs/PolicyRegistry/README.md @@ -16,7 +16,7 @@ Four policy types are supported, split into two kinds: - **`UNION`** (OR) — authorized if *any* child policy authorizes the account. - **`INTERSECT`** (AND) — authorized only if *every* child policy authorizes the account. -A composite's child set is 2–4 existing simple (`ALLOWLIST`/`BLOCKLIST`) policy IDs — never another composite, and never a built-in sentinel (`ALWAYS_ALLOW`/`ALWAYS_BLOCK`). Composites reference their children live: `isAuthorized` reads current child membership on every call, not a snapshot taken at composite-creation time, so updating a child's membership immediately changes what the composite authorizes. +A composite's child set is 2–4 existing simple (`ALLOWLIST`/`BLOCKLIST`) policy IDs — never another composite, and never a built-in sentinel (`ALWAYS_ALLOW`/`ALWAYS_BLOCK`). Composites reference their children live: `isAuthorized` reads current child membership on every call. So updating a child's membership immediately changes what the composite authorizes. ## Policy IDs @@ -134,7 +134,7 @@ sequenceDiagram PolicyRegistry-->>PolicyAdmin: emit CompositePolicyUpdated(policyId, updater, childPolicyIds) ``` -`childPolicyIds` is a full replacement, not a merge — a child omitted from the new set no longer governs the composite, even if it governed before. There is no clear-the-list path: the new set must still satisfy the same size and child-validity rules as creation. Because composites read child membership live, `isAuthorized` reflects the new set immediately. +`childPolicyIds` is a full replacement, a child omitted from the new set no longer governs the composite. The new set must still satisfy the same size and child-validity rules as creation. Reverts: `PolicyNotFound` (unknown `policyId` or a child that doesn't exist), `IncompatiblePolicyType` (`policyId` isn't `UNION`/`INTERSECT`), `Unauthorized` (caller isn't current admin — a renounced composite can never be updated), `ChildPoliciesOutsideOfRange` (child count outside `[2, 4]`), `InvalidChildPolicy` (a child is a composite or a built-in sentinel). From 5e3cfbb874f9285b7b597692a036597f567d6cd5 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 7 Aug 2026 13:57:26 -0400 Subject: [PATCH 3/3] docs(BOP-509): call out built-in sentinel exclusion in composite NatSpec createCompositePolicy/updateComposite already revert InvalidChildPolicy for ALWAYS_ALLOW/ALWAYS_BLOCK children, but the leading @dev lines only called out "not another composite" and left the sentinel case implicit. Spell it out in both, matching the behavior in the composite test suite and the PolicyRegistry docs. Co-Authored-By: Claude --- src/interfaces/IPolicyRegistry.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index 210560b..8969163 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -124,14 +124,16 @@ interface IPolicyRegistry { /// @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. - /// The child-policy set is capped at 4. + /// @dev Child policies must be simple policies (ALLOWLIST or BLOCKLIST), never another composite + /// and never a built-in sentinel (ALWAYS_ALLOW / ALWAYS_BLOCK). 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` when `childPolicyIds.length` is not in /// `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]`. /// @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 Reverts with `InvalidChildPolicy` when any child policy is not a simple policy or is a + /// built-in sentinel (ALWAYS_ALLOW / ALWAYS_BLOCK). /// @dev Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value. /// /// @param admin Initial admin authorized to update child policies and transfer or renounce @@ -209,8 +211,8 @@ interface IPolicyRegistry { /// `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]`; 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). + /// @dev Reverts with `InvalidChildPolicy` when any child policy is not a simple policy — i.e. + /// it is itself a composite or a built-in sentinel (ALWAYS_ALLOW / ALWAYS_BLOCK). /// /// @param policyId Composite policy to update. /// @param childPolicyIds Complete new set of existing simple policy IDs.