diff --git a/src/interfaces/IPolicyRegistry.sol b/src/interfaces/IPolicyRegistry.sol index 2d144c1..9c75eba 100644 --- a/src/interfaces/IPolicyRegistry.sol +++ b/src/interfaces/IPolicyRegistry.sol @@ -92,6 +92,12 @@ interface IPolicyRegistry { /// @notice A required address argument was the zero address. error ZeroAddress(); + /// @notice A membership batch exceeded the registry limit. + /// @param maxBatchSize The maximum number of accounts permitted per + /// `createPolicyWithAccounts`, `updateAllowlist`, or + /// `updateBlocklist` call. + error BatchSizeTooLarge(uint256 maxBatchSize); + /// @notice `finalizeUpdateAdmin` was called for a policy with no /// currently-staged pending admin. error NoPendingAdmin(); diff --git a/test/lib/PolicyRegistryTest.sol b/test/lib/PolicyRegistryTest.sol index b41f6af..806c6d3 100644 --- a/test/lib/PolicyRegistryTest.sol +++ b/test/lib/PolicyRegistryTest.sol @@ -81,4 +81,25 @@ contract PolicyRegistryTest is BaseTest { } return accounts; } + + // ============================================================ + // BATCH-LIMIT HELPERS + // ============================================================ + + /// @notice Per-call membership-batch limit enforced by the registry. + /// @dev Mirrors `MockPolicyRegistry.MAX_BATCH_SIZE`. Kept as a + /// test-side literal (rather than reading from the mock) so + /// fork tests against the real precompile use the same + /// compile-time constant. + uint256 internal constant MAX_BATCH_SIZE = 64; + + /// @notice Build an `address[]` of length `n` with deterministic, + /// distinct, non-zero entries. Used by batch-limit tests + /// that need arrays straddling `MAX_BATCH_SIZE`. + function _makeAccounts(uint256 n) internal pure returns (address[] memory accounts) { + accounts = new address[](n); + for (uint256 i = 0; i < n; ++i) { + accounts[i] = address(uint160(0x1000 + i)); + } + } } diff --git a/test/lib/mocks/MockPolicyRegistry.sol b/test/lib/mocks/MockPolicyRegistry.sol index bead618..8e084fb 100644 --- a/test/lib/mocks/MockPolicyRegistry.sol +++ b/test/lib/mocks/MockPolicyRegistry.sol @@ -64,6 +64,13 @@ contract MockPolicyRegistry is IPolicyRegistry { // Policy ID encoding: top byte = uint8(PolicyType), low 56 bits = counter. uint64 internal constant POLICY_ID_TYPE_SHIFT = 56; + /// @notice Per-call membership-batch limit. `createPolicyWithAccounts`, + /// `updateAllowlist`, and `updateBlocklist` revert with + /// `BatchSizeTooLarge(MAX_BATCH_SIZE)` when `accounts.length` + /// exceeds this value. Mirrors the Rust PolicyRegistry + /// precompile (base/base#2876). + uint256 internal constant MAX_BATCH_SIZE = 64; + // ============================================================ // POLICY CREATION // ============================================================ @@ -209,6 +216,7 @@ contract MockPolicyRegistry is IPolicyRegistry { function _batchSetMembers(uint64 policyId, PolicyType policyType, bool value, address[] calldata accounts) internal { + if (accounts.length > MAX_BATCH_SIZE) revert BatchSizeTooLarge(MAX_BATCH_SIZE); mapping(address => bool) storage members = MockPolicyRegistryStorage.layout().members[policyId]; for (uint256 i = 0; i < accounts.length; ++i) { members[accounts[i]] = value; diff --git a/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol b/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol index c7da850..72720d6 100644 --- a/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol +++ b/test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol @@ -119,4 +119,36 @@ contract PolicyRegistryCreatePolicyWithAccountsTest is PolicyRegistryTest { uint64 policyId = policyRegistry.createPolicyWithAccounts(admin_, pt, empty); assertTrue(policyRegistry.policyExists(policyId)); } + + /// @notice Verifies createPolicyWithAccounts reverts when the batch exceeds MAX_BATCH_SIZE + /// @dev Mirrors the Rust precompile's batch limit (base/base#2876); checks + /// BatchSizeTooLarge(maxBatchSize). Fuzz drives `overflow` so the test exercises + /// arbitrary over-the-limit sizes, not just the immediate neighbor. + function test_createPolicyWithAccounts_revert_batchSizeTooLarge( + address caller, + address admin_, + uint8 typeIdx, + uint8 overflow + ) public { + _assumeValidCaller(caller); + vm.assume(admin_ != address(0)); + IPolicyRegistry.PolicyType pt = _creatablePolicyType(typeIdx); + uint256 n = MAX_BATCH_SIZE + 1 + (uint256(overflow) % 16); + address[] memory accounts = _makeAccounts(n); + vm.expectRevert(abi.encodeWithSelector(IPolicyRegistry.BatchSizeTooLarge.selector, MAX_BATCH_SIZE)); + vm.prank(caller); + policyRegistry.createPolicyWithAccounts(admin_, pt, accounts); + } + + /// @notice Verifies createPolicyWithAccounts accepts a batch exactly at MAX_BATCH_SIZE + /// @dev Boundary check: the limit is inclusive (length == MAX_BATCH_SIZE succeeds). + function test_createPolicyWithAccounts_success_batchAtLimit(address caller, address admin_, uint8 typeIdx) public { + _assumeValidCaller(caller); + vm.assume(admin_ != address(0)); + IPolicyRegistry.PolicyType pt = _creatablePolicyType(typeIdx); + address[] memory accounts = _makeAccounts(MAX_BATCH_SIZE); + vm.prank(caller); + uint64 policyId = policyRegistry.createPolicyWithAccounts(admin_, pt, accounts); + assertTrue(policyRegistry.policyExists(policyId)); + } } diff --git a/test/unit/PolicyRegistry/updateAllowlist.t.sol b/test/unit/PolicyRegistry/updateAllowlist.t.sol index 458c89f..43b955d 100644 --- a/test/unit/PolicyRegistry/updateAllowlist.t.sol +++ b/test/unit/PolicyRegistry/updateAllowlist.t.sol @@ -127,4 +127,29 @@ contract PolicyRegistryUpdateAllowlistTest is PolicyRegistryTest { vm.prank(currentAdmin); policyRegistry.updateAllowlist(policyId, allowed, accounts); } + + /// @notice Verifies updateAllowlist reverts when the batch exceeds MAX_BATCH_SIZE + /// @dev Mirrors the Rust precompile's batch limit (base/base#2876); checks + /// BatchSizeTooLarge(maxBatchSize). Fuzz drives `overflow` so the test exercises + /// arbitrary over-the-limit sizes. + function test_updateAllowlist_revert_batchSizeTooLarge(address currentAdmin, bool allowed, uint8 overflow) public { + vm.assume(currentAdmin != address(0)); + uint64 policyId = _createAllowlist(admin, currentAdmin); + uint256 n = MAX_BATCH_SIZE + 1 + (uint256(overflow) % 16); + address[] memory accounts = _makeAccounts(n); + vm.expectRevert(abi.encodeWithSelector(IPolicyRegistry.BatchSizeTooLarge.selector, MAX_BATCH_SIZE)); + vm.prank(currentAdmin); + policyRegistry.updateAllowlist(policyId, allowed, accounts); + } + + /// @notice Verifies updateAllowlist accepts a batch exactly at MAX_BATCH_SIZE + /// @dev Boundary check: the limit is inclusive. + function test_updateAllowlist_success_batchAtLimit(address currentAdmin, bool allowed) public { + vm.assume(currentAdmin != address(0)); + uint64 policyId = _createAllowlist(admin, currentAdmin); + address[] memory accounts = _makeAccounts(MAX_BATCH_SIZE); + vm.prank(currentAdmin); + policyRegistry.updateAllowlist(policyId, allowed, accounts); + assertTrue(policyRegistry.policyExists(policyId)); + } } diff --git a/test/unit/PolicyRegistry/updateBlocklist.t.sol b/test/unit/PolicyRegistry/updateBlocklist.t.sol index d6849c2..b6d15cd 100644 --- a/test/unit/PolicyRegistry/updateBlocklist.t.sol +++ b/test/unit/PolicyRegistry/updateBlocklist.t.sol @@ -127,4 +127,29 @@ contract PolicyRegistryUpdateBlocklistTest is PolicyRegistryTest { vm.prank(currentAdmin); policyRegistry.updateBlocklist(policyId, blocked, accounts); } + + /// @notice Verifies updateBlocklist reverts when the batch exceeds MAX_BATCH_SIZE + /// @dev Mirrors the Rust precompile's batch limit (base/base#2876); checks + /// BatchSizeTooLarge(maxBatchSize). Fuzz drives `overflow` so the test exercises + /// arbitrary over-the-limit sizes. + function test_updateBlocklist_revert_batchSizeTooLarge(address currentAdmin, bool blocked, uint8 overflow) public { + vm.assume(currentAdmin != address(0)); + uint64 policyId = _createBlocklist(admin, currentAdmin); + uint256 n = MAX_BATCH_SIZE + 1 + (uint256(overflow) % 16); + address[] memory accounts = _makeAccounts(n); + vm.expectRevert(abi.encodeWithSelector(IPolicyRegistry.BatchSizeTooLarge.selector, MAX_BATCH_SIZE)); + vm.prank(currentAdmin); + policyRegistry.updateBlocklist(policyId, blocked, accounts); + } + + /// @notice Verifies updateBlocklist accepts a batch exactly at MAX_BATCH_SIZE + /// @dev Boundary check: the limit is inclusive. + function test_updateBlocklist_success_batchAtLimit(address currentAdmin, bool blocked) public { + vm.assume(currentAdmin != address(0)); + uint64 policyId = _createBlocklist(admin, currentAdmin); + address[] memory accounts = _makeAccounts(MAX_BATCH_SIZE); + vm.prank(currentAdmin); + policyRegistry.updateBlocklist(policyId, blocked, accounts); + assertTrue(policyRegistry.policyExists(policyId)); + } }