diff --git a/docs/PolicyRegistry/README.md b/docs/PolicyRegistry/README.md index c2475e4..bc8e57a 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. 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, 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). + ### 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. 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.