From 596f20f0d96dae86ce87c029c00b8a8908dab0bd Mon Sep 17 00:00:00 2001 From: wadealexc Date: Thu, 26 Oct 2023 16:18:20 +0000 Subject: [PATCH] refactor: wip refactor to move "createQuorum" to the registry coordinator currently everything is broken because interfaces live in core and fixing it is a pain --- src/BLSPubkeyRegistry.sol | 42 +++- src/BLSRegistryCoordinatorWithIndices.sol | 95 ++++++-- src/IndexRegistry.sol | 47 ++-- src/StakeRegistry.sol | 254 +++++++++++++++++----- src/VoteWeigherBase.sol | 232 -------------------- src/VoteWeigherBaseStorage.sol | 17 +- src/interfaces/IRegistry.sol | 13 +- src/interfaces/IRegistryCoordinator.sol | 95 +++++++- src/interfaces/IStakeRegistry.sol | 158 +++++++++++++- test/harnesses/StakeRegistryHarness.sol | 24 +- test/unit/VoteWeigherBaseUnit.t.sol | 28 +-- 11 files changed, 622 insertions(+), 383 deletions(-) delete mode 100644 src/VoteWeigherBase.sol diff --git a/src/BLSPubkeyRegistry.sol b/src/BLSPubkeyRegistry.sol index 34875bd6..f37f8b53 100644 --- a/src/BLSPubkeyRegistry.sol +++ b/src/BLSPubkeyRegistry.sol @@ -93,6 +93,20 @@ contract BLSPubkeyRegistry is BLSPubkeyRegistryStorage { emit OperatorRemovedFromQuorums(operator, quorumNumbers); } + /** + * @notice Creates a new quorum by pushing its first apk update + * @param quorumNumber The number of the new quorum + */ + function createQuorum(uint8 quorumNumber) public virtual onlyRegistryCoordinator { + require(quorumApkUpdates[quorumNumber].length == 0, "BLSPubkeyRegistry.createQuorum: quorum already exists"); + + quorumApkUpdates[quorumNumber].push(ApkUpdate({ + apkHash: bytes24(0), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + })); + } + /******************************************************************************* INTERNAL FUNCTIONS *******************************************************************************/ @@ -103,21 +117,24 @@ contract BLSPubkeyRegistry is BLSPubkeyRegistryStorage { for (uint i = 0; i < quorumNumbers.length; ) { uint8 quorumNumber = uint8(quorumNumbers[i]); - uint256 quorumApkUpdatesLength = quorumApkUpdates[quorumNumber].length; - if (quorumApkUpdatesLength > 0) { - // update nextUpdateBlockNumber of the current latest ApkUpdate - quorumApkUpdates[quorumNumber][quorumApkUpdatesLength - 1].nextUpdateBlockNumber = uint32(block.number); - } + // Validate quorumNumber + uint256 historyLength = quorumApkUpdates[quorumNumber].length; + require(historyLength != 0, "BLSPubkeyRegistry._processQuorumApkUpdate: quorum does not exist"); - apkAfterUpdate = quorumApk[quorumNumber].plus(point); + // Update the last entry to point at the current block + // TODO - if the last entry was made in this block, update the entry instead + quorumApkUpdates[quorumNumber][historyLength - 1].nextUpdateBlockNumber = uint32(block.number); - //update aggregate public key for this quorum + // Update aggregate public key for this quorum + apkAfterUpdate = quorumApk[quorumNumber].plus(point); quorumApk[quorumNumber] = apkAfterUpdate; - //create new ApkUpdate to add to the mapping - ApkUpdate memory latestApkUpdate; - latestApkUpdate.apkHash = bytes24(BN254.hashG1Point(apkAfterUpdate)); - latestApkUpdate.updateBlockNumber = uint32(block.number); - quorumApkUpdates[quorumNumber].push(latestApkUpdate); + + // Push update to history + quorumApkUpdates[quorumNumber].push(ApkUpdate({ + apkHash: bytes24(BN254.hashG1Point(apkAfterUpdate)), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + })); unchecked { ++i; @@ -125,6 +142,7 @@ contract BLSPubkeyRegistry is BLSPubkeyRegistryStorage { } } + // TODO - should this fail if apkUpdate.apkHash == 0? This will be the case for the first entry in each quorum function _validateApkHashForQuorumAtBlockNumber(ApkUpdate memory apkUpdate, uint32 blockNumber) internal pure { require( blockNumber >= apkUpdate.updateBlockNumber, diff --git a/src/BLSRegistryCoordinatorWithIndices.sol b/src/BLSRegistryCoordinatorWithIndices.sol index 99897ef3..1dc08f51 100644 --- a/src/BLSRegistryCoordinatorWithIndices.sol +++ b/src/BLSRegistryCoordinatorWithIndices.sol @@ -20,8 +20,6 @@ import "src/interfaces/IStakeRegistry.sol"; import "src/interfaces/IIndexRegistry.sol"; import "src/interfaces/IRegistryCoordinator.sol"; - - /** * @title A `RegistryCoordinator` that has three registries: * 1) a `StakeRegistry` that keeps track of operators' stakes @@ -44,18 +42,22 @@ contract BLSRegistryCoordinatorWithIndices is EIP712, Initializable, IBLSRegistr uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; /// @notice Index for flag that pauses operator deregistration uint8 internal constant PAUSED_DEREGISTER_OPERATOR = 1; + /// @notice The maximum number of quorums this contract supports + uint8 internal constant MAX_QUORUM_COUNT = 192; /// @notice the EigenLayer Slasher ISlasher public immutable slasher; /// @notice the Service Manager for the service that this contract is coordinating IServiceManager public immutable serviceManager; /// @notice the BLS Pubkey Registry contract that will keep track of operators' BLS public keys - IBLSPubkeyRegistry public immutable blsPubkeyRegistry; + BLSPubkeyRegistry public immutable blsPubkeyRegistry; /// @notice the Stake Registry contract that will keep track of operators' stakes - IStakeRegistry public immutable stakeRegistry; + StakeRegistry public immutable stakeRegistry; /// @notice the Index Registry contract that will keep track of operators' indexes - IIndexRegistry public immutable indexRegistry; + IndexRegistry public immutable indexRegistry; + /// @notice the current number of quorums supported by the registry coordinator + uint8 public quorumCount; /// @notice the mapping from quorum number to a quorums operator cap and kick parameters mapping(uint8 => OperatorSetParam) internal _quorumOperatorSetParams; /// @notice the mapping from operator's operatorId to the updates of the bitmap of quorums they are registered for @@ -82,6 +84,14 @@ contract BLSRegistryCoordinatorWithIndices is EIP712, Initializable, IBLSRegistr _; } + modifier quorumExists(uint8 quorumNumber) { + require( + quorumNumber < quorumCount, + "BLSRegistryCoordinatorWithIndices.quorumExists: quorum does not exist" + ); + _; + } + constructor( ISlasher _slasher, IServiceManager _serviceManager, @@ -99,25 +109,30 @@ contract BLSRegistryCoordinatorWithIndices is EIP712, Initializable, IBLSRegistr function initialize( address _churnApprover, address _ejector, - OperatorSetParam[] memory _operatorSetParams, IPauserRegistry _pauserRegistry, - uint256 _initialPausedStatus + uint256 _initialPausedStatus, + OperatorSetParam[] memory _operatorSetParams, + uint96[] memory _minimumStakes, + IVoteWeigher.StrategyAndWeightingMultiplier[][] memory _strategyParams ) external initializer { - // set initial paused status + require( + _operatorSetParams.length == _minimumStakes.length && _minimumStakes.length == _strategyParams.length, + "BLSRegistryCoordinatorWithIndices.initialize: input length mismatch" + ); + + // Initialize roles _initializePauser(_pauserRegistry, _initialPausedStatus); - // set the churnApprover _setChurnApprover(_churnApprover); - // set the ejector _setEjector(_ejector); - // add registry contracts to the registries array + + // Add registry contracts to the registries array registries.push(address(stakeRegistry)); registries.push(address(blsPubkeyRegistry)); registries.push(address(indexRegistry)); - // set the operator set params - require(IVoteWeigher(address(stakeRegistry)).quorumCount() == _operatorSetParams.length, "BLSRegistryCoordinatorWithIndices: operator set params length mismatch"); - for (uint8 i = 0; i < _operatorSetParams.length; i++) { - _setOperatorSetParams(i, _operatorSetParams[i]); + // Create quorums + for (uint256 i = 0; i < _operatorSetParams.length; i++) { + _createQuorum(_operatorSetParams[i], _minimumStakes[i], _strategyParams[i]); } } @@ -337,16 +352,27 @@ contract BLSRegistryCoordinatorWithIndices is EIP712, Initializable, IBLSRegistr *******************************************************************************/ /** - * @notice Sets parameters of the operator set for the given `quorumNumber` + * @notice Creates a quorum and initializes it in each registry contract + */ + function createQuorum( + OperatorSetParam memory operatorSetParams, + uint96 minimumStake, + IVoteWeigher.StrategyAndWeightingMultiplier[] memory strategyParams + ) external virtual onlyServiceManagerOwner { + _createQuorum(operatorSetParams, minimumStake, strategyParams); + } + + /** + * @notice Updates a quorum's OperatorSetParams * @param quorumNumber is the quorum number to set the maximum number of operators for - * @param operatorSetParam is the parameters of the operator set for the `quorumNumber` + * @param operatorSetParams is the parameters of the operator set for the `quorumNumber` * @dev only callable by the service manager owner */ function setOperatorSetParams( uint8 quorumNumber, - OperatorSetParam memory operatorSetParam - ) external onlyServiceManagerOwner { - _setOperatorSetParams(quorumNumber, operatorSetParam); + OperatorSetParam memory operatorSetParams + ) external onlyServiceManagerOwner quorumExists(quorumNumber) { + _setOperatorSetParams(quorumNumber, operatorSetParams); } /** @@ -535,9 +561,32 @@ contract BLSRegistryCoordinatorWithIndices is EIP712, Initializable, IBLSRegistr EIP1271SignatureUtils.checkSignature_EIP1271(churnApprover, calculateOperatorChurnApprovalDigestHash(registeringOperatorId, operatorKickParams, signatureWithSaltAndExpiry.salt, signatureWithSaltAndExpiry.expiry), signatureWithSaltAndExpiry.signature); } - function _setOperatorSetParams(uint8 quorumNumber, OperatorSetParam memory operatorSetParam) internal { - _quorumOperatorSetParams[quorumNumber] = operatorSetParam; - emit OperatorSetParamsUpdated(quorumNumber, operatorSetParam); + /** + * @notice Creates and initializes a quorum in each registry contract + */ + function _createQuorum( + OperatorSetParam memory operatorSetParams, + uint96 minimumStake, + IVoteWeigher.StrategyAndWeightingMultiplier[] memory strategyParams + ) internal { + // Increment the total quorum count. Fails if we're already at the max + uint8 prevQuorumCount = quorumCount; + require(prevQuorumCount < MAX_QUORUM_COUNT, "BLSRegistryCoordinatorWithIndicies.createQuorum: max quorums reached"); + quorumCount = prevQuorumCount + 1; + + // The previous count is the new quorum's number + uint8 quorumNumber = prevQuorumCount; + + // Initialize the quorum here and in each registry + _setOperatorSetParams(quorumNumber, operatorSetParams); + stakeRegistry.createQuorum(quorumNumber, minimumStake, strategyParams); + indexRegistry.createQuorum(quorumNumber); + blsPubkeyRegistry.createQuorum(quorumNumber); + } + + function _setOperatorSetParams(uint8 quorumNumber, OperatorSetParam memory operatorSetParams) internal { + _quorumOperatorSetParams[quorumNumber] = operatorSetParams; + emit OperatorSetParamsUpdated(quorumNumber, operatorSetParams); } function _setChurnApprover(address newChurnApprover) internal { diff --git a/src/IndexRegistry.sol b/src/IndexRegistry.sol index c3b5ed5b..f2ebdffb 100644 --- a/src/IndexRegistry.sol +++ b/src/IndexRegistry.sol @@ -48,9 +48,12 @@ contract IndexRegistry is IndexRegistryStorage { for (uint256 i = 0; i < quorumNumbers.length; i++) { uint8 quorumNumber = uint8(quorumNumbers[i]); - //this is the would-be index of the operator being registered, the total number of operators for that quorum (which is last index + 1) - uint256 quorumHistoryLength = _totalOperatorsHistory[quorumNumber].length; - uint32 numOperators = quorumHistoryLength > 0 ? _totalOperatorsHistory[quorumNumber][quorumHistoryLength - 1].numOperators : 0; + // Check that the quorum exists + uint256 historyLength = _totalOperatorsHistory[quorumNumber].length; + require(historyLength != 0, "IndexRegistry.registerOperator: quorum does not exist"); + + // Get the number of operators currently in the quorum, which will be operator's new index + uint32 numOperators = historyLength > 0 ? _totalOperatorsHistory[quorumNumber][historyLength - 1].numOperators : 0; _updateOperatorIdToIndexHistory({ operatorId: operatorId, quorumNumber: quorumNumber, @@ -92,6 +95,11 @@ contract IndexRegistry is IndexRegistryStorage { for (uint256 i = 0; i < quorumNumbers.length; i++) { uint8 quorumNumber = uint8(quorumNumbers[i]); + + // Check that the quorum exists + uint256 historyLength = _totalOperatorsHistory[quorumNumber].length; + require(historyLength != 0, "IndexRegistry.registerOperator: quorum does not exist"); + uint32 indexOfOperatorToRemove = _operatorIdToIndexHistory[operatorId][quorumNumber][_operatorIdToIndexHistory[operatorId][quorumNumber].length - 1].index; _processOperatorRemoval({ operatorId: operatorId, @@ -101,11 +109,24 @@ contract IndexRegistry is IndexRegistryStorage { }); _updateTotalOperatorHistory({ quorumNumber: quorumNumber, - numOperators: _totalOperatorsHistory[quorumNumber][_totalOperatorsHistory[quorumNumber].length - 1].numOperators - 1 + numOperators: _totalOperatorsHistory[quorumNumber][historyLength - 1].numOperators - 1 }); } } + /** + * @notice Creates a new quorum by pushing its first quorum update + * @param quorumNumber The number of the new quorum + */ + function createQuorum(uint8 quorumNumber) public virtual onlyRegistryCoordinator { + require(_totalOperatorsHistory[quorumNumber].length == 0, "IndexRegistry.createQuorum: quorum already exists"); + + _totalOperatorsHistory[quorumNumber].push(QuorumUpdate({ + numOperators: 0, + fromBlockNumber: uint32(block.number) + })); + } + /******************************************************************************* INTERNAL FUNCTIONS *******************************************************************************/ @@ -116,12 +137,10 @@ contract IndexRegistry is IndexRegistryStorage { * @param numOperators is the number of operators in the quorum */ function _updateTotalOperatorHistory(uint8 quorumNumber, uint32 numOperators) internal { - QuorumUpdate memory quorumUpdate; - // In the case of totalOperatorsHistory, the index parameter is the number of operators in the quorum - quorumUpdate.numOperators = numOperators; - quorumUpdate.fromBlockNumber = uint32(block.number); - - _totalOperatorsHistory[quorumNumber].push(quorumUpdate); + _totalOperatorsHistory[quorumNumber].push(QuorumUpdate({ + numOperators: numOperators, + fromBlockNumber: uint32(block.number) + })); } /** @@ -130,10 +149,10 @@ contract IndexRegistry is IndexRegistryStorage { * @param index the latest index of that operator in the list of operators registered for this quorum */ function _updateOperatorIdToIndexHistory(bytes32 operatorId, uint8 quorumNumber, uint32 index) internal { - OperatorIndexUpdate memory latestOperatorIndex; - latestOperatorIndex.index = index; - latestOperatorIndex.fromBlockNumber = uint32(block.number); - _operatorIdToIndexHistory[operatorId][quorumNumber].push(latestOperatorIndex); + _operatorIdToIndexHistory[operatorId][quorumNumber].push(OperatorIndexUpdate({ + index: index, + fromBlockNumber: uint32(block.number) + })); emit QuorumIndexUpdate(operatorId, quorumNumber, index); } diff --git a/src/StakeRegistry.sol b/src/StakeRegistry.sol index 10bf2c1c..a4cc29db 100644 --- a/src/StakeRegistry.sol +++ b/src/StakeRegistry.sol @@ -7,7 +7,7 @@ import "src/interfaces/IServiceManager.sol"; import "src/interfaces/IStakeRegistry.sol"; import "src/interfaces/IRegistryCoordinator.sol"; import "src/StakeRegistryStorage.sol"; -import {VoteWeigherBase} from "src/VoteWeigherBase.sol"; +import "src/VoteWeigherBaseStorage.sol"; /** * @title A `Registry` that keeps track of stakes of operators for up to 256 quorums. @@ -18,8 +18,8 @@ import {VoteWeigherBase} from "src/VoteWeigherBase.sol"; * It allows an additional functionality (in addition to registering and deregistering) to update the stake of an operator. * @author Layr Labs, Inc. */ -contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { - /// @notice requires that the caller is the RegistryCoordinator +contract StakeRegistry is VoteWeigherBaseStorage, StakeRegistryStorage { + modifier onlyRegistryCoordinator() { require( msg.sender == address(registryCoordinator), @@ -28,42 +28,21 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { _; } + modifier onlyServiceManagerOwner() { + require(msg.sender == serviceManager.owner(), "StakeRegistry.onlyServiceManagerOwner: caller is not the owner of the serviceManager"); + _; + } + + modifier quorumExists(uint8 quorumNumber) { + require(_totalStakeHistory[quorumNumber].length != 0, "StakeRegistry.quorumExists: quorum does not exist"); + _; + } + constructor( IRegistryCoordinator _registryCoordinator, - IStrategyManager _strategyManager, + IDelegationManager _delegationManager, IServiceManager _serviceManager - ) VoteWeigherBase(_strategyManager, _serviceManager) StakeRegistryStorage(_registryCoordinator) {} - - /** - * @notice Sets the minimum stake for each quorum and adds `_quorumStrategiesConsideredAndMultipliers` for each - * quorum the Registry is being initialized with - */ - function initialize( - uint96[] memory _minimumStakeForQuorum, - StrategyAndWeightingMultiplier[][] memory _quorumStrategiesConsideredAndMultipliers - ) external virtual initializer { - _initialize(_minimumStakeForQuorum, _quorumStrategiesConsideredAndMultipliers); - } - - function _initialize( - uint96[] memory _minimumStakeForQuorum, - StrategyAndWeightingMultiplier[][] memory _quorumStrategiesConsideredAndMultipliers - ) internal virtual onlyInitializing { - // sanity check lengths - require( - _minimumStakeForQuorum.length == _quorumStrategiesConsideredAndMultipliers.length, - "Registry._initialize: minimumStakeForQuorum length mismatch" - ); - - // add the strategies considered and multipliers for each quorum - for (uint8 quorumNumber = 0; quorumNumber < _quorumStrategiesConsideredAndMultipliers.length; ) { - _setMinimumStakeForQuorum(quorumNumber, _minimumStakeForQuorum[quorumNumber]); - _createQuorum(_quorumStrategiesConsideredAndMultipliers[quorumNumber]); - unchecked { - ++quorumNumber; - } - } - } + ) VoteWeigherBaseStorage(_delegationManager, _serviceManager) StakeRegistryStorage(_registryCoordinator) {} /******************************************************************************* EXTERNAL FUNCTIONS @@ -78,9 +57,14 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { // for each quorum, loop through operators and see if they are a part of the quorum // if they are, get their new weight and update their individual stake history and the // quorum's total stake history accordingly + uint8 quorumCount = registryCoordinator.quorumCount(); for (uint8 quorumNumber = 0; quorumNumber < quorumCount; ) { int256 totalStakeDelta; + // TODO - not a huge fan of this dependency on the reg coord, but i do prefer this + // over the stakereg also keeping its own count. + require(_totalStakeHistory[quorumNumber].length != 0, "StakeRegistry.updateStakes: quorum does not exist"); + for (uint256 i = 0; i < operators.length; ) { bytes32 operatorId = registryCoordinator.getOperatorId(operators[i]); uint192 quorumBitmap = registryCoordinator.getCurrentQuorumBitmapByOperatorId(operatorId); @@ -133,20 +117,18 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { bytes32 operatorId, bytes calldata quorumNumbers ) public virtual onlyRegistryCoordinator { - // check the operator is registering for only valid quorums - require( - uint8(quorumNumbers[quorumNumbers.length - 1]) < quorumCount, - "StakeRegistry._registerOperator: greatest quorumNumber must be less than quorumCount" - ); for (uint256 i = 0; i < quorumNumbers.length; ) { + + uint8 quorumNumber = uint8(quorumNumbers[i]); + require(_totalStakeHistory[quorumNumber].length != 0, "StakeRegistry.registerOperator: quorum does not exist"); + /** * Update the operator's stake for the quorum and retrieve their current stake * as well as the change in stake. * - If this method returns `hasMinimumStake == false`, the operator has not met * the minimum stake requirement for this quorum */ - uint8 quorumNumber = uint8(quorumNumbers[i]); (int256 stakeDelta, bool hasMinimumStake) = _updateOperatorStake({ operator: operator, operatorId: operatorId, @@ -154,7 +136,7 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { }); require( hasMinimumStake, - "StakeRegistry._registerOperator: Operator does not meet minimum stake requirement for quorum" + "StakeRegistry.registerOperator: Operator does not meet minimum stake requirement for quorum" ); // Update this quorum's total stake @@ -186,8 +168,10 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { * the quorum's total stake to account for the removal */ for (uint256 i = 0; i < quorumNumbers.length; ) { - // Update the operator's stake for the quorum and retrieve the shares removed uint8 quorumNumber = uint8(quorumNumbers[i]); + require(_totalStakeHistory[quorumNumber].length != 0, "StakeRegistry.deregisterOperator: quorum does not exist"); + + // Update the operator's stake for the quorum and retrieve the shares removed int256 stakeDelta = _recordOperatorStakeUpdate({ operatorId: operatorId, quorumNumber: quorumNumber, @@ -203,15 +187,85 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { } } - /******************************************************************************* - EXTERNAL FUNCTIONS - SERVICE MANAGER OWNER - *******************************************************************************/ + /// @notice Create a new quorum and add the strategies and their associated weights to the quorum. + function createQuorum( + uint8 quorumNumber, + uint96 minimumStake, + StrategyAndWeightingMultiplier[] memory strategyParams + ) public virtual onlyRegistryCoordinator { + require(_totalStakeHistory[quorumNumber].length == 0, "StakeRegistry.createQuorum: quorum already exists"); + _addStrategyParams(quorumNumber, strategyParams); + _setMinimumStakeForQuorum(quorumNumber, minimumStake); + } - /// @notice Adjusts the `minimumStakeFirstQuorum` -- i.e. the node stake (weight) requirement for inclusion in the 1st quorum. - function setMinimumStakeForQuorum(uint8 quorumNumber, uint96 minimumStake) external onlyServiceManagerOwner { + function setMinimumStakeForQuorum( + uint8 quorumNumber, + uint96 minimumStake + ) public virtual onlyServiceManagerOwner quorumExists(quorumNumber) { _setMinimumStakeForQuorum(quorumNumber, minimumStake); } + /** + * @notice Adds strategies and weights to the quorum + * @dev Checks to make sure that the *same* strategy cannot be added multiple times (checks against both against existing and new strategies). + * @dev This function has no check to make sure that the strategies for a single quorum have the same underlying asset. This is a concious choice, + * since a middleware may want, e.g., a stablecoin quorum that accepts USDC, USDT, DAI, etc. as underlying assets and trades them as "equivalent". + */ + function addStrategyParams( + uint8 quorumNumber, + StrategyAndWeightingMultiplier[] memory strategyParams + ) public virtual onlyServiceManagerOwner quorumExists(quorumNumber) { + _addStrategyParams(quorumNumber, strategyParams); + } + + /** + * @notice Remove strategies and their associated weights from the quorum's considered strategies + * @dev higher indices should be *first* in the list of @param indicesToRemove, since otherwise + * the removal of lower index entries will cause a shift in the indices of the other strategies to remove + */ + function removeStrategyParams( + uint8 quorumNumber, + uint256[] memory indicesToRemove + ) public virtual onlyServiceManagerOwner quorumExists(quorumNumber) { + uint256 toRemoveLength = indicesToRemove.length; + require(toRemoveLength > 0, "StakeRegistry.removeStrategyParams: no indices to remove provided"); + + StrategyAndWeightingMultiplier[] storage strategyParams = strategiesConsideredAndMultipliers[quorumNumber]; + + for (uint256 i = 0; i < toRemoveLength; i++) { + emit StrategyRemovedFromQuorum(quorumNumber, strategyParams[indicesToRemove[i]].strategy); + emit StrategyMultiplierUpdated(quorumNumber, strategyParams[indicesToRemove[i]].strategy, 0); + + // Replace index to remove with the last item in the list, then pop the last item + strategyParams[indicesToRemove[i]] = strategyParams[strategyParams.length - 1]; + strategyParams.pop(); + } + } + + /** + * @notice Modifys the weights of existing strategies for a specific quorum + * @param quorumNumber is the quorum number to which the strategies belong + * @param strategyIndices are the indices of the strategies to change + * @param newMultipliers are the new multipliers for the strategies + */ + function modifyStrategyParams( + uint8 quorumNumber, + uint256[] calldata strategyIndices, + uint96[] calldata newMultipliers + ) public virtual onlyServiceManagerOwner quorumExists(quorumNumber) { + uint256 numStrats = strategyIndices.length; + require(numStrats > 0, "StakeRegistry.modifyStrategyParams: no strategy indices provided"); + require(newMultipliers.length == numStrats, "StakeRegistry.modifyStrategyParams: input length mismatch"); + + StrategyAndWeightingMultiplier[] storage strategyParams = strategiesConsideredAndMultipliers[quorumNumber]; + + for (uint256 i = 0; i < numStrats; i++) { + // Change the strategy's associated multiplier + strategyParams[strategyIndices[i]].multiplier = newMultipliers[i]; + emit StrategyMultiplierUpdated(quorumNumber, strategyParams[strategyIndices[i]].strategy, newMultipliers[i]); + } + } + /******************************************************************************* INTERNAL FUNCTIONS *******************************************************************************/ @@ -258,7 +312,7 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { * Get the operator's current stake for the quorum. If their stake * is below the quorum's threshold, set their stake to 0 */ - uint96 currentStake = weightOfOperatorForQuorum(quorumNumber, operator); + uint96 currentStake = _weightOfOperatorForQuorum(quorumNumber, operator); if (currentStake < minimumStakeForQuorum[quorumNumber]) { currentStake = uint96(0); } else { @@ -359,6 +413,52 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { } } + /** + * @notice Adds `strategyParams` to the `quorumNumber`-th quorum. + * @dev Checks to make sure that the *same* strategy cannot be added multiple times (checks against both against existing and new strategies). + * @dev This function has no check to make sure that the strategies for a single quorum have the same underlying asset. This is a concious choice, + * since a middleware may want, e.g., a stablecoin quorum that accepts USDC, USDT, DAI, etc. as underlying assets and trades them as "equivalent". + */ + function _addStrategyParams( + uint8 quorumNumber, + StrategyAndWeightingMultiplier[] memory strategyParams + ) internal { + require(strategyParams.length > 0, "StakeRegistry._addStrategyParams: no strategies provided"); + uint256 numStratsToAdd = strategyParams.length; + uint256 numStratsExisting = strategiesConsideredAndMultipliers[quorumNumber].length; + require( + numStratsExisting + numStratsToAdd <= MAX_WEIGHING_FUNCTION_LENGTH, + "StakeRegistry._addStrategyParams: exceed MAX_WEIGHING_FUNCTION_LENGTH" + ); + for (uint256 i = 0; i < numStratsToAdd; ) { + // fairly gas-expensive internal loop to make sure that the *same* strategy cannot be added multiple times + for (uint256 j = 0; j < (numStratsExisting + i); ) { + require( + strategiesConsideredAndMultipliers[quorumNumber][j].strategy != + strategyParams[i].strategy, + "StakeRegistry._addStrategyParams: cannot add same strategy 2x" + ); + unchecked { + ++j; + } + } + require( + strategyParams[i].multiplier > 0, + "StakeRegistry._addStrategyParams: cannot add strategy with zero weight" + ); + strategiesConsideredAndMultipliers[quorumNumber].push(strategyParams[i]); + emit StrategyAddedToQuorum(quorumNumber, strategyParams[i].strategy); + emit StrategyMultiplierUpdated( + quorumNumber, + strategyParams[i].strategy, + strategyParams[i].multiplier + ); + unchecked { + ++i; + } + } + } + /// @notice Returns the change between a previous and current value as a signed int function _calculateDelta(uint96 prev, uint96 cur) internal pure returns (int256) { return int256(uint256(cur)) - int256(uint256(prev)); @@ -388,10 +488,64 @@ contract StakeRegistry is VoteWeigherBase, StakeRegistryStorage { ); } + /** + * @notice This function computes the total weight of the @param operator in the quorum @param quorumNumber. + * @dev this method DOES NOT check that the quorum exists + */ + function _weightOfOperatorForQuorum(uint8 quorumNumber, address operator) internal view returns (uint96) { + uint96 weight; + uint256 stratsLength = strategiesConsideredAndMultipliersLength(quorumNumber); + StrategyAndWeightingMultiplier memory strategyAndMultiplier; + + for (uint256 i = 0; i < stratsLength;) { + // accessing i^th StrategyAndWeightingMultiplier struct for the quorumNumber + strategyAndMultiplier = strategiesConsideredAndMultipliers[quorumNumber][i]; + + // shares of the operator in the strategy + uint256 sharesAmount = delegation.operatorShares(operator, strategyAndMultiplier.strategy); + + // add the weight from the shares for this strategy to the total weight + if (sharesAmount > 0) { + weight += uint96(sharesAmount * strategyAndMultiplier.multiplier / WEIGHTING_DIVISOR); + } + + unchecked { + ++i; + } + } + + return weight; + } + /******************************************************************************* VIEW FUNCTIONS *******************************************************************************/ + /** + * @notice This function computes the total weight of the @param operator in the quorum @param quorumNumber. + * @dev reverts if the quorum does not exist + */ + function weightOfOperatorForQuorum( + uint8 quorumNumber, + address operator + ) public virtual view quorumExists(quorumNumber) returns (uint96) { + return _weightOfOperatorForQuorum(quorumNumber, operator); + } + + /// @notice Returns the length of the dynamic array stored in `strategiesConsideredAndMultipliers[quorumNumber]`. + function strategiesConsideredAndMultipliersLength(uint8 quorumNumber) public view returns (uint256) { + return strategiesConsideredAndMultipliers[quorumNumber].length; + } + + /// @notice Returns the strategy and weight multiplier for the `index`'th strategy in the quorum `quorumNumber` + function strategyAndWeightingMultiplierForQuorumByIndex( + uint8 quorumNumber, + uint256 index + ) public view returns (StrategyAndWeightingMultiplier memory) + { + return strategiesConsideredAndMultipliers[quorumNumber][index]; + } + /** * @notice Returns the entire `operatorIdToStakeHistory[operatorId][quorumNumber]` array. * @param operatorId The id of the operator of interest. diff --git a/src/VoteWeigherBase.sol b/src/VoteWeigherBase.sol deleted file mode 100644 index 24e06faf..00000000 --- a/src/VoteWeigherBase.sol +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; -import "src/VoteWeigherBaseStorage.sol"; - -/** - * @title A simple implementation of the `IVoteWeigher` interface. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - * @notice This contract is used for - * - computing the total weight of an operator for any of the quorums that are considered - * by the middleware - * - addition and removal of strategies and the associated weighting criteria that are assigned - * by the middleware for each of the quorum(s) - */ -contract VoteWeigherBase is VoteWeigherBaseStorage { - /// @notice when applied to a function, ensures that the function is only callable by the current `owner` of the `serviceManager` - modifier onlyServiceManagerOwner() { - require(msg.sender == serviceManager.owner(), "VoteWeigherBase.onlyServiceManagerOwner: caller is not the owner of the serviceManager"); - _; - } - - /// @notice when applied to a function, ensures that the `quorumNumber` corresponds to a valid quorum added to the VoteWeigher - modifier validQuorumNumber(uint8 quorumNumber) { - require(quorumNumber < quorumCount, "VoteWeigherBase.validQuorumNumber: quorumNumber is not valid"); - _; - } - - /// @notice Sets the (immutable) `strategyManager` and `serviceManager` addresses - constructor( - IStrategyManager _strategyManager, - IServiceManager _serviceManager - ) VoteWeigherBaseStorage(_strategyManager, _serviceManager) {} - - /******************************************************************************* - EXTERNAL FUNCTIONS - SERVICE MANAGER OWNER - *******************************************************************************/ - - /// @notice Create a new quorum and add the strategies and their associated weights to the quorum. - function createQuorum( - StrategyAndWeightingMultiplier[] memory _strategiesConsideredAndMultipliers - ) external virtual onlyServiceManagerOwner { - _createQuorum(_strategiesConsideredAndMultipliers); - } - - /// @notice Adds new strategies and the associated multipliers to the @param quorumNumber. - function addStrategiesConsideredAndMultipliers( - uint8 quorumNumber, - StrategyAndWeightingMultiplier[] memory _newStrategiesConsideredAndMultipliers - ) external virtual onlyServiceManagerOwner validQuorumNumber(quorumNumber) { - _addStrategiesConsideredAndMultipliers(quorumNumber, _newStrategiesConsideredAndMultipliers); - } - - /** - * @notice This function is used for removing strategies and their associated weights from the - * mapping strategiesConsideredAndMultipliers for a specific @param quorumNumber. - * @dev higher indices should be *first* in the list of @param indicesToRemove, since otherwise - * the removal of lower index entries will cause a shift in the indices of the other strategiesToRemove - */ - function removeStrategiesConsideredAndMultipliers( - uint8 quorumNumber, - uint256[] calldata indicesToRemove - ) external virtual onlyServiceManagerOwner validQuorumNumber(quorumNumber) { - uint256 indicesToRemoveLength = indicesToRemove.length; - require(indicesToRemoveLength > 0, "VoteWeigherBase.removeStrategiesConsideredAndMultipliers: no indices to remove provided"); - for (uint256 i = 0; i < indicesToRemoveLength;) { - emit StrategyRemovedFromQuorum(quorumNumber, strategiesConsideredAndMultipliers[quorumNumber][indicesToRemove[i]].strategy); - emit StrategyMultiplierUpdated(quorumNumber, strategiesConsideredAndMultipliers[quorumNumber][indicesToRemove[i]].strategy, 0); - // remove strategy and its associated multiplier - strategiesConsideredAndMultipliers[quorumNumber][indicesToRemove[i]] = strategiesConsideredAndMultipliers[ - quorumNumber - ][strategiesConsideredAndMultipliers[quorumNumber].length - 1]; - strategiesConsideredAndMultipliers[quorumNumber].pop(); - - unchecked { - ++i; - } - } - } - - /** - * @notice This function is used for modifying the weights of strategies that are already in the - * mapping strategiesConsideredAndMultipliers for a specific - * @param quorumNumber is the quorum number to change the strategy for - * @param strategyIndices are the indices of the strategies to change - * @param newMultipliers are the new multipliers for the strategies - */ - function modifyStrategyWeights( - uint8 quorumNumber, - uint256[] calldata strategyIndices, - uint96[] calldata newMultipliers - ) external virtual onlyServiceManagerOwner validQuorumNumber(quorumNumber) { - uint256 numStrats = strategyIndices.length; - require(numStrats > 0, "VoteWeigherBase.modifyStrategyWeights: no strategy indices provided"); - // sanity check on input lengths - require(newMultipliers.length == numStrats, "VoteWeigherBase.modifyStrategyWeights: input length mismatch"); - - for (uint256 i = 0; i < numStrats; ) { - // change the strategy's associated multiplier - strategiesConsideredAndMultipliers[quorumNumber][strategyIndices[i]].multiplier = newMultipliers[i]; - emit StrategyMultiplierUpdated(quorumNumber, strategiesConsideredAndMultipliers[quorumNumber][strategyIndices[i]].strategy, newMultipliers[i]); - unchecked { - ++i; - } - } - } - - /******************************************************************************* - INTERNAL FUNCTIONS - *******************************************************************************/ - - /** - * @notice Creates a quorum with the given_strategiesConsideredAndMultipliers. - */ - function _createQuorum( - StrategyAndWeightingMultiplier[] memory _strategiesConsideredAndMultipliers - ) internal { - uint16 quorumCountMem = quorumCount; - require(quorumCountMem < MAX_QUORUM_COUNT, "VoteWeigherBase._createQuorum: number of quorums cannot exceed MAX_QUORUM_COUNT"); - uint8 quorumNumber = uint8(quorumCountMem); - // increment quorumCount - quorumCount = quorumCountMem + 1; - // add the strategies and their associated weights to the quorum - _addStrategiesConsideredAndMultipliers(quorumNumber, _strategiesConsideredAndMultipliers); - // emit event - emit QuorumCreated(quorumNumber); - } - - /** - * @notice Adds `_newStrategiesConsideredAndMultipliers` to the `quorumNumber`-th quorum. - * @dev Checks to make sure that the *same* strategy cannot be added multiple times (checks against both against existing and new strategies). - * @dev This function has no check to make sure that the strategies for a single quorum have the same underlying asset. This is a concious choice, - * since a middleware may want, e.g., a stablecoin quorum that accepts USDC, USDT, DAI, etc. as underlying assets and trades them as "equivalent". - */ - function _addStrategiesConsideredAndMultipliers( - uint8 quorumNumber, - StrategyAndWeightingMultiplier[] memory _newStrategiesConsideredAndMultipliers - ) internal { - require(_newStrategiesConsideredAndMultipliers.length > 0, "VoteWeigherBase._addStrategiesConsideredAndMultipliers: no strategies provided"); - uint256 numStratsToAdd = _newStrategiesConsideredAndMultipliers.length; - uint256 numStratsExisting = strategiesConsideredAndMultipliers[quorumNumber].length; - require( - numStratsExisting + numStratsToAdd <= MAX_WEIGHING_FUNCTION_LENGTH, - "VoteWeigherBase._addStrategiesConsideredAndMultipliers: exceed MAX_WEIGHING_FUNCTION_LENGTH" - ); - for (uint256 i = 0; i < numStratsToAdd; ) { - // fairly gas-expensive internal loop to make sure that the *same* strategy cannot be added multiple times - for (uint256 j = 0; j < (numStratsExisting + i); ) { - require( - strategiesConsideredAndMultipliers[quorumNumber][j].strategy != - _newStrategiesConsideredAndMultipliers[i].strategy, - "VoteWeigherBase._addStrategiesConsideredAndMultipliers: cannot add same strategy 2x" - ); - unchecked { - ++j; - } - } - require( - _newStrategiesConsideredAndMultipliers[i].multiplier > 0, - "VoteWeigherBase._addStrategiesConsideredAndMultipliers: cannot add strategy with zero weight" - ); - strategiesConsideredAndMultipliers[quorumNumber].push(_newStrategiesConsideredAndMultipliers[i]); - emit StrategyAddedToQuorum(quorumNumber, _newStrategiesConsideredAndMultipliers[i].strategy); - emit StrategyMultiplierUpdated( - quorumNumber, - _newStrategiesConsideredAndMultipliers[i].strategy, - _newStrategiesConsideredAndMultipliers[i].multiplier - ); - unchecked { - ++i; - } - } - } - - /******************************************************************************* - VIEW FUNCTIONS - *******************************************************************************/ - - /// @notice Returns the length of the dynamic array stored in `strategiesConsideredAndMultipliers[quorumNumber]`. - function strategiesConsideredAndMultipliersLength(uint8 quorumNumber) public view returns (uint256) { - return strategiesConsideredAndMultipliers[quorumNumber].length; - } - - /// @notice Returns the strategy and weight multiplier for the `index`'th strategy in the quorum `quorumNumber` - function strategyAndWeightingMultiplierForQuorumByIndex( - uint8 quorumNumber, - uint256 index - ) public view returns (StrategyAndWeightingMultiplier memory) - { - return strategiesConsideredAndMultipliers[quorumNumber][index]; - } - - - /// TODO remove when core gets updated - function weightOfOperatorForQuorumView( - uint8 quorumNumber, - address operator - ) public virtual view validQuorumNumber(quorumNumber) returns (uint96) {} - /** - * @notice This function computes the total weight of the @param operator in the quorum @param quorumNumber. - * @dev reverts in the case that `quorumNumber` is greater than or equal to `quorumCount` - */ - function weightOfOperatorForQuorum( - uint8 quorumNumber, - address operator - ) public virtual view validQuorumNumber(quorumNumber) returns (uint96) { - uint96 weight; - uint256 stratsLength = strategiesConsideredAndMultipliersLength(quorumNumber); - StrategyAndWeightingMultiplier memory strategyAndMultiplier; - - for (uint256 i = 0; i < stratsLength;) { - // accessing i^th StrategyAndWeightingMultiplier struct for the quorumNumber - strategyAndMultiplier = strategiesConsideredAndMultipliers[quorumNumber][i]; - - // shares of the operator in the strategy - uint256 sharesAmount = delegation.operatorShares(operator, strategyAndMultiplier.strategy); - - // add the weight from the shares for this strategy to the total weight - if (sharesAmount > 0) { - weight += uint96(sharesAmount * strategyAndMultiplier.multiplier / WEIGHTING_DIVISOR); - } - - unchecked { - ++i; - } - } - - return weight; - } -} diff --git a/src/VoteWeigherBaseStorage.sol b/src/VoteWeigherBaseStorage.sol index e09dbb4a..45d53655 100644 --- a/src/VoteWeigherBaseStorage.sol +++ b/src/VoteWeigherBaseStorage.sol @@ -21,24 +21,13 @@ abstract contract VoteWeigherBaseStorage is Initializable, IVoteWeigher { uint8 public constant MAX_WEIGHING_FUNCTION_LENGTH = 32; /// @notice Constant used as a divisor in dealing with BIPS amounts. uint256 internal constant MAX_BIPS = 10000; - /// @notice The maximum number of quorums that the VoteWeigher is considering. - uint8 public constant MAX_QUORUM_COUNT = 192; /// @notice The address of the Delegation contract for EigenLayer. IDelegationManager public immutable delegation; - /// @notice The address of the StrategyManager contract for EigenLayer. - IStrategyManager public immutable strategyManager; - - /// @notice The address of the Slasher contract for EigenLayer. - ISlasher public immutable slasher; - /// @notice The ServiceManager contract for this middleware, where tasks are created / initiated. IServiceManager public immutable serviceManager; - /// @notice The number of quorums that the VoteWeigher is considering. - uint16 public quorumCount; - /** * @notice mapping from quorum number to the list of strategies considered and their * corresponding multipliers for that specific quorum @@ -46,12 +35,10 @@ abstract contract VoteWeigherBaseStorage is Initializable, IVoteWeigher { mapping(uint8 => StrategyAndWeightingMultiplier[]) public strategiesConsideredAndMultipliers; constructor( - IStrategyManager _strategyManager, + IDelegationManager _delegationManager, IServiceManager _serviceManager ) { - strategyManager = _strategyManager; - delegation = _strategyManager.delegation(); - slasher = _strategyManager.slasher(); + delegation = _delegationManager; serviceManager = _serviceManager; // disable initializers so that the implementation contract cannot be initialized _disableInitializers(); diff --git a/src/interfaces/IRegistry.sol b/src/interfaces/IRegistry.sol index 0b8bb3c0..49e7d545 100644 --- a/src/interfaces/IRegistry.sol +++ b/src/interfaces/IRegistry.sol @@ -1,4 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity >=0.5.0; -import "eigenlayer-contracts/src/contracts/interfaces/IRegistry.sol"; +import "./IRegistryCoordinator.sol"; + +/** + * @title Minimal interface for a `Registry`-type contract. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice Functions related to the registration process itself have been intentionally excluded + * because their function signatures may vary significantly. + */ +interface IRegistry { + function registryCoordinator() external view returns (IRegistryCoordinator); +} diff --git a/src/interfaces/IRegistryCoordinator.sol b/src/interfaces/IRegistryCoordinator.sol index 7bbad67e..3ee5b1a9 100644 --- a/src/interfaces/IRegistryCoordinator.sol +++ b/src/interfaces/IRegistryCoordinator.sol @@ -1,4 +1,97 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "eigenlayer-contracts/src/contracts/interfaces/IRegistryCoordinator.sol"; +/** + * @title Interface for a contract that coordinates between various registries for an AVS. + * @author Layr Labs, Inc. + */ +interface IRegistryCoordinator { + // EVENTS + /// Emits when an operator is registered + event OperatorRegistered(address indexed operator, bytes32 indexed operatorId); + + /// Emits when an operator is deregistered + event OperatorDeregistered(address indexed operator, bytes32 indexed operatorId); + + // DATA STRUCTURES + enum OperatorStatus + { + // default is NEVER_REGISTERED + NEVER_REGISTERED, + REGISTERED, + DEREGISTERED + } + + // STRUCTS + + /** + * @notice Data structure for storing info on operators + */ + struct Operator { + // the id of the operator, which is likely the keccak256 hash of the operator's public key if using BLSRegsitry + bytes32 operatorId; + // indicates whether the operator is actively registered for serving the middleware or not + OperatorStatus status; + } + + /** + * @notice Data structure for storing info on quorum bitmap updates where the `quorumBitmap` is the bitmap of the + * quorums the operator is registered for starting at (inclusive)`updateBlockNumber` and ending at (exclusive) `nextUpdateBlockNumber` + * @dev nextUpdateBlockNumber is initialized to 0 for the latest update + */ + struct QuorumBitmapUpdate { + uint32 updateBlockNumber; + uint32 nextUpdateBlockNumber; + uint192 quorumBitmap; + } + + /// @notice Returns the operator struct for the given `operator` + function getOperator(address operator) external view returns (Operator memory); + + /// @notice Returns the operatorId for the given `operator` + function getOperatorId(address operator) external view returns (bytes32); + + /// @notice Returns the operator address for the given `operatorId` + function getOperatorFromId(bytes32 operatorId) external view returns (address operator); + + /// @notice Returns the status for the given `operator` + function getOperatorStatus(address operator) external view returns (IRegistryCoordinator.OperatorStatus); + + /// @notice Returns the indices of the quorumBitmaps for the provided `operatorIds` at the given `blockNumber` + function getQuorumBitmapIndicesByOperatorIdsAtBlockNumber(uint32 blockNumber, bytes32[] memory operatorIds) external view returns (uint32[] memory); + + /** + * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index` + * @dev reverts if `index` is incorrect + */ + function getQuorumBitmapByOperatorIdAtBlockNumberByIndex(bytes32 operatorId, uint32 blockNumber, uint256 index) external view returns (uint192); + + /// @notice Returns the `index`th entry in the operator with `operatorId`'s bitmap history + function getQuorumBitmapUpdateByOperatorIdByIndex(bytes32 operatorId, uint256 index) external view returns (QuorumBitmapUpdate memory); + + /// @notice Returns the current quorum bitmap for the given `operatorId` + function getCurrentQuorumBitmapByOperatorId(bytes32 operatorId) external view returns (uint192); + + /// @notice Returns the length of the quorum bitmap history for the given `operatorId` + function getQuorumBitmapUpdateByOperatorIdLength(bytes32 operatorId) external view returns (uint256); + + /// @notice Returns the registry at the desired index + function registries(uint256) external view returns (address); + + /// @notice Returns the number of registries + function numRegistries() external view returns (uint256); + + /** + * @notice Registers msg.sender as an operator with the middleware + * @param quorumNumbers are the bytes representing the quorum numbers that the operator is registering for + * @param registrationData is the data that is decoded to get the operator's registration information + */ + function registerOperatorWithCoordinator(bytes memory quorumNumbers, bytes calldata registrationData) external; + + /** + * @notice Deregisters the msg.sender as an operator from the middleware + * @param quorumNumbers are the bytes representing the quorum numbers that the operator is registered for + * @param deregistrationData is the the data that is decoded to get the operator's deregistration information + */ + function deregisterOperatorWithCoordinator(bytes calldata quorumNumbers, bytes calldata deregistrationData) external; +} \ No newline at end of file diff --git a/src/interfaces/IStakeRegistry.sol b/src/interfaces/IStakeRegistry.sol index 713df01e..078eb570 100644 --- a/src/interfaces/IStakeRegistry.sol +++ b/src/interfaces/IStakeRegistry.sol @@ -1,4 +1,160 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "eigenlayer-contracts/src/contracts/interfaces/IStakeRegistry.sol"; +import "./IRegistry.sol"; + +/** + * @title Interface for a `Registry` that keeps track of stakes of operators for up to 256 quorums. + * @author Layr Labs, Inc. + */ +interface IStakeRegistry is IRegistry { + // EVENTS + /// @notice emitted whenever the stake of `operator` is updated + event StakeUpdate( + bytes32 indexed operatorId, + uint8 quorumNumber, + uint96 stake + ); + + // DATA STRUCTURES + + /// @notice struct used to store the stakes of an individual operator or the sum of all operators' stakes, for storage + struct OperatorStakeUpdate { + // the block number at which the stake amounts were updated and stored + uint32 updateBlockNumber; + // the block number at which the *next update* occurred. + /// @notice This entry has the value **0** until another update takes place. + uint32 nextUpdateBlockNumber; + // stake weight for the quorum + uint96 stake; + } + + // EVENTS + event MinimumStakeForQuorumUpdated(uint8 indexed quorumNumber, uint96 minimumStake); + + /** + * @notice Registers the `operator` with `operatorId` for the specified `quorumNumbers`. + * @param operator The address of the operator to register. + * @param operatorId The id of the operator to register. + * @param quorumNumbers The quorum numbers the operator is registering for, where each byte is an 8 bit integer quorumNumber. + * @dev access restricted to the RegistryCoordinator + * @dev Preconditions (these are assumed, not validated in this contract): + * 1) `quorumNumbers` has no duplicates + * 2) `quorumNumbers.length` != 0 + * 3) `quorumNumbers` is ordered in ascending order + * 4) the operator is not already registered + */ + function registerOperator(address operator, bytes32 operatorId, bytes memory quorumNumbers) external; + + /** + * @notice Deregisters the operator with `operatorId` for the specified `quorumNumbers`. + * @param operatorId The id of the operator to deregister. + * @param quorumNumbers The quorum numbers the operator is deregistering from, where each byte is an 8 bit integer quorumNumber. + * @dev access restricted to the RegistryCoordinator + * @dev Preconditions (these are assumed, not validated in this contract): + * 1) `quorumNumbers` has no duplicates + * 2) `quorumNumbers.length` != 0 + * 3) `quorumNumbers` is ordered in ascending order + * 4) the operator is not already deregistered + * 5) `quorumNumbers` is a subset of the quorumNumbers that the operator is registered for + */ + function deregisterOperator(bytes32 operatorId, bytes memory quorumNumbers) external; + + /// @notice In order to register for a quorum i, an operator must have at least `minimumStakeForQuorum[i]` + function minimumStakeForQuorum(uint256 quorumNumber) external view returns (uint96); + + /** + * @notice Returns the entire `operatorIdToStakeHistory[operatorId][quorumNumber]` array. + * @param operatorId The id of the operator of interest. + * @param quorumNumber The quorum number to get the stake for. + */ + function getOperatorIdToStakeHistory(bytes32 operatorId, uint8 quorumNumber) external view returns (OperatorStakeUpdate[] memory); + + function getLengthOfTotalStakeHistoryForQuorum(uint8 quorumNumber) external view returns (uint256); + + /** + * @notice Returns the `index`-th entry in the dynamic array of total stake, `totalStakeHistory` for quorum `quorumNumber`. + * @param quorumNumber The quorum number to get the stake for. + * @param index Array index for lookup, within the dynamic array `totalStakeHistory[quorumNumber]`. + */ + function getTotalStakeUpdateForQuorumFromIndex(uint8 quorumNumber, uint256 index) external view returns (OperatorStakeUpdate memory); + + /// @notice Returns the indices of the operator stakes for the provided `quorumNumber` at the given `blockNumber` + function getStakeUpdateIndexForOperatorIdForQuorumAtBlockNumber(bytes32 operatorId, uint8 quorumNumber, uint32 blockNumber) + external + view + returns (uint32); + + /// @notice Returns the indices of the total stakes for the provided `quorumNumbers` at the given `blockNumber` + function getTotalStakeIndicesByQuorumNumbersAtBlockNumber(uint32 blockNumber, bytes calldata quorumNumbers) external view returns(uint32[] memory) ; + + /** + * @notice Returns the `index`-th entry in the `operatorIdToStakeHistory[operatorId][quorumNumber]` array. + * @param quorumNumber The quorum number to get the stake for. + * @param operatorId The id of the operator of interest. + * @param index Array index for lookup, within the dynamic array `operatorIdToStakeHistory[operatorId][quorumNumber]`. + * @dev Function will revert if `index` is out-of-bounds. + */ + function getStakeUpdateForQuorumFromOperatorIdAndIndex(uint8 quorumNumber, bytes32 operatorId, uint256 index) + external + view + returns (OperatorStakeUpdate memory); + + /** + * @notice Returns the most recent stake weight for the `operatorId` for a certain quorum + * @dev Function returns an OperatorStakeUpdate struct with **every entry equal to 0** in the event that the operator has no stake history + */ + function getMostRecentStakeUpdateByOperatorId(bytes32 operatorId, uint8 quorumNumber) external view returns (OperatorStakeUpdate memory); + + /** + * @notice Returns the stake weight corresponding to `operatorId` for quorum `quorumNumber`, at the + * `index`-th entry in the `operatorIdToStakeHistory[operatorId][quorumNumber]` array if the entry + * corresponds to the operator's stake at `blockNumber`. Reverts otherwise. + * @param quorumNumber The quorum number to get the stake for. + * @param operatorId The id of the operator of interest. + * @param index Array index for lookup, within the dynamic array `operatorIdToStakeHistory[operatorId][quorumNumber]`. + * @param blockNumber Block number to make sure the stake is from. + * @dev Function will revert if `index` is out-of-bounds. + * @dev used the BLSSignatureChecker to get past stakes of signing operators + */ + function getStakeForQuorumAtBlockNumberFromOperatorIdAndIndex(uint8 quorumNumber, uint32 blockNumber, bytes32 operatorId, uint256 index) + external + view + returns (uint96); + + /** + * @notice Returns the total stake weight for quorum `quorumNumber`, at the `index`-th entry in the + * `totalStakeHistory[quorumNumber]` array if the entry corresponds to the total stake at `blockNumber`. + * Reverts otherwise. + * @param quorumNumber The quorum number to get the stake for. + * @param index Array index for lookup, within the dynamic array `totalStakeHistory[quorumNumber]`. + * @param blockNumber Block number to make sure the stake is from. + * @dev Function will revert if `index` is out-of-bounds. + * @dev used the BLSSignatureChecker to get past stakes of signing operators + */ + function getTotalStakeAtBlockNumberFromIndex(uint8 quorumNumber, uint32 blockNumber, uint256 index) external view returns (uint96); + + /** + * @notice Returns the most recent stake weight for the `operatorId` for quorum `quorumNumber` + * @dev Function returns weight of **0** in the event that the operator has no stake history + */ + function getCurrentOperatorStakeForQuorum(bytes32 operatorId, uint8 quorumNumber) external view returns (uint96); + + /// @notice Returns the stake of the operator for the provided `quorumNumber` at the given `blockNumber` + function getStakeForOperatorIdForQuorumAtBlockNumber(bytes32 operatorId, uint8 quorumNumber, uint32 blockNumber) + external + view + returns (uint96); + + /** + * @notice Returns the stake weight from the latest entry in `_totalStakeHistory` for quorum `quorumNumber`. + * @dev Will revert if `_totalStakeHistory[quorumNumber]` is empty. + */ + function getCurrentTotalStakeForQuorum(uint8 quorumNumber) external view returns (uint96); + + /** + * @notice Used for updating information on deposits of nodes. + * @param operators are the addresses of the operators whose stake information is getting updated + */ + function updateStakes(address[] memory operators) external; +} \ No newline at end of file diff --git a/test/harnesses/StakeRegistryHarness.sol b/test/harnesses/StakeRegistryHarness.sol index 96fc9c94..235674c7 100644 --- a/test/harnesses/StakeRegistryHarness.sol +++ b/test/harnesses/StakeRegistryHarness.sol @@ -5,7 +5,7 @@ import "src/StakeRegistry.sol"; // wrapper around the StakeRegistry contract that exposes the internal functions for unit testing. contract StakeRegistryHarness is StakeRegistry { - mapping(uint8 => mapping(address => uint96)) private _weightOfOperatorForQuorum; + mapping(uint8 => mapping(address => uint96)) private __weightOfOperatorForQuorum; constructor( IRegistryCoordinator _registryCoordinator, @@ -28,36 +28,28 @@ contract StakeRegistryHarness is StakeRegistry { // mocked function so we can set this arbitrarily without having to mock other elements function weightOfOperatorForQuorum(uint8 quorumNumber, address operator) public override view returns(uint96) { - return _weightOfOperatorForQuorum[quorumNumber][operator]; - } - - /// TODO remove when core gets updated - function weightOfOperatorForQuorumView(uint8 quorumNumber, address operator) public override view returns(uint96) { - return _weightOfOperatorForQuorum[quorumNumber][operator]; + return __weightOfOperatorForQuorum[quorumNumber][operator]; } // mocked function so we can set this arbitrarily without having to mock other elements function setOperatorWeight(uint8 quorumNumber, address operator, uint96 weight) external { - _weightOfOperatorForQuorum[quorumNumber][operator] = weight; + __weightOfOperatorForQuorum[quorumNumber][operator] = weight; } // mocked function to register an operator without having to mock other elements // This is just a copy/paste from `registerOperator`, since that no longer uses an internal method function registerOperatorNonCoordinator(address operator, bytes32 operatorId, bytes calldata quorumNumbers) external { - // check the operator is registering for only valid quorums - require( - uint8(quorumNumbers[quorumNumbers.length - 1]) < quorumCount, - "StakeRegistry._registerOperator: greatest quorumNumber must be less than quorumCount" - ); - for (uint256 i = 0; i < quorumNumbers.length; ) { + + uint8 quorumNumber = uint8(quorumNumbers[i]); + require(_totalStakeHistory[quorumNumber].length != 0, "StakeRegistry.registerOperator: quorum does not exist"); + /** * Update the operator's stake for the quorum and retrieve their current stake * as well as the change in stake. * - If this method returns `hasMinimumStake == false`, the operator has not met * the minimum stake requirement for this quorum */ - uint8 quorumNumber = uint8(quorumNumbers[i]); (int256 stakeDelta, bool hasMinimumStake) = _updateOperatorStake({ operator: operator, operatorId: operatorId, @@ -65,7 +57,7 @@ contract StakeRegistryHarness is StakeRegistry { }); require( hasMinimumStake, - "StakeRegistry._registerOperator: Operator does not meet minimum stake requirement for quorum" + "StakeRegistry.registerOperator: Operator does not meet minimum stake requirement for quorum" ); // Update this quorum's total stake diff --git a/test/unit/VoteWeigherBaseUnit.t.sol b/test/unit/VoteWeigherBaseUnit.t.sol index fb2d1309..ca5cef44 100644 --- a/test/unit/VoteWeigherBaseUnit.t.sol +++ b/test/unit/VoteWeigherBaseUnit.t.sol @@ -12,9 +12,9 @@ import {IEigenPodManager} from "eigenlayer-contracts/src/contracts/interfaces/IE import {ISlasher} from "eigenlayer-contracts/src/contracts/interfaces/ISlasher.sol"; import {IServiceManager} from "src/interfaces/IServiceManager.sol"; import {IVoteWeigher} from "src/interfaces/IVoteWeigher.sol"; -import {VoteWeigherBase} from "src/VoteWeigherBase.sol"; +import {StakeRegistry} from "src/StakeRegistry.sol"; -import {StrategyManagerMock} from "eigenlayer-contracts/src/test/mocks/StrategyManagerMock.sol"; +import {RegistryCoordinatorMock} from "test/mocks/RegistryCoordinatorMock.sol"; import {OwnableMock} from "eigenlayer-contracts/src/test/mocks/OwnableMock.sol"; import {DelegationManagerMock} from "eigenlayer-contracts/src/test/mocks/DelegationManagerMock.sol"; @@ -25,16 +25,16 @@ contract VoteWeigherBaseUnitTests is Test { ProxyAdmin public proxyAdmin; PauserRegistry public pauserRegistry; - IStrategyManager public strategyManager; address serviceManagerOwner; IServiceManager public serviceManager; DelegationManagerMock delegationMock; + RegistryCoordinatorMock registryCoordinatorMock; - VoteWeigherBase public voteWeigher; + StakeRegistry public voteWeigher; - VoteWeigherBase public voteWeigherImplementation; + StakeRegistry public voteWeigherImplementation; address public pauser = address(555); address public unpauser = address(999); @@ -71,31 +71,23 @@ contract VoteWeigherBaseUnitTests is Test { pausers[0] = pauser; pauserRegistry = new PauserRegistry(pausers, unpauser); - StrategyManagerMock strategyManagerMock = new StrategyManagerMock(); + registryCoordinatorMock = new RegistryCoordinatorMock(); delegationMock = new DelegationManagerMock(); - strategyManagerMock.setAddresses( - delegationMock, - IEigenPodManager(address(uint160(uint256(keccak256(abi.encodePacked("eigenPodManager")))))), - ISlasher(address(uint160(uint256(keccak256(abi.encodePacked("slasher")))))) - ); - - strategyManager = IStrategyManager(address(strategyManagerMock)); // make the serviceManagerOwner the owner of the serviceManager contract cheats.prank(serviceManagerOwner); serviceManager = IServiceManager(address(new OwnableMock())); - voteWeigherImplementation = new VoteWeigherBase(strategyManager, serviceManager); + voteWeigherImplementation = new StakeRegistry(registryCoordinatorMock, delegationMock, serviceManager); - voteWeigher = VoteWeigherBase(address(new TransparentUpgradeableProxy(address(voteWeigherImplementation), address(proxyAdmin), ""))); + voteWeigher = StakeRegistry(address(new TransparentUpgradeableProxy(address(voteWeigherImplementation), address(proxyAdmin), ""))); fuzzedAddressMapping[address(proxyAdmin)] = true; } function testCorrectConstructionParameters() public { - assertEq(address(voteWeigherImplementation.strategyManager()), address(strategyManager)); - assertEq(address(voteWeigherImplementation.slasher()), address(strategyManager.slasher())); - assertEq(address(voteWeigherImplementation.delegation()), address(strategyManager.delegation())); + assertEq(address(voteWeigherImplementation.registryCoordinator()), address(registryCoordinatorMock)); + assertEq(address(voteWeigherImplementation.delegation()), address(delegationMock)); assertEq(address(voteWeigherImplementation.serviceManager()), address(serviceManager)); }