diff --git a/src/interfaces/IECDSAStakeRegistry.sol b/src/interfaces/IECDSAStakeRegistry.sol index e17086f8..c6da054c 100644 --- a/src/interfaces/IECDSAStakeRegistry.sol +++ b/src/interfaces/IECDSAStakeRegistry.sol @@ -37,6 +37,16 @@ interface IECDSAStakeRegistryErrors { error OperatorAlreadyRegistered(); /// @notice Thrown when de-registering or updating the stake for an unregisted operator. error OperatorNotRegistered(); + /// @notice Thrown when the sender is not the AVS Registrar. + error InvalidSender(); + /// @notice Thrown when the M2 quorum registration is disabled. + error M2QuorumRegistrationIsDisabled(); + /// @notice Thrown when the operator set ids are invalid. + error InvalidOperatorSetIdsLength(); + /// @notice Thrown when the AllocationManager is not correctly configured. + error InvalidAllocationManager(); + /// @notice Thrown when the AllocationManager is already initialized. + error AllocationManagerAlreadyInitialized(); } interface IECDSAStakeRegistryTypes { @@ -48,6 +58,13 @@ interface IECDSAStakeRegistryTypes { uint96 multiplier; } + /// @notice Struct to represent operator set strategy multiplier + struct OperatorSetStrategyMultiplier { + uint32 operatorSetId; + address strategy; + uint256 multiplier; + } + /// @notice Configuration for a quorum's strategies. /// @param strategies An array of strategy parameters defining the quorum. struct Quorum { @@ -124,6 +141,15 @@ interface IECDSAStakeRegistryEvents is IECDSAStakeRegistryTypes { address indexed newSigningKey, address oldSigningKey ); + + /* + * @notice Emitted when the M2 quorum registration is disabled. + */ + event M2QuorumRegistrationDisabled(); + + + /// @notice Event emitted when operator set strategy multipliers are updated + event OperatorSetStrategyMultipliersUpdated(OperatorSetStrategyMultiplier[] params); } interface IECDSAStakeRegistry is @@ -138,7 +164,7 @@ interface IECDSAStakeRegistry is * @param operatorSignature Contains the operator's signature, salt, and expiry. * @param signingKey The signing key to add to the operator's history. */ - function registerOperatorWithSignature( + function registerOperatorM2Quorum( ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature, address signingKey ) external; @@ -146,7 +172,22 @@ interface IECDSAStakeRegistry is /* * @notice Deregisters an existing operator. */ - function deregisterOperator() external; + function deregisterOperatorM2Quorum() external; + + /* + * @notice called by the AVS Registrar when an operator is registered on the allocation manager. + * @param operator The address of the operator. + * @param signingKey The signing key of the operator. + */ + function onOperatorSetRegistered(address operator, address signingKey) external; + + /* + * @notice called by the AVS Registrar when an operator is deregistered on the allocation manager. + * @param operator The address of the operator. + */ + function onOperatorSetDeregistered( + address operator + ) external; /* * @notice Updates the signing key for an operator. @@ -199,6 +240,12 @@ interface IECDSAStakeRegistry is */ function quorum() external view returns (IECDSAStakeRegistryTypes.Quorum memory); + /* + * @notice Retrieves the current operator set id + * @return The current operator set id + */ + function getCurrentOperatorSetIds() external view returns (uint32[] memory); + /* * @notice Retrieves the latest signing key for a given operator. * @param operator The address of the operator. @@ -275,6 +322,42 @@ interface IECDSAStakeRegistry is address operator ) external view returns (uint256); + /* + * @notice Retrieves the operator's weight in the m2 quorum. + * @param operator The address of the operator. + * @return The operator's weight in the m2 quorum. + */ + function getQuorumWeight( + address operator + ) external view returns (uint256); + + /* + * @notice Retrieves the operator's weight in the current operator set. + * @param operator The address of the operator. + * @return The operator's weight in the current operator set. + */ + function getOperatorSetWeight( + address operator + ) external view returns (uint256); + + /* + * @notice Checks if an operator is registered on the AVS Directory(M2). + * @param operator The address of the operator to check. + * @return bool True if the operator is registered on the AVS Directory, false otherwise. + */ + function operatorRegisteredOnAVSDirectory( + address operator + ) external view returns (bool); + + /* + * @notice Checks if an operator is registered on the current operator set. + * @param operator The address of the operator to check. + * @return bool True if the operator is registered on the current operator set, false otherwise. + */ + function operatorRegisteredOnCurrentOperatorSets( + address operator + ) external view returns (bool); + /* * @notice Updates operators for a specific quorum. * @param operatorsPerQuorum Array of operator addresses per quorum. diff --git a/src/unaudited/ECDSAAVSRegistrar.sol b/src/unaudited/ECDSAAVSRegistrar.sol new file mode 100644 index 00000000..826e88d8 --- /dev/null +++ b/src/unaudited/ECDSAAVSRegistrar.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IECDSAStakeRegistry} from "../interfaces/IECDSAStakeRegistry.sol"; +import {IServiceManager} from "../interfaces/IServiceManager.sol"; + +contract AVSRegistrar is IAVSRegistrar, Ownable { + IAllocationManager public immutable allocationManager; + IECDSAStakeRegistry public immutable stakeRegistry; + address public immutable avs; + + error AVSRegistrar__OnlyAllocationManager(); + + modifier onlyAllocationManager() { + if (msg.sender != address(allocationManager)) { + revert AVSRegistrar__OnlyAllocationManager(); + } + _; + } + + constructor(address _allocationManager, address _stakeRegistry, address _avs) Ownable() { + allocationManager = IAllocationManager(_allocationManager); + stakeRegistry = IECDSAStakeRegistry(_stakeRegistry); + avs = _avs; + } + + function registerOperator( + address operator, + address avs, + uint32[] calldata operatorSetIds, + bytes calldata data + ) external onlyAllocationManager { + // Decode signing key + (address signingKey) = abi.decode(data, (address)); + + // Call stake registry to register operator + // Weight check will be done in ECDSAStakeRegistry + stakeRegistry.onOperatorSetRegistered(operator, signingKey); + } + + function deregisterOperator( + address operator, + address avs, + uint32[] calldata operatorSetIds + ) external onlyAllocationManager { + stakeRegistry.onOperatorSetDeregistered(operator); + } + + function supportsAVS( + address avsAddr + ) external view returns (bool) { + return avs == avsAddr; + } +} diff --git a/src/unaudited/ECDSAServiceManagerBase.sol b/src/unaudited/ECDSAServiceManagerBase.sol index d5bf2c55..e16b1476 100644 --- a/src/unaudited/ECDSAServiceManagerBase.sol +++ b/src/unaudited/ECDSAServiceManagerBase.sol @@ -17,8 +17,13 @@ import {IRewardsCoordinator} from import {IECDSAStakeRegistryTypes} from "../interfaces/IECDSAStakeRegistry.sol"; import {ECDSAStakeRegistry} from "../unaudited/ECDSAStakeRegistry.sol"; import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; -import {IAllocationManager} from - "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import { + IAllocationManager, + IAllocationManagerTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; abstract contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable { using SafeERC20 for IERC20; @@ -38,6 +43,9 @@ abstract contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable /// @notice Address of the delegation manager contract, which manages staker delegations to operators. address internal immutable delegationManager; + /// @notice Address of the permission controller contract, which manages permissions for the service manager. + address internal immutable permissionController; + /// @notice Address of the rewards initiator, which is allowed to create AVS rewards submissions. address public rewardsInitiator; @@ -74,13 +82,15 @@ abstract contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable address _stakeRegistry, address _rewardsCoordinator, address _delegationManager, - address _allocationManager + address _allocationManager, + address _permissionController ) { avsDirectory = _avsDirectory; stakeRegistry = _stakeRegistry; rewardsCoordinator = _rewardsCoordinator; delegationManager = _delegationManager; allocationManager = _allocationManager; + permissionController = _permissionController; _disableInitializers(); } @@ -139,6 +149,81 @@ abstract contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable _deregisterOperatorFromAVS(operator); } + /// @notice Deregisters an operator from a set of operator sets. + /// @dev This function is used to deregister an operator from a set of operator sets. + /// @param operator The address of the operator to deregister. + /// @param operatorSetIds The set of operator set ids to deregister from. + function deregisterOperatorFromOperatorSets( + address operator, + uint32[] memory operatorSetIds + ) external virtual onlyStakeRegistry { + IAllocationManager.DeregisterParams memory params = IAllocationManagerTypes.DeregisterParams({ + operator: operator, + avs: address(this), + operatorSetIds: operatorSetIds + }); + IAllocationManager(allocationManager).deregisterFromOperatorSets(params); + } + + /// @inheritdoc IServiceManager + + function addPendingAdmin( + address admin + ) external virtual onlyOwner { + IPermissionController(permissionController).addPendingAdmin({ + account: address(this), + admin: admin + }); + } + + /// @inheritdoc IServiceManager + function removePendingAdmin( + address pendingAdmin + ) external virtual onlyOwner { + IPermissionController(permissionController).removePendingAdmin({ + account: address(this), + admin: pendingAdmin + }); + } + + /// @inheritdoc IServiceManager + function removeAdmin( + address admin + ) external virtual onlyOwner { + IPermissionController(permissionController).removeAdmin({ + account: address(this), + admin: admin + }); + } + + /// @inheritdoc IServiceManager + function setAppointee( + address appointee, + address target, + bytes4 selector + ) external virtual onlyOwner { + IPermissionController(permissionController).setAppointee({ + account: address(this), + appointee: appointee, + target: target, + selector: selector + }); + } + + /// @inheritdoc IServiceManager + function removeAppointee( + address appointee, + address target, + bytes4 selector + ) external virtual onlyOwner { + IPermissionController(permissionController).removeAppointee({ + account: address(this), + appointee: appointee, + target: target, + selector: selector + }); + } + /// @inheritdoc IServiceManagerUI function getRestakeableStrategies() external view virtual returns (address[] memory) { return _getRestakeableStrategies(); @@ -151,6 +236,18 @@ abstract contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable return _getOperatorRestakedStrategies(_operator); } + /** + * @notice Returns the strategies from a specific operator set for this AVS + * @dev Uses AllocationManager to fetch strategies from a single operator set + * @param operatorSetId The ID of the operator set to query + * @return Array of strategy addresses from the specified operator set + */ + function getOperatorSetStrategies( + uint32 operatorSetId + ) external view virtual returns (address[] memory) { + return _getOperatorSetStrategies(operatorSetId); + } + /** * @notice Forwards the call to update AVS metadata URI in the AVSDirectory contract. * @dev This internal function is a proxy to the `updateAVSMetadataURI` function of the AVSDirectory contract. @@ -263,6 +360,35 @@ abstract contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable return strategies; } + /** + * @notice Retrieves the addresses of strategies from a specific operator set + * @dev Returns an empty array if the operator set is invalid + * @param operatorSetId The ID of the operator set to get strategies from + * @return Array of strategy addresses from the specified operator set + */ + function _getOperatorSetStrategies( + uint32 operatorSetId + ) internal view virtual returns (address[] memory) { + OperatorSet memory operatorSet = OperatorSet(address(this), operatorSetId); + + // Return empty array if this is not a valid operator set + if (!IAllocationManager(allocationManager).isOperatorSet(operatorSet)) { + return new address[](0); + } + + // Get strategies for this operator set + IStrategy[] memory strategies = + IAllocationManager(allocationManager).getStrategiesInOperatorSet(operatorSet); + + // Convert IStrategy array to address array + address[] memory strategyAddresses = new address[](strategies.length); + for (uint256 i = 0; i < strategies.length; i++) { + strategyAddresses[i] = address(strategies[i]); + } + + return strategyAddresses; + } + /** * @notice Sets the AVS registrar address in the AllocationManager * @param registrar The new AVS registrar address diff --git a/src/unaudited/ECDSAStakeRegistry.sol b/src/unaudited/ECDSAStakeRegistry.sol index c961c83e..e3f3dd80 100644 --- a/src/unaudited/ECDSAStakeRegistry.sol +++ b/src/unaudited/ECDSAStakeRegistry.sol @@ -19,6 +19,17 @@ import {SignatureCheckerUpgradeable} from "@openzeppelin-upgrades/contracts/utils/cryptography/SignatureCheckerUpgradeable.sol"; import {IERC1271Upgradeable} from "@openzeppelin-upgrades/contracts/interfaces/IERC1271Upgradeable.sol"; +import {IAVSDirectoryTypes} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +interface IAVSDirectory { + function avsOperatorStatus( + address avs, + address operator + ) external view returns (IAVSDirectoryTypes.OperatorAVSRegistrationStatus); +} /// @title ECDSA Stake Registry /// @dev THIS CONTRACT IS NOT AUDITED @@ -34,11 +45,24 @@ contract ECDSAStakeRegistry is /// @dev Constructor to create ECDSAStakeRegistry. /// @param _delegationManager Address of the DelegationManager contract that this registry interacts with. constructor( - IDelegationManager _delegationManager - ) ECDSAStakeRegistryStorage(_delegationManager) { + IDelegationManager _delegationManager, + IAllocationManager _allocationManager, + address _avsRegistrar, + IAVSDirectory _avsDirectory + ) + ECDSAStakeRegistryStorage(_delegationManager, _allocationManager, _avsRegistrar, _avsDirectory) + { // _disableInitializers(); } + /// @notice Modifier to ensure the caller is the AVS Registrar. + modifier onlyAVSRegistrar() { + if (msg.sender != address(avsRegistrar)) { + revert InvalidSender(); + } + _; + } + /// @notice Initializes the contract with the given parameters. /// @param _serviceManager The address of the service manager. /// @param thresholdWeight The threshold weight in basis points. @@ -46,9 +70,13 @@ contract ECDSAStakeRegistry is function initialize( address _serviceManager, uint256 thresholdWeight, - IECDSAStakeRegistryTypes.Quorum memory quorum + IECDSAStakeRegistryTypes.Quorum memory quorum, + uint32[] calldata operatorSetIds, + OperatorSetStrategyMultiplier[] calldata operatorSetStrategyMultipliers ) external initializer { - __ECDSAStakeRegistry_init(_serviceManager, thresholdWeight, quorum); + __ECDSAStakeRegistry_init( + _serviceManager, thresholdWeight, quorum, operatorSetIds, operatorSetStrategyMultipliers + ); } /// @notice Initializes state for the StakeRegistry @@ -56,32 +84,72 @@ contract ECDSAStakeRegistry is function __ECDSAStakeRegistry_init( address _serviceManagerAddr, uint256 thresholdWeight, - IECDSAStakeRegistryTypes.Quorum memory quorum + IECDSAStakeRegistryTypes.Quorum memory quorum, + uint32[] calldata operatorSetIds, + OperatorSetStrategyMultiplier[] calldata operatorSetStrategyMultipliers ) internal onlyInitializing { _serviceManager = _serviceManagerAddr; _updateStakeThreshold(thresholdWeight); _updateQuorumConfig(quorum); + _setCurrentOperatorSetIds(operatorSetIds); + _updateOperatorSetsConfig(operatorSetStrategyMultipliers); __Ownable_init(); } + function disableM2QuorumRegistration() external onlyOwner { + if (isM2QuorumRegistrationDisabled) { + revert M2QuorumRegistrationIsDisabled(); + } + + isM2QuorumRegistrationDisabled = true; + emit M2QuorumRegistrationDisabled(); + } + /// @inheritdoc IECDSAStakeRegistry - function registerOperatorWithSignature( + function registerOperatorM2Quorum( ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature, address signingKey ) external { - _registerOperatorWithSig(msg.sender, operatorSignature, signingKey); + if (isM2QuorumRegistrationDisabled) { + revert M2QuorumRegistrationIsDisabled(); + } + _registerOperatorM2Quorum(msg.sender, operatorSignature, signingKey); } /// @inheritdoc IECDSAStakeRegistry - function deregisterOperator() external { - _deregisterOperator(msg.sender); + function deregisterOperatorM2Quorum() external { + _deregisterOperatorM2Quorum(msg.sender); + } + + function onOperatorSetRegistered( + address operator, + address signingKey + ) external onlyAVSRegistrar { + int256 delta = _updateOperatorWeight(operator); + _updateTotalWeight(delta); + _updateOperatorSigningKey(operator, signingKey); + if (!operatorRegisteredOnAVSDirectory(operator)) { + _totalOperators++; + emit OperatorRegistered(operator, _serviceManager); + } + } + + function onOperatorSetDeregistered( + address operator + ) external onlyAVSRegistrar { + int256 delta = _updateOperatorWeight(operator); + _updateTotalWeight(delta); + if (!operatorRegisteredOnAVSDirectory(operator)) { + _totalOperators--; + emit OperatorDeregistered(operator, _serviceManager); + } } /// @inheritdoc IECDSAStakeRegistry function updateOperatorSigningKey( address newSigningKey ) external { - if (!_operatorRegistered[msg.sender]) { + if (!operatorRegistered(msg.sender)) { revert OperatorNotRegistered(); } _updateOperatorSigningKey(msg.sender, newSigningKey); @@ -103,6 +171,19 @@ contract ECDSAStakeRegistry is _updateOperators(operators); } + /** + * @notice Updates strategy parameters for multiple operator sets (from stake registry view) + * @notice Strategy params must align with operator set ids on the allocation manager + * @param operatorSetStrategyMultipliers Array of operator set strategy multipliers to update + */ + function updateOperatorSetsConfig( + OperatorSetStrategyMultiplier[] calldata operatorSetStrategyMultipliers, + address[] calldata operators + ) external onlyOwner { + _updateOperatorSetsConfig(operatorSetStrategyMultipliers); + _updateOperators(operators); + } + /// @inheritdoc IECDSAStakeRegistry function updateMinimumWeight( uint256 newMinimumWeight, @@ -119,6 +200,36 @@ contract ECDSAStakeRegistry is _updateStakeThreshold(thresholdWeight); } + /// @notice Sets the current operator set ids + /// @param _ids The ids of the operator sets to set + function setCurrentOperatorSetIds( + uint32[] calldata _ids + ) external onlyOwner { + _setCurrentOperatorSetIds(_ids); + } + + /// @notice Sets the allocation manager + /// @param _allocationManager The allocation manager to set + function initAllocationManager( + IAllocationManager _allocationManager + ) external onlyOwner { + if (address(allocationManager) != address(0)) { + revert AllocationManagerAlreadyInitialized(); + } + if (address(_allocationManager) == address(0)) { + revert InvalidAllocationManager(); + } + allocationManager = _allocationManager; + } + + /// @notice Sets the AVS Registrar + /// @param _avsRegistrar The AVS Registrar to set + function setAVSRegistrar( + address _avsRegistrar + ) external onlyOwner { + avsRegistrar = _avsRegistrar; + } + function isValidSignature( bytes32 digest, bytes memory _signatureData @@ -134,6 +245,12 @@ contract ECDSAStakeRegistry is return _quorum; } + /// @notice Gets the current operator set ids + /// @return The current operator set ids + function getCurrentOperatorSetIds() external view returns (uint32[] memory) { + return currentOperatorSetIds; + } + /// @inheritdoc IECDSAStakeRegistry function getLatestOperatorSigningKey( address operator @@ -188,22 +305,38 @@ contract ECDSAStakeRegistry is return _thresholdWeightHistory.getAtBlock(blockNumber); } - /// @inheritdoc IECDSAStakeRegistry - function operatorRegistered( - address operator - ) external view returns (bool) { - return _operatorRegistered[operator]; - } - /// @inheritdoc IECDSAStakeRegistry function minimumWeight() external view returns (uint256) { return _minimumWeight; } - /// @inheritdoc IECDSAStakeRegistry + /// @notice Calculates an operator's current weight based on their delegated stake + /// @param _operator Address of the operator to calculate weight for + /// @return Current weight of the operator (0 if below minimum threshold) + /// @dev Queries mainnet delegation manager for current shares function getOperatorWeight( + address _operator + ) public view virtual returns (uint256) { + uint256 quorumWeight = getQuorumWeight(_operator); + uint256 operatorSetWeight = getOperatorSetWeight(_operator); + uint256 totalWeight = quorumWeight + operatorSetWeight; + + if (totalWeight >= _minimumWeight) { + return totalWeight; + } else { + return 0; + } + } + + /// @notice Calculates operator's weight in the quorum + /// @param operator The operator address to calculate weight for + /// @return The operator's weight in quorum + function getQuorumWeight( address operator ) public view returns (uint256) { + if (!operatorRegisteredOnAVSDirectory(operator)) { + return 0; + } StrategyParams[] memory strategyParams = _quorum.strategies; uint256 weight; IStrategy[] memory strategies = new IStrategy[](strategyParams.length); @@ -216,11 +349,58 @@ contract ECDSAStakeRegistry is } weight = weight / BPS; - if (weight >= _minimumWeight) { - return weight; - } else { + return weight; + } + + /// @notice Calculates operator's available weight in current operator set + /// @dev Weight calculation: + /// 1. Check operator set membership + /// 2. Get shares and allocation for each strategy + /// 3. Calculate available proportion (currentMagnitude/maxMagnitude) + /// 4. Sum up available shares weighted by proportion + /// @param operator The operator address to calculate weight for + /// @return The operator's available weight in set + function getOperatorSetWeight( + address operator + ) public view virtual returns (uint256) { + if (address(allocationManager) == address(0)) { return 0; } + uint256 totalWeight; + + for (uint256 setIndex = 0; setIndex < currentOperatorSetIds.length; setIndex++) { + uint32 operatorSetId = currentOperatorSetIds[setIndex]; + OperatorSet memory operatorSet = + OperatorSet({avs: address(_serviceManager), id: operatorSetId}); + if (!allocationManager.isMemberOfOperatorSet(operator, operatorSet)) { + continue; + } + + IStrategy[] memory strategies = + allocationManager.getStrategiesInOperatorSet(operatorSet); + if (strategies.length == 0) { + continue; + } + + uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(operator, strategies); + + for (uint256 i = 0; i < strategies.length; i++) { + IAllocationManager.Allocation memory allocation = + allocationManager.getAllocation(operator, operatorSet, strategies[i]); + uint64 maxMagnitude = allocationManager.getMaxMagnitude(operator, strategies[i]); + + if (maxMagnitude == 0) { + continue; + } + + uint256 slashableProportion = uint256(allocation.currentMagnitude) * WAD / maxMagnitude; + uint256 multiplier = operatorSetStrategyMultipliers[operatorSetId][address(strategies[i])]; + + totalWeight += shares[i] * slashableProportion * multiplier / WAD / BPS; + } + } + + return totalWeight; } /// @inheritdoc IECDSAStakeRegistry @@ -231,6 +411,46 @@ contract ECDSAStakeRegistry is _updateAllOperators(operatorsPerQuorum[0]); } + /// @notice Checks if an operator is registered on the AVS Directory(M2). + /// @param operator The address of the operator to check. + /// @return bool True if the operator is registered on the AVS Directory, false otherwise. + function operatorRegisteredOnAVSDirectory( + address operator + ) public view returns (bool) { + return AVS_DIRECTORY.avsOperatorStatus(_serviceManager, operator) + == IAVSDirectoryTypes.OperatorAVSRegistrationStatus.REGISTERED; + } + + /// @notice Checks if an operator is registered on the current operator set. + /// @param operator The address of the operator to check. + /// @return bool True if the operator is registered on the current operator set, false otherwise. + function operatorRegisteredOnCurrentOperatorSets( + address operator + ) public view returns (bool) { + if (address(allocationManager) == address(0)) { + return false; + } + // Check if operator is registered in any current set + for (uint256 i = 0; i < currentOperatorSetIds.length; i++) { + OperatorSet memory operatorSet = + OperatorSet({avs: address(_serviceManager), id: currentOperatorSetIds[i]}); + if (allocationManager.isMemberOfOperatorSet(operator, operatorSet)) { + return true; + } + } + return false; + } + + /// @notice Checks if an operator is registered on the AVS Directory or the current operator set. + /// @param operator The address of the operator to check. + /// @return bool True if the operator is registered on the AVS Directory or the current operator set, false otherwise. + function operatorRegistered( + address operator + ) public view returns (bool) { + return operatorRegisteredOnAVSDirectory(operator) + || operatorRegisteredOnCurrentOperatorSets(operator); + } + /// @dev Updates the list of operators if the provided list has the correct number of operators. /// Reverts if the provided list of operators does not match the expected total count of operators. /// @param operators The list of operator addresses to update. @@ -296,38 +516,48 @@ contract ECDSAStakeRegistry is /// @dev Internal function to deregister an operator /// @param operator The operator's address to deregister - function _deregisterOperator( + function _deregisterOperatorM2Quorum( address operator ) internal { - if (!_operatorRegistered[operator]) { - revert OperatorNotRegistered(); - } - _totalOperators--; - delete _operatorRegistered[operator]; + // need to first remove the operator from the AVS Directory to correctly update the operator weight + IServiceManager(_serviceManager).deregisterOperatorFromAVS(operator); int256 delta = _updateOperatorWeight(operator); _updateTotalWeight(delta); - IServiceManager(_serviceManager).deregisterOperatorFromAVS(operator); - emit OperatorDeregistered(operator, address(_serviceManager)); + + if (!operatorRegisteredOnCurrentOperatorSets(operator)) { + _totalOperators--; + emit OperatorDeregistered(operator, address(_serviceManager)); + } } /// @dev registers an operator through a provided signature /// @param operatorSignature Contains the operator's signature, salt, and expiry /// @param signingKey The signing key to add to the operator's history - function _registerOperatorWithSig( + function _registerOperatorM2Quorum( address operator, ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature, address signingKey ) internal virtual { - if (_operatorRegistered[operator]) { - revert OperatorAlreadyRegistered(); - } - _totalOperators++; - _operatorRegistered[operator] = true; + // need to first register the operator to the AVS Directory to correctly update the operator weight + IServiceManager(_serviceManager).registerOperatorToAVS(operator, operatorSignature); int256 delta = _updateOperatorWeight(operator); _updateTotalWeight(delta); _updateOperatorSigningKey(operator, signingKey); - IServiceManager(_serviceManager).registerOperatorToAVS(operator, operatorSignature); - emit OperatorRegistered(operator, _serviceManager); + if (!operatorRegisteredOnCurrentOperatorSets(operator)) { + _totalOperators++; + emit OperatorRegistered(operator, _serviceManager); + } + } + + /// @notice Deregisters an operator from a set of operator sets. + /// @dev This function is used to deregister an operator from a set of operator sets. + /// @param operator The address of the operator to deregister. + function _deregisterOperatorFromOperatorSets( + address operator + ) internal virtual { + IServiceManager(_serviceManager).deregisterOperatorFromOperatorSets( + operator, currentOperatorSetIds + ); } /// @dev Internal function to update an operator's signing key @@ -350,7 +580,7 @@ contract ECDSAStakeRegistry is int256 delta; uint256 newWeight; uint256 oldWeight = _operatorWeightHistory[operator].latest(); - if (!_operatorRegistered[operator]) { + if (!operatorRegistered(operator)) { delta -= int256(oldWeight); if (delta == 0) { return delta; @@ -409,6 +639,17 @@ contract ECDSAStakeRegistry is } } + /// @notice Internal function to set the current operator set ids + /// @param _ids The ids of the operator sets to set + function _setCurrentOperatorSetIds( + uint32[] calldata _ids + ) internal { + if (_ids.length == 0 || _ids.length > 10) { + revert InvalidOperatorSetIdsLength(); + } + currentOperatorSetIds = _ids; + } + /** * @notice Common logic to verify a batch of ECDSA signatures against a hash, using either last stake weight or at a specific block. * @param digest The hash of the data the signers endorsed. @@ -549,4 +790,19 @@ contract ECDSAStakeRegistry is revert InsufficientSignedStake(); } } + + /// @notice Internal function to update strategy multipliers for multiple operator sets + /// @param params Array of operator set strategy multipliers to update + function _updateOperatorSetsConfig( + OperatorSetStrategyMultiplier[] calldata params + ) internal { + for (uint256 i = 0; i < params.length; i++) { + uint32 operatorSetId = params[i].operatorSetId; + address strategy = params[i].strategy; + uint256 multiplier = params[i].multiplier; + operatorSetStrategyMultipliers[operatorSetId][strategy] = multiplier; + } + + emit OperatorSetStrategyMultipliersUpdated(params); + } } diff --git a/src/unaudited/ECDSAStakeRegistryStorage.sol b/src/unaudited/ECDSAStakeRegistryStorage.sol index 8d9d69d8..54648df6 100644 --- a/src/unaudited/ECDSAStakeRegistryStorage.sol +++ b/src/unaudited/ECDSAStakeRegistryStorage.sol @@ -8,11 +8,34 @@ import {CheckpointsUpgradeable} from import { IECDSAStakeRegistry, IECDSAStakeRegistryTypes } from "../interfaces/IECDSAStakeRegistry.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IAVSDirectoryTypes} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; +import {IAVSDirectory} from "./ECDSAStakeRegistry.sol"; abstract contract ECDSAStakeRegistryStorage is IECDSAStakeRegistry { /// @notice Manages staking delegations through the DelegationManager interface IDelegationManager internal immutable DELEGATION_MANAGER; + /// @notice The AVS Directory contract + IAVSDirectory internal immutable AVS_DIRECTORY; + + /// @notice Manages staking delegations through the DelegationManager interface + IAllocationManager internal allocationManager; + + /// @notice The address of the AVS Registrar of the AVS + address internal avsRegistrar; + + /// @notice Whether M2 quorum registration is disabled + bool public isM2QuorumRegistrationDisabled; + + /// @notice The current operator set ids + uint32[] public currentOperatorSetIds; + + /// @notice The total amount of multipliers to weigh stakes + uint256 public constant WAD = 1e18; + /// @dev The total amount of multipliers to weigh stakes uint256 internal constant BPS = 10000; @@ -22,15 +45,15 @@ abstract contract ECDSAStakeRegistryStorage is IECDSAStakeRegistry { /// @notice Stores the current quorum configuration IECDSAStakeRegistryTypes.Quorum internal _quorum; + /// @notice strategy parameters for operator sets, mapping from operatorSetId to strategy to multiplier + mapping(uint32 => mapping(address => uint256)) internal operatorSetStrategyMultipliers; + /// @notice Specifies the weight required to become an operator uint256 internal _minimumWeight; /// @notice Holds the address of the service manager address internal _serviceManager; - /// @notice Defines the duration after which the stake's weight expires. - uint256 internal _stakeExpiry; - /// @notice Maps an operator to their signing key history using checkpoints mapping(address => CheckpointsUpgradeable.History) internal _operatorSigningKeyHistory; @@ -43,14 +66,17 @@ abstract contract ECDSAStakeRegistryStorage is IECDSAStakeRegistry { /// @notice Maps operator addresses to their respective stake histories using checkpoints mapping(address => CheckpointsUpgradeable.History) internal _operatorWeightHistory; - /// @notice Maps an operator to their registration status - mapping(address => bool) internal _operatorRegistered; - /// @param _delegationManager Connects this registry with the DelegationManager constructor( - IDelegationManager _delegationManager + IDelegationManager _delegationManager, + IAllocationManager _allocationManager, + address _avsRegistrar, + IAVSDirectory _avsDirectory ) { DELEGATION_MANAGER = _delegationManager; + allocationManager = _allocationManager; + avsRegistrar = _avsRegistrar; + AVS_DIRECTORY = _avsDirectory; } // slither-disable-next-line shadowing-state diff --git a/src/unaudited/examples/ECDSAStakeRegistryEqualWeight.sol b/src/unaudited/examples/ECDSAStakeRegistryEqualWeight.sol index 3edd6278..985cb6ee 100644 --- a/src/unaudited/examples/ECDSAStakeRegistryEqualWeight.sol +++ b/src/unaudited/examples/ECDSAStakeRegistryEqualWeight.sol @@ -7,6 +7,9 @@ import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {CheckpointsUpgradeable} from "@openzeppelin-upgrades/contracts/utils/CheckpointsUpgradeable.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSDirectory} from "../ECDSAStakeRegistry.sol"; /// @title ECDSA Stake Registry with Equal Weight /// @dev THIS CONTRACT IS NOT AUDITED @@ -18,8 +21,18 @@ contract ECDSAStakeRegistryEqualWeight is ECDSAStakeRegistryPermissioned { /// @dev Passes the delegation manager to the parent constructor. /// @param _delegationManager The address of the delegation manager contract. constructor( - IDelegationManager _delegationManager - ) ECDSAStakeRegistryPermissioned(_delegationManager) { + IDelegationManager _delegationManager, + IAllocationManager _allocationManager, + address _avsRegistrar, + IAVSDirectory _avsDirectory + ) + ECDSAStakeRegistryPermissioned( + _delegationManager, + _allocationManager, + _avsRegistrar, + _avsDirectory + ) + { // _disableInitializers(); } @@ -33,7 +46,7 @@ contract ECDSAStakeRegistryEqualWeight is ECDSAStakeRegistryPermissioned { uint256 oldWeight; uint256 newWeight; int256 delta; - if (_operatorRegistered[_operator]) { + if (operatorRegistered(_operator)) { (oldWeight,) = _operatorWeightHistory[_operator].push(1); delta = int256(1) - int256(oldWeight); // handles if they were already registered } else { diff --git a/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol b/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol index f6afffdf..4a504705 100644 --- a/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol +++ b/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol @@ -5,6 +5,10 @@ import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISi import {ECDSAStakeRegistry} from "../ECDSAStakeRegistry.sol"; import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSDirectory} from "../ECDSAStakeRegistry.sol"; +import {IAVSDirectoryTypes} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; /// @title ECDSA Stake Registry with an Operator Allowlist /// @dev THIS CONTRACT IS NOT AUDITED @@ -29,8 +33,11 @@ contract ECDSAStakeRegistryPermissioned is ECDSAStakeRegistry { error OperatorAlreadyAllowlisted(); constructor( - IDelegationManager _delegationManager - ) ECDSAStakeRegistry(_delegationManager) { + IDelegationManager _delegationManager, + IAllocationManager _allocationManager, + address _avsRegistrar, + IAVSDirectory _avsDirectory + ) ECDSAStakeRegistry(_delegationManager, _allocationManager, _avsRegistrar, _avsDirectory) { // _disableInitializers(); } @@ -66,7 +73,16 @@ contract ECDSAStakeRegistryPermissioned is ECDSAStakeRegistry { function _ejectOperator( address _operator ) internal { - _deregisterOperator(_operator); + if (!operatorRegistered(_operator)) { + revert OperatorNotRegistered(); + } + + if (operatorRegisteredOnAVSDirectory(_operator)) { + _deregisterOperatorM2Quorum(_operator); + } + if (operatorRegisteredOnCurrentOperatorSets(_operator)) { + _deregisterOperatorFromOperatorSets(_operator); + } emit OperatorEjected(_operator); } @@ -94,13 +110,13 @@ contract ECDSAStakeRegistryPermissioned is ECDSAStakeRegistry { } delete allowlistedOperators[_operator]; emit OperatorRevoked(_operator); - if (_operatorRegistered[_operator]) { + if (operatorRegistered(_operator)) { _ejectOperator(_operator); } } /// @inheritdoc ECDSAStakeRegistry - function _registerOperatorWithSig( + function _registerOperatorM2Quorum( address _operator, ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature, address _operatorSigningKey @@ -108,6 +124,6 @@ contract ECDSAStakeRegistryPermissioned is ECDSAStakeRegistry { if (allowlistedOperators[_operator] != true) { revert OperatorNotAllowlisted(); } - super._registerOperatorWithSig(_operator, _operatorSignature, _operatorSigningKey); + super._registerOperatorM2Quorum(_operator, _operatorSignature, _operatorSigningKey); } } diff --git a/src/unaudited/examples/ECDSAStakeRegistryRatioWeight.sol b/src/unaudited/examples/ECDSAStakeRegistryRatioWeight.sol new file mode 100644 index 00000000..5a0fdb91 --- /dev/null +++ b/src/unaudited/examples/ECDSAStakeRegistryRatioWeight.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {ECDSAStakeRegistry} from "../ECDSAStakeRegistry.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSDirectory} from "../ECDSAStakeRegistry.sol"; + +/// @title ECDSAStakeRegistryRatioWeight +/// @notice An example implementation of ECDSAStakeRegistry that allows setting different weight ratios +/// for quorum weight and operator set weight +/// @dev The total weight ratio is 10000 (100%). The quorum weight ratio can be set between 0 and 10000, +/// and the operator set weight ratio will be (10000 - quorumWeightRatio) +contract ECDSAStakeRegistryRatioWeight is ECDSAStakeRegistry { + /// @notice Event emitted when the weight ratio is updated + event WeightRatioUpdated(uint256 oldRatio, uint256 newRatio); + + /// @notice The weight ratio for quorum weight (0-10000) + uint256 private _quorumWeightRatio; + + error InvalidWeightRatio(uint256 ratio); + + constructor( + IDelegationManager _delegationManager, + IAllocationManager _allocationManager, + address _avsRegistrar, + IAVSDirectory _avsDirectory + ) ECDSAStakeRegistry(_delegationManager, _allocationManager, _avsRegistrar, _avsDirectory) {} + + /// @notice External function to set the weight ratio, only callable by owner + /// @param ratio The new ratio (0-10000) + /// @dev The operator set weight ratio will be (10000 - ratio) + function setWeightRatio( + uint256 ratio + ) external onlyOwner { + _setWeightRatio(ratio); + } + + /// @notice Returns the current weight ratio + /// @return The current quorum weight ratio (0-10000) + function getWeightRatio() external view returns (uint256) { + return _quorumWeightRatio; + } + + /// @inheritdoc ECDSAStakeRegistry + /// @dev Overrides the weight calculation to apply the configured ratios + function getOperatorWeight( + address _operator + ) public view virtual override returns (uint256) { + uint256 quorumWeight = getQuorumWeight(_operator); + uint256 operatorSetWeight = getOperatorSetWeight(_operator); + + uint256 weightedQuorumWeight = (quorumWeight * _quorumWeightRatio) / 10000; + uint256 weightedOperatorSetWeight = + (operatorSetWeight * (10000 - _quorumWeightRatio)) / 10000; + + return weightedQuorumWeight + weightedOperatorSetWeight; + } + + /// @notice Sets the weight ratio for quorum weight + /// @param ratio The new ratio (0-10000) + /// @dev The operator set weight ratio will be (10000 - ratio) + function _setWeightRatio( + uint256 ratio + ) internal { + if (ratio > 10000) { + revert InvalidWeightRatio(ratio); + } + uint256 oldRatio = _quorumWeightRatio; + _quorumWeightRatio = ratio; + emit WeightRatioUpdated(oldRatio, ratio); + } +} diff --git a/test/mocks/AVSDirectoryMock.sol b/test/mocks/AVSDirectoryMock.sol index d76f1abf..cc745507 100644 --- a/test/mocks/AVSDirectoryMock.sol +++ b/test/mocks/AVSDirectoryMock.sol @@ -151,4 +151,9 @@ contract AVSDirectoryMock is IAVSDirectory { function isOperatorSetBatch( OperatorSet[] calldata operatorSets ) external view returns (bool) {} + + function avsOperatorStatus( + address avs, + address operator + ) external view returns (OperatorAVSRegistrationStatus) {} } diff --git a/test/mocks/ECDSAServiceManagerMock.sol b/test/mocks/ECDSAServiceManagerMock.sol index 07a263cf..091bdcd7 100644 --- a/test/mocks/ECDSAServiceManagerMock.sol +++ b/test/mocks/ECDSAServiceManagerMock.sol @@ -11,42 +11,29 @@ contract ECDSAServiceManagerMock is ECDSAServiceManagerBase { address _stakeRegistry, address _rewardsCoordinator, address _delegationManager, - address _allocationManager + address _allocationManager, + address _permissionController, + address _initialOwner, + address _rewardsInitiator ) ECDSAServiceManagerBase( _avsDirectory, _stakeRegistry, _rewardsCoordinator, _delegationManager, - _allocationManager + _allocationManager, + _permissionController ) - {} - - function initialize( - address initialOwner, - address rewardsInitiator - ) public virtual initializer { - __ServiceManagerBase_init(initialOwner, rewardsInitiator); + { + // disable initializer and directly set owner and rewardsInitiator for testing + _transferOwnership(_initialOwner); + _setRewardsInitiator(_rewardsInitiator); } - function addPendingAdmin( - address admin - ) external {} - - function removePendingAdmin( - address pendingAdmin - ) external {} - - function deregisterOperatorFromOperatorSets( - address operator, - uint32[] memory operatorSetIds - ) external {} - - function removeAdmin( - address admin - ) external {} - - function setAppointee(address appointee, address target, bytes4 selector) external {} - - function removeAppointee(address appointee, address target, bytes4 selector) external {} + // function initialize( + // address initialOwner, + // address rewardsInitiator + // ) public virtual initializer { + // __ServiceManagerBase_init(initialOwner, rewardsInitiator); + // } } diff --git a/test/mocks/ECDSAStakeRegistryMock.sol b/test/mocks/ECDSAStakeRegistryMock.sol index fb43079b..19007010 100644 --- a/test/mocks/ECDSAStakeRegistryMock.sol +++ b/test/mocks/ECDSAStakeRegistryMock.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import "../../src/unaudited/ECDSAStakeRegistry.sol"; +import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; /** * @title Mock for ECDSAStakeRegistry @@ -9,6 +10,9 @@ import "../../src/unaudited/ECDSAStakeRegistry.sol"; */ contract ECDSAStakeRegistryMock is ECDSAStakeRegistry { constructor( - IDelegationManager _delegationManager - ) ECDSAStakeRegistry(_delegationManager) {} + IDelegationManager _delegationManager, + IAllocationManager _allocationManager, + address _avsRegistrar, + IAVSDirectory _avsDirectory + ) ECDSAStakeRegistry(_delegationManager, _allocationManager, _avsRegistrar, _avsDirectory) {} } diff --git a/test/unit/ECDSAServiceManager.t.sol b/test/unit/ECDSAServiceManager.t.sol index 5afc891e..d6497c21 100644 --- a/test/unit/ECDSAServiceManager.t.sol +++ b/test/unit/ECDSAServiceManager.t.sol @@ -10,10 +10,38 @@ import {IRewardsCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {IAVSRegistrar} from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; +import {IECDSAStakeRegistryTypes} from "../../src/interfaces/IECDSAStakeRegistry.sol"; +import {IECDSAStakeRegistry} from "../../src/interfaces/IECDSAStakeRegistry.sol"; import {ECDSAServiceManagerMock} from "../mocks/ECDSAServiceManagerMock.sol"; import {ECDSAStakeRegistryMock} from "../mocks/ECDSAStakeRegistryMock.sol"; -import {IECDSAStakeRegistryTypes} from "../../src/interfaces/IECDSAStakeRegistry.sol"; +import {AVSDirectoryMock} from "../mocks/AVSDirectoryMock.sol"; +import {IPermissionController} from + "eigenlayer-contracts/src/contracts/interfaces/IPermissionController.sol"; +import { + IAllocationManager, + IAllocationManagerTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSDirectory, IAVSDirectoryTypes} from "../../src/unaudited/ECDSAStakeRegistry.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + +contract MockPermissionController { + function addPendingAdmin(address account, address admin) external {} + function removePendingAdmin(address account, address admin) external {} + function removeAdmin(address account, address admin) external {} + function setAppointee( + address account, + address appointee, + address target, + bytes4 selector + ) external {} + function removeAppointee( + address account, + address appointee, + address target, + bytes4 selector + ) external {} +} contract MockDelegationManager { function operatorShares(address, address) external pure returns (uint256) { @@ -32,23 +60,77 @@ contract MockDelegationManager { } } -contract MockAVSDirectory { - function registerOperatorToAVS( - address, - ISignatureUtils.SignatureWithSaltAndExpiry memory - ) external pure {} +contract MockAllocationManager { + mapping(address => mapping(bytes32 => IAllocationManagerTypes.Allocation)) public allocations; + mapping(address => mapping(IStrategy => uint64)) public maxMagnitudes; + mapping(address => mapping(bytes32 => bool)) public operatorSetMembership; + + function getAllocation( + address operator, + OperatorSet memory operatorSet, + IStrategy strategy + ) external view returns (IAllocationManagerTypes.Allocation memory) { + bytes32 key = keccak256(abi.encode(operator, operatorSet, address(strategy))); + return allocations[operator][key]; + } - function deregisterOperatorFromAVS( - address - ) external pure {} + function setOperatorWeight( + address operator, + OperatorSet memory operatorSet, + address strategy, + uint256 weight + ) external { + bytes32 key = keccak256(abi.encode(operator, operatorSet, address(strategy))); + allocations[operator][key] = IAllocationManagerTypes.Allocation({ + currentMagnitude: uint64(weight), + pendingDiff: 0, + effectBlock: 0 + }); + bytes32 membershipKey = keccak256(abi.encode(operator, operatorSet)); + operatorSetMembership[operator][membershipKey] = true; + } - function updateAVSMetadataURI( - string memory - ) external pure {} -} + function isMemberOfOperatorSet( + address operator, + OperatorSet memory operatorSet + ) external view returns (bool) { + bytes32 key = keccak256(abi.encode(operator, operatorSet)); + return operatorSetMembership[operator][key]; + } -contract MockAllocationManager { - function setAVSRegistrar(address avs, address registrar) external {} + function setOperatorSetMembership( + address operator, + OperatorSet memory operatorSet, + bool isMember + ) external { + bytes32 key = keccak256(abi.encode(operator, operatorSet)); + operatorSetMembership[operator][key] = isMember; + } + + function isOperatorSet( + OperatorSet memory + ) external pure returns (bool) { + return true; + } + + function getStrategiesInOperatorSet( + OperatorSet memory + ) external pure returns (IStrategy[] memory) { + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = IStrategy(address(900)); + strategies[1] = IStrategy(address(901)); + return strategies; + } + + function setAVSRegistrar(address, address) external {} + + function getMaxMagnitude(address operator, IStrategy strategy) external view returns (uint64) { + return maxMagnitudes[operator][strategy]; + } + + function setMaxMagnitude(address operator, IStrategy strategy, uint64 magnitude) external { + maxMagnitudes[operator][strategy] = magnitude; + } } contract MockRewardsCoordinator { @@ -67,13 +149,56 @@ contract MockRewardsCoordinator { ) external pure {} } +contract MockAVSDirectory { + mapping(address => mapping(address => IAVSDirectoryTypes.OperatorAVSRegistrationStatus)) private + operatorStatus; + + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory + ) external { + operatorStatus[msg.sender][operator] = + IAVSDirectoryTypes.OperatorAVSRegistrationStatus.REGISTERED; + } + + function deregisterOperatorFromAVS( + address operator + ) external { + operatorStatus[msg.sender][operator] = + IAVSDirectoryTypes.OperatorAVSRegistrationStatus.UNREGISTERED; + } + + function updateAVSMetadataURI( + string memory + ) external pure {} + + function setAvsOperatorStatus( + address avs, + address operator, + IAVSDirectoryTypes.OperatorAVSRegistrationStatus status + ) external { + operatorStatus[avs][operator] = status; + } + + function avsOperatorStatus( + address avs, + address operator + ) external view returns (IAVSDirectoryTypes.OperatorAVSRegistrationStatus) { + return operatorStatus[avs][operator]; + } +} + contract ECDSAServiceManagerSetup is Test { MockDelegationManager public mockDelegationManager; - MockAVSDirectory public mockAVSDirectory; + AVSDirectoryMock public mockAVSDirectory; MockAllocationManager public mockAllocationManager; ECDSAStakeRegistryMock public mockStakeRegistry; MockRewardsCoordinator public mockRewardsCoordinator; ECDSAServiceManagerMock public serviceManager; + MockPermissionController public mockPermissionController; + address public mockAVSRegistrarAddr; + address public owner = makeAddr("owner"); + address public rewardsInitiator = makeAddr("rewardsInitiator"); address internal operator1; address internal operator2; uint256 internal operator1Pk; @@ -81,18 +206,28 @@ contract ECDSAServiceManagerSetup is Test { function setUp() public { mockDelegationManager = new MockDelegationManager(); - mockAVSDirectory = new MockAVSDirectory(); + mockAVSDirectory = new AVSDirectoryMock(); mockAllocationManager = new MockAllocationManager(); - mockStakeRegistry = - new ECDSAStakeRegistryMock(IDelegationManager(address(mockDelegationManager))); + mockAVSRegistrarAddr = makeAddr("mockAVSRegistrar"); + mockStakeRegistry = new ECDSAStakeRegistryMock( + IDelegationManager(address(mockDelegationManager)), + IAllocationManager(address(mockAllocationManager)), + mockAVSRegistrarAddr, + IAVSDirectory(address(mockAVSDirectory)) + ); mockRewardsCoordinator = new MockRewardsCoordinator(); + mockPermissionController = new MockPermissionController(); + serviceManager = new ECDSAServiceManagerMock( address(mockAVSDirectory), address(mockStakeRegistry), address(mockRewardsCoordinator), address(mockDelegationManager), - address(mockAllocationManager) + address(mockAllocationManager), + address(mockPermissionController), + owner, + rewardsInitiator ); operator1Pk = 1; @@ -112,21 +247,49 @@ contract ECDSAServiceManagerSetup is Test { strategy: IStrategy(address(421)), multiplier: 5000 }); - address[] memory operators = new address[](0); + uint32[] memory operatorSetIds = new uint32[](2); + operatorSetIds[0] = 1; + operatorSetIds[1] = 2; - vm.prank(mockStakeRegistry.owner()); + IECDSAStakeRegistryTypes.StrategyParams[][] memory strategyParamsArray = + new IECDSAStakeRegistryTypes.StrategyParams[][](2); + + strategyParamsArray[0] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[0][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(900)), + multiplier: 3000 + }); + strategyParamsArray[0][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(901)), + multiplier: 3000 + }); + // Strategy params for second operator set + strategyParamsArray[1] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[1][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(902)), + multiplier: 3000 + }); + strategyParamsArray[1][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(903)), + multiplier: 3000 + }); + + vm.prank(owner); mockStakeRegistry.initialize( address(serviceManager), 10000, // Assuming a threshold weight of 10000 basis points - quorum + quorum, + operatorSetIds, + strategyParamsArray ); + ISignatureUtils.SignatureWithSaltAndExpiry memory dummySignature; vm.prank(operator1); - mockStakeRegistry.registerOperatorWithSignature(dummySignature, operator1); + mockStakeRegistry.registerOperatorM2Quorum(dummySignature, operator1); vm.prank(operator2); - mockStakeRegistry.registerOperatorWithSignature(dummySignature, operator2); + mockStakeRegistry.registerOperatorM2Quorum(dummySignature, operator2); } function testRegisterOperatorToAVS() public { @@ -204,7 +367,7 @@ contract ECDSAServiceManagerSetup is Test { function testSetClaimerFor() public { address claimer = address(0x123); - vm.prank(mockStakeRegistry.owner()); + vm.prank(owner); serviceManager.setClaimerFor(claimer); } @@ -214,4 +377,53 @@ contract ECDSAServiceManagerSetup is Test { vm.prank(mockStakeRegistry.owner()); serviceManager.setAVSRegistrar(IAVSRegistrar(registrar)); } + + function testGetOperatorSetStrategies() public { + uint32 operatorSetId = 1; + + address[] memory strategies = serviceManager.getOperatorSetStrategies(operatorSetId); + + assertEq(strategies.length, 2, "Should return 2 strategies"); + assertEq(strategies[0], address(900), "First strategy should match"); + assertEq(strategies[1], address(901), "Second strategy should match"); + } + + function testAddPendingAdmin() public { + address admin = makeAddr("admin"); + + vm.prank(owner); + serviceManager.addPendingAdmin(admin); + } + + function testRemovePendingAdmin() public { + address pendingAdmin = makeAddr("pendingAdmin"); + + vm.prank(owner); + serviceManager.removePendingAdmin(pendingAdmin); + } + + function testRemoveAdmin() public { + address admin = makeAddr("admin"); + + vm.prank(owner); + serviceManager.removeAdmin(admin); + } + + function testSetAppointee() public { + address appointee = makeAddr("appointee"); + address target = makeAddr("target"); + bytes4 selector = bytes4(keccak256("someFunction()")); + + vm.prank(owner); + serviceManager.setAppointee(appointee, target, selector); + } + + function testRemoveAppointee() public { + address appointee = makeAddr("appointee"); + address target = makeAddr("target"); + bytes4 selector = bytes4(keccak256("someFunction()")); + + vm.prank(owner); + serviceManager.removeAppointee(appointee, target, selector); + } } diff --git a/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol b/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol index befc76c4..076c7393 100644 --- a/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol +++ b/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol @@ -5,67 +5,112 @@ import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISi import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import { IECDSAStakeRegistry, - IECDSAStakeRegistryTypes + IECDSAStakeRegistryTypes, + IECDSAStakeRegistryErrors } from "../../src/interfaces/IECDSAStakeRegistry.sol"; import {ECDSAStakeRegistrySetup} from "./ECDSAStakeRegistryUnit.t.sol"; import {ECDSAStakeRegistryEqualWeight} from "../../src/unaudited/examples/ECDSAStakeRegistryEqualWeight.sol"; +import {IAVSDirectory} from "../../src/unaudited/ECDSAStakeRegistry.sol"; contract EqualWeightECDSARegistry is ECDSAStakeRegistrySetup { ECDSAStakeRegistryEqualWeight internal fixedWeightRegistry; + address internal operator6 = makeAddr("operator6"); + address internal operator7 = makeAddr("operator7"); function setUp() public virtual override { super.setUp(); - fixedWeightRegistry = - new ECDSAStakeRegistryEqualWeight(IDelegationManager(address(mockDelegationManager))); + fixedWeightRegistry = new ECDSAStakeRegistryEqualWeight( + IDelegationManager(address(mockDelegationManager)), + IAllocationManager(address(mockAllocationManager)), + mockAVSRegistrarAddr, + IAVSDirectory(address(mockAVSDirectory)) + ); + IStrategy mockStrategy = IStrategy(address(0x1234)); - IECDSAStakeRegistryTypes.Quorum memory quorum = - IECDSAStakeRegistryTypes.Quorum({strategies: new StrategyParams[](1)}); - quorum.strategies[0] = StrategyParams({strategy: mockStrategy, multiplier: 10000}); - fixedWeightRegistry.initialize(address(mockServiceManager), 100, quorum); + IECDSAStakeRegistryTypes.Quorum memory quorum = IECDSAStakeRegistryTypes.Quorum({ + strategies: new IECDSAStakeRegistryTypes.StrategyParams[](1) + }); + quorum.strategies[0] = + IECDSAStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 10000}); + + uint32[] memory operatorSetIds = new uint32[](2); + operatorSetIds[0] = 1; + operatorSetIds[1] = 2; + IECDSAStakeRegistryTypes.StrategyParams[][] memory strategyParamsArray = + new IECDSAStakeRegistryTypes.StrategyParams[][](2); + + strategyParamsArray[0] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[0][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(900)), + multiplier: 3000 + }); + strategyParamsArray[0][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(901)), + multiplier: 3000 + }); + + strategyParamsArray[1] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[1][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(902)), + multiplier: 3000 + }); + strategyParamsArray[1][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(903)), + multiplier: 3000 + }); + + fixedWeightRegistry.initialize( + address(mockServiceManager), 100, quorum, operatorSetIds, strategyParamsArray + ); + + fixedWeightRegistry.permitOperator(operator6); + fixedWeightRegistry.permitOperator(operator7); - fixedWeightRegistry.permitOperator(operator1); - fixedWeightRegistry.permitOperator(operator2); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - vm.prank(operator1); - fixedWeightRegistry.registerOperatorWithSignature(operatorSignature, operator1); - vm.prank(operator2); - fixedWeightRegistry.registerOperatorWithSignature(operatorSignature, operator2); + + vm.prank(operator6); + fixedWeightRegistry.registerOperatorM2Quorum(operatorSignature, operator6); + + vm.prank(operator7); + fixedWeightRegistry.registerOperatorM2Quorum(operatorSignature, operator7); } function test_FixedStakeUpdates() public { - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 1); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator6), 1); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator7), 1); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 2); vm.roll(block.number + 1); - vm.prank(operator1); - fixedWeightRegistry.deregisterOperator(); + vm.prank(operator6); + fixedWeightRegistry.deregisterOperatorM2Quorum(); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 0); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator6), 0); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator7), 1); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 1); vm.roll(block.number + 1); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - vm.prank(operator1); - fixedWeightRegistry.registerOperatorWithSignature(operatorSignature, operator1); + vm.prank(operator6); + fixedWeightRegistry.registerOperatorM2Quorum(operatorSignature, operator6); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 1); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator6), 1); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator7), 1); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 2); vm.roll(block.number + 1); address[] memory operators = new address[](2); - operators[0] = operator1; - operators[1] = operator2; + operators[0] = operator6; + operators[1] = operator7; fixedWeightRegistry.updateOperators(operators); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 1); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator6), 1); + assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator7), 1); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 2); } } diff --git a/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol b/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol index 7269e1ce..e5968505 100644 --- a/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol +++ b/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol @@ -5,6 +5,8 @@ import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISi import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import { IECDSAStakeRegistry, @@ -14,27 +16,74 @@ import { import {ECDSAStakeRegistrySetup} from "./ECDSAStakeRegistryUnit.t.sol"; import {ECDSAStakeRegistryPermissioned} from "../../src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol"; +import {IAVSDirectory} from "../../src/unaudited/ECDSAStakeRegistry.sol"; contract PermissionedECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { ECDSAStakeRegistryPermissioned internal permissionedRegistry; + address internal operator6 = makeAddr("operator6"); + address internal operator7 = makeAddr("operator7"); function setUp() public virtual override { super.setUp(); - permissionedRegistry = - new ECDSAStakeRegistryPermissioned(IDelegationManager(address(mockDelegationManager))); + permissionedRegistry = new ECDSAStakeRegistryPermissioned( + IDelegationManager(address(mockDelegationManager)), + IAllocationManager(address(mockAllocationManager)), + mockAVSRegistrarAddr, + IAVSDirectory(address(mockAVSDirectory)) + ); + IStrategy mockStrategy = IStrategy(address(0x1234)); - IECDSAStakeRegistryTypes.Quorum memory quorum = - IECDSAStakeRegistryTypes.Quorum({strategies: new StrategyParams[](1)}); - quorum.strategies[0] = StrategyParams({strategy: mockStrategy, multiplier: 10000}); - permissionedRegistry.initialize(address(mockServiceManager), 100, quorum); + IECDSAStakeRegistryTypes.Quorum memory quorum = IECDSAStakeRegistryTypes.Quorum({ + strategies: new IECDSAStakeRegistryTypes.StrategyParams[](1) + }); + quorum.strategies[0] = + IECDSAStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 10000}); + + // Create operator set IDs and strategy params + uint32[] memory operatorSetIds = new uint32[](2); + operatorSetIds[0] = 1; + operatorSetIds[1] = 2; + IECDSAStakeRegistryTypes.StrategyParams[][] memory strategyParamsArray = + new IECDSAStakeRegistryTypes.StrategyParams[][](2); + + // Strategy params for first operator set + strategyParamsArray[0] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[0][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(900)), // Lower address + multiplier: 3000 + }); + strategyParamsArray[0][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(901)), // Higher address + multiplier: 3000 + }); + + // Strategy params for second operator set + strategyParamsArray[1] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[1][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(902)), // Lower address + multiplier: 3000 + }); + strategyParamsArray[1][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(903)), // Higher address + multiplier: 3000 + }); + + permissionedRegistry.initialize( + address(mockServiceManager), 100, quorum, operatorSetIds, strategyParamsArray + ); + + permissionedRegistry.permitOperator(operator6); + permissionedRegistry.permitOperator(operator7); - permissionedRegistry.permitOperator(operator1); - permissionedRegistry.permitOperator(operator2); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - vm.prank(operator1); - permissionedRegistry.registerOperatorWithSignature(operatorSignature, operator1); - vm.prank(operator2); - permissionedRegistry.registerOperatorWithSignature(operatorSignature, operator1); + + vm.prank(operator6); + permissionedRegistry.registerOperatorM2Quorum(operatorSignature, operator6); + + vm.prank(operator7); + permissionedRegistry.registerOperatorM2Quorum(operatorSignature, operator7); + + vm.roll(block.number + 1); } function test_RevertsWhen_NotOwner_PermitOperator() public { @@ -45,8 +94,8 @@ contract PermissionedECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_When_Owner_PermitOperator() public { - address operator3 = address(0xBEEF); - permissionedRegistry.permitOperator(operator3); + address operator8 = address(0xBEEF); + permissionedRegistry.permitOperator(operator8); } function test_RevertsWhen_NotOwner_RevokeOperator() public { @@ -64,55 +113,53 @@ contract PermissionedECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_When_Owner_RevokeOperator() public { - permissionedRegistry.revokeOperator(operator1); + permissionedRegistry.revokeOperator(operator6); } function test_RevertsWhen_NotOwner_EjectOperator() public { address notOwner = address(0xBEEF); vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); - permissionedRegistry.ejectOperator(operator1); + permissionedRegistry.ejectOperator(operator6); } function test_RevertsWhen_NotOperator_EjectOperator() public { address notOperator = address(0xBEEF); - vm.expectRevert( - abi.encodeWithSelector(IECDSAStakeRegistryErrors.OperatorNotRegistered.selector) - ); + vm.expectRevert(); permissionedRegistry.ejectOperator(notOperator); } function test_When_Owner_EjectOperator() public { - permissionedRegistry.ejectOperator(operator1); + permissionedRegistry.ejectOperator(operator6); } - function test_RevertsWhen_NotAllowlisted_RegisterOperatorWithSig() public { - address operator3 = address(0xBEEF); + function test_RevertsWhen_NotAllowlisted_RegisterOperatorM2Quorum() public { + address operator8 = address(0xBEEF); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; vm.expectRevert( abi.encodeWithSelector(ECDSAStakeRegistryPermissioned.OperatorNotAllowlisted.selector) ); - vm.prank(operator3); - permissionedRegistry.registerOperatorWithSignature(operatorSignature, operator3); + vm.prank(operator8); + permissionedRegistry.registerOperatorM2Quorum(operatorSignature, operator8); } - function test_WhenAllowlisted_RegisterOperatorWithSig() public { - address operator3 = address(0xBEEF); - permissionedRegistry.permitOperator(operator3); + function test_WhenAllowlisted_RegisterOperatorM2Quorum() public { + address operator8 = address(0xBEEF); + permissionedRegistry.permitOperator(operator8); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - vm.prank(operator3); - permissionedRegistry.registerOperatorWithSignature(operatorSignature, operator3); + vm.prank(operator8); + permissionedRegistry.registerOperatorM2Quorum(operatorSignature, operator8); } - function test_DeregisterOperator() public { - address operator3 = address(0xBEEF); - permissionedRegistry.permitOperator(operator3); + function test_DeregisterOperatorM2Quorum() public { + address operator8 = address(0xBEEF); + permissionedRegistry.permitOperator(operator8); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - vm.prank(operator3); - permissionedRegistry.registerOperatorWithSignature(operatorSignature, operator3); + vm.prank(operator8); + permissionedRegistry.registerOperatorM2Quorum(operatorSignature, operator8); - vm.prank(operator3); - permissionedRegistry.deregisterOperator(); + vm.prank(operator8); + permissionedRegistry.deregisterOperatorM2Quorum(); } } diff --git a/test/unit/ECDSAStakeRegistryRatioWeightUnit.t.sol b/test/unit/ECDSAStakeRegistryRatioWeightUnit.t.sol new file mode 100644 index 00000000..cd6bf6d7 --- /dev/null +++ b/test/unit/ECDSAStakeRegistryRatioWeightUnit.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {IDelegationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IAllocationManager} from + "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; + +import { + IECDSAStakeRegistry, + IECDSAStakeRegistryTypes, + IECDSAStakeRegistryErrors +} from "../../src/interfaces/IECDSAStakeRegistry.sol"; +import {ECDSAStakeRegistrySetup} from "./ECDSAStakeRegistryUnit.t.sol"; +import {ECDSAStakeRegistryRatioWeight} from + "../../src/unaudited/examples/ECDSAStakeRegistryRatioWeight.sol"; +import {IAVSDirectory} from "../../src/unaudited/ECDSAStakeRegistry.sol"; + +contract RatioWeightECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { + /// @notice Event emitted when the weight ratio is updated + event WeightRatioUpdated(uint256 oldRatio, uint256 newRatio); + + ECDSAStakeRegistryRatioWeight internal ratioWeightRegistry; + + function setUp() public virtual override { + super.setUp(); + ratioWeightRegistry = new ECDSAStakeRegistryRatioWeight( + IDelegationManager(address(mockDelegationManager)), + IAllocationManager(address(mockAllocationManager)), + mockAVSRegistrarAddr, + IAVSDirectory(address(mockAVSDirectory)) + ); + + IStrategy mockStrategy = IStrategy(address(0x1234)); + IECDSAStakeRegistryTypes.Quorum memory quorum = IECDSAStakeRegistryTypes.Quorum({ + strategies: new IECDSAStakeRegistryTypes.StrategyParams[](1) + }); + quorum.strategies[0] = + IECDSAStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 10000}); + + uint32[] memory operatorSetIds = new uint32[](2); + operatorSetIds[0] = 1; + operatorSetIds[1] = 2; + IECDSAStakeRegistryTypes.StrategyParams[][] memory strategyParamsArray = + new IECDSAStakeRegistryTypes.StrategyParams[][](2); + + strategyParamsArray[0] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[0][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(900)), + multiplier: 3000 + }); + strategyParamsArray[0][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(901)), + multiplier: 3000 + }); + + strategyParamsArray[1] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[1][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(902)), + multiplier: 3000 + }); + strategyParamsArray[1][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(903)), + multiplier: 3000 + }); + ratioWeightRegistry.initialize( + address(mockServiceManager), 100, quorum, operatorSetIds, strategyParamsArray + ); + // Register operator3 and operator4 to M2 quorum + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + vm.prank(operator3); + ratioWeightRegistry.registerOperatorM2Quorum(operatorSignature, operator3); + vm.prank(operator4); + ratioWeightRegistry.registerOperatorM2Quorum(operatorSignature, operator4); + } + + function test_RevertsWhen_NotOwner_SetWeightRatio() public { + address notOwner = address(0xBEEF); + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + ratioWeightRegistry.setWeightRatio(5000); + } + + function test_RevertsWhen_InvalidRatio_SetWeightRatio() public { + vm.expectRevert( + abi.encodeWithSelector(ECDSAStakeRegistryRatioWeight.InvalidWeightRatio.selector, 10001) + ); + ratioWeightRegistry.setWeightRatio(10001); + } + + function test_When_Owner_SetWeightRatio() public { + // Test setting ratio to 70% quorum weight, 30% operator set weight + vm.expectEmit(true, true, true, true); + emit WeightRatioUpdated(0, 7000); + ratioWeightRegistry.setWeightRatio(7000); + assertEq(ratioWeightRegistry.getWeightRatio(), 7000); + } + + function test_WeightCalculation_WithDifferentRatios() public { + // Initial weights should be equal since ratio is 0 + uint256 initialWeight = ratioWeightRegistry.getOperatorWeight(operator3); + + // Set ratio to 70% quorum weight, 30% operator set weight + ratioWeightRegistry.setWeightRatio(7000); + uint256 quorumWeight = ratioWeightRegistry.getQuorumWeight(operator3); + uint256 operatorSetWeight = ratioWeightRegistry.getOperatorSetWeight(operator3); + uint256 expectedWeight = (quorumWeight * 7000 / 10000) + (operatorSetWeight * 3000 / 10000); + assertEq(ratioWeightRegistry.getOperatorWeight(operator3), expectedWeight); + + // Change ratio to 30% quorum weight, 70% operator set weight + ratioWeightRegistry.setWeightRatio(3000); + + expectedWeight = (quorumWeight * 3000 / 10000) + (operatorSetWeight * 7000 / 10000); + assertEq(ratioWeightRegistry.getOperatorWeight(operator3), expectedWeight); + } + + function test_WeightCalculation_WithEqualRatio() public { + // Set ratio to 50% quorum weight, 50% operator set weight + ratioWeightRegistry.setWeightRatio(5000); + uint256 quorumWeight = ratioWeightRegistry.getQuorumWeight(operator3); + uint256 operatorSetWeight = ratioWeightRegistry.getOperatorSetWeight(operator3); + + uint256 expectedWeight = (quorumWeight * 5000 / 10000) + (operatorSetWeight * 5000 / 10000); + assertEq(ratioWeightRegistry.getOperatorWeight(operator3), expectedWeight); + } +} diff --git a/test/unit/ECDSAStakeRegistryUnit.t.sol b/test/unit/ECDSAStakeRegistryUnit.t.sol index 540f8dcc..9f83dca4 100644 --- a/test/unit/ECDSAStakeRegistryUnit.t.sol +++ b/test/unit/ECDSAStakeRegistryUnit.t.sol @@ -15,17 +15,34 @@ import { IECDSAStakeRegistryTypes, IECDSAStakeRegistryEvents } from "../../src/interfaces/IECDSAStakeRegistry.sol"; +import { + IAllocationManager, + IAllocationManagerTypes +} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAVSDirectory, IAVSDirectoryTypes} from "../../src/unaudited/ECDSAStakeRegistry.sol"; +import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; contract MockServiceManager { - // solhint-disable-next-line + MockAVSDirectory public immutable avsDirectory; + + constructor( + address _avsDirectory + ) { + avsDirectory = MockAVSDirectory(_avsDirectory); + } + function deregisterOperatorFromAVS( - address - ) external {} + address operator + ) external { + avsDirectory.deregisterOperatorFromAVS(operator); + } function registerOperatorToAVS( - address, - ISignatureUtils.SignatureWithSaltAndExpiry memory // solhint-disable-next-line - ) external {} + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external { + avsDirectory.registerOperatorToAVS(operator, operatorSignature); + } } contract MockDelegationManager { @@ -45,38 +62,258 @@ contract MockDelegationManager { } } +contract MockAVSDirectory { + error OperatorAlreadyRegistered(); + error OperatorNotRegistered(); + + mapping(address => IAVSDirectoryTypes.OperatorAVSRegistrationStatus) private operatorStatus; + + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory + ) external { + if (operatorStatus[operator] == IAVSDirectoryTypes.OperatorAVSRegistrationStatus.REGISTERED) + { + revert OperatorAlreadyRegistered(); + } + operatorStatus[operator] = IAVSDirectoryTypes.OperatorAVSRegistrationStatus.REGISTERED; + } + + function deregisterOperatorFromAVS( + address operator + ) external { + if ( + operatorStatus[operator] + == IAVSDirectoryTypes.OperatorAVSRegistrationStatus.UNREGISTERED + ) { + revert OperatorNotRegistered(); + } + operatorStatus[operator] = IAVSDirectoryTypes.OperatorAVSRegistrationStatus.UNREGISTERED; + } + + function updateAVSMetadataURI( + string memory + ) external pure {} + + function setAvsOperatorStatus( + address avs, + address operator, + IAVSDirectoryTypes.OperatorAVSRegistrationStatus status + ) external { + operatorStatus[operator] = status; + } + + function avsOperatorStatus( + address avs, + address operator + ) external view returns (IAVSDirectoryTypes.OperatorAVSRegistrationStatus) { + return operatorStatus[operator]; + } +} + +contract MockAllocationManager { + mapping(address => mapping(bytes32 => IAllocationManagerTypes.Allocation)) public allocations; + mapping(address => mapping(IStrategy => uint64)) public maxMagnitudes; + mapping(address => mapping(bytes32 => bool)) public operatorSetMembership; + + function getAllocation( + address operator, + OperatorSet memory operatorSet, + IStrategy strategy + ) external view returns (IAllocationManagerTypes.Allocation memory) { + bytes32 key = keccak256(abi.encode(operator, operatorSet, address(strategy))); + return allocations[operator][key]; + } + + function setOperatorWeight( + address operator, + OperatorSet memory operatorSet, + address strategy, + uint256 weight + ) external { + bytes32 key = keccak256(abi.encode(operator, operatorSet, address(strategy))); + allocations[operator][key] = IAllocationManagerTypes.Allocation({ + currentMagnitude: uint64(weight), + pendingDiff: 0, + effectBlock: 0 + }); + bytes32 membershipKey = keccak256(abi.encode(operator, operatorSet)); + operatorSetMembership[operator][membershipKey] = true; + } + + function isMemberOfOperatorSet( + address operator, + OperatorSet memory operatorSet + ) external view returns (bool) { + bytes32 key = keccak256(abi.encode(operator, operatorSet)); + return operatorSetMembership[operator][key]; + } + + function setOperatorSetMembership( + address operator, + OperatorSet memory operatorSet, + bool isMember + ) external { + bytes32 key = keccak256(abi.encode(operator, operatorSet)); + operatorSetMembership[operator][key] = isMember; + } + + function isOperatorSet( + OperatorSet memory + ) external pure returns (bool) { + return true; + } + + function getStrategiesInOperatorSet( + OperatorSet memory + ) external pure returns (IStrategy[] memory) { + IStrategy[] memory strategies = new IStrategy[](2); + strategies[0] = IStrategy(address(900)); + strategies[1] = IStrategy(address(901)); + return strategies; + } + + function setAVSRegistrar(address, address) external {} + + function getMaxMagnitude(address operator, IStrategy strategy) external view returns (uint64) { + return maxMagnitudes[operator][strategy]; + } + + function setMaxMagnitude(address operator, IStrategy strategy, uint64 magnitude) external { + maxMagnitudes[operator][strategy] = magnitude; + } +} + contract ECDSAStakeRegistrySetup is Test, IECDSAStakeRegistryEvents { MockDelegationManager public mockDelegationManager; + MockAVSDirectory public mockAVSDirectory; + MockAllocationManager public mockAllocationManager; MockServiceManager public mockServiceManager; ECDSAStakeRegistry public registry; + address public owner = makeAddr("owner"); address internal operator1; address internal operator2; + address internal operator3; + address internal operator4; uint256 internal operator1Pk; uint256 internal operator2Pk; + uint256 internal operator3Pk; + uint256 internal operator4Pk; bytes internal signature1; bytes internal signature2; + bytes internal signature3; + bytes internal signature4; address[] internal signers; bytes[] internal signatures; bytes32 internal msgHash; + address public mockAVSRegistrarAddr; function setUp() public virtual { + // operator 1 and 2 are registered on M2 quorum (operator1, operator1Pk) = makeAddrAndKey("Signer 1"); (operator2, operator2Pk) = makeAddrAndKey("Signer 2"); + // operator 3 and 4 are registered on operator sets + (operator3, operator3Pk) = makeAddrAndKey("Signer 3"); + (operator4, operator4Pk) = makeAddrAndKey("Signer 4"); + + mockAVSDirectory = new MockAVSDirectory(); + mockServiceManager = new MockServiceManager(address(mockAVSDirectory)); + mockDelegationManager = new MockDelegationManager(); - mockServiceManager = new MockServiceManager(); + mockAllocationManager = new MockAllocationManager(); + mockAVSRegistrarAddr = makeAddr("mockAVSRegistrar"); + IStrategy mockStrategy = IStrategy(address(0x1234)); IECDSAStakeRegistryTypes.Quorum memory quorum = IECDSAStakeRegistryTypes.Quorum({ strategies: new IECDSAStakeRegistryTypes.StrategyParams[](1) }); quorum.strategies[0] = IECDSAStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 10000}); - registry = new ECDSAStakeRegistry(IDelegationManager(address(mockDelegationManager))); - registry.initialize(address(mockServiceManager), 100, quorum); + + registry = new ECDSAStakeRegistry( + IDelegationManager(address(mockDelegationManager)), + IAllocationManager(address(mockAllocationManager)), + mockAVSRegistrarAddr, + IAVSDirectory(address(mockAVSDirectory)) + ); + + vm.prank(owner); + uint32[] memory operatorSetIds = new uint32[](2); + operatorSetIds[0] = 1; + operatorSetIds[1] = 2; + IECDSAStakeRegistryTypes.StrategyParams[][] memory strategyParamsArray = + new IECDSAStakeRegistryTypes.StrategyParams[][](2); + + // Strategy params for first operator set + strategyParamsArray[0] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[0][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(900)), + multiplier: 3000 + }); + strategyParamsArray[0][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(901)), + multiplier: 3000 + }); + + // Strategy params for second operator set + strategyParamsArray[1] = new IECDSAStakeRegistryTypes.StrategyParams[](2); + strategyParamsArray[1][0] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(902)), + multiplier: 3000 + }); + strategyParamsArray[1][1] = IECDSAStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(903)), + multiplier: 3000 + }); + + registry.initialize( + address(mockServiceManager), 100, quorum, operatorSetIds, strategyParamsArray + ); + + vm.prank(owner); + registry.setCurrentOperatorSetIds(operatorSetIds); + + // Set up operator weights for operator3 and operator4 + address[] memory operators = new address[](2); + operators[0] = operator3; + operators[1] = operator4; + + // Set weights for each operator in each set + for (uint32 setId = 1; setId <= 2; setId++) { + OperatorSet memory operatorSet = + OperatorSet({avs: address(mockServiceManager), id: setId}); + + // Get strategies for this set + IStrategy[] memory strategies = + mockAllocationManager.getStrategiesInOperatorSet(operatorSet); + + // Set weights and maxMagnitudes for each operator and strategy + for (uint256 i = 0; i < operators.length; i++) { + for (uint256 j = 0; j < strategies.length; j++) { + // Set operator weight/allocation + mockAllocationManager.setOperatorWeight( + operators[i], + operatorSet, + address(strategies[j]), + 3e17 // 30% in WAD + ); + mockAllocationManager.setMaxMagnitude(operators[i], strategies[j], 1e18); + } + } + } + vm.startPrank(mockAVSRegistrarAddr); + registry.onOperatorSetRegistered(operator3, operator3); + registry.onOperatorSetRegistered(operator4, operator3); + vm.stopPrank(); + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + vm.prank(operator1); - registry.registerOperatorWithSignature(operatorSignature, operator1); + registry.registerOperatorM2Quorum(operatorSignature, operator1); + vm.prank(operator2); - registry.registerOperatorWithSignature(operatorSignature, operator2); + registry.registerOperatorM2Quorum(operatorSignature, operator2); + vm.roll(block.number + 1); } } @@ -98,6 +335,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { vm.expectEmit(true, true, false, true); emit QuorumUpdated(oldQuorum, newQuorum); + vm.prank(owner); registry.updateQuorumConfig(newQuorum, operators); } @@ -115,6 +353,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { operators[1] = operator2; vm.expectRevert(IECDSAStakeRegistryErrors.InvalidQuorum.selector); + vm.prank(owner); registry.updateQuorumConfig(invalidQuorum, operators); } @@ -145,6 +384,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { operators[1] = operator2; /// Showing this doesnt revert + vm.prank(owner); registry.updateQuorumConfig(quorum, operators); } @@ -160,6 +400,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { invalidQuorum.strategies[1] = StrategyParams({strategy: IStrategy(address(420)), multiplier: 5000}); vm.expectRevert(IECDSAStakeRegistryErrors.NotSorted.selector); + vm.prank(owner); registry.updateQuorumConfig(invalidQuorum, operators); } @@ -175,6 +416,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { invalidQuorum.strategies[1] = StrategyParams({strategy: IStrategy(address(419)), multiplier: 5000}); vm.expectRevert(IECDSAStakeRegistryErrors.NotSorted.selector); + vm.prank(owner); registry.updateQuorumConfig(invalidQuorum, operators); } @@ -188,29 +430,30 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { operators[1] = operator2; vm.expectRevert(IECDSAStakeRegistryErrors.InvalidQuorum.selector); + vm.prank(owner); registry.updateQuorumConfig(invalidQuorum, operators); } - function test_RegisterOperatorWithSignature() public { - address operator3 = address(0x125); + function test_RegisterOperatorM2Quorum() public { + address operator6 = address(0x125); ISignatureUtils.SignatureWithSaltAndExpiry memory signature; - vm.prank(operator3); - registry.registerOperatorWithSignature(signature, operator3); - assertTrue(registry.operatorRegistered(operator3)); - assertEq(registry.getLastCheckpointOperatorWeight(operator3), 1000); + vm.prank(operator6); + registry.registerOperatorM2Quorum(signature, operator6); + assertTrue(registry.operatorRegistered(operator6)); + assertEq(registry.getLastCheckpointOperatorWeight(operator6), 1000); } - function test_RevertsWhen_AlreadyRegistered_RegisterOperatorWithSignature() public { + function test_RevertsWhen_AlreadyRegistered_RegisterOperatorM2Quorum() public { assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1000); - assertEq(registry.getLastCheckpointTotalWeight(), 2000); + assertEq(registry.getLastCheckpointTotalWeight(), 2720); ISignatureUtils.SignatureWithSaltAndExpiry memory signature; - vm.expectRevert(IECDSAStakeRegistryErrors.OperatorAlreadyRegistered.selector); + vm.expectRevert(); vm.prank(operator1); - registry.registerOperatorWithSignature(signature, operator1); + registry.registerOperatorM2Quorum(signature, operator1); } - function test_RevertsWhen_SignatureIsInvalid_RegisterOperatorWithSignature() public { + function test_RevertsWhen_SignatureIsInvalid_RegisterOperatorM2Quorum() public { bytes memory signatureData; vm.mockCall( address(mockServiceManager), @@ -227,22 +470,23 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { ); } - function test_DeregisterOperator() public { + function test_DeregisterOperatorM2Quorum() public { assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1000); - assertEq(registry.getLastCheckpointTotalWeight(), 2000); + assertEq(registry.getLastCheckpointTotalWeight(), 2720); vm.prank(operator1); - registry.deregisterOperator(); + registry.deregisterOperatorM2Quorum(); + console.log("operator1 weight", registry.getLastCheckpointOperatorWeight(operator1)); assertEq(registry.getLastCheckpointOperatorWeight(operator1), 0); - assertEq(registry.getLastCheckpointTotalWeight(), 1000); + assertEq(registry.getLastCheckpointTotalWeight(), 1720); } - function test_RevertsWhen_NotOperator_DeregisterOperator() public { + function test_RevertsWhen_NotOperator_DeregisterOperatorM2Quorum() public { address notOperator = address(0x2); vm.prank(notOperator); vm.expectRevert(IECDSAStakeRegistryErrors.OperatorNotRegistered.selector); - registry.deregisterOperator(); + registry.deregisterOperatorM2Quorum(); } function test_When_Empty_UpdateOperators() public { @@ -252,12 +496,12 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { function test_When_OperatorNotRegistered_UpdateOperators() public { address[] memory operators = new address[](3); - address operator3 = address(0xBEEF); + address operator6 = address(0xBEEF); operators[0] = operator1; operators[1] = operator2; - operators[2] = operator3; + operators[2] = operator6; registry.updateOperators(operators); - assertEq(registry.getLastCheckpointOperatorWeight(operator3), 0); + assertEq(registry.getLastCheckpointOperatorWeight(operator6), 0); } function test_When_SingleOperator_UpdateOperators() public { @@ -275,17 +519,17 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { operators[1] = operator2; assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1000); - assertEq(registry.getLastCheckpointTotalWeight(), 2000); + assertEq(registry.getLastCheckpointTotalWeight(), 2720); registry.updateOperators(operators); assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1000); - assertEq(registry.getLastCheckpointTotalWeight(), 2000); + assertEq(registry.getLastCheckpointTotalWeight(), 2720); registry.updateOperators(operators); assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1000); - assertEq(registry.getLastCheckpointTotalWeight(), 2000); + assertEq(registry.getLastCheckpointTotalWeight(), 2720); /// TODO: Need to confirm we always pull the last if the block numbers are the same for checkpoints /// in the getAtBlock function or if we need to prevent this behavior } @@ -327,6 +571,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { operators[0] = operator1; operators[1] = operator2; + vm.prank(owner); registry.updateQuorumConfig(quorum, operators); address[] memory strategies = new address[](2); @@ -361,6 +606,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; + vm.prank(owner); registry.updateMinimumWeight(newMinimumWeight, operators); uint256 updatedMinimumWeight = registry.minimumWeight(); @@ -382,6 +628,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; + vm.prank(owner); registry.updateMinimumWeight(initialMinimumWeight, operators); uint256 updatedMinimumWeight = registry.minimumWeight(); @@ -393,10 +640,12 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; + vm.prank(owner); registry.updateMinimumWeight(initialMinimumWeight, operators); uint256 newMinimumWeight = 0; + vm.prank(owner); registry.updateMinimumWeight(newMinimumWeight, operators); uint256 updatedMinimumWeight = registry.minimumWeight(); @@ -426,7 +675,6 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { signatures[0] = abi.encodePacked(r, s, v); (v, r, s) = vm.sign(operator2Pk, msgHash); signatures[1] = abi.encodePacked(r, s, v); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, block.number - 1)); } @@ -595,16 +843,16 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { uint256 before = gasleft(); vm.pauseGasMetering(); vm.prank(operator1); - registry.deregisterOperator(); + registry.deregisterOperatorM2Quorum(); vm.prank(operator2); - registry.deregisterOperator(); + registry.deregisterOperatorM2Quorum(); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; address[] memory operators = new address[](30); for (uint256 i; i < operators.length; i++) { operators[i] = address(uint160(i)); vm.prank(operators[i]); - registry.registerOperatorWithSignature(operatorSignature, operators[i]); + registry.registerOperatorM2Quorum(operatorSignature, operators[i]); } vm.resumeGasMetering(); registry.updateOperators(operators); @@ -616,9 +864,9 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { uint256 before = gasleft(); vm.pauseGasMetering(); vm.prank(operator1); - registry.deregisterOperator(); + registry.deregisterOperatorM2Quorum(); vm.prank(operator2); - registry.deregisterOperator(); + registry.deregisterOperatorM2Quorum(); msgHash = keccak256("data"); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; @@ -630,7 +878,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { for (uint256 i = 1; i < operators.length + 1; i++) { operators[i - 1] = address(vm.addr(i)); vm.prank(operators[i - 1]); - registry.registerOperatorWithSignature(operatorSignature, operators[i - 1]); + registry.registerOperatorM2Quorum(operatorSignature, operators[i - 1]); (v, r, s) = vm.sign(i, msgHash); signatures[i - 1] = abi.encodePacked(r, s, v); } @@ -645,19 +893,20 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } // Define private and public keys for operator3 and signer - uint256 private operator3Pk = 3; - address private operator3 = address(vm.addr(operator3Pk)); + // Define private and public keys for operator5 and signer + uint256 private operator5Pk = 3; + address private operator5 = address(vm.addr(operator5Pk)); uint256 private signerPk = 4; address private signer = address(vm.addr(signerPk)); function test_WhenUsingSigningKey_RegierOperatorWithSignature() public { - address operator = operator3; + address operator = operator5; ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; // Register operator with a different signing key vm.prank(operator); - registry.registerOperatorWithSignature(operatorSignature, signer); + registry.registerOperatorM2Quorum(operatorSignature, signer); // Verify that the signing key has been successfully registered for the operator address registeredSigningKey = registry.getLatestOperatorSigningKey(operator); @@ -669,13 +918,13 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_Twice_RegierOperatorWithSignature() public { - address operator = operator3; + address operator = operator5; ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; // Register operator with a different signing key vm.prank(operator); - registry.registerOperatorWithSignature(operatorSignature, signer); + registry.registerOperatorM2Quorum(operatorSignature, signer); /// Register a second time vm.prank(operator); @@ -695,13 +944,13 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_WhenUsingSigningKey_CheckSignatures() public { - address operator = operator3; + address operator = operator5; ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; // Register operator with a different signing key vm.prank(operator); - registry.registerOperatorWithSignature(operatorSignature, signer); + registry.registerOperatorM2Quorum(operatorSignature, signer); vm.roll(block.number + 1); // Prepare data for signature @@ -719,7 +968,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_WhenUsingSigningKey_CheckSignaturesAtBlock() public { - address operator = operator3; + address operator = operator5; address initialSigningKey = address(vm.addr(signerPk)); address updatedSigningKey = address(vm.addr(signerPk + 1)); @@ -727,7 +976,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { // Register operator with the initial signing key vm.prank(operator); - registry.registerOperatorWithSignature(operatorSignature, initialSigningKey); + registry.registerOperatorM2Quorum(operatorSignature, initialSigningKey); vm.roll(block.number + 1); // Prepare data for signature with initial signing key @@ -760,7 +1009,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_WhenUsingPriorSigningKey_CheckSignaturesAtBlock() public { - address operator = operator3; + address operator = operator5; address initialSigningKey = address(vm.addr(signerPk)); address updatedSigningKey = address(vm.addr(signerPk + 1)); @@ -768,7 +1017,7 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { // Register operator with the initial signing key vm.prank(operator); - registry.registerOperatorWithSignature(operatorSignature, initialSigningKey); + registry.registerOperatorM2Quorum(operatorSignature, initialSigningKey); vm.roll(block.number + 1); // Prepare data for signature with initial signing key @@ -860,4 +1109,318 @@ contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } return (operators, signatures); } + + function test_OnOperatorSetDeregistered() public { + address operator = operator3; + uint256 initialTotalWeight = registry.getLastCheckpointTotalWeight(); + + for (uint32 setId = 1; setId <= 2; setId++) { + OperatorSet memory operatorSet = + OperatorSet({avs: address(mockServiceManager), id: setId}); + IStrategy[] memory strategies = + mockAllocationManager.getStrategiesInOperatorSet(operatorSet); + for (uint256 j = 0; j < strategies.length; j++) { + mockAllocationManager.setOperatorWeight( + operator, + operatorSet, + address(strategies[j]), + 0 // Set currentMagnitude to 0 + ); + } + } + + vm.expectEmit(); + emit OperatorDeregistered(operator, address(mockServiceManager)); + + vm.prank(mockAVSRegistrarAddr); + registry.onOperatorSetDeregistered(operator); + + assertEq(registry.getLastCheckpointOperatorWeight(operator), 0); + assertEq(registry.getLastCheckpointTotalWeight(), initialTotalWeight - 360); + console.log(registry.operatorRegisteredOnAVSDirectory(operator)); + } + + function test_MultipleOperatorRegistration() public { + address operator6 = makeAddr("operator6"); + + address[] memory operators = new address[](1); + operators[0] = operator6; + + for (uint32 setId = 1; setId <= 2; setId++) { + OperatorSet memory operatorSet = + OperatorSet({avs: address(mockServiceManager), id: setId}); + + IStrategy[] memory strategies = + mockAllocationManager.getStrategiesInOperatorSet(operatorSet); + for (uint256 i = 0; i < operators.length; i++) { + for (uint256 j = 0; j < strategies.length; j++) { + // Set operator weight/allocation + mockAllocationManager.setOperatorWeight( + operators[i], + operatorSet, + address(strategies[j]), + 3e17 // 30% in WAD + ); + mockAllocationManager.setMaxMagnitude(operators[i], strategies[j], 1e18); + } + } + } + + uint256 initialTotalWeight = registry.getLastCheckpointTotalWeight(); + for (uint256 i = 0; i < operators.length; i++) { + vm.prank(mockAVSRegistrarAddr); + registry.onOperatorSetRegistered(operators[i], operators[i]); + assertEq(registry.getLastCheckpointOperatorWeight(operators[i]), 360); + } + + assertEq(registry.getLastCheckpointTotalWeight(), initialTotalWeight + 360); + } + + function test_SetAndGetCurrentOperatorSetIds() public { + uint32[] memory newIds = new uint32[](2); + newIds[0] = 3; + newIds[1] = 4; + + vm.prank(owner); + registry.setCurrentOperatorSetIds(newIds); + + uint32[] memory retrievedIds = registry.getCurrentOperatorSetIds(); + assertEq(retrievedIds.length, 2); + assertEq(retrievedIds[0], 3); + assertEq(retrievedIds[1], 4); + } + + function test_RevertsWhen_NotOwner_SetCurrentOperatorSetIds() public { + uint32[] memory newIds = new uint32[](2); + newIds[0] = 3; + newIds[1] = 4; + + address notOwner = makeAddr("notOwner"); + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + registry.setCurrentOperatorSetIds(newIds); + } + + function test_UpdateOperatorSetsConfig() public { + uint32[] memory operatorSetIds = new uint32[](2); + operatorSetIds[0] = 1; + operatorSetIds[1] = 2; + + StrategyParams[][] memory strategyParamsArray = new StrategyParams[][](2); + + strategyParamsArray[0] = new StrategyParams[](2); + strategyParamsArray[0][0] = + StrategyParams({strategy: IStrategy(address(800)), multiplier: 4000}); + strategyParamsArray[0][1] = + StrategyParams({strategy: IStrategy(address(801)), multiplier: 4000}); + strategyParamsArray[1] = new StrategyParams[](2); + strategyParamsArray[1][0] = + StrategyParams({strategy: IStrategy(address(802)), multiplier: 4000}); + strategyParamsArray[1][1] = + StrategyParams({strategy: IStrategy(address(803)), multiplier: 4000}); + + address[] memory operators = new address[](2); + operators[0] = operator3; + operators[1] = operator4; + + vm.prank(owner); + registry.updateOperatorSetsConfig(operatorSetIds, strategyParamsArray, operators); + StrategyParams[] memory params = registry.getOperatorSetConfig(1); + assertEq(params.length, 2); + assertEq(address(params[0].strategy), address(800)); + assertEq(params[0].multiplier, 4000); + assertEq(address(params[1].strategy), address(801)); + assertEq(params[1].multiplier, 4000); + + params = registry.getOperatorSetConfig(2); + assertEq(params.length, 2); + assertEq(address(params[0].strategy), address(802)); + assertEq(params[0].multiplier, 4000); + assertEq(address(params[1].strategy), address(803)); + assertEq(params[1].multiplier, 4000); + } + + function test_RevertsWhen_NotOwner_UpdateOperatorSetsConfig() public { + uint32[] memory operatorSetIds = new uint32[](1); + operatorSetIds[0] = 1; + + StrategyParams[][] memory strategyParamsArray = new StrategyParams[][](1); + strategyParamsArray[0] = new StrategyParams[](1); + strategyParamsArray[0][0] = + StrategyParams({strategy: IStrategy(address(800)), multiplier: 4000}); + + address[] memory operators = new address[](1); + operators[0] = operator3; + + address notOwner = makeAddr("notOwner"); + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + registry.updateOperatorSetsConfig(operatorSetIds, strategyParamsArray, operators); + } + + function test_RevertsWhen_LengthMismatch_UpdateOperatorSetsConfig() public { + uint32[] memory operatorSetIds = new uint32[](2); + operatorSetIds[0] = 1; + operatorSetIds[1] = 2; + + StrategyParams[][] memory strategyParamsArray = new StrategyParams[][](1); + strategyParamsArray[0] = new StrategyParams[](1); + strategyParamsArray[0][0] = + StrategyParams({strategy: IStrategy(address(800)), multiplier: 4000}); + + address[] memory operators = new address[](1); + operators[0] = operator3; + + vm.prank(owner); + vm.expectRevert(IECDSAStakeRegistryErrors.InvalidOperatorSetIdsLength.selector); + registry.updateOperatorSetsConfig(operatorSetIds, strategyParamsArray, operators); + } + + function test_RevertsWhen_UnsortedStrategies_UpdateOperatorSetsConfig() public { + uint32[] memory operatorSetIds = new uint32[](1); + operatorSetIds[0] = 1; + + StrategyParams[][] memory strategyParamsArray = new StrategyParams[][](1); + strategyParamsArray[0] = new StrategyParams[](2); + strategyParamsArray[0][0] = StrategyParams({ + strategy: IStrategy(address(801)), // Higher address first + multiplier: 4000 + }); + strategyParamsArray[0][1] = StrategyParams({ + strategy: IStrategy(address(800)), // Lower address second + multiplier: 4000 + }); + + address[] memory operators = new address[](1); + operators[0] = operator3; + + vm.prank(owner); + vm.expectRevert(IECDSAStakeRegistryErrors.NotSorted.selector); + registry.updateOperatorSetsConfig(operatorSetIds, strategyParamsArray, operators); + } + + function test_RegisterOperatorSetAfterM2Quorum() public { + // operator1 is already registered in M2 quorum from setUp() + assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1000); + assertTrue(registry.operatorRegistered(operator1)); + + for (uint32 setId = 1; setId <= 2; setId++) { + OperatorSet memory operatorSet = + OperatorSet({avs: address(mockServiceManager), id: setId}); + + IStrategy[] memory strategies = + mockAllocationManager.getStrategiesInOperatorSet(operatorSet); + for (uint256 j = 0; j < strategies.length; j++) { + mockAllocationManager.setOperatorWeight( + operator1, + operatorSet, + address(strategies[j]), + 3e17 // 30% in WAD + ); + mockAllocationManager.setMaxMagnitude(operator1, strategies[j], 1e18); + } + } + + vm.prank(mockAVSRegistrarAddr); + registry.onOperatorSetRegistered(operator1, operator1); + + assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1360); // 1000 from M2 + 360 from operator sets + assertTrue(registry.operatorRegistered(operator1)); + assertTrue(registry.operatorRegisteredOnAVSDirectory(operator1)); + } + + function test_RegisterM2QuorumAfterOperatorSet() public { + // operator3 is already registered in operator sets from setUp() + assertEq(registry.getLastCheckpointOperatorWeight(operator3), 360); + assertTrue(registry.operatorRegistered(operator3)); + + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + vm.prank(operator3); + registry.registerOperatorM2Quorum(operatorSignature, operator3); + + assertEq(registry.getLastCheckpointOperatorWeight(operator3), 1360); // 360 from operator sets + 1000 from M2 + assertTrue(registry.operatorRegistered(operator3)); + assertTrue(registry.operatorRegisteredOnAVSDirectory(operator3)); + } + + function test_OperatorRegistrationStatus() public { + // Test operator1: Only M2 quorum registration + assertTrue(registry.operatorRegisteredOnAVSDirectory(operator1)); + assertFalse(registry.operatorRegisteredOnCurrentOperatorSets(operator1)); + assertTrue(registry.operatorRegistered(operator1)); + + // Test operator3: Only Operator Sets registration + assertFalse(registry.operatorRegisteredOnAVSDirectory(operator3)); + assertTrue(registry.operatorRegisteredOnCurrentOperatorSets(operator3)); + assertTrue(registry.operatorRegistered(operator3)); + + // Test unregistered operator + address unregisteredOperator = makeAddr("unregistered"); + assertFalse(registry.operatorRegisteredOnAVSDirectory(unregisteredOperator)); + assertFalse(registry.operatorRegisteredOnCurrentOperatorSets(unregisteredOperator)); + assertFalse(registry.operatorRegistered(unregisteredOperator)); + } + + function test_GetOperatorSetWeight_WithMagnitudeChanges() public { + assertEq(registry.getOperatorSetWeight(operator3), 360); + OperatorSet memory operatorSet = OperatorSet({avs: address(mockServiceManager), id: 1}); + IStrategy[] memory strategies = + mockAllocationManager.getStrategiesInOperatorSet(operatorSet); + + for (uint256 j = 0; j < strategies.length; j++) { + mockAllocationManager.setOperatorWeight( + operator3, + operatorSet, + address(strategies[j]), + 1.5e17 // 15% in WAD (half of original 30%) + ); + } + + address[] memory operators = new address[](1); + operators[0] = operator3; + registry.updateOperators(operators); + + // Weight should be halved for first set (180->90) plus full weight from second set (180) = 270 + assertEq(registry.getOperatorSetWeight(operator3), 270); + mockAllocationManager.setOperatorSetMembership(operator3, operatorSet, false); + registry.updateOperators(operators); + + assertEq(registry.getOperatorSetWeight(operator3), 180); + } + + function test_DisableM2QuorumRegistration() public { + // Verify initial state - can register + address newOperator = makeAddr("newOperator"); + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + vm.prank(newOperator); + registry.registerOperatorM2Quorum(operatorSignature, newOperator); + assertTrue(registry.operatorRegisteredOnAVSDirectory(newOperator)); + + vm.expectEmit(true, true, true, true); + emit M2QuorumRegistrationDisabled(); + vm.prank(owner); + registry.disableM2QuorumRegistration(); + + // Try to register new operator after disable - should fail + address anotherOperator = makeAddr("anotherOperator"); + vm.prank(anotherOperator); + vm.expectRevert(IECDSAStakeRegistryErrors.M2QuorumRegistrationIsDisabled.selector); + registry.registerOperatorM2Quorum(operatorSignature, anotherOperator); + + assertTrue(registry.operatorRegisteredOnAVSDirectory(newOperator)); + assertTrue(registry.operatorRegisteredOnAVSDirectory(operator1)); + assertTrue(registry.operatorRegisteredOnAVSDirectory(operator2)); + + // Try to disable again - should fail + vm.prank(owner); + vm.expectRevert(IECDSAStakeRegistryErrors.M2QuorumRegistrationIsDisabled.selector); + registry.disableM2QuorumRegistration(); + } + + function test_RevertsWhen_NotOwner_DisableM2QuorumRegistration() public { + address notOwner = makeAddr("notOwner"); + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + registry.disableM2QuorumRegistration(); + } }