From befc8b31d47aa38ebd3f5eb934316cf711992cf4 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Sun, 14 Jan 2024 20:00:04 -0600 Subject: [PATCH 01/12] feat: ejector --- src/Ejector.sol | 99 +++++++++++++++++++++++++++++++++++++ src/interfaces/IEjector.sol | 42 ++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/Ejector.sol create mode 100644 src/interfaces/IEjector.sol diff --git a/src/Ejector.sol b/src/Ejector.sol new file mode 100644 index 00000000..13213602 --- /dev/null +++ b/src/Ejector.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.9; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IEjector} from "./interfaces/IEjector.sol"; +import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; +import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; +import {BitmapUtils} from "./libraries/BitmapUtils.sol"; + +/** + * @title Used for automated ejection of operators from the registryCoordinator + * @author Layr Labs, Inc. + */ +contract Ejector is IEjector, Ownable{ + + IRegistryCoordinator public immutable registryCoordinator; + 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 within a time delta + mapping(uint8 => mapping(uint256 => uint256)) public stakeEjectedForQuorumInDelta; + /// @notice Ratelimit parameters for each quorum + mapping(uint8 => QuorumEjectionParams) public quorumEjectionParams; + + constructor( + IRegistryCoordinator _registryCoordinator, + IStakeRegistry _stakeRegistry, + address _owner, + address _ejector, + QuorumEjectionParams[] memory _quorumEjectionParams + ) { + registryCoordinator = _registryCoordinator; + stakeRegistry = _stakeRegistry; + _setEjector(_ejector); + _transferOwnership(_owner); + + for(uint8 i = 0; i < _quorumEjectionParams.length; i++) { + quorumEjectionParams[i] = _quorumEjectionParams[i]; + } + } + + /** + * @notice Ejects operators from the AVSs registryCoordinator + * @param _operatorIds The addresses of the operators to eject + * @param _quorumBitmaps The quorum bitmaps for each respective operator + */ + function ejectOperators(bytes32[] memory _operatorIds, uint256[] memory _quorumBitmaps) external { + require(msg.sender == ejector || msg.sender == owner(), "Ejector: Only owner or ejector can eject"); + require(_operatorIds.length == _quorumBitmaps.length, "Ejector: _operatorIds and _quorumBitmaps must be same length"); + + for(uint i = 0; i < _operatorIds.length; i++) { + bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(_quorumBitmaps[i]); + + for(uint8 j = 0; j < quorumNumbers.length; j++) { + uint8 quorumNumber = uint8(quorumNumbers[j]); + uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i], quorumNumber); + + uint256 timeBlock = block.timestamp % quorumEjectionParams[quorumNumber].timeDelta; + if( + msg.sender == ejector && + stakeEjectedForQuorumInDelta[quorumNumber][timeBlock] + operatorStake > quorumEjectionParams[quorumNumber].maxStakePerDelta + ){ + revert("Ejector: Operator stake exceeds max stake per delta"); + } + stakeEjectedForQuorumInDelta[quorumNumber][timeBlock] += operatorStake; + } + + registryCoordinator.ejectOperator( + registryCoordinator.getOperatorFromId(_operatorIds[i]), + quorumNumbers + ); + } + } + + /** + * @notice Sets the ratelimit parameters for a quorum + * @param _quorumNumber The quorum number to set the ratelimit parameters for + * @param _quorumEjectionParams The quorum bitmaps for each respective operator + */ + function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() { + quorumEjectionParams[_quorumNumber] = _quorumEjectionParams; + } + + /** + * @notice Sets the address permissioned to eject operators + * @param _ejector The address to permission + */ + function setEjector(address _ejector) external onlyOwner() { + _setEjector(_ejector); + } + + function _setEjector(address _ejector) internal { + emit EjectorChanged(ejector, _ejector); + ejector = _ejector; + } + +} diff --git a/src/interfaces/IEjector.sol b/src/interfaces/IEjector.sol new file mode 100644 index 00000000..b2780a58 --- /dev/null +++ b/src/interfaces/IEjector.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +import {IRegistryCoordinator} from "./IRegistryCoordinator.sol"; +import {IStakeRegistry} from "./IStakeRegistry.sol"; + +/** + * @title Interface for a contract that ejects operators from an AVS + * @author Layr Labs, Inc. + */ +interface IEjector { + + /// @notice A quorum's ratelimit parameters + struct QuorumEjectionParams { + uint256 timeDelta; // Time delta to track ejection over + uint256 maxStakePerDelta; // Max stake to be ejectable per time delta + } + + ///@notice Emitted when the ejector address is set + event EjectorChanged(address previousAddress, address newAddress); + + /** + * @notice Ejects operators from the AVSs registryCoordinator + * @param _operatorIds The addresses of the operators to eject + * @param _quorumBitmaps The quorum bitmaps for each respective operator + */ + function ejectOperators(bytes32[] memory _operatorIds, uint256[] memory _quorumBitmaps) external; + + /** + * @notice Sets the ratelimit parameters for a quorum + * @param _quorumNumber The quorum number to set the ratelimit parameters for + * @param _quorumEjectionParams The quorum bitmaps for each respective operator + */ + function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external; + + /** + * @notice Sets the address permissioned to eject operators + * @param _ejector The address to permission + */ + function setEjector(address _ejector) external; + +} From dd9911d6b08e33d9236c28a079fb29d224595c4f Mon Sep 17 00:00:00 2001 From: QUAQ Date: Mon, 15 Jan 2024 11:25:02 -0600 Subject: [PATCH 02/12] improvments - trailing window - try/catch ejection --- src/Ejector.sol | 81 ++++++++++++++++++++++++++++--------- src/interfaces/IEjector.sol | 23 ++++++++++- 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/Ejector.sol b/src/Ejector.sol index 13213602..28dc9a55 100644 --- a/src/Ejector.sol +++ b/src/Ejector.sol @@ -8,7 +8,7 @@ import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; import {BitmapUtils} from "./libraries/BitmapUtils.sol"; /** - * @title Used for automated ejection of operators from the registryCoordinator + * @title Used for automated ejection of operators from the RegistryCoordinator * @author Layr Labs, Inc. */ contract Ejector is IEjector, Ownable{ @@ -20,21 +20,21 @@ contract Ejector is IEjector, Ownable{ address public ejector; /// @notice Keeps track of the total stake ejected for a quorum within a time delta - mapping(uint8 => mapping(uint256 => uint256)) public stakeEjectedForQuorumInDelta; + mapping(uint8 => StakeEjection[]) public stakeEjectedForQuorum; /// @notice Ratelimit parameters for each quorum mapping(uint8 => QuorumEjectionParams) public quorumEjectionParams; constructor( - IRegistryCoordinator _registryCoordinator, - IStakeRegistry _stakeRegistry, address _owner, address _ejector, + IRegistryCoordinator _registryCoordinator, + IStakeRegistry _stakeRegistry, QuorumEjectionParams[] memory _quorumEjectionParams ) { registryCoordinator = _registryCoordinator; stakeRegistry = _stakeRegistry; - _setEjector(_ejector); _transferOwnership(_owner); + _setEjector(_ejector); for(uint8 i = 0; i < _quorumEjectionParams.length; i++) { quorumEjectionParams[i] = _quorumEjectionParams[i]; @@ -50,27 +50,34 @@ contract Ejector is IEjector, Ownable{ require(msg.sender == ejector || msg.sender == owner(), "Ejector: Only owner or ejector can eject"); require(_operatorIds.length == _quorumBitmaps.length, "Ejector: _operatorIds and _quorumBitmaps must be same length"); - for(uint i = 0; i < _operatorIds.length; i++) { + for(uint i = 0; i < _operatorIds.length; ++i) { bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(_quorumBitmaps[i]); - for(uint8 j = 0; j < quorumNumbers.length; j++) { + for(uint8 j = 0; j < quorumNumbers.length; ++j) { uint8 quorumNumber = uint8(quorumNumbers[j]); uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i], quorumNumber); - uint256 timeBlock = block.timestamp % quorumEjectionParams[quorumNumber].timeDelta; - if( - msg.sender == ejector && - stakeEjectedForQuorumInDelta[quorumNumber][timeBlock] + operatorStake > quorumEjectionParams[quorumNumber].maxStakePerDelta - ){ - revert("Ejector: Operator stake exceeds max stake per delta"); - } - stakeEjectedForQuorumInDelta[quorumNumber][timeBlock] += operatorStake; + if(msg.sender == ejector){ + require(canEject(operatorStake, quorumNumber), "Ejector: Stake exceeds quorum ejection ratelimit"); + } + + stakeEjectedForQuorum[quorumNumber].push(StakeEjection({ + timestamp: block.timestamp, + stakeEjected: operatorStake + })); } - registryCoordinator.ejectOperator( + try registryCoordinator.ejectOperator( registryCoordinator.getOperatorFromId(_operatorIds[i]), quorumNumbers - ); + ) { + emit OperatorEjected(_operatorIds[i], _quorumBitmaps[i]); + } catch (bytes memory err) { + for(uint8 j = 0; j < quorumNumbers.length; ++j) { + stakeEjectedForQuorum[uint8(quorumNumbers[j])].pop(); + } + emit FailedOperatorEjection(_operatorIds[i], _quorumBitmaps[i], err); + } } } @@ -81,6 +88,7 @@ contract Ejector is IEjector, Ownable{ */ function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() { quorumEjectionParams[_quorumNumber] = _quorumEjectionParams; + emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.timeDelta, _quorumEjectionParams.maxStakePerDelta); } /** @@ -91,9 +99,44 @@ contract Ejector is IEjector, Ownable{ _setEjector(_ejector); } + ///@dev internal function to set the ejector function _setEjector(address _ejector) internal { - emit EjectorChanged(ejector, _ejector); + emit EjectorUpdated(ejector, _ejector); ejector = _ejector; } - + + /** + * @dev Cleans up old ejections for a quorums StakeEjection array + * @param _quorumNumber The addresses of the operators to eject + */ + function _cleanOldEjections(uint8 _quorumNumber) internal { + uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].timeDelta; + uint256 index = 0; + StakeEjection[] storage stakeEjections = stakeEjectedForQuorum[_quorumNumber]; + while (index < stakeEjections.length && stakeEjections[index].timestamp < cutoffTime) { + index++; + } + if (index > 0) { + for (uint256 i = index; i < stakeEjections.length; ++i) { + stakeEjections[i - index] = stakeEjections[i]; + } + for (uint256 i = 0; i < index; ++i) { + stakeEjections.pop(); + } + } + } + + /** + * @notice Checks if an amount of stake can be ejected for a quorum with ratelimit + * @param _amount The amount of stake to eject + * @param _quorumNumber The quorum number to eject for + */ + function canEject(uint256 _amount, uint8 _quorumNumber) public view returns (bool) { + uint256 totalEjected = 0; + for (uint256 i = 0; i < stakeEjectedForQuorum[_quorumNumber].length; i++) { + totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; + } + return totalEjected + _amount <= quorumEjectionParams[_quorumNumber].maxStakePerDelta; + } + } diff --git a/src/interfaces/IEjector.sol b/src/interfaces/IEjector.sol index b2780a58..432d78d0 100644 --- a/src/interfaces/IEjector.sol +++ b/src/interfaces/IEjector.sol @@ -5,7 +5,7 @@ import {IRegistryCoordinator} from "./IRegistryCoordinator.sol"; import {IStakeRegistry} from "./IStakeRegistry.sol"; /** - * @title Interface for a contract that ejects operators from an AVS + * @title Interface for a contract that ejects operators from an AVSs RegistryCoordinator * @author Layr Labs, Inc. */ interface IEjector { @@ -16,8 +16,20 @@ interface IEjector { uint256 maxStakePerDelta; // 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 EjectorChanged(address previousAddress, address newAddress); + event EjectorUpdated(address previousAddress, address newAddress); + ///@notice Emitted when an operator is ejected + event OperatorEjected(bytes32 operatorId, uint256 quorumBitmap); + ///@notice Emitted when an operator ejection fails + event FailedOperatorEjection(bytes32 operatorId, uint256 quorumBitmap, bytes err); + ///@notice Emitted when the ratelimit parameters for a quorum are set + event QuorumEjectionParamsSet(uint8 quorumNumber, uint256 timeDelta, uint256 maxStakePerDelta); /** * @notice Ejects operators from the AVSs registryCoordinator @@ -38,5 +50,12 @@ interface IEjector { * @param _ejector The address to permission */ function setEjector(address _ejector) external; + + /** + * @notice Checks if an amount of stake can be ejected for a quorum with ratelimit + * @param _amount The amount of stake to eject + * @param _quorumNumber The quorum number to eject for + */ + function canEject(uint256 _amount, uint8 _quorumNumber) external view returns (bool); } From b2d6f7c6e8ad3d8259f6c068112e9d8d78fc0432 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Mon, 15 Jan 2024 12:32:07 -0600 Subject: [PATCH 03/12] percentage --- src/Ejector.sol | 9 +++++++-- src/interfaces/IEjector.sol | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Ejector.sol b/src/Ejector.sol index 28dc9a55..030e268b 100644 --- a/src/Ejector.sol +++ b/src/Ejector.sol @@ -13,6 +13,9 @@ import {BitmapUtils} from "./libraries/BitmapUtils.sol"; */ contract Ejector is IEjector, Ownable{ + /// @notice The basis point denominator for the ejectable stake percent + uint16 internal constant BIPS_DENOMINATOR = 10000; + IRegistryCoordinator public immutable registryCoordinator; IStakeRegistry public immutable stakeRegistry; @@ -58,6 +61,7 @@ contract Ejector is IEjector, Ownable{ uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i], quorumNumber); if(msg.sender == ejector){ + _cleanOldEjections(quorumNumber); require(canEject(operatorStake, quorumNumber), "Ejector: Stake exceeds quorum ejection ratelimit"); } @@ -88,7 +92,7 @@ contract Ejector is IEjector, Ownable{ */ function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() { quorumEjectionParams[_quorumNumber] = _quorumEjectionParams; - emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.timeDelta, _quorumEjectionParams.maxStakePerDelta); + emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.timeDelta, _quorumEjectionParams.ejectableStakePercent); } /** @@ -136,7 +140,8 @@ contract Ejector is IEjector, Ownable{ for (uint256 i = 0; i < stakeEjectedForQuorum[_quorumNumber].length; i++) { totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; } - return totalEjected + _amount <= quorumEjectionParams[_quorumNumber].maxStakePerDelta; + uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR; + return totalEjected + _amount <= totalEjectable; } } diff --git a/src/interfaces/IEjector.sol b/src/interfaces/IEjector.sol index 432d78d0..ce0a107d 100644 --- a/src/interfaces/IEjector.sol +++ b/src/interfaces/IEjector.sol @@ -12,8 +12,8 @@ interface IEjector { /// @notice A quorum's ratelimit parameters struct QuorumEjectionParams { - uint256 timeDelta; // Time delta to track ejection over - uint256 maxStakePerDelta; // Max stake to be ejectable per time delta + uint32 timeDelta; // Time delta to track ejection over + uint16 ejectableStakePercent; // Max stake to be ejectable per time delta } /// @notice A stake ejection event @@ -29,7 +29,7 @@ interface IEjector { ///@notice Emitted when an operator ejection fails event FailedOperatorEjection(bytes32 operatorId, uint256 quorumBitmap, bytes err); ///@notice Emitted when the ratelimit parameters for a quorum are set - event QuorumEjectionParamsSet(uint8 quorumNumber, uint256 timeDelta, uint256 maxStakePerDelta); + event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 timeDelta, uint16 ejectableStakePercent); /** * @notice Ejects operators from the AVSs registryCoordinator From ee49669cfc06b2ecb3c758dfee83414303eacfb6 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Mon, 15 Jan 2024 12:40:53 -0600 Subject: [PATCH 04/12] fix --- src/Ejector.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ejector.sol b/src/Ejector.sol index 030e268b..8beae3c6 100644 --- a/src/Ejector.sol +++ b/src/Ejector.sol @@ -58,6 +58,8 @@ contract Ejector is IEjector, Ownable{ for(uint8 j = 0; j < quorumNumbers.length; ++j) { uint8 quorumNumber = uint8(quorumNumbers[j]); + require(quorumEjectionParams[quorumNumber].timeDelta > 0, "Ejector: Quorum ejection params not set"); + uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i], quorumNumber); if(msg.sender == ejector){ From f991bf92d548d3779bc27924f2be167349bc3dea Mon Sep 17 00:00:00 2001 From: QUAQ Date: Thu, 18 Jan 2024 19:27:19 -0600 Subject: [PATCH 05/12] nits --- src/{Ejector.sol => EjectionManager.sol} | 26 ++++++++++++------- .../{IEjector.sol => IEjectionManager.sol} | 6 ++--- 2 files changed, 19 insertions(+), 13 deletions(-) rename src/{Ejector.sol => EjectionManager.sol} (90%) rename src/interfaces/{IEjector.sol => IEjectionManager.sol} (91%) diff --git a/src/Ejector.sol b/src/EjectionManager.sol similarity index 90% rename from src/Ejector.sol rename to src/EjectionManager.sol index 8beae3c6..f585e740 100644 --- a/src/Ejector.sol +++ b/src/EjectionManager.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.9; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IEjector} from "./interfaces/IEjector.sol"; +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"; import {BitmapUtils} from "./libraries/BitmapUtils.sol"; @@ -11,7 +11,7 @@ import {BitmapUtils} from "./libraries/BitmapUtils.sol"; * @title Used for automated ejection of operators from the RegistryCoordinator * @author Layr Labs, Inc. */ -contract Ejector is IEjector, Ownable{ +contract EjectionManager is IEjectionManager, OwnableUpgradeable{ /// @notice The basis point denominator for the ejectable stake percent uint16 internal constant BIPS_DENOMINATOR = 10000; @@ -28,14 +28,20 @@ contract Ejector is IEjector, Ownable{ mapping(uint8 => QuorumEjectionParams) public quorumEjectionParams; constructor( - address _owner, - address _ejector, IRegistryCoordinator _registryCoordinator, - IStakeRegistry _stakeRegistry, - QuorumEjectionParams[] memory _quorumEjectionParams + IStakeRegistry _stakeRegistry ) { registryCoordinator = _registryCoordinator; stakeRegistry = _stakeRegistry; + + _disableInitializers(); + } + + function initialize( + address _owner, + address _ejector, + QuorumEjectionParams[] memory _quorumEjectionParams + ) external initializer { _transferOwnership(_owner); _setEjector(_ejector); @@ -58,7 +64,7 @@ contract Ejector is IEjector, Ownable{ for(uint8 j = 0; j < quorumNumbers.length; ++j) { uint8 quorumNumber = uint8(quorumNumbers[j]); - require(quorumEjectionParams[quorumNumber].timeDelta > 0, "Ejector: Quorum ejection params not set"); + require(quorumEjectionParams[quorumNumber].rateLimitWindow > 0, "Ejector: Quorum ejection params not set"); uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i], quorumNumber); @@ -94,7 +100,7 @@ contract Ejector is IEjector, Ownable{ */ function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() { quorumEjectionParams[_quorumNumber] = _quorumEjectionParams; - emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.timeDelta, _quorumEjectionParams.ejectableStakePercent); + emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.rateLimitWindow, _quorumEjectionParams.ejectableStakePercent); } /** @@ -116,7 +122,7 @@ contract Ejector is IEjector, Ownable{ * @param _quorumNumber The addresses of the operators to eject */ function _cleanOldEjections(uint8 _quorumNumber) internal { - uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].timeDelta; + uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow; uint256 index = 0; StakeEjection[] storage stakeEjections = stakeEjectedForQuorum[_quorumNumber]; while (index < stakeEjections.length && stakeEjections[index].timestamp < cutoffTime) { diff --git a/src/interfaces/IEjector.sol b/src/interfaces/IEjectionManager.sol similarity index 91% rename from src/interfaces/IEjector.sol rename to src/interfaces/IEjectionManager.sol index ce0a107d..4220346c 100644 --- a/src/interfaces/IEjector.sol +++ b/src/interfaces/IEjectionManager.sol @@ -8,11 +8,11 @@ import {IStakeRegistry} from "./IStakeRegistry.sol"; * @title Interface for a contract that ejects operators from an AVSs RegistryCoordinator * @author Layr Labs, Inc. */ -interface IEjector { +interface IEjectionManager { /// @notice A quorum's ratelimit parameters struct QuorumEjectionParams { - uint32 timeDelta; // Time delta to track ejection over + uint32 rateLimitWindow; // Time delta to track ejection over uint16 ejectableStakePercent; // Max stake to be ejectable per time delta } @@ -29,7 +29,7 @@ interface IEjector { ///@notice Emitted when an operator ejection fails event FailedOperatorEjection(bytes32 operatorId, uint256 quorumBitmap, bytes err); ///@notice Emitted when the ratelimit parameters for a quorum are set - event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 timeDelta, uint16 ejectableStakePercent); + event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 rateLimitWindow, uint16 ejectableStakePercent); /** * @notice Ejects operators from the AVSs registryCoordinator From b21373e6982e6cb11692926f97befabc0a9fe918 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Mon, 4 Mar 2024 16:40:27 -0600 Subject: [PATCH 06/12] feat: non signing metric helpers - adds operatorId on reg and dereg events - adds function to OperatorStateRetriever to get bitmaps for a set of operators at a timestamp --- src/BLSApkRegistry.sol | 4 ++-- src/OperatorStateRetriever.sol | 13 ++++++++++++ src/interfaces/IBLSApkRegistry.sol | 2 ++ test/events/IBLSApkRegistryEvents.sol | 2 ++ test/unit/BLSApkRegistryUnit.t.sol | 12 +++++++---- test/unit/RegistryCoordinatorUnit.t.sol | 28 +++++++++++++------------ 6 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/BLSApkRegistry.sol b/src/BLSApkRegistry.sol index 9d145ddd..a6142c36 100644 --- a/src/BLSApkRegistry.sol +++ b/src/BLSApkRegistry.sol @@ -50,7 +50,7 @@ contract BLSApkRegistry is BLSApkRegistryStorage { _processQuorumApkUpdate(quorumNumbers, pubkey); // Return pubkeyHash, which will become the operator's unique id - emit OperatorAddedToQuorums(operator, quorumNumbers); + emit OperatorAddedToQuorums(operator, getOperatorId(operator), quorumNumbers); } /** @@ -74,7 +74,7 @@ contract BLSApkRegistry is BLSApkRegistryStorage { // Update each quorum's aggregate pubkey _processQuorumApkUpdate(quorumNumbers, pubkey.negate()); - emit OperatorRemovedFromQuorums(operator, quorumNumbers); + emit OperatorRemovedFromQuorums(operator, getOperatorId(operator), quorumNumbers); } /** diff --git a/src/OperatorStateRetriever.sol b/src/OperatorStateRetriever.sol index 590814bc..1dd7398d 100644 --- a/src/OperatorStateRetriever.sol +++ b/src/OperatorStateRetriever.sol @@ -159,4 +159,17 @@ contract OperatorStateRetriever { return checkSignaturesIndices; } + + function getQuorumBitmapsAtBlockNumber( + IRegistryCoordinator registryCoordinator, + bytes32[] memory operatorIds, + uint32 blockNumber + ) external view returns (uint256[] memory) { + uint32[] memory quorumBitmapIndices = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + uint256[] memory quorumBitmaps = new uint256[](operatorIds.length); + for (uint256 i = 0; i < operatorIds.length; i++) { + quorumBitmaps[i] = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorIds[i], blockNumber, quorumBitmapIndices[i]); + } + return quorumBitmaps; + } } diff --git a/src/interfaces/IBLSApkRegistry.sol b/src/interfaces/IBLSApkRegistry.sol index f46ab4f2..d00eeffc 100644 --- a/src/interfaces/IBLSApkRegistry.sol +++ b/src/interfaces/IBLSApkRegistry.sol @@ -40,12 +40,14 @@ interface IBLSApkRegistry is IRegistry { // @notice Emitted when a new operator pubkey is registered for a set of quorums event OperatorAddedToQuorums( address operator, + bytes32 operatorId, bytes quorumNumbers ); // @notice Emitted when an operator pubkey is removed from a set of quorums event OperatorRemovedFromQuorums( address operator, + bytes32 operatorId, bytes quorumNumbers ); diff --git a/test/events/IBLSApkRegistryEvents.sol b/test/events/IBLSApkRegistryEvents.sol index db8951f9..e59f99f6 100644 --- a/test/events/IBLSApkRegistryEvents.sol +++ b/test/events/IBLSApkRegistryEvents.sol @@ -11,12 +11,14 @@ interface IBLSApkRegistryEvents { // @notice Emitted when a new operator pubkey is registered for a set of quorums event OperatorAddedToQuorums( address operator, + bytes32 operatorId, bytes quorumNumbers ); // @notice Emitted when an operator pubkey is removed from a set of quorums event OperatorRemovedFromQuorums( address operator, + bytes32 operatorId, bytes quorumNumbers ); } diff --git a/test/unit/BLSApkRegistryUnit.t.sol b/test/unit/BLSApkRegistryUnit.t.sol index 82ff0a87..62604db2 100644 --- a/test/unit/BLSApkRegistryUnit.t.sol +++ b/test/unit/BLSApkRegistryUnit.t.sol @@ -178,9 +178,10 @@ contract BLSApkRegistryUnitTests is BLSMockAVSDeployer, IBLSApkRegistryEvents { * @dev register operator, assumes operator has a registered BLS public key and that quorumNumbers are valid */ function _registerOperator(address operator, bytes memory quorumNumbers) internal { + bytes32 operatorId = blsApkRegistry.getOperatorId(operator); cheats.prank(address(registryCoordinator)); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorAddedToQuorums(operator, quorumNumbers); + emit OperatorAddedToQuorums(operator, operatorId, quorumNumbers); blsApkRegistry.registerOperator(operator, quorumNumbers); } @@ -188,9 +189,10 @@ contract BLSApkRegistryUnitTests is BLSMockAVSDeployer, IBLSApkRegistryEvents { * @dev deregister operator, assumes operator has a registered BLS public key and that quorumNumbers are valid */ function _deregisterOperator(address operator, bytes memory quorumNumbers) internal { + bytes32 operatorId = blsApkRegistry.getOperatorId(operator); cheats.prank(address(registryCoordinator)); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(operator, quorumNumbers); + emit OperatorRemovedFromQuorums(operator, operatorId, quorumNumbers); blsApkRegistry.deregisterOperator(operator, quorumNumbers); } @@ -482,9 +484,10 @@ contract BLSApkRegistryUnitTests_registerOperator is BLSApkRegistryUnitTests { } // registerOperator with expected OperatorAddedToQuorums event + bytes32 operatorId = blsApkRegistry.getOperatorId(operator); cheats.prank(address(registryCoordinator)); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorAddedToQuorums(operator, quorumNumbers); + emit OperatorAddedToQuorums(operator, operatorId, quorumNumbers); blsApkRegistry.registerOperator(operator, quorumNumbers); // check updated storage values for each quorum @@ -591,9 +594,10 @@ contract BLSApkRegistryUnitTests_deregisterOperator is BLSApkRegistryUnitTests { } // registerOperator with expected OperatorAddedToQuorums event + bytes32 operatorId = blsApkRegistry.getOperatorId(operator); cheats.prank(address(registryCoordinator)); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(operator, quorumNumbers); + emit OperatorRemovedFromQuorums(operator, operatorId, quorumNumbers); blsApkRegistry.deregisterOperator(operator, quorumNumbers); // check updated storage values for each quorum diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index b977f780..de6f9ecd 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -28,12 +28,14 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { // Emitted when a new operator pubkey is registered for a set of quorums event OperatorAddedToQuorums( address operator, + bytes32 operatorId, bytes quorumNumbers ); // Emitted when an operator pubkey is removed from a set of quorums event OperatorRemovedFromQuorums( address operator, + bytes32 operatorId, bytes quorumNumbers ); @@ -304,7 +306,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorAddedToQuorums(defaultOperator, quorumNumbers); + emit OperatorAddedToQuorums(defaultOperator, defaultOperatorId, quorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, defaultQuorumNumber, actualStake); cheats.expectEmit(true, true, true, true, address(indexRegistry)); @@ -356,7 +358,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni emit OperatorRegistered(defaultOperator, defaultOperatorId); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorAddedToQuorums(defaultOperator, quorumNumbers); + emit OperatorAddedToQuorums(defaultOperator, defaultOperatorId, quorumNumbers); for (uint i = 0; i < quorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); @@ -415,7 +417,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorAddedToQuorums(defaultOperator, newQuorumNumbers); + emit OperatorAddedToQuorums(defaultOperator, defaultOperatorId, newQuorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, uint8(newQuorumNumbers[0]), actualStake); cheats.expectEmit(true, true, true, true, address(indexRegistry)); @@ -546,7 +548,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorAddedToQuorums(defaultOperator, quorumNumbers); + emit OperatorAddedToQuorums(defaultOperator, defaultOperatorId, quorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, defaultQuorumNumber, defaultStake); cheats.expectEmit(true, true, true, true, address(indexRegistry)); @@ -639,7 +641,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbers); + emit OperatorRemovedFromQuorums(defaultOperator, defaultOperatorId, quorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, defaultQuorumNumber, 0); @@ -691,7 +693,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbers); + emit OperatorRemovedFromQuorums(defaultOperator, defaultOperatorId, quorumNumbers); for (uint i = 0; i < quorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[i]), 0); @@ -753,7 +755,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory deregistrationquorumNumbers = BitmapUtils.bitmapToBytesArray(deregistrationQuorumBitmap); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, deregistrationquorumNumbers); + emit OperatorRemovedFromQuorums(defaultOperator, defaultOperatorId, deregistrationquorumNumbers); for (uint i = 0; i < deregistrationquorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, uint8(deregistrationquorumNumbers[i]), 0); @@ -855,7 +857,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist } cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(operatorToDeregister, operatorToDeregisterQuorumNumbers); + emit OperatorRemovedFromQuorums(operatorToDeregister, operatorToDeregisterId, operatorToDeregisterQuorumNumbers); for (uint i = 0; i < operatorToDeregisterQuorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); @@ -1016,7 +1018,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory deregistrationquorumNumbers = BitmapUtils.bitmapToBytesArray(deregistrationQuorumBitmap); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, deregistrationquorumNumbers); + emit OperatorRemovedFromQuorums(defaultOperator, defaultOperatorId, deregistrationquorumNumbers); for (uint i = 0; i < deregistrationquorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, uint8(deregistrationquorumNumbers[i]), 0); @@ -1081,7 +1083,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbers); + emit OperatorRemovedFromQuorums(defaultOperator, defaultOperatorId, quorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[0]), 0); @@ -1121,7 +1123,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist quorumNumbersToEject[0] = quorumNumbers[0]; cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbersToEject); + emit OperatorRemovedFromQuorums(defaultOperator, defaultOperatorId, quorumNumbersToEject); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbersToEject[0]), 0); @@ -1328,7 +1330,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord emit OperatorRegistered(operatorToRegister, operatorToRegisterId); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorAddedToQuorums(operatorToRegister, quorumNumbers); + emit OperatorAddedToQuorums(operatorToRegister, operatorToRegisterId, quorumNumbers); cheats.expectEmit(true, true, true, false, address(stakeRegistry)); emit OperatorStakeUpdate(operatorToRegisterId, defaultQuorumNumber, registeringStake - 1); cheats.expectEmit(true, true, true, true, address(indexRegistry)); @@ -1338,7 +1340,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorDeregistered(operatorKickParams[0].operator, operatorToKickId); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(operatorKickParams[0].operator, quorumNumbers); + emit OperatorRemovedFromQuorums(operatorKickParams[0].operator, operatorToKickId, quorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); emit OperatorStakeUpdate(operatorToKickId, defaultQuorumNumber, 0); cheats.expectEmit(true, true, true, true, address(indexRegistry)); From 2e92c1e0a9a9ededc5cc12225fe712b386303f37 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Fri, 5 Apr 2024 22:40:15 -0500 Subject: [PATCH 07/12] fix: conflict --- src/OperatorStateRetriever.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/OperatorStateRetriever.sol b/src/OperatorStateRetriever.sol index 1dd7398d..66792e78 100644 --- a/src/OperatorStateRetriever.sol +++ b/src/OperatorStateRetriever.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; +pragma solidity ^0.8.12; import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; import {IBLSApkRegistry} from "./interfaces/IBLSApkRegistry.sol"; @@ -160,6 +160,12 @@ contract OperatorStateRetriever { return checkSignaturesIndices; } + /** + * @notice this function returns the quorumBitmaps for each of the operators in the operatorIds array at the given blocknumber + * @param registryCoordinator is the AVS registry coordinator to fetch the operator information from + * @param operatorIds are the ids of the operators to get the quorumBitmaps for + * @param blockNumber is the block number to get the quorumBitmaps for + */ function getQuorumBitmapsAtBlockNumber( IRegistryCoordinator registryCoordinator, bytes32[] memory operatorIds, @@ -172,4 +178,4 @@ contract OperatorStateRetriever { } return quorumBitmaps; } -} +} \ No newline at end of file From fab636eac692ff113bffcedd0cb07c8114c74129 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Sat, 6 Apr 2024 16:11:28 -0500 Subject: [PATCH 08/12] refactor: ejection --- src/EjectionManager.sol | 91 +++++++++++++++++------------ src/interfaces/IEjectionManager.sol | 20 +++---- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/EjectionManager.sol b/src/EjectionManager.sol index f585e740..a92c64bd 100644 --- a/src/EjectionManager.sol +++ b/src/EjectionManager.sol @@ -46,50 +46,60 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ _setEjector(_ejector); for(uint8 i = 0; i < _quorumEjectionParams.length; i++) { - quorumEjectionParams[i] = _quorumEjectionParams[i]; + _setQuorumEjectionParams(i, _quorumEjectionParams[i]); } } /** - * @notice Ejects operators from the AVSs registryCoordinator - * @param _operatorIds The addresses of the operators to eject - * @param _quorumBitmaps The quorum bitmaps for each respective operator + * @notice Ejects operators from the AVSs registryCoordinator under a ratelimit + * @dev This function will eject as many operators as possible without reverting + * @param _operatorIds The ids of the operators to eject for each quorum */ - function ejectOperators(bytes32[] memory _operatorIds, uint256[] memory _quorumBitmaps) external { + function ejectOperators(bytes32[][] memory _operatorIds) external { require(msg.sender == ejector || msg.sender == owner(), "Ejector: Only owner or ejector can eject"); - require(_operatorIds.length == _quorumBitmaps.length, "Ejector: _operatorIds and _quorumBitmaps must be same length"); for(uint i = 0; i < _operatorIds.length; ++i) { - bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(_quorumBitmaps[i]); - - for(uint8 j = 0; j < quorumNumbers.length; ++j) { - uint8 quorumNumber = uint8(quorumNumbers[j]); - require(quorumEjectionParams[quorumNumber].rateLimitWindow > 0, "Ejector: Quorum ejection params not set"); - - uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i], quorumNumber); - - if(msg.sender == ejector){ - _cleanOldEjections(quorumNumber); - require(canEject(operatorStake, quorumNumber), "Ejector: Stake exceeds quorum ejection ratelimit"); + uint8 quorumNumber = uint8(i); + + _cleanOldEjections(quorumNumber); + 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( + msg.sender == ejector && + quorumEjectionParams[quorumNumber].rateLimitWindow > 0 && + stakeForEjection + operatorStake > amountEjectable + ){ + stakeEjectedForQuorum[quorumNumber].push(StakeEjection({ + timestamp: block.timestamp, + stakeEjected: stakeForEjection + })); + broke = true; + break; + } + + 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); } + } + if(!broke){ stakeEjectedForQuorum[quorumNumber].push(StakeEjection({ timestamp: block.timestamp, - stakeEjected: operatorStake + stakeEjected: stakeForEjection })); } - try registryCoordinator.ejectOperator( - registryCoordinator.getOperatorFromId(_operatorIds[i]), - quorumNumbers - ) { - emit OperatorEjected(_operatorIds[i], _quorumBitmaps[i]); - } catch (bytes memory err) { - for(uint8 j = 0; j < quorumNumbers.length; ++j) { - stakeEjectedForQuorum[uint8(quorumNumbers[j])].pop(); - } - emit FailedOperatorEjection(_operatorIds[i], _quorumBitmaps[i], err); - } } } @@ -99,8 +109,7 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ * @param _quorumEjectionParams The quorum bitmaps for each respective operator */ function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() { - quorumEjectionParams[_quorumNumber] = _quorumEjectionParams; - emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.rateLimitWindow, _quorumEjectionParams.ejectableStakePercent); + _setQuorumEjectionParams(_quorumNumber, _quorumEjectionParams); } /** @@ -111,6 +120,12 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ _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); @@ -118,8 +133,8 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ } /** - * @dev Cleans up old ejections for a quorums StakeEjection array - * @param _quorumNumber The addresses of the operators to eject + * @dev Removes stale ejections for a quorums history + * @param _quorumNumber The quorum number to clean ejections for */ function _cleanOldEjections(uint8 _quorumNumber) internal { uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow; @@ -139,17 +154,17 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ } /** - * @notice Checks if an amount of stake can be ejected for a quorum with ratelimit - * @param _amount The amount of stake to eject - * @param _quorumNumber The quorum number to eject for + * @notice Returns the amount of stake that can be ejected for a quorum + * @dev This function only returns a valid amount after _cleanOldEjections has been called + * @param _quorumNumber The quorum number to view ejectable stake for */ - function canEject(uint256 _amount, uint8 _quorumNumber) public view returns (bool) { + function _amountEjectableForQuorum(uint8 _quorumNumber) internal view returns (uint256) { uint256 totalEjected = 0; for (uint256 i = 0; i < stakeEjectedForQuorum[_quorumNumber].length; i++) { totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; } uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR; - return totalEjected + _amount <= totalEjectable; + return totalEjectable - totalEjected; } } diff --git a/src/interfaces/IEjectionManager.sol b/src/interfaces/IEjectionManager.sol index 4220346c..0f5cb490 100644 --- a/src/interfaces/IEjectionManager.sol +++ b/src/interfaces/IEjectionManager.sol @@ -25,18 +25,17 @@ interface IEjectionManager { ///@notice Emitted when the ejector address is set event EjectorUpdated(address previousAddress, address newAddress); ///@notice Emitted when an operator is ejected - event OperatorEjected(bytes32 operatorId, uint256 quorumBitmap); + event OperatorEjected(bytes32 operatorId, uint8 quorumNumber); ///@notice Emitted when an operator ejection fails - event FailedOperatorEjection(bytes32 operatorId, uint256 quorumBitmap, bytes err); + event FailedOperatorEjection(bytes32 operatorId, uint8 quorumNumber, bytes err); ///@notice Emitted when the ratelimit parameters for a quorum are set event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 rateLimitWindow, uint16 ejectableStakePercent); - /** - * @notice Ejects operators from the AVSs registryCoordinator - * @param _operatorIds The addresses of the operators to eject - * @param _quorumBitmaps The quorum bitmaps for each respective operator + /** + * @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, uint256[] memory _quorumBitmaps) external; + function ejectOperators(bytes32[][] memory _operatorIds) external; /** * @notice Sets the ratelimit parameters for a quorum @@ -51,11 +50,6 @@ interface IEjectionManager { */ function setEjector(address _ejector) external; - /** - * @notice Checks if an amount of stake can be ejected for a quorum with ratelimit - * @param _amount The amount of stake to eject - * @param _quorumNumber The quorum number to eject for - */ - function canEject(uint256 _amount, uint8 _quorumNumber) external view returns (bool); + } From 1d06dd52ca781fe7c043e3d9d33d8167703f6aea Mon Sep 17 00:00:00 2001 From: QUAQ Date: Sat, 6 Apr 2024 17:30:04 -0500 Subject: [PATCH 09/12] fix: stake recording --- src/EjectionManager.sol | 71 +++++++++++++---------------- src/interfaces/IEjectionManager.sol | 24 +++++----- 2 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/EjectionManager.sol b/src/EjectionManager.sol index a92c64bd..0291ddf9 100644 --- a/src/EjectionManager.sol +++ b/src/EjectionManager.sol @@ -1,14 +1,13 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.9; +// 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"; -import {BitmapUtils} from "./libraries/BitmapUtils.sol"; /** - * @title Used for automated ejection of operators from the RegistryCoordinator + * @title Used for automated ejection of operators from the RegistryCoordinator under a ratelimit * @author Layr Labs, Inc. */ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ @@ -16,13 +15,15 @@ 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 within a time delta + /// @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; @@ -37,6 +38,11 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ _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, @@ -51,9 +57,10 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ } /** - * @notice Ejects operators from the AVSs registryCoordinator under a ratelimit - * @dev This function will eject as many operators as possible without reverting + * @notice Ejects operators from the AVSs RegistryCoordinator under a ratelimit * @param _operatorIds The ids of the operators to eject for each quorum + * @dev This function will eject as many operators as possible without reverting + * @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"); @@ -61,14 +68,14 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ for(uint i = 0; i < _operatorIds.length; ++i) { uint8 quorumNumber = uint8(i); - _cleanOldEjections(quorumNumber); - uint256 amountEjectable = _amountEjectableForQuorum(quorumNumber); + 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 && @@ -82,6 +89,7 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ break; } + //try-catch used to prevent race condition of operator deregistering before ejection try registryCoordinator.ejectOperator( registryCoordinator.getOperatorFromId(_operatorIds[i][j]), abi.encodePacked(quorumNumber) @@ -93,7 +101,8 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ } } - if(!broke){ + //record the stake ejected if ejector and ratelimit enforced + if(!broke && msg.sender == ejector){ stakeEjectedForQuorum[quorumNumber].push(StakeEjection({ timestamp: block.timestamp, stakeEjected: stakeForEjection @@ -106,14 +115,14 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ /** * @notice Sets the ratelimit parameters for a quorum * @param _quorumNumber The quorum number to set the ratelimit parameters for - * @param _quorumEjectionParams The quorum bitmaps for each respective operator + * @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 + * @notice Sets the address permissioned to eject operators under a ratelimit * @param _ejector The address to permission */ function setEjector(address _ejector) external onlyOwner() { @@ -133,38 +142,22 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ } /** - * @dev Removes stale ejections for a quorums history - * @param _quorumNumber The quorum number to clean ejections for - */ - function _cleanOldEjections(uint8 _quorumNumber) internal { - uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow; - uint256 index = 0; - StakeEjection[] storage stakeEjections = stakeEjectedForQuorum[_quorumNumber]; - while (index < stakeEjections.length && stakeEjections[index].timestamp < cutoffTime) { - index++; - } - if (index > 0) { - for (uint256 i = index; i < stakeEjections.length; ++i) { - stakeEjections[i - index] = stakeEjections[i]; - } - for (uint256 i = 0; i < index; ++i) { - stakeEjections.pop(); - } - } - } - - /** - * @notice Returns the amount of stake that can be ejected for a quorum - * @dev This function only returns a valid amount after _cleanOldEjections has been called + * @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) internal view returns (uint256) { - uint256 totalEjected = 0; - for (uint256 i = 0; i < stakeEjectedForQuorum[_quorumNumber].length; i++) { + function amountEjectableForQuorum(uint8 _quorumNumber) public view returns (uint256) { + uint256 totalEjected; + uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow; + uint256 i = stakeEjectedForQuorum[_quorumNumber].length - 1; + while(stakeEjectedForQuorum[_quorumNumber][i].timestamp > cutoffTime) { totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; + if(i == 0){ + break; + } else { + --i; + } } uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR; return totalEjectable - totalEjected; } - } diff --git a/src/interfaces/IEjectionManager.sol b/src/interfaces/IEjectionManager.sol index 0f5cb490..94aebd0f 100644 --- a/src/interfaces/IEjectionManager.sol +++ b/src/interfaces/IEjectionManager.sol @@ -1,8 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.9; - -import {IRegistryCoordinator} from "./IRegistryCoordinator.sol"; -import {IStakeRegistry} from "./IStakeRegistry.sol"; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.12; /** * @title Interface for a contract that ejects operators from an AVSs RegistryCoordinator @@ -24,13 +21,13 @@ interface IEjectionManager { ///@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 Emitted when the ratelimit parameters for a quorum are set - event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 rateLimitWindow, uint16 ejectableStakePercent); - + /** * @notice Ejects operators from the AVSs registryCoordinator under a ratelimit * @param _operatorIds The ids of the operators to eject for each quorum @@ -40,16 +37,19 @@ interface IEjectionManager { /** * @notice Sets the ratelimit parameters for a quorum * @param _quorumNumber The quorum number to set the ratelimit parameters for - * @param _quorumEjectionParams The quorum bitmaps for each respective operator + * @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 + * @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); } From d965f9df8a8d02f93335e7b78c59a52aa64e8623 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Wed, 10 Apr 2024 23:21:29 -0500 Subject: [PATCH 10/12] test: unit --- src/EjectionManager.sol | 11 +- test/unit/EjectionManagerUnit.t.sol | 383 ++++++++++++++++++++++++++++ 2 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 test/unit/EjectionManagerUnit.t.sol diff --git a/src/EjectionManager.sol b/src/EjectionManager.sol index 0291ddf9..b2c914bc 100644 --- a/src/EjectionManager.sol +++ b/src/EjectionManager.sol @@ -146,9 +146,15 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ * @param _quorumNumber The quorum number to view ejectable stake for */ function amountEjectableForQuorum(uint8 _quorumNumber) public view returns (uint256) { - uint256 totalEjected; uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow; - uint256 i = stakeEjectedForQuorum[_quorumNumber].length - 1; + uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR; + uint256 totalEjected; + uint256 i; + if (stakeEjectedForQuorum[_quorumNumber].length == 0) { + return totalEjectable; + } else { + i = stakeEjectedForQuorum[_quorumNumber].length - 1; + } while(stakeEjectedForQuorum[_quorumNumber][i].timestamp > cutoffTime) { totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; if(i == 0){ @@ -157,7 +163,6 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ --i; } } - uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR; return totalEjectable - totalEjected; } } 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 From 3b48a3955331162fa0377c8afe37c42600d22865 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Wed, 10 Apr 2024 23:25:02 -0500 Subject: [PATCH 11/12] style: natspec --- src/EjectionManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EjectionManager.sol b/src/EjectionManager.sol index b2c914bc..5e4784b6 100644 --- a/src/EjectionManager.sol +++ b/src/EjectionManager.sol @@ -58,8 +58,8 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ /** * @notice Ejects operators from the AVSs RegistryCoordinator under a ratelimit - * @param _operatorIds The ids of the operators to eject for each quorum - * @dev This function will eject as many operators as possible without reverting + * @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 { From a32bbc2c3ab91ecad53b5bde3f49437ac9b40fa6 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Thu, 11 Apr 2024 14:07:40 -0500 Subject: [PATCH 12/12] fix: totalEjected > totalEjectable nit: else --- src/EjectionManager.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/EjectionManager.sol b/src/EjectionManager.sol index 5e4784b6..d6ed1b46 100644 --- a/src/EjectionManager.sol +++ b/src/EjectionManager.sol @@ -152,9 +152,9 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ uint256 i; if (stakeEjectedForQuorum[_quorumNumber].length == 0) { return totalEjectable; - } else { - i = stakeEjectedForQuorum[_quorumNumber].length - 1; } + i = stakeEjectedForQuorum[_quorumNumber].length - 1; + while(stakeEjectedForQuorum[_quorumNumber][i].timestamp > cutoffTime) { totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; if(i == 0){ @@ -163,6 +163,10 @@ contract EjectionManager is IEjectionManager, OwnableUpgradeable{ --i; } } + + if(totalEjected >= totalEjectable){ + return 0; + } return totalEjectable - totalEjected; } }