|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.26; |
| 3 | + |
| 4 | +import { Test } from "forge-std/Test.sol"; |
| 5 | +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; |
| 6 | +import { InflationPool } from "../../src/rewards/InflationPool.sol"; |
| 7 | + |
| 8 | +/// @title InflationPoolDeregisterTest |
| 9 | +/// @notice Regression: `deregisterOperator` removes an operator from `trackedOperators`, |
| 10 | +/// clears its registration epoch, and lets the index shrink instead of growing |
| 11 | +/// monotonically as inactive operators accumulate. |
| 12 | +contract InflationPoolDeregisterTest is Test { |
| 13 | + InflationPool pool; |
| 14 | + address admin = makeAddr("admin"); |
| 15 | + address tnt = address(0xDEAD); |
| 16 | + |
| 17 | + function setUp() public { |
| 18 | + InflationPool impl = new InflationPool(); |
| 19 | + ERC1967Proxy proxy = new ERC1967Proxy( |
| 20 | + address(impl), |
| 21 | + abi.encodeCall(InflationPool.initialize, (admin, tnt, address(0), address(0), 7 days)) |
| 22 | + ); |
| 23 | + pool = InflationPool(payable(address(proxy))); |
| 24 | + } |
| 25 | + |
| 26 | + function _trackedCount() internal view returns (uint256 count) { |
| 27 | + // `trackedOperators` is `address[] public`; length is exposed indirectly. |
| 28 | + // Iterate by slot until we hit a revert or zero - simpler to just probe with try/catch. |
| 29 | + while (true) { |
| 30 | + try pool.trackedOperators(count) returns (address) { |
| 31 | + count++; |
| 32 | + } catch { |
| 33 | + return count; |
| 34 | + } |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + function test_DeregisterOperator_RemovesFromTrackedList() public { |
| 39 | + address[] memory ops = new address[](3); |
| 40 | + ops[0] = makeAddr("op1"); |
| 41 | + ops[1] = makeAddr("op2"); |
| 42 | + ops[2] = makeAddr("op3"); |
| 43 | + |
| 44 | + vm.prank(admin); |
| 45 | + pool.registerOperators(ops); |
| 46 | + |
| 47 | + assertEq(_trackedCount(), 3, "all three tracked"); |
| 48 | + assertTrue(pool.isTrackedOperator(ops[1])); |
| 49 | + |
| 50 | + vm.prank(admin); |
| 51 | + pool.deregisterOperator(ops[1]); |
| 52 | + |
| 53 | + assertEq(_trackedCount(), 2, "shrinks after deregister"); |
| 54 | + assertFalse(pool.isTrackedOperator(ops[1])); |
| 55 | + assertEq(pool.operatorRegistrationEpoch(ops[1]), 0, "registration epoch cleared"); |
| 56 | + } |
| 57 | + |
| 58 | + function test_DeregisterOperator_NonAdminReverts() public { |
| 59 | + address attacker = makeAddr("attacker"); |
| 60 | + vm.prank(admin); |
| 61 | + pool.registerOperator(makeAddr("op")); |
| 62 | + |
| 63 | + vm.prank(attacker); |
| 64 | + vm.expectRevert(); |
| 65 | + pool.deregisterOperator(makeAddr("op")); |
| 66 | + } |
| 67 | + |
| 68 | + function test_DeregisterOperator_AllowsReregistration() public { |
| 69 | + address op = makeAddr("op"); |
| 70 | + |
| 71 | + vm.startPrank(admin); |
| 72 | + pool.registerOperator(op); |
| 73 | + pool.deregisterOperator(op); |
| 74 | + pool.registerOperator(op); |
| 75 | + vm.stopPrank(); |
| 76 | + |
| 77 | + assertTrue(pool.isTrackedOperator(op)); |
| 78 | + assertEq(_trackedCount(), 1); |
| 79 | + } |
| 80 | +} |
0 commit comments