From 55f2ff3948b780e2c4bcc5440f50c8943531a349 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:12:19 -0500 Subject: [PATCH 1/5] chore: update submodule --- lib/eigenlayer-contracts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index 85e12bab..cf63d637 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit 85e12bab5f3d4c6ff21790fe5909755ee7154b07 +Subproject commit cf63d6379c6d351209ce062cd6eb471d20c9cf94 From 72515c3470c11ae7c3129aa9d062a1d9ac4a4cfc Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:57:10 -0500 Subject: [PATCH 2/5] chore: update submodule --- lib/eigenlayer-contracts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eigenlayer-contracts b/lib/eigenlayer-contracts index cf63d637..387f7c6c 160000 --- a/lib/eigenlayer-contracts +++ b/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit cf63d6379c6d351209ce062cd6eb471d20c9cf94 +Subproject commit 387f7c6c7bab0dd4e1f0631ab230750785c4a64c From 37f4549a239611e1e9a2213f2d67e11718f69130 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:02:32 -0500 Subject: [PATCH 3/5] feat: add `createOperatorDirectedOperatorSetRewardsSubmission` to SMB --- src/ServiceManagerBase.sol | 37 +++++++++++++++++++-------- test/integration/User.t.sol | 2 +- test/mocks/DelegationMock.sol | 26 +++++++++++++++++++ test/mocks/EigenPodManagerMock.sol | 4 +++ test/mocks/RewardsCoordinatorMock.sol | 12 +++++++-- 5 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/ServiceManagerBase.sol b/src/ServiceManagerBase.sol index 86c1e937..c199278e 100644 --- a/src/ServiceManagerBase.sol +++ b/src/ServiceManagerBase.sol @@ -5,7 +5,7 @@ import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initia import {ISignatureUtils} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; import {IAVSDirectory} from "eigenlayer-contracts/src/contracts/interfaces/IAVSDirectory.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; -import {IRewardsCoordinator} from +import {IRewardsCoordinator, IERC20, OperatorSet} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol"; import {IAllocationManager, IAllocationManagerTypes} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; @@ -97,6 +97,10 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { _allocationManager.slashOperator(address(this), params); } + function _increaseAllowance(IERC20 token, address spender, uint256 amount) internal { + token.approve(spender, amount + token.allowance(address(this), spender)); + } + /** * @notice Creates a new rewards submission to the EigenLayer RewardsCoordinator contract, to be split amongst the * set of stakers delegated to operators who are registered to this `avs` @@ -112,21 +116,32 @@ abstract contract ServiceManagerBase is ServiceManagerBaseStorage { 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.approve( - address(_rewardsCoordinator), rewardsSubmissions[i].amount + allowance - ); + IRewardsCoordinator.RewardsSubmission calldata submission = rewardsSubmissions[i]; + + submission.token.transferFrom(msg.sender, address(this), submission.amount); + + _increaseAllowance(submission.token, address(_rewardsCoordinator), submission.amount); } _rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions); } + function createOperatorDirectedOperatorSetRewardsSubmission( + OperatorSet memory operatorSet, + IRewardsCoordinator.OperatorDirectedRewardsSubmission[] calldata rewardsSubmissions, + uint256[] memory totalAmounts + ) public virtual onlyRewardsInitiator { + for (uint256 i = 0; i < rewardsSubmissions.length; ++i) { + IRewardsCoordinator.OperatorDirectedRewardsSubmission calldata submission = rewardsSubmissions[i]; + + submission.token.transferFrom(msg.sender, address(this), totalAmounts[i]); + + _increaseAllowance(submission.token, address(_rewardsCoordinator), totalAmounts[i]); + } + + _rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, rewardsSubmissions); + } + function createOperatorSets(IAllocationManager.CreateSetParams[] memory params) external onlyRegistryCoordinator { _allocationManager.createOperatorSets(address(this), params); } diff --git a/test/integration/User.t.sol b/test/integration/User.t.sol index 4b58950a..23e9ffcc 100644 --- a/test/integration/User.t.sol +++ b/test/integration/User.t.sol @@ -271,7 +271,7 @@ contract User is Test { params[0] = IDelegationManagerTypes.QueuedWithdrawalParams({ strategies: strategies, depositShares: shares, - withdrawer: address(this) + __deprecated_withdrawer: address(this) }); delegationManager.queueWithdrawals(params); diff --git a/test/mocks/DelegationMock.sol b/test/mocks/DelegationMock.sol index e3b1ed77..f4101b19 100644 --- a/test/mocks/DelegationMock.sol +++ b/test/mocks/DelegationMock.sol @@ -18,6 +18,13 @@ contract DelegationIntermediate is IDelegationManager { uint256 initialPausedStatus ) external virtual {} + function convertToDepositShares( + address staker, + IStrategy[] memory strategies, + uint256[] memory withdrawableShares + ) external view returns (uint256[] memory) {} + + function registerAsOperator( OperatorDetails calldata registeringOperatorDetails, uint32 allocationDelay, @@ -239,6 +246,25 @@ contract DelegationIntermediate is IDelegationManager { ) external virtual {} function minWithdrawalDelayBlocks() external view virtual override returns (uint32) {} + + /// @notice Returns the Withdrawal associated with a `withdrawalRoot`, if it exists. NOTE that + /// withdrawals queued before the slashing release can NOT be queried with this method. + function getQueuedWithdrawal( + bytes32 withdrawalRoot + ) external virtual override view returns (Withdrawal memory) {} + + /// @notice Returns a list of queued withdrawal roots for the `staker`. + /// NOTE that this only returns withdrawals queued AFTER the slashing release. + function getQueuedWithdrawalRoots( + address staker + ) external virtual override view returns (bytes32[] memory) {} + + function slashOperatorShares( + address operator, + IStrategy strategy, + uint64 prevMaxMagnitude, + uint64 newMaxMagnitude + ) external virtual override {} } contract DelegationMock is DelegationIntermediate { diff --git a/test/mocks/EigenPodManagerMock.sol b/test/mocks/EigenPodManagerMock.sol index cefd5b7a..889fb069 100644 --- a/test/mocks/EigenPodManagerMock.sol +++ b/test/mocks/EigenPodManagerMock.sol @@ -15,6 +15,10 @@ contract EigenPodManagerMock is Test, Pausable, IEigenPodManager { _setPausedStatus(0); } + function burnableETHShares() external view returns (uint256) {} + + function increaseBurnableShares(IStrategy strategy, uint256 addedSharesToBurn) external {} + function podOwnerShares(address podOwner) external view returns (int256) { return podShares[podOwner]; } diff --git a/test/mocks/RewardsCoordinatorMock.sol b/test/mocks/RewardsCoordinatorMock.sol index 9d992962..cf419ab9 100644 --- a/test/mocks/RewardsCoordinatorMock.sol +++ b/test/mocks/RewardsCoordinatorMock.sol @@ -30,10 +30,18 @@ contract RewardsCoordinatorMock is IRewardsCoordinator { function createOperatorDirectedAVSRewardsSubmission( address avs, - OperatorDirectedRewardsSubmission[] - calldata operatorDirectedRewardsSubmissions + OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions ) external override {} + function createOperatorDirectedOperatorSetRewardsSubmission( + OperatorSet calldata operatorSet, + OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions + ) external override {} + + function getOperatorSetSplit(address operator, OperatorSet calldata operatorSet) external override view returns (uint16) {} + + function setOperatorSetSplit(address operator, OperatorSet calldata operatorSet, uint16 split) external override {} + function processClaim( RewardsMerkleClaim calldata claim, address recipient From b29c5ea1852e60012914480f069ae6681c4715dc Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:56:08 -0500 Subject: [PATCH 4/5] test(wip): `createOperatorDirectedOperatorSetRewardsSubmission` --- test/mocks/AllocationManagerMock.sol | 20 +++- test/unit/ServiceManagerBase.t.sol | 143 ++++++++++++++++++++++----- 2 files changed, 138 insertions(+), 25 deletions(-) diff --git a/test/mocks/AllocationManagerMock.sol b/test/mocks/AllocationManagerMock.sol index b355af95..cdd2add0 100644 --- a/test/mocks/AllocationManagerMock.sol +++ b/test/mocks/AllocationManagerMock.sol @@ -1,11 +1,18 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; -import {IAllocationManager, OperatorSet} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {IAllocationManager, OperatorSet } from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; +import {OperatorSetLib} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol"; + import {IAVSRegistrar } from "eigenlayer-contracts/src/contracts/interfaces/IAVSRegistrar.sol"; import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol"; contract AllocationManagerIntermediate is IAllocationManager { + using OperatorSetLib for OperatorSet; + + + mapping(bytes32 => bool) internal _isOperatorSet; + function initialize( address initialOwner, uint256 initialPausedStatus @@ -139,7 +146,16 @@ contract AllocationManagerIntermediate is IAllocationManager { function isOperatorSet( OperatorSet memory operatorSet - ) external view virtual returns (bool) {} + ) external view virtual returns (bool) { + return _isOperatorSet[operatorSet.key()]; + } + + function setIsOperatorSet( + OperatorSet memory operatorSet, + bool isSet + ) external virtual { + _isOperatorSet[operatorSet.key()] = isSet; + } function getMembers( OperatorSet memory operatorSet diff --git a/test/unit/ServiceManagerBase.t.sol b/test/unit/ServiceManagerBase.t.sol index 8d37f9d6..67748000 100644 --- a/test/unit/ServiceManagerBase.t.sol +++ b/test/unit/ServiceManagerBase.t.sol @@ -5,9 +5,12 @@ import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import { RewardsCoordinator, IRewardsCoordinator, + IRewardsCoordinatorErrors, IRewardsCoordinatorTypes, + OperatorSet, IERC20 } from "eigenlayer-contracts/src/contracts/core/RewardsCoordinator.sol"; +import {IAllocationManagerTypes} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol"; import {PermissionController} from "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol"; import {StrategyBase} from "eigenlayer-contracts/src/contracts/strategies/StrategyBase.sol"; import {IStrategyManager} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; @@ -40,8 +43,9 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve IStrategy strategyMock1; IStrategy strategyMock2; IStrategy strategyMock3; + IStrategy[] strategies; StrategyBase strategyImplementation; - IRewardsCoordinator.StrategyAndMultiplier[] defaultStrategyAndMultipliers; + IRewardsCoordinatorTypes.StrategyAndMultiplier[] defaultStrategyAndMultipliers; // mapping to setting fuzzed inputs mapping(address => bool) public addressIsExcludedFromFuzzedInputs; @@ -177,11 +181,14 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve ) ) ); - IStrategy[] memory strategies = new IStrategy[](3); - strategies[0] = strategyMock1; - strategies[1] = strategyMock2; - strategies[2] = strategyMock3; - strategies = _sortArrayAsc(strategies); + + IStrategy[] memory strats = new IStrategy[](3); + strats[0] = strategyMock1; + strats[1] = strategyMock2; + strats[2] = strategyMock3; + strats = _sortArrayAsc(strats); + + strategies = strats; strategyManagerMock.setStrategyWhitelist(strategies[0], true); strategyManagerMock.setStrategyWhitelist(strategies[1], true); @@ -217,12 +224,29 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve return timestamp1 > timestamp2 ? timestamp1 : timestamp2; } + // function test_setRewardsInitiator() public { + // address newRewardsInitiator = address(uint160(uint256(keccak256("newRewardsInitiator")))); + // cheats.prank(serviceManagerOwner); + // serviceManager.setRewardsInitiator(newRewardsInitiator); + // assertEq(newRewardsInitiator, serviceManager.rewardsInitiator()); + // } + + // function test_setRewardsInitiator_revert_notOwner() public { + // address caller = address(uint160(uint256(keccak256("caller")))); + // address newRewardsInitiator = address(uint160(uint256(keccak256("newRewardsInitiator")))); + // cheats.expectRevert("Ownable: caller is not the owner"); + // cheats.prank(caller); + // serviceManager.setRewardsInitiator(newRewardsInitiator); + // } +} + +contract ServiceManagerBase_createAVSRewardsSubmission_UnitTests is ServiceManagerBase_UnitTests { function testFuzz_createAVSRewardsSubmission_Revert_WhenNotOwner(address caller) public filterFuzzedAddressInputs(caller) { cheats.assume(caller != rewardsInitiator); - IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions; + IRewardsCoordinatorTypes.RewardsSubmission[] memory rewardsSubmissions; cheats.prank(caller); cheats.expectRevert( @@ -337,8 +361,8 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve cheats.assume(2 <= numSubmissions && numSubmissions <= 10); cheats.prank(rewardsCoordinator.owner()); - IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions = - new IRewardsCoordinator.RewardsSubmission[](numSubmissions); + IRewardsCoordinatorTypes.RewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinatorTypes.RewardsSubmission[](numSubmissions); bytes32[] memory avsSubmissionHashes = new bytes32[](numSubmissions); uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); _deployMockRewardTokens(rewardsInitiator, numSubmissions); @@ -430,8 +454,8 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve cheats.assume(2 <= numSubmissions && numSubmissions <= 10); cheats.prank(rewardsCoordinator.owner()); - IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions = - new IRewardsCoordinator.RewardsSubmission[](numSubmissions); + IRewardsCoordinatorTypes.RewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinatorTypes.RewardsSubmission[](numSubmissions); bytes32[] memory avsSubmissionHashes = new bytes32[](numSubmissions); uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce(address(serviceManager)); IERC20 rewardToken = new ERC20PresetFixedSupply( @@ -518,19 +542,92 @@ contract ServiceManagerBase_UnitTests is MockAVSDeployer, IServiceManagerBaseEve ); } } +} - function test_setRewardsInitiator() public { - address newRewardsInitiator = address(uint160(uint256(keccak256("newRewardsInitiator")))); - cheats.prank(serviceManagerOwner); - serviceManager.setRewardsInitiator(newRewardsInitiator); - assertEq(newRewardsInitiator, serviceManager.rewardsInitiator()); - } +contract ServiceManagerBase_createOperatorDirectedOperatorSetRewardsSubmission_UnitTests is + ServiceManagerBase_UnitTests +{ + OperatorSet operatorSet = OperatorSet(address(this), 1); + + function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_Revert_WhenNotOwner( + address caller + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(caller != rewardsInitiator); - function test_setRewardsInitiator_revert_notOwner() public { - address caller = address(uint160(uint256(keccak256("caller")))); - address newRewardsInitiator = address(uint160(uint256(keccak256("newRewardsInitiator")))); - cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(caller); - serviceManager.setRewardsInitiator(newRewardsInitiator); + cheats.expectRevert( + "ServiceManagerBase.onlyRewardsInitiator: caller is not the rewards initiator" + ); + serviceManager.createOperatorDirectedOperatorSetRewardsSubmission( + operatorSet, + new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](0), + new uint256[](0) + ); + } + + function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_Revert_InvalidOperatorSet() public { + permissionControllerMock.setCanCall({ + account: address(this), + caller: address(serviceManager), + target: address(rewardsCoordinator), + selector: IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission.selector + }); + + cheats.prank(rewardsInitiator); + cheats.expectRevert(IRewardsCoordinatorErrors.InvalidOperatorSet.selector); + serviceManager.createOperatorDirectedOperatorSetRewardsSubmission( + operatorSet, new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](0), new uint256[](0) + ); + } + + function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_Correctness() public { + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + uint256 numSubmissions = cheats.randomUint(1, 10); + uint256 maxOperatorsPerSubmission = cheats.randomUint(1, 32); + + _deployMockRewardTokens(rewardsInitiator, numSubmissions); + + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](numSubmissions); + uint256[] memory totalAmounts = new uint256[](numSubmissions); + + for (uint256 i = 0; i < numSubmissions; ++i) { + uint256 numOperators = cheats.randomUint(1, maxOperatorsPerSubmission); + IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = + new IRewardsCoordinatorTypes.OperatorReward[](numOperators); + uint256 totalAmount = 0; + + for (uint256 j = 0; j < numOperators; ++j) { + address operator = cheats.randomAddress(); + uint256 amount = cheats.randomUint(1 ether, 100 ether); + operatorRewards[j] = + IRewardsCoordinatorTypes.OperatorReward({operator: operator, amount: amount}); + totalAmount += amount; + } + + rewardsSubmissions[i] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardTokens[i % rewardTokens.length], + operatorRewards: operatorRewards, + startTimestamp: uint32(block.timestamp), + duration: uint32(1 weeks), + description: string.concat("Test submission #", cheats.toString(i)) + }); + + totalAmounts[i] = totalAmount; + } + + permissionControllerMock.setCanCall({ + account: address(this), + caller: address(serviceManager), + target: address(rewardsCoordinator), + selector: IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission.selector + }); + + cheats.prank(rewardsInitiator); + serviceManager.createOperatorDirectedOperatorSetRewardsSubmission( + operatorSet, rewardsSubmissions, totalAmounts + ); } -} \ No newline at end of file +} From 17d3332b9b5b3027169fe704f21136949409a821 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:34:56 -0500 Subject: [PATCH 5/5] test: passing --- test/unit/ServiceManagerBase.t.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/unit/ServiceManagerBase.t.sol b/test/unit/ServiceManagerBase.t.sol index 67748000..6c59ce12 100644 --- a/test/unit/ServiceManagerBase.t.sol +++ b/test/unit/ServiceManagerBase.t.sol @@ -581,6 +581,7 @@ contract ServiceManagerBase_createOperatorDirectedOperatorSetRewardsSubmission_U } function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_Correctness() public { + vm.warp(block.timestamp - (block.timestamp % CALCULATION_INTERVAL_SECONDS)); allocationManagerMock.setIsOperatorSet(operatorSet, true); uint256 numSubmissions = cheats.randomUint(1, 10); @@ -599,7 +600,7 @@ contract ServiceManagerBase_createOperatorDirectedOperatorSetRewardsSubmission_U uint256 totalAmount = 0; for (uint256 j = 0; j < numOperators; ++j) { - address operator = cheats.randomAddress(); + address operator = address(uint160(j + 1)); uint256 amount = cheats.randomUint(1 ether, 100 ether); operatorRewards[j] = IRewardsCoordinatorTypes.OperatorReward({operator: operator, amount: amount}); @@ -610,14 +611,16 @@ contract ServiceManagerBase_createOperatorDirectedOperatorSetRewardsSubmission_U strategiesAndMultipliers: defaultStrategyAndMultipliers, token: rewardTokens[i % rewardTokens.length], operatorRewards: operatorRewards, - startTimestamp: uint32(block.timestamp), - duration: uint32(1 weeks), + startTimestamp: uint32(block.timestamp - CALCULATION_INTERVAL_SECONDS), + duration: uint32(CALCULATION_INTERVAL_SECONDS), description: string.concat("Test submission #", cheats.toString(i)) }); totalAmounts[i] = totalAmount; } + vm.warp(block.timestamp + 1); + permissionControllerMock.setCanCall({ account: address(this), caller: address(serviceManager),