From f2c6b5c872c6632f639651727f9dfb52ebadd236 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 28 Dec 2023 00:31:19 -0800 Subject: [PATCH 1/4] feat: (very) rough draft ECDSA StakeRegistry comments are in a poor state no interface or separated storage definitions eliminated historical storage behavior is otherwise changed ~minimally from BLS version --- src/ECDSAStakeRegistry.sol | 496 +++++++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 src/ECDSAStakeRegistry.sol diff --git a/src/ECDSAStakeRegistry.sol b/src/ECDSAStakeRegistry.sol new file mode 100644 index 00000000..1eecb3ae --- /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 delegationManager; + + /// @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 _delegationManager + ) { + registryCoordinator = address(_registryCoordinator); + delegationManager = _delegationManager; + } + + /******************************************************************************* + 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 = delegationManager.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]; + } + +} From eb5b4da86d6666e7a5a73ae340a0c1537e54701f Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 28 Dec 2023 01:09:13 -0800 Subject: [PATCH 2/4] feat: (very) rough draft ECDSA RegistryCoordinator basically combines the RegistryCoordinator, IndexRegistry, and APK registry functionality removes historical storage comments and documentation are definitely incorrect --- src/ECDSARegistryCoordinator.sol | 867 +++++++++++++++++++++++++++++++ 1 file changed, 867 insertions(+) create mode 100644 src/ECDSARegistryCoordinator.sol 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(); + } +} From 3635f6a360aab9c3adc89b4911a4f463c4bb326d Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 28 Dec 2023 01:39:53 -0800 Subject: [PATCH 3/4] feat: (very) rough draft ECDSA SignatureChecker again no interface or historical storage comments are definitely wrong signatory record hash may no longer be useful implementation might have critical safety errors this just queries the current stakes and adds together signing stakes --- src/ECDSASignatureChecker.sol | 170 ++++++++++++++++++++++++++++++++++ src/ECDSAStakeRegistry.sol | 8 +- 2 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 src/ECDSASignatureChecker.sol diff --git a/src/ECDSASignatureChecker.sol b/src/ECDSASignatureChecker.sol new file mode 100644 index 00000000..69b9d3ac --- /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[i]); + if (BitmapUtils.isSet(operatorBitmap, quorumNumber)) { + stakeTotals.signedStakeForQuorum[i] += 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 index 1eecb3ae..e51b412e 100644 --- a/src/ECDSAStakeRegistry.sol +++ b/src/ECDSAStakeRegistry.sol @@ -57,7 +57,7 @@ contract ECDSAStakeRegistry { uint256 internal constant MAX_BIPS = 10000; /// @notice The address of the DelegationManager contract for EigenLayer. - IDelegationManager public immutable delegationManager; + IDelegationManager public immutable delegation; /// @notice the coordinator contract that this registry is associated with address public immutable registryCoordinator; @@ -107,10 +107,10 @@ contract ECDSAStakeRegistry { constructor( IRegistryCoordinator _registryCoordinator, - IDelegationManager _delegationManager + IDelegationManager _delegation ) { registryCoordinator = address(_registryCoordinator); - delegationManager = _delegationManager; + delegation = _delegation; } /******************************************************************************* @@ -450,7 +450,7 @@ contract ECDSAStakeRegistry { strategyAndMultiplier = strategyParams[quorumNumber][i]; // shares of the operator in the strategy - uint256 sharesAmount = delegationManager.operatorShares(operator, strategyAndMultiplier.strategy); + uint256 sharesAmount = delegation.operatorShares(operator, strategyAndMultiplier.strategy); // add the weight from the shares for this strategy to the total weight if (sharesAmount > 0) { From c95f663109e1250f318b74a6b9ef702ffe4a3dcf Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 28 Dec 2023 21:24:52 -0800 Subject: [PATCH 4/4] fix: use correct index classic i <> j mix up, ya know? there could definitely be more of these lurking. --- src/ECDSASignatureChecker.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ECDSASignatureChecker.sol b/src/ECDSASignatureChecker.sol index 69b9d3ac..20da2960 100644 --- a/src/ECDSASignatureChecker.sol +++ b/src/ECDSASignatureChecker.sol @@ -126,9 +126,9 @@ contract BLSSignatureChecker { uint256 operatorBitmap = registryCoordinator.operatorBitmap(signerIds[i]); for (uint256 j = 0; j < quorumNumbers.length; j++) { - uint8 quorumNumber = uint8(quorumNumbers[i]); + uint8 quorumNumber = uint8(quorumNumbers[j]); if (BitmapUtils.isSet(operatorBitmap, quorumNumber)) { - stakeTotals.signedStakeForQuorum[i] += stakeRegistry.operatorStake(signerIds[i], quorumNumber); + stakeTotals.signedStakeForQuorum[j] += stakeRegistry.operatorStake(signerIds[i], quorumNumber); } } }