Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
18 changes: 8 additions & 10 deletions test/lib/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import {StdPrecompiles} from "src/StdPrecompiles.sol";
/// deploy a token, and need the policy-registry mock so the token's
/// cross-precompile `isAuthorized` calls don't hit empty code and
/// revert at the EVM level (the most common B20 policy tests use the
/// built-in sentinel IDs `0` / `type(uint64).max` to exercise both
/// authorize and forbid paths without any custom registry state).
/// Centralizing the etch here means concrete bases don't need to
/// built-in sentinel IDs `0` (ALWAYS_ALLOW) and `1` (ALWAYS_BLOCK) to
/// exercise both authorize and forbid paths without any custom registry
/// state). Centralizing the etch here means concrete bases don't need to
/// reason about which precompiles they "depend on" — they're all just
/// available, the way the EVM has `SLOAD`.
///
Expand All @@ -45,13 +45,11 @@ import {StdPrecompiles} from "src/StdPrecompiles.sol";
/// surface function is live, with the bootstrap-window auth bypass
/// for factory-originated calls and the standard role / policy /
/// pause / supply-cap checks otherwise.
/// - `MockPolicyRegistry` is a SKELETON: implements only the two
/// built-in sentinel IDs (`0` → always-allow,
/// `type(uint64).max` → always-reject) so the most common B-20 tests
/// can exercise both authorize and forbid paths without any custom
/// policy state. Tests that need custom policies (create, update
/// membership, rotate admin, etc.) will fail until the full mock
/// lands in a follow-up PR.
/// - `MockPolicyRegistry` is fully implemented: every `IPolicyRegistry`
/// surface function is live. Custom policy creation, membership
/// mutation, and admin rotation all work. Built-in IDs `0`
/// (ALWAYS_ALLOW) and `1` (ALWAYS_BLOCK) are short-circuited before
/// any storage read.
/// - `MockActivationRegistry` is a SKELETON: implements only `admin()`
/// to return the hardcoded test admin.
abstract contract BaseTest is Test {
Expand Down
28 changes: 25 additions & 3 deletions test/lib/PolicyRegistryTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,32 @@ import {StdPrecompiles} from "src/StdPrecompiles.sol";
/// @notice Base test contract for `IPolicyRegistry` unit tests.
///
/// Inherits all precompile-mock etch wiring and common actors from
/// `BaseTest`; adds the registry handle. Test bodies that need to set
/// up policies or rotate admins do so inline so the `vm.prank` / call
/// is visible at the test site rather than hidden behind a wrapper.
/// `BaseTest`; adds the registry handle and policy-creation helpers.
contract PolicyRegistryTest is BaseTest {
// -- Precompile handle --
IPolicyRegistry internal policyRegistry = StdPrecompiles.POLICY_REGISTRY;

// -- Helpers --

/// @notice Create an ALLOWLIST policy with explicit admin and caller.
function _createAllowlist(address caller, address policyAdmin) internal returns (uint64 policyId) {
vm.prank(caller);
policyId = policyRegistry.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.ALLOWLIST);
}

/// @notice Create an ALLOWLIST policy as the default admin (no prank needed at call site).
function _createAllowlist() internal returns (uint64 policyId) {
policyId = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.ALLOWLIST);
}

/// @notice Create a BLOCKLIST policy with explicit admin and caller.
function _createBlocklist(address caller, address policyAdmin) internal returns (uint64 policyId) {
vm.prank(caller);
policyId = policyRegistry.createPolicy(policyAdmin, IPolicyRegistry.PolicyType.BLOCKLIST);
}

/// @notice Create a BLOCKLIST policy as the default admin.
function _createBlocklist() internal returns (uint64 policyId) {
policyId = policyRegistry.createPolicy(admin, IPolicyRegistry.PolicyType.BLOCKLIST);
}
}
227 changes: 185 additions & 42 deletions test/lib/mocks/MockPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,80 +3,223 @@ pragma solidity ^0.8.20;

import {IPolicyRegistry} from "src/interfaces/IPolicyRegistry.sol";

/// @notice Placeholder mock for the `IPolicyRegistry` precompile.
import {MockPolicyRegistryStorage} from "test/lib/mocks/MockPolicyRegistryStorage.sol";

/// @title MockPolicyRegistry
/// @notice Reference implementation of the `IPolicyRegistry` precompile.
/// Etched at the canonical policy-registry address via `vm.etch`
/// from `BaseTest.setUp`.
///
/// @dev Written as Solidity-as-if-Rust: unambiguous spec-correspondence
/// with the production Rust precompile is the goal, not gas
/// optimisation or Solidity idiom adherence.
///
/// Implements only the built-in sentinel semantics that B20 tests rely
/// on to exercise policy gating without configuring custom policies:
/// - `isAuthorized(0, _)` → true (ALWAYS_ALLOW, built-in ID 0)
/// - `isAuthorized(1, _)` → false (ALWAYS_BLOCK, built-in ID 1)
/// - `policyExists` returns true for those two built-ins
/// - `policyType` returns the matching PolicyType enum for those two
/// All mutable state lives in `MockPolicyRegistryStorage.layout()` at
/// a single ERC-7201-namespaced root. The struct field order IS the
/// slot layout the Rust impl mirrors. See `MockPolicyRegistryStorage`
/// for the full layout documentation and per-field slot offsets.
///
/// Built-in ID assignments updated per IPolicyRegistry's
/// ALWAYS_ALLOW = 0, ALWAYS_BLOCK = 1 design (PR #24): the built-in
/// IDs are now the small numeric values that match the PolicyType
/// ordinals rather than the previous (0, type(uint64).max) convention.
/// **Policy ID encoding:**
/// [63:56] uint8(PolicyType) discriminator
/// [55:0] nextCounter value at creation time
/// `_create` rejects ALWAYS_ALLOW and ALWAYS_BLOCK types, so no
/// custom ID ever carries discriminator 0x00 or 0x01.
///
/// Every other method reverts pending the full mock implementation in
/// a follow-up PR. Custom policy creation, admin rotation, and
/// membership mutation are out of scope until then.
/// **Built-in IDs** (short-circuited before any storage read):
/// 0 — ALWAYS_ALLOW: isAuthorized always returns true.
/// 1 — ALWAYS_BLOCK: isAuthorized always returns false.
contract MockPolicyRegistry is IPolicyRegistry {
// ============================================================
// CONSTANTS
// ============================================================

uint64 internal constant ALWAYS_ALLOW_ID = 0;
uint64 internal constant ALWAYS_BLOCK_ID = 1;

function isAuthorized(uint64 policyId, address /*account*/ ) external pure returns (bool) {
if (policyId == ALWAYS_ALLOW_ID) return true;
if (policyId == ALWAYS_BLOCK_ID) return false;
revert PolicyNotFound();
// Policy ID encoding: top byte = uint8(PolicyType), low 56 bits = counter.
uint256 internal constant TYPE_SHIFT = 56;

// Admin address occupies bits [167:8]; PolicyType occupies bits [7:0].
uint256 internal constant ADMIN_SHIFT = 8;

// ============================================================
// POLICY CREATION
// ============================================================

/// @inheritdoc IPolicyRegistry
function createPolicy(address admin, PolicyType policyType) external returns (uint64 newPolicyId) {
newPolicyId = _create(admin, policyType);
}

function policyExists(uint64 policyId) external pure returns (bool) {
return policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID;
/// @inheritdoc IPolicyRegistry
function createPolicyWithAccounts(address admin, PolicyType policyType, address[] calldata accounts)
external
returns (uint64 newPolicyId)
{
newPolicyId = _create(admin, policyType);
_batchSetMembers({policyId: newPolicyId, policyType: policyType, value: true, accounts: accounts});
}

function createPolicy(address, PolicyType) external pure returns (uint64) {
revert("MockPolicyRegistry: not implemented");
// ============================================================
// POLICY ADMINISTRATION
// ============================================================

/// @inheritdoc IPolicyRegistry
function stageUpdateAdmin(uint64 policyId, address newAdmin) external {
uint256 packed = _requireCustom(policyId);
if (_decodeAdmin(packed) != msg.sender) revert Unauthorized();
MockPolicyRegistryStorage.layout().pendingAdmins[policyId] = newAdmin;
emit PolicyAdminStaged(policyId, msg.sender, newAdmin);
}

function createPolicyWithAccounts(address, PolicyType, address[] calldata) external pure returns (uint64) {
revert("MockPolicyRegistry: not implemented");
/// @inheritdoc IPolicyRegistry
function finalizeUpdateAdmin(uint64 policyId) external {
MockPolicyRegistryStorage.Layout storage $ = MockPolicyRegistryStorage.layout();
uint256 packed = $.policies[policyId];
if (packed == 0) revert PolicyNotFound();
address pending = $.pendingAdmins[policyId];
if (pending == address(0)) revert NoPendingAdmin();
if (pending != msg.sender) revert Unauthorized();
address previousAdmin = _decodeAdmin(packed);
$.policies[policyId] = _encode({policyType: _decodeType(packed), admin: msg.sender});
delete $.pendingAdmins[policyId];
emit PolicyAdminUpdated(policyId, previousAdmin, msg.sender);
}

function stageUpdateAdmin(uint64, address) external pure {
revert("MockPolicyRegistry: not implemented");
/// @inheritdoc IPolicyRegistry
function renounceAdmin(uint64 policyId) external {
MockPolicyRegistryStorage.Layout storage $ = MockPolicyRegistryStorage.layout();
uint256 packed = $.policies[policyId];
if (packed == 0) revert PolicyNotFound();
if (_decodeAdmin(packed) != msg.sender) revert Unauthorized();
$.policies[policyId] = _encode({policyType: _decodeType(packed), admin: address(0)});
delete $.pendingAdmins[policyId];
emit PolicyAdminUpdated(policyId, msg.sender, address(0));
}

function finalizeUpdateAdmin(uint64) external pure {
revert("MockPolicyRegistry: not implemented");
/// @inheritdoc IPolicyRegistry
function updateAllowlist(uint64 policyId, bool allowed, address[] calldata accounts) external {
uint256 packed = _requireCustom(policyId);
if (_decodeType(packed) != PolicyType.ALLOWLIST) revert IncompatiblePolicyType();
if (_decodeAdmin(packed) != msg.sender) revert Unauthorized();
_batchSetMembers({policyId: policyId, policyType: PolicyType.ALLOWLIST, value: allowed, accounts: accounts});
}

function renounceAdmin(uint64) external pure {
revert("MockPolicyRegistry: not implemented");
/// @inheritdoc IPolicyRegistry
function updateBlocklist(uint64 policyId, bool blocked, address[] calldata accounts) external {
uint256 packed = _requireCustom(policyId);
if (_decodeType(packed) != PolicyType.BLOCKLIST) revert IncompatiblePolicyType();
if (_decodeAdmin(packed) != msg.sender) revert Unauthorized();
_batchSetMembers({policyId: policyId, policyType: PolicyType.BLOCKLIST, value: blocked, accounts: accounts});
}

function updateAllowlist(uint64, bool, address[] calldata) external pure {
revert("MockPolicyRegistry: not implemented");
// ============================================================
// AUTHORIZATION QUERIES
// ============================================================

/// @inheritdoc IPolicyRegistry
function isAuthorized(uint64 policyId, address account) external view returns (bool) {
// Built-in short-circuits MUST precede any storage read: IDs 0 and 1
// have no entry in storage and must never reach the storage path.
if (policyId == ALWAYS_ALLOW_ID) return true;
if (policyId == ALWAYS_BLOCK_ID) return false;
MockPolicyRegistryStorage.Layout storage $ = MockPolicyRegistryStorage.layout();
uint256 packed = $.policies[policyId];
if (packed == 0) revert PolicyNotFound();
bool member = $.members[policyId][account];
return _decodeType(packed) == PolicyType.ALLOWLIST ? member : !member;
}

function updateBlocklist(uint64, bool, address[] calldata) external pure {
revert("MockPolicyRegistry: not implemented");
// ============================================================
// POLICY QUERIES
// ============================================================

/// @inheritdoc IPolicyRegistry
function nextPolicyId(PolicyType policyType) external view returns (uint64) {
return _makeId({policyType: policyType, counter: MockPolicyRegistryStorage.layout().nextCounter});
}

function nextPolicyId(PolicyType) external pure returns (uint64) {
revert("MockPolicyRegistry: not implemented");
/// @inheritdoc IPolicyRegistry
function policyExists(uint64 policyId) external view returns (bool) {
if (policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID) return true;
return MockPolicyRegistryStorage.layout().policies[policyId] != 0;
}

function policyType(uint64 policyId) external pure returns (PolicyType) {
/// @inheritdoc IPolicyRegistry
function policyType(uint64 policyId) external view returns (PolicyType) {
if (policyId == ALWAYS_ALLOW_ID) return PolicyType.ALWAYS_ALLOW;
if (policyId == ALWAYS_BLOCK_ID) return PolicyType.ALWAYS_BLOCK;
revert("MockPolicyRegistry: not implemented");
uint256 packed = MockPolicyRegistryStorage.layout().policies[policyId];
if (packed == 0) revert PolicyNotFound();
return _decodeType(packed);
}

/// @inheritdoc IPolicyRegistry
function policyAdmin(uint64 policyId) external view returns (address) {
if (policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID) return address(0);
uint256 packed = MockPolicyRegistryStorage.layout().policies[policyId];
if (packed == 0) revert PolicyNotFound();
return _decodeAdmin(packed);
}

/// @inheritdoc IPolicyRegistry
function pendingPolicyAdmin(uint64 policyId) external view returns (address) {
if (policyId == ALWAYS_ALLOW_ID || policyId == ALWAYS_BLOCK_ID) return address(0);
return MockPolicyRegistryStorage.layout().pendingAdmins[policyId];
}

// ============================================================
// INTERNAL HELPERS
// ============================================================

function _create(address admin, PolicyType policyType) internal returns (uint64 newPolicyId) {
if (policyType != PolicyType.ALLOWLIST && policyType != PolicyType.BLOCKLIST) revert InvalidPolicyType();
if (admin == address(0)) revert ZeroAddress();
MockPolicyRegistryStorage.Layout storage $ = MockPolicyRegistryStorage.layout();
uint56 counter = $.nextCounter;
// No overflow guard: at one policy per 2-second block, exhausting the
// 56-bit counter space (~7.2e16 values) takes ~4.6 billion years.
unchecked {
$.nextCounter = counter + 1;
}
newPolicyId = _makeId({policyType: policyType, counter: counter});
$.policies[newPolicyId] = _encode({policyType: policyType, admin: admin});
emit PolicyCreated(newPolicyId, msg.sender, policyType);
emit PolicyAdminUpdated(newPolicyId, address(0), admin);
}

function _batchSetMembers(uint64 policyId, PolicyType policyType, bool value, address[] calldata accounts)
internal
{
mapping(address => bool) storage members = MockPolicyRegistryStorage.layout().members[policyId];
for (uint256 i = 0; i < accounts.length; ++i) {
members[accounts[i]] = value;
}
if (policyType == PolicyType.ALLOWLIST) {
emit AllowlistUpdated(policyId, msg.sender, value, accounts);
} else {
emit BlocklistUpdated(policyId, msg.sender, value, accounts);
}
}

function _requireCustom(uint64 policyId) internal view returns (uint256 packed) {
packed = MockPolicyRegistryStorage.layout().policies[policyId];
if (packed == 0) revert PolicyNotFound();
}

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

function _encode(PolicyType policyType, address admin) internal pure returns (uint256) {
return (uint256(uint160(admin)) << ADMIN_SHIFT) | uint256(policyType);
}

function policyAdmin(uint64) external pure returns (address) {
revert("MockPolicyRegistry: not implemented");
function _decodeType(uint256 packed) internal pure returns (PolicyType) {
return PolicyType(uint8(packed));
}

function pendingPolicyAdmin(uint64) external pure returns (address) {
revert("MockPolicyRegistry: not implemented");
function _decodeAdmin(uint256 packed) internal pure returns (address) {
return address(uint160(packed >> ADMIN_SHIFT));
}
}
Loading