diff --git a/src/ECDSARegistryCoordinator.sol b/src/ECDSARegistryCoordinator.sol new file mode 100644 index 00000000..bb218cb5 --- /dev/null +++ b/src/ECDSARegistryCoordinator.sol @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; + +import {EIP1271SignatureUtils} from "eigenlayer-contracts/src/contracts/libraries/EIP1271SignatureUtils.sol"; +import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; +import {Pausable} from "eigenlayer-contracts/src/contracts/permissions/Pausable.sol"; + +import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {ISocketUpdater} from "./interfaces/ISocketUpdater.sol"; +import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; +import {IServiceManager} from "./interfaces/IServiceManager.sol"; + +import {BitmapUtils} from "./libraries/BitmapUtils.sol"; + +/** + * @title A `RegistryCoordinator` that has three registries: + * 1) a `StakeRegistry` that keeps track of operators' stakes + * 2) a `BLSApkRegistry` that keeps track of operators' BLS public keys and aggregate BLS public keys for each quorum + * 3) an `IndexRegistry` that keeps track of an ordered list of operators for each quorum + * + * @author Layr Labs, Inc. + */ +contract ECDSARegistryCoordinator is + EIP712, + Initializable, + Pausable, + OwnableUpgradeable, + ISocketUpdater, + ISignatureUtils +{ + using BitmapUtils for *; + + // 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); + + event OperatorSetParamsUpdated(uint8 indexed quorumNumber, OperatorSetParam operatorSetParams); + + event ChurnApproverUpdated(address prevChurnApprover, address newChurnApprover); + + event EjectorUpdated(address prevEjector, address newEjector); + + /// @notice emitted when all the operators for a quorum are updated at once + event QuorumBlockNumberUpdated(uint8 indexed quorumNumber, uint256 blocknumber); + + // DATA STRUCTURES + enum OperatorStatus + { + // default is NEVER_REGISTERED + NEVER_REGISTERED, + REGISTERED, + DEREGISTERED + } + + // STRUCTS + + /** + * @notice Data structure for storing info on operators + */ + struct OperatorInfo { + // the id of the operator, which is likely the keccak256 hash of the operator's public key if using BLSRegistry + bytes32 operatorId; + // indicates whether the operator is actively registered for serving the middleware or not + OperatorStatus status; + } + + /** + * @notice Data structure for storing operator set params for a given quorum. Specifically the + * `maxOperatorCount` is the maximum number of operators that can be registered for the quorum, + * `kickBIPsOfOperatorStake` is the basis points of a new operator needs to have of an operator they are trying to kick from the quorum, + * and `kickBIPsOfTotalStake` is the basis points of the total stake of the quorum that an operator needs to be below to be kicked. + */ + struct OperatorSetParam { + uint32 maxOperatorCount; + uint16 kickBIPsOfOperatorStake; + uint16 kickBIPsOfTotalStake; + } + + /** + * @notice Data structure for the parameters needed to kick an operator from a quorum with number `quorumNumber`, used during registration churn. + * `operator` is the address of the operator to kick + */ + struct OperatorKickParam { + uint8 quorumNumber; + address operator; + } + + // TODO: doument + struct ECDSAPubkeyRegistrationParams { + address signingAddress; + SignatureWithSaltAndExpiry signatureAndExpiry; + } + + /// @notice The EIP-712 typehash for the `DelegationApproval` struct used by the contract + bytes32 public constant OPERATOR_CHURN_APPROVAL_TYPEHASH = + keccak256("OperatorChurnApproval(bytes32 registeringOperatorId,OperatorKickParam[] operatorKickParams)OperatorKickParam(address operator,bytes32[] operatorIdsToSwap)"); + /// @notice The EIP-712 typehash used for registering BLS public keys + bytes32 public constant PUBKEY_REGISTRATION_TYPEHASH = keccak256("ECDSAPubkeyRegistration(address operator)"); + /// @notice The maximum value of a quorum bitmap + uint256 internal constant MAX_QUORUM_BITMAP = type(uint256).max; + /// @notice The basis point denominator + uint16 internal constant BIPS_DENOMINATOR = 10000; + /// @notice Index for flag that pauses operator registration + uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; + /// @notice Index for flag that pauses operator deregistration + uint8 internal constant PAUSED_DEREGISTER_OPERATOR = 1; + /// @notice Index for flag pausing operator stake updates + uint8 internal constant PAUSED_UPDATE_OPERATOR = 2; + /// @notice The maximum number of quorums this contract supports + uint8 internal constant MAX_QUORUM_COUNT = 192; + + /// @notice the ServiceManager for this AVS, which forwards calls onto EigenLayer's core contracts + IServiceManager public immutable serviceManager; + /// @notice the Stake Registry contract that will keep track of operators' stakes + IStakeRegistry public immutable stakeRegistry; + + /// @notice the current number of quorums supported by the registry coordinator + uint8 public quorumCount; + /// @notice maps quorum number => operator cap and kick params + mapping(uint8 => OperatorSetParam) internal _quorumParams; + /// @notice maps operator id => current quorums they are registered for + mapping(bytes32 => uint256) public operatorBitmap; + /// @notice maps operator address => operator id and status + mapping(address => OperatorInfo) internal _operatorInfo; + /// @notice whether the salt has been used for an operator churn approval + mapping(bytes32 => bool) public isChurnApproverSaltUsed; + /// @notice mapping from quorum number to the latest block that all quorums were updated all at once + mapping(uint8 => uint256) public quorumUpdateBlockNumber; + + // TODO: document + mapping(bytes32 => address) operatorIdToOperator; + mapping(uint8 => uint32) public totalOperatorsForQuorum; + + /// @notice the dynamic-length array of the registries this coordinator is coordinating + address[] public registries; + /// @notice the address of the entity allowed to sign off on operators getting kicked out of the AVS during registration + address public churnApprover; + /// @notice the address of the entity allowed to eject operators from the AVS + address public ejector; + + modifier onlyEjector { + require(msg.sender == ejector, "RegistryCoordinator.onlyEjector: caller is not the ejector"); + _; + } + + modifier quorumExists(uint8 quorumNumber) { + require( + quorumNumber < quorumCount, + "RegistryCoordinator.quorumExists: quorum does not exist" + ); + _; + } + + constructor( + IServiceManager _serviceManager, + IStakeRegistry _stakeRegistry + ) EIP712("AVSRegistryCoordinator", "v0.0.1") { + serviceManager = _serviceManager; + stakeRegistry = _stakeRegistry; + + _disableInitializers(); + } + + function initialize( + address _initialOwner, + address _churnApprover, + address _ejector, + IPauserRegistry _pauserRegistry, + uint256 _initialPausedStatus, + OperatorSetParam[] memory _operatorSetParams, + uint96[] memory _minimumStakes, + IStakeRegistry.StrategyParams[][] memory _strategyParams + ) external initializer { + require( + _operatorSetParams.length == _minimumStakes.length && _minimumStakes.length == _strategyParams.length, + "RegistryCoordinator.initialize: input length mismatch" + ); + + // Initialize roles + _transferOwnership(_initialOwner); + _initializePauser(_pauserRegistry, _initialPausedStatus); + _setChurnApprover(_churnApprover); + _setEjector(_ejector); + + // Add registry contracts to the registries array + registries.push(address(stakeRegistry)); + + // Create quorums + for (uint256 i = 0; i < _operatorSetParams.length; i++) { + _createQuorum(_operatorSetParams[i], _minimumStakes[i], _strategyParams[i]); + } + } + + /******************************************************************************* + EXTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice Registers msg.sender as an operator for one or more quorums. If any quorum reaches its maximum + * operator capacity, this method will fail. + * @param quorumNumbers is an ordered byte array containing the quorum numbers being registered for + * @param socket is the socket of the operator + * @param params TODO: document + * @dev the `params` input param is ignored if the caller has previously registered a public key + * @param operatorSignature is the signature of the operator used by the AVS to register the operator in the delegation manager + */ + function registerOperator( + bytes calldata quorumNumbers, + string calldata socket, + ECDSAPubkeyRegistrationParams memory params, + SignatureWithSaltAndExpiry memory operatorSignature + ) external onlyWhenNotPaused(PAUSED_REGISTER_OPERATOR) { + /** + * IF the operator has never registered a pubkey before, THEN register their pubkey + * OTHERWISE, simply ignore the provided `params` input + */ + bytes32 operatorId = _getOrCreateOperatorId(msg.sender, params); + + // Register the operator in each of the registry contracts + uint32[] memory numOperatorsPerQuorum = _registerOperator({ + operator: msg.sender, + operatorId: operatorId, + quorumNumbers: quorumNumbers, + socket: socket, + operatorSignature: operatorSignature + }).numOperatorsPerQuorum; + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + + /** + * The new operator count for each quorum may not exceed the configured maximum + * If it does, use `registerOperatorWithChurn` instead. + */ + require( + numOperatorsPerQuorum[i] <= _quorumParams[quorumNumber].maxOperatorCount, + "RegistryCoordinator.registerOperator: operator count exceeds maximum" + ); + } + } + + /** + * @notice Registers msg.sender as an operator for one or more quorums. If any quorum reaches its maximum operator + * capacity, `operatorKickParams` is used to replace an old operator with the new one. + * @param quorumNumbers is an ordered byte array containing the quorum numbers being registered for + * @param params TODO: document + * @param operatorKickParams are used to determine which operator is removed to maintain quorum capacity as the + * operator registers for quorums. + * @param churnApproverSignature is the signature of the churnApprover on the operator kick params + * @param operatorSignature is the signature of the operator used by the AVS to register the operator in the delegation manager + * @dev the `params` input param is ignored if the caller has previously registered a public key + */ + function registerOperatorWithChurn( + bytes calldata quorumNumbers, + string calldata socket, + ECDSAPubkeyRegistrationParams memory params, + OperatorKickParam[] calldata operatorKickParams, + SignatureWithSaltAndExpiry memory churnApproverSignature, + SignatureWithSaltAndExpiry memory operatorSignature + ) external onlyWhenNotPaused(PAUSED_REGISTER_OPERATOR) { + require(operatorKickParams.length == quorumNumbers.length, "RegistryCoordinator.registerOperatorWithChurn: input length mismatch"); + + /** + * IF the operator has never registered a pubkey before, THEN register their pubkey + * OTHERWISE, simply ignore the provided `params` input + */ + bytes32 operatorId = _getOrCreateOperatorId(msg.sender, params); + + // Verify the churn approver's signature for the registering operator and kick params + _verifyChurnApproverSignature({ + registeringOperatorId: operatorId, + operatorKickParams: operatorKickParams, + churnApproverSignature: churnApproverSignature + }); + + // Register the operator in each of the registry contracts + RegisterResults memory results = _registerOperator({ + operator: msg.sender, + operatorId: operatorId, + quorumNumbers: quorumNumbers, + socket: socket, + operatorSignature: operatorSignature + }); + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + // reference: uint8 quorumNumber = uint8(quorumNumbers[i]); + OperatorSetParam memory operatorSetParams = _quorumParams[uint8(quorumNumbers[i])]; + + /** + * If the new operator count for any quorum exceeds the maximum, validate + * that churn can be performed, then deregister the specified operator + */ + if (results.numOperatorsPerQuorum[i] > operatorSetParams.maxOperatorCount) { + _validateChurn({ + quorumNumber: uint8(quorumNumbers[i]), + totalQuorumStake: results.totalStakes[i], + newOperator: msg.sender, + newOperatorStake: results.operatorStakes[i], + kickParams: operatorKickParams[i], + setParams: operatorSetParams + }); + + _deregisterOperator(operatorKickParams[i].operator, quorumNumbers[i:i+1]); + } + } + } + + /** + * @notice Deregisters the caller from one or more quorums + * @param quorumNumbers is an ordered byte array containing the quorum numbers being deregistered from + */ + function deregisterOperator( + bytes calldata quorumNumbers + ) external onlyWhenNotPaused(PAUSED_DEREGISTER_OPERATOR) { + _deregisterOperator({ + operator: msg.sender, + quorumNumbers: quorumNumbers + }); + } + + /** + * @notice Updates the stakes of one or more operators in the StakeRegistry, for each quorum + * the operator is registered for. + * + * If any operator no longer meets the minimum stake required to remain in the quorum, + * they are deregistered. + */ + function updateOperators(address[] calldata operators) external onlyWhenNotPaused(PAUSED_UPDATE_OPERATOR) { + for (uint256 i = 0; i < operators.length; i++) { + address operator = operators[i]; + OperatorInfo memory operatorInfo = _operatorInfo[operator]; + bytes32 operatorId = operatorInfo.operatorId; + + // Update the operator's stake for their active quorums + uint256 currentBitmap = _currentOperatorBitmap(operatorId); + bytes memory quorumsToUpdate = BitmapUtils.bitmapToBytesArray(currentBitmap); + _updateOperator(operator, operatorInfo, quorumsToUpdate); + } + } + + /** + * @notice Updates the stakes of all operators for each of the specified quorums in the StakeRegistry. Each quorum also + * has their quorumUpdateBlockNumber updated. which is meant to keep track of when operators were last all updated at once. + * @param operatorsPerQuorum is an array of arrays of operators to update for each quorum. Note that each nested array + * of operators must be sorted in ascending address order to ensure that all operators in the quorum are updated + * @param quorumNumbers is an array of quorum numbers to update + * @dev This method is used to update the stakes of all operators in a quorum at once, rather than individually. Performs + * sanitization checks on the input array lengths, quorumNumbers existing, and that quorumNumbers are ordered. Function must + * also not be paused by the PAUSED_UPDATE_OPERATOR flag. + */ + function updateOperatorsForQuorum( + address[][] calldata operatorsPerQuorum, + bytes calldata quorumNumbers + ) external onlyWhenNotPaused(PAUSED_UPDATE_OPERATOR) { + uint256 quorumBitmap = uint256(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount)); + require(_quorumsAllExist(quorumBitmap), "RegistryCoordinator.updateOperatorsForQuorum: some quorums do not exist"); + require( + operatorsPerQuorum.length == quorumNumbers.length, + "RegistryCoordinator.updateOperatorsForQuorum: input length mismatch" + ); + + for (uint256 i = 0; i < quorumNumbers.length; ++i) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + address[] calldata currQuorumOperators = operatorsPerQuorum[i]; + require( + currQuorumOperators.length == totalOperatorsForQuorum[quorumNumber], + "RegistryCoordinator.updateOperatorsForQuorum: number of updated operators does not match quorum total" + ); + address prevOperatorAddress = address(0); + // Update stakes for each operator in this quorum + for (uint256 j = 0; j < currQuorumOperators.length; ++j) { + address operator = currQuorumOperators[j]; + OperatorInfo memory operatorInfo = _operatorInfo[operator]; + bytes32 operatorId = operatorInfo.operatorId; + { + uint256 currentBitmap = _currentOperatorBitmap(operatorId); + require( + BitmapUtils.isSet(currentBitmap, quorumNumber), + "RegistryCoordinator.updateOperatorsForQuorum: operator not in quorum" + ); + // Require check is to prevent duplicate operators and that all quorum operators are updated + require( + operator > prevOperatorAddress, + "RegistryCoordinator.updateOperatorsForQuorum: operators array must be sorted in ascending address order" + ); + } + _updateOperator(operator, operatorInfo, quorumNumbers[i:i+1]); + prevOperatorAddress = operator; + } + + // Update timestamp that all operators in quorum have been updated all at once + quorumUpdateBlockNumber[quorumNumber] = block.number; + emit QuorumBlockNumberUpdated(quorumNumber, block.number); + } + } + + /** + * @notice Updates the socket of the msg.sender given they are a registered operator + * @param socket is the new socket of the operator + */ + function updateSocket(string memory socket) external { + require(_operatorInfo[msg.sender].status == OperatorStatus.REGISTERED, "RegistryCoordinator.updateSocket: operator is not registered"); + emit OperatorSocketUpdate(_operatorInfo[msg.sender].operatorId, socket); + } + + /******************************************************************************* + EXTERNAL FUNCTIONS - EJECTOR + *******************************************************************************/ + + /** + * @notice Ejects the provided operator from the provided quorums from the AVS + * @param operator is the operator to eject + * @param quorumNumbers are the quorum numbers to eject the operator from + */ + function ejectOperator( + address operator, + bytes calldata quorumNumbers + ) external onlyEjector { + _deregisterOperator({ + operator: operator, + quorumNumbers: quorumNumbers + }); + } + + /******************************************************************************* + EXTERNAL FUNCTIONS - OWNER + *******************************************************************************/ + + /** + * @notice Creates a quorum and initializes it in each registry contract + */ + function createQuorum( + OperatorSetParam memory operatorSetParams, + uint96 minimumStake, + IStakeRegistry.StrategyParams[] memory strategyParams + ) external virtual onlyOwner { + _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 operatorSetParams is the parameters of the operator set for the `quorumNumber` + * @dev only callable by the owner + */ + function setOperatorSetParams( + uint8 quorumNumber, + OperatorSetParam memory operatorSetParams + ) external onlyOwner quorumExists(quorumNumber) { + _setOperatorSetParams(quorumNumber, operatorSetParams); + } + + /** + * @notice Sets the churnApprover + * @param _churnApprover is the address of the churnApprover + * @dev only callable by the owner + */ + function setChurnApprover(address _churnApprover) external onlyOwner { + _setChurnApprover(_churnApprover); + } + + /** + * @notice Sets the ejector + * @param _ejector is the address of the ejector + * @dev only callable by the owner + */ + function setEjector(address _ejector) external onlyOwner { + _setEjector(_ejector); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + struct RegisterResults { + uint32[] numOperatorsPerQuorum; + uint96[] operatorStakes; + uint96[] totalStakes; + } + + /** + * @notice Register the operator for one or more quorums. This method updates the + * operator's quorum bitmap, socket, and status, then registers them with each registry. + */ + function _registerOperator( + address operator, + bytes32 operatorId, + bytes calldata quorumNumbers, + string memory socket, + SignatureWithSaltAndExpiry memory operatorSignature + ) internal virtual returns (RegisterResults memory results) { + /** + * Get bitmap of quorums to register for and operator's current bitmap. Validate that: + * - we're trying to register for at least 1 quorum + * - the operator is not currently registered for any quorums we're registering for + * Then, calculate the operator's new bitmap after registration + */ + uint256 quorumsToAdd = uint256(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount)); + uint256 currentBitmap = _currentOperatorBitmap(operatorId); + require(!quorumsToAdd.isEmpty(), "RegistryCoordinator._registerOperator: bitmap cannot be 0"); + require(_quorumsAllExist(quorumsToAdd), "RegistryCoordinator._registerOperator: some quorums do not exist"); + require(quorumsToAdd.noBitsInCommon(currentBitmap), "RegistryCoordinator._registerOperator: operator already registered for some quorums being registered for"); + uint256 newBitmap = uint256(currentBitmap.plus(quorumsToAdd)); + + /** + * Update operator's bitmap, socket, and status. Only update operatorInfo if needed: + * if we're `REGISTERED`, the operatorId and status are already correct. + */ + _updateOperatorBitmap({ + operatorId: operatorId, + newBitmap: newBitmap + }); + + emit OperatorSocketUpdate(operatorId, socket); + + if (_operatorInfo[operator].status != OperatorStatus.REGISTERED) { + _operatorInfo[operator] = OperatorInfo({ + operatorId: operatorId, + status: OperatorStatus.REGISTERED + }); + + // Register the operator with the EigenLayer via this AVS's ServiceManager + serviceManager.registerOperatorToAVS(operator, operatorSignature); + + emit OperatorRegistered(operator, operatorId); + } + + /** + * Register the operator with the BLSApkRegistry, StakeRegistry, and IndexRegistry + */ + (results.operatorStakes, results.totalStakes) = + stakeRegistry.registerOperator(operator, operatorId, quorumNumbers); + results.numOperatorsPerQuorum = new uint32[](quorumNumbers.length); + for (uint256 i = 0; i < quorumNumbers.length; ++i) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + uint32 newTotalOperatorsForQuorum = totalOperatorsForQuorum[quorumNumber] + 1; + totalOperatorsForQuorum[quorumNumber] = newTotalOperatorsForQuorum; + results.numOperatorsPerQuorum[i] = newTotalOperatorsForQuorum; + } + + return results; + } + + function _getOrCreateOperatorId( + address operator, + ECDSAPubkeyRegistrationParams memory params + ) internal returns (bytes32 operatorId) { + operatorId = _operatorInfo[operator].operatorId; + if (operatorId == 0) { + operatorId = _registerECDSAPublicKey(operator, params, pubkeyRegistrationMessageHash(operator)); + } + return operatorId; + } + + function _registerECDSAPublicKey( + address operator, + ECDSAPubkeyRegistrationParams memory params, + bytes32 _pubkeyRegistrationMessageHash + ) internal returns (bytes32 operatorId) { + require( + params.signingAddress != address(0), + "ECDSARegistryCoordinator._registerECDSAPublicKey: cannot register zero pubkey" + ); + require( + _operatorInfo[operator].operatorId == bytes32(0), + "ECDSARegistryCoordinator._registerECDSAPublicKey: operator already registered pubkey" + ); + + operatorId = bytes32(uint256(uint160(params.signingAddress))); + require( + operatorIdToOperator[operatorId] == address(0), + "ECDSARegistryCoordinator._registerECDSAPublicKey: public key already registered" + ); + + // check the signature expiry + require( + params.signatureAndExpiry.expiry >= block.timestamp, + "ECDSARegistryCoordinator._registerECDSAPublicKey: signature expired" + ); + + // actually check that the signature is valid + EIP1271SignatureUtils.checkSignature_EIP1271(params.signingAddress, _pubkeyRegistrationMessageHash, params.signatureAndExpiry.signature); + + operatorIdToOperator[operatorId] = operator; + + // TODO: event + // emit NewPubkeyRegistration(operator, params.pubkeyG1, params.pubkeyG2); + return operatorId; + } + + function _validateChurn( + uint8 quorumNumber, + uint96 totalQuorumStake, + address newOperator, + uint96 newOperatorStake, + OperatorKickParam memory kickParams, + OperatorSetParam memory setParams + ) internal view { + address operatorToKick = kickParams.operator; + bytes32 idToKick = _operatorInfo[operatorToKick].operatorId; + require(newOperator != operatorToKick, "RegistryCoordinator._validateChurn: cannot churn self"); + require(kickParams.quorumNumber == quorumNumber, "RegistryCoordinator._validateChurn: quorumNumber not the same as signed"); + + // Get the target operator's stake and check that it is below the kick thresholds + uint96 operatorToKickStake = stakeRegistry.getCurrentStake(idToKick, quorumNumber); + require( + newOperatorStake > _individualKickThreshold(operatorToKickStake, setParams), + "RegistryCoordinator._validateChurn: incoming operator has insufficient stake for churn" + ); + require( + operatorToKickStake < _totalKickThreshold(totalQuorumStake, setParams), + "RegistryCoordinator._validateChurn: cannot kick operator with more than kickBIPsOfTotalStake" + ); + } + + /** + * @dev Deregister the operator from one or more quorums + * This method updates the operator's quorum bitmap and status, then deregisters + * the operator with the BLSApkRegistry, IndexRegistry, and StakeRegistry + */ + function _deregisterOperator( + address operator, + bytes memory quorumNumbers + ) internal virtual { + // Fetch the operator's info and ensure they are registered + OperatorInfo storage operatorInfo = _operatorInfo[operator]; + bytes32 operatorId = operatorInfo.operatorId; + require(operatorInfo.status == OperatorStatus.REGISTERED, "RegistryCoordinator._deregisterOperator: operator is not registered"); + + /** + * Get bitmap of quorums to deregister from and operator's current bitmap. Validate that: + * - we're trying to deregister from at least 1 quorum + * - the operator is currently registered for any quorums we're trying to deregister from + * Then, calculate the opreator's new bitmap after deregistration + */ + uint256 quorumsToRemove = uint256(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount)); + uint256 currentBitmap = _currentOperatorBitmap(operatorId); + require(!quorumsToRemove.isEmpty(), "RegistryCoordinator._deregisterOperator: bitmap cannot be 0"); + require(_quorumsAllExist(quorumsToRemove), "RegistryCoordinator._deregisterOperator: some quorums do not exist"); + require(quorumsToRemove.isSubsetOf(currentBitmap), "RegistryCoordinator._deregisterOperator: operator is not registered for specified quorums"); + uint256 newBitmap = uint256(currentBitmap.minus(quorumsToRemove)); + + /** + * Update operator's bitmap and status: + */ + _updateOperatorBitmap({ + operatorId: operatorId, + newBitmap: newBitmap + }); + + // If the operator is no longer registered for any quorums, update their status and deregister from EigenLayer via this AVS's ServiceManager + if (newBitmap.isEmpty()) { + operatorInfo.status = OperatorStatus.DEREGISTERED; + serviceManager.deregisterOperatorFromAVS(operator); + emit OperatorDeregistered(operator, operatorId); + } + + // Deregister operator with each of the registry contracts: + stakeRegistry.deregisterOperator(operatorId, quorumNumbers); + for (uint256 i = 0; i < quorumNumbers.length; ++i) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + --totalOperatorsForQuorum[quorumNumber]; + } + } + + /** + * @notice update operator stake for specified quorumsToUpdate, and deregister if necessary + * does nothing if operator is not registered for any quorums. + */ + function _updateOperator( + address operator, + OperatorInfo memory operatorInfo, + bytes memory quorumsToUpdate + ) internal { + if (operatorInfo.status != OperatorStatus.REGISTERED) { + return; + } + bytes32 operatorId = operatorInfo.operatorId; + uint256 quorumsToRemove = stakeRegistry.updateOperatorStake(operator, operatorId, quorumsToUpdate); + + if (!quorumsToRemove.isEmpty()) { + _deregisterOperator({ + operator: operator, + quorumNumbers: BitmapUtils.bitmapToBytesArray(quorumsToRemove) + }); + } + } + + /** + * @notice Returns the stake threshold required for an incoming operator to replace an existing operator + * The incoming operator must have more stake than the return value. + */ + function _individualKickThreshold(uint96 operatorStake, OperatorSetParam memory setParams) internal pure returns (uint96) { + return operatorStake * setParams.kickBIPsOfOperatorStake / BIPS_DENOMINATOR; + } + + /** + * @notice Returns the total stake threshold required for an operator to remain in a quorum. + * The operator must have at least the returned stake amount to keep their position. + */ + function _totalKickThreshold(uint96 totalStake, OperatorSetParam memory setParams) internal pure returns (uint96) { + return totalStake * setParams.kickBIPsOfTotalStake / BIPS_DENOMINATOR; + } + + /// @notice verifies churnApprover's signature on operator churn approval and increments the churnApprover nonce + function _verifyChurnApproverSignature( + bytes32 registeringOperatorId, + OperatorKickParam[] memory operatorKickParams, + SignatureWithSaltAndExpiry memory churnApproverSignature + ) internal { + // make sure the salt hasn't been used already + require(!isChurnApproverSaltUsed[churnApproverSignature.salt], "RegistryCoordinator._verifyChurnApproverSignature: churnApprover salt already used"); + require(churnApproverSignature.expiry >= block.timestamp, "RegistryCoordinator._verifyChurnApproverSignature: churnApprover signature expired"); + + // set salt used to true + isChurnApproverSaltUsed[churnApproverSignature.salt] = true; + + // check the churnApprover's signature + EIP1271SignatureUtils.checkSignature_EIP1271( + churnApprover, + calculateOperatorChurnApprovalDigestHash(registeringOperatorId, operatorKickParams, churnApproverSignature.salt, churnApproverSignature.expiry), + churnApproverSignature.signature + ); + } + + /** + * @notice Creates and initializes a quorum in each registry contract + */ + function _createQuorum( + OperatorSetParam memory operatorSetParams, + uint96 minimumStake, + IStakeRegistry.StrategyParams[] memory strategyParams + ) internal { + // Increment the total quorum count. Fails if we're already at the max + uint8 prevQuorumCount = quorumCount; + require(prevQuorumCount < MAX_QUORUM_COUNT, "RegistryCoordinator.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.initializeQuorum(quorumNumber, minimumStake, strategyParams); + } + + /** + * @notice Record an update to an operator's quorum bitmap. + * @param newBitmap is the most up-to-date set of bitmaps the operator is registered for + */ + function _updateOperatorBitmap(bytes32 operatorId, uint256 newBitmap) internal { + operatorBitmap[operatorId] = newBitmap; + } + + /** + * @notice Returns true iff all of the bits in `quorumBitmap` belong to initialized quorums + */ + function _quorumsAllExist(uint256 quorumBitmap) internal view returns (bool) { + uint256 initializedQuorumBitmap = uint256((1 << quorumCount) - 1); + return quorumBitmap.isSubsetOf(initializedQuorumBitmap); + } + + /// @notice Get the most recent bitmap for the operator, returning an empty bitmap if + /// the operator is not registered. + function _currentOperatorBitmap(bytes32 operatorId) internal view returns (uint256) { + return operatorBitmap[operatorId]; + } + + function _setOperatorSetParams(uint8 quorumNumber, OperatorSetParam memory operatorSetParams) internal { + _quorumParams[quorumNumber] = operatorSetParams; + emit OperatorSetParamsUpdated(quorumNumber, operatorSetParams); + } + + function _setChurnApprover(address newChurnApprover) internal { + emit ChurnApproverUpdated(churnApprover, newChurnApprover); + churnApprover = newChurnApprover; + } + + function _setEjector(address newEjector) internal { + emit EjectorUpdated(ejector, newEjector); + ejector = newEjector; + } + + /******************************************************************************* + VIEW FUNCTIONS + *******************************************************************************/ + + /// @notice Returns the operator set params for the given `quorumNumber` + function getOperatorSetParams(uint8 quorumNumber) external view returns (OperatorSetParam memory) { + return _quorumParams[quorumNumber]; + } + + /// @notice Returns the operator struct for the given `operator` + function getOperator(address operator) external view returns (OperatorInfo memory) { + return _operatorInfo[operator]; + } + + /// @notice Returns the operatorId for the given `operator` + function getOperatorId(address operator) external view returns (bytes32) { + return _operatorInfo[operator].operatorId; + } + + /// @notice Returns the operator address for the given `operatorId` + function getOperatorFromId(bytes32 operatorId) external view returns (address) { + return operatorIdToOperator[operatorId]; + } + + /// @notice Returns the status for the given `operator` + function getOperatorStatus(address operator) external view returns (OperatorStatus) { + return _operatorInfo[operator].status; + } + + /// @notice Returns the current quorum bitmap for the given `operatorId` or 0 if the operator is not registered for any quorum + function getCurrentQuorumBitmap(bytes32 operatorId) external view returns (uint256) { + return _currentOperatorBitmap(operatorId); + } + + /// @notice Returns the number of registries + function numRegistries() external view returns (uint256) { + return registries.length; + } + + /** + * @notice Public function for the the churnApprover signature hash calculation when operators are being kicked from quorums + * @param registeringOperatorId The is of the registering operator + * @param operatorKickParams The parameters needed to kick the operator from the quorums that have reached their caps + * @param salt The salt to use for the churnApprover's signature + * @param expiry The desired expiry time of the churnApprover's signature + */ + function calculateOperatorChurnApprovalDigestHash( + bytes32 registeringOperatorId, + OperatorKickParam[] memory operatorKickParams, + bytes32 salt, + uint256 expiry + ) public view returns (bytes32) { + // calculate the digest hash + return _hashTypedDataV4(keccak256(abi.encode(OPERATOR_CHURN_APPROVAL_TYPEHASH, registeringOperatorId, operatorKickParams, salt, expiry))); + } + + /** + * @notice Returns the message hash that an operator must sign to register their BLS public key. + * @param operator is the address of the operator registering their BLS public key + */ + function pubkeyRegistrationMessageHash(address operator) public view returns (bytes32) { + return _hashTypedDataV4( + keccak256(abi.encode(PUBKEY_REGISTRATION_TYPEHASH, operator)) + ); + } + + /// @dev need to override function here since its defined in both these contracts + function owner() + public + view + override(OwnableUpgradeable) + returns (address) + { + return OwnableUpgradeable.owner(); + } +} diff --git a/src/ECDSASignatureChecker.sol b/src/ECDSASignatureChecker.sol new file mode 100644 index 00000000..20da2960 --- /dev/null +++ b/src/ECDSASignatureChecker.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import {ECDSARegistryCoordinator} from "./ECDSARegistryCoordinator.sol"; +import {ECDSAStakeRegistry, IDelegationManager} from "./ECDSAStakeRegistry.sol"; +import {EIP1271SignatureUtils} from "eigenlayer-contracts/src/contracts/libraries/EIP1271SignatureUtils.sol"; + +import {BitmapUtils} from "./libraries/BitmapUtils.sol"; + +/** + * @title Used for checking BLS aggregate signatures from the operators of a `BLSRegistry`. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This is the contract for checking the validity of aggregate operator signatures. + */ +contract BLSSignatureChecker { + + /** + * @notice this data structure is used for recording the details on the total stake of the registered + * operators and those operators who are part of the quorum for a particular taskNumber + */ + struct QuorumStakeTotals { + // total stake of the operators in each quorum + uint96[] signedStakeForQuorum; + // total amount staked by all operators in each quorum + uint96[] totalStakeForQuorum; + } + + // EVENTS + /// @notice Emitted when `staleStakesForbiddenUpdat + event StaleStakesForbiddenUpdate(bool value); + + // CONSTANTS & IMMUTABLES + + ECDSARegistryCoordinator public immutable registryCoordinator; + ECDSAStakeRegistry public immutable stakeRegistry; + IDelegationManager public immutable delegation; + /// @notice If true, check the staleness of the operator stakes and that its within the delegation withdrawalDelayBlocks window. + bool public staleStakesForbidden; + + modifier onlyCoordinatorOwner() { + require(msg.sender == registryCoordinator.owner(), "BLSSignatureChecker.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator"); + _; + } + + constructor(ECDSARegistryCoordinator _registryCoordinator) { + registryCoordinator = _registryCoordinator; + stakeRegistry = ECDSAStakeRegistry(address(_registryCoordinator.stakeRegistry())); + delegation = stakeRegistry.delegation(); + + staleStakesForbidden = true; + } + + /** + * RegistryCoordinator owner can either enforce or not that operator stakes are staler + * than the delegation.withdrawalDelayBlocks() window. + * @param value to toggle staleStakesForbidden + */ + function setStaleStakesForbidden(bool value) external onlyCoordinatorOwner { + staleStakesForbidden = value; + emit StaleStakesForbiddenUpdate(value); + } + + /** + * @notice This function is called by disperser when it has aggregated all the signatures of the operators + * that are part of the quorum for a particular taskNumber and is asserting them into onchain. The function + * checks that the claim for aggregated signatures are valid. + * + * The thesis of this procedure entails: + * - getting the aggregated pubkey of all registered nodes at the time of pre-commit by the + * disperser (represented by apk in the parameters), + * - subtracting the pubkeys of all the signers not in the quorum (nonSignerPubkeys) and storing + * the output in apk to get aggregated pubkey of all operators that are part of quorum. + * - use this aggregated pubkey to verify the aggregated signature under BLS scheme. + * + * @dev Before signature verification, the function verifies operator stake information. This includes ensuring that the provided `referenceBlockNumber` + * is correct, i.e., ensure that the stake returned from the specified block number is recent enough and that the stake is either the most recent update + * for the total stake (of the operator) or latest before the referenceBlockNumber. + * @param msgHash is the hash being signed + * @param quorumNumbers is the bytes array of quorum numbers that are being signed for + * @param signerIds TODO: document + * @param signatures TODO: document + * @return quorumStakeTotals is the struct containing the total and signed stake for each quorum + * @return signatoryRecordHash is the hash of the signatory record, which is used for fraud proofs + */ + function checkSignatures( + bytes32 msgHash, + bytes calldata quorumNumbers, + bytes32[] memory signerIds, + bytes[] memory signatures + ) + public + view + returns ( + QuorumStakeTotals memory, + bytes32 + ) + { + require( + signerIds.length == signatures.length, + "BLSSignatureChecker.checkSignatures: signature input length mismatch" + ); + // For each quorum, we're also going to query the total stake for all registered operators + // at the referenceBlockNumber, and derive the stake held by signers by subtracting out + // stakes held by nonsigners. + QuorumStakeTotals memory stakeTotals; + stakeTotals.totalStakeForQuorum = new uint96[](quorumNumbers.length); + stakeTotals.signedStakeForQuorum = new uint96[](quorumNumbers.length); + + // Get a bitmap of the quorums signing the message, and validate that + // quorumNumbers contains only unique, valid quorum numbers + uint256 signingQuorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, registryCoordinator.quorumCount()); + + for (uint256 i = 0; i < signerIds.length; ++i) { + + // The check below validates that operatorIds are sorted (and therefore free of duplicates) + if (i != 0) { + require( + uint256(signerIds[i]) > uint256(signerIds[i - 1]), + "ECDSASignatureChecker.checkSignatures: signer keys not sorted" + ); + } + + // check the operator's signature + EIP1271SignatureUtils.checkSignature_EIP1271(address(uint160(uint256(signerIds[i]))), msgHash, signatures[i]); + + uint256 operatorBitmap = registryCoordinator.operatorBitmap(signerIds[i]); + for (uint256 j = 0; j < quorumNumbers.length; j++) { + uint8 quorumNumber = uint8(quorumNumbers[j]); + if (BitmapUtils.isSet(operatorBitmap, quorumNumber)) { + stakeTotals.signedStakeForQuorum[j] += stakeRegistry.operatorStake(signerIds[i], quorumNumber); + } + } + } + + + /** + * For each quorum (at referenceBlockNumber): + * - add the apk for all registered operators + * - query the total stake for each quorum + * - subtract the stake for each nonsigner to calculate the stake belonging to signers + */ + { + uint256 withdrawalDelayBlocks = delegation.withdrawalDelayBlocks(); + bool _staleStakesForbidden = staleStakesForbidden; + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + // If we're disallowing stale stake updates, check that each quorum's last update block + // is within withdrawalDelayBlocks + if (_staleStakesForbidden) { + require( + registryCoordinator.quorumUpdateBlockNumber(uint8(quorumNumbers[i])) + withdrawalDelayBlocks >= block.number, + "BLSSignatureChecker.checkSignatures: StakeRegistry updates must be within withdrawalDelayBlocks window" + ); + } + + // Get the total and starting signed stake for the quorum at referenceBlockNumber + stakeTotals.totalStakeForQuorum[i] = stakeRegistry.totalStake(uint8(quorumNumbers[i])); + } + + } + + // set signatoryRecordHash variable used for fraudproofs + bytes32 signatoryRecordHash = keccak256(abi.encodePacked(block.number, signerIds)); + + // return the total stakes that signed for each quorum, and a hash of the information required to prove the exact signers and stake + return (stakeTotals, signatoryRecordHash); + } + +} diff --git a/src/ECDSAStakeRegistry.sol b/src/ECDSAStakeRegistry.sol new file mode 100644 index 00000000..e51b412e --- /dev/null +++ b/src/ECDSAStakeRegistry.sol @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; + +import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; + +import {BitmapUtils} from "./libraries/BitmapUtils.sol"; + +/** + * @title A `Registry` that keeps track of stakes of operators for up to 256 quorums. + * Specifically, it keeps track of + * 1) The stake of each operator in all the quorums they are a part of for block ranges + * 2) The total stake of all operators in each quorum for block ranges + * 3) The minimum stake required to register for each quorum + * It allows an additional functionality (in addition to registering and deregistering) to update the stake of an operator. + * @author Layr Labs, Inc. + */ +contract ECDSAStakeRegistry { + + using BitmapUtils for *; + + /** + * @notice In weighing a particular strategy, the amount of underlying asset for that strategy is + * multiplied by its multiplier, then divided by WEIGHTING_DIVISOR + */ + struct StrategyParams { + IStrategy strategy; + uint96 multiplier; + } + + // EVENTS + + /// @notice emitted whenever the stake of `operator` is updated + event OperatorStakeUpdate( + bytes32 indexed operatorId, + uint8 quorumNumber, + uint96 stake + ); + /// @notice emitted when the minimum stake for a quorum is updated + event MinimumStakeForQuorumUpdated(uint8 indexed quorumNumber, uint96 minimumStake); + /// @notice emitted when a new quorum is created + event QuorumCreated(uint8 indexed quorumNumber); + /// @notice emitted when `strategy` has been added to the array at `strategyParams[quorumNumber]` + event StrategyAddedToQuorum(uint8 indexed quorumNumber, IStrategy strategy); + /// @notice emitted when `strategy` has removed from the array at `strategyParams[quorumNumber]` + event StrategyRemovedFromQuorum(uint8 indexed quorumNumber, IStrategy strategy); + /// @notice emitted when `strategy` has its `multiplier` updated in the array at `strategyParams[quorumNumber]` + event StrategyMultiplierUpdated(uint8 indexed quorumNumber, IStrategy strategy, uint256 multiplier); + + /// @notice Constant used as a divisor in calculating weights. + uint256 public constant WEIGHTING_DIVISOR = 1e18; + /// @notice Maximum length of dynamic arrays in the `strategiesConsideredAndMultipliers` mapping. + 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 address of the DelegationManager contract for EigenLayer. + IDelegationManager public immutable delegation; + + /// @notice the coordinator contract that this registry is associated with + address public immutable registryCoordinator; + + /// @notice In order to register for a quorum i, an operator must have at least `minimumStakeForQuorum[i]` + /// evaluated by this contract's 'VoteWeigher' logic. + uint96[256] public minimumStakeForQuorum; + + /// @notice mapping from operator's operatorId to quorum number to their current stake + mapping(bytes32 => mapping(uint8 => uint96)) public operatorStake; + + /** + * @notice mapping from quorum number to the total stake for that quorum + */ + mapping(uint8 => uint96) public totalStake; + + /** + * @notice mapping from quorum number to the list of strategies considered and their + * corresponding multipliers for that specific quorum + */ + mapping(uint8 => StrategyParams[]) public strategyParams; + + // @notice mapping from quorum number to whether or not it exists + mapping(uint8 => bool) internal _quorumExists; + + // storage gap for upgradeability + // slither-disable-next-line shadowing-state + uint256[40] private __GAP; + + modifier onlyRegistryCoordinator() { + require( + msg.sender == address(registryCoordinator), + "StakeRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator" + ); + _; + } + + modifier onlyCoordinatorOwner() { + require(msg.sender == IRegistryCoordinator(registryCoordinator).owner(), "StakeRegistry.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator"); + _; + } + + modifier quorumExists(uint8 quorumNumber) { + require(_quorumExists[quorumNumber], "StakeRegistry.quorumExists: quorum does not exist"); + _; + } + + constructor( + IRegistryCoordinator _registryCoordinator, + IDelegationManager _delegation + ) { + registryCoordinator = address(_registryCoordinator); + delegation = _delegation; + } + + /******************************************************************************* + EXTERNAL FUNCTIONS - REGISTRY COORDINATOR + *******************************************************************************/ + + /** + * @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. + * @return The operator's current stake for each quorum, and the total stake for each quorum + * @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 calldata quorumNumbers + ) public virtual onlyRegistryCoordinator returns (uint96[] memory, uint96[] memory) { + + uint96[] memory currentStakes = new uint96[](quorumNumbers.length); + uint96[] memory totalStakes = new uint96[](quorumNumbers.length); + for (uint256 i = 0; i < quorumNumbers.length; i++) { + + uint8 quorumNumber = uint8(quorumNumbers[i]); + require(_quorumExists[quorumNumber], "StakeRegistry.registerOperator: quorum does not exist"); + + // Retrieve the operator's current weighted stake for the quorum, reverting if they have not met + // the minimum. + (uint96 currentStake, bool hasMinimumStake) = _weightOfOperatorForQuorum(quorumNumber, operator); + require( + hasMinimumStake, + "StakeRegistry.registerOperator: Operator does not meet minimum stake requirement for quorum" + ); + + // Update the operator's stake + int256 stakeDelta = _recordOperatorStakeUpdate({ + operatorId: operatorId, + quorumNumber: quorumNumber, + newStake: currentStake + }); + + // Update this quorum's total stake by applying the operator's delta + currentStakes[i] = currentStake; + totalStakes[i] = _recordTotalStakeUpdate(quorumNumber, stakeDelta); + } + + return (currentStakes, totalStakes); + } + + /** + * @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 calldata quorumNumbers + ) public virtual onlyRegistryCoordinator { + /** + * For each quorum, remove the operator's stake for the quorum and update + * the quorum's total stake to account for the removal + */ + for (uint256 i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + require(_quorumExists[quorumNumber], "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, + newStake: 0 + }); + + // Apply the operator's stake delta to the total stake for this quorum + _recordTotalStakeUpdate(quorumNumber, stakeDelta); + } + } + + /** + * @notice Called by the registry coordinator to update an operator's stake for one + * or more quorums. + * + * If the operator no longer has the minimum stake required for a quorum, they are + * added to the `quorumsToRemove`, which is returned to the registry coordinator + * @return A bitmap of quorums where the operator no longer meets the minimum stake + * and should be deregistered. + */ + function updateOperatorStake( + address operator, + bytes32 operatorId, + bytes calldata quorumNumbers + ) external onlyRegistryCoordinator returns (uint192) { + uint192 quorumsToRemove; + + /** + * For each quorum, update the operator's stake and record the delta + * in the quorum's total stake. + * + * If the operator no longer has the minimum stake required to be registered + * in the quorum, the quorum number is added to `quorumsToRemove`, which + * is returned to the registry coordinator. + */ + for (uint256 i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + require(_quorumExists[quorumNumber], "StakeRegistry.updateOperatorStake: quorum does not exist"); + + // Fetch the operator's current stake, applying weighting parameters and checking + // against the minimum stake requirements for the quorum. + (uint96 stakeWeight, bool hasMinimumStake) = _weightOfOperatorForQuorum(quorumNumber, operator); + + // If the operator no longer meets the minimum stake, set their stake to zero and mark them for removal + if (!hasMinimumStake) { + stakeWeight = 0; + quorumsToRemove = uint192(quorumsToRemove.setBit(quorumNumber)); + } + + // Update the operator's stake and retrieve the delta + // If we're deregistering them, their weight is set to 0 + int256 stakeDelta = _recordOperatorStakeUpdate({ + operatorId: operatorId, + quorumNumber: quorumNumber, + newStake: stakeWeight + }); + + // Apply the delta to the quorum's total stake + _recordTotalStakeUpdate(quorumNumber, stakeDelta); + } + + return quorumsToRemove; + } + + /// @notice Initialize a new quorum and push its first history update + function initializeQuorum( + uint8 quorumNumber, + uint96 minimumStake, + StrategyParams[] memory _strategyParams + ) public virtual onlyRegistryCoordinator { + require(!_quorumExists[quorumNumber], "StakeRegistry.initializeQuorum: quorum already exists"); + _addStrategyParams(quorumNumber, _strategyParams); + _setMinimumStakeForQuorum(quorumNumber, minimumStake); + + _quorumExists[quorumNumber] = true; + } + + function setMinimumStakeForQuorum( + uint8 quorumNumber, + uint96 minimumStake + ) public virtual onlyCoordinatorOwner 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 addStrategies( + uint8 quorumNumber, + StrategyParams[] memory _strategyParams + ) public virtual onlyCoordinatorOwner 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 removeStrategies( + uint8 quorumNumber, + uint256[] memory indicesToRemove + ) public virtual onlyCoordinatorOwner quorumExists(quorumNumber) { + uint256 toRemoveLength = indicesToRemove.length; + require(toRemoveLength > 0, "StakeRegistry.removeStrategies: no indices to remove provided"); + + StrategyParams[] storage _strategyParams = strategyParams[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 onlyCoordinatorOwner quorumExists(quorumNumber) { + uint256 numStrats = strategyIndices.length; + require(numStrats > 0, "StakeRegistry.modifyStrategyParams: no strategy indices provided"); + require(newMultipliers.length == numStrats, "StakeRegistry.modifyStrategyParams: input length mismatch"); + + StrategyParams[] storage _strategyParams = strategyParams[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 + *******************************************************************************/ + + function _setMinimumStakeForQuorum(uint8 quorumNumber, uint96 minimumStake) internal { + minimumStakeForQuorum[quorumNumber] = minimumStake; + emit MinimumStakeForQuorumUpdated(quorumNumber, minimumStake); + } + + /** + * @notice Records that `operatorId`'s current stake for `quorumNumber` is now `newStake` + * @return The change in the operator's stake as a signed int256 + */ + function _recordOperatorStakeUpdate( + bytes32 operatorId, + uint8 quorumNumber, + uint96 newStake + ) internal returns (int256) { + + uint96 prevStake = operatorStake[operatorId][quorumNumber]; + operatorStake[operatorId][quorumNumber] = newStake; + + // Log update and return stake delta + emit OperatorStakeUpdate(operatorId, quorumNumber, newStake); + return _calculateDelta({ prev: prevStake, cur: newStake }); + } + + /// @notice Applies a delta to the total stake recorded for `quorumNumber` + /// @return Returns the new total stake for the quorum + function _recordTotalStakeUpdate(uint8 quorumNumber, int256 stakeDelta) internal returns (uint96) { + uint96 prevStake = totalStake[quorumNumber]; + + // Return early if no update is needed + if (stakeDelta == 0) { + return prevStake; + } + + // Calculate the new total stake by applying the delta to our previous stake + uint96 newStake = _applyDelta(prevStake, stakeDelta); + + totalStake[quorumNumber] = newStake; + + return newStake; + } + + /** + * @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, + StrategyParams[] memory _strategyParams + ) internal { + require(_strategyParams.length > 0, "StakeRegistry._addStrategyParams: no strategies provided"); + uint256 numStratsToAdd = _strategyParams.length; + uint256 numStratsExisting = strategyParams[quorumNumber].length; + require( + numStratsExisting + numStratsToAdd <= MAX_WEIGHING_FUNCTION_LENGTH, + "StakeRegistry._addStrategyParams: exceed MAX_WEIGHING_FUNCTION_LENGTH" + ); + for (uint256 i = 0; i < numStratsToAdd; i++) { + // fairly gas-expensive internal loop to make sure that the *same* strategy cannot be added multiple times + for (uint256 j = 0; j < (numStratsExisting + i); j++) { + require( + strategyParams[quorumNumber][j].strategy != _strategyParams[i].strategy, + "StakeRegistry._addStrategyParams: cannot add same strategy 2x" + ); + } + require( + _strategyParams[i].multiplier > 0, + "StakeRegistry._addStrategyParams: cannot add strategy with zero weight" + ); + strategyParams[quorumNumber].push(_strategyParams[i]); + emit StrategyAddedToQuorum(quorumNumber, _strategyParams[i].strategy); + emit StrategyMultiplierUpdated( + quorumNumber, + _strategyParams[i].strategy, + _strategyParams[i].multiplier + ); + } + } + + /// @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)); + } + + /// @notice Adds or subtracts delta from value, according to its sign + function _applyDelta(uint96 value, int256 delta) internal pure returns (uint96) { + if (delta < 0) { + return value - uint96(uint256(-delta)); + } else { + return value + uint96(uint256(delta)); + } + } + + /** + * @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 + * @return `uint96` The weighted sum of the operator's shares across each strategy considered by the quorum + * @return `bool` True if the operator meets the quorum's minimum stake + */ + function _weightOfOperatorForQuorum(uint8 quorumNumber, address operator) internal virtual view returns (uint96, bool) { + uint96 weight; + uint256 stratsLength = strategyParamsLength(quorumNumber); + StrategyParams memory strategyAndMultiplier; + + for (uint256 i = 0; i < stratsLength; i++) { + // accessing i^th StrategyParams struct for the quorumNumber + strategyAndMultiplier = strategyParams[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); + } + } + + // Return the weight, and `true` if the operator meets the quorum's minimum stake + bool hasMinimumStake = weight >= minimumStakeForQuorum[quorumNumber]; + return (weight, hasMinimumStake); + } + + /******************************************************************************* + 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) { + (uint96 stake, ) = _weightOfOperatorForQuorum(quorumNumber, operator); + return stake; + } + + /// @notice Returns the length of the dynamic array stored in `strategyParams[quorumNumber]`. + function strategyParamsLength(uint8 quorumNumber) public view returns (uint256) { + return strategyParams[quorumNumber].length; + } + + /// @notice Returns the strategy and weight multiplier for the `index`'th strategy in the quorum `quorumNumber` + function strategyParamsByIndex( + uint8 quorumNumber, + uint256 index + ) public view returns (StrategyParams memory) + { + return strategyParams[quorumNumber][index]; + } + +}