diff --git a/script/configs/devnet/deploy_from_scratch.anvil.config.json b/script/configs/devnet/deploy_from_scratch.anvil.config.json index 6d8b5fef76..a262317eff 100644 --- a/script/configs/devnet/deploy_from_scratch.anvil.config.json +++ b/script/configs/devnet/deploy_from_scratch.anvil.config.json @@ -49,7 +49,8 @@ "calculation_interval_seconds": 604800, "global_operator_commission_bips": 1000, "OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP": 1720656000, - "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000 + "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000, + "emissions_controller_address": "0x0000000000000000000000000000000000000000" }, "ethPOSDepositAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa", "semver": "v1.0.3" diff --git a/script/configs/devnet/deploy_from_scratch.holesky.config.json b/script/configs/devnet/deploy_from_scratch.holesky.config.json index 054a432868..7f254ab0ef 100644 --- a/script/configs/devnet/deploy_from_scratch.holesky.config.json +++ b/script/configs/devnet/deploy_from_scratch.holesky.config.json @@ -49,7 +49,8 @@ "calculation_interval_seconds": 604800, "global_operator_commission_bips": 1000, "OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP": 1720656000, - "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000 + "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000, + "emissions_controller_address": "0x0000000000000000000000000000000000000000" }, "ethPOSDepositAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa", "semver": "v0.0.0" diff --git a/script/configs/devnet/deploy_from_scratch.holesky.slashing.config.json b/script/configs/devnet/deploy_from_scratch.holesky.slashing.config.json index 99d3c54cbe..854d69c35e 100644 --- a/script/configs/devnet/deploy_from_scratch.holesky.slashing.config.json +++ b/script/configs/devnet/deploy_from_scratch.holesky.slashing.config.json @@ -36,7 +36,8 @@ "calculation_interval_seconds": 604800, "global_operator_commission_bips": 1000, "OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP": 1720656000, - "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000 + "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000, + "emissions_controller_address": "0x0000000000000000000000000000000000000000" }, "allocationManager": { "init_paused_status": 0, diff --git a/script/configs/local/deploy_from_scratch.slashing.anvil.config.json b/script/configs/local/deploy_from_scratch.slashing.anvil.config.json index b557f8ab74..b01ab763ed 100644 --- a/script/configs/local/deploy_from_scratch.slashing.anvil.config.json +++ b/script/configs/local/deploy_from_scratch.slashing.anvil.config.json @@ -55,7 +55,8 @@ "calculation_interval_seconds": 604800, "global_operator_commission_bips": 1000, "OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP": 1720656000, - "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000 + "OPERATOR_SET_MAX_RETROACTIVE_LENGTH": 2592000, + "emissions_controller_address": "0x0000000000000000000000000000000000000000" }, "addresses": { "token": { diff --git a/script/deploy/devnet/deploy_from_scratch.s.sol b/script/deploy/devnet/deploy_from_scratch.s.sol index 9062066d47..4cfa1e9594 100644 --- a/script/deploy/devnet/deploy_from_scratch.s.sol +++ b/script/deploy/devnet/deploy_from_scratch.s.sol @@ -106,6 +106,8 @@ contract DeployFromScratch is Script, Test { uint32 REWARDS_COORDINATOR_OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP; uint32 REWARDS_COORDINATOR_OPERATOR_SET_MAX_RETROACTIVE_LENGTH; + address REWARDS_COORDINATOR_EMISSIONS_CONTROLLER; + // AllocationManager uint256 ALLOCATION_MANAGER_INIT_PAUSED_STATUS; @@ -172,6 +174,9 @@ contract DeployFromScratch is Script, Test { require(executorMultisig != address(0), "executorMultisig address not configured correctly!"); require(operationsMultisig != address(0), "operationsMultisig address not configured correctly!"); + REWARDS_COORDINATOR_EMISSIONS_CONTROLLER = + stdJson.readAddress(config_data, ".rewardsCoordinator.emissions_controller_address"); + // START RECORDING TRANSACTIONS FOR DEPLOYMENT vm.startBroadcast(); @@ -249,6 +254,7 @@ contract DeployFromScratch is Script, Test { delegation, strategyManager, IAllocationManager(address(allocationManager)), + IEmissionsController(address(REWARDS_COORDINATOR_EMISSIONS_CONTROLLER)), eigenLayerPauserReg, permissionController, REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS, diff --git a/script/deploy/local/deploy_from_scratch.slashing.s.sol b/script/deploy/local/deploy_from_scratch.slashing.s.sol index 44ae57285d..aeff770fe2 100644 --- a/script/deploy/local/deploy_from_scratch.slashing.s.sol +++ b/script/deploy/local/deploy_from_scratch.slashing.s.sol @@ -111,6 +111,8 @@ contract DeployFromScratch is Script, Test { uint32 REWARDS_COORDINATOR_OPERATOR_SET_GENESIS_REWARDS_TIMESTAMP; uint32 REWARDS_COORDINATOR_OPERATOR_SET_MAX_RETROACTIVE_LENGTH; + address REWARDS_COORDINATOR_EMISSIONS_CONTROLLER; + // AllocationManager uint256 ALLOCATION_MANAGER_INIT_PAUSED_STATUS; @@ -160,6 +162,9 @@ contract DeployFromScratch is Script, Test { REWARDS_COORDINATOR_OPERATOR_SET_MAX_RETROACTIVE_LENGTH = uint32(stdJson.readUint(config_data, ".rewardsCoordinator.OPERATOR_SET_MAX_RETROACTIVE_LENGTH")); + REWARDS_COORDINATOR_EMISSIONS_CONTROLLER = + stdJson.readAddress(config_data, ".rewardsCoordinator.emissions_controller_address"); + STRATEGY_MANAGER_INIT_WITHDRAWAL_DELAY_BLOCKS = uint32(stdJson.readUint(config_data, ".strategyManager.init_withdrawal_delay_blocks")); @@ -258,6 +263,7 @@ contract DeployFromScratch is Script, Test { delegation, strategyManager, IAllocationManager(address(allocationManager)), + IEmissionsController(address(REWARDS_COORDINATOR_EMISSIONS_CONTROLLER)), eigenLayerPauserReg, permissionController, REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS, diff --git a/script/releases/CoreContractsDeployer.sol b/script/releases/CoreContractsDeployer.sol index 0eb3d8cb58..af8976d5e9 100644 --- a/script/releases/CoreContractsDeployer.sol +++ b/script/releases/CoreContractsDeployer.sol @@ -93,6 +93,7 @@ abstract contract CoreContractsDeployer is EOADeployer { delegationManager: Env.proxy.delegationManager(), strategyManager: Env.proxy.strategyManager(), allocationManager: Env.proxy.allocationManager(), + emissionsController: Env.proxy.emissionsController(), pauserRegistry: Env.impl.pauserRegistry(), permissionController: Env.proxy.permissionController(), CALCULATION_INTERVAL_SECONDS: Env.CALCULATION_INTERVAL_SECONDS(), diff --git a/src/contracts/core/EmissionsController.sol b/src/contracts/core/EmissionsController.sol index 13806a86ac..b6ed5cc705 100644 --- a/src/contracts/core/EmissionsController.sol +++ b/src/contracts/core/EmissionsController.sol @@ -208,7 +208,10 @@ contract EmissionsController is ); } else if (distribution.distributionType == DistributionType.EigenDA) { success = _tryCallRewardsCoordinator( - abi.encodeCall(IRewardsCoordinator.createAVSRewardsSubmission, (rewardsSubmissions)) + abi.encodeCall( + IRewardsCoordinator.createEigenDARewardsSubmission, + (distribution.operatorSet.avs, rewardsSubmissions) + ) ); } } else { diff --git a/src/contracts/core/RewardsCoordinator.sol b/src/contracts/core/RewardsCoordinator.sol index ff28813da9..7d9be15164 100644 --- a/src/contracts/core/RewardsCoordinator.sol +++ b/src/contracts/core/RewardsCoordinator.sol @@ -51,6 +51,7 @@ contract RewardsCoordinator is params.delegationManager, params.strategyManager, params.allocationManager, + params.emissionsController, params.CALCULATION_INTERVAL_SECONDS, params.MAX_REWARDS_DURATION, params.MAX_RETROACTIVE_LENGTH, @@ -87,30 +88,19 @@ contract RewardsCoordinator is /// ----------------------------------------------------------------------- /// @inheritdoc IRewardsCoordinator - function createAVSRewardsSubmission( + function createEigenDARewardsSubmission( + address avs, RewardsSubmission[] calldata rewardsSubmissions ) external onlyWhenNotPaused(PAUSED_AVS_REWARDS_SUBMISSION) nonReentrant { - for (uint256 i = 0; i < rewardsSubmissions.length; i++) { - RewardsSubmission memory rewardsSubmission = rewardsSubmissions[i]; - - // First validate the submission. - _validateRewardsSubmission(rewardsSubmission); - - // Then transfer the full amount to the contract. - rewardsSubmission.token.safeTransferFrom(msg.sender, address(this), rewardsSubmission.amount); - - // Then take the protocol fee (if the submitter is opted in for protocol fees). - rewardsSubmission.amount = _takeProtocolFee(msg.sender, rewardsSubmission.token, rewardsSubmission.amount); - - // Last update storage. - uint256 nonce = submissionNonce[msg.sender]; - bytes32 rewardsSubmissionHash = keccak256(abi.encode(msg.sender, nonce, rewardsSubmission)); - - isAVSRewardsSubmissionHash[msg.sender][rewardsSubmissionHash] = true; - submissionNonce[msg.sender] = nonce + 1; + require(msg.sender == address(emissionsController), UnauthorizedCaller()); + _createAVSRewardsSubmission(avs, rewardsSubmissions); + } - emit AVSRewardsSubmissionCreated(msg.sender, nonce, rewardsSubmissionHash, rewardsSubmission); - } + /// @inheritdoc IRewardsCoordinator + function createAVSRewardsSubmission( + RewardsSubmission[] calldata rewardsSubmissions + ) external onlyWhenNotPaused(PAUSED_AVS_REWARDS_SUBMISSION) nonReentrant { + _createAVSRewardsSubmission(msg.sender, rewardsSubmissions); } /// @inheritdoc IRewardsCoordinator @@ -469,6 +459,36 @@ contract RewardsCoordinator is /// Internal Helper Functions /// ----------------------------------------------------------------------- + /// @notice Internal helper to create AVS rewards submissions. + /// @param avs The address of the AVS submitting the rewards. + /// @param rewardsSubmissions The RewardsSubmissions to be created. + function _createAVSRewardsSubmission( + address avs, + RewardsSubmission[] calldata rewardsSubmissions + ) internal { + for (uint256 i = 0; i < rewardsSubmissions.length; i++) { + RewardsSubmission memory rewardsSubmission = rewardsSubmissions[i]; + + // First validate the submission. + _validateRewardsSubmission(rewardsSubmission); + + // Then transfer the full amount to the contract. + rewardsSubmission.token.safeTransferFrom(avs, address(this), rewardsSubmission.amount); + + // Then take the protocol fee (if the submitter is opted in for protocol fees). + rewardsSubmission.amount = _takeProtocolFee(avs, rewardsSubmission.token, rewardsSubmission.amount); + + // Last update storage. + uint256 nonce = submissionNonce[avs]; + bytes32 rewardsSubmissionHash = keccak256(abi.encode(avs, nonce, rewardsSubmission)); + + isAVSRewardsSubmissionHash[avs][rewardsSubmissionHash] = true; + submissionNonce[avs] = nonce + 1; + + emit AVSRewardsSubmissionCreated(avs, nonce, rewardsSubmissionHash, rewardsSubmission); + } + } + /// @notice Internal helper to process reward claims. /// @param claim The RewardsMerkleClaims to be processed. /// @param recipient The address recipient that receives the ERC20 rewards diff --git a/src/contracts/core/storage/RewardsCoordinatorStorage.sol b/src/contracts/core/storage/RewardsCoordinatorStorage.sol index 038a8324dd..5341883ba1 100644 --- a/src/contracts/core/storage/RewardsCoordinatorStorage.sol +++ b/src/contracts/core/storage/RewardsCoordinatorStorage.sol @@ -62,6 +62,9 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { /// @notice The AllocationManager contract for EigenLayer IAllocationManager public immutable allocationManager; + /// @notice The RewardsCoordinator contract for EigenLayer + IEmissionsController public immutable emissionsController; + /// @notice The interval in seconds at which the calculation for rewards distribution is done. /// @dev RewardsSubmission durations must be multiples of this interval. This is going to be configured to 1 day uint32 public immutable CALCULATION_INTERVAL_SECONDS; @@ -150,6 +153,7 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { IDelegationManager _delegationManager, IStrategyManager _strategyManager, IAllocationManager _allocationManager, + IEmissionsController _emissionsController, uint32 _CALCULATION_INTERVAL_SECONDS, uint32 _MAX_REWARDS_DURATION, uint32 _MAX_RETROACTIVE_LENGTH, @@ -163,6 +167,7 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { delegationManager = _delegationManager; strategyManager = _strategyManager; allocationManager = _allocationManager; + emissionsController = _emissionsController; CALCULATION_INTERVAL_SECONDS = _CALCULATION_INTERVAL_SECONDS; MAX_REWARDS_DURATION = _MAX_REWARDS_DURATION; MAX_RETROACTIVE_LENGTH = _MAX_RETROACTIVE_LENGTH; diff --git a/src/contracts/interfaces/IEmissionsController.sol b/src/contracts/interfaces/IEmissionsController.sol index 9c0d9dac01..5df818e4a2 100644 --- a/src/contracts/interfaces/IEmissionsController.sol +++ b/src/contracts/interfaces/IEmissionsController.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity >=0.5.0; +import "./IAllocationManager.sol"; import "./IRewardsCoordinator.sol"; import "./IEigen.sol"; import "./IBackingEigen.sol"; import "./IPausable.sol"; +import {OperatorSet} from "../libraries/OperatorSetLib.sol"; /// @title IEmissionsControllerErrors /// @notice Errors for the IEmissionsController contract. diff --git a/src/contracts/interfaces/IRewardsCoordinator.sol b/src/contracts/interfaces/IRewardsCoordinator.sol index 29e9b1dbe7..1054bf493a 100644 --- a/src/contracts/interfaces/IRewardsCoordinator.sol +++ b/src/contracts/interfaces/IRewardsCoordinator.sol @@ -10,6 +10,7 @@ import "./IStrategyManager.sol"; import "./IPauserRegistry.sol"; import "./IPermissionController.sol"; import "./IStrategy.sol"; +import "./IEmissionsController.sol"; interface IRewardsCoordinatorErrors { /// @dev Thrown when msg.sender is not allowed to call a function @@ -244,6 +245,7 @@ interface IRewardsCoordinatorTypes { IDelegationManager delegationManager; IStrategyManager strategyManager; IAllocationManager allocationManager; + IEmissionsController emissionsController; IPauserRegistry pauserRegistry; IPermissionController permissionController; uint32 CALCULATION_INTERVAL_SECONDS; @@ -446,6 +448,21 @@ interface IRewardsCoordinator is IRewardsCoordinatorErrors, IRewardsCoordinatorE address _feeRecipient ) external; + /// @notice Creates a new rewards submission on behalf of the Eigen DA AVS, to be split amongst the + /// set of stakers delegated to operators who are registered to the `avs` + /// @param rewardsSubmissions The rewards submissions being created + /// @dev Expected to be called by the EmissionsController behalf of the Eigen DA AVS of which the submission is being made + /// @dev The duration of the `rewardsSubmission` cannot exceed `MAX_REWARDS_DURATION` + /// @dev The duration of the `rewardsSubmission` cannot be 0 and must be a multiple of `CALCULATION_INTERVAL_SECONDS` + /// @dev The tokens are sent to the `RewardsCoordinator` contract + /// @dev Strategies must be in ascending order of addresses to check for duplicates + /// @dev This function will revert if the `rewardsSubmission` is malformed, + /// e.g. if the `strategies` and `weights` arrays are of non-equal lengths + function createEigenDARewardsSubmission( + address avs, + RewardsSubmission[] calldata rewardsSubmissions + ) external; + /// @notice Creates a new rewards submission on behalf of an AVS, to be split amongst the /// set of stakers delegated to operators who are registered to the `avs` /// @param rewardsSubmissions The rewards submissions being created diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index ab5bf59230..a4e6e4cae1 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -414,6 +414,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { delegationManager: delegationManager, strategyManager: strategyManager, allocationManager: allocationManager, + emissionsController: emissionsController, pauserRegistry: eigenLayerPauserReg, permissionController: permissionController, CALCULATION_INTERVAL_SECONDS: REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS, diff --git a/src/test/unit/RewardsCoordinatorUnit.t.sol b/src/test/unit/RewardsCoordinatorUnit.t.sol index e144d061d6..7ff1058b22 100644 --- a/src/test/unit/RewardsCoordinatorUnit.t.sol +++ b/src/test/unit/RewardsCoordinatorUnit.t.sol @@ -117,6 +117,7 @@ contract RewardsCoordinatorUnitTests is EigenLayerUnitTestSetup, IRewardsCoordin delegationManager: IDelegationManager(address(delegationManagerMock)), strategyManager: IStrategyManager(address(strategyManagerMock)), allocationManager: IAllocationManager(address(allocationManagerMock)), + emissionsController: IEmissionsController(address(vm.addr(0x123123ff))), pauserRegistry: pauserRegistry, permissionController: IPermissionController(address(permissionController)), CALCULATION_INTERVAL_SECONDS: CALCULATION_INTERVAL_SECONDS, @@ -1548,6 +1549,49 @@ contract RewardsCoordinatorUnitTests_createAVSRewardsSubmission is RewardsCoordi } } +contract RewardsCoordinatorUnitTests_createEigenDARewardsSubmission is RewardsCoordinatorUnitTests { + // Revert when not called by emissionsController + function testFuzz_Revert_WhenNotEmissionsController(address unauthorizedCaller) public filterFuzzedAddressInputs(unauthorizedCaller) { + address emissionsController = address(vm.addr(0x123123ff)); + cheats.assume(unauthorizedCaller != emissionsController); + + cheats.prank(unauthorizedCaller); + cheats.expectRevert(UnauthorizedCaller.selector); + RewardsSubmission[] memory rewardsSubmissions; + rewardsCoordinator.createEigenDARewardsSubmission(address(defaultAVS), rewardsSubmissions); + } + + // Test that emissionsController can successfully call + function test_EmissionsControllerCanCall() public { + address emissionsController = address(vm.addr(0x123123ff)); + address avs = defaultAVS; + + // Setup token and approval - AVS needs to have the tokens since it's the one submitting rewards + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + uint amount = 1e18; + + RewardsSubmission[] memory rewardsSubmissions = new RewardsSubmission[](1); + rewardsSubmissions[0] = RewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + amount: amount, + startTimestamp: uint32(block.timestamp), + duration: CALCULATION_INTERVAL_SECONDS + }); + + // AVS approves the token transfer + cheats.prank(avs); + rewardToken.approve(address(rewardsCoordinator), amount); + + // Call from emissionsController should succeed + cheats.prank(emissionsController); + rewardsCoordinator.createEigenDARewardsSubmission(avs, rewardsSubmissions); + + // Verify the rewards submission was created + assertEq(rewardToken.balanceOf(address(rewardsCoordinator)), amount); + } +} + contract RewardsCoordinatorUnitTests_createRewardsForAllSubmission is RewardsCoordinatorUnitTests { // Revert when paused function test_Revert_WhenPaused() public {