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.
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.
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:
ZeroAddress—admin == address(0).IncompatiblePolicyType—policyTypeis notUNIONorINTERSECT.ChildPoliciesOutsideOfRange—childPolicyIds.lengthis outside[2, 4].PolicyNotFound— any listed child does not exist (checked in one pass over the whole set).InvalidChildPolicy— any listed child is a composite or a built-in sentinel (second pass).
updateComposite runs its checks in this fixed order:
PolicyNotFound—policyIddoes not exist.IncompatiblePolicyType—policyIdis a simple policy, not a composite.Unauthorized— caller is not the current admin. A renounced composite (adminaddress(0)) can never be updated.ChildPoliciesOutsideOfRange— new child count is outside[2, 4].PolicyNotFound— any new child does not exist.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.
createCompositePolicyandupdateCompositerevertInvalidChildPolicy(childPolicyId)for any child whose type isUNIONorINTERSECT. - No built-in sentinels as children.
ALWAYS_ALLOWandALWAYS_BLOCKalso revert withInvalidChildPolicy. To fold always-allow / always-block behavior into a composite, create a realALLOWLISTorBLOCKLISTpolicy 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 —
UNIONandINTERSECTare idempotent under duplicates. - No path to an under-sized composite. Every
updateCompositecall re-enforces[2, 4], so a composite can never be shrunk below two children. - Renouncing a child admin does not break the parent.
renounceAdminon a child clears the child’s admin and freezes its future membership changes. The child still exists,isAuthorizedon it still resolves, and the composite keeps evaluating it. - No composite-specific activation flag.
createCompositePolicyandupdateCompositeare gated by the same ActivationRegistry flag ascreatePolicy,updateAllowlist, and the rest.compositePolicyChildIds, theMIN/MAXbounds, andisAuthorizedon a composite ID are always callable.