Skip to content

Commit f0d7001

Browse files
committed
feat(access): add on-chain enumeration via EnumerableSet
1 parent 93a645c commit f0d7001

3 files changed

Lines changed: 117 additions & 10 deletions

File tree

solidity/src/access/Allowlist.sol

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,41 @@
22
pragma solidity ^0.8.20;
33

44
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5-
import {IAllowlist} from "./IAllowlist.sol";
5+
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
6+
import {IAllowlistEnumerable} from "./IAllowlistEnumerable.sol";
67

78
/// @title Allowlist
8-
/// @notice Mapping-backed allowlist of addresses, administered by a single owner.
9+
/// @notice Enumerable allowlist of addresses, administered by a single owner.
910
/// @dev Idempotent edits: re-adding an existing entry (or removing an absent
1011
/// one) does not revert and does not emit. Matches OpenZeppelin
1112
/// `_grantRole` semantics.
12-
contract Allowlist is IAllowlist, Ownable {
13-
mapping(address account => bool) private _allowed;
13+
contract Allowlist is IAllowlistEnumerable, Ownable {
14+
using EnumerableSet for EnumerableSet.AddressSet;
15+
16+
EnumerableSet.AddressSet private _allowed;
1417

1518
/// @dev The zero address cannot be allow-listed. (Disallowing the zero address is a no-op.)
1619
error ZeroAddress();
1720

1821
constructor(address initialOwner) Ownable(initialOwner) {}
1922

20-
/// @inheritdoc IAllowlist
2123
function isAllowed(address account) external view returns (bool) {
22-
return _allowed[account];
24+
return _allowed.contains(account);
25+
}
26+
27+
/// @inheritdoc IAllowlistEnumerable
28+
function length() external view returns (uint256) {
29+
return _allowed.length();
30+
}
31+
32+
/// @inheritdoc IAllowlistEnumerable
33+
function addressAt(uint256 index) external view returns (address) {
34+
return _allowed.at(index);
35+
}
36+
37+
/// @inheritdoc IAllowlistEnumerable
38+
function values() external view returns (address[] memory) {
39+
return _allowed.values();
2340
}
2441

2542
function allow(address account) external onlyOwner {
@@ -44,15 +61,13 @@ contract Allowlist is IAllowlist, Ownable {
4461

4562
function _allow(address account) internal {
4663
if (account == address(0)) revert ZeroAddress();
47-
if (!_allowed[account]) {
48-
_allowed[account] = true;
64+
if (_allowed.add(account)) {
4965
emit AddressAllowed(account);
5066
}
5167
}
5268

5369
function _disallow(address account) internal {
54-
if (_allowed[account]) {
55-
_allowed[account] = false;
70+
if (_allowed.remove(account)) {
5671
emit AddressDisallowed(account);
5772
}
5873
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.20;
3+
4+
import {IAllowlist} from "./IAllowlist.sol";
5+
6+
/// @title IAllowlistEnumerable
7+
/// @notice Extends IAllowlist with on-chain enumeration of allowed addresses.
8+
/// @dev Optional capability — consumers that only need membership checks
9+
/// (`isAllowed`) should depend on the narrower {IAllowlist} interface.
10+
interface IAllowlistEnumerable is IAllowlist {
11+
/// @notice Returns the number of currently allowed addresses.
12+
function length() external view returns (uint256);
13+
14+
/// @notice Returns the allowed address at `index`.
15+
/// @dev Reverts on out-of-bounds. Index order is not stable across edits:
16+
/// removing an entry moves the last element into the freed slot
17+
/// (swap-and-pop), so indices should not be treated as identifiers.
18+
function addressAt(uint256 index) external view returns (address);
19+
20+
/// @notice Returns the full set of currently allowed addresses.
21+
/// @dev Gas-expensive for large sets. Prefer paginated reads via
22+
/// `length` + `addressAt` from on-chain callers. Safe off-chain via eth_call.
23+
function values() external view returns (address[] memory);
24+
}

solidity/test/Allowlist.t.sol

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,72 @@ contract AllowlistTest is Test {
239239

240240
vm.stopPrank();
241241
}
242+
243+
/// @notice length() reflects the current number of allowed addresses.
244+
function test_LengthReflectsAddsAndRemoves() public {
245+
assertEq(allowlist.length(), 0);
246+
247+
vm.startPrank(owner);
248+
allowlist.allow(alice);
249+
assertEq(allowlist.length(), 1);
250+
allowlist.allow(bob);
251+
assertEq(allowlist.length(), 2);
252+
allowlist.allow(alice); // idempotent: no length change
253+
assertEq(allowlist.length(), 2);
254+
allowlist.disallow(alice);
255+
assertEq(allowlist.length(), 1);
256+
allowlist.disallow(carol); // absent: no length change
257+
assertEq(allowlist.length(), 1);
258+
vm.stopPrank();
259+
}
260+
261+
/// @notice addressAt(index) returns an allowed entry; index in [0, length()).
262+
function test_AddressAtReturnsAllowedAddress() public {
263+
vm.startPrank(owner);
264+
allowlist.allow(alice);
265+
allowlist.allow(bob);
266+
vm.stopPrank();
267+
268+
address first = allowlist.addressAt(0);
269+
address second = allowlist.addressAt(1);
270+
// Order is implementation-defined; both must be allowed and distinct.
271+
assertTrue(first == alice || first == bob);
272+
assertTrue(second == alice || second == bob);
273+
assertTrue(first != second);
274+
}
275+
276+
/// @notice addressAt() reverts when index >= length().
277+
function test_AddressAtRevertsOnOutOfBounds() public {
278+
vm.expectRevert();
279+
allowlist.addressAt(0);
280+
281+
vm.prank(owner);
282+
allowlist.allow(alice);
283+
284+
vm.expectRevert();
285+
allowlist.addressAt(1);
286+
}
287+
288+
/// @notice values() returns the full set of allowed addresses (order unspecified).
289+
function test_ValuesReturnsFullSet() public {
290+
vm.startPrank(owner);
291+
allowlist.allow(alice);
292+
allowlist.allow(bob);
293+
allowlist.allow(carol);
294+
allowlist.disallow(bob);
295+
vm.stopPrank();
296+
297+
address[] memory all = allowlist.values();
298+
assertEq(all.length, 2);
299+
300+
bool foundAlice;
301+
bool foundCarol;
302+
for (uint256 i = 0; i < all.length; ++i) {
303+
if (all[i] == alice) foundAlice = true;
304+
if (all[i] == carol) foundCarol = true;
305+
assertTrue(all[i] != bob);
306+
}
307+
assertTrue(foundAlice);
308+
assertTrue(foundCarol);
309+
}
242310
}

0 commit comments

Comments
 (0)