Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/interfaces/IPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ interface IPolicyRegistry {
/// @notice Creates a new policy with no initial members. Permissionless.
///
/// @dev Reverts with `ZeroAddress` when `admin` is `address(0)`.
/// @dev Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value.
///
/// @param admin Initial admin authorized to modify membership and transfer or renounce administration.
/// @param policyType BLOCKLIST or ALLOWLIST.
Expand All @@ -80,6 +81,7 @@ interface IPolicyRegistry {
///
/// @dev Reverts with `ZeroAddress` when `admin` is `address(0)`. Takes precedence over `BatchSizeTooLarge`.
/// @dev Reverts with `BatchSizeTooLarge` when `accounts.length` exceeds the registry limit.
/// @dev Panics with arithmetic overflow (Panic 0x11) when the policy counter has reached its maximum value.
///
/// @param admin Initial admin authorized to modify membership and transfer or renounce administration.
/// @param policyType BLOCKLIST or ALLOWLIST.
Expand Down
8 changes: 3 additions & 5 deletions test/lib/mocks/MockPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,9 @@ contract MockPolicyRegistry is IPolicyRegistry {
_writeBuiltins();
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;
}
// Solidity checked arithmetic panics with Panic(0x11) on uint56 overflow,
// matching the Rust precompile which reverts with Panic(UnderOverflow) at COUNTER_MASK.
$.nextCounter = counter + 1;
newPolicyId = _makeId({policyType: policyType, counter: counter});
$.policies[newPolicyId] = _encode(admin);
emit PolicyCreated(newPolicyId, msg.sender, policyType);
Expand Down
17 changes: 17 additions & 0 deletions test/unit/PolicyRegistry/createPolicy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,21 @@ contract PolicyRegistryCreatePolicyTest is PolicyRegistryTest {
vm.prank(caller);
policyRegistry.createPolicy(admin_, pt);
}

/// @notice Verifies createPolicy panics with arithmetic overflow when the counter is at uint56 max.
/// @dev Slot-writes nextCounter to type(uint56).max to avoid iterating 2^56 times.
/// Matches the Rust precompile which reverts with Panic(UnderOverflow) = Panic(0x11).
function test_createPolicy_revert_counterOverflow(address caller, address admin_, uint8 typeIdx) public {
_assumeValidCaller(caller);
vm.assume(admin_ != address(0));
IPolicyRegistry.PolicyType pt = _creatablePolicyType(typeIdx);

vm.store(
address(policyRegistry), MockPolicyRegistryStorage.nextCounterSlot(), bytes32(uint256(type(uint56).max))
);

vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11));
vm.prank(caller);
policyRegistry.createPolicy(admin_, pt);
}
}
15 changes: 14 additions & 1 deletion test/unit/PolicyRegistry/createPolicy_revertOrder.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ pragma solidity ^0.8.20;
import {IPolicyRegistry} from "src/interfaces/IPolicyRegistry.sol";

import {PolicyRegistryTest} from "test/lib/PolicyRegistryTest.sol";
import {MockPolicyRegistryStorage} from "test/lib/mocks/MockPolicyRegistryStorage.sol";

/// @title Sequential revert-order test for `createPolicy`.
///
/// @notice **Canonical order:**
/// 1. ZERO-ADMIN (`admin == address(0)`) → `ZeroAddress`
/// 2. COUNTER-OVERFLOW (nextCounter == type(uint56).max) → `Panic(0x11)`
///
/// Single revert condition; walks from that condition to success.
/// Walks from the first failing condition to success.
contract PolicyRegistryCreatePolicyRevertOrderTest is PolicyRegistryTest {
/// @notice Walks through every revert in canonical order, fixing one per step, ending at success.
function test_createPolicy_revertOrder(address caller, address admin_, uint8 typeIdx) public {
Expand All @@ -19,12 +21,23 @@ contract PolicyRegistryCreatePolicyRevertOrderTest is PolicyRegistryTest {
IPolicyRegistry.PolicyType pt = _creatablePolicyType(typeIdx);

// 1. ZERO-ADMIN: admin == address(0) → ZeroAddress
vm.store(
address(policyRegistry), MockPolicyRegistryStorage.nextCounterSlot(), bytes32(uint256(type(uint56).max))
);
vm.prank(caller);
vm.expectRevert(IPolicyRegistry.ZeroAddress.selector);
policyRegistry.createPolicy(address(0), pt);

// Fix: use a non-zero admin.

// 2. COUNTER-OVERFLOW: nextCounter == type(uint56).max → Panic(0x11)
vm.prank(caller);
vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11));
policyRegistry.createPolicy(admin_, pt);

// Fix: reset counter to a valid value.
vm.store(address(policyRegistry), MockPolicyRegistryStorage.nextCounterSlot(), bytes32(uint256(2)));

// Success
vm.prank(caller);
policyRegistry.createPolicy(admin_, pt);
Expand Down
Loading