Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 53 additions & 3 deletions docs/PolicyRegistry/README.md
Original file line number Diff line number Diff line change
@@ -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.

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.

I wasn't aware we were excluding the sentinels (though that makes sense). I think the interface doc is missing this nuance. From createCompositePolicy in IPolicyRegistry L124

    /// @dev Child policies must be simple policies (ALLOWLIST or BLOCKLIST), never another composite.
    ///      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 Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value.
    ///

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.

Good catch — the top-level @dev line only called out "never another composite" and left the sentinel exclusion to be inferred from the later revert line. Fixed the NatSpec on both createCompositePolicy and updateComposite to spell out the built-in-sentinel exclusion explicitly, matching the revert conditions and the README.


## Policy IDs

Expand All @@ -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.

Expand All @@ -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<br>allocate new policyId<br>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.
Expand All @@ -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<br>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.
Expand Down
12 changes: 7 additions & 5 deletions src/interfaces/IPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading