diff --git a/src/EjectionManager.sol b/src/EjectionManager.sol new file mode 100644 index 00000000..d6ed1b46 --- /dev/null +++ b/src/EjectionManager.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import {IEjectionManager} from "./interfaces/IEjectionManager.sol"; +import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; +import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; + +/** + * @title Used for automated ejection of operators from the RegistryCoordinator under a ratelimit + * @author Layr Labs, Inc. + */ +contract EjectionManager is IEjectionManager, OwnableUpgradeable{ + + /// @notice The basis point denominator for the ejectable stake percent + uint16 internal constant BIPS_DENOMINATOR = 10000; + + /// @notice the RegistryCoordinator contract that is the entry point for ejection + IRegistryCoordinator public immutable registryCoordinator; + /// @notice the StakeRegistry contract that keeps track of quorum stake + IStakeRegistry public immutable stakeRegistry; + + /// @notice Address permissioned to eject operators under a ratelimit + address public ejector; + + /// @notice Keeps track of the total stake ejected for a quorum + mapping(uint8 => StakeEjection[]) public stakeEjectedForQuorum; + /// @notice Ratelimit parameters for each quorum + mapping(uint8 => QuorumEjectionParams) public quorumEjectionParams; + + constructor( + IRegistryCoordinator _registryCoordinator, + IStakeRegistry _stakeRegistry + ) { + registryCoordinator = _registryCoordinator; + stakeRegistry = _stakeRegistry; + + _disableInitializers(); + } + + /** + * @param _owner will hold the owner role + * @param _ejector will hold the ejector role + * @param _quorumEjectionParams are the ratelimit parameters for the quorum at each index + */ + function initialize( + address _owner, + address _ejector, + QuorumEjectionParams[] memory _quorumEjectionParams + ) external initializer { + _transferOwnership(_owner); + _setEjector(_ejector); + + for(uint8 i = 0; i < _quorumEjectionParams.length; i++) { + _setQuorumEjectionParams(i, _quorumEjectionParams[i]); + } + } + + /** + * @notice Ejects operators from the AVSs RegistryCoordinator under a ratelimit + * @param _operatorIds The ids of the operators 'j' to eject for each quorum 'i' + * @dev This function will eject as many operators as possible without reverting prioritizing operators at the lower index + * @dev The owner can eject operators without recording of stake ejection + */ + function ejectOperators(bytes32[][] memory _operatorIds) external { + require(msg.sender == ejector || msg.sender == owner(), "Ejector: Only owner or ejector can eject"); + + for(uint i = 0; i < _operatorIds.length; ++i) { + uint8 quorumNumber = uint8(i); + + uint256 amountEjectable = amountEjectableForQuorum(quorumNumber); + uint256 stakeForEjection; + + bool broke; + for(uint8 j = 0; j < _operatorIds[i].length; ++j) { + uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i][j], quorumNumber); + + //if caller is ejector enforce ratelimit + if( + msg.sender == ejector && + quorumEjectionParams[quorumNumber].rateLimitWindow > 0 && + stakeForEjection + operatorStake > amountEjectable + ){ + stakeEjectedForQuorum[quorumNumber].push(StakeEjection({ + timestamp: block.timestamp, + stakeEjected: stakeForEjection + })); + broke = true; + break; + } + + //try-catch used to prevent race condition of operator deregistering before ejection + try registryCoordinator.ejectOperator( + registryCoordinator.getOperatorFromId(_operatorIds[i][j]), + abi.encodePacked(quorumNumber) + ) { + stakeForEjection += operatorStake; + emit OperatorEjected(_operatorIds[i][j], quorumNumber); + } catch (bytes memory err) { + emit FailedOperatorEjection(_operatorIds[i][j], quorumNumber, err); + } + } + + //record the stake ejected if ejector and ratelimit enforced + if(!broke && msg.sender == ejector){ + stakeEjectedForQuorum[quorumNumber].push(StakeEjection({ + timestamp: block.timestamp, + stakeEjected: stakeForEjection + })); + } + + } + } + + /** + * @notice Sets the ratelimit parameters for a quorum + * @param _quorumNumber The quorum number to set the ratelimit parameters for + * @param _quorumEjectionParams The quorum ratelimit parameters to set for the given quorum + */ + function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() { + _setQuorumEjectionParams(_quorumNumber, _quorumEjectionParams); + } + + /** + * @notice Sets the address permissioned to eject operators under a ratelimit + * @param _ejector The address to permission + */ + function setEjector(address _ejector) external onlyOwner() { + _setEjector(_ejector); + } + + ///@dev internal function to set the quorum ejection params + function _setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) internal { + quorumEjectionParams[_quorumNumber] = _quorumEjectionParams; + emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.rateLimitWindow, _quorumEjectionParams.ejectableStakePercent); + } + + ///@dev internal function to set the ejector + function _setEjector(address _ejector) internal { + emit EjectorUpdated(ejector, _ejector); + ejector = _ejector; + } + + /** + * @notice Returns the amount of stake that can be ejected for a quorum at the current block.timestamp + * @param _quorumNumber The quorum number to view ejectable stake for + */ + function amountEjectableForQuorum(uint8 _quorumNumber) public view returns (uint256) { + uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow; + uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR; + uint256 totalEjected; + uint256 i; + if (stakeEjectedForQuorum[_quorumNumber].length == 0) { + return totalEjectable; + } + i = stakeEjectedForQuorum[_quorumNumber].length - 1; + + while(stakeEjectedForQuorum[_quorumNumber][i].timestamp > cutoffTime) { + totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; + if(i == 0){ + break; + } else { + --i; + } + } + + if(totalEjected >= totalEjectable){ + return 0; + } + return totalEjectable - totalEjected; + } +} diff --git a/src/interfaces/IEjectionManager.sol b/src/interfaces/IEjectionManager.sol new file mode 100644 index 00000000..94aebd0f --- /dev/null +++ b/src/interfaces/IEjectionManager.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; + +/** + * @title Interface for a contract that ejects operators from an AVSs RegistryCoordinator + * @author Layr Labs, Inc. + */ +interface IEjectionManager { + + /// @notice A quorum's ratelimit parameters + struct QuorumEjectionParams { + uint32 rateLimitWindow; // Time delta to track ejection over + uint16 ejectableStakePercent; // Max stake to be ejectable per time delta + } + + /// @notice A stake ejection event + struct StakeEjection { + uint256 timestamp; // Timestamp of the ejection + uint256 stakeEjected; // Amount of stake ejected at the timestamp + } + + ///@notice Emitted when the ejector address is set + event EjectorUpdated(address previousAddress, address newAddress); + ///@notice Emitted when the ratelimit parameters for a quorum are set + event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 rateLimitWindow, uint16 ejectableStakePercent); + ///@notice Emitted when an operator is ejected + event OperatorEjected(bytes32 operatorId, uint8 quorumNumber); + ///@notice Emitted when an operator ejection fails + event FailedOperatorEjection(bytes32 operatorId, uint8 quorumNumber, bytes err); + + /** + * @notice Ejects operators from the AVSs registryCoordinator under a ratelimit + * @param _operatorIds The ids of the operators to eject for each quorum + */ + function ejectOperators(bytes32[][] memory _operatorIds) external; + + /** + * @notice Sets the ratelimit parameters for a quorum + * @param _quorumNumber The quorum number to set the ratelimit parameters for + * @param _quorumEjectionParams The quorum ratelimit parameters to set for the given quorum + */ + function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external; + + /** + * @notice Sets the address permissioned to eject operators under a ratelimit + * @param _ejector The address to permission + */ + function setEjector(address _ejector) external; + + /** + * @notice Returns the amount of stake that can be ejected for a quorum at the current block.timestamp + * @param _quorumNumber The quorum number to view ejectable stake for + */ + function amountEjectableForQuorum(uint8 _quorumNumber) external view returns (uint256); +} diff --git a/test/unit/EjectionManagerUnit.t.sol b/test/unit/EjectionManagerUnit.t.sol new file mode 100644 index 00000000..59e5fd01 --- /dev/null +++ b/test/unit/EjectionManagerUnit.t.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {EjectionManager} from "../../src/EjectionManager.sol"; +import {IEjectionManager} from "../../src/interfaces/IEjectionManager.sol"; + +import "../utils/MockAVSDeployer.sol"; + +contract EjectionManagerUnitTests is MockAVSDeployer { + + event EjectorUpdated(address previousAddress, address newAddress); + event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 rateLimitWindow, uint16 ejectableStakePercent); + event OperatorEjected(bytes32 operatorId, uint8 quorumNumber); + event FailedOperatorEjection(bytes32 operatorId, uint8 quorumNumber, bytes err); + + EjectionManager public ejectionManager; + IEjectionManager public ejectionManagerImplementation; + + IEjectionManager.QuorumEjectionParams[] public quorumEjectionParams; + + uint32 public ratelimitWindow = 1 days; + uint16 public ejectableStakePercent = 1000; + + function setUp() virtual public { + for(uint8 i = 0; i < numQuorums; i++) { + quorumEjectionParams.push(IEjectionManager.QuorumEjectionParams({ + rateLimitWindow: ratelimitWindow, + ejectableStakePercent: ejectableStakePercent + })); + } + + defaultMaxOperatorCount = 200; + _deployMockEigenLayerAndAVS(); + + ejectionManager = EjectionManager(address( + new TransparentUpgradeableProxy( + address(emptyContract), + address(proxyAdmin), + "" + ) + )); + + ejectionManagerImplementation = new EjectionManager(registryCoordinator, stakeRegistry); + + cheats.prank(proxyAdminOwner); + proxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(ejectionManager))), + address(ejectionManagerImplementation), + abi.encodeWithSelector( + EjectionManager.initialize.selector, + registryCoordinatorOwner, + ejector, + quorumEjectionParams + ) + ); + + cheats.prank(registryCoordinatorOwner); + registryCoordinator.setEjector(address(ejectionManager)); + + cheats.warp(block.timestamp + ratelimitWindow); + } + + function testEjectOperators_OneOperatorInsideRatelimit() public { + uint8 operatorsToEject = 1; + uint8 numOperators = 10; + uint96 stake = 1 ether; + _registerOperaters(numOperators, stake); + + bytes32[][] memory operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, j)); + } + } + + assertEq(uint8(registryCoordinator.getOperatorStatus(defaultOperator)), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + + for(uint8 i = 0; i < numQuorums; i++) { + for(uint8 j = 0; j < operatorsToEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(ejector); + ejectionManager.ejectOperators(operatorIds); + + assertEq(uint8(registryCoordinator.getOperatorStatus(defaultOperator)), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + + function testEjectOperators_MultipleOperatorInsideRatelimit() public { + uint8 operatorsToEject = 10; + uint8 numOperators = 100; + uint96 stake = 1 ether; + _registerOperaters(numOperators, stake); + + bytes32[][] memory operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, j)); + } + } + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + + for(uint8 i = 0; i < numQuorums; i++) { + for(uint8 j = 0; j < operatorsToEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(ejector); + ejectionManager.ejectOperators(operatorIds); + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + } + + function testEjectOperators_MultipleOperatorOutsideRatelimit() public { + uint8 operatorsCanEject = 1; + uint8 operatorsToEject = 10; + uint8 numOperators = 10; + uint96 stake = 1 ether; + _registerOperaters(numOperators, stake); + + bytes32[][] memory operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, j)); + } + } + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + + for(uint8 i = 0; i < numQuorums; i++) { + for(uint8 j = 0; j < operatorsCanEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(ejector); + ejectionManager.ejectOperators(operatorIds); + + for(uint8 i = 0; i < operatorsCanEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + + for(uint8 i = operatorsCanEject; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + } + + function testEjectOperators_MultipleOperatorMultipleTimesInsideRatelimit() public { + uint8 operatorsToEject = 4; + uint8 numOperators = 100; + uint96 stake = 1 ether; + _registerOperaters(numOperators, stake); + + bytes32[][] memory operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, j)); + } + } + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + + for(uint8 i = 0; i < numQuorums; i++) { + for(uint8 j = 0; j < operatorsToEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(ejector); + ejectionManager.ejectOperators(operatorIds); + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + + cheats.warp(block.timestamp + (ratelimitWindow / 2)); + + operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, operatorsToEject + j)); + } + } + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, operatorsToEject + i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + + for(uint8 i = 0; i < numQuorums; i++) { + for(uint8 j = 0; j < operatorsToEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(ejector); + ejectionManager.ejectOperators(operatorIds); + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, operatorsToEject + i))), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + } + + function testEjectOperators_MultipleOperatorAfterRatelimitReset() public { + uint8 operatorsToEject = 10; + uint8 numOperators = 100; + uint96 stake = 1 ether; + + testEjectOperators_MultipleOperatorInsideRatelimit(); + + _registerOperaters(operatorsToEject, stake); + + vm.warp(block.timestamp + ratelimitWindow); + + bytes32[][] memory operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, j)); + } + } + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + + for(uint8 i = 0; i < numQuorums; i++) { + for(uint8 j = 0; j < operatorsToEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(ejector); + ejectionManager.ejectOperators(operatorIds); + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + } + + function testEjectOperators_NoRatelimitForOwner() public { + uint8 operatorsToEject = 100; + uint8 numOperators = 100; + uint96 stake = 1 ether; + _registerOperaters(numOperators, stake); + + bytes32[][] memory operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, j)); + } + } + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + + for(uint8 i = 0; i < numQuorums; i++) { + for(uint8 j = 0; j < operatorsToEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(registryCoordinatorOwner); + ejectionManager.ejectOperators(operatorIds); + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + } + + function testEjectOperators_NoRevertOnMissedEjection() public { + uint8 operatorsToEject = 10; + uint8 numOperators = 100; + uint96 stake = 1 ether; + _registerOperaters(numOperators, stake); + + bytes32[][] memory operatorIds = new bytes32[][](numQuorums); + for (uint8 i = 0; i < numQuorums; i++) { + operatorIds[i] = new bytes32[](operatorsToEject); + for (uint j = 0; j < operatorsToEject; j++) { + operatorIds[i][j] = registryCoordinator.getOperatorId(_incrementAddress(defaultOperator, j)); + } + } + + cheats.prank(defaultOperator); + registryCoordinator.deregisterOperator(BitmapUtils.bitmapToBytesArray(MAX_QUORUM_BITMAP)); + + for(uint8 i = 1; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.REGISTERED)); + } + + for(uint8 i = 0; i < numQuorums; i++) { + //cheats.expectEmit(true, true, true, true, address(ejectionManager)); + //emit FailedOperatorEjection(operatorIds[i][0], i, abi.encodePacked("revert: RegistryCoordinator._deregisterOperator: operator is not registered")); + for(uint8 j = 1; j < operatorsToEject; j++) { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit OperatorEjected(operatorIds[i][j], i); + } + } + + cheats.prank(ejector); + ejectionManager.ejectOperators(operatorIds); + + for(uint8 i = 0; i < operatorsToEject; i++) { + assertEq(uint8(registryCoordinator.getOperatorStatus(_incrementAddress(defaultOperator, i))), uint8(IRegistryCoordinator.OperatorStatus.DEREGISTERED)); + } + } + + function testSetQuorumEjectionParams() public { + uint8 quorumNumber = 0; + ratelimitWindow = 2 days; + ejectableStakePercent = 2000; + IEjectionManager.QuorumEjectionParams memory _quorumEjectionParams = IEjectionManager.QuorumEjectionParams({ + rateLimitWindow: ratelimitWindow, + ejectableStakePercent: ejectableStakePercent + }); + + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit QuorumEjectionParamsSet(quorumNumber, ratelimitWindow, ejectableStakePercent); + + cheats.prank(registryCoordinatorOwner); + ejectionManager.setQuorumEjectionParams(quorumNumber, _quorumEjectionParams); + + (uint32 setRatelimitWindow, uint16 setEjectableStakePercent) = ejectionManager.quorumEjectionParams(quorumNumber); + assertEq(setRatelimitWindow, _quorumEjectionParams.rateLimitWindow); + assertEq(setEjectableStakePercent, _quorumEjectionParams.ejectableStakePercent); + } + + function testSetEjector() public { + cheats.expectEmit(true, true, true, true, address(ejectionManager)); + emit EjectorUpdated(ejector, address(0)); + + cheats.prank(registryCoordinatorOwner); + ejectionManager.setEjector(address(0)); + + assertEq(ejectionManager.ejector(), address(0)); + } + + function test_Revert_NotPermissioned() public { + bytes32[][] memory operatorIds; + cheats.expectRevert("Ejector: Only owner or ejector can eject"); + ejectionManager.ejectOperators(operatorIds); + + EjectionManager.QuorumEjectionParams memory _quorumEjectionParams; + cheats.expectRevert("Ownable: caller is not the owner"); + ejectionManager.setQuorumEjectionParams(0, _quorumEjectionParams); + + cheats.expectRevert("Ownable: caller is not the owner"); + ejectionManager.setEjector(address(0)); + } + + function _registerOperaters(uint8 numOperators, uint96 stake) internal { + for (uint i = 0; i < numOperators; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(i))); + address operator = _incrementAddress(defaultOperator, i); + _registerOperatorWithCoordinator(operator, MAX_QUORUM_BITMAP, pubKey, stake); + } + } +} \ No newline at end of file