From e945d8d37b16bef70081f9764d52eab294acd3b0 Mon Sep 17 00:00:00 2001 From: eigenmikem Date: Thu, 9 Jan 2025 13:05:52 -0500 Subject: [PATCH 1/3] Slashing integration tests (#1003) * Validate that users who are slashed fully can redeposit once undelegated * removing arraylib use * test for updating eigenpod state -> slash/undelegate * removing unnecessary logging function * fixing strategy set * beacon chain tests in progress --------- Co-authored-by: Michael --- src/test/integration/IntegrationBase.t.sol | 62 +++++- src/test/integration/IntegrationChecks.t.sol | 50 +++++ ...egate_Allocate_Slash_Queue_Redeposit.t.sol | 97 ++++++++++ .../integration/tests/Slashing_Combined.sol | 177 ++++++++++++++++++ .../tests/WithdrawalSlashing_Combined.sol | 124 ++++++++++++ 5 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol create mode 100644 src/test/integration/tests/Slashing_Combined.sol create mode 100644 src/test/integration/tests/WithdrawalSlashing_Combined.sol diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 852352e91..1df949a05 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -186,6 +186,36 @@ abstract contract IntegrationBase is IntegrationDeployer { return result; } + + /// @dev Choose a random subset of validators (selects AT LEAST ONE but NOT ALL) + function _chooseSubset(uint40[] memory validators) internal returns (uint40[] memory) { + require(validators.length >= 2, "Need at least 2 validators to choose subset"); + + uint40[] memory result = new uint40[](validators.length); + uint newLen; + + uint rand = _randUint({ min: 1, max: validators.length ** 2 }); + for (uint i = 0; i < validators.length; i++) { + if (rand >> i & 1 == 1) { + result[newLen] = validators[i]; + newLen++; + } + } + + // If we picked all, remove one random validator + if (newLen == validators.length) { + uint indexToRemove = _randUint({ min: 0, max: validators.length - 1 }); + for (uint i = indexToRemove; i < newLen - 1; i++) { + result[i] = result[i + 1]; + } + newLen--; + } + + // Update array length + assembly { mstore(result, newLen) } + + return result; + } function _getTokenName(IERC20 token) internal view returns (string memory) { if (token == NATIVE_ETH) { @@ -561,8 +591,11 @@ abstract contract IntegrationBase is IntegrationDeployer { uint wadToSlash = slashingParams.wadsToSlash[slashingParams.strategies.indexOf(strat)]; slashedShares = prevShares[i].mulWadRoundUp(allocateParams.newMagnitudes[i].mulWadRoundUp(wadToSlash)); } + console.log(prevShares[i]); + console.log(slashedShares); + console.log(curShares[i]); - assertApproxEqAbs(prevShares[i] - slashedShares, curShares[i], 1, err); + assertApproxEqAbs(prevShares[i] - slashedShares, curShares[i], 1000, err); } } @@ -1133,6 +1166,21 @@ abstract contract IntegrationBase is IntegrationDeployer { return (strategies.sort(), wadsToSlash); } + + function _strategiesAndWadsForFullSlash( + OperatorSet memory operatorSet + ) internal view returns (IStrategy[] memory strategies, uint[] memory wadsToSlash) { + // Get list of all strategies in an operator set. + strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); + + wadsToSlash = new uint[](strategies.length); + + for (uint i; i < strategies.length; ++i) { + wadsToSlash[i] = 1 ether; + } + + return (strategies.sort(), wadsToSlash); + } function _randMagnitudes(uint64 sum, uint256 len) internal returns (uint64[] memory magnitudes) { magnitudes = new uint64[](len); @@ -1151,6 +1199,18 @@ abstract contract IntegrationBase is IntegrationDeployer { } } + function _maxMagnitudes(OperatorSet memory operatorSet, User operator) internal view returns (uint64[] memory magnitudes) { + IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); + uint256 len = strategies.length; + magnitudes = new uint64[](len); + + if (len == 0) return magnitudes; + + for (uint256 i; i < len; ++i) { + magnitudes[i] = allocationManager.getMaxMagnitude(address(operator), strategies[i]); + } + } + function _randWithdrawal( IStrategy[] memory strategies, uint[] memory shares diff --git a/src/test/integration/IntegrationChecks.t.sol b/src/test/integration/IntegrationChecks.t.sol index c01db4e72..cd0ce115d 100644 --- a/src/test/integration/IntegrationChecks.t.sol +++ b/src/test/integration/IntegrationChecks.t.sol @@ -390,6 +390,56 @@ contract IntegrationCheckUtils is IntegrationBase { } } + function check_Withdrawal_AsTokens_State_AfterBeaconSlash( + User staker, + User operator, + IDelegationManagerTypes.Withdrawal memory withdrawal, + IAllocationManagerTypes.AllocateParams memory allocateParams, + IAllocationManagerTypes.SlashingParams memory slashingParams, + uint[] memory expectedTokens + ) internal { + IERC20[] memory tokens = new IERC20[](withdrawal.strategies.length); + + for (uint i; i < withdrawal.strategies.length; i++) { + IStrategy strat = withdrawal.strategies[i]; + + bool isBeaconChainETHStrategy = strat == beaconChainETHStrategy; + + tokens[i] = isBeaconChainETHStrategy ? NATIVE_ETH : withdrawal.strategies[i].underlyingToken(); + + if (slashingParams.strategies.contains(strat)) { + uint wadToSlash = slashingParams.wadsToSlash[slashingParams.strategies.indexOf(strat)]; + + expectedTokens[i] -= expectedTokens[i] + .mulWadRoundUp(allocateParams.newMagnitudes[i].mulWadRoundUp(wadToSlash)); + + uint256 max = allocationManager.getMaxMagnitude(address(operator), strat); + + withdrawal.scaledShares[i] -= withdrawal.scaledShares[i].calcSlashedAmount(WAD, max); + + // Round down to the nearest gwei for beaconchain ETH strategy. + if (isBeaconChainETHStrategy) { + expectedTokens[i] -= expectedTokens[i] % 1 gwei; + } + } + } + + // Common checks + assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending"); + + // assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); + assert_Snap_Unchanged_StakerDepositShares(staker, "staker shares should not have changed"); + assert_Snap_Removed_StrategyShares(withdrawal.strategies, withdrawal.scaledShares, "strategies should have total shares decremented"); + + // Checks specific to an operator that the Staker has delegated to + if (operator != User(payable(0))) { + if (operator != staker) { + assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed"); + } + assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); + } + } + function check_Withdrawal_AsShares_State_AfterSlash( User staker, User operator, diff --git a/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol b/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol new file mode 100644 index 000000000..fcfc65b44 --- /dev/null +++ b/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/IntegrationChecks.t.sol"; +import "src/test/integration/users/User.t.sol"; +import {console} from "forge-std/console.sol"; + +contract Integration_Deposit_Delegate_Allocate_Slash_Queue_Redeposit is IntegrationCheckUtils, IDelegationManagerTypes { + + function testFuzz_deposit_delegate_allocate_fullSlash_queue_complete_redeposit( + uint24 _random + ) public { + _configRand({_randomSeed: _random, _assetTypes: HOLDS_LST, _userTypes: DEFAULT}); + _upgradeEigenLayerContracts(); + + (User staker, IStrategy[] memory strategies, uint256[] memory tokenBalances) = _newRandomStaker(); + (User operator,,) = _newRandomOperator(); + (AVS avs,) = _newRandomAVS(); + + uint256[] memory tokensToDeposit = new uint256[](tokenBalances.length); + uint256[] memory numTokensRemaining = new uint256[](tokenBalances.length); + for (uint256 i = 0; i < tokenBalances.length; i++) { + tokensToDeposit[i] = tokenBalances[i]/2; + numTokensRemaining[i] = tokenBalances[i] - tokensToDeposit[i]; + } + + uint256[] memory shares = _calculateExpectedShares(strategies, tokensToDeposit); + + // 1. Deposit Into Strategies + staker.depositIntoEigenlayer(strategies, tokensToDeposit); + check_Deposit_State_PartialDeposit(staker, strategies, shares, numTokensRemaining); + + // 2. Delegate to operator + staker.delegateTo(operator); + check_Delegation_State(staker, operator, strategies, shares); + + // Create operator set and register operator + OperatorSet memory operatorSet = avs.createOperatorSet(strategies); + operator.registerForOperatorSet(operatorSet); + + // 3. Allocate to operator set + IAllocationManagerTypes.AllocateParams memory allocateParams = + operator.modifyAllocations(operatorSet, _maxMagnitudes(operatorSet, operator)); + + assert_Snap_Allocations_Modified( + operator, + allocateParams, + false, + "operator allocations should be updated before delay" + ); + + _rollBlocksForCompleteAllocation(operator, operatorSet, strategies); + + assert_Snap_Allocations_Modified( + operator, + allocateParams, + true, + "operator allocations should be updated after delay" + ); + + // 4. Fully slash operator + IAllocationManagerTypes.SlashingParams memory slashingParams; + { + (IStrategy[] memory strategiesToSlash, uint256[] memory wadsToSlash) = + _strategiesAndWadsForFullSlash(operatorSet); + + slashingParams = avs.slashOperator(operator, operatorSet.id, strategiesToSlash, wadsToSlash); + assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed"); + assert_Snap_Unchanged_StakerDepositShares(staker, "staker deposit shares should be unchanged after slashing"); + assert_Snap_StakerWithdrawableShares_AfterSlash(staker, allocateParams, slashingParams, "staker deposit shares should be slashed"); + } + + // 5. Undelegate from an operator + IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.undelegate(); + bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); + + // 6. Complete withdrawal + _rollBlocksForCompleteWithdrawals(withdrawals); + for (uint256 i = 0; i < withdrawals.length; ++i) { + uint256[] memory expectedTokens = + _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares); + for (uint256 i = 0; i < expectedTokens.length; i++) { + } + staker.completeWithdrawalAsTokens(withdrawals[i]); + check_Withdrawal_AsTokens_State_AfterSlash(staker, operator, withdrawals[i], allocateParams, slashingParams, expectedTokens); + } + + // 7. Redeposit + shares = _calculateExpectedShares(strategies, numTokensRemaining); + staker.depositIntoEigenlayer(strategies, numTokensRemaining); + check_Deposit_State(staker, strategies, shares); + + // Final state checks + assert_HasExpectedShares(staker, strategies, shares, "staker should have expected shares after redeposit"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } +} \ No newline at end of file diff --git a/src/test/integration/tests/Slashing_Combined.sol b/src/test/integration/tests/Slashing_Combined.sol new file mode 100644 index 000000000..fff647ba4 --- /dev/null +++ b/src/test/integration/tests/Slashing_Combined.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/IntegrationChecks.t.sol"; +import "src/test/integration/users/User.t.sol"; + +contract Integration_Slashing_Combined is IntegrationCheckUtils { + // Helper struct to reduce stack variables + struct TestContext { + User staker; + User operator; + AVS avs; + IStrategy[] strategies; + uint256[] tokenBalances; + uint40[] validators; + OperatorSet operatorSet; + IAllocationManagerTypes.AllocateParams allocateParams; + IAllocationManagerTypes.SlashingParams slashingParams; + } + + function testFuzz_deposit_slashBeacon_delegate_slashEigen_withdraw(uint24 _random) public { + TestContext memory ctx; + + // Initial setup and configuration + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_ETH, + _userTypes: DEFAULT + }); + + _upgradeEigenLayerContracts(); + + // Initialize actors and store in context + (ctx.staker, ctx.strategies, ctx.tokenBalances) = _newRandomStaker(); + (ctx.operator,,) = _newRandomOperator(); + (ctx.avs,) = _newRandomAVS(); + + // Handle validator setup and slashing + _handleValidatorSetupAndSlashing(ctx); + + // Handle delegation and operator setup + _handleDelegationAndOperatorSetup(ctx); + + // Handle EigenLayer slashing + _handleEigenLayerSlashing(ctx); + + // Handle withdrawal + _handleWithdrawal(ctx); + } + + function _handleValidatorSetupAndSlashing(TestContext memory ctx) internal { + // Create and verify validators + (ctx.validators,) = ctx.staker.startValidators(); + beaconChain.advanceEpoch_NoRewards(); + ctx.staker.verifyWithdrawalCredentials(ctx.validators); + + // Slash validators and record via checkpoint + uint40[] memory slashedValidators = _chooseSubset(ctx.validators); + uint64 slashedGwei = beaconChain.slashValidators(slashedValidators); + beaconChain.advanceEpoch_NoRewards(); + + ctx.staker.startCheckpoint(); + console.log("Active validator count after starting checkpoint:", ctx.staker.pod().activeValidatorCount()); + ctx.staker.completeCheckpoint(); + console.log("Active validator count after completing checkpoint:", ctx.staker.pod().activeValidatorCount()); + check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(ctx.staker, slashedValidators, slashedGwei); + } + + function _handleDelegationAndOperatorSetup(TestContext memory ctx) internal { + // Handle delegation + ctx.staker.delegateTo(ctx.operator); + check_Delegation_State( + ctx.staker, + ctx.operator, + ctx.strategies, + _getStakerDepositShares(ctx.staker, ctx.strategies) + ); + + // Setup operator set + ctx.operatorSet = ctx.avs.createOperatorSet(ctx.strategies); + ctx.operator.registerForOperatorSet(ctx.operatorSet); + + ctx.allocateParams = ctx.operator.modifyAllocations( + ctx.operatorSet, + _randMagnitudes({sum: 1 ether, len: ctx.strategies.length}) + ); + _rollBlocksForCompleteAllocation(ctx.operator, ctx.operatorSet, ctx.strategies); + } + + function _handleEigenLayerSlashing(TestContext memory ctx) internal { + (IStrategy[] memory strategiesToSlash, uint256[] memory wadsToSlash) = + _randStrategiesAndWadsToSlash(ctx.operatorSet); + + ctx.slashingParams = ctx.avs.slashOperator( + ctx.operator, + ctx.operatorSet.id, + strategiesToSlash, + wadsToSlash + ); + + assert_Snap_Allocations_Slashed( + ctx.slashingParams, + ctx.operatorSet, + true, + "operator allocations should be slashed" + ); + assert_Snap_Unchanged_StakerDepositShares( + ctx.staker, + "staker deposit shares should be unchanged after slashing" + ); + assert_Snap_StakerWithdrawableShares_AfterSlash( + ctx.staker, + ctx.allocateParams, + ctx.slashingParams, + "staker deposit shares should be slashed" + ); + } + + function _handleWithdrawal(TestContext memory ctx) internal { + EigenPod pod = ctx.staker.pod(); + console.log("Before checkpoint:"); + console.log("Restaked execution layer gwei:", pod.withdrawableRestakedExecutionLayerGwei()); + console.log("Total shares:", eigenPodManager.stakerDepositShares(address(ctx.staker), beaconChainETHStrategy)); + + // Advance beacon chain epoch + beaconChain.advanceEpoch_NoRewards(); + + // checkpoint + //ctx.staker.startCheckpoint(); + //ctx.staker.completeCheckpoint(); + + console.log("After checkpoint:"); + console.log("Restaked execution layer gwei:", pod.withdrawableRestakedExecutionLayerGwei()); + console.log("Total shares:", eigenPodManager.stakerDepositShares(address(ctx.staker), beaconChainETHStrategy)); + + // Queue withdrawal + IDelegationManagerTypes.Withdrawal[] memory withdrawals = ctx.staker.undelegate(); + bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); + + console.log("After undelegate:"); + console.log("withdrawals length:", withdrawals.length); + for (uint i = 0; i < withdrawals.length; i++) { + console.log("Is pending?", delegationManager.pendingWithdrawals(withdrawalRoots[i])); + } + + // Complete withdrawals after delay + _rollBlocksForCompleteWithdrawals(withdrawals); + + for (uint256 i = 0; i < withdrawals.length; i++) { + console.log("Attempting to complete withdrawal", i); + console.log("Is pending before completion?", delegationManager.pendingWithdrawals(withdrawalRoots[i])); + + uint[] memory expectedTokens = _calculateExpectedTokens( + withdrawals[i].strategies, + withdrawals[i].scaledShares + ); + + for (uint256 i = 0; i < expectedTokens.length; i++) { + console.log(expectedTokens[i]); + } + IERC20[] memory tokens = ctx.staker.completeWithdrawalAsTokens(withdrawals[i]); + check_Withdrawal_AsTokens_State_AfterSlash( + ctx.staker, + ctx.operator, + withdrawals[i], + ctx.allocateParams, + ctx.slashingParams, + expectedTokens + ); + } + + // Final state checks + assert_HasNoDelegatableShares(ctx.staker, "staker should have withdrawn all shares"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be completed"); + } + +} \ No newline at end of file diff --git a/src/test/integration/tests/WithdrawalSlashing_Combined.sol b/src/test/integration/tests/WithdrawalSlashing_Combined.sol new file mode 100644 index 000000000..b91fb550b --- /dev/null +++ b/src/test/integration/tests/WithdrawalSlashing_Combined.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/IntegrationChecks.t.sol"; +import "src/test/integration/users/User.t.sol"; + +contract Integration_WithdrawalSlashing_Combined is IntegrationCheckUtils { + // Helper struct to reduce stack variables + struct TestContext { + User staker; + User operator; + IStrategy[] strategies; + uint256[] tokenBalances; + uint40[] validators; + bytes32[] withdrawalRoots; + IDelegationManagerTypes.Withdrawal[] withdrawals; + uint64 slashedGwei; + } + function testFuzz_deposit_delegate_queueWithdrawal_slashBeacon_checkpoint(uint24 _random) public { + TestContext memory ctx; + + // Initial setup and configuration + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_ETH, + _userTypes: DEFAULT + }); + + _upgradeEigenLayerContracts(); + + // Initialize actors and store in context + (ctx.staker, ctx.strategies, ctx.tokenBalances) = _newRandomStaker(); + (ctx.operator,,) = _newRandomOperator(); + + // Handle validator setup and delegation + _handleValidatorSetupAndDelegation(ctx); + + // Queue withdrawal before slashing + _handleQueueWithdrawal(ctx); + + // Execute slashing while withdrawal is in queue + _handleBeaconChainSlashing(ctx); + + // Start a checkpoint to reflect slashing + _handlePostSlashingCheckpoint(ctx); + + // Complete the withdrawal and verify slashing was applied + _handleWithdrawalCompletion(ctx); + } + + function _handleValidatorSetupAndDelegation(TestContext memory ctx) internal { + // Create and verify validators + (ctx.validators,) = ctx.staker.startValidators(); + beaconChain.advanceEpoch_NoRewards(); + ctx.staker.verifyWithdrawalCredentials(ctx.validators); + + // Delegate to operator + ctx.staker.delegateTo(ctx.operator); + check_Delegation_State( + ctx.staker, + ctx.operator, + ctx.strategies, + _getStakerDepositShares(ctx.staker, ctx.strategies) + ); + } + + function _handleQueueWithdrawal(TestContext memory ctx) internal { + // Queue withdrawal by undelegating + ctx.withdrawals = ctx.staker.undelegate(); + ctx.withdrawalRoots = _getWithdrawalHashes(ctx.withdrawals); + + // Verify withdrawal state + assert_AllWithdrawalsPending(ctx.withdrawalRoots, "withdrawals should be pending"); + assert_ValidWithdrawalHashes(ctx.withdrawals, ctx.withdrawalRoots, "withdrawal hashes should be valid"); + } + + function _handleBeaconChainSlashing(TestContext memory ctx) internal { + // Choose subset of validators to slash + uint40[] memory slashedValidators = _chooseSubset(ctx.validators); + + // Execute slashing on beacon chain + ctx.slashedGwei = beaconChain.slashValidators(slashedValidators); + beaconChain.advanceEpoch_NoRewards(); + + console.log("Slashed amount (gwei)", ctx.slashedGwei); + } + + function _handlePostSlashingCheckpoint(TestContext memory ctx) internal { + // Start and complete checkpoint to reflect slashing + ctx.staker.startCheckpoint(); + ctx.staker.completeCheckpoint(); + console.log("Active validator count after completing checkpoint:", ctx.staker.pod().activeValidatorCount()); + + } + + function _handleWithdrawalCompletion(TestContext memory ctx) internal { + // Advance blocks to complete withdrawal + _rollBlocksForCompleteWithdrawals(ctx.withdrawals); + + // Complete each withdrawal and verify state + for (uint i = 0; i < ctx.withdrawals.length; i++) { + uint[] memory expectedTokens = _calculateExpectedTokens( + ctx.withdrawals[i].strategies, + ctx.withdrawals[i].scaledShares + ); + + IERC20[] memory tokens = ctx.staker.completeWithdrawalAsTokens(ctx.withdrawals[i]); + + check_Withdrawal_AsTokens_State( + ctx.staker, + ctx.operator, + ctx.withdrawals[i], + ctx.withdrawals[i].strategies, + ctx.withdrawals[i].scaledShares, + tokens, + expectedTokens + ); + } + + // Final checks + assert_HasNoDelegatableShares(ctx.staker, "staker should have no shares after withdrawal"); + assert_NoWithdrawalsPending(ctx.withdrawalRoots, "all withdrawals should be completed"); + } +} \ No newline at end of file From 84dbd2fc5ca3ee5c01e5ecafb0852431f1d90972 Mon Sep 17 00:00:00 2001 From: eigenmikem Date: Thu, 9 Jan 2025 13:07:17 -0500 Subject: [PATCH 2/3] Revert "Slashing integration tests (#1003)" (#1007) This reverts commit e945d8d37b16bef70081f9764d52eab294acd3b0. --- src/test/integration/IntegrationBase.t.sol | 62 +----- src/test/integration/IntegrationChecks.t.sol | 50 ----- ...egate_Allocate_Slash_Queue_Redeposit.t.sol | 97 ---------- .../integration/tests/Slashing_Combined.sol | 177 ------------------ .../tests/WithdrawalSlashing_Combined.sol | 124 ------------ 5 files changed, 1 insertion(+), 509 deletions(-) delete mode 100644 src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol delete mode 100644 src/test/integration/tests/Slashing_Combined.sol delete mode 100644 src/test/integration/tests/WithdrawalSlashing_Combined.sol diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 1df949a05..852352e91 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -186,36 +186,6 @@ abstract contract IntegrationBase is IntegrationDeployer { return result; } - - /// @dev Choose a random subset of validators (selects AT LEAST ONE but NOT ALL) - function _chooseSubset(uint40[] memory validators) internal returns (uint40[] memory) { - require(validators.length >= 2, "Need at least 2 validators to choose subset"); - - uint40[] memory result = new uint40[](validators.length); - uint newLen; - - uint rand = _randUint({ min: 1, max: validators.length ** 2 }); - for (uint i = 0; i < validators.length; i++) { - if (rand >> i & 1 == 1) { - result[newLen] = validators[i]; - newLen++; - } - } - - // If we picked all, remove one random validator - if (newLen == validators.length) { - uint indexToRemove = _randUint({ min: 0, max: validators.length - 1 }); - for (uint i = indexToRemove; i < newLen - 1; i++) { - result[i] = result[i + 1]; - } - newLen--; - } - - // Update array length - assembly { mstore(result, newLen) } - - return result; - } function _getTokenName(IERC20 token) internal view returns (string memory) { if (token == NATIVE_ETH) { @@ -591,11 +561,8 @@ abstract contract IntegrationBase is IntegrationDeployer { uint wadToSlash = slashingParams.wadsToSlash[slashingParams.strategies.indexOf(strat)]; slashedShares = prevShares[i].mulWadRoundUp(allocateParams.newMagnitudes[i].mulWadRoundUp(wadToSlash)); } - console.log(prevShares[i]); - console.log(slashedShares); - console.log(curShares[i]); - assertApproxEqAbs(prevShares[i] - slashedShares, curShares[i], 1000, err); + assertApproxEqAbs(prevShares[i] - slashedShares, curShares[i], 1, err); } } @@ -1166,21 +1133,6 @@ abstract contract IntegrationBase is IntegrationDeployer { return (strategies.sort(), wadsToSlash); } - - function _strategiesAndWadsForFullSlash( - OperatorSet memory operatorSet - ) internal view returns (IStrategy[] memory strategies, uint[] memory wadsToSlash) { - // Get list of all strategies in an operator set. - strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); - - wadsToSlash = new uint[](strategies.length); - - for (uint i; i < strategies.length; ++i) { - wadsToSlash[i] = 1 ether; - } - - return (strategies.sort(), wadsToSlash); - } function _randMagnitudes(uint64 sum, uint256 len) internal returns (uint64[] memory magnitudes) { magnitudes = new uint64[](len); @@ -1199,18 +1151,6 @@ abstract contract IntegrationBase is IntegrationDeployer { } } - function _maxMagnitudes(OperatorSet memory operatorSet, User operator) internal view returns (uint64[] memory magnitudes) { - IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); - uint256 len = strategies.length; - magnitudes = new uint64[](len); - - if (len == 0) return magnitudes; - - for (uint256 i; i < len; ++i) { - magnitudes[i] = allocationManager.getMaxMagnitude(address(operator), strategies[i]); - } - } - function _randWithdrawal( IStrategy[] memory strategies, uint[] memory shares diff --git a/src/test/integration/IntegrationChecks.t.sol b/src/test/integration/IntegrationChecks.t.sol index cd0ce115d..c01db4e72 100644 --- a/src/test/integration/IntegrationChecks.t.sol +++ b/src/test/integration/IntegrationChecks.t.sol @@ -390,56 +390,6 @@ contract IntegrationCheckUtils is IntegrationBase { } } - function check_Withdrawal_AsTokens_State_AfterBeaconSlash( - User staker, - User operator, - IDelegationManagerTypes.Withdrawal memory withdrawal, - IAllocationManagerTypes.AllocateParams memory allocateParams, - IAllocationManagerTypes.SlashingParams memory slashingParams, - uint[] memory expectedTokens - ) internal { - IERC20[] memory tokens = new IERC20[](withdrawal.strategies.length); - - for (uint i; i < withdrawal.strategies.length; i++) { - IStrategy strat = withdrawal.strategies[i]; - - bool isBeaconChainETHStrategy = strat == beaconChainETHStrategy; - - tokens[i] = isBeaconChainETHStrategy ? NATIVE_ETH : withdrawal.strategies[i].underlyingToken(); - - if (slashingParams.strategies.contains(strat)) { - uint wadToSlash = slashingParams.wadsToSlash[slashingParams.strategies.indexOf(strat)]; - - expectedTokens[i] -= expectedTokens[i] - .mulWadRoundUp(allocateParams.newMagnitudes[i].mulWadRoundUp(wadToSlash)); - - uint256 max = allocationManager.getMaxMagnitude(address(operator), strat); - - withdrawal.scaledShares[i] -= withdrawal.scaledShares[i].calcSlashedAmount(WAD, max); - - // Round down to the nearest gwei for beaconchain ETH strategy. - if (isBeaconChainETHStrategy) { - expectedTokens[i] -= expectedTokens[i] % 1 gwei; - } - } - } - - // Common checks - assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending"); - - // assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); - assert_Snap_Unchanged_StakerDepositShares(staker, "staker shares should not have changed"); - assert_Snap_Removed_StrategyShares(withdrawal.strategies, withdrawal.scaledShares, "strategies should have total shares decremented"); - - // Checks specific to an operator that the Staker has delegated to - if (operator != User(payable(0))) { - if (operator != staker) { - assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed"); - } - assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); - } - } - function check_Withdrawal_AsShares_State_AfterSlash( User staker, User operator, diff --git a/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol b/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol deleted file mode 100644 index fcfc65b44..000000000 --- a/src/test/integration/tests/Deposit_Delegate_Allocate_Slash_Queue_Redeposit.t.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "src/test/integration/IntegrationChecks.t.sol"; -import "src/test/integration/users/User.t.sol"; -import {console} from "forge-std/console.sol"; - -contract Integration_Deposit_Delegate_Allocate_Slash_Queue_Redeposit is IntegrationCheckUtils, IDelegationManagerTypes { - - function testFuzz_deposit_delegate_allocate_fullSlash_queue_complete_redeposit( - uint24 _random - ) public { - _configRand({_randomSeed: _random, _assetTypes: HOLDS_LST, _userTypes: DEFAULT}); - _upgradeEigenLayerContracts(); - - (User staker, IStrategy[] memory strategies, uint256[] memory tokenBalances) = _newRandomStaker(); - (User operator,,) = _newRandomOperator(); - (AVS avs,) = _newRandomAVS(); - - uint256[] memory tokensToDeposit = new uint256[](tokenBalances.length); - uint256[] memory numTokensRemaining = new uint256[](tokenBalances.length); - for (uint256 i = 0; i < tokenBalances.length; i++) { - tokensToDeposit[i] = tokenBalances[i]/2; - numTokensRemaining[i] = tokenBalances[i] - tokensToDeposit[i]; - } - - uint256[] memory shares = _calculateExpectedShares(strategies, tokensToDeposit); - - // 1. Deposit Into Strategies - staker.depositIntoEigenlayer(strategies, tokensToDeposit); - check_Deposit_State_PartialDeposit(staker, strategies, shares, numTokensRemaining); - - // 2. Delegate to operator - staker.delegateTo(operator); - check_Delegation_State(staker, operator, strategies, shares); - - // Create operator set and register operator - OperatorSet memory operatorSet = avs.createOperatorSet(strategies); - operator.registerForOperatorSet(operatorSet); - - // 3. Allocate to operator set - IAllocationManagerTypes.AllocateParams memory allocateParams = - operator.modifyAllocations(operatorSet, _maxMagnitudes(operatorSet, operator)); - - assert_Snap_Allocations_Modified( - operator, - allocateParams, - false, - "operator allocations should be updated before delay" - ); - - _rollBlocksForCompleteAllocation(operator, operatorSet, strategies); - - assert_Snap_Allocations_Modified( - operator, - allocateParams, - true, - "operator allocations should be updated after delay" - ); - - // 4. Fully slash operator - IAllocationManagerTypes.SlashingParams memory slashingParams; - { - (IStrategy[] memory strategiesToSlash, uint256[] memory wadsToSlash) = - _strategiesAndWadsForFullSlash(operatorSet); - - slashingParams = avs.slashOperator(operator, operatorSet.id, strategiesToSlash, wadsToSlash); - assert_Snap_Allocations_Slashed(slashingParams, operatorSet, true, "operator allocations should be slashed"); - assert_Snap_Unchanged_StakerDepositShares(staker, "staker deposit shares should be unchanged after slashing"); - assert_Snap_StakerWithdrawableShares_AfterSlash(staker, allocateParams, slashingParams, "staker deposit shares should be slashed"); - } - - // 5. Undelegate from an operator - IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.undelegate(); - bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); - - // 6. Complete withdrawal - _rollBlocksForCompleteWithdrawals(withdrawals); - for (uint256 i = 0; i < withdrawals.length; ++i) { - uint256[] memory expectedTokens = - _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].scaledShares); - for (uint256 i = 0; i < expectedTokens.length; i++) { - } - staker.completeWithdrawalAsTokens(withdrawals[i]); - check_Withdrawal_AsTokens_State_AfterSlash(staker, operator, withdrawals[i], allocateParams, slashingParams, expectedTokens); - } - - // 7. Redeposit - shares = _calculateExpectedShares(strategies, numTokensRemaining); - staker.depositIntoEigenlayer(strategies, numTokensRemaining); - check_Deposit_State(staker, strategies, shares); - - // Final state checks - assert_HasExpectedShares(staker, strategies, shares, "staker should have expected shares after redeposit"); - assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); - } -} \ No newline at end of file diff --git a/src/test/integration/tests/Slashing_Combined.sol b/src/test/integration/tests/Slashing_Combined.sol deleted file mode 100644 index fff647ba4..000000000 --- a/src/test/integration/tests/Slashing_Combined.sol +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "src/test/integration/IntegrationChecks.t.sol"; -import "src/test/integration/users/User.t.sol"; - -contract Integration_Slashing_Combined is IntegrationCheckUtils { - // Helper struct to reduce stack variables - struct TestContext { - User staker; - User operator; - AVS avs; - IStrategy[] strategies; - uint256[] tokenBalances; - uint40[] validators; - OperatorSet operatorSet; - IAllocationManagerTypes.AllocateParams allocateParams; - IAllocationManagerTypes.SlashingParams slashingParams; - } - - function testFuzz_deposit_slashBeacon_delegate_slashEigen_withdraw(uint24 _random) public { - TestContext memory ctx; - - // Initial setup and configuration - _configRand({ - _randomSeed: _random, - _assetTypes: HOLDS_ETH, - _userTypes: DEFAULT - }); - - _upgradeEigenLayerContracts(); - - // Initialize actors and store in context - (ctx.staker, ctx.strategies, ctx.tokenBalances) = _newRandomStaker(); - (ctx.operator,,) = _newRandomOperator(); - (ctx.avs,) = _newRandomAVS(); - - // Handle validator setup and slashing - _handleValidatorSetupAndSlashing(ctx); - - // Handle delegation and operator setup - _handleDelegationAndOperatorSetup(ctx); - - // Handle EigenLayer slashing - _handleEigenLayerSlashing(ctx); - - // Handle withdrawal - _handleWithdrawal(ctx); - } - - function _handleValidatorSetupAndSlashing(TestContext memory ctx) internal { - // Create and verify validators - (ctx.validators,) = ctx.staker.startValidators(); - beaconChain.advanceEpoch_NoRewards(); - ctx.staker.verifyWithdrawalCredentials(ctx.validators); - - // Slash validators and record via checkpoint - uint40[] memory slashedValidators = _chooseSubset(ctx.validators); - uint64 slashedGwei = beaconChain.slashValidators(slashedValidators); - beaconChain.advanceEpoch_NoRewards(); - - ctx.staker.startCheckpoint(); - console.log("Active validator count after starting checkpoint:", ctx.staker.pod().activeValidatorCount()); - ctx.staker.completeCheckpoint(); - console.log("Active validator count after completing checkpoint:", ctx.staker.pod().activeValidatorCount()); - check_CompleteCheckpoint_WithSlashing_HandleRoundDown_State(ctx.staker, slashedValidators, slashedGwei); - } - - function _handleDelegationAndOperatorSetup(TestContext memory ctx) internal { - // Handle delegation - ctx.staker.delegateTo(ctx.operator); - check_Delegation_State( - ctx.staker, - ctx.operator, - ctx.strategies, - _getStakerDepositShares(ctx.staker, ctx.strategies) - ); - - // Setup operator set - ctx.operatorSet = ctx.avs.createOperatorSet(ctx.strategies); - ctx.operator.registerForOperatorSet(ctx.operatorSet); - - ctx.allocateParams = ctx.operator.modifyAllocations( - ctx.operatorSet, - _randMagnitudes({sum: 1 ether, len: ctx.strategies.length}) - ); - _rollBlocksForCompleteAllocation(ctx.operator, ctx.operatorSet, ctx.strategies); - } - - function _handleEigenLayerSlashing(TestContext memory ctx) internal { - (IStrategy[] memory strategiesToSlash, uint256[] memory wadsToSlash) = - _randStrategiesAndWadsToSlash(ctx.operatorSet); - - ctx.slashingParams = ctx.avs.slashOperator( - ctx.operator, - ctx.operatorSet.id, - strategiesToSlash, - wadsToSlash - ); - - assert_Snap_Allocations_Slashed( - ctx.slashingParams, - ctx.operatorSet, - true, - "operator allocations should be slashed" - ); - assert_Snap_Unchanged_StakerDepositShares( - ctx.staker, - "staker deposit shares should be unchanged after slashing" - ); - assert_Snap_StakerWithdrawableShares_AfterSlash( - ctx.staker, - ctx.allocateParams, - ctx.slashingParams, - "staker deposit shares should be slashed" - ); - } - - function _handleWithdrawal(TestContext memory ctx) internal { - EigenPod pod = ctx.staker.pod(); - console.log("Before checkpoint:"); - console.log("Restaked execution layer gwei:", pod.withdrawableRestakedExecutionLayerGwei()); - console.log("Total shares:", eigenPodManager.stakerDepositShares(address(ctx.staker), beaconChainETHStrategy)); - - // Advance beacon chain epoch - beaconChain.advanceEpoch_NoRewards(); - - // checkpoint - //ctx.staker.startCheckpoint(); - //ctx.staker.completeCheckpoint(); - - console.log("After checkpoint:"); - console.log("Restaked execution layer gwei:", pod.withdrawableRestakedExecutionLayerGwei()); - console.log("Total shares:", eigenPodManager.stakerDepositShares(address(ctx.staker), beaconChainETHStrategy)); - - // Queue withdrawal - IDelegationManagerTypes.Withdrawal[] memory withdrawals = ctx.staker.undelegate(); - bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); - - console.log("After undelegate:"); - console.log("withdrawals length:", withdrawals.length); - for (uint i = 0; i < withdrawals.length; i++) { - console.log("Is pending?", delegationManager.pendingWithdrawals(withdrawalRoots[i])); - } - - // Complete withdrawals after delay - _rollBlocksForCompleteWithdrawals(withdrawals); - - for (uint256 i = 0; i < withdrawals.length; i++) { - console.log("Attempting to complete withdrawal", i); - console.log("Is pending before completion?", delegationManager.pendingWithdrawals(withdrawalRoots[i])); - - uint[] memory expectedTokens = _calculateExpectedTokens( - withdrawals[i].strategies, - withdrawals[i].scaledShares - ); - - for (uint256 i = 0; i < expectedTokens.length; i++) { - console.log(expectedTokens[i]); - } - IERC20[] memory tokens = ctx.staker.completeWithdrawalAsTokens(withdrawals[i]); - check_Withdrawal_AsTokens_State_AfterSlash( - ctx.staker, - ctx.operator, - withdrawals[i], - ctx.allocateParams, - ctx.slashingParams, - expectedTokens - ); - } - - // Final state checks - assert_HasNoDelegatableShares(ctx.staker, "staker should have withdrawn all shares"); - assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be completed"); - } - -} \ No newline at end of file diff --git a/src/test/integration/tests/WithdrawalSlashing_Combined.sol b/src/test/integration/tests/WithdrawalSlashing_Combined.sol deleted file mode 100644 index b91fb550b..000000000 --- a/src/test/integration/tests/WithdrawalSlashing_Combined.sol +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.27; - -import "src/test/integration/IntegrationChecks.t.sol"; -import "src/test/integration/users/User.t.sol"; - -contract Integration_WithdrawalSlashing_Combined is IntegrationCheckUtils { - // Helper struct to reduce stack variables - struct TestContext { - User staker; - User operator; - IStrategy[] strategies; - uint256[] tokenBalances; - uint40[] validators; - bytes32[] withdrawalRoots; - IDelegationManagerTypes.Withdrawal[] withdrawals; - uint64 slashedGwei; - } - function testFuzz_deposit_delegate_queueWithdrawal_slashBeacon_checkpoint(uint24 _random) public { - TestContext memory ctx; - - // Initial setup and configuration - _configRand({ - _randomSeed: _random, - _assetTypes: HOLDS_ETH, - _userTypes: DEFAULT - }); - - _upgradeEigenLayerContracts(); - - // Initialize actors and store in context - (ctx.staker, ctx.strategies, ctx.tokenBalances) = _newRandomStaker(); - (ctx.operator,,) = _newRandomOperator(); - - // Handle validator setup and delegation - _handleValidatorSetupAndDelegation(ctx); - - // Queue withdrawal before slashing - _handleQueueWithdrawal(ctx); - - // Execute slashing while withdrawal is in queue - _handleBeaconChainSlashing(ctx); - - // Start a checkpoint to reflect slashing - _handlePostSlashingCheckpoint(ctx); - - // Complete the withdrawal and verify slashing was applied - _handleWithdrawalCompletion(ctx); - } - - function _handleValidatorSetupAndDelegation(TestContext memory ctx) internal { - // Create and verify validators - (ctx.validators,) = ctx.staker.startValidators(); - beaconChain.advanceEpoch_NoRewards(); - ctx.staker.verifyWithdrawalCredentials(ctx.validators); - - // Delegate to operator - ctx.staker.delegateTo(ctx.operator); - check_Delegation_State( - ctx.staker, - ctx.operator, - ctx.strategies, - _getStakerDepositShares(ctx.staker, ctx.strategies) - ); - } - - function _handleQueueWithdrawal(TestContext memory ctx) internal { - // Queue withdrawal by undelegating - ctx.withdrawals = ctx.staker.undelegate(); - ctx.withdrawalRoots = _getWithdrawalHashes(ctx.withdrawals); - - // Verify withdrawal state - assert_AllWithdrawalsPending(ctx.withdrawalRoots, "withdrawals should be pending"); - assert_ValidWithdrawalHashes(ctx.withdrawals, ctx.withdrawalRoots, "withdrawal hashes should be valid"); - } - - function _handleBeaconChainSlashing(TestContext memory ctx) internal { - // Choose subset of validators to slash - uint40[] memory slashedValidators = _chooseSubset(ctx.validators); - - // Execute slashing on beacon chain - ctx.slashedGwei = beaconChain.slashValidators(slashedValidators); - beaconChain.advanceEpoch_NoRewards(); - - console.log("Slashed amount (gwei)", ctx.slashedGwei); - } - - function _handlePostSlashingCheckpoint(TestContext memory ctx) internal { - // Start and complete checkpoint to reflect slashing - ctx.staker.startCheckpoint(); - ctx.staker.completeCheckpoint(); - console.log("Active validator count after completing checkpoint:", ctx.staker.pod().activeValidatorCount()); - - } - - function _handleWithdrawalCompletion(TestContext memory ctx) internal { - // Advance blocks to complete withdrawal - _rollBlocksForCompleteWithdrawals(ctx.withdrawals); - - // Complete each withdrawal and verify state - for (uint i = 0; i < ctx.withdrawals.length; i++) { - uint[] memory expectedTokens = _calculateExpectedTokens( - ctx.withdrawals[i].strategies, - ctx.withdrawals[i].scaledShares - ); - - IERC20[] memory tokens = ctx.staker.completeWithdrawalAsTokens(ctx.withdrawals[i]); - - check_Withdrawal_AsTokens_State( - ctx.staker, - ctx.operator, - ctx.withdrawals[i], - ctx.withdrawals[i].strategies, - ctx.withdrawals[i].scaledShares, - tokens, - expectedTokens - ); - } - - // Final checks - assert_HasNoDelegatableShares(ctx.staker, "staker should have no shares after withdrawal"); - assert_NoWithdrawalsPending(ctx.withdrawalRoots, "all withdrawals should be completed"); - } -} \ No newline at end of file From 85f31ad49d0710bcd910469a618c0f733c0937fc Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:49:14 -0500 Subject: [PATCH 3/3] feat: rewards v2.1 feat: operator centric rewards feat: add new interfaces feat(wip): implement `createOperatorSetPerformanceRewardsSubmission` chore: forge fmt fix: compile feat(wip): implement `setOperatorSetOperatorSplit` fix: review changes fix: add missing `onlyWhenPaused` + `checkCanCall` feat(wip): add missing `getOperatorSetPerformanceSplit` + rename internals test(wip): `setOperatorSetPerformanceSplit` test(wip): `createOperatorSetPerformanceRewardsSubmission` - some failing chore: forge fmt refactor: renaming chore: storage report refactor: review changes refactor: review changes fix: gap refactor: review changes --- docs/storage-report/RewardsCoordinator.md | 118 +- .../RewardsCoordinatorStorage.md | 78 +- src/contracts/core/RewardsCoordinator.sol | 61 +- .../core/RewardsCoordinatorStorage.sol | 29 +- .../interfaces/IRewardsCoordinator.sol | 55 + src/test/mocks/AllocationManagerMock.sol | 15 +- src/test/unit/RewardsCoordinatorUnit.t.sol | 1114 ++++++++++++++++- 7 files changed, 1356 insertions(+), 114 deletions(-) diff --git a/docs/storage-report/RewardsCoordinator.md b/docs/storage-report/RewardsCoordinator.md index 3c14eea58..113781b56 100644 --- a/docs/storage-report/RewardsCoordinator.md +++ b/docs/storage-report/RewardsCoordinator.md @@ -1,59 +1,63 @@ -╭--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------╮ -| Name | Type | Slot | Offset | Bytes | Contract | -+===========================================================================================================================================================================================================================+ -| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| _initializing | bool | 0 | 1 | 1 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| _owner | address | 51 | 0 | 20 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| _status | uint256 | 151 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| __gap | uint256[49] | 152 | 0 | 1568 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| __deprecated_DOMAIN_SEPARATOR | bytes32 | 201 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| _distributionRoots | struct IRewardsCoordinatorTypes.DistributionRoot[] | 202 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| rewardsUpdater | address | 203 | 0 | 20 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| activationDelay | uint32 | 203 | 20 | 4 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| currRewardsCalculationEndTimestamp | uint32 | 203 | 24 | 4 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| defaultOperatorSplitBips | uint16 | 203 | 28 | 2 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| claimerFor | mapping(address => address) | 204 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| cumulativeClaimed | mapping(address => mapping(contract IERC20 => uint256)) | 205 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| submissionNonce | mapping(address => uint256) | 206 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| isAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 207 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| isRewardsSubmissionForAllHash | mapping(address => mapping(bytes32 => bool)) | 208 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| isRewardsForAllSubmitter | mapping(address => bool) | 209 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| isRewardsSubmissionForAllEarnersHash | mapping(address => mapping(bytes32 => bool)) | 210 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| isOperatorDirectedAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 211 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| operatorAVSSplitBips | mapping(address => mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit)) | 212 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| operatorPISplitBips | mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit) | 213 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| -| __gap | uint256[37] | 214 | 0 | 1184 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | -╰--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------╯ +╭-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------╮ +| Name | Type | Slot | Offset | Bytes | Contract | ++==============================================================================================================================================================================================================================+ +| _initialized | uint8 | 0 | 0 | 1 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _initializing | bool | 0 | 1 | 1 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| __gap | uint256[50] | 1 | 0 | 1600 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _owner | address | 51 | 0 | 20 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| __gap | uint256[49] | 52 | 0 | 1568 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| __deprecated_pauserRegistry | contract IPauserRegistry | 101 | 0 | 20 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _paused | uint256 | 102 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| __gap | uint256[48] | 103 | 0 | 1536 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _status | uint256 | 151 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| __gap | uint256[49] | 152 | 0 | 1568 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| __deprecated_DOMAIN_SEPARATOR | bytes32 | 201 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _distributionRoots | struct IRewardsCoordinatorTypes.DistributionRoot[] | 202 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| rewardsUpdater | address | 203 | 0 | 20 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| activationDelay | uint32 | 203 | 20 | 4 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| currRewardsCalculationEndTimestamp | uint32 | 203 | 24 | 4 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| defaultOperatorSplitBips | uint16 | 203 | 28 | 2 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| claimerFor | mapping(address => address) | 204 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| cumulativeClaimed | mapping(address => mapping(contract IERC20 => uint256)) | 205 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| submissionNonce | mapping(address => uint256) | 206 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| isAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 207 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| isRewardsSubmissionForAllHash | mapping(address => mapping(bytes32 => bool)) | 208 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| isRewardsForAllSubmitter | mapping(address => bool) | 209 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| isRewardsSubmissionForAllEarnersHash | mapping(address => mapping(bytes32 => bool)) | 210 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| isOperatorDirectedAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 211 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| isOperatorSetPerformanceRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 212 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _operatorAVSSplitBips | mapping(address => mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit)) | 213 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _operatorPISplitBips | mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit) | 214 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| _operatorOperatorSetSplitBips | mapping(address => mapping(bytes32 => struct IRewardsCoordinatorTypes.OperatorSplit)) | 215 | 0 | 32 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------| +| __gap | uint256[37] | 216 | 0 | 1184 | src/contracts/core/RewardsCoordinator.sol:RewardsCoordinator | +╰-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+--------------------------------------------------------------╯ diff --git a/docs/storage-report/RewardsCoordinatorStorage.md b/docs/storage-report/RewardsCoordinatorStorage.md index 9aa44693d..5ffb2e442 100644 --- a/docs/storage-report/RewardsCoordinatorStorage.md +++ b/docs/storage-report/RewardsCoordinatorStorage.md @@ -1,39 +1,43 @@ -╭--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------╮ -| Name | Type | Slot | Offset | Bytes | Contract | -+=========================================================================================================================================================================================================================================+ -| __deprecated_DOMAIN_SEPARATOR | bytes32 | 0 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| _distributionRoots | struct IRewardsCoordinatorTypes.DistributionRoot[] | 1 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| rewardsUpdater | address | 2 | 0 | 20 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| activationDelay | uint32 | 2 | 20 | 4 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| currRewardsCalculationEndTimestamp | uint32 | 2 | 24 | 4 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| defaultOperatorSplitBips | uint16 | 2 | 28 | 2 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| claimerFor | mapping(address => address) | 3 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| cumulativeClaimed | mapping(address => mapping(contract IERC20 => uint256)) | 4 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| submissionNonce | mapping(address => uint256) | 5 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| isAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 6 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| isRewardsSubmissionForAllHash | mapping(address => mapping(bytes32 => bool)) | 7 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| isRewardsForAllSubmitter | mapping(address => bool) | 8 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| isRewardsSubmissionForAllEarnersHash | mapping(address => mapping(bytes32 => bool)) | 9 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| isOperatorDirectedAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 10 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| operatorAVSSplitBips | mapping(address => mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit)) | 11 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| operatorPISplitBips | mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit) | 12 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -|--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| -| __gap | uint256[37] | 13 | 0 | 1184 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | -╰--------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------╯ +╭-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------╮ +| Name | Type | Slot | Offset | Bytes | Contract | ++============================================================================================================================================================================================================================================+ +| __deprecated_DOMAIN_SEPARATOR | bytes32 | 0 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| _distributionRoots | struct IRewardsCoordinatorTypes.DistributionRoot[] | 1 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| rewardsUpdater | address | 2 | 0 | 20 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| activationDelay | uint32 | 2 | 20 | 4 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| currRewardsCalculationEndTimestamp | uint32 | 2 | 24 | 4 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| defaultOperatorSplitBips | uint16 | 2 | 28 | 2 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| claimerFor | mapping(address => address) | 3 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| cumulativeClaimed | mapping(address => mapping(contract IERC20 => uint256)) | 4 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| submissionNonce | mapping(address => uint256) | 5 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| isAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 6 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| isRewardsSubmissionForAllHash | mapping(address => mapping(bytes32 => bool)) | 7 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| isRewardsForAllSubmitter | mapping(address => bool) | 8 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| isRewardsSubmissionForAllEarnersHash | mapping(address => mapping(bytes32 => bool)) | 9 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| isOperatorDirectedAVSRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 10 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| isOperatorSetPerformanceRewardsSubmissionHash | mapping(address => mapping(bytes32 => bool)) | 11 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| _operatorAVSSplitBips | mapping(address => mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit)) | 12 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| _operatorPISplitBips | mapping(address => struct IRewardsCoordinatorTypes.OperatorSplit) | 13 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| _operatorOperatorSetSplitBips | mapping(address => mapping(bytes32 => struct IRewardsCoordinatorTypes.OperatorSplit)) | 14 | 0 | 32 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +|-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------| +| __gap | uint256[37] | 15 | 0 | 1184 | src/contracts/core/RewardsCoordinatorStorage.sol:RewardsCoordinatorStorage | +╰-----------------------------------------------+---------------------------------------------------------------------------------------+------+--------+-------+----------------------------------------------------------------------------╯ diff --git a/src/contracts/core/RewardsCoordinator.sol b/src/contracts/core/RewardsCoordinator.sol index 13e2569d5..a16409951 100644 --- a/src/contracts/core/RewardsCoordinator.sol +++ b/src/contracts/core/RewardsCoordinator.sol @@ -29,6 +29,7 @@ contract RewardsCoordinator is PermissionControllerMixin { using SafeERC20 for IERC20; + using OperatorSetLib for OperatorSet; modifier onlyRewardsUpdater() { require(msg.sender == rewardsUpdater, UnauthorizedCaller()); @@ -177,6 +178,34 @@ contract RewardsCoordinator is } } + /// @inheritdoc IRewardsCoordinator + function createOperatorDirectedOperatorSetRewardsSubmission( + OperatorSet calldata operatorSet, + OperatorDirectedRewardsSubmission[] calldata rewardsSubmissions + ) + external + onlyWhenNotPaused(PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION) + checkCanCall(operatorSet.avs) + nonReentrant + { + require(allocationManager.isOperatorSet(operatorSet), InvalidOperatorSet()); + for (uint256 i = 0; i < rewardsSubmissions.length; i++) { + OperatorDirectedRewardsSubmission calldata rewardsSubmission = rewardsSubmissions[i]; + uint256 nonce = submissionNonce[operatorSet.avs]; + bytes32 rewardsSubmissionHash = keccak256(abi.encode(operatorSet.avs, nonce, rewardsSubmission)); + + uint256 totalAmount = _validateOperatorDirectedRewardsSubmission(rewardsSubmission); + + isOperatorSetPerformanceRewardsSubmissionHash[operatorSet.avs][rewardsSubmissionHash] = true; + submissionNonce[operatorSet.avs] = nonce + 1; + + emit OperatorDirectedOperatorSetRewardsSubmissionCreated( + msg.sender, operatorSet, rewardsSubmissionHash, nonce, rewardsSubmission + ); + rewardsSubmission.token.safeTransferFrom(msg.sender, address(this), totalAmount); + } + } + /// @inheritdoc IRewardsCoordinator function processClaim( RewardsMerkleClaim calldata claim, @@ -268,8 +297,8 @@ contract RewardsCoordinator is uint16 split ) external onlyWhenNotPaused(PAUSED_OPERATOR_AVS_SPLIT) checkCanCall(operator) { uint32 activatedAt = uint32(block.timestamp) + activationDelay; - uint16 oldSplit = _getOperatorSplit(operatorAVSSplitBips[operator][avs]); - _setOperatorSplit(operatorAVSSplitBips[operator][avs], split, activatedAt); + uint16 oldSplit = _getOperatorSplit(_operatorAVSSplitBips[operator][avs]); + _setOperatorSplit(_operatorAVSSplitBips[operator][avs], split, activatedAt); emit OperatorAVSSplitBipsSet(msg.sender, operator, avs, activatedAt, oldSplit, split); } @@ -280,12 +309,27 @@ contract RewardsCoordinator is uint16 split ) external onlyWhenNotPaused(PAUSED_OPERATOR_PI_SPLIT) checkCanCall(operator) { uint32 activatedAt = uint32(block.timestamp) + activationDelay; - uint16 oldSplit = _getOperatorSplit(operatorPISplitBips[operator]); - _setOperatorSplit(operatorPISplitBips[operator], split, activatedAt); + uint16 oldSplit = _getOperatorSplit(_operatorPISplitBips[operator]); + _setOperatorSplit(_operatorPISplitBips[operator], split, activatedAt); emit OperatorPISplitBipsSet(msg.sender, operator, activatedAt, oldSplit, split); } + /// @inheritdoc IRewardsCoordinator + function setOperatorSetSplit( + address operator, + OperatorSet calldata operatorSet, + uint16 split + ) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_OPERATOR_SPLIT) checkCanCall(operator) { + require(allocationManager.isOperatorSet(operatorSet), InvalidOperatorSet()); + + uint32 activatedAt = uint32(block.timestamp) + activationDelay; + uint16 oldSplit = _getOperatorSplit(_operatorOperatorSetSplitBips[operator][operatorSet.key()]); + _setOperatorSplit(_operatorOperatorSetSplitBips[operator][operatorSet.key()], split, activatedAt); + + emit OperatorOperatorSetSplitBipsSet(msg.sender, operator, operatorSet, activatedAt, oldSplit, split); + } + /// @inheritdoc IRewardsCoordinator function setRewardsUpdater( address _rewardsUpdater @@ -602,14 +646,19 @@ contract RewardsCoordinator is /// @inheritdoc IRewardsCoordinator function getOperatorAVSSplit(address operator, address avs) external view returns (uint16) { - return _getOperatorSplit(operatorAVSSplitBips[operator][avs]); + return _getOperatorSplit(_operatorAVSSplitBips[operator][avs]); } /// @inheritdoc IRewardsCoordinator function getOperatorPISplit( address operator ) external view returns (uint16) { - return _getOperatorSplit(operatorPISplitBips[operator]); + return _getOperatorSplit(_operatorPISplitBips[operator]); + } + + /// @inheritdoc IRewardsCoordinator + function getOperatorSetSplit(address operator, OperatorSet calldata operatorSet) external view returns (uint16) { + return _getOperatorSplit(_operatorOperatorSetSplitBips[operator][operatorSet.key()]); } /// @inheritdoc IRewardsCoordinator diff --git a/src/contracts/core/RewardsCoordinatorStorage.sol b/src/contracts/core/RewardsCoordinatorStorage.sol index 6c519214f..edc9c9522 100644 --- a/src/contracts/core/RewardsCoordinatorStorage.sol +++ b/src/contracts/core/RewardsCoordinatorStorage.sol @@ -27,10 +27,14 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { uint8 internal constant PAUSED_REWARD_ALL_STAKERS_AND_OPERATORS = 4; /// @dev Index for flag that pauses calling createOperatorDirectedAVSRewardsSubmission uint8 internal constant PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION = 5; + /// @dev Index for flag that pauses calling setOperatorSetPerformanceRewardsSubmission + uint8 internal constant PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION = 6; /// @dev Index for flag that pauses calling setOperatorAVSSplit - uint8 internal constant PAUSED_OPERATOR_AVS_SPLIT = 6; + uint8 internal constant PAUSED_OPERATOR_AVS_SPLIT = 7; /// @dev Index for flag that pauses calling setOperatorPISplit - uint8 internal constant PAUSED_OPERATOR_PI_SPLIT = 7; + uint8 internal constant PAUSED_OPERATOR_PI_SPLIT = 8; + /// @dev Index for flag that pauses calling setOperatorSetSplit + uint8 internal constant PAUSED_OPERATOR_SET_OPERATOR_SPLIT = 9; /// @dev Salt for the earner leaf, meant to distinguish from tokenLeaf since they have the same sized data uint8 internal constant EARNER_LEAF_SALT = 0; @@ -115,14 +119,21 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { // Construction - /// @notice Mapping: avs => operatorDirectedAVSRewardsSubmissionHash => bool to check if operator-directed rewards submission hash has been submitted - mapping(address => mapping(bytes32 => bool)) public isOperatorDirectedAVSRewardsSubmissionHash; + /// @notice Returns whether a `hash` is a `valid` operator set performance rewards submission hash for a given `avs`. + mapping(address avs => mapping(bytes32 hash => bool valid)) public isOperatorDirectedAVSRewardsSubmissionHash; - /// @notice Mapping: operator => avs => OperatorSplit. The split an operator takes for a specific AVS. - mapping(address => mapping(address => OperatorSplit)) internal operatorAVSSplitBips; + /// @notice Returns whether a `hash` is a `valid` operator set performance rewards submission hash for a given `avs`. + mapping(address avs => mapping(bytes32 hash => bool valid)) public isOperatorSetPerformanceRewardsSubmissionHash; - /// @notice Mapping: operator => OperatorPISplit. The split an operator takes for Programmatic Incentives. - mapping(address => OperatorSplit) internal operatorPISplitBips; + /// @notice Returns the `split` an `operator` takes for an `avs`. + mapping(address operator => mapping(address avs => OperatorSplit split)) internal _operatorAVSSplitBips; + + /// @notice Returns the `split` an `operator` takes for Programmatic Incentives. + mapping(address operator => OperatorSplit split) internal _operatorPISplitBips; + + /// @notice Returns the `split` an `operator` takes for a given operator set. + mapping(address operator => mapping(bytes32 operatorSetKey => OperatorSplit split)) internal + _operatorOperatorSetSplitBips; constructor( IDelegationManager _delegationManager, @@ -153,5 +164,5 @@ abstract contract RewardsCoordinatorStorage is IRewardsCoordinator { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[37] private __gap; + uint256[35] private __gap; } diff --git a/src/contracts/interfaces/IRewardsCoordinator.sol b/src/contracts/interfaces/IRewardsCoordinator.sol index c019c8efe..2c94435e2 100644 --- a/src/contracts/interfaces/IRewardsCoordinator.sol +++ b/src/contracts/interfaces/IRewardsCoordinator.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../libraries/OperatorSetLib.sol"; import "./IPauserRegistry.sol"; import "./IStrategy.sol"; @@ -27,6 +28,8 @@ interface IRewardsCoordinatorErrors { error NewRootMustBeForNewCalculatedPeriod(); /// @dev Thrown when rewards end timestamp has not elapsed. error RewardsEndTimestampNotElapsed(); + /// @dev Thrown when an invalid operator set is provided. + error InvalidOperatorSet(); /// Rewards Submissions @@ -277,6 +280,22 @@ interface IRewardsCoordinatorEvents is IRewardsCoordinatorTypes { OperatorDirectedRewardsSubmission operatorDirectedRewardsSubmission ); + /** + * @notice Emitted when an AVS creates a valid performance based `OperatorDirectedRewardsSubmission` + * @param caller The address calling `createOperatorDirectedOperatorSetRewardsSubmission`. + * @param operatorSet The operatorSet on behalf of which the performance rewards are being submitted. + * @param performanceRewardsSubmissionHash Keccak256 hash of (`avs`, `submissionNonce` and `performanceRewardsSubmission`). + * @param submissionNonce Current nonce of the avs. Used to generate a unique submission hash. + * @param performanceRewardsSubmission The Performance Rewards Submission. Contains the token, start timestamp, duration, description and, strategy and multipliers. + */ + event OperatorDirectedOperatorSetRewardsSubmissionCreated( + address indexed caller, + OperatorSet indexed operatorSet, + bytes32 indexed performanceRewardsSubmissionHash, + uint256 submissionNonce, + OperatorDirectedRewardsSubmission performanceRewardsSubmission + ); + /// @notice rewardsUpdater is responsible for submiting DistributionRoots, only owner can set rewardsUpdater event RewardsUpdaterSet(address indexed oldRewardsUpdater, address indexed newRewardsUpdater); @@ -321,6 +340,24 @@ interface IRewardsCoordinatorEvents is IRewardsCoordinatorTypes { uint16 newOperatorPISplitBips ); + /** + * @notice Emitted when the operator split for a given operatorSet is set. + * @param caller The address calling `setOperatorSetSplit`. + * @param operator The operator on behalf of which the split is being set. + * @param operatorSet The operatorSet for which the split is being set. + * @param activatedAt The timestamp at which the split will be activated. + * @param oldOperatorSetSplitBips The old split for the operator for the operatorSet. + * @param newOperatorSetSplitBips The new split for the operator for the operatorSet. + */ + event OperatorOperatorSetSplitBipsSet( + address indexed caller, + address indexed operator, + OperatorSet indexed operatorSet, + uint32 activatedAt, + uint16 oldOperatorSetSplitBips, + uint16 newOperatorSetSplitBips + ); + 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 @@ -420,6 +457,13 @@ interface IRewardsCoordinator is IRewardsCoordinatorErrors, IRewardsCoordinatorE OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions ) external; + /// @notice operatorSet parallel of createAVSPerformanceRewardsSubmission + /// @dev sender must be the avs of the given operatorSet + function createOperatorDirectedOperatorSetRewardsSubmission( + OperatorSet calldata operatorSet, + OperatorDirectedRewardsSubmission[] calldata performanceRewardsSubmissions + ) external; + /** * @notice Claim rewards against a given root (read from _distributionRoots[claim.rootIndex]). * Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for, @@ -522,6 +566,14 @@ interface IRewardsCoordinator is IRewardsCoordinatorErrors, IRewardsCoordinatorE */ function setOperatorPISplit(address operator, uint16 split) external; + /** + * @notice Sets the split for a specific operator for a specific operatorSet. + * @param operator The operator who is setting the split. + * @param operatorSet The operatorSet for which the split is being set by the operator. + * @param split The split for the operator for the specific operatorSet in bips. + */ + function setOperatorSetSplit(address operator, OperatorSet calldata operatorSet, uint16 split) external; + /** * @notice Sets the permissioned `rewardsUpdater` address which can post new roots * @dev Only callable by the contract owner @@ -570,6 +622,9 @@ interface IRewardsCoordinator is IRewardsCoordinatorErrors, IRewardsCoordinatorE address operator ) external view returns (uint16); + /// @notice Returns the split for a specific `operator` for a given `operatorSet` + function getOperatorSetSplit(address operator, OperatorSet calldata operatorSet) external view returns (uint16); + /// @notice return the hash of the earner's leaf function calculateEarnerLeafHash( EarnerTreeMerkleLeaf calldata leaf diff --git a/src/test/mocks/AllocationManagerMock.sol b/src/test/mocks/AllocationManagerMock.sol index 9ccb063a0..04d64f52c 100644 --- a/src/test/mocks/AllocationManagerMock.sol +++ b/src/test/mocks/AllocationManagerMock.sol @@ -2,18 +2,29 @@ pragma solidity ^0.8.9; import "forge-std/Test.sol"; -import "../../contracts/interfaces/IStrategy.sol"; -import "../../contracts/libraries/Snapshots.sol"; +import "src/contracts/interfaces/IStrategy.sol"; +import "src/contracts/libraries/Snapshots.sol"; +import "src/contracts/libraries/OperatorSetLib.sol"; contract AllocationManagerMock is Test { using Snapshots for Snapshots.DefaultWadHistory; + using OperatorSetLib for OperatorSet; receive() external payable {} fallback() external payable {} + mapping(bytes32 operatorSetKey => bool) public _isOperatorSet; mapping(address avs => uint256) public getOperatorSetCount; mapping(address => mapping(IStrategy => Snapshots.DefaultWadHistory)) internal _maxMagnitudeHistory; + function setIsOperatorSet(OperatorSet memory operatorSet, bool boolean) external { + _isOperatorSet[operatorSet.key()] = boolean; + } + + function isOperatorSet(OperatorSet memory operatorSet) external view returns (bool) { + return _isOperatorSet[operatorSet.key()]; + } + function setMaxMagnitudes( address operator, IStrategy[] calldata strategies, diff --git a/src/test/unit/RewardsCoordinatorUnit.t.sol b/src/test/unit/RewardsCoordinatorUnit.t.sol index 899d5c0a0..d48c22c24 100644 --- a/src/test/unit/RewardsCoordinatorUnit.t.sol +++ b/src/test/unit/RewardsCoordinatorUnit.t.sol @@ -83,11 +83,17 @@ contract RewardsCoordinatorUnitTests is EigenLayerUnitTestSetup, IRewardsCoordin /// @dev Index for flag that pauses calling createOperatorDirectedAVSRewardsSubmission uint8 internal constant PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION = 5; + /// @dev Index for flag that pauses calling setOperatorSetPerformanceRewardsSubmission + uint8 internal constant PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION = 6; + /// @dev Index for flag that pauses calling setOperatorAVSSplit - uint8 internal constant PAUSED_OPERATOR_AVS_SPLIT = 6; + uint8 internal constant PAUSED_OPERATOR_AVS_SPLIT = 7; /// @dev Index for flag that pauses calling setOperatorPISplit - uint8 internal constant PAUSED_OPERATOR_PI_SPLIT = 7; + uint8 internal constant PAUSED_OPERATOR_PI_SPLIT = 8; + + /// @dev Index for flag that pauses calling setOperatorSetSplit + uint8 internal constant PAUSED_OPERATOR_SET_OPERATOR_SPLIT = 9; // RewardsCoordinator entities address rewardsUpdater = address(1000); @@ -780,6 +786,161 @@ contract RewardsCoordinatorUnitTests_setOperatorPISplit is RewardsCoordinatorUni } } +contract RewardsCoordinatorUnitsTests_setOperatorSetSplit is RewardsCoordinatorUnitTests { + OperatorSet operatorSet; + + function setUp() public virtual override { + RewardsCoordinatorUnitTests.setUp(); + operatorSet = OperatorSet(address(this), 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + } + + // Revert when paused + function testFuzz_Revert_WhenPaused(address operator, uint16 split) public filterFuzzedAddressInputs(operator) { + cheats.assume(operator != address(0)); + split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS)); + cheats.prank(pauser); + rewardsCoordinator.pause(2 ** PAUSED_OPERATOR_SET_OPERATOR_SPLIT); + + cheats.prank(operator); + cheats.expectRevert(IPausable.CurrentlyPaused.selector); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split); + } + + // Revert when split is greater than 100% + function testFuzz_Revert_WhenSplitGreaterThan100( + address operator, + uint16 split + ) public filterFuzzedAddressInputs(operator) { + cheats.assume(operator != address(0)); + split = uint16(bound(split, ONE_HUNDRED_IN_BIPS + 1, type(uint16).max)); + + cheats.prank(operator); + cheats.expectRevert(SplitExceedsMax.selector); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split); + } + + function testFuzz_setOperatorSetSplit(address operator, uint16 split) public filterFuzzedAddressInputs(operator) { + cheats.assume(operator != address(0)); + + split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS)); + uint32 activatedAt = uint32(block.timestamp) + activationDelay; + uint16 oldSplit = rewardsCoordinator.getOperatorSetSplit(operator, operatorSet); + + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorOperatorSetSplitBipsSet(operator, operator, operatorSet, activatedAt, oldSplit, split); + cheats.prank(operator); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split); + + assertEq(oldSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + cheats.warp(activatedAt); + assertEq(split, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + } + + function testFuzz_setOperatorSetSplit_UAM(address operator, uint16 split) public filterFuzzedAddressInputs(operator) { + cheats.assume(operator != address(0)); + + // Set UAM + cheats.prank(operator); + permissionController.setAppointee( + operator, + defaultAppointee, + address(rewardsCoordinator), + IRewardsCoordinator.setOperatorSetSplit.selector + ); + + split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS)); + uint32 activatedAt = uint32(block.timestamp) + activationDelay; + uint16 oldSplit = rewardsCoordinator.getOperatorSetSplit(operator, operatorSet); + + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorOperatorSetSplitBipsSet(defaultAppointee, operator, operatorSet, activatedAt, oldSplit, split); + cheats.prank(defaultAppointee); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split); + + assertEq(oldSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + cheats.warp(activatedAt); + assertEq(split, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + } + + // Testing that the split has been initialized for the first time. + function testFuzz_setOperatorSetSplitFirstTime( + address operator, + uint16 split + ) public filterFuzzedAddressInputs(operator) { + cheats.assume(operator != address(0)); + split = uint16(bound(split, 0, ONE_HUNDRED_IN_BIPS)); + uint32 activatedAt = uint32(block.timestamp) + activationDelay; + uint16 oldSplit = rewardsCoordinator.getOperatorSetSplit(operator, operatorSet); + assertEq(oldSplit, defaultSplitBips, "Operator split is not Default split before Initialization"); + + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorOperatorSetSplitBipsSet(operator, operator, operatorSet, activatedAt, oldSplit, split); + cheats.prank(operator); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, split); + + assertEq(oldSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + cheats.warp(activatedAt); + assertEq(split, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + } + + // Testing the split setting for a second time prior to the earlier activation. + function testFuzz_Revert_setOperatorSetSplitSecondTimeBeforePriorActivation( + address operator, + uint16 firstSplit, + uint16 secondSplit, + uint32 warpTime + ) public filterFuzzedAddressInputs(operator) { + cheats.assume(operator != address(0)); + firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS)); + secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS)); + warpTime = uint32(bound(warpTime, uint32(block.timestamp), uint32(block.timestamp) + activationDelay)); + + // Setting First Split + cheats.prank(operator); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, firstSplit); + // Warping to time before activation of First split + cheats.warp(warpTime); + + // Trying to set Second Split + cheats.prank(operator); + cheats.expectRevert(PreviousSplitPending.selector); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, secondSplit); + } + + // Testing the split setting for a second time after earlier activation. + function testFuzz_setOperatorSetSplitSecondTimeAfterPriorActivation( + address operator, + uint16 firstSplit, + uint16 secondSplit, + uint32 warpTime + ) public filterFuzzedAddressInputs(operator) { + cheats.assume(operator != address(0)); + firstSplit = uint16(bound(firstSplit, 0, ONE_HUNDRED_IN_BIPS)); + secondSplit = uint16(bound(secondSplit, 0, ONE_HUNDRED_IN_BIPS)); + warpTime = uint32( + bound(warpTime, uint32(block.timestamp) + activationDelay + 1, type(uint32).max - activationDelay) + ); + + // Setting First Split + cheats.prank(operator); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, firstSplit); + // Warping to time after activation of First split + cheats.warp(warpTime); + uint32 activatedAt = uint32(block.timestamp) + activationDelay; + + // Setting Second Split + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorOperatorSetSplitBipsSet(operator, operator, operatorSet, activatedAt, firstSplit, secondSplit); + cheats.prank(operator); + rewardsCoordinator.setOperatorSetSplit(operator, operatorSet, secondSplit); + + assertEq(firstSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + cheats.warp(activatedAt); + assertEq(secondSplit, rewardsCoordinator.getOperatorSetSplit(operator, operatorSet), "Incorrect Operator split"); + } +} + contract RewardsCoordinatorUnitTests_createAVSRewardsSubmission is RewardsCoordinatorUnitTests { // Revert when paused function test_Revert_WhenPaused() public { @@ -2471,7 +2632,7 @@ contract RewardsCoordinatorUnitTests_createOperatorDirectedAVSRewardsSubmission avs, avs, rewardsSubmissionHash, - currSubmissionNonce, + currSubmissionNonce, operatorDirectedRewardsSubmissions[0] ); rewardsCoordinator.createOperatorDirectedAVSRewardsSubmission(avs, operatorDirectedRewardsSubmissions); @@ -2691,6 +2852,953 @@ contract RewardsCoordinatorUnitTests_createOperatorDirectedAVSRewardsSubmission } } +contract RewardsCoordinatorUnitTests_createOperatorDirectedOperatorSetRewardsSubmission is RewardsCoordinatorUnitTests { + OperatorSet operatorSet; + // used for stack too deep + + struct FuzzOperatorDirectedAVSRewardsSubmission { + address avs; + uint256 startTimestamp; + uint256 duration; + } + + OperatorReward[] defaultOperatorRewards; + + function setUp() public virtual override { + RewardsCoordinatorUnitTests.setUp(); + + address[] memory operators = new address[](3); + operators[0] = makeAddr("operator1"); + operators[1] = makeAddr("operator2"); + operators[2] = makeAddr("operator3"); + operators = _sortAddressArrayAsc(operators); + + defaultOperatorRewards.push(OperatorReward(operators[0], 1e18)); + defaultOperatorRewards.push(OperatorReward(operators[1], 2e18)); + defaultOperatorRewards.push(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); + operatorSet = OperatorSet(address(this), 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + } + + /// @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( + OperatorReward[] memory operatorRewards + ) internal pure returns (uint256) { + uint256 totalAmount = 0; + for (uint256 i = 0; i < operatorRewards.length; ++i) { + totalAmount += operatorRewards[i].amount; + } + return totalAmount; + } + + // Revert when paused + function test_Revert_WhenPaused() public { + cheats.prank(pauser); + rewardsCoordinator.pause(2 ** PAUSED_OPERATOR_DIRECTED_OPERATOR_SET_REWARDS_SUBMISSION); + + cheats.expectRevert(IPausable.CurrentlyPaused.selector); + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions; + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // // Revert from reentrancy + // function testFuzz_Revert_WhenReentrancy(uint256 startTimestamp, uint256 duration) public { + // // 1. Bound fuzz inputs to valid ranges and amounts + // 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. Deploy Reenterer + // Reenterer reenterer = new Reenterer(); + + // // 2. Create operator directed rewards submission input param + // OperatorDirectedRewardsSubmission[] + // memory performanceRewardsSubmissions = new OperatorDirectedRewardsSubmission[](1); + // performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + // strategiesAndMultipliers: defaultStrategyAndMultipliers, + // token: IERC20(address(reenterer)), + // operatorRewards: defaultOperatorRewards, + // startTimestamp: uint32(startTimestamp), + // duration: uint32(duration), + // description: "" + // }); + + // address targetToUse = address(rewardsCoordinator); + // uint256 msgValueToUse = 0; + // bytes memory calldataToUse = abi.encodeWithSelector( + // RewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission.selector, + // address(reenterer), + // performanceRewardsSubmissions + // ); + // reenterer.prepare(targetToUse, msgValueToUse, calldataToUse); + + // cheats.prank(address(reenterer)); + // cheats.expectRevert("ReentrancyGuard: reentrant call"); + // rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission( + // address(reenterer), + // performanceRewardsSubmissions + // ); + // } + + // Revert with 0 length strats and multipliers + function testFuzz_Revert_WhenEmptyStratsAndMultipliers( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + StrategyAndMultiplier[] memory emptyStratsAndMultipliers; + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: emptyStratsAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(InputArrayLengthZero.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert with 0 length operator rewards + function testFuzz_Revert_WhenEmptyOperatorRewards( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + OperatorReward[] memory emptyOperatorRewards; + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: emptyOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(InputArrayLengthZero.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert when operator is zero address + function testFuzz_Revert_WhenOperatorIsZeroAddress( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + defaultOperatorRewards[0].operator = address(0); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(InvalidAddressZero.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert when duplicate operators + function testFuzz_Revert_WhenDuplicateOperators( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + OperatorReward[] memory dupOperatorRewards = new OperatorReward[](2); + dupOperatorRewards[0] = defaultOperatorRewards[0]; + dupOperatorRewards[1] = defaultOperatorRewards[0]; + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: dupOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(OperatorsNotInAscendingOrder.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert when operator amount is zero + function testFuzz_Revert_WhenOperatorAmountIsZero( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + defaultOperatorRewards[0].amount = 0; + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(AmountIsZero.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert when operator amount is zero + function testFuzz_Revert_TotalAmountTooLarge( + address avs, + uint256 startTimestamp, + uint256 duration, + uint256 amount + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + amount = bound(amount, 1e38, type(uint256).max - 5e18); + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + defaultOperatorRewards[0].amount = amount; + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(AmountExceedsMax.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert with exceeding max duration + function testFuzz_Revert_WhenExceedingMaxDuration( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + duration = bound(duration, MAX_REWARDS_DURATION + 1, type(uint32).max); + startTimestamp = bound( + startTimestamp, + uint256(_maxTimestamp(GENESIS_REWARDS_TIMESTAMP, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH)) + + CALCULATION_INTERVAL_SECONDS - 1, + block.timestamp + ); + startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // 2. Create operator directed rewards submission input param + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(DurationExceedsMax.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert with invalid interval seconds + function testFuzz_Revert_WhenInvalidIntervalSeconds( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + duration = bound(duration, 0, MAX_REWARDS_DURATION); + cheats.assume(duration % CALCULATION_INTERVAL_SECONDS != 0); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(InvalidDurationRemainder.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert with invalid interval start timestamp + function testFuzz_Revert_WhenInvalidIntervalStartTimestamp( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + ); + cheats.assume(startTimestamp % CALCULATION_INTERVAL_SECONDS != 0); + + // 2. Create operator directed rewards submission input param + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(InvalidStartTimestampRemainder.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert with retroactive rewards enabled and set too far in past + // - either before genesis rewards timestamp + // - before max retroactive length + function testFuzz_Revert_WhenRewardsSubmissionTooStale( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + duration = bound(duration, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound(startTimestamp, 0, uint32(block.timestamp) - MAX_RETROACTIVE_LENGTH - 1); + startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // 2. Create operator directed rewards submission input param + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(StartTimestampTooFarInPast.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert when not retroactive + function testFuzz_Revert_WhenRewardsSubmissionNotRetroactive( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + duration = bound(duration, 0, MAX_REWARDS_DURATION); + duration = duration - (duration % CALCULATION_INTERVAL_SECONDS); + startTimestamp = bound( + startTimestamp, block.timestamp - duration + CALCULATION_INTERVAL_SECONDS, type(uint32).max - duration + ); + startTimestamp = startTimestamp - (startTimestamp % CALCULATION_INTERVAL_SECONDS); + + // 2. Create operator directed rewards submission input param + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(SubmissionNotRetroactive.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert with non whitelisted strategy + function testFuzz_Revert_WhenInvalidStrategy( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + defaultStrategyAndMultipliers[0].strategy = IStrategy(address(999)); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(StrategyNotWhitelisted.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + // Revert when duplicate strategies + function testFuzz_Revert_WhenDuplicateStrategies( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + StrategyAndMultiplier[] memory dupStratsAndMultipliers = new StrategyAndMultiplier[](2); + dupStratsAndMultipliers[0] = defaultStrategyAndMultipliers[0]; + dupStratsAndMultipliers[1] = defaultStrategyAndMultipliers[0]; + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: dupStratsAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected revert + cheats.prank(avs); + cheats.expectRevert(StrategiesNotInAscendingOrder.selector); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + } + + /** + * @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 avs and rewardsCoordinator + */ + function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_SingleSubmission( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, avs); + 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 + OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new OperatorDirectedRewardsSubmission[](1); + performanceRewardsSubmissions[0] = OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected event emitted + uint256 avsBalanceBefore = rewardToken.balanceOf(avs); + uint256 rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator)); + + cheats.startPrank(avs); + uint256 amount = _getTotalRewardsAmount(defaultOperatorRewards); + rewardToken.approve(address(rewardsCoordinator), amount); + uint256 currSubmissionNonce = rewardsCoordinator.submissionNonce(avs); + bytes32 rewardsSubmissionHash = + keccak256(abi.encode(avs, currSubmissionNonce, performanceRewardsSubmissions[0])); + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorDirectedOperatorSetRewardsSubmissionCreated( + avs, operatorSet, rewardsSubmissionHash, currSubmissionNonce, performanceRewardsSubmissions[0] + ); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + cheats.stopPrank(); + + assertTrue( + rewardsCoordinator.isOperatorSetPerformanceRewardsSubmissionHash(avs, rewardsSubmissionHash), + "rewards submission hash not submitted" + ); + assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(avs), "submission nonce not incremented"); + assertEq( + avsBalanceBefore - amount, + rewardToken.balanceOf(avs), + "AVS 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 Same test as above, uses UAM + * - 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 avs and rewardsCoordinator + */ + function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_SingleSubmission_UAM( + address avs, + uint256 startTimestamp, + uint256 duration + ) public filterFuzzedAddressInputs(avs) { + cheats.assume(avs != address(0)); + + operatorSet = OperatorSet(avs, 1); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + // Set UAM + cheats.prank(avs); + permissionController.setAppointee( + avs, + defaultAppointee, + address(rewardsCoordinator), + IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission.selector + ); + + // 1. Bound fuzz inputs to valid ranges and amounts + IERC20 rewardToken = + new ERC20PresetFixedSupply("dog wif hat", "MOCK1", mockTokenInitialSupply, defaultAppointee); + 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 + IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory performanceRewardsSubmissions = + new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1); + performanceRewardsSubmissions[0] = IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ + strategiesAndMultipliers: defaultStrategyAndMultipliers, + token: rewardToken, + operatorRewards: defaultOperatorRewards, + startTimestamp: uint32(startTimestamp), + duration: uint32(duration), + description: "" + }); + + // 3. call createOperatorDirectedOperatorSetRewardsSubmission() with expected event emitted + uint256 submitterBalanceBefore = rewardToken.balanceOf(defaultAppointee); + uint256 rewardsCoordinatorBalanceBefore = rewardToken.balanceOf(address(rewardsCoordinator)); + + cheats.startPrank(defaultAppointee); + uint256 amount = _getTotalRewardsAmount(defaultOperatorRewards); + rewardToken.approve(address(rewardsCoordinator), amount); + uint256 currSubmissionNonce = rewardsCoordinator.submissionNonce(avs); + bytes32 rewardsSubmissionHash = + keccak256(abi.encode(avs, currSubmissionNonce, performanceRewardsSubmissions[0])); + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorDirectedOperatorSetRewardsSubmissionCreated( + defaultAppointee, operatorSet, rewardsSubmissionHash, currSubmissionNonce, performanceRewardsSubmissions[0] + ); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, performanceRewardsSubmissions); + cheats.stopPrank(); + + assertTrue( + rewardsCoordinator.isOperatorSetPerformanceRewardsSubmissionHash(avs, rewardsSubmissionHash), + "rewards submission hash not submitted" + ); + assertEq(currSubmissionNonce + 1, rewardsCoordinator.submissionNonce(avs), "submission nonce not incremented"); + assertEq( + submitterBalanceBefore - amount, + rewardToken.balanceOf(defaultAppointee), + "Submitter 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 avs and rewardsCoordinator + */ + function testFuzz_createOperatorDirectedOperatorSetRewardsSubmission_MultipleSubmissions( + FuzzOperatorDirectedAVSRewardsSubmission memory param, + uint256 numSubmissions + ) public filterFuzzedAddressInputs(param.avs) { + cheats.assume(2 <= numSubmissions && numSubmissions <= 10); + cheats.assume(param.avs != address(0)); + + operatorSet = OperatorSet(param.avs, 2); + allocationManagerMock.setIsOperatorSet(operatorSet, true); + + cheats.prank(rewardsCoordinator.owner()); + + OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = + new OperatorDirectedRewardsSubmission[](numSubmissions); + bytes32[] memory rewardsSubmissionHashes = new bytes32[](numSubmissions); + uint256 startSubmissionNonce = rewardsCoordinator.submissionNonce(param.avs); + _deployMockRewardTokens(param.avs, numSubmissions); + + uint256[] memory avsBalancesBefore = _getBalanceForTokens(rewardTokens, param.avs); + 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 + OperatorDirectedRewardsSubmission memory rewardsSubmission = IRewardsCoordinatorTypes + .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(param.avs, startSubmissionNonce + i, rewardsSubmissions[i])); + cheats.expectEmit(true, true, true, true, address(rewardsCoordinator)); + emit OperatorDirectedOperatorSetRewardsSubmissionCreated( + param.avs, operatorSet, rewardsSubmissionHashes[i], startSubmissionNonce + i, rewardsSubmissions[i] + ); + } + + // 4. call createAVSRewardsSubmission() + cheats.prank(param.avs); + rewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission(operatorSet, rewardsSubmissions); + + // 5. Check for submissionNonce() and rewardsSubmissionHashes being set + assertEq( + startSubmissionNonce + numSubmissions, + rewardsCoordinator.submissionNonce(param.avs), + "submission nonce not incremented properly" + ); + + for (uint256 i = 0; i < numSubmissions; ++i) { + assertTrue( + rewardsCoordinator.isOperatorSetPerformanceRewardsSubmissionHash(param.avs, rewardsSubmissionHashes[i]), + "rewards submission hash not submitted" + ); + assertEq( + avsBalancesBefore[i] - amounts[i], + rewardTokens[i].balanceOf(param.avs), + "AVS 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" + ); + } + } +} + contract RewardsCoordinatorUnitTests_submitRoot is RewardsCoordinatorUnitTests { // only callable by rewardsUpdater function testFuzz_Revert_WhenNotRewardsUpdater(