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
6 changes: 6 additions & 0 deletions src/interfaces/IPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
21 changes: 21 additions & 0 deletions test/lib/PolicyRegistryTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
8 changes: 8 additions & 0 deletions test/lib/mocks/MockPolicyRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================
Expand Down Expand Up @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions test/unit/PolicyRegistry/createPolicyWithAccounts.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
25 changes: 25 additions & 0 deletions test/unit/PolicyRegistry/updateAllowlist.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
25 changes: 25 additions & 0 deletions test/unit/PolicyRegistry/updateBlocklist.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}