Skip to main content
At the Cobalt hardfork, the PolicyRegistry gains composite policies: a policy that authorizes an account by combining 2–4 existing simple ALLOWLIST / BLOCKLIST policies under a UNION (OR) or INTERSECT (AND) gate. Every Beryl selector, event topic, and error keeps its exact 4-byte selector and topic0. The only change to existing behavior is one new revert path on createPolicy and createPolicyWithAccounts that rejects composite policyType values with the already-existing IncompatiblePolicyType error.
Cobalt is not yet live. Until the hardfork activates, every composite selector on this page is undialable. Only the Beryl simple-policy surface exists on-chain today.

When to use a composite policy

Reach for a composite policy when a single scope on a B20 token needs to combine multiple simple policies:
  • UNION (OR). Authorize an account if any child policy authorizes it. Example: allow transfers from either a KYC allowlist or a market-maker allowlist.
  • INTERSECT (AND). Authorize an account only if every child policy authorizes it. Example: require an account to pass a jurisdiction allowlist and not appear on a sanctions blocklist.
B20 policy scopes (TRANSFER_SENDER_POLICY, TRANSFER_RECEIVER_POLICY, MINT_RECEIVER_POLICY, SEIZE_HOLDER_POLICY, and the rest) store an opaque uint64 policy ID. A composite ID drops into any scope with no B20-side change.

Policy types

PolicyType is append-only. Existing values and the packed policy-ID top-byte encoding are unchanged.

Bounds

Both constants are read-only and always callable, whether or not Cobalt is active.

Functions

createPolicy(address,uint8) (0xca5d55f6) and createPolicyWithAccounts(address,uint8,address[]) (0xa2d3044f) keep their selectors and now revert with IncompatiblePolicyType when policyType is UNION or INTERSECT.

Events

PolicyCreated(uint64,address,uint8) (topic0 0x718d87917f0c4cfd1263707ef0e77c656ed8d8bfaca06152bdb0b8094142ec27) is also emitted for composite creation, with policyType set to UNION or INTERSECT. CompositePolicyUpdated(uint64 policyId, address updater, uint64[] childPolicyIds) is new at Cobalt (topic0 0x4ff6adaab31b0df87aa7b8b7320c52b8b3b5eede3bf28a6baaaa8b8b7e1d6363). It is emitted on composite creation and on every updateComposite, and carries the complete post-update child set. AllowlistUpdated and BlocklistUpdated are not emitted for composites — composites have no membership set of their own.

Errors

Revert precedence

createCompositePolicy runs its checks in this fixed order — each check fires before the next:
  1. ZeroAddressadmin == address(0).
  2. IncompatiblePolicyTypepolicyType is not UNION or INTERSECT.
  3. ChildPoliciesOutsideOfRangechildPolicyIds.length is outside [2, 4].
  4. PolicyNotFound — any listed child does not exist (checked in one pass over the whole set).
  5. InvalidChildPolicy — any listed child is a composite or a built-in sentinel (second pass).
updateComposite runs its checks in this fixed order:
  1. PolicyNotFoundpolicyId does not exist.
  2. IncompatiblePolicyTypepolicyId is a simple policy, not a composite.
  3. Unauthorized — caller is not the current admin. A renounced composite (admin address(0)) can never be updated.
  4. ChildPoliciesOutsideOfRange — new child count is outside [2, 4].
  5. PolicyNotFound — any new child does not exist.
  6. InvalidChildPolicy — any new child is a composite or a built-in sentinel.

Evaluation

isAuthorized on a composite calls each child policy’s isAuthorized live. It is never a snapshot taken at creation or last update. UNION returns true on the first authorizing child and short-circuits. INTERSECT returns false on the first non-authorizing child. Recursion never exceeds depth 1: every child is validated to be a simple ALLOWLIST or BLOCKLIST policy at write time, so a composite’s children can never themselves be composites.

Example

Guarantees and edge cases

  • No nested composites. createCompositePolicy and updateComposite revert InvalidChildPolicy(childPolicyId) for any child whose type is UNION or INTERSECT.
  • No built-in sentinels as children. ALWAYS_ALLOW and ALWAYS_BLOCK also revert with InvalidChildPolicy. To fold always-allow / always-block behavior into a composite, create a real ALLOWLIST or BLOCKLIST policy that reproduces the effect and reference it instead.
  • Duplicates are permitted. The registry neither sorts nor de-duplicates the stored child list. Duplicates cost extra per-call evaluation but do not change the result — UNION and INTERSECT are idempotent under duplicates.
  • No path to an under-sized composite. Every updateComposite call re-enforces [2, 4], so a composite can never be shrunk below two children.
  • Renouncing a child admin does not break the parent. renounceAdmin on a child clears the child’s admin and freezes its future membership changes. The child still exists, isAuthorized on it still resolves, and the composite keeps evaluating it.
  • No composite-specific activation flag. createCompositePolicy and updateComposite are gated by the same ActivationRegistry flag as createPolicy, updateAllowlist, and the rest. compositePolicyChildIds, the MIN / MAX bounds, and isAuthorized on a composite ID are always callable.
As with any policy ID, validate policyExists(policyId) before writing a composite ID to a B20 policy scope. isAuthorized never reverts on a non-existent ID — it collapses to empty-member-set semantics and will silently authorize or deny every account.