Abstract
The Denim hardfork adds an invert flag to the Policy Registry’suint64 policy ID. When bit 63 (INVERTED_POLICY_BIT) is set, isAuthorized resolves the base policy and returns the opposite of its result. No new storage is allocated; the flag is query-time only. Every existing policy type — ALLOWLIST, BLOCKLIST, UNION, and INTERSECT — can be inverted. Existing IDs are unaffected because bit 63 was previously unused.
Motivation
The Policy Registry is increasingly used as a shared registry of addresses that other policies compose around. Without invert, expressing “NOT policy A” requires a second policy of the opposite type containing a copy of A’s membership. Any membership change must then land on both policies; a lagging update admits invalid accounts or rejects valid ones. A composite that needs “A AND NOT X” cannot reuse X — it must point at a separately-maintained mirror. Encoding inversion in the policy reference solves this. The same registry entry supports expressions such asA OR B, A AND NOT B, or NOT A without additional policies. One address list, managed once, represents either side of a rule depending on how it is referenced.
What Changed
New constant and helper
PolicyRegistryConstants
IPolicyRegistry
invertedPolicyId returns policyId ^ INVERTED_POLICY_BIT. It is pure (reads no state), never reverts, and is involutive: invertedPolicyId(invertedPolicyId(id)) == id.
Updated selector table
Authorization behavior
isAuthorized gains a leading invert branch. All non-inverted paths are byte-identical to the previous behavior.
isAuthorized pseudocode
false — it never becomes allow-everyone. This guards against a typo’d or garbage ID with bit 63 set from bypassing mint, transfer, or seize checks.
Getter strip semantics
Read views strip bit 63 via_basePolicyId(id) = id & ~INVERTED_POLICY_BIT and load the base record. An inverted ID has no independent storage record; it mirrors the base’s existence, admin, pending admin, and child set.
Composite child invert
A child ID increateCompositePolicy or updateComposite may carry the invert bit. The registry validates the child against its base: an inverted simple child (ALLOWLIST or BLOCKLIST) is accepted; an inverted composite child is rejected with InvalidChildPolicy to preserve the flat-tree invariant. Across the whole child set, PolicyNotFound takes precedence over InvalidChildPolicy.
Storage and gas
No new storage slots. Invert is query-time only — one boolean flip in memory. There is no extraSLOAD.
Examples
Invert a sanctions blocklist so the policy reads “not sanctioned”:Standalone invert
INTERSECT composite:
Composite with inverted child
isAuthorized(base | INVERTED_POLICY_BIT, account) returns false.
Migration
This change is not breaking. All existing selectors, events, and errors are unchanged. Existing IDs have bit 63 unset, so all existing behavior is identical.1
Compute the inverted ID
Call
invertedPolicyId(policyId) on the registry, or set bit 63 directly with policyId | (uint64(1) << 63).2
Bind it to a B20 scope
Pass the inverted ID to
updatePolicy for a standalone scope, or include it as a child in createCompositePolicy / updateComposite. B20 treats the ID as an opaque uint64 and requires no changes.3
Validate at write time
Consumers that store policy IDs must still call
policyExists(policyId) at write time. This works for inverted IDs because existence resolves to the base.Alternatives Considered
Alternative 1 — New NOT policy type
createNot(admin, base) allocates a fresh record pointing at a base. A first-class NOT node wraps any policy and offers the clearest explorer legibility. Rejected: standalone NOT costs about 3 SLOADs versus 1 for a mirror blocklist; “A AND NOT X” costs about 6 versus the chosen approach’s 4. It also adds a new create path and deepens hot-path recursion as a composite child.
Alternative 2 — Per-child invert bitmask on the composite
A bitmask packed into the children length word flips individual children.mask = 0 reproduces today’s behavior with no migration. Rejected: the flag only works inside a composite — a simple policy cannot be inverted without wrapping it in a composite with a minimum of two children. There is no standalone referenceable inverse of an arbitrary policy.
Test Cases
The new suite (test/unit/PolicyRegistry/isAuthorizedInvert.t.sol, 16 cases) covers:
- Fail-closed invariants: inverted unknown base returns
false - Simple/built-in truth tables for
ALLOWLISTandBLOCKLIST INTERSECT[A, ~X]composite evaluation- Child validation: inverted simple child accepted, inverted composite child rejected
- Getter strip semantics:
policyExists,policyAdmin,pendingPolicyAdmin,compositePolicyChildIds invertedPolicyIdround-trip involution