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
20 changes: 20 additions & 0 deletions src/interfaces/IPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ pragma solidity >=0.8.20 <0.9.0;
///
/// @notice Singleton registry of simple and composite policies. Policies are referenced by
/// `uint64 policyId` and queried via `isAuthorized(policyId, account)`.
///
/// @dev Invert (`invertedPolicyId`): all view functions see an inverted policy ID as an
/// extension of the base policy — same existence, admin, pending admin, and child
/// set as the base; `isAuthorized` returns the negated base result.
interface IPolicyRegistry {
/*//////////////////////////////////////////////////////////////
TYPES
Expand Down Expand Up @@ -127,6 +131,8 @@ interface IPolicyRegistry {
/// @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
Comment thread
rayyan224 marked this conversation as resolved.
/// capped at 4.
/// @dev A child policy ID may be inverted (its invert flag, bit 63, set via
/// `invertedPolicyId`), in which case the child is evaluated as the inverse of the base.
/// @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
Expand Down Expand Up @@ -227,6 +233,8 @@ interface IPolicyRegistry {
/// BLOCKLIST -> true).
///
/// @dev Callers that store policy IDs MUST validate `policyExists(policyId)` at write time.
/// @dev Invert: `isAuthorized(invertedPolicyId(id), account)` returns the negated
/// result of the base. Applies to every policy type.
///
/// @param policyId Policy to query.
/// @param account Account to check.
Expand Down Expand Up @@ -280,9 +288,21 @@ interface IPolicyRegistry {
/// @dev An empty return unambiguously means "not a composite".
/// @dev The registry preserves the caller's ordering verbatim and neither sorts nor
/// de-duplicates.
/// @dev Child IDs are returned as stored, including any per-child invert.
///
/// @param policyId Policy to query.
///
/// @return Child policy IDs, or an empty array.
function compositePolicyChildIds(uint64 policyId) external view returns (uint64[] memory);

/// @notice Returns `policyId` with its invert flag (bit 63) flipped. Never reverts
/// and reads no state.
///
/// @dev This call does not check that `policyId` exists; a missing
/// or malformed base is denied later, at `isAuthorized`.
///
/// @param policyId Policy to invert.
///
/// @return The policy ID with its invert flag toggled.
function invertedPolicyId(uint64 policyId) external view returns (uint64);
}
66 changes: 52 additions & 14 deletions test/lib/mocks/MockPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ library PolicyRegistryConstants {
/// @dev Encodes as an ALLOWLIST at counter 1 (empty allowlist → block all).
uint64 internal constant ALWAYS_BLOCK_ID = (uint64(uint8(IPolicyRegistry.PolicyType.ALLOWLIST)) << 56) | 1;

/// @notice High bit of a `uint64` policy ID that inverts the base policy.
/// @dev All view functions see an inverted ID as an extension of the base — policy ID
uint64 internal constant INVERTED_POLICY_BIT = uint64(1) << 63;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INVERTED_POLICY_MASK might be more accurate

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 don't think "mask" is correct since we're using it as a switch not as a mask. if we applied the entire bit field to an underlying bitfield then it would be a mask, but in this case, it's acting as a single purpose bool packed into reserved bitspace.


/// @notice Number of built-in policies the registry initializes on
/// first use. The global counter is advanced to this value
/// once both sentinels are populated, so custom policies
Expand Down Expand Up @@ -70,6 +74,11 @@ contract MockPolicyRegistry is IPolicyRegistry {
// Policy ID encoding: top byte = uint8(PolicyType), low 56 bits = counter.
uint64 internal constant POLICY_ID_TYPE_SHIFT = 56;

/// @notice Invert flag on a policy ID: bit 63.
/// @dev Sourced from `PolicyRegistryConstants` so the mock and tests share one
/// definition of the bit.
uint64 internal constant INVERTED_POLICY_BIT = PolicyRegistryConstants.INVERTED_POLICY_BIT;

/// @notice Per-call membership-batch limit. `createPolicyWithAccounts`,
/// `updateAllowlist`, and `updateBlocklist` revert with
/// `BatchSizeTooLarge(MAX_BATCH_SIZE)` when `accounts.length`
Expand Down Expand Up @@ -238,20 +247,18 @@ contract MockPolicyRegistry is IPolicyRegistry {
// ============================================================

/// @inheritdoc IPolicyRegistry
/// @dev An inverted ID resolves to the existence of its base: `policyExists(~id)`
/// equals `policyExists(id)`, so a token may store and later re-validate an
/// inverted policy exactly as it would a plain one.
function policyExists(uint64 policyId) external view returns (bool) {
if (policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID) return true;
if (!_isWellFormed(policyId)) return false;
// Use the typed `policyExistsFromPacked` helper rather than a raw
// `packed != 0` test. Functionally identical given the encoding
// invariant (exists bit is always set when `_encode` writes the
// slot), but matches the Rust precompile's `packed.exists()`
// call and survives any future encoding change that adds bits
// above the admin lane without setting the exists bit.
return MockPolicyRegistryStorage.policyExistsFromPacked(MockPolicyRegistryStorage.layout().policies[policyId]);
return _policyExists(policyId);
}

/// @inheritdoc IPolicyRegistry
/// @dev An inverted ID has no record of its own; it resolves to its base's admin,
/// matching `policyExists` (`policyAdmin(~id) == policyAdmin(id)`).
function policyAdmin(uint64 policyId) external view returns (address) {
policyId = _basePolicyId(policyId);
if (!_isWellFormed(policyId)) return address(0);
// No fast path for built-in IDs needed: lazy init writes them with
// a zero admin, so the normal storage read returns address(0) for
Expand All @@ -276,18 +283,27 @@ contract MockPolicyRegistry is IPolicyRegistry {
// below would also return `address(0)` for built-ins in normal
// operation (they never have a pending admin staged), but the
// explicit branch removes that assumption from the trust boundary.
policyId = _basePolicyId(policyId);
if (policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID) return address(0);
if (!_isWellFormed(policyId)) return address(0);
return MockPolicyRegistryStorage.layout().pendingAdmins[policyId];
}

/// @inheritdoc IPolicyRegistry

function compositePolicyChildIds(uint64 policyId) external view returns (uint64[] memory) {
policyId = _basePolicyId(policyId);
if (!_isWellFormed(policyId)) return new uint64[](0);
if (!_isComposite(policyId)) return new uint64[](0);
return MockPolicyRegistryStorage.layout().children[policyId];
}

/// @inheritdoc IPolicyRegistry
/// @dev Pure toggle of the invert flag on a policy ID; never reverts and reads no state.
function invertedPolicyId(uint64 policyId) external pure returns (uint64) {
return policyId ^ INVERTED_POLICY_BIT;
}

// ============================================================
// INTERNAL HELPERS
// ============================================================
Expand Down Expand Up @@ -349,6 +365,16 @@ contract MockPolicyRegistry is IPolicyRegistry {
if (packed == 0) revert PolicyNotFound();
}

/// @dev Existence predicate shared by the external `policyExists` view and the
/// fail-closed guard in `_isAuthorized`.
function _policyExists(uint64 policyId) internal view returns (bool) {
policyId = _basePolicyId(policyId);
if (policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID) return true;
if (!_isWellFormed(policyId)) return false;

return MockPolicyRegistryStorage.policyExistsFromPacked(MockPolicyRegistryStorage.layout().policies[policyId]);
}

/// @dev Core authorization logic shared by the external view and composite
/// child evaluation. Never reverts.
///
Expand All @@ -358,6 +384,12 @@ contract MockPolicyRegistry is IPolicyRegistry {
/// `_isAuthorized` per child, each of which resolves via the simple path
/// (or a built-in short-circuit).
function _isAuthorized(uint64 policyId, address account) internal view returns (bool) {
bool isInverted = policyId & INVERTED_POLICY_BIT != 0;
if (isInverted) {
uint64 base = _basePolicyId(policyId);
if (!_policyExists(base)) return false;
return !_isAuthorized(base, account);
}
// Built-in short-circuits precede any SLOAD; sentinels have no
// storage entry.
if (policyId == ALWAYS_ALLOW_ID) return true;
Expand Down Expand Up @@ -404,16 +436,17 @@ contract MockPolicyRegistry is IPolicyRegistry {
/// and must not itself be a composite. Two passes so `PolicyNotFound` takes
/// precedence over `InvalidChildPolicy` across the whole set (matches the
/// canonical revert order the Rust precompile mirrors).
/// @dev An inverted valid policy ID counts as a valid composite child.
function _requireCreatedSimplePolicies(uint64[] calldata childPolicyIds) internal view {
MockPolicyRegistryStorage.Layout storage $ = MockPolicyRegistryStorage.layout();
// Pass 1: existence.
// Pass 1: existence of the base (an inverted child references its base's members).
for (uint256 i = 0; i < childPolicyIds.length; ++i) {
if ($.policies[childPolicyIds[i]] == 0) revert PolicyNotFound();
if ($.policies[_basePolicyId(childPolicyIds[i])] == 0) revert PolicyNotFound();
}
// Pass 2: must be a simple policy
// Pass 2: the base must be a simple policy (never a sentinel or a composite).
for (uint256 i = 0; i < childPolicyIds.length; ++i) {
uint64 child = childPolicyIds[i];
if (_isBuiltin(child) || _isComposite(child)) revert InvalidChildPolicy(child);
uint64 base = _basePolicyId(childPolicyIds[i]);
if (_isBuiltin(base) || _isComposite(base)) revert InvalidChildPolicy(childPolicyIds[i]);
}
}

Expand All @@ -434,6 +467,11 @@ contract MockPolicyRegistry is IPolicyRegistry {
return policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID;
}

/// @dev Strips the invert flag from the policy ID.
function _basePolicyId(uint64 policyId) internal pure returns (uint64) {
return policyId & ~INVERTED_POLICY_BIT;
}

function _makeId(PolicyType policyType, uint56 counter) internal pure returns (uint64) {
return (uint64(uint8(policyType)) << POLICY_ID_TYPE_SHIFT) | uint64(counter);
}
Expand Down
Loading
Loading