|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; |
| 5 | + |
| 6 | +/** |
| 7 | + * @title AddressSetBatchLib |
| 8 | + * @notice The batch add/remove mechanics shared by every address-list rule in this library. |
| 9 | + * @dev Extracted because the same two loops were written three times: once in |
| 10 | + * {RuleAddressSetInternal} and twice in `RuleERC2980Internal`, for its whitelist and its frozenlist. |
| 11 | + * The copies had already drifted -- only the `RuleAddressSetInternal` one was covered by a test for |
| 12 | + * the zero-address rejection (`FEEDBACK_12.md` D-1, F-5). |
| 13 | + * |
| 14 | + * Only the two *loops* live here. `add` / `remove` / `contains` / `length` on a single address stay |
| 15 | + * in the inheriting contracts as one-line delegations to {EnumerableSet}: routing those through a |
| 16 | + * library would add a layer of indirection without removing any real duplication. |
| 17 | + * |
| 18 | + * ## Why the zero-address guard is a function parameter |
| 19 | + * Each rule reverts with its OWN custom error (`RuleAddressSet_ZeroAddressNotAllowed`, |
| 20 | + * `RuleERC2980_ZeroAddressNotAllowed`), matching the codebase-wide "one error namespace per rule" |
| 21 | + * convention. A shared library cannot name those errors, and the alternatives are worse: |
| 22 | + * |
| 23 | + * - Reverting with a single shared error would change the revert data callers and tests already |
| 24 | + * depend on, and break the per-rule error convention. |
| 25 | + * - Returning a "a zero was found" flag for the caller to check would make the guard optional in |
| 26 | + * practice: a caller that forgot the check would silently list `address(0)`, which is the exact |
| 27 | + * outcome the guard exists to prevent. |
| 28 | + * |
| 29 | + * Passing the guard as an `internal pure` function pointer keeps it MANDATORY -- it is a required |
| 30 | + * parameter, so the call does not compile without one -- while each rule keeps its own error. The |
| 31 | + * pointer is resolved at compile time and the library is `internal`, so this is a jump inside the |
| 32 | + * calling contract, not a `DELEGATECALL`. |
| 33 | + */ |
| 34 | +library AddressSetBatchLib { |
| 35 | + using EnumerableSet for EnumerableSet.AddressSet; |
| 36 | + |
| 37 | + /** |
| 38 | + * @notice Adds every address in `addressesToAdd` to `set`, skipping entries already present. |
| 39 | + * @dev Duplicates are skipped and counted rather than rejected: an idempotent no-op that the |
| 40 | + * caller's batch event still describes truthfully. `address(0)` is NOT skipped -- `guard` is |
| 41 | + * invoked for every entry and is expected to revert on it, rejecting the whole batch. Silently |
| 42 | + * dropping the sentinel would make the caller's `Add*` event, which echoes the input array, |
| 43 | + * report a member that is not in the set. |
| 44 | + * @param set The address set to modify. |
| 45 | + * @param addressesToAdd The addresses to add. |
| 46 | + * @param guard Per-entry validation supplied by the calling rule; reverts with that rule's own |
| 47 | + * error. Invoked before the entry is inserted. |
| 48 | + * @return added The number of addresses newly inserted. |
| 49 | + * @return skipped The number of addresses already present. |
| 50 | + */ |
| 51 | + function addBatch( |
| 52 | + EnumerableSet.AddressSet storage set, |
| 53 | + address[] calldata addressesToAdd, |
| 54 | + function(address) internal pure guard |
| 55 | + ) internal returns (uint256 added, uint256 skipped) { |
| 56 | + for (uint256 i = 0; i < addressesToAdd.length; ++i) { |
| 57 | + guard(addressesToAdd[i]); |
| 58 | + if (set.add(addressesToAdd[i])) { |
| 59 | + added += 1; |
| 60 | + } else { |
| 61 | + skipped += 1; |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * @notice Removes every address in `addressesToRemove` from `set`, skipping absent entries. |
| 68 | + * @dev No guard: removal has no invalid input. Removing an address that is not present is an |
| 69 | + * idempotent no-op, counted in `skipped`. |
| 70 | + * @param set The address set to modify. |
| 71 | + * @param addressesToRemove The addresses to remove. |
| 72 | + * @return removed The number of addresses actually removed. |
| 73 | + * @return skipped The number of addresses that were not present. |
| 74 | + */ |
| 75 | + function removeBatch(EnumerableSet.AddressSet storage set, address[] calldata addressesToRemove) |
| 76 | + internal |
| 77 | + returns (uint256 removed, uint256 skipped) |
| 78 | + { |
| 79 | + for (uint256 i = 0; i < addressesToRemove.length; ++i) { |
| 80 | + if (set.remove(addressesToRemove[i])) { |
| 81 | + removed += 1; |
| 82 | + } else { |
| 83 | + skipped += 1; |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | +} |
0 commit comments