diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index 426f461c..ac57bc1b 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit 426f461c59b4f0e16f8becdffd747075edcaded8 +Subproject commit ac57bc1b28c83d9d7143c0da19167c148c3596a3 diff --git a/src/ServiceManagerBase.sol b/src/ServiceManagerBase.sol index 6680edd9..dd9c17b3 100644 --- a/src/ServiceManagerBase.sol +++ b/src/ServiceManagerBase.sol @@ -72,7 +72,9 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { * @param _metadataURI is the metadata URI for the AVS * @dev only callable by the owner */ - function updateAVSMetadataURI(string memory _metadataURI) public virtual onlyOwner { + function updateAVSMetadataURI( + string memory _metadataURI + ) public virtual onlyOwner { _avsDirectory.updateAVSMetadataURI(_metadataURI); } @@ -87,25 +89,95 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { * @dev This function will revert if the `rewardsSubmission` is malformed, * e.g. if the `strategies` and `weights` arrays are of non-equal lengths */ - function createAVSRewardsSubmission(IRewardsCoordinator.RewardsSubmission[] calldata rewardsSubmissions) - public - virtual - onlyRewardsInitiator - { + function createAVSRewardsSubmission( + IRewardsCoordinator.RewardsSubmission[] calldata rewardsSubmissions + ) public virtual onlyRewardsInitiator { for (uint256 i = 0; i < rewardsSubmissions.length; ++i) { // transfer token to ServiceManager and approve RewardsCoordinator to transfer again // in createAVSRewardsSubmission() call - rewardsSubmissions[i].token.transferFrom(msg.sender, address(this), rewardsSubmissions[i].amount); - uint256 allowance = - rewardsSubmissions[i].token.allowance(address(this), address(_rewardsCoordinator)); + rewardsSubmissions[i].token.transferFrom( + msg.sender, + address(this), + rewardsSubmissions[i].amount + ); + uint256 allowance = rewardsSubmissions[i].token.allowance( + address(this), + address(_rewardsCoordinator) + ); rewardsSubmissions[i].token.approve( - address(_rewardsCoordinator), rewardsSubmissions[i].amount + allowance + address(_rewardsCoordinator), + rewardsSubmissions[i].amount + allowance ); } _rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions); } + /** + * @notice Creates a new operator-directed rewards submission, to be split amongst the operators and + * set of stakers delegated to operators who are registered to this `avs`. + * @param operatorDirectedRewardsSubmissions The operator-directed rewards submissions being created. + * @dev Only callabe by the permissioned rewardsInitiator address + * @dev The duration of the `rewardsSubmission` cannot exceed `MAX_REWARDS_DURATION` + * @dev The tokens are sent to the `RewardsCoordinator` contract + * @dev This contract needs a token approval of sum of all `operatorRewards` in the `operatorDirectedRewardsSubmissions`, before calling this function. + * @dev Strategies must be in ascending order of addresses to check for duplicates + * @dev Operators must be in ascending order of addresses to check for duplicates. + * @dev This function will revert if the `operatorDirectedRewardsSubmissions` is malformed. + */ + function createOperatorDirectedAVSRewardsSubmission( + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + calldata operatorDirectedRewardsSubmissions + ) public virtual onlyRewardsInitiator { + for ( + uint256 i = 0; + i < operatorDirectedRewardsSubmissions.length; + ++i + ) { + // Calculate total amount of token to transfer + uint256 totalAmount = 0; + for ( + uint256 j = 0; + j < + operatorDirectedRewardsSubmissions[i].operatorRewards.length; + ++j + ) { + totalAmount += operatorDirectedRewardsSubmissions[i] + .operatorRewards[j] + .amount; + } + + // Transfer token to ServiceManager and approve RewardsCoordinator to transfer again + // in createAVSPerformanceRewardsSubmission() call + operatorDirectedRewardsSubmissions[i].token.transferFrom( + msg.sender, + address(this), + totalAmount + ); + uint256 allowance = operatorDirectedRewardsSubmissions[i] + .token + .allowance(address(this), address(_rewardsCoordinator)); + operatorDirectedRewardsSubmissions[i].token.approve( + address(_rewardsCoordinator), + totalAmount + allowance + ); + } + + _rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission( + address(this), + operatorDirectedRewardsSubmissions + ); + } + + /** + * @notice Forwards a call to Eigenlayer's RewardsCoordinator contract to set the address of the entity that can call `processClaim` on behalf of this contract. + * @param claimer The address of the entity that can call `processClaim` on behalf of the earner + * @dev Only callabe by the owner. + */ + function setClaimerFor(address claimer) public virtual onlyOwner { + _rewardsCoordinator.setClaimerFor(claimer); + } + /** * @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. @@ -122,7 +194,9 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { * @notice Forwards a call to EigenLayer's AVSDirectory contract to confirm operator deregistration from the AVS * @param operator The address of the operator to deregister. */ - function deregisterOperatorFromAVS(address operator) public virtual onlyRegistryCoordinator { + function deregisterOperatorFromAVS( + address operator + ) public virtual onlyRegistryCoordinator { _avsDirectory.deregisterOperatorFromAVS(operator); } @@ -131,7 +205,9 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { * @param newRewardsInitiator The new rewards initiator address * @dev only callable by the owner */ - function setRewardsInitiator(address newRewardsInitiator) external onlyOwner { + function setRewardsInitiator( + address newRewardsInitiator + ) external onlyOwner { _setRewardsInitiator(newRewardsInitiator); } @@ -146,7 +222,12 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { * @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 virtual view returns (address[] memory) { + function getRestakeableStrategies() + external + view + virtual + returns (address[] memory) + { uint256 quorumCount = _registryCoordinator.quorumCount(); if (quorumCount == 0) { @@ -161,10 +242,13 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { address[] memory restakedStrategies = new address[](strategyCount); uint256 index = 0; for (uint256 i = 0; i < _registryCoordinator.quorumCount(); i++) { - uint256 strategyParamsLength = _stakeRegistry.strategyParamsLength(uint8(i)); + uint256 strategyParamsLength = _stakeRegistry.strategyParamsLength( + uint8(i) + ); for (uint256 j = 0; j < strategyParamsLength; j++) { - restakedStrategies[index] = - address(_stakeRegistry.strategyParamsByIndex(uint8(i), j).strategy); + restakedStrategies[index] = address( + _stakeRegistry.strategyParamsByIndex(uint8(i), j).strategy + ); index++; } } @@ -178,24 +262,27 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { * @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 - virtual - view - returns (address[] memory) - { + function getOperatorRestakedStrategies( + address operator + ) external view virtual returns (address[] memory) { bytes32 operatorId = _registryCoordinator.getOperatorId(operator); - uint192 operatorBitmap = _registryCoordinator.getCurrentQuorumBitmap(operatorId); + uint192 operatorBitmap = _registryCoordinator.getCurrentQuorumBitmap( + operatorId + ); if (operatorBitmap == 0 || _registryCoordinator.quorumCount() == 0) { return new address[](0); } // Get number of strategies for each quorum in operator bitmap - bytes memory operatorRestakedQuorums = BitmapUtils.bitmapToBytesArray(operatorBitmap); + bytes memory operatorRestakedQuorums = BitmapUtils.bitmapToBytesArray( + operatorBitmap + ); uint256 strategyCount; for (uint256 i = 0; i < operatorRestakedQuorums.length; i++) { - strategyCount += _stakeRegistry.strategyParamsLength(uint8(operatorRestakedQuorums[i])); + strategyCount += _stakeRegistry.strategyParamsLength( + uint8(operatorRestakedQuorums[i]) + ); } // Get strategies for each quorum in operator bitmap @@ -203,10 +290,13 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { uint256 index = 0; for (uint256 i = 0; i < operatorRestakedQuorums.length; i++) { uint8 quorum = uint8(operatorRestakedQuorums[i]); - uint256 strategyParamsLength = _stakeRegistry.strategyParamsLength(quorum); + uint256 strategyParamsLength = _stakeRegistry.strategyParamsLength( + quorum + ); for (uint256 j = 0; j < strategyParamsLength; j++) { - restakedStrategies[index] = - address(_stakeRegistry.strategyParamsByIndex(quorum, j).strategy); + restakedStrategies[index] = address( + _stakeRegistry.strategyParamsByIndex(quorum, j).strategy + ); index++; } } diff --git a/src/interfaces/IServiceManager.sol b/src/interfaces/IServiceManager.sol index ad953ec0..b232ebb5 100644 --- a/src/interfaces/IServiceManager.sol +++ b/src/interfaces/IServiceManager.sol @@ -20,8 +20,37 @@ interface IServiceManager is IServiceManagerUI { * @dev This function will revert if the `rewardsSubmission` is malformed, * e.g. if the `strategies` and `weights` arrays are of non-equal lengths */ - function createAVSRewardsSubmission(IRewardsCoordinator.RewardsSubmission[] calldata rewardsSubmissions) external; + function createAVSRewardsSubmission( + IRewardsCoordinator.RewardsSubmission[] calldata rewardsSubmissions + ) external; + + /** + * @notice Creates a new operator-directed rewards submission on behalf of an AVS, to be split amongst the operators and + * set of stakers delegated to operators who are registered to the `avs`. + * @param operatorDirectedRewardsSubmissions The operator-directed rewards submissions being created + * @dev Only callabe by the permissioned rewardsInitiator address + * @dev The duration of the `rewardsSubmission` cannot exceed `MAX_REWARDS_DURATION` + * @dev The tokens are sent to the `RewardsCoordinator` contract + * @dev This contract needs a token approval of sum of all `operatorRewards` in the `operatorDirectedRewardsSubmissions`, before calling this function. + * @dev Strategies must be in ascending order of addresses to check for duplicates + * @dev Operators must be in ascending order of addresses to check for duplicates. + * @dev This function will revert if the `operatorDirectedRewardsSubmissions` is malformed. + */ + function createOperatorDirectedAVSRewardsSubmission( + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + calldata operatorDirectedRewardsSubmissions + ) external; + + /** + * @notice Forwards a call to Eigenlayer's RewardsCoordinator contract to set the address of the entity that can call `processClaim` on behalf of this contract. + * @param claimer The address of the entity that can call `processClaim` on behalf of the earner + * @dev Only callabe by the owner. + */ + function setClaimerFor(address claimer) external; // EVENTS - event RewardsInitiatorUpdated(address prevRewardsInitiator, address newRewardsInitiator); + event RewardsInitiatorUpdated( + address prevRewardsInitiator, + address newRewardsInitiator + ); } diff --git a/src/unaudited/ECDSAServiceManagerBase.sol b/src/unaudited/ECDSAServiceManagerBase.sol index 57326bea..6ad640b4 100644 --- a/src/unaudited/ECDSAServiceManagerBase.sol +++ b/src/unaudited/ECDSAServiceManagerBase.sol @@ -106,6 +106,21 @@ abstract contract ECDSAServiceManagerBase is _createAVSRewardsSubmission(rewardsSubmissions); } + /// @inheritdoc IServiceManager + function createOperatorDirectedAVSRewardsSubmission( + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + calldata operatorDirectedRewardsSubmissions + ) external virtual onlyRewardsInitiator { + _createOperatorDirectedAVSRewardsSubmission( + operatorDirectedRewardsSubmissions + ); + } + + /// @inheritdoc IServiceManager + function setClaimerFor(address claimer) external virtual onlyOwner { + _setClaimerFor(claimer); + } + /// @inheritdoc IServiceManagerUI function registerOperatorToAVS( address operator, @@ -203,6 +218,64 @@ abstract contract ECDSAServiceManagerBase is ); } + /** + * @notice Creates a new operator-directed rewards submission, to be split amongst the operators and + * set of stakers delegated to operators who are registered to this `avs`. + * @param operatorDirectedRewardsSubmissions The operator-directed rewards submissions being created. + */ + function _createOperatorDirectedAVSRewardsSubmission( + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + calldata operatorDirectedRewardsSubmissions + ) internal virtual { + for ( + uint256 i = 0; + i < operatorDirectedRewardsSubmissions.length; + ++i + ) { + // Calculate total amount of token to transfer + uint256 totalAmount = 0; + for ( + uint256 j = 0; + j < + operatorDirectedRewardsSubmissions[i].operatorRewards.length; + ++j + ) { + totalAmount += operatorDirectedRewardsSubmissions[i] + .operatorRewards[j] + .amount; + } + + // Transfer token to ServiceManager and approve RewardsCoordinator to transfer again + // in createOperatorDirectedAVSRewardsSubmission() call + operatorDirectedRewardsSubmissions[i].token.transferFrom( + msg.sender, + address(this), + totalAmount + ); + uint256 allowance = operatorDirectedRewardsSubmissions[i] + .token + .allowance(address(this), rewardsCoordinator); + operatorDirectedRewardsSubmissions[i].token.approve( + rewardsCoordinator, + totalAmount + allowance + ); + } + + IRewardsCoordinator(rewardsCoordinator) + .createOperatorDirectedAVSRewardsSubmission( + address(this), + operatorDirectedRewardsSubmissions + ); + } + + /** + * @notice Forwards a call to Eigenlayer's RewardsCoordinator contract to set the address of the entity that can call `processClaim` on behalf of this contract. + * @param claimer The address of the entity that can call `processClaim` on behalf of the earner. + */ + function _setClaimerFor(address claimer) internal virtual { + IRewardsCoordinator(rewardsCoordinator).setClaimerFor(claimer); + } + /** * @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. diff --git a/test/events/IServiceManagerBaseEvents.sol b/test/events/IServiceManagerBaseEvents.sol index 6defff0d..4ae9b92e 100644 --- a/test/events/IServiceManagerBaseEvents.sol +++ b/test/events/IServiceManagerBaseEvents.sol @@ -1,10 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.12; -import { - IRewardsCoordinator, - IERC20 -} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; +import {IRewardsCoordinator, IERC20} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; interface IServiceManagerBaseEvents { /// RewardsCoordinator EVENTS /// @@ -24,15 +21,28 @@ interface IServiceManagerBaseEvents { IRewardsCoordinator.RewardsSubmission rewardsSubmission ); /// @notice rewardsUpdater is responsible for submiting DistributionRoots, only owner can set rewardsUpdater - event RewardsUpdaterSet(address indexed oldRewardsUpdater, address indexed newRewardsUpdater); + event RewardsUpdaterSet( + address indexed oldRewardsUpdater, + address indexed newRewardsUpdater + ); event RewardsForAllSubmitterSet( address indexed rewardsForAllSubmitter, bool indexed oldValue, bool indexed newValue ); - event ActivationDelaySet(uint32 oldActivationDelay, uint32 newActivationDelay); - event GlobalCommissionBipsSet(uint16 oldGlobalCommissionBips, uint16 newGlobalCommissionBips); - event ClaimerForSet(address indexed earner, address indexed oldClaimer, address indexed claimer); + event ActivationDelaySet( + uint32 oldActivationDelay, + uint32 newActivationDelay + ); + event DefaultOperatorSplitBipsSet( + uint16 oldDefaultOperatorSplitBips, + uint16 newDefaultOperatorSplitBips + ); + 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, @@ -49,8 +59,53 @@ interface IServiceManagerBaseEvents { IERC20 token, uint256 claimedAmount ); - - + /** + * @notice Emitted when an AVS creates a valid `OperatorDirectedRewardsSubmission` + * @param caller The address calling `createOperatorDirectedAVSRewardsSubmission`. + * @param avs The avs on behalf of which the operator-directed rewards are being submitted. + * @param operatorDirectedRewardsSubmissionHash Keccak256 hash of (`avs`, `submissionNonce` and `operatorDirectedRewardsSubmission`). + * @param submissionNonce Current nonce of the avs. Used to generate a unique submission hash. + * @param operatorDirectedRewardsSubmission The Operator-Directed Rewards Submission. Contains the token, start timestamp, duration, operator rewards, description and, strategy and multipliers. + */ + event OperatorDirectedAVSRewardsSubmissionCreated( + address indexed caller, + address indexed avs, + bytes32 indexed operatorDirectedRewardsSubmissionHash, + uint256 submissionNonce, + IRewardsCoordinator.OperatorDirectedRewardsSubmission operatorDirectedRewardsSubmission + ); + /** + * @notice Emitted when the operator split for an AVS is set. + * @param caller The address calling `setOperatorAVSSplit`. + * @param operator The operator on behalf of which the split is being set. + * @param avs The avs for which the split is being set by the operator. + * @param activatedAt The timestamp at which the split will be activated. + * @param oldOperatorAVSSplitBips The old split for the operator for the AVS. + * @param newOperatorAVSSplitBips The new split for the operator for the AVS. + */ + event OperatorAVSSplitBipsSet( + address indexed caller, + address indexed operator, + address indexed avs, + uint32 activatedAt, + uint16 oldOperatorAVSSplitBips, + uint16 newOperatorAVSSplitBips + ); + /** + * @notice Emitted when the operator split for Programmatic Incentives is set. + * @param caller The address calling `setOperatorPISplit`. + * @param operator The operator on behalf of which the split is being set. + * @param activatedAt The timestamp at which the split will be activated. + * @param oldOperatorPISplitBips The old split for the operator for Programmatic Incentives. + * @param newOperatorPISplitBips The new split for the operator for Programmatic Incentives. + */ + event OperatorPISplitBipsSet( + address indexed caller, + address indexed operator, + uint32 activatedAt, + uint16 oldOperatorPISplitBips, + uint16 newOperatorPISplitBips + ); /// TOKEN EVENTS FOR TESTING /// /** @@ -65,5 +120,9 @@ interface IServiceManagerBaseEvents { * @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); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); } diff --git a/test/mocks/RewardsCoordinatorMock.sol b/test/mocks/RewardsCoordinatorMock.sol index 28bd0308..c3e36490 100644 --- a/test/mocks/RewardsCoordinatorMock.sol +++ b/test/mocks/RewardsCoordinatorMock.sol @@ -22,41 +22,96 @@ contract RewardsCoordinatorMock is IRewardsCoordinator { function claimerFor(address earner) external view returns (address) {} - function cumulativeClaimed(address claimer, IERC20 token) external view returns (uint256) {} + function cumulativeClaimed( + address claimer, + IERC20 token + ) external view returns (uint256) {} - function globalOperatorCommissionBips() external view returns (uint16) {} + function defaultOperatorSplitBips() external view returns (uint16) {} - function operatorCommissionBips(address operator, address avs) external view returns (uint16) {} + function calculateEarnerLeafHash( + EarnerTreeMerkleLeaf calldata leaf + ) external pure returns (bytes32) {} - function calculateEarnerLeafHash(EarnerTreeMerkleLeaf calldata leaf) external pure returns (bytes32) {} + function calculateTokenLeafHash( + TokenTreeMerkleLeaf calldata leaf + ) external pure returns (bytes32) {} - function calculateTokenLeafHash(TokenTreeMerkleLeaf calldata leaf) external pure returns (bytes32) {} + function checkClaim( + RewardsMerkleClaim calldata claim + ) external view returns (bool) {} - function checkClaim(RewardsMerkleClaim calldata claim) external view returns (bool) {} - - function currRewardsCalculationEndTimestamp() external view returns (uint32) {} + function currRewardsCalculationEndTimestamp() + external + view + returns (uint32) + {} function getDistributionRootsLength() external view returns (uint256) {} - function getDistributionRootAtIndex(uint256 index) external view returns (DistributionRoot memory) {} + function getDistributionRootAtIndex( + uint256 index + ) external view returns (DistributionRoot memory) {} - function getCurrentDistributionRoot() external view returns (DistributionRoot memory) {} + function getCurrentDistributionRoot() + external + view + returns (DistributionRoot memory) + {} - function getCurrentClaimableDistributionRoot() external view returns (DistributionRoot memory) {} + function getCurrentClaimableDistributionRoot() + external + view + returns (DistributionRoot memory) + {} - function getRootIndexFromHash(bytes32 rootHash) external view returns (uint32) {} + function getRootIndexFromHash( + bytes32 rootHash + ) external view returns (uint32) {} function domainSeparator() external view returns (bytes32) {} - function createAVSRewardsSubmission(RewardsSubmission[] calldata rewardsSubmissions) external {} + function getOperatorAVSSplit( + address operator, + address avs + ) external view returns (uint16) {} + + function getOperatorPISplit( + address operator + ) external view returns (uint16) {} + + function createAVSRewardsSubmission( + RewardsSubmission[] calldata rewardsSubmissions + ) external {} - function createRewardsForAllSubmission(RewardsSubmission[] calldata rewardsSubmission) external {} + function createRewardsForAllSubmission( + RewardsSubmission[] calldata rewardsSubmission + ) external {} - function createRewardsForAllEarners(RewardsSubmission[] calldata rewardsSubmissions) external {} + function createRewardsForAllEarners( + RewardsSubmission[] calldata rewardsSubmissions + ) external {} - function processClaim(RewardsMerkleClaim calldata claim, address recipient) external {} + function createOperatorDirectedAVSRewardsSubmission( + address avs, + OperatorDirectedRewardsSubmission[] + calldata operatorDirectedRewardsSubmissions + ) external {} - function submitRoot(bytes32 root, uint32 rewardsCalculationEndTimestamp) external {} + function processClaim( + RewardsMerkleClaim calldata claim, + address recipient + ) external {} + + function processClaims( + RewardsMerkleClaim[] calldata claims, + address recipient + ) external {} + + function submitRoot( + bytes32 root, + uint32 rewardsCalculationEndTimestamp + ) external {} function disableRoot(uint32 rootIndex) external {} @@ -64,10 +119,20 @@ contract RewardsCoordinatorMock is IRewardsCoordinator { function setActivationDelay(uint32 _activationDelay) external {} - function setGlobalOperatorCommission(uint16 _globalCommissionBips) external {} + function setDefaultOperatorSplit(uint16 split) external {} function setRewardsUpdater(address _rewardsUpdater) external {} - function setRewardsForAllSubmitter(address _submitter, bool _newValue) external {} + function setRewardsForAllSubmitter( + address _submitter, + bool _newValue + ) external {} + + function setOperatorAVSSplit( + address operator, + address avs, + uint16 split + ) external {} -} \ No newline at end of file + function setOperatorPISplit(address operator, uint16 split) external {} +} diff --git a/test/unit/ServiceManagerBase.t.sol b/test/unit/ServiceManagerBase.t.sol index 2b04dabd..a7f6464a 100644 --- a/test/unit/ServiceManagerBase.t.sol +++ b/test/unit/ServiceManagerBase.t.sol @@ -2,19 +2,19 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; -import { - RewardsCoordinator, - IRewardsCoordinator, - IERC20 -} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol"; +import {RewardsCoordinator, IRewardsCoordinator, IERC20} from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.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 { +contract ServiceManagerBase_UnitTests is + MockAVSDeployer, + IServiceManagerBaseEvents +{ // RewardsCoordinator config - address rewardsUpdater = address(uint160(uint256(keccak256("rewardsUpdater")))); + address rewardsUpdater = + address(uint160(uint256(keccak256("rewardsUpdater")))); uint32 CALCULATION_INTERVAL_SECONDS = 7 days; uint32 MAX_REWARDS_DURATION = 70 days; uint32 MAX_RETROACTIVE_LENGTH = 84 days; @@ -28,7 +28,8 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve // Testing Config and Mocks address serviceManagerOwner; - address rewardsInitiator = address(uint160(uint256(keccak256("rewardsInitiator")))); + address rewardsInitiator = + address(uint160(uint256(keccak256("rewardsInitiator")))); IERC20[] rewardTokens; uint256 mockTokenInitialSupply = 10e50; IStrategy strategyMock1; @@ -67,7 +68,7 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve RewardsCoordinator.initialize.selector, msg.sender, pauserRegistry, - 0, /*initialPausedStatus*/ + 0 /*initialPausedStatus*/, rewardsUpdater, activationDelay, globalCommissionBips @@ -89,7 +90,9 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve address(serviceManagerImplementation), address(proxyAdmin), abi.encodeWithSelector( - ServiceManagerMock.initialize.selector, msg.sender, msg.sender + ServiceManagerMock.initialize.selector, + msg.sender, + msg.sender ) ) ) @@ -108,11 +111,18 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve } /// @notice deploy token to owner and approve ServiceManager. Used for deploying reward tokens - function _deployMockRewardTokens(address owner, uint256 numTokens) internal virtual { + function _deployMockRewardTokens( + 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); + IERC20 token = new ERC20PresetFixedSupply( + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + owner + ); rewardTokens.push(token); token.approve(address(serviceManager), mockTokenInitialSupply); } @@ -133,12 +143,22 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve function _setUpDefaultStrategiesAndMultipliers() internal virtual { // Deploy Mock Strategies IERC20 token1 = new ERC20PresetFixedSupply( - "dog wif hat", "MOCK1", mockTokenInitialSupply, address(this) + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + address(this) + ); + IERC20 token2 = new ERC20PresetFixedSupply( + "jeo boden", + "MOCK2", + mockTokenInitialSupply, + address(this) ); - IERC20 token2 = - new ERC20PresetFixedSupply("jeo boden", "MOCK2", mockTokenInitialSupply, address(this)); IERC20 token3 = new ERC20PresetFixedSupply( - "pepe wif avs", "MOCK3", mockTokenInitialSupply, address(this) + "pepe wif avs", + "MOCK3", + mockTokenInitialSupply, + address(this) ); strategyImplementation = new StrategyBase(strategyManagerMock); strategyMock1 = StrategyBase( @@ -146,7 +166,11 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve new TransparentUpgradeableProxy( address(strategyImplementation), address(proxyAdmin), - abi.encodeWithSelector(StrategyBase.initialize.selector, token1, pauserRegistry) + abi.encodeWithSelector( + StrategyBase.initialize.selector, + token1, + pauserRegistry + ) ) ) ); @@ -155,7 +179,11 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve new TransparentUpgradeableProxy( address(strategyImplementation), address(proxyAdmin), - abi.encodeWithSelector(StrategyBase.initialize.selector, token2, pauserRegistry) + abi.encodeWithSelector( + StrategyBase.initialize.selector, + token2, + pauserRegistry + ) ) ) ); @@ -164,7 +192,11 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve new TransparentUpgradeableProxy( address(strategyImplementation), address(proxyAdmin), - abi.encodeWithSelector(StrategyBase.initialize.selector, token3, pauserRegistry) + abi.encodeWithSelector( + StrategyBase.initialize.selector, + token3, + pauserRegistry + ) ) ) ); @@ -179,18 +211,29 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve strategyManagerMock.setStrategyWhitelist(strategies[2], true); defaultStrategyAndMultipliers.push( - IRewardsCoordinator.StrategyAndMultiplier(IStrategy(address(strategies[0])), 1e18) + IRewardsCoordinator.StrategyAndMultiplier( + IStrategy(address(strategies[0])), + 1e18 + ) ); defaultStrategyAndMultipliers.push( - IRewardsCoordinator.StrategyAndMultiplier(IStrategy(address(strategies[1])), 2e18) + IRewardsCoordinator.StrategyAndMultiplier( + IStrategy(address(strategies[1])), + 2e18 + ) ); defaultStrategyAndMultipliers.push( - IRewardsCoordinator.StrategyAndMultiplier(IStrategy(address(strategies[2])), 3e18) + IRewardsCoordinator.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) { + 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++) { @@ -204,14 +247,16 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve return arr; } - function _maxTimestamp(uint32 timestamp1, uint32 timestamp2) internal pure returns (uint32) { + function _maxTimestamp( + uint32 timestamp1, + uint32 timestamp2 + ) internal pure returns (uint32) { return timestamp1 > timestamp2 ? timestamp1 : timestamp2; } - function testFuzz_createAVSRewardsSubmission_Revert_WhenNotOwner(address caller) - public - filterFuzzedAddressInputs(caller) - { + function testFuzz_createAVSRewardsSubmission_Revert_WhenNotOwner( + address caller + ) public filterFuzzedAddressInputs(caller) { cheats.assume(caller != rewardsInitiator); IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions; @@ -222,13 +267,20 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve serviceManager.createAVSRewardsSubmission(rewardsSubmissions); } - function test_createAVSRewardsSubmission_Revert_WhenERC20NotApproved() public { + function test_createAVSRewardsSubmission_Revert_WhenERC20NotApproved() + public + { IERC20 token = new ERC20PresetFixedSupply( - "dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsInitiator + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + rewardsInitiator ); - IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions = - new IRewardsCoordinator.RewardsSubmission[](1); + IRewardsCoordinator.RewardsSubmission[] + memory rewardsSubmissions = new IRewardsCoordinator.RewardsSubmission[]( + 1 + ); rewardsSubmissions[0] = IRewardsCoordinator.RewardsSubmission({ strategiesAndMultipliers: defaultStrategyAndMultipliers, token: token, @@ -249,7 +301,10 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve ) public { // 1. Bound fuzz inputs to valid ranges and amounts IERC20 rewardToken = new ERC20PresetFixedSupply( - "dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsInitiator + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + rewardsInitiator ); amount = bound(amount, 1, MAX_REWARDS_AMOUNT); duration = bound(duration, 0, MAX_REWARDS_DURATION); @@ -258,16 +313,23 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve startTimestamp, uint256( _maxTimestamp( - GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + GENESIS_REWARDS_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH ) - ) + CALCULATION_INTERVAL_SECONDS - 1, + ) + + CALCULATION_INTERVAL_SECONDS - + 1, block.timestamp + uint256(MAX_FUTURE_LENGTH) ); - startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + startTimestamp = + startTimestamp - + (startTimestamp % CALCULATION_INTERVAL_SECONDS); // 2. Create reward submission input param - IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions = - new IRewardsCoordinator.RewardsSubmission[](1); + IRewardsCoordinator.RewardsSubmission[] + memory rewardsSubmissions = new IRewardsCoordinator.RewardsSubmission[]( + 1 + ); rewardsSubmissions[0] = IRewardsCoordinator.RewardsSubmission({ strategiesAndMultipliers: defaultStrategyAndMultipliers, token: rewardToken, @@ -281,25 +343,40 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve rewardToken.approve(address(serviceManager), amount); // 4. call createAVSRewardsSubmission() with expected event emitted - uint256 rewardsInitiatorBalanceBefore = - rewardToken.balanceOf(address(rewardsInitiator)); - uint256 rewardsCoordinatorBalanceBefore = - rewardToken.balanceOf(address(rewardsCoordinator)); + uint256 rewardsInitiatorBalanceBefore = rewardToken.balanceOf( + address(rewardsInitiator) + ); + uint256 rewardsCoordinatorBalanceBefore = rewardToken.balanceOf( + address(rewardsCoordinator) + ); rewardToken.approve(address(rewardsCoordinator), amount); - uint256 currSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); - bytes32 avsSubmissionHash = - keccak256(abi.encode(address(serviceManager), currSubmissionNonce, rewardsSubmissions[0])); + uint256 currSubmissionNonce = rewardsCoordinator.submissionNonce( + address(serviceManager) + ); + bytes32 avsSubmissionHash = keccak256( + abi.encode( + address(serviceManager), + currSubmissionNonce, + rewardsSubmissions[0] + ) + ); cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); emit AVSRewardsSubmissionCreated( - address(serviceManager), currSubmissionNonce, avsSubmissionHash, rewardsSubmissions[0] + address(serviceManager), + currSubmissionNonce, + avsSubmissionHash, + rewardsSubmissions[0] ); serviceManager.createAVSRewardsSubmission(rewardsSubmissions); cheats.stopPrank(); assertTrue( - rewardsCoordinator.isAVSRewardsSubmissionHash(address(serviceManager), avsSubmissionHash), + rewardsCoordinator.isAVSRewardsSubmissionHash( + address(serviceManager), + avsSubmissionHash + ), "reward submission hash not submitted" ); assertEq( @@ -328,16 +405,25 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve cheats.assume(2 <= numSubmissions && numSubmissions <= 10); cheats.prank(rewardsCoordinator.owner()); - IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions = - new IRewardsCoordinator.RewardsSubmission[](numSubmissions); + IRewardsCoordinator.RewardsSubmission[] + memory rewardsSubmissions = new IRewardsCoordinator.RewardsSubmission[]( + numSubmissions + ); bytes32[] memory avsSubmissionHashes = new bytes32[](numSubmissions); - uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); + uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce( + address(serviceManager) + ); _deployMockRewardTokens(rewardsInitiator, numSubmissions); - uint256[] memory avsBalancesBefore = - _getBalanceForTokens(rewardTokens, rewardsInitiator); - uint256[] memory rewardsCoordinatorBalancesBefore = - _getBalanceForTokens(rewardTokens, address(rewardsCoordinator)); + uint256[] memory avsBalancesBefore = _getBalanceForTokens( + rewardTokens, + rewardsInitiator + ); + uint256[] + memory rewardsCoordinatorBalancesBefore = _getBalanceForTokens( + rewardTokens, + address(rewardsCoordinator) + ); uint256[] memory amounts = new uint256[](numSubmissions); // Create multiple rewards submissions and their expected event @@ -351,28 +437,45 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve startTimestamp + i, uint256( _maxTimestamp( - GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + GENESIS_REWARDS_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH ) - ) + CALCULATION_INTERVAL_SECONDS - 1, + ) + + CALCULATION_INTERVAL_SECONDS - + 1, block.timestamp + uint256(MAX_FUTURE_LENGTH) ); - startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + startTimestamp = + startTimestamp - + (startTimestamp % CALCULATION_INTERVAL_SECONDS); // 2. Create reward submission input param - IRewardsCoordinator.RewardsSubmission memory rewardsSubmission = IRewardsCoordinator.RewardsSubmission({ - strategiesAndMultipliers: defaultStrategyAndMultipliers, - token: rewardTokens[i], - amount: amounts[i], - startTimestamp: uint32(startTimestamp), - duration: uint32(duration) - }); + IRewardsCoordinator.RewardsSubmission + memory rewardsSubmission = IRewardsCoordinator + .RewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardTokens[i], + amount: amounts[i], + startTimestamp: uint32(startTimestamp), + duration: uint32(duration) + }); rewardsSubmissions[i] = rewardsSubmission; // 3. expected event emitted for this rewardsSubmission avsSubmissionHashes[i] = keccak256( - abi.encode(address(serviceManager), startSubmissionNonce + i, rewardsSubmissions[i]) + abi.encode( + address(serviceManager), + startSubmissionNonce + i, + rewardsSubmissions[i] + ) + ); + cheats.expectEmit( + true, + true, + true, + true, + address(rewardsCoordinator) ); - cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); emit AVSRewardsSubmissionCreated( address(serviceManager), startSubmissionNonce + i, @@ -395,7 +498,8 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve for (uint256 i = 0; i < numSubmissions; ++i) { assertTrue( rewardsCoordinator.isAVSRewardsSubmissionHash( - address(serviceManager), avsSubmissionHashes[i] + address(serviceManager), + avsSubmissionHashes[i] ), "rewards submission hash not submitted" ); @@ -421,18 +525,26 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve cheats.assume(2 <= numSubmissions && numSubmissions <= 10); cheats.prank(rewardsCoordinator.owner()); - IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions = - new IRewardsCoordinator.RewardsSubmission[](numSubmissions); + IRewardsCoordinator.RewardsSubmission[] + memory rewardsSubmissions = new IRewardsCoordinator.RewardsSubmission[]( + numSubmissions + ); bytes32[] memory avsSubmissionHashes = new bytes32[](numSubmissions); - uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); + uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce( + address(serviceManager) + ); IERC20 rewardToken = new ERC20PresetFixedSupply( - "dog wif hat", "MOCK1", mockTokenInitialSupply, rewardsInitiator + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + rewardsInitiator ); cheats.prank(rewardsInitiator); rewardToken.approve(address(serviceManager), mockTokenInitialSupply); uint256 avsBalanceBefore = rewardToken.balanceOf(rewardsInitiator); - uint256 rewardsCoordinatorBalanceBefore = - rewardToken.balanceOf(address(rewardsCoordinator)); + uint256 rewardsCoordinatorBalanceBefore = rewardToken.balanceOf( + address(rewardsCoordinator) + ); uint256 totalAmount = 0; uint256[] memory amounts = new uint256[](numSubmissions); @@ -449,28 +561,45 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve startTimestamp + i, uint256( _maxTimestamp( - GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + GENESIS_REWARDS_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH ) - ) + CALCULATION_INTERVAL_SECONDS - 1, + ) + + CALCULATION_INTERVAL_SECONDS - + 1, block.timestamp + uint256(MAX_FUTURE_LENGTH) ); - startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + startTimestamp = + startTimestamp - + (startTimestamp % CALCULATION_INTERVAL_SECONDS); // 2. Create reward submission input param - IRewardsCoordinator.RewardsSubmission memory rewardsSubmission = IRewardsCoordinator.RewardsSubmission({ - strategiesAndMultipliers: defaultStrategyAndMultipliers, - token: rewardToken, - amount: amounts[i], - startTimestamp: uint32(startTimestamp), - duration: uint32(duration) - }); + IRewardsCoordinator.RewardsSubmission + memory rewardsSubmission = IRewardsCoordinator + .RewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + amount: amounts[i], + startTimestamp: uint32(startTimestamp), + duration: uint32(duration) + }); rewardsSubmissions[i] = rewardsSubmission; // 3. expected event emitted for this avs rewards submission avsSubmissionHashes[i] = keccak256( - abi.encode(address(serviceManager), startSubmissionNonce + i, rewardsSubmissions[i]) + abi.encode( + address(serviceManager), + startSubmissionNonce + i, + rewardsSubmissions[i] + ) + ); + cheats.expectEmit( + true, + true, + true, + true, + address(rewardsCoordinator) ); - cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); emit AVSRewardsSubmissionCreated( address(serviceManager), startSubmissionNonce + i, @@ -503,7 +632,8 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve for (uint256 i = 0; i < numSubmissions; ++i) { assertTrue( rewardsCoordinator.isAVSRewardsSubmissionHash( - address(serviceManager), avsSubmissionHashes[i] + address(serviceManager), + avsSubmissionHashes[i] ), "rewards submission hash not submitted" ); @@ -511,7 +641,9 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve } function test_setRewardsInitiator() public { - address newRewardsInitiator = address(uint160(uint256(keccak256("newRewardsInitiator")))); + address newRewardsInitiator = address( + uint160(uint256(keccak256("newRewardsInitiator"))) + ); cheats.prank(serviceManagerOwner); serviceManager.setRewardsInitiator(newRewardsInitiator); assertEq(newRewardsInitiator, serviceManager.rewardsInitiator()); @@ -519,9 +651,432 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve function test_setRewardsInitiator_revert_notOwner() public { address caller = address(uint160(uint256(keccak256("caller")))); - address newRewardsInitiator = address(uint160(uint256(keccak256("newRewardsInitiator")))); + address newRewardsInitiator = address( + uint160(uint256(keccak256("newRewardsInitiator"))) + ); cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(caller); serviceManager.setRewardsInitiator(newRewardsInitiator); } + + function testFuzz_setClaimerFor(address claimer) public { + cheats.startPrank(serviceManagerOwner); + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit ClaimerForSet( + address(serviceManager), + rewardsCoordinator.claimerFor(address(serviceManager)), + claimer + ); + serviceManager.setClaimerFor(claimer); + assertEq( + claimer, + rewardsCoordinator.claimerFor(address(serviceManager)), + "claimerFor not set" + ); + cheats.stopPrank(); + } + + function testFuzz_setClaimerFor_revert_notOwner( + address caller, + address claimer + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(caller != serviceManagerOwner); + cheats.prank(caller); + cheats.expectRevert("Ownable: caller is not the owner"); + serviceManager.setClaimerFor(claimer); + } +} + +contract ServiceManagerBase_createOperatorDirectedAVSRewardsSubmission is + ServiceManagerBase_UnitTests +{ + // used for stack too deep + struct FuzzOperatorDirectedAVSRewardsSubmission { + uint256 startTimestamp; + uint256 duration; + } + + IRewardsCoordinator.OperatorReward[] defaultOperatorRewards; + + function setUp() public virtual override { + ServiceManagerBase_UnitTests.setUp(); + + address[] memory operators = new address[](3); + operators[0] = makeAddr("operator1"); + operators[1] = makeAddr("operator2"); + operators[2] = makeAddr("operator3"); + operators = _sortAddressArrayAsc(operators); + + defaultOperatorRewards.push( + IRewardsCoordinator.OperatorReward(operators[0], 1e18) + ); + defaultOperatorRewards.push( + IRewardsCoordinator.OperatorReward(operators[1], 2e18) + ); + defaultOperatorRewards.push( + IRewardsCoordinator.OperatorReward(operators[2], 3e18) + ); + + // Set the timestamp to when Rewards v2 will realisticly go out (i.e 6 months) + cheats.warp(GENESIS_REWARDS_TIMESTAMP + 168 days); + } + + /// @dev Sort to ensure that the array is in ascending order for addresses + function _sortAddressArrayAsc( + address[] memory arr + ) internal pure returns (address[] memory) { + uint256 l = arr.length; + for (uint256 i = 0; i < l; i++) { + for (uint256 j = i + 1; j < l; j++) { + if (arr[i] > arr[j]) { + address temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + } + } + return arr; + } + + function _getTotalRewardsAmount( + IRewardsCoordinator.OperatorReward[] memory operatorRewards + ) internal pure returns (uint256) { + uint256 totalAmount = 0; + for (uint256 i = 0; i < operatorRewards.length; ++i) { + totalAmount += operatorRewards[i].amount; + } + return totalAmount; + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_Revert_WhenNotOwner( + address caller + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(caller != rewardsInitiator); + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + memory operatorDirectedRewardsSubmissions; + + cheats.prank(caller); + cheats.expectRevert( + "ServiceManagerBase.onlyRewardsInitiator: caller is not the rewards initiator" + ); + serviceManager.createOperatorDirectedAVSRewardsSubmission( + operatorDirectedRewardsSubmissions + ); + } + + function testFuzz_createOperatorDirectedAVSRewardsSubmission_Revert_WhenERC20NotApproved( + uint256 startTimestamp, + uint256 duration + ) public { + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply( + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + rewardsInitiator + ); + duration = bound(duration, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound( + startTimestamp, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + + CALCULATION_INTERVAL_SECONDS - + 1, + block.timestamp - duration - 1 + ); + startTimestamp = + startTimestamp - + (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // 2. Create operator directed rewards submission input param + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + memory operatorDirectedRewardsSubmissions = new IRewardsCoordinator.OperatorDirectedRewardsSubmission[]( + 1 + ); + operatorDirectedRewardsSubmissions[0] = IRewardsCoordinator + .OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. Call createOperatorDirectedAVSRewardsSubmission() + cheats.prank(rewardsInitiator); + cheats.expectRevert("ERC20: insufficient allowance"); + serviceManager.createOperatorDirectedAVSRewardsSubmission( + operatorDirectedRewardsSubmissions + ); + } + + /** + * @notice test a single rewards submission asserting for the following + * - correct event emitted + * - submission nonce incrementation by 1, and rewards submission hash being set in storage. + * - rewards submission hash being set in storage + * - token balance before and after of rewards initiator and rewardsCoordinator + */ + function testFuzz_createOperatorDirectedAVSRewardsSubmission_SingleSubmission( + uint256 startTimestamp, + uint256 duration + ) public { + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply( + "dog wif hat", + "MOCK1", + mockTokenInitialSupply, + rewardsInitiator + ); + duration = bound(duration, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound( + startTimestamp, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + + CALCULATION_INTERVAL_SECONDS - + 1, + block.timestamp - duration - 1 + ); + startTimestamp = + startTimestamp - + (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // 2. Create operator directed rewards submission input param + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + memory operatorDirectedRewardsSubmissions = new IRewardsCoordinator.OperatorDirectedRewardsSubmission[]( + 1 + ); + operatorDirectedRewardsSubmissions[0] = IRewardsCoordinator + .OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. Get total amount + uint256 amount = _getTotalRewardsAmount(defaultOperatorRewards); + + // 4. Approve serviceManager for ERC20 + cheats.startPrank(rewardsInitiator); + rewardToken.approve(address(serviceManager), amount); + + // 3. call createOperatorDirectedAVSRewardsSubmission() with expected event emitted + uint256 rewardsInitiatorBalanceBefore = rewardToken.balanceOf( + rewardsInitiator + ); + uint256 rewardsCoordinatorBalanceBefore = rewardToken.balanceOf( + address(rewardsCoordinator) + ); + uint256 currSubmissionNonce = rewardsCoordinator.submissionNonce( + address(serviceManager) + ); + bytes32 rewardsSubmissionHash = keccak256( + abi.encode( + address(serviceManager), + currSubmissionNonce, + operatorDirectedRewardsSubmissions[0] + ) + ); + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorDirectedAVSRewardsSubmissionCreated( + address(serviceManager), + address(serviceManager), + rewardsSubmissionHash, + currSubmissionNonce, + operatorDirectedRewardsSubmissions[0] + ); + serviceManager.createOperatorDirectedAVSRewardsSubmission( + operatorDirectedRewardsSubmissions + ); + cheats.stopPrank(); + + assertTrue( + rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash( + address(serviceManager), + rewardsSubmissionHash + ), + "rewards submission hash not submitted" + ); + assertEq( + currSubmissionNonce + 1, + rewardsCoordinator.submissionNonce(address(serviceManager)), + "submission nonce not incremented" + ); + assertEq( + rewardsInitiatorBalanceBefore - amount, + rewardToken.balanceOf(rewardsInitiator), + "rewardsInitiator balance not decremented by amount of rewards submission" + ); + assertEq( + rewardsCoordinatorBalanceBefore + amount, + rewardToken.balanceOf(address(rewardsCoordinator)), + "RewardsCoordinator balance not incremented by amount of rewards submission" + ); + } + + /** + * @notice test a multiple rewards submission asserting for the following + * - correct event emitted + * - submission nonce incrementation by 1, and rewards submission hash being set in storage. + * - rewards submission hash being set in storage + * - token balance before and after of rewards initiator and rewardsCoordinator + */ + function testFuzz_createOperatorDirectedAVSRewardsSubmission_MultipleSubmissions( + FuzzOperatorDirectedAVSRewardsSubmission memory param, + uint256 numSubmissions + ) public { + cheats.assume(2 <= numSubmissions && numSubmissions <= 10); + cheats.prank(rewardsCoordinator.owner()); + + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] + memory rewardsSubmissions = new IRewardsCoordinator.OperatorDirectedRewardsSubmission[]( + numSubmissions + ); + bytes32[] memory rewardsSubmissionHashes = new bytes32[]( + numSubmissions + ); + uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce( + address(serviceManager) + ); + _deployMockRewardTokens(rewardsInitiator, numSubmissions); + + uint256[] memory rewardsInitiatorBalancesBefore = _getBalanceForTokens( + rewardTokens, + rewardsInitiator + ); + uint256[] + memory rewardsCoordinatorBalancesBefore = _getBalanceForTokens( + rewardTokens, + address(rewardsCoordinator) + ); + uint256[] memory amounts = new uint256[](numSubmissions); + + // Create multiple rewards submissions and their expected event + for (uint256 i = 0; i < numSubmissions; ++i) { + // 1. Bound fuzz inputs to valid ranges and amounts using randSeed for each + amounts[i] = _getTotalRewardsAmount(defaultOperatorRewards); + param.duration = bound(param.duration, 0, MAX_REWARDS_DURATION); + param.duration = + param.duration - + (param.duration % CALCULATION_INTERVAL_SECONDS); + param.startTimestamp = bound( + param.startTimestamp + i, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + + CALCULATION_INTERVAL_SECONDS - + 1, + block.timestamp + uint256(MAX_FUTURE_LENGTH) + ); + param.startTimestamp = + param.startTimestamp - + (param.startTimestamp % CALCULATION_INTERVAL_SECONDS); + + param.duration = bound(param.duration, 0, MAX_REWARDS_DURATION); + param.duration = + param.duration - + (param.duration % CALCULATION_INTERVAL_SECONDS); + param.startTimestamp = bound( + param.startTimestamp, + uint256( + _maxTimestamp( + GENESIS_REWARDS_TIMESTAMP, + uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH + ) + ) + + CALCULATION_INTERVAL_SECONDS - + 1, + block.timestamp - param.duration - 1 + ); + param.startTimestamp = + param.startTimestamp - + (param.startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // 2. Create rewards submission input param + IRewardsCoordinator.OperatorDirectedRewardsSubmission + memory rewardsSubmission = IRewardsCoordinator + .OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardTokens[i], + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(param.startTimestamp), + duration: uint32(param.duration), + description: "" + }); + rewardsSubmissions[i] = rewardsSubmission; + + // 3. expected event emitted for this rewardsSubmission + rewardsSubmissionHashes[i] = keccak256( + abi.encode( + address(serviceManager), + startSubmissionNonce + i, + rewardsSubmissions[i] + ) + ); + cheats.expectEmit( + true, + true, + true, + true, + address(rewardsCoordinator) + ); + emit OperatorDirectedAVSRewardsSubmissionCreated( + address(serviceManager), + address(serviceManager), + rewardsSubmissionHashes[i], + startSubmissionNonce + i, + rewardsSubmissions[i] + ); + } + + // 4. call createAVSRewardsSubmission() + cheats.prank(rewardsInitiator); + serviceManager.createOperatorDirectedAVSRewardsSubmission( + rewardsSubmissions + ); + + // 5. Check for submissionNonce() and rewardsSubmissionHashes being set + assertEq( + startSubmissionNonce + numSubmissions, + rewardsCoordinator.submissionNonce(address(serviceManager)), + "submission nonce not incremented properly" + ); + + for (uint256 i = 0; i < numSubmissions; ++i) { + assertTrue( + rewardsCoordinator.isOperatorDirectedAVSRewardsSubmissionHash( + address(serviceManager), + rewardsSubmissionHashes[i] + ), + "rewards submission hash not submitted" + ); + assertEq( + rewardsInitiatorBalancesBefore[i] - amounts[i], + rewardTokens[i].balanceOf(rewardsInitiator), + "rewardsInitiator balance not decremented by amount of rewards submission" + ); + assertEq( + rewardsCoordinatorBalancesBefore[i] + amounts[i], + rewardTokens[i].balanceOf(address(rewardsCoordinator)), + "RewardsCoordinator balance not incremented by amount of rewards submission" + ); + } + } }