From f2cbfa2f53b5c9740a6d4813c04f0093cbbe9f87 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Thu, 11 Jul 2024 14:00:41 -0500 Subject: [PATCH 1/2] feat: ejection cooldown --- src/RegistryCoordinator.sol | 61 +++++++++++++++++++++++++----- src/RegistryCoordinatorStorage.sol | 9 ++++- test/integration/User.t.sol | 4 +- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 19367892..6d2ff3f4 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -42,17 +42,14 @@ contract RegistryCoordinator is using BN254 for BN254.G1Point; modifier onlyEjector { - require(msg.sender == ejector, "RegistryCoordinator.onlyEjector: caller is not the ejector"); + _checkEjector(); _; } /// @dev Checks that `quorumNumber` corresponds to a quorum that has been created /// via `initialize` or `createQuorum` modifier quorumExists(uint8 quorumNumber) { - require( - quorumNumber < quorumCount, - "RegistryCoordinator.quorumExists: quorum does not exist" - ); + _checkQuorumExists(quorumNumber); _; } @@ -359,15 +356,28 @@ contract RegistryCoordinator is * @notice Forcibly deregisters an operator from one or more quorums * @param operator the operator to eject * @param quorumNumbers the quorum numbers to eject the operator from + * @dev possible race condition if prior to being ejected for a set of quorums the operator self deregisters from a subset */ function ejectOperator( address operator, bytes calldata quorumNumbers ) external onlyEjector { - _deregisterOperator({ - operator: operator, - quorumNumbers: quorumNumbers - }); + lastEjectionTimestamp[operator] = block.timestamp; + + OperatorInfo storage operatorInfo = _operatorInfo[operator]; + bytes32 operatorId = operatorInfo.operatorId; + uint192 quorumsToRemove = uint192(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount)); + uint192 currentBitmap = _currentOperatorBitmap(operatorId); + if( + operatorInfo.status == OperatorStatus.REGISTERED && + !quorumsToRemove.isEmpty() && + quorumsToRemove.isSubsetOf(currentBitmap) + ){ + _deregisterOperator({ + operator: operator, + quorumNumbers: quorumNumbers + }); + } } /******************************************************************************* @@ -423,6 +433,16 @@ contract RegistryCoordinator is _setEjector(_ejector); } + /** + * @notice Sets the ejection cooldown, which is the time an operator must wait in + * seconds afer ejection before registering for any quorum + * @param _ejectionCooldown the new ejection cooldown in seconds + * @dev only callable by the owner + */ + function setEjectionCooldown(uint256 _ejectionCooldown) external onlyOwner { + ejectionCooldown = _ejectionCooldown; + } + /******************************************************************************* INTERNAL FUNCTIONS *******************************************************************************/ @@ -457,6 +477,9 @@ contract RegistryCoordinator is require(quorumsToAdd.noBitsInCommon(currentBitmap), "RegistryCoordinator._registerOperator: operator already registered for some quorums being registered for"); uint192 newBitmap = uint192(currentBitmap.plus(quorumsToAdd)); + // Check that the operator can reregister if ejected + require(lastEjectionTimestamp[operator] + ejectionCooldown < block.timestamp, "RegistryCoordinator._registerOperator: operator cannot reregister yet"); + /** * Update operator's bitmap, socket, and status. Only update operatorInfo if needed: * if we're `REGISTERED`, the operatorId and status are already correct. @@ -491,6 +514,26 @@ contract RegistryCoordinator is return results; } + /** + * @notice Checks if the caller is the ejector + * @dev Reverts if the caller is not the ejector + */ + function _checkEjector() internal view { + require(msg.sender == ejector, "RegistryCoordinator.onlyEjector: caller is not the ejector"); + } + + /** + * @notice Checks if a quorum exists + * @param quorumNumber The quorum number to check + * @dev Reverts if the quorum does not exist + */ + function _checkQuorumExists(uint8 quorumNumber) internal view { + require( + quorumNumber < quorumCount, + "RegistryCoordinator.quorumExists: quorum does not exist" + ); + } + /** * @notice Fetches an operator's pubkey hash from the BLSApkRegistry. If the * operator has not registered a pubkey, attempts to register a pubkey using diff --git a/src/RegistryCoordinatorStorage.sol b/src/RegistryCoordinatorStorage.sol index e8c42850..1200edde 100644 --- a/src/RegistryCoordinatorStorage.sol +++ b/src/RegistryCoordinatorStorage.sol @@ -64,6 +64,11 @@ abstract contract RegistryCoordinatorStorage is IRegistryCoordinator { /// @notice the address of the entity allowed to eject operators from the AVS address public ejector; + /// @notice the last timestamp an operator was ejected + mapping(address => uint256) public lastEjectionTimestamp; + /// @notice the delay in seconds before an operator can reregister after being ejected + uint256 public ejectionCooldown; + constructor( IServiceManager _serviceManager, IStakeRegistry _stakeRegistry, @@ -78,5 +83,5 @@ abstract contract RegistryCoordinatorStorage is IRegistryCoordinator { // storage gap for upgradeability // slither-disable-next-line shadowing-state - uint256[41] private __GAP; -} + uint256[39] private __GAP; +} \ No newline at end of file diff --git a/test/integration/User.t.sol b/test/integration/User.t.sol index ef6557e5..fbb55198 100644 --- a/test/integration/User.t.sol +++ b/test/integration/User.t.sol @@ -43,7 +43,7 @@ contract User is Test { using BitmapStrings for *; using BitmapUtils for *; - Vm cheats = Vm(HEVM_ADDRESS); + Vm cheats = Vm(VM_ADDRESS); // Core contracts DelegationManager delegationManager; @@ -117,6 +117,7 @@ contract User is Test { function registerOperator(bytes calldata quorums) public createSnapshot virtual returns (bytes32) { _log("registerOperator", quorums); + vm.warp(block.timestamp + 1); registryCoordinator.registerOperator({ quorumNumbers: quorums, socket: NAME, @@ -208,6 +209,7 @@ contract User is Test { expiry: expiry }); + vm.warp(block.timestamp + 1); registryCoordinator.registerOperatorWithChurn({ quorumNumbers: allQuorums, socket: NAME, From 9c6e1bce21a3bf59f5fe7438ee238549ae722b69 Mon Sep 17 00:00:00 2001 From: QUAQ Date: Fri, 12 Jul 2024 00:04:20 -0500 Subject: [PATCH 2/2] docs: update impl --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f33b6920..b56836f0 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ The current mainnet deployment is from our M2 mainnet release. You can view the | Name | Proxy | Implementation | Notes | | -------- | -------- | -------- | -------- | -[`RegistryCoordinator`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/mainnet/src/RegistryCoordinator.sol) | [`0x0baac79acd45a023e19345c352d8a7a83c4e5656`](https://etherscan.io/address/0x0baac79acd45a023e19345c352d8a7a83c4e5656#readProxyContract) | [`0xd3e0...EECF`](https://etherscan.io/address/0xd3e09a0c2a9a6fdf5e92ae65d3cc090a4df8eecf#code) | Proxy: [`TUP@4.7.1`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | +[`RegistryCoordinator`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/mainnet/src/RegistryCoordinator.sol) | [`0x0baac79acd45a023e19345c352d8a7a83c4e5656`](https://etherscan.io/address/0x0baac79acd45a023e19345c352d8a7a83c4e5656#readProxyContract) | [`0xdcab...E03F`](https://etherscan.io/address/0xdcabf0be991d4609096cce316df08d091356e03f#code) | Proxy: [`TUP@4.7.1`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | [`StakeRegistry`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/mainnet/src/StakeRegistry.sol) | [`0x006124ae7976137266feebfb3f4d2be4c073139d`](https://etherscan.io/address/0x006124ae7976137266feebfb3f4d2be4c073139d#readProxyContract) | [`0x1C46...dd96`](https://etherscan.io/address/0x1c468cf7089d263c2f53e2579b329b16abc4dd96#code) | Proxy: [`TUP@4.7.1`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | [`IndexRegistry`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/mainnet/src/IndexRegistry.sol) | [`0xbd35a7a1cdef403a6a99e4e8ba0974d198455030`](https://etherscan.io/address/0xbd35a7a1cdef403a6a99e4e8ba0974d198455030#readProxyContract) | [`0x1ae0...a14c`](https://etherscan.io/address/0x1ae0b73118906f39d5ed30ae4a484ce2f479a14c#code) | Proxy: [`TUP@4.7.1`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | [`BLSApkRegistry`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/mainnet/src/BLSApkRegistry.sol) | [`0x00a5fd09f6cee6ae9c8b0e5e33287f7c82880505`](https://etherscan.io/address/0x00a5fd09f6cee6ae9c8b0e5e33287f7c82880505#readProxyContract) | [`0x5d0B...eD2b`](https://etherscan.io/address/0x5d0b9ce2e277daf508528e9f6bf6314e79e4ed2b#code) | Proxy: [`TUP@4.7.1`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) |