From ad178c713c5ef03b044597731ebc37003841573f Mon Sep 17 00:00:00 2001 From: 8sunyuan Date: Fri, 19 Apr 2024 10:31:40 -0400 Subject: [PATCH 1/4] feat: service manager payments --- lib/eigenlayer-contracts | 2 +- src/ServiceManagerBase.sol | 30 ++++- src/interfaces/IServiceManager.sol | 16 +++ test/integration/CoreRegistration.t.sol | 7 +- test/integration/IntegrationDeployer.t.sol | 47 +++++++- test/mocks/DelegationMock.sol | 2 + test/mocks/PaymentCoordinatorMock.sol | 125 +++++++++++++++++++++ test/mocks/ServiceManagerMock.sol | 5 +- test/unit/ServiceManagerRouter.t.sol | 3 +- test/utils/MockAVSDeployer.sol | 11 +- 10 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 test/mocks/PaymentCoordinatorMock.sol diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index a31a2f3b..81c39197 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit a31a2f3b226faee8a06b8664dd3b16ff95c01071 +Subproject commit 81c39197839308c580ef2608ff91ab0f017ee889 diff --git a/src/ServiceManagerBase.sol b/src/ServiceManagerBase.sol index 71d262d1..89d96653 100644 --- a/src/ServiceManagerBase.sol +++ b/src/ServiceManagerBase.sol @@ -6,6 +6,7 @@ import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/Ownabl import {BitmapUtils} from "./libraries/BitmapUtils.sol"; import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; +import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; import {IServiceManager} from "./interfaces/IServiceManager.sol"; import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; @@ -22,6 +23,7 @@ abstract contract ServiceManagerBase is IServiceManager, OwnableUpgradeable { IRegistryCoordinator internal immutable _registryCoordinator; IStakeRegistry internal immutable _stakeRegistry; IAVSDirectory internal immutable _avsDirectory; + IPaymentCoordinator internal immutable _paymentCoordinator; /// @notice when applied to a function, only allows the RegistryCoordinator to call it modifier onlyRegistryCoordinator() { @@ -36,11 +38,13 @@ abstract contract ServiceManagerBase is IServiceManager, OwnableUpgradeable { constructor( IAVSDirectory __avsDirectory, IRegistryCoordinator __registryCoordinator, - IStakeRegistry __stakeRegistry + IStakeRegistry __stakeRegistry, + IPaymentCoordinator ___paymentCoordinator ) { _avsDirectory = __avsDirectory; _registryCoordinator = __registryCoordinator; _stakeRegistry = __stakeRegistry; + _paymentCoordinator = ___paymentCoordinator; _disableInitializers(); } @@ -57,6 +61,30 @@ abstract contract ServiceManagerBase is IServiceManager, OwnableUpgradeable { _avsDirectory.updateAVSMetadataURI(_metadataURI); } + /** + * @notice Creates a new range payment on behalf of an AVS, to be split amongst the + * set of stakers delegated to operators who are registered to the `avs`. + * Note that the owner calling this function must have approved the tokens to be transferred to the ServiceManager + * and of course has the required balances. + * @param rangePayments The range payments being created + * @dev Expected to be called by the ServiceManager of the AVS on behalf of which the payment is being made + * @dev The duration of the `rangePayment` cannot exceed `paymentCoordinator.MAX_PAYMENT_DURATION()` + * @dev The tokens are sent to the `PaymentCoordinator` contract + * @dev Strategies must be in ascending order of addresses to check for duplicates + * @dev This function will revert if the `rangePayment` is malformed, + * e.g. if the `strategies` and `weights` arrays are of non-equal lengths + */ + function submitRangePayments(IPaymentCoordinator.RangePayment[] calldata rangePayments) public virtual onlyOwner { + for (uint256 i = 0; i < rangePayments.length; ++i) { + // transfer token to ServiceManager and approve PaymentCoordinator to transfer again + // in payForRange() call + rangePayments[i].token.transferFrom(msg.sender, address(this), rangePayments[i].amount); + rangePayments[i].token.approve(address(_paymentCoordinator), rangePayments[i].amount); + } + + _paymentCoordinator.payForRange(rangePayments); + } + /** * @notice Forwards a call to EigenLayer's AVSDirectory contract to confirm operator registration with the AVS * @param operator The address of the operator to register. diff --git a/src/interfaces/IServiceManager.sol b/src/interfaces/IServiceManager.sol index bda03a88..0357cf62 100644 --- a/src/interfaces/IServiceManager.sol +++ b/src/interfaces/IServiceManager.sol @@ -3,6 +3,7 @@ pragma solidity >=0.5.0; import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; /** * @title Minimal interface for a ServiceManager-type contract that forms the single point for an AVS to push updates to EigenLayer @@ -15,6 +16,21 @@ interface IServiceManager { */ function updateAVSMetadataURI(string memory _metadataURI) external; + /** + * @notice Creates a new range payment on behalf of an AVS, to be split amongst the + * set of stakers delegated to operators who are registered to the `avs`. + * Note that the owner calling this function must have approved the tokens to be transferred to the ServiceManager + * and of course has the required balances. + * @param rangePayments The range payments being created + * @dev Expected to be called by the ServiceManager of the AVS on behalf of which the payment is being made + * @dev The duration of the `rangePayment` cannot exceed `paymentCoordinator.MAX_PAYMENT_DURATION()` + * @dev The tokens are sent to the `PaymentCoordinator` contract + * @dev Strategies must be in ascending order of addresses to check for duplicates + * @dev This function will revert if the `rangePayment` is malformed, + * e.g. if the `strategies` and `weights` arrays are of non-equal lengths + */ + function submitRangePayments(IPaymentCoordinator.RangePayment[] calldata rangePayments) external; + /** * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator registration with the AVS * @param operator The address of the operator to register. diff --git a/test/integration/CoreRegistration.t.sol b/test/integration/CoreRegistration.t.sol index c655b4e2..2c63dee1 100644 --- a/test/integration/CoreRegistration.t.sol +++ b/test/integration/CoreRegistration.t.sol @@ -6,6 +6,8 @@ import { AVSDirectory } from "eigenlayer-contracts/src/contracts/core/AVSDirecto import { IAVSDirectory } from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; import { DelegationManager } from "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; import { IDelegationManager } from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import { PaymentCoordinator } from "eigenlayer-contracts/src/contracts/core/PaymentCoordinator.sol"; +import { IPaymentCoordinator } from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; contract Test_CoreRegistration is MockAVSDeployer { // Contracts @@ -62,12 +64,15 @@ contract Test_CoreRegistration is MockAVSDeployer { ) ); + // Deploy Mock PaymentCoordinator + paymentCoordinatorMock = new PaymentCoordinatorMock(); // Deploy New ServiceManager & RegistryCoordinator implementations serviceManagerImplementation = new ServiceManagerMock( avsDirectory, registryCoordinator, - stakeRegistry + stakeRegistry, + paymentCoordinatorMock ); registryCoordinatorImplementation = new RegistryCoordinatorHarness( diff --git a/test/integration/IntegrationDeployer.t.sol b/test/integration/IntegrationDeployer.t.sol index 7bdd9204..3f99da3c 100644 --- a/test/integration/IntegrationDeployer.t.sol +++ b/test/integration/IntegrationDeployer.t.sol @@ -16,6 +16,7 @@ import "eigenlayer-contracts/src/contracts/core/DelegationManager.sol"; import "eigenlayer-contracts/src/contracts/core/StrategyManager.sol"; import "eigenlayer-contracts/src/contracts/core/Slasher.sol"; import "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; +import "eigenlayer-contracts/src/contracts/core/PaymentCoordinator.sol"; import "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol"; import "eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol"; import "eigenlayer-contracts/src/contracts/pods/EigenPod.sol"; @@ -52,6 +53,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { AVSDirectory public avsDirectory; StrategyManager strategyManager; EigenPodManager eigenPodManager; + PaymentCoordinator paymentCoordinator; PauserRegistry pauserRegistry; Slasher slasher; IBeacon eigenPodBeacon; @@ -87,6 +89,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { uint256 public churnApproverPrivateKey = uint256(keccak256("churnApproverPrivateKey")); address public churnApprover = cheats.addr(churnApproverPrivateKey); address ejector = address(uint160(uint256(keccak256("ejector")))); + address paymentUpdater = address(uint160(uint256(keccak256("paymentUpdater")))); // Constants/Defaults uint64 constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; @@ -94,6 +97,17 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { uint constant MAX_BALANCE = 5e6; uint constant MAX_STRATEGY_COUNT = 32; // From StakeRegistry.MAX_WEIGHING_FUNCTION_LENGTH uint96 constant DEFAULT_STRATEGY_MULTIPLIER = 1e18; + // PaymentCoordinator + uint32 MAX_PAYMENT_DURATION = 70 days; + uint32 MAX_RETROACTIVE_LENGTH = 84 days; + uint32 MAX_FUTURE_LENGTH = 28 days; + uint32 GENESIS_PAYMENT_TIMESTAMP = 1712092632; + /// @notice Delay in timestamp before a posted root can be claimed against + uint32 activationDelay = 7 days; + /// @notice intervals(epochs) are 2 weeks + uint32 calculationIntervalSeconds = 14 days; + /// @notice the commission for all operators across all avss + uint16 globalCommissionBips = 1000; function setUp() public virtual { // Deploy ProxyAdmin @@ -131,6 +145,9 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { avsDirectory = AVSDirectory( address(new TransparentUpgradeableProxy(address(emptyContract), address(proxyAdmin), "")) ); + // paymentCoordinator = PaymentCoordinator( + // address(new TransparentUpgradeableProxy(address(emptyContract), address(proxyAdmin), "")) + // ); // Deploy EigenPod Contracts pod = new EigenPod( @@ -156,6 +173,14 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { ); DelayedWithdrawalRouter delayedWithdrawalRouterImplementation = new DelayedWithdrawalRouter(eigenPodManager); AVSDirectory avsDirectoryImplemntation = new AVSDirectory(delegationManager); + // PaymentCoordinator paymentCoordinatorImplementation = new PaymentCoordinator( + // delegationManager, + // IStrategyManager(address(strategyManager)), + // MAX_PAYMENT_DURATION, + // MAX_RETROACTIVE_LENGTH, + // MAX_FUTURE_LENGTH, + // GENESIS_PAYMENT_TIMESTAMP + // ); // Third, upgrade the proxy contracts to point to the implementations uint256 minWithdrawalDelayBlocks = 7 days / 12 seconds; @@ -233,6 +258,21 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { 0 // initialPausedStatus ) ); + // // PaymentCoordinator + // proxyAdmin.upgradeAndCall( + // TransparentUpgradeableProxy(payable(address(paymentCoordinator))), + // address(paymentCoordinatorImplementation), + // abi.encodeWithSelector( + // PaymentCoordinator.initialize.selector, + // eigenLayerReputedMultisig, // initialOwner + // pauserRegistry, + // 0, // initialPausedStatus + // paymentUpdater, + // activationDelay, + // calculationIntervalSeconds, + // globalCommissionBips + // ) + // ); // Deploy and whitelist strategies baseStrategyImplementation = new StrategyBase(strategyManager); @@ -299,7 +339,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { StakeRegistry stakeRegistryImplementation = new StakeRegistry(IRegistryCoordinator(registryCoordinator), IDelegationManager(delegationManager)); BLSApkRegistry blsApkRegistryImplementation = new BLSApkRegistry(IRegistryCoordinator(registryCoordinator)); IndexRegistry indexRegistryImplementation = new IndexRegistry(IRegistryCoordinator(registryCoordinator)); - ServiceManagerMock serviceManagerImplementation = new ServiceManagerMock(IAVSDirectory(avsDirectory), IRegistryCoordinator(registryCoordinator), stakeRegistry); + ServiceManagerMock serviceManagerImplementation = new ServiceManagerMock( + IAVSDirectory(avsDirectory), + IRegistryCoordinator(registryCoordinator), + stakeRegistry, + paymentCoordinator + ); proxyAdmin.upgrade( TransparentUpgradeableProxy(payable(address(stakeRegistry))), diff --git a/test/mocks/DelegationMock.sol b/test/mocks/DelegationMock.sol index f8a9915e..02cb4634 100644 --- a/test/mocks/DelegationMock.sol +++ b/test/mocks/DelegationMock.sol @@ -65,6 +65,8 @@ contract DelegationMock is IDelegationManager { return returnValue; } + function beaconChainETHStrategy() external pure returns (IStrategy) {} + function earningsReceiver(address operator) external pure returns (address) { return operator; } diff --git a/test/mocks/PaymentCoordinatorMock.sol b/test/mocks/PaymentCoordinatorMock.sol new file mode 100644 index 00000000..679eb73f --- /dev/null +++ b/test/mocks/PaymentCoordinatorMock.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; + +contract PaymentCoordinatorMock is IPaymentCoordinator { + /// @notice The address of the entity that can update the contract with new merkle roots + function paymentUpdater() external view returns (address) {} + + /** + * @notice The interval in seconds at which the calculation for range payment distribution is done. + * @dev Payment durations must be multiples of this interval. + */ + function calculationIntervalSeconds() external view returns (uint32) {} + + /// @notice The maximum amount of time that a range payment can end in the future + function MAX_PAYMENT_DURATION() external view returns (uint32) {} + + /// @notice max amount of time that a payment can start in the past + function MAX_RETROACTIVE_LENGTH() external view returns (uint32) {} + + /// @notice max amount of time that a payment can start in the future + function MAX_FUTURE_LENGTH() external view returns (uint32) {} + + /// @notice absolute min timestamp that a payment can start at + function GENESIS_PAYMENT_TIMESTAMP() external view returns (uint32) {} + + /// @notice Delay in timestamp before a posted root can be claimed against + function activationDelay() external view returns (uint32) {} + + /// @notice Mapping: earner => the address of the entity to which new payments are directed on behalf of the earner + function claimerFor(address earner) external view returns (address) {} + + /// @notice Mapping: claimer => token => total amount claimed + function cumulativeClaimed(address claimer, IERC20 token) external view returns (uint256) {} + + /// @notice the commission for all operators across all avss + function globalOperatorCommissionBips() external view returns (uint16) {} + + /// @notice return the hash of the earner's leaf + function calculateEarnerLeafHash(EarnerTreeMerkleLeaf calldata leaf) external pure returns (bytes32) {} + + /// @notice returns the hash of the earner's token leaf + function calculateTokenLeafHash(TokenTreeMerkleLeaf calldata leaf) external pure returns (bytes32) {} + + /// @notice returns 'true' if the claim would currently pass the check in `processClaims` + /// but will revert if not valid + function checkClaim(PaymentMerkleClaim calldata claim) external view returns (bool) {} + + /// EXTERNAL FUNCTIONS /// + + /** + * @notice Creates a new range payment on behalf of an AVS, to be split amongst the + * set of stakers delegated to operators who are registered to the `avs` + * @param rangePayments The range payments being created + * @dev Expected to be called by the ServiceManager of the AVS on behalf of which the payment is being made + * @dev The duration of the `rangePayment` cannot exceed `MAX_PAYMENT_DURATION` + * @dev The tokens are sent to the `PaymentCoordinator` contract + * @dev Strategies must be in ascending order of addresses to check for duplicates + * @dev This function will revert if the `rangePayment` is malformed, + * e.g. if the `strategies` and `weights` arrays are of non-equal lengths + */ + function payForRange(RangePayment[] calldata rangePayments) external {} + + /** + * @notice similar to `payForRange` except the payment is split amongst *all* stakers + * rather than just those delegated to operators who are registered to a single avs and is + * a permissioned call based on isPayAllForRangeSubmitter mapping. + */ + function payAllForRange(RangePayment[] calldata rangePayment) external {} + + /** + * @notice Claim payments against a given root (read from distributionRoots[claim.rootIndex]). + * Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for, + * they can simply claim against the latest root and the contract will calculate the difference between + * their cumulativeEarnings and cumulativeClaimed. This difference is then transferred to claimerFor[claim.earner] + * @param claim The PaymentMerkleClaim to be processed. + * Contains the root index, earner, payment leaves, and required proofs + * @dev only callable by the valid claimer, that is + * if claimerFor[claim.earner] is address(0) then only the earner can claim, otherwise only + * claimerFor[claim.earner] can claim the payments. + */ + function processClaim(PaymentMerkleClaim calldata claim) external {} + + /** + * @notice Creates a new distribution root. activatedAt is set to block.timestamp + activationDelay + * @param root The merkle root of the distribution + * @param paymentCalculationEndTimestamp The timestamp until which payments have been calculated + * @dev Only callable by the paymentUpdater + */ + function submitRoot( + bytes32 root, + uint32 paymentCalculationEndTimestamp + ) external {} + + /** + * @notice Sets the permissioned `paymentUpdater` address which can post new roots + * @dev Only callable by the contract owner + */ + function setPaymentUpdater(address _paymentUpdater) external {} + + /** + * @notice Sets the delay in timestamp before a posted root can be claimed against + * @param _activationDelay Delay in timestamp before a posted root can be claimed against + * @dev Only callable by the contract owner + */ + function setActivationDelay(uint32 _activationDelay) external {} + + /** + * @notice Sets the global commission for all operators across all avss + * @param _globalCommissionBips The commission for all operators across all avss + * @dev Only callable by the contract owner + */ + function setGlobalOperatorCommission(uint16 _globalCommissionBips) external {} + + /** + * @notice Sets the address of the entity that can claim payments on behalf of the earner (msg.sender) + * @param claimer The address of the entity that can claim payments on behalf of the earner + * @dev Only callable by the `earner` + */ + function setClaimerFor(address claimer) external {} + +} \ No newline at end of file diff --git a/test/mocks/ServiceManagerMock.sol b/test/mocks/ServiceManagerMock.sol index 1a7f0089..72d2370d 100644 --- a/test/mocks/ServiceManagerMock.sol +++ b/test/mocks/ServiceManagerMock.sol @@ -7,8 +7,9 @@ contract ServiceManagerMock is ServiceManagerBase { constructor( IAVSDirectory _avsDirectory, IRegistryCoordinator _registryCoordinator, - IStakeRegistry _stakeRegistry - ) ServiceManagerBase(_avsDirectory, _registryCoordinator, _stakeRegistry) {} + IStakeRegistry _stakeRegistry, + IPaymentCoordinator _paymentCoordinator + ) ServiceManagerBase(_avsDirectory, _registryCoordinator, _stakeRegistry, _paymentCoordinator) {} function initialize(address initialOwner) public virtual initializer { __ServiceManagerBase_init(initialOwner); diff --git a/test/unit/ServiceManagerRouter.t.sol b/test/unit/ServiceManagerRouter.t.sol index 0064fcb1..c9ac9823 100644 --- a/test/unit/ServiceManagerRouter.t.sol +++ b/test/unit/ServiceManagerRouter.t.sol @@ -18,7 +18,8 @@ contract ServiceManagerRouter_UnitTests is MockAVSDeployer { dummyServiceManager = new ServiceManagerMock( avsDirectory, registryCoordinatorImplementation, - stakeRegistryImplementation + stakeRegistryImplementation, + paymentCoordinatorImplementation ); _registerOperatorWithCoordinator(defaultOperator, MAX_QUORUM_BITMAP, defaultPubKey); diff --git a/test/utils/MockAVSDeployer.sol b/test/utils/MockAVSDeployer.sol index a4936c52..9dcd9699 100644 --- a/test/utils/MockAVSDeployer.sol +++ b/test/utils/MockAVSDeployer.sol @@ -32,7 +32,9 @@ import {AVSDirectoryMock} from "../mocks/AVSDirectoryMock.sol"; import {DelegationMock} from "../mocks/DelegationMock.sol"; import {AVSDirectory} from "eigenlayer-contracts/src/contracts/core/AVSDirectory.sol"; import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; - +import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; +import {PaymentCoordinator} from "eigenlayer-contracts/src/contracts/core/PaymentCoordinator.sol"; +import {PaymentCoordinatorMock} from "../mocks/PaymentCoordinatorMock.sol"; import {BLSApkRegistryHarness} from "../harnesses/BLSApkRegistryHarness.sol"; import {EmptyContract} from "eigenlayer-contracts/src/test/mocks/EmptyContract.sol"; @@ -73,6 +75,9 @@ contract MockAVSDeployer is Test { AVSDirectory public avsDirectory; AVSDirectory public avsDirectoryImplementation; AVSDirectoryMock public avsDirectoryMock; + PaymentCoordinator public paymentCoordinator; + PaymentCoordinator public paymentCoordinatorImplementation; + PaymentCoordinatorMock public paymentCoordinatorMock; /// @notice StakeRegistry, Constant used as a divisor in calculating weights. uint256 public constant WEIGHTING_DIVISOR = 1e18; @@ -161,6 +166,7 @@ contract MockAVSDeployer is Test { ) ) ); + paymentCoordinatorMock = new PaymentCoordinatorMock(); strategyManagerMock.setAddresses( delegationMock, @@ -253,7 +259,8 @@ contract MockAVSDeployer is Test { serviceManagerImplementation = new ServiceManagerMock( avsDirectoryMock, registryCoordinator, - stakeRegistry + stakeRegistry, + IPaymentCoordinator(address(paymentCoordinatorMock)) ); proxyAdmin.upgrade( From 612418ed138b4120d2c9c98084f5f706fe528430 Mon Sep 17 00:00:00 2001 From: 8sunyuan Date: Mon, 22 Apr 2024 10:56:57 -0400 Subject: [PATCH 2/4] test: unit tests --- lib/eigenlayer-contracts | 2 +- src/ServiceManagerBase.sol | 4 +- test/events/IServiceManagerBaseEvents.sol | 66 +++ test/unit/ServiceManagerBase.t.sol | 501 ++++++++++++++++++++++ 4 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 test/events/IServiceManagerBaseEvents.sol create mode 100644 test/unit/ServiceManagerBase.t.sol diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index 81c39197..f13e1c21 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit 81c39197839308c580ef2608ff91ab0f017ee889 +Subproject commit f13e1c21c5c4a90f1550fb80c132428bbacd731c diff --git a/src/ServiceManagerBase.sol b/src/ServiceManagerBase.sol index 89d96653..4e396163 100644 --- a/src/ServiceManagerBase.sol +++ b/src/ServiceManagerBase.sol @@ -74,7 +74,9 @@ abstract contract ServiceManagerBase is IServiceManager, OwnableUpgradeable { * @dev This function will revert if the `rangePayment` is malformed, * e.g. if the `strategies` and `weights` arrays are of non-equal lengths */ - function submitRangePayments(IPaymentCoordinator.RangePayment[] calldata rangePayments) public virtual onlyOwner { + function submitRangePayments( + IPaymentCoordinator.RangePayment[] calldata rangePayments + ) public virtual onlyOwner { for (uint256 i = 0; i < rangePayments.length; ++i) { // transfer token to ServiceManager and approve PaymentCoordinator to transfer again // in payForRange() call diff --git a/test/events/IServiceManagerBaseEvents.sol b/test/events/IServiceManagerBaseEvents.sol new file mode 100644 index 00000000..8227e65c --- /dev/null +++ b/test/events/IServiceManagerBaseEvents.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; + +interface IServiceManagerBaseEvents { + /// PaymentCoordinator EVENTS /// + + /// @notice emitted when an AVS creates a valid RangePayment + event RangePaymentCreated( + address indexed avs, + uint256 indexed paymentNonce, + bytes32 indexed rangePaymentHash, + IPaymentCoordinator.RangePayment rangePayment + ); + /// @notice emitted when a valid RangePayment is created for all stakers by a valid submitter + event RangePaymentForAllCreated( + address indexed submitter, + uint256 indexed paymentNonce, + bytes32 indexed rangePaymentHash, + IPaymentCoordinator.RangePayment rangePayment + ); + /// @notice paymentUpdater is responsible for submiting DistributionRoots, only owner can set paymentUpdater + event PaymentUpdaterSet(address indexed oldPaymentUpdater, address indexed newPaymentUpdater); + event PayAllForRangeSubmitterSet( + address indexed payAllForRangeSubmitter, + bool indexed oldValue, + bool indexed newValue + ); + event ActivationDelaySet(uint32 oldActivationDelay, uint32 newActivationDelay); + event CalculationIntervalSecondsSet(uint32 oldCalculationIntervalSeconds, uint32 newCalculationIntervalSeconds); + event GlobalCommissionBipsSet(uint16 oldGlobalCommissionBips, uint16 newGlobalCommissionBips); + event ClaimerForSet(address indexed earner, address indexed oldClaimer, address indexed claimer); + /// @notice rootIndex is the specific array index of the newly created root in the storage array + event DistributionRootSubmitted( + uint32 indexed rootIndex, + bytes32 indexed root, + uint32 paymentCalculationEndTimestamp, + uint32 activatedAt + ); + /// @notice root is one of the submitted distribution roots that was claimed against + event PaymentClaimed( + bytes32 root, + address indexed earner, + address indexed claimer, + IERC20 indexed token, + uint256 claimedAmount + ); + + + + /// TOKEN EVENTS FOR TESTING /// + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/test/unit/ServiceManagerBase.t.sol b/test/unit/ServiceManagerBase.t.sol new file mode 100644 index 00000000..c6d0fb36 --- /dev/null +++ b/test/unit/ServiceManagerBase.t.sol @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import {PaymentCoordinator, IPaymentCoordinator, IERC20} from "eigenlayer-contracts/src/contracts/core/PaymentCoordinator.sol"; +import {StrategyBase} from "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol"; +import {IServiceManagerBaseEvents} from "../events/IServiceManagerBaseEvents.sol"; + +import "../utils/MockAVSDeployer.sol"; + +contract ServiceManagerBase_UnitTests is + MockAVSDeployer, + IServiceManagerBaseEvents +{ + // PaymentCoordinator config + address paymentUpdater = + address(uint160(uint256(keccak256("paymentUpdater")))); + uint32 MAX_PAYMENT_DURATION = 70 days; + uint32 MAX_RETROACTIVE_LENGTH = 84 days; + uint32 MAX_FUTURE_LENGTH = 28 days; + uint32 GENESIS_PAYMENT_TIMESTAMP = 1712092632; + /// @notice Delay in timestamp before a posted root can be claimed against + uint32 activationDelay = 7 days; + /// @notice intervals(epochs) are 2 weeks + uint32 calculationIntervalSeconds = 14 days; + /// @notice the commission for all operators across all avss + uint16 globalCommissionBips = 1000; + + // Testing Config and Mocks + address serviceManagerOwner; + IERC20[] paymentTokens; + uint256 mockTokenInitialSupply = 10e50; + IStrategy strategyMock1; + IStrategy strategyMock2; + IStrategy strategyMock3; + StrategyBase strategyImplementation; + IPaymentCoordinator.StrategyAndMultiplier[] defaultStrategyAndMultipliers; + + // mapping to setting fuzzed inputs + mapping(address => bool) public addressIsExcludedFromFuzzedInputs; + + modifier filterFuzzedAddressInputs(address fuzzedAddress) { + cheats.assume(!addressIsExcludedFromFuzzedInputs[fuzzedAddress]); + _; + } + + function setUp() public virtual { + _deployMockEigenLayerAndAVS(); + // Deploy paymentcoordinator + paymentCoordinatorImplementation = new PaymentCoordinator( + delegationMock, + strategyManagerMock, + MAX_PAYMENT_DURATION, + MAX_RETROACTIVE_LENGTH, + MAX_FUTURE_LENGTH, + GENESIS_PAYMENT_TIMESTAMP + ); + + paymentCoordinator = PaymentCoordinator( + address( + new TransparentUpgradeableProxy( + address(paymentCoordinatorImplementation), + address(proxyAdmin), + abi.encodeWithSelector( + PaymentCoordinator.initialize.selector, + msg.sender, + pauserRegistry, + 0 /*initialPausedStatus*/, + paymentUpdater, + activationDelay, + calculationIntervalSeconds, + globalCommissionBips + ) + ) + ) + ); + // Deploy ServiceManager + serviceManagerImplementation = new ServiceManagerMock( + avsDirectory, + registryCoordinatorImplementation, + stakeRegistryImplementation, + paymentCoordinator + ); + serviceManager = ServiceManagerMock( + address( + new TransparentUpgradeableProxy( + address(serviceManagerImplementation), + address(proxyAdmin), + abi.encodeWithSelector( + ServiceManagerMock.initialize.selector, + msg.sender + ) + ) + ) + ); + serviceManagerOwner = serviceManager.owner(); + + _setUpDefaultStrategiesAndMultipliers(); + + cheats.warp(GENESIS_PAYMENT_TIMESTAMP + 2 weeks); + + addressIsExcludedFromFuzzedInputs[address(pauserRegistry)] = true; + addressIsExcludedFromFuzzedInputs[address(proxyAdmin)] = true; + } + + /// @notice deploy token to owner and approve ServiceManager. Used for deploying payment tokens + function _deployMockPaymentTokens( + address owner, + uint256 numTokens + ) internal virtual { + cheats.startPrank(owner); + for (uint256 i = 0; i < numTokens; ++i) { + IERC20 token = new ERC20PresetFixedSupply( + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + owner + ); + paymentTokens.push(token); + token.approve(address(serviceManager), mockTokenInitialSupply); + } + cheats.stopPrank(); + } + + function _getBalanceForTokens( + IERC20[] memory tokens, + address holder + ) internal view returns (uint256[] memory) { + uint256[] memory balances = new uint256[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + balances[i] = tokens[i].balanceOf(holder); + } + return balances; + } + + function _setUpDefaultStrategiesAndMultipliers() internal virtual { + // Deploy Mock Strategies + IERC20 token1 = new ERC20PresetFixedSupply( + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + address(this) + ); + IERC20 token2 = new ERC20PresetFixedSupply( + "jeo boden", + "MOCK2", + mockTokenInitialSupply, + address(this) + ); + IERC20 token3 = new ERC20PresetFixedSupply( + "pepe wif avs", + "MOCK3", + mockTokenInitialSupply, + address(this) + ); + strategyImplementation = new StrategyBase(strategyManagerMock); + strategyMock1 = StrategyBase( + address( + new TransparentUpgradeableProxy( + address(strategyImplementation), + address(proxyAdmin), + abi.encodeWithSelector( + StrategyBase.initialize.selector, + token1, + pauserRegistry + ) + ) + ) + ); + strategyMock2 = StrategyBase( + address( + new TransparentUpgradeableProxy( + address(strategyImplementation), + address(proxyAdmin), + abi.encodeWithSelector( + StrategyBase.initialize.selector, + token2, + pauserRegistry + ) + ) + ) + ); + strategyMock3 = StrategyBase( + address( + new TransparentUpgradeableProxy( + address(strategyImplementation), + address(proxyAdmin), + abi.encodeWithSelector( + StrategyBase.initialize.selector, + token3, + pauserRegistry + ) + ) + ) + ); + IStrategy[] memory strategies = new IStrategy[](3); + strategies[0] = strategyMock1; + strategies[1] = strategyMock2; + strategies[2] = strategyMock3; + strategies = _sortArrayAsc(strategies); + + strategyManagerMock.setStrategyWhitelist(strategies[0], true); + strategyManagerMock.setStrategyWhitelist(strategies[1], true); + strategyManagerMock.setStrategyWhitelist(strategies[2], true); + + defaultStrategyAndMultipliers.push( + IPaymentCoordinator.StrategyAndMultiplier( + IStrategy(address(strategies[0])), + 1e18 + ) + ); + defaultStrategyAndMultipliers.push( + IPaymentCoordinator.StrategyAndMultiplier( + IStrategy(address(strategies[1])), + 2e18 + ) + ); + defaultStrategyAndMultipliers.push( + IPaymentCoordinator.StrategyAndMultiplier( + IStrategy(address(strategies[2])), + 3e18 + ) + ); + } + + /// @dev Sort to ensure that the array is in ascending order for strategies + function _sortArrayAsc( + IStrategy[] memory arr + ) internal pure returns (IStrategy[] memory) { + uint256 l = arr.length; + for (uint256 i = 0; i < l; i++) { + for (uint256 j = i + 1; j < l; j++) { + if (address(arr[i]) > address(arr[j])) { + IStrategy temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + } + } + return arr; + } + + function _maxTimestamp( + uint32 timestamp1, + uint32 timestamp2 + ) internal pure returns (uint32) { + return timestamp1 > timestamp2 ? timestamp1 : timestamp2; + } + + function testFuzz_submitPayments_Revert_WhenNotOwner( + address caller + ) public filterFuzzedAddressInputs(caller) { + IPaymentCoordinator.RangePayment[] memory rangePayments; + + cheats.prank(caller); + cheats.expectRevert("Ownable: caller is not the owner"); + serviceManager.submitRangePayments(rangePayments); + } + + function test_submitPayments_Revert_WhenERC20NotApproved() public { + IERC20 token = new ERC20PresetFixedSupply( + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + serviceManagerOwner + ); + + IPaymentCoordinator.RangePayment[] + memory rangePayments = new IPaymentCoordinator.RangePayment[](1); + rangePayments[0] = IPaymentCoordinator.RangePayment({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: token, + amount: 100, + startTimestamp: uint32(block.timestamp), + duration: uint32(1 weeks) + }); + + cheats.prank(serviceManagerOwner); + cheats.expectRevert("ERC20: insufficient allowance"); + serviceManager.submitRangePayments(rangePayments); + } + + function test_submitPayments_SingleRangePayment( + uint256 startTimestamp, + uint256 duration, + uint256 amount + ) public { + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 paymentToken = new ERC20PresetFixedSupply( + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + serviceManagerOwner + ); + amount = bound(amount, 1, mockTokenInitialSupply); + duration = bound(duration, 0, MAX_PAYMENT_DURATION); + duration = duration - (duration % calculationIntervalSeconds); + startTimestamp = bound( + startTimestamp, + uint256( + _maxTimestamp( + GENESIS_PAYMENT_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + + calculationIntervalSeconds - + 1, + block.timestamp + uint256(MAX_FUTURE_LENGTH) + ); + startTimestamp = + startTimestamp - + (startTimestamp % calculationIntervalSeconds); + + // 2. Create range payment input param + IPaymentCoordinator.RangePayment[] + memory rangePayments = new IPaymentCoordinator.RangePayment[](1); + rangePayments[0] = IPaymentCoordinator.RangePayment({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: paymentToken, + amount: amount, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration) + }); + + // 3. Approve serviceManager for ERC20 + cheats.startPrank(serviceManagerOwner); + paymentToken.approve(address(serviceManager), amount); + + // 4. call submitRangePayments() with expected event emitted + uint256 serviceManagerOwnerBalanceBefore = paymentToken.balanceOf( + address(serviceManagerOwner) + ); + uint256 paymentCoordinatorBalanceBefore = paymentToken.balanceOf( + address(paymentCoordinator) + ); + + paymentToken.approve(address(paymentCoordinator), amount); + uint256 currPaymentNonce = paymentCoordinator.paymentNonce( + address(serviceManager) + ); + bytes32 rangePaymentHash = keccak256( + abi.encode( + address(serviceManager), + currPaymentNonce, + rangePayments[0] + ) + ); + + cheats.expectEmit(true, true, true, true, address(paymentCoordinator)); + emit RangePaymentCreated( + address(serviceManager), + currPaymentNonce, + rangePaymentHash, + rangePayments[0] + ); + serviceManager.submitRangePayments(rangePayments); + cheats.stopPrank(); + + assertTrue( + paymentCoordinator.isRangePaymentHash( + address(serviceManager), + rangePaymentHash + ), + "Range payment hash not submitted" + ); + assertEq( + currPaymentNonce + 1, + paymentCoordinator.paymentNonce(address(serviceManager)), + "Payment nonce not incremented" + ); + assertEq( + serviceManagerOwnerBalanceBefore - amount, + paymentToken.balanceOf(serviceManagerOwner), + "serviceManagerOwner balance not decremented by amount of range payment" + ); + assertEq( + paymentCoordinatorBalanceBefore + amount, + paymentToken.balanceOf(address(paymentCoordinator)), + "PaymentCoordinator balance not incremented by amount of range payment" + ); + } + + function test_submitPayments_MultipleRangePayments( + uint256 startTimestamp, + uint256 duration, + uint256 amount, + uint256 numPayments + ) public { + cheats.assume(2 <= numPayments && numPayments <= 10); + cheats.prank(paymentCoordinator.owner()); + + IPaymentCoordinator.RangePayment[] + memory rangePayments = new IPaymentCoordinator.RangePayment[]( + numPayments + ); + bytes32[] memory rangePaymentHashes = new bytes32[](numPayments); + uint256 startPaymentNonce = paymentCoordinator.paymentNonce( + address(serviceManager) + ); + _deployMockPaymentTokens(serviceManagerOwner, numPayments); + + uint256[] memory avsBalancesBefore = _getBalanceForTokens( + paymentTokens, + serviceManagerOwner + ); + uint256[] + memory paymentCoordinatorBalancesBefore = _getBalanceForTokens( + paymentTokens, + address(paymentCoordinator) + ); + uint256[] memory amounts = new uint256[](numPayments); + + // Create multiple range payments and their expected event + for (uint256 i = 0; i < numPayments; ++i) { + // 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each + amount = bound(amount + i, 1, mockTokenInitialSupply); + amounts[i] = amount; + duration = bound(duration + i, 0, MAX_PAYMENT_DURATION); + duration = duration - (duration % calculationIntervalSeconds); + startTimestamp = bound( + startTimestamp + i, + uint256( + _maxTimestamp( + GENESIS_PAYMENT_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + + calculationIntervalSeconds - + 1, + block.timestamp + uint256(MAX_FUTURE_LENGTH) + ); + startTimestamp = + startTimestamp - + (startTimestamp % calculationIntervalSeconds); + + // 2. Create range payment input param + IPaymentCoordinator.RangePayment + memory rangePayment = IPaymentCoordinator.RangePayment({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: paymentTokens[i], + amount: amounts[i], + startTimestamp: uint32(startTimestamp), + duration: uint32(duration) + }); + rangePayments[i] = rangePayment; + + // 3. expected event emitted for this rangePayment + rangePaymentHashes[i] = keccak256( + abi.encode( + address(serviceManager), + startPaymentNonce + i, + rangePayments[i] + ) + ); + cheats.expectEmit( + true, + true, + true, + true, + address(paymentCoordinator) + ); + emit RangePaymentCreated( + address(serviceManager), + startPaymentNonce + i, + rangePaymentHashes[i], + rangePayments[i] + ); + } + + // 4. call submitRangePayments() + cheats.prank(serviceManagerOwner); + serviceManager.submitRangePayments(rangePayments); + + // 5. Check for paymentNonce() and rangePaymentHashes being set + assertEq( + startPaymentNonce + numPayments, + paymentCoordinator.paymentNonce(address(serviceManager)), + "Payment nonce not incremented properly" + ); + + for (uint256 i = 0; i < numPayments; ++i) { + assertTrue( + paymentCoordinator.isRangePaymentHash( + address(serviceManager), + rangePaymentHashes[i] + ), + "Range payment hash not submitted" + ); + assertEq( + avsBalancesBefore[i] - amounts[i], + paymentTokens[i].balanceOf(serviceManagerOwner), + "AVS balance not decremented by amount of range payment" + ); + assertEq( + paymentCoordinatorBalancesBefore[i] + amounts[i], + paymentTokens[i].balanceOf(address(paymentCoordinator)), + "PaymentCoordinator balance not incremented by amount of range payment" + ); + } + } +} From 60abef3679de6d7a602240712990eb9178ed6322 Mon Sep 17 00:00:00 2001 From: 8sunyuan Date: Mon, 22 Apr 2024 11:18:47 -0400 Subject: [PATCH 3/4] feat: refactor serviceManager interfaces --- src/ServiceManagerBase.sol | 1 + src/ServiceManagerRouter.sol | 6 +-- src/interfaces/IServiceManager.sol | 47 +-------------------- src/interfaces/IServiceManagerUI.sol | 62 ++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 48 deletions(-) create mode 100644 src/interfaces/IServiceManagerUI.sol diff --git a/src/ServiceManagerBase.sol b/src/ServiceManagerBase.sol index 4e396163..5a9cef2a 100644 --- a/src/ServiceManagerBase.sol +++ b/src/ServiceManagerBase.sol @@ -9,6 +9,7 @@ import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSD import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; import {IServiceManager} from "./interfaces/IServiceManager.sol"; +// import {IServiceManagerUI} from "./interfaces/IServiceManagerUI.sol"; import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; diff --git a/src/ServiceManagerRouter.sol b/src/ServiceManagerRouter.sol index 477ed2db..e2259cfb 100644 --- a/src/ServiceManagerRouter.sol +++ b/src/ServiceManagerRouter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.12; -import {IServiceManager} from "./interfaces/IServiceManager.sol"; +import {IServiceManagerUI} from "./interfaces/IServiceManagerUI.sol"; /** * @title Contract that proxies calls to a ServiceManager contract. @@ -20,7 +20,7 @@ contract ServiceManagerRouter { */ function getRestakeableStrategies(address serviceManager) external view returns (address[] memory) { bytes memory data = abi.encodeWithSelector( - IServiceManager.getRestakeableStrategies.selector + IServiceManagerUI.getRestakeableStrategies.selector ); return _makeCall(serviceManager, data); } @@ -32,7 +32,7 @@ contract ServiceManagerRouter { */ function getOperatorRestakedStrategies(address serviceManager, address operator) external view returns (address[] memory) { bytes memory data = abi.encodeWithSelector( - IServiceManager.getOperatorRestakedStrategies.selector, + IServiceManagerUI.getOperatorRestakedStrategies.selector, operator ); return _makeCall(serviceManager, data); diff --git a/src/interfaces/IServiceManager.sol b/src/interfaces/IServiceManager.sol index 0357cf62..82764466 100644 --- a/src/interfaces/IServiceManager.sol +++ b/src/interfaces/IServiceManager.sol @@ -1,21 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity >=0.5.0; -import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; -import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; +import {IServiceManagerUI} from "./IServiceManagerUI.sol"; /** * @title Minimal interface for a ServiceManager-type contract that forms the single point for an AVS to push updates to EigenLayer * @author Layr Labs, Inc. */ -interface IServiceManager { - /** - * @notice Updates the metadata URI for the AVS - * @param _metadataURI is the metadata URI for the AVS - */ - function updateAVSMetadataURI(string memory _metadataURI) external; - +interface IServiceManager is IServiceManagerUI { /** * @notice Creates a new range payment on behalf of an AVS, to be split amongst the * set of stakers delegated to operators who are registered to the `avs`. @@ -30,40 +23,4 @@ interface IServiceManager { * e.g. if the `strategies` and `weights` arrays are of non-equal lengths */ function submitRangePayments(IPaymentCoordinator.RangePayment[] calldata rangePayments) external; - - /** - * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator registration with the AVS - * @param operator The address of the operator to register. - * @param operatorSignature The signature, salt, and expiry of the operator's signature. - */ - function registerOperatorToAVS( - address operator, - ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature - ) external; - - /** - * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator deregistration from the AVS - * @param operator The address of the operator to deregister. - */ - function deregisterOperatorFromAVS(address operator) external; - - /** - * @notice Returns the list of strategies that the operator has potentially restaked on the AVS - * @param operator The address of the operator to get restaked strategies for - * @dev This function is intended to be called off-chain - * @dev No guarantee is made on whether the operator has shares for a strategy in a quorum or uniqueness - * of each element in the returned array. The off-chain service should do that validation separately - */ - function getOperatorRestakedStrategies(address operator) external view returns (address[] memory); - - /** - * @notice Returns the list of strategies that the AVS supports for restaking - * @dev This function is intended to be called off-chain - * @dev No guarantee is made on uniqueness of each element in the returned array. - * The off-chain service should do that validation separately - */ - function getRestakeableStrategies() external view returns (address[] memory); - - /// @notice Returns the EigenLayer AVSDirectory contract. - function avsDirectory() external view returns (address); } diff --git a/src/interfaces/IServiceManagerUI.sol b/src/interfaces/IServiceManagerUI.sol new file mode 100644 index 00000000..9050988a --- /dev/null +++ b/src/interfaces/IServiceManagerUI.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; +import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; + +/** + * @title Minimal interface for a ServiceManager-type contract that AVS ServiceManager contracts must implement + * to index their data for the V1 AVS Marketplace + * @author Layr Labs, Inc. + */ +interface IServiceManagerUI { + /** + * Metadata should follow the format outlined by this example. + { + "name": "EigenLabs AVS 1", + "website": "https://www.eigenlayer.xyz/", + "description": "This is my 1st AVS", + "logo": "https://holesky-operator-metadata.s3.amazonaws.com/eigenlayer.png", + "twitter": "https://twitter.com/eigenlayer" + } + * @notice Updates the metadata URI for the AVS + * @param _metadataURI is the metadata URI for the AVS + */ + function updateAVSMetadataURI(string memory _metadataURI) external; + + /** + * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator registration with the AVS + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external; + + /** + * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator deregistration from the AVS + * @param operator The address of the operator to deregister. + */ + function deregisterOperatorFromAVS(address operator) external; + + /** + * @notice Returns the list of strategies that the operator has potentially restaked on the AVS + * @param operator The address of the operator to get restaked strategies for + * @dev This function is intended to be called off-chain + * @dev No guarantee is made on whether the operator has shares for a strategy in a quorum or uniqueness + * of each element in the returned array. The off-chain service should do that validation separately + */ + function getOperatorRestakedStrategies(address operator) external view returns (address[] memory); + + /** + * @notice Returns the list of strategies that the AVS supports for restaking + * @dev This function is intended to be called off-chain + * @dev No guarantee is made on uniqueness of each element in the returned array. + * The off-chain service should do that validation separately + */ + function getRestakeableStrategies() external view returns (address[] memory); + + /// @notice Returns the EigenLayer AVSDirectory contract. + function avsDirectory() external view returns (address); +} From cbe2107dd590fd49c5aa97274ca7beecb9b19644 Mon Sep 17 00:00:00 2001 From: 8sunyuan Date: Tue, 23 Apr 2024 13:39:53 -0400 Subject: [PATCH 4/4] chore: requested changes --- src/ServiceManagerBase.sol | 16 +++++++--------- src/interfaces/IServiceManager.sol | 2 +- src/interfaces/IServiceManagerUI.sol | 2 +- test/integration/CoreRegistration.t.sol | 4 ++-- test/integration/IntegrationDeployer.t.sol | 4 ++-- test/mocks/ServiceManagerMock.sol | 6 +++--- test/unit/ServiceManagerBase.t.sol | 16 ++++++++-------- test/unit/ServiceManagerRouter.t.sol | 4 ++-- test/utils/MockAVSDeployer.sol | 4 ++-- 9 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/ServiceManagerBase.sol b/src/ServiceManagerBase.sol index 5a9cef2a..64b5f056 100644 --- a/src/ServiceManagerBase.sol +++ b/src/ServiceManagerBase.sol @@ -2,16 +2,14 @@ pragma solidity ^0.8.12; import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; - -import {BitmapUtils} from "./libraries/BitmapUtils.sol"; import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; import {IPaymentCoordinator} from "eigenlayer-contracts/src/contracts/interfaces/IPaymentCoordinator.sol"; import {IServiceManager} from "./interfaces/IServiceManager.sol"; -// import {IServiceManagerUI} from "./interfaces/IServiceManagerUI.sol"; import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol"; import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; +import {BitmapUtils} from "./libraries/BitmapUtils.sol"; /** * @title Minimal implementation of a ServiceManager-type contract. @@ -21,10 +19,10 @@ import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol"; abstract contract ServiceManagerBase is IServiceManager, OwnableUpgradeable { using BitmapUtils for *; - IRegistryCoordinator internal immutable _registryCoordinator; - IStakeRegistry internal immutable _stakeRegistry; IAVSDirectory internal immutable _avsDirectory; IPaymentCoordinator internal immutable _paymentCoordinator; + IRegistryCoordinator internal immutable _registryCoordinator; + IStakeRegistry internal immutable _stakeRegistry; /// @notice when applied to a function, only allows the RegistryCoordinator to call it modifier onlyRegistryCoordinator() { @@ -38,14 +36,14 @@ abstract contract ServiceManagerBase is IServiceManager, OwnableUpgradeable { /// @notice Sets the (immutable) `_registryCoordinator` address constructor( IAVSDirectory __avsDirectory, + IPaymentCoordinator ___paymentCoordinator, IRegistryCoordinator __registryCoordinator, - IStakeRegistry __stakeRegistry, - IPaymentCoordinator ___paymentCoordinator + IStakeRegistry __stakeRegistry ) { _avsDirectory = __avsDirectory; + _paymentCoordinator = ___paymentCoordinator; _registryCoordinator = __registryCoordinator; _stakeRegistry = __stakeRegistry; - _paymentCoordinator = ___paymentCoordinator; _disableInitializers(); } @@ -75,7 +73,7 @@ abstract contract ServiceManagerBase is IServiceManager, OwnableUpgradeable { * @dev This function will revert if the `rangePayment` is malformed, * e.g. if the `strategies` and `weights` arrays are of non-equal lengths */ - function submitRangePayments( + function payForRange( IPaymentCoordinator.RangePayment[] calldata rangePayments ) public virtual onlyOwner { for (uint256 i = 0; i < rangePayments.length; ++i) { diff --git a/src/interfaces/IServiceManager.sol b/src/interfaces/IServiceManager.sol index 82764466..e5229ed3 100644 --- a/src/interfaces/IServiceManager.sol +++ b/src/interfaces/IServiceManager.sol @@ -22,5 +22,5 @@ interface IServiceManager is IServiceManagerUI { * @dev This function will revert if the `rangePayment` is malformed, * e.g. if the `strategies` and `weights` arrays are of non-equal lengths */ - function submitRangePayments(IPaymentCoordinator.RangePayment[] calldata rangePayments) external; + function payForRange(IPaymentCoordinator.RangePayment[] calldata rangePayments) external; } diff --git a/src/interfaces/IServiceManagerUI.sol b/src/interfaces/IServiceManagerUI.sol index 9050988a..9f06766c 100644 --- a/src/interfaces/IServiceManagerUI.sol +++ b/src/interfaces/IServiceManagerUI.sol @@ -6,7 +6,7 @@ import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/ /** * @title Minimal interface for a ServiceManager-type contract that AVS ServiceManager contracts must implement - * to index their data for the V1 AVS Marketplace + * for eigenlabs to be able to index their data on the AVS marketplace frontend. * @author Layr Labs, Inc. */ interface IServiceManagerUI { diff --git a/test/integration/CoreRegistration.t.sol b/test/integration/CoreRegistration.t.sol index 2c63dee1..3e6c1dba 100644 --- a/test/integration/CoreRegistration.t.sol +++ b/test/integration/CoreRegistration.t.sol @@ -70,9 +70,9 @@ contract Test_CoreRegistration is MockAVSDeployer { // Deploy New ServiceManager & RegistryCoordinator implementations serviceManagerImplementation = new ServiceManagerMock( avsDirectory, + paymentCoordinatorMock, registryCoordinator, - stakeRegistry, - paymentCoordinatorMock + stakeRegistry ); registryCoordinatorImplementation = new RegistryCoordinatorHarness( diff --git a/test/integration/IntegrationDeployer.t.sol b/test/integration/IntegrationDeployer.t.sol index 3f99da3c..5cb2ffee 100644 --- a/test/integration/IntegrationDeployer.t.sol +++ b/test/integration/IntegrationDeployer.t.sol @@ -341,9 +341,9 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { IndexRegistry indexRegistryImplementation = new IndexRegistry(IRegistryCoordinator(registryCoordinator)); ServiceManagerMock serviceManagerImplementation = new ServiceManagerMock( IAVSDirectory(avsDirectory), + paymentCoordinator, IRegistryCoordinator(registryCoordinator), - stakeRegistry, - paymentCoordinator + stakeRegistry ); proxyAdmin.upgrade( diff --git a/test/mocks/ServiceManagerMock.sol b/test/mocks/ServiceManagerMock.sol index 72d2370d..9df0caa5 100644 --- a/test/mocks/ServiceManagerMock.sol +++ b/test/mocks/ServiceManagerMock.sol @@ -6,10 +6,10 @@ import "../../src/ServiceManagerBase.sol"; contract ServiceManagerMock is ServiceManagerBase { constructor( IAVSDirectory _avsDirectory, + IPaymentCoordinator _paymentCoordinator, IRegistryCoordinator _registryCoordinator, - IStakeRegistry _stakeRegistry, - IPaymentCoordinator _paymentCoordinator - ) ServiceManagerBase(_avsDirectory, _registryCoordinator, _stakeRegistry, _paymentCoordinator) {} + IStakeRegistry _stakeRegistry + ) ServiceManagerBase(_avsDirectory, _paymentCoordinator, _registryCoordinator, _stakeRegistry) {} function initialize(address initialOwner) public virtual initializer { __ServiceManagerBase_init(initialOwner); diff --git a/test/unit/ServiceManagerBase.t.sol b/test/unit/ServiceManagerBase.t.sol index c6d0fb36..da715940 100644 --- a/test/unit/ServiceManagerBase.t.sol +++ b/test/unit/ServiceManagerBase.t.sol @@ -77,9 +77,9 @@ contract ServiceManagerBase_UnitTests is // Deploy ServiceManager serviceManagerImplementation = new ServiceManagerMock( avsDirectory, + paymentCoordinator, registryCoordinatorImplementation, - stakeRegistryImplementation, - paymentCoordinator + stakeRegistryImplementation ); serviceManager = ServiceManagerMock( address( @@ -254,7 +254,7 @@ contract ServiceManagerBase_UnitTests is cheats.prank(caller); cheats.expectRevert("Ownable: caller is not the owner"); - serviceManager.submitRangePayments(rangePayments); + serviceManager.payForRange(rangePayments); } function test_submitPayments_Revert_WhenERC20NotApproved() public { @@ -277,7 +277,7 @@ contract ServiceManagerBase_UnitTests is cheats.prank(serviceManagerOwner); cheats.expectRevert("ERC20: insufficient allowance"); - serviceManager.submitRangePayments(rangePayments); + serviceManager.payForRange(rangePayments); } function test_submitPayments_SingleRangePayment( @@ -326,7 +326,7 @@ contract ServiceManagerBase_UnitTests is cheats.startPrank(serviceManagerOwner); paymentToken.approve(address(serviceManager), amount); - // 4. call submitRangePayments() with expected event emitted + // 4. call payForRange() with expected event emitted uint256 serviceManagerOwnerBalanceBefore = paymentToken.balanceOf( address(serviceManagerOwner) ); @@ -353,7 +353,7 @@ contract ServiceManagerBase_UnitTests is rangePaymentHash, rangePayments[0] ); - serviceManager.submitRangePayments(rangePayments); + serviceManager.payForRange(rangePayments); cheats.stopPrank(); assertTrue( @@ -467,9 +467,9 @@ contract ServiceManagerBase_UnitTests is ); } - // 4. call submitRangePayments() + // 4. call payForRange() cheats.prank(serviceManagerOwner); - serviceManager.submitRangePayments(rangePayments); + serviceManager.payForRange(rangePayments); // 5. Check for paymentNonce() and rangePaymentHashes being set assertEq( diff --git a/test/unit/ServiceManagerRouter.t.sol b/test/unit/ServiceManagerRouter.t.sol index c9ac9823..a5649b3c 100644 --- a/test/unit/ServiceManagerRouter.t.sol +++ b/test/unit/ServiceManagerRouter.t.sol @@ -17,9 +17,9 @@ contract ServiceManagerRouter_UnitTests is MockAVSDeployer { // Deploy dummy serviceManager dummyServiceManager = new ServiceManagerMock( avsDirectory, + paymentCoordinatorImplementation, registryCoordinatorImplementation, - stakeRegistryImplementation, - paymentCoordinatorImplementation + stakeRegistryImplementation ); _registerOperatorWithCoordinator(defaultOperator, MAX_QUORUM_BITMAP, defaultPubKey); diff --git a/test/utils/MockAVSDeployer.sol b/test/utils/MockAVSDeployer.sol index 9dcd9699..b0421caa 100644 --- a/test/utils/MockAVSDeployer.sol +++ b/test/utils/MockAVSDeployer.sol @@ -258,9 +258,9 @@ contract MockAVSDeployer is Test { serviceManagerImplementation = new ServiceManagerMock( avsDirectoryMock, + IPaymentCoordinator(address(paymentCoordinatorMock)), registryCoordinator, - stakeRegistry, - IPaymentCoordinator(address(paymentCoordinatorMock)) + stakeRegistry ); proxyAdmin.upgrade(