diff --git a/src/unaudited/ECDSAServiceManagerBase.sol b/src/unaudited/ECDSAServiceManagerBase.sol new file mode 100644 index 00000000..4a606c87 --- /dev/null +++ b/src/unaudited/ECDSAServiceManagerBase.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; + +import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; + +import {IServiceManager} from "../interfaces/IServiceManager.sol"; +import {IServiceManagerUI} from "../interfaces/IServiceManagerUI.sol"; +import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IStakeRegistry} from "../interfaces/IStakeRegistry.sol"; +import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; +import {Quorum} from "../interfaces/IECDSAStakeRegistryEventsAndErrors.sol"; +import {ECDSAStakeRegistry} from "../unaudited/ECDSAStakeRegistry.sol"; + +abstract contract ECDSAServiceManagerBase is + IServiceManager, + OwnableUpgradeable +{ + /// @notice Address of the stake registry contract, which manages registration and stake recording. + address public immutable stakeRegistry; + + /// @notice Address of the AVS directory contract, which manages AVS-related data for registered operators. + address public immutable avsDirectory; + + /// @notice Address of the payment coordinator contract, which handles payment distributions. + address internal immutable paymentCoordinator; + + /// @notice Address of the delegation manager contract, which manages staker delegations to operators. + address internal immutable delegationManager; + /** + * @dev Ensures that the function is only callable by the `stakeRegistry` contract. + * This is used to restrict certain registration and deregistration functionality to the `stakeRegistry` + */ + modifier onlyStakeRegistry() { + require( + msg.sender == stakeRegistry, + "ECDSAServiceManagerBase.onlyStakeRegistry: caller is not the stakeRegistry" + ); + _; + } + + /** + * @dev Constructor for ECDSAServiceManagerBase, initializing immutable contract addresses and disabling initializers. + * @param _avsDirectory The address of the AVS directory contract, managing AVS-related data for registered operators. + * @param _stakeRegistry The address of the stake registry contract, managing registration and stake recording. + * @param _paymentCoordinator The address of the payment coordinator contract, handling payment distributions. + * @param _delegationManager The address of the delegation manager contract, managing staker delegations to operators. + */ + constructor( + address _avsDirectory, + address _stakeRegistry, + address _paymentCoordinator, + address _delegationManager + ) { + avsDirectory = _avsDirectory; + stakeRegistry = _stakeRegistry; + paymentCoordinator = _paymentCoordinator; + delegationManager = _delegationManager; + _disableInitializers(); + } + + /** + * @dev Initializes the base service manager by transferring ownership to the initial owner. + * @param initialOwner The address to which the ownership of the contract will be transferred. + */ + function __ServiceManagerBase_init( + address initialOwner + ) internal virtual onlyInitializing { + _transferOwnership(initialOwner); + } + + /// @inheritdoc IServiceManagerUI + function updateAVSMetadataURI( + string memory _metadataURI + ) external virtual onlyOwner { + _updateAVSMetadataURI(_metadataURI); + } + + /// @inheritdoc IServiceManager + function payForRange( + IPaymentCoordinator.RangePayment[] calldata rangePayments + ) external virtual onlyOwner { + _payForRange(rangePayments); + } + + /// @inheritdoc IServiceManagerUI + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external virtual onlyStakeRegistry { + _registerOperatorToAVS(operator, operatorSignature); + } + + /// @inheritdoc IServiceManagerUI + function deregisterOperatorFromAVS( + address operator + ) external virtual onlyStakeRegistry { + _deregisterOperatorFromAVS(operator); + } + + /// @inheritdoc IServiceManagerUI + function getRestakeableStrategies() + external + view + virtual + returns (address[] memory) + { + return _getRestakeableStrategies(); + } + + /// @inheritdoc IServiceManagerUI + function getOperatorRestakedStrategies( + address _operator + ) external view virtual returns (address[] memory) { + return _getOperatorRestakedStrategies(_operator); + } + + /** + * @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. + * @param _metadataURI The new metadata URI to be set. + */ + function _updateAVSMetadataURI( + string memory _metadataURI + ) internal virtual { + IAVSDirectory(avsDirectory).updateAVSMetadataURI(_metadataURI); + } + + /** + * @notice Forwards the call to register an operator in the AVSDirectory contract. + * @dev This internal function is a proxy to the `registerOperatorToAVS` function of the AVSDirectory contract. + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry details of the operator's registration. + */ + function _registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) internal virtual { + IAVSDirectory(avsDirectory).registerOperatorToAVS( + operator, + operatorSignature + ); + } + + /** + * @notice Forwards the call to deregister an operator from the AVSDirectory contract. + * @dev This internal function is a proxy to the `deregisterOperatorFromAVS` function of the AVSDirectory contract. + * @param operator The address of the operator to deregister. + */ + function _deregisterOperatorFromAVS(address operator) internal virtual { + IAVSDirectory(avsDirectory).deregisterOperatorFromAVS(operator); + } + + /** + * @notice Processes a batch of range payments by transferring the specified amounts from the sender to this contract and then approving the PaymentCoordinator to use these amounts. + * @dev This function handles the transfer and approval of tokens necessary for range payments. It then delegates the actual payment logic to the PaymentCoordinator contract. + * @param rangePayments An array of `RangePayment` structs, each representing a payment for a specific range. + */ + function _payForRange( + IPaymentCoordinator.RangePayment[] calldata rangePayments + ) internal virtual { + for (uint256 i = 0; i < rangePayments.length; ++i) { + rangePayments[i].token.transferFrom( + msg.sender, + address(this), + rangePayments[i].amount + ); + rangePayments[i].token.approve( + paymentCoordinator, + rangePayments[i].amount + ); + } + + IPaymentCoordinator(paymentCoordinator).payForRange(rangePayments); + } + + /** + * @notice Retrieves the addresses of all strategies that are part of the current quorum. + * @dev Fetches the quorum configuration from the ECDSAStakeRegistry and extracts the strategy addresses. + * @return strategies An array of addresses representing the strategies in the current quorum. + */ + function _getRestakeableStrategies() + internal + view + virtual + returns (address[] memory) + { + Quorum memory quorum = ECDSAStakeRegistry(stakeRegistry).quorum(); + address[] memory strategies = new address[](quorum.strategies.length); + for (uint256 i = 0; i < quorum.strategies.length; i++) { + strategies[i] = address(quorum.strategies[i].strategy); + } + return strategies; + } + + /** + * @notice Retrieves the addresses of strategies where the operator has restaked. + * @dev This function fetches the quorum details from the ECDSAStakeRegistry, retrieves the operator's shares for each strategy, + * and filters out strategies with non-zero shares indicating active restaking by the operator. + * @param _operator The address of the operator whose restaked strategies are to be retrieved. + * @return restakedStrategies An array of addresses of strategies where the operator has active restakes. + */ + function _getOperatorRestakedStrategies( + address _operator + ) internal view virtual returns (address[] memory) { + Quorum memory quorum = ECDSAStakeRegistry(stakeRegistry).quorum(); + uint256 count = quorum.strategies.length; + IStrategy[] memory strategies = new IStrategy[](count); + for (uint256 i; i < count; i++) { + strategies[i] = quorum.strategies[i].strategy; + } + uint256[] memory shares = IDelegationManager(delegationManager) + .getOperatorShares(_operator, strategies); + + address[] memory activeStrategies = new address[](count); + uint256 activeCount; + for (uint256 i; i < count; i++) { + if (shares[i] > 0) { + activeCount++; + } + } + + // Resize the array to fit only the active strategies + address[] memory restakedStrategies = new address[](activeCount); + for (uint256 j = 0; j < count; j++) { + if (shares[j] > 0) { + restakedStrategies[j] = activeStrategies[j]; + } + } + + return restakedStrategies; + } + + // storage gap for upgradeability + // slither-disable-next-line shadowing-state + uint256[50] private __GAP; +} diff --git a/src/unaudited/ECDSAStakeRegistry.sol b/src/unaudited/ECDSAStakeRegistry.sol index 94b6c638..5a168cb3 100644 --- a/src/unaudited/ECDSAStakeRegistry.sol +++ b/src/unaudited/ECDSAStakeRegistry.sol @@ -58,7 +58,7 @@ contract ECDSAStakeRegistry is } /** - * @notice Updates the StakeRegistry's view of one or more operators' stakes adding a new entry in their history of stake checkpoints, + * @notice Updates the StakeRegistry's view of one or more operators' stakes adding a new entry in their history of stake checkpoints, * @dev Queries stakes from the Eigenlayer core DelegationManager contract * @param _operators A list of operator addresses to update */ @@ -68,12 +68,15 @@ contract ECDSAStakeRegistry is /** * @notice Updates the quorum configuration and the set of operators - * @dev Only callable by the contract owner. + * @dev Only callable by the contract owner. * It first updates the quorum configuration and then updates the list of operators. * @param _quorum The new quorum configuration, including strategies and their new weights * @param _operators The list of operator addresses to update stakes for */ - function updateQuorumConfig(Quorum memory _quorum, address[] memory _operators) external onlyOwner { + function updateQuorumConfig( + Quorum memory _quorum, + address[] memory _operators + ) external onlyOwner { _updateQuorumConfig(_quorum); _updateOperators(_operators); } @@ -81,17 +84,20 @@ contract ECDSAStakeRegistry is /// @notice Updates the weight an operator must have to join the operator set /// @dev Access controlled to the contract owner /// @param _newMinimumWeight The new weight an operator must have to join the operator set - function updateMinimumWeight(uint256 _newMinimumWeight, address[] memory _operators) external onlyOwner { + function updateMinimumWeight( + uint256 _newMinimumWeight, + address[] memory _operators + ) external onlyOwner { _updateMinimumWeight(_newMinimumWeight); _updateOperators(_operators); } /** * @notice Sets a new cumulative threshold weight for message validation by operator set signatures. - * @dev This function can only be invoked by the owner of the contract. It delegates the update to - * an internal function `_updateStakeThreshold`. - * @param _thresholdWeight The updated threshold weight required to validate a message. This is the - * cumulative weight that must be met or exceeded by the sum of the stakes of the signatories for + * @dev This function can only be invoked by the owner of the contract. It delegates the update to + * an internal function `_updateStakeThreshold`. + * @param _thresholdWeight The updated threshold weight required to validate a message. This is the + * cumulative weight that must be met or exceeded by the sum of the stakes of the signatories for * a message to be deemed valid. */ function updateStakeThreshold(uint256 _thresholdWeight) external onlyOwner { @@ -106,10 +112,11 @@ contract ECDSAStakeRegistry is bytes32 _dataHash, bytes memory _signatureData ) external view returns (bytes4) { - (address[] memory signers, bytes[] memory signatures, uint32 referenceBlock) = abi.decode( - _signatureData, - (address[], bytes[], uint32) - ); + ( + address[] memory signers, + bytes[] memory signatures, + uint32 referenceBlock + ) = abi.decode(_signatureData, (address[], bytes[], uint32)); _checkSignatures(_dataHash, signers, signatures, referenceBlock); return IERC1271Upgradeable.isValidSignature.selector; } @@ -123,7 +130,9 @@ contract ECDSAStakeRegistry is /// @notice Retrieves the last recorded weight for a given operator. /// @param _operator The address of the operator. /// @return uint256 - The latest weight of the operator. - function getLastCheckpointOperatorWeight(address _operator) external view returns (uint256) { + function getLastCheckpointOperatorWeight( + address _operator + ) external view returns (uint256) { return _operatorWeightHistory[_operator].latest(); } @@ -133,9 +142,13 @@ contract ECDSAStakeRegistry is return _totalWeightHistory.latest(); } - /// @notice Retrieves the last recorded threshold weight + /// @notice Retrieves the last recorded threshold weight /// @return uint256 - The latest threshold weight. - function getLastCheckpointThresholdWeight() external view returns (uint256) { + function getLastCheckpointThresholdWeight() + external + view + returns (uint256) + { return _thresholdWeightHistory.latest(); } @@ -168,10 +181,12 @@ contract ECDSAStakeRegistry is return _thresholdWeightHistory.getAtBlock(_blockNumber); } - function operatorRegistered(address _operator) external view returns (bool) { + function operatorRegistered( + address _operator + ) external view returns (bool) { return _operatorRegistered[_operator]; } - + /// @notice Returns the weight an operator must have to contribute to validating an AVS function minimumWeight() external view returns (uint256) { return _minimumWeight; @@ -180,18 +195,21 @@ contract ECDSAStakeRegistry is /// @notice Calculates the current weight of an operator based on their delegated stake in the strategies considered in the quorum /// @param _operator The address of the operator. /// @return uint256 - The current weight of the operator; returns 0 if below the threshold. - function getOperatorWeight(address _operator) public view returns (uint256) { + function getOperatorWeight( + address _operator + ) public view returns (uint256) { StrategyParams[] memory strategyParams = _quorum.strategies; uint256 weight; IStrategy[] memory strategies = new IStrategy[](strategyParams.length); for (uint256 i; i < strategyParams.length; i++) { - strategies[i]=strategyParams[i].strategy; - + strategies[i] = strategyParams[i].strategy; } - uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(_operator, strategies); - for (uint256 i; i= _currentSigner){ + function _validateSortedSigners( + address _lastSigner, + address _currentSigner + ) internal pure { + if (_lastSigner >= _currentSigner) { revert NotSorted(); } } @@ -440,7 +473,6 @@ contract ECDSAStakeRegistry is } } - /// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block. /// @param _signer The address of the signer whose weight is returned. /// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint. @@ -460,7 +492,9 @@ contract ECDSAStakeRegistry is /// @dev If the `_referenceBlock` is the maximum value for uint32, the latest total weight is returned. /// @param _referenceBlock The block number to retrieve the total stake weight from. /// @return The total stake weight at the given block or the latest if the given block is the max uint32 value. - function _getTotalWeight(uint32 _referenceBlock) internal view returns (uint256) { + function _getTotalWeight( + uint32 _referenceBlock + ) internal view returns (uint256) { if (_referenceBlock == type(uint32).max) { return _totalWeightHistory.latest(); } else { @@ -472,7 +506,9 @@ contract ECDSAStakeRegistry is /// @param _referenceBlock The block number to query the threshold stake for. /// If set to the maximum uint32 value, it retrieves the latest threshold stake. /// @return The threshold stake in basis points for the reference block. - function _getThresholdStake(uint32 _referenceBlock) internal view returns (uint256) { + function _getThresholdStake( + uint32 _referenceBlock + ) internal view returns (uint256) { if (_referenceBlock == type(uint32).max) { return _thresholdWeightHistory.latest(); } else { @@ -483,13 +519,16 @@ contract ECDSAStakeRegistry is /// @notice Validates that the cumulative stake of signed messages meets or exceeds the required threshold. /// @param _signedWeight The cumulative weight of the signers that have signed the message. /// @param _referenceBlock The block number to verify the stake threshold for - function _validateThresholdStake(uint256 _signedWeight, uint32 _referenceBlock) internal view { + function _validateThresholdStake( + uint256 _signedWeight, + uint32 _referenceBlock + ) internal view { uint256 totalWeight = _getTotalWeight(_referenceBlock); - if (_signedWeight > totalWeight){ + if (_signedWeight > totalWeight) { revert InvalidSignedWeight(); } uint256 thresholdStake = _getThresholdStake(_referenceBlock); - if (thresholdStake > _signedWeight){ + if (thresholdStake > _signedWeight) { revert InsufficientSignedStake(); } }