diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index 9300ea4..bc767c3 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -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. @@ -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. diff --git a/test/lib/mocks/MockPolicyRegistry.sol b/test/lib/mocks/MockPolicyRegistry.sol index bec7f49..c20462d 100644 --- a/test/lib/mocks/MockPolicyRegistry.sol +++ b/test/lib/mocks/MockPolicyRegistry.sol @@ -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); diff --git a/test/unit/PolicyRegistry/createPolicy.t.sol b/test/unit/PolicyRegistry/createPolicy.t.sol index d0afa7c..97d10c1 100644 --- a/test/unit/PolicyRegistry/createPolicy.t.sol +++ b/test/unit/PolicyRegistry/createPolicy.t.sol @@ -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); + } } diff --git a/test/unit/PolicyRegistry/createPolicy_revertOrder.t.sol b/test/unit/PolicyRegistry/createPolicy_revertOrder.t.sol index 7dc9c8a..936f490 100644 --- a/test/unit/PolicyRegistry/createPolicy_revertOrder.t.sol +++ b/test/unit/PolicyRegistry/createPolicy_revertOrder.t.sol @@ -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 { @@ -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);