diff --git a/src/contracts/core/AllocationManager.sol b/src/contracts/core/AllocationManager.sol index 4e24f49d38..3e8b0703c2 100644 --- a/src/contracts/core/AllocationManager.sol +++ b/src/contracts/core/AllocationManager.sol @@ -64,15 +64,25 @@ contract AllocationManager is function slashOperator( address avs, SlashingParams calldata params - ) external onlyWhenNotPaused(PAUSED_OPERATOR_SLASHING) checkCanCall(avs) { + ) + external + onlyWhenNotPaused(PAUSED_OPERATOR_SLASHING) + checkCanCall(avs) + returns (uint256 slashId, uint256[] memory shares) + { // Check that the operator set exists and the operator is registered to it OperatorSet memory operatorSet = OperatorSet(avs, params.operatorSetId); - require(params.strategies.length == params.wadsToSlash.length, InputArrayLengthMismatch()); - require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet()); + _checkArrayLengthsMatch(params.strategies.length, params.wadsToSlash.length); + _checkIsOperatorSet(operatorSet); require(isOperatorSlashable(params.operator, operatorSet), OperatorNotSlashable()); uint256[] memory wadSlashed = new uint256[](params.strategies.length); + // Increment the slash count for the operator set, first `slashId` equals 1. + slashId = ++_slashCount[operatorSet.key()]; + // Initialize the shares array. + shares = new uint256[](params.strategies.length); + // For each strategy in the operator set, slash any existing allocation for (uint256 i = 0; i < params.strategies.length; i++) { // Check that `strategies` is in ascending order. @@ -155,7 +165,7 @@ contract AllocationManager is ) external onlyWhenNotPaused(PAUSED_MODIFY_ALLOCATIONS) { // Check that the caller is allowed to modify allocations on behalf of the operator // We do not use a modifier to avoid `stack too deep` errors - require(_checkCanCall(operator), InvalidCaller()); + _checkCanCall(operator); // Check that the operator exists and has configured an allocation delay uint32 operatorAllocationDelay; @@ -166,7 +176,7 @@ contract AllocationManager is } for (uint256 i = 0; i < params.length; i++) { - require(params[i].strategies.length == params[i].newMagnitudes.length, InputArrayLengthMismatch()); + _checkArrayLengthsMatch(params[i].strategies.length, params[i].newMagnitudes.length); // Check that the operator set exists and get the operator's registration status // Operators do not need to be registered for an operator set in order to allocate @@ -174,7 +184,7 @@ contract AllocationManager is // allocate magnitude before registering, as AVS's will likely only accept // registrations from operators that are already slashable. OperatorSet memory operatorSet = params[i].operatorSet; - require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet()); + _checkIsOperatorSet(operatorSet); bool _isOperatorSlashable = isOperatorSlashable(operator, operatorSet); @@ -246,8 +256,7 @@ contract AllocationManager is IStrategy[] calldata strategies, uint16[] calldata numToClear ) external onlyWhenNotPaused(PAUSED_MODIFY_ALLOCATIONS) { - require(strategies.length == numToClear.length, InputArrayLengthMismatch()); - + _checkArrayLengthsMatch(strategies.length, numToClear.length); for (uint256 i = 0; i < strategies.length; ++i) { _clearDeallocationQueue({operator: operator, strategy: strategies[i], numToClear: numToClear[i]}); } @@ -258,13 +267,13 @@ contract AllocationManager is address operator, RegisterParams calldata params ) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION) checkCanCall(operator) { - // Check that the operator exists - require(delegation.isOperator(operator), InvalidOperator()); + // Check if the operator has registered. + _checkIsOperator(operator); for (uint256 i = 0; i < params.operatorSetIds.length; i++) { // Check the operator set exists and the operator is not currently registered to it OperatorSet memory operatorSet = OperatorSet(params.avs, params.operatorSetIds[i]); - require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet()); + _checkIsOperatorSet(operatorSet); require(!isOperatorSlashable(operator, operatorSet), AlreadyMemberOfSet()); // Add operator to operator set @@ -285,12 +294,12 @@ contract AllocationManager is DeregisterParams calldata params ) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION) { // Check that the caller is either authorized on behalf of the operator or AVS - require(_checkCanCall(params.operator) || _checkCanCall(params.avs), InvalidCaller()); + require(_canCall(params.operator) || _canCall(params.avs), InvalidPermissions()); for (uint256 i = 0; i < params.operatorSetIds.length; i++) { // Check the operator set exists and the operator is registered to it OperatorSet memory operatorSet = OperatorSet(params.avs, params.operatorSetIds[i]); - require(_operatorSets[params.avs].contains(operatorSet.id), InvalidOperatorSet()); + _checkIsOperatorSet(operatorSet); require(registrationStatus[params.operator][operatorSet.key()].registered, NotMemberOfSet()); // Remove operator from operator set @@ -313,8 +322,8 @@ contract AllocationManager is /// @inheritdoc IAllocationManager function setAllocationDelay(address operator, uint32 delay) external { if (msg.sender != address(delegation)) { - require(_checkCanCall(operator), InvalidCaller()); - require(delegation.isOperator(operator), InvalidOperator()); + _checkCanCall(operator); + _checkIsOperator(operator); } _setAllocationDelay(operator, delay); } @@ -330,31 +339,28 @@ contract AllocationManager is /// @inheritdoc IAllocationManager function updateAVSMetadataURI(address avs, string calldata metadataURI) external checkCanCall(avs) { - if (!_avsRegisteredMetadata[avs]) { - _avsRegisteredMetadata[avs] = true; - } - + if (!_avsRegisteredMetadata[avs]) _avsRegisteredMetadata[avs] = true; emit AVSMetadataURIUpdated(avs, metadataURI); } /// @inheritdoc IAllocationManager function createOperatorSets(address avs, CreateSetParams[] calldata params) external checkCanCall(avs) { - // Check that the AVS exists and has registered metadata require(_avsRegisteredMetadata[avs], NonexistentAVSMetadata()); - for (uint256 i = 0; i < params.length; i++) { - OperatorSet memory operatorSet = OperatorSet(avs, params[i].operatorSetId); - - // Create the operator set, ensuring it does not already exist - require(_operatorSets[avs].add(operatorSet.id), InvalidOperatorSet()); - emit OperatorSetCreated(OperatorSet(avs, operatorSet.id)); + _createOperatorSet(avs, params[i], DEFAULT_BURN_ADDRESS); + } + } - // Add strategies to the operator set - bytes32 operatorSetKey = operatorSet.key(); - for (uint256 j = 0; j < params[i].strategies.length; j++) { - _operatorSetStrategies[operatorSetKey].add(address(params[i].strategies[j])); - emit StrategyAddedToOperatorSet(operatorSet, params[i].strategies[j]); - } + /// @inheritdoc IAllocationManager + function createRedistributingOperatorSets( + address avs, + CreateSetParams[] calldata params, + address[] calldata redistributionRecipients + ) external checkCanCall(avs) { + _checkArrayLengthsMatch(params.length, redistributionRecipients.length); + require(_avsRegisteredMetadata[avs], NonexistentAVSMetadata()); + for (uint256 i = 0; i < params.length; i++) { + _createOperatorSet(avs, params[i], redistributionRecipients[i]); } } @@ -365,10 +371,8 @@ contract AllocationManager is IStrategy[] calldata strategies ) external checkCanCall(avs) { OperatorSet memory operatorSet = OperatorSet(avs, operatorSetId); + _checkIsOperatorSet(operatorSet); bytes32 operatorSetKey = operatorSet.key(); - - require(_operatorSets[avs].contains(operatorSet.id), InvalidOperatorSet()); - for (uint256 i = 0; i < strategies.length; i++) { require(_operatorSetStrategies[operatorSetKey].add(address(strategies[i])), StrategyAlreadyInOperatorSet()); emit StrategyAddedToOperatorSet(operatorSet, strategies[i]); @@ -382,8 +386,7 @@ contract AllocationManager is IStrategy[] calldata strategies ) external checkCanCall(avs) { OperatorSet memory operatorSet = OperatorSet(avs, operatorSetId); - require(_operatorSets[avs].contains(operatorSet.id), InvalidOperatorSet()); - + _checkIsOperatorSet(operatorSet); bytes32 operatorSetKey = operatorSet.key(); for (uint256 i = 0; i < strategies.length; i++) { require(_operatorSetStrategies[operatorSetKey].remove(address(strategies[i])), StrategyNotInOperatorSet()); @@ -397,6 +400,41 @@ contract AllocationManager is * */ + /** + * @notice Creates a new operator set for an AVS. + * @param avs The AVS address that owns the operator set. + * @param params The parameters for creating the operator set. + * @param redistributionRecipient Address to receive redistributed funds when operators are slashed. + * @dev If `redistributionRecipient` is address(0), the operator set is considered non-redistributing + * and slashed funds are sent to the `DEFAULT_BURN_ADDRESS`. + * @dev Providing `BEACONCHAIN_ETH_STRAT` as a strategy will revert since it's not currently supported. + */ + function _createOperatorSet( + address avs, + CreateSetParams calldata params, + address redistributionRecipient + ) internal { + OperatorSet memory operatorSet = OperatorSet(avs, params.operatorSetId); + + // Create the operator set, ensuring it does not already exist. + require(_operatorSets[avs].add(operatorSet.id), InvalidOperatorSet()); + emit OperatorSetCreated(operatorSet); + + bool isRedistributing = redistributionRecipient != DEFAULT_BURN_ADDRESS; + + if (isRedistributing) { + _redistributionRecipients[operatorSet.key()] = redistributionRecipient; + emit RedistributionAddressSet(operatorSet, redistributionRecipient); + } + + // Add strategies to the operator set + for (uint256 j = 0; j < params.strategies.length; j++) { + if (isRedistributing && params.strategies[j] == BEACONCHAIN_ETH_STRAT) revert InvalidStrategy(); + _operatorSetStrategies[operatorSet.key()].add(address(params.strategies[j])); + emit StrategyAddedToOperatorSet(operatorSet, params.strategies[j]); + } + } + /** * @dev Clear one or more pending deallocations to a strategy's allocated magnitude * @param operator the operator whose pending deallocations will be cleared @@ -605,6 +643,25 @@ contract AllocationManager is return uint256(int256(int128(uint128(a)) + b)).toUint64(); } + /// @dev Reverts if the operator set does not exist. + function _checkIsOperatorSet( + OperatorSet memory operatorSet + ) internal view { + require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet()); + } + + /// @dev Reverts if the provided arrays have different lengths. + function _checkArrayLengthsMatch(uint256 left, uint256 right) internal pure { + require(left == right, InputArrayLengthMismatch()); + } + + /// @dev Reverts if the operator is not registered. + function _checkIsOperator( + address operator + ) internal view { + require(delegation.isOperator(operator), InvalidOperator()); + } + /** * * VIEW FUNCTIONS @@ -905,4 +962,27 @@ contract AllocationManager is // less than or equal to return status.registered || block.number <= status.slashableUntil; } + + /// @inheritdoc IAllocationManager + function getRedistributionRecipient( + OperatorSet memory operatorSet + ) external view returns (address) { + // Load the redistribution recipient and return it if set, otherwise return the default burn address. + address redistributionRecipient = _redistributionRecipients[operatorSet.key()]; + return redistributionRecipient == address(0) ? DEFAULT_BURN_ADDRESS : redistributionRecipient; + } + + /// @inheritdoc IAllocationManager + function isRedistributingOperatorSet( + OperatorSet memory operatorSet + ) external view returns (bool) { + return _redistributionRecipients[operatorSet.key()] != address(0); + } + + /// @inheritdoc IAllocationManager + function getSlashCount( + OperatorSet memory operatorSet + ) external view returns (uint256) { + return _slashCount[operatorSet.key()]; + } } diff --git a/src/contracts/core/AllocationManagerStorage.sol b/src/contracts/core/AllocationManagerStorage.sol index 24bbb8f3e7..0e1c7d1555 100644 --- a/src/contracts/core/AllocationManagerStorage.sol +++ b/src/contracts/core/AllocationManagerStorage.sol @@ -14,6 +14,12 @@ abstract contract AllocationManagerStorage is IAllocationManager { // Constants + /// @dev The default burn address for slashed funds. + address internal constant DEFAULT_BURN_ADDRESS = 0x00000000000000000000000000000000000E16E4; + + /// @dev The beacon chain ETH strategy. + IStrategy internal constant BEACONCHAIN_ETH_STRAT = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); + /// @dev Index for flag that pauses operator allocations/deallocations when set. uint8 internal constant PAUSED_MODIFY_ALLOCATIONS = 0; @@ -93,6 +99,16 @@ abstract contract AllocationManagerStorage is IAllocationManager { /// @notice bool is not used and is always true if the avs has registered metadata mapping(address avs => bool) internal _avsRegisteredMetadata; + /// @notice Returns the number of slashes for a given operator set. + /// @dev This is also used as a unique slash identifier. + /// @dev This tracks the number of slashes after the redistribution release. + mapping(bytes32 operatorSetKey => uint256 slashId) public _slashCount; + + /// @notice Returns the address where slashed funds will be sent for a given operator set. + /// @dev For redistributing Operator Sets, returns the configured redistribution address set during Operator Set creation. + /// For non-redistributing or non-existing operator sets, returns `address(0)`. + mapping(bytes32 operatorSetKey => address redistributionAddr) internal _redistributionRecipients; + // Construction constructor(IDelegationManager _delegation, uint32 _DEALLOCATION_DELAY, uint32 _ALLOCATION_CONFIGURATION_DELAY) { @@ -106,5 +122,5 @@ abstract contract AllocationManagerStorage is IAllocationManager { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[36] private __gap; + uint256[34] private __gap; } diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index 9db9a8f7b0..57de4e35d8 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -158,7 +158,7 @@ contract DelegationManager is if (msg.sender != staker) { address operator = delegatedTo[staker]; - require(_checkCanCall(operator) || msg.sender == delegationApprover(operator), CallerCannotUndelegate()); + require(_canCall(operator) || msg.sender == delegationApprover(operator), CallerCannotUndelegate()); emit StakerForceUndelegated(staker, operator); } diff --git a/src/contracts/interfaces/IAllocationManager.sol b/src/contracts/interfaces/IAllocationManager.sol index 6f7236107c..57786f22a5 100644 --- a/src/contracts/interfaces/IAllocationManager.sol +++ b/src/contracts/interfaces/IAllocationManager.sol @@ -20,8 +20,8 @@ interface IAllocationManagerErrors { /// Caller - /// @dev Thrown when caller is not authorized to call a function. - error InvalidCaller(); + /// @dev Thrown when an invalid strategy is provided. + error InvalidStrategy(); /// Operator Status @@ -209,6 +209,9 @@ interface IAllocationManagerEvents is IAllocationManagerTypes { /// @notice Emitted when an operator is removed from an operator set. event OperatorRemovedFromOperatorSet(address indexed operator, OperatorSet operatorSet); + /// @notice Emitted when a redistributing operator set is created by an AVS. + event RedistributionAddressSet(OperatorSet operatorSet, address redistributionRecipient); + /// @notice Emitted when a strategy is added to an operator set. event StrategyAddedToOperatorSet(OperatorSet operatorSet, IStrategy strategy); @@ -244,7 +247,10 @@ interface IAllocationManager is IAllocationManagerErrors, IAllocationManagerEven * rounding, which will result in small amounts of tokens locked in the contract * rather than fully burning through the burn mechanism. */ - function slashOperator(address avs, SlashingParams calldata params) external; + function slashOperator( + address avs, + SlashingParams calldata params + ) external returns (uint256 slashId, uint256[] memory shares); /** * @notice Modifies the proportions of slashable stake allocated to an operator set from a list of strategies @@ -325,6 +331,20 @@ interface IAllocationManager is IAllocationManagerErrors, IAllocationManagerEven */ function createOperatorSets(address avs, CreateSetParams[] calldata params) external; + /** + * @notice Allows an AVS to create new Redistribution operator sets. + * @param avs The AVS creating the new operator sets. + * @param params An array of operator set creation parameters. + * @param redistributionRecipients An array of addresses that will receive redistributed funds when operators are slashed. + * @dev Same logic as `createOperatorSets`, except `redistributionRecipients` corresponding to each operator set are stored. + * Additionally, emits `RedistributionOperatorSetCreated` event instead of `OperatorSetCreated` for each created operator set. + */ + function createRedistributingOperatorSets( + address avs, + CreateSetParams[] calldata params, + address[] calldata redistributionRecipients + ) external; + /** * @notice Allows an AVS to add strategies to an operator set * @dev Strategies MUST NOT already exist in the operator set @@ -601,4 +621,34 @@ interface IAllocationManager is IAllocationManagerErrors, IAllocationManagerEven * @param operatorSet the operator set to check slashability for */ function isOperatorSlashable(address operator, OperatorSet memory operatorSet) external view returns (bool); + + /** + * @notice Returns the address where slashed funds will be sent for a given operator set. + * @param operatorSet The Operator Set to query. + * @return For redistributing Operator Sets, returns the configured redistribution address set during Operator Set creation. + * For non-redistributing operator sets, returns the `DEFAULT_BURN_ADDRESS`. + */ + function getRedistributionRecipient( + OperatorSet memory operatorSet + ) external view returns (address); + + /** + * @notice Returns whether a given operator set supports redistribution + * or not when funds are slashed and burned from EigenLayer. + * @param operatorSet The Operator Set to query. + * @return For redistributing Operator Sets, returns true. + * For non-redistributing Operator Sets, returns false. + */ + function isRedistributingOperatorSet( + OperatorSet memory operatorSet + ) external view returns (bool); + + /** + * @notice Returns the number of slashes for a given operator set. + * @param operatorSet The operator set to query. + * @return The number of slashes for the operator set. + */ + function getSlashCount( + OperatorSet memory operatorSet + ) external view returns (uint256); } diff --git a/src/contracts/mixins/PermissionControllerMixin.sol b/src/contracts/mixins/PermissionControllerMixin.sol index 88fceff918..8b86636f30 100644 --- a/src/contracts/mixins/PermissionControllerMixin.sol +++ b/src/contracts/mixins/PermissionControllerMixin.sol @@ -16,23 +16,35 @@ abstract contract PermissionControllerMixin { permissionController = _permissionController; } - /// @notice Checks if the caller (msg.sender) can call on behalf of an account. + /// @notice Reverts if the caller does not have permission to call on behalf of `account`. modifier checkCanCall( address account ) { - require(_checkCanCall(account), InvalidPermissions()); + _checkCanCall(account); _; } /** * @notice Checks if the caller is allowed to call a function on behalf of an account. - * @param account the account to check + * @param account The account to check permissions for. * @dev `msg.sender` is the caller to check that can call the function on behalf of `account`. - * @dev Returns a bool, instead of reverting + * @dev Returns whether the caller has permission to call the function on behalf of the account. */ - function _checkCanCall( + function _canCall( address account ) internal returns (bool) { return permissionController.canCall(account, msg.sender, address(this), msg.sig); } + + /** + * @notice Checks if the caller is allowed to call a function on behalf of an account. + * @param account The account to check permissions for. + * @dev `msg.sender` is the caller to check that can call the function on behalf of `account`. + * @dev Reverts if the caller does not have permission. + */ + function _checkCanCall( + address account + ) internal { + require(_canCall(account), InvalidPermissions()); + } } diff --git a/src/test/unit/AllocationManagerUnit.t.sol b/src/test/unit/AllocationManagerUnit.t.sol index 2365a0a911..c9634efa9d 100644 --- a/src/test/unit/AllocationManagerUnit.t.sol +++ b/src/test/unit/AllocationManagerUnit.t.sol @@ -13,6 +13,8 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag /// Constants /// ----------------------------------------------------------------------- + error InvalidPermissions(); + /// NOTE: Raising these values directly increases cpu time for tests. uint internal constant FUZZ_MAX_ALLOCATIONS = 8; uint internal constant FUZZ_MAX_STRATS = 8; @@ -137,6 +139,35 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag allocationManager.createOperatorSets(operatorSets[0].avs, createSetParams); } + function _createRedistributingOperatorSet( + OperatorSet memory operatorSet, + IStrategy[] memory strategies, + address redistributionRecipient + ) internal returns (OperatorSet memory) { + cheats.prank(operatorSet.avs); + allocationManager.createRedistributingOperatorSets( + operatorSet.avs, + CreateSetParams({operatorSetId: operatorSet.id, strategies: strategies}).toArray(), + redistributionRecipient.toArray() + ); + return operatorSet; + } + + function _createRedistributingOperatorSets( + OperatorSet[] memory operatorSets, + IStrategy[] memory strategies, + address[] memory redistributionRecipients + ) internal { + CreateSetParams[] memory createSetParams = new CreateSetParams[](operatorSets.length); + + for (uint i; i < operatorSets.length; ++i) { + createSetParams[i] = CreateSetParams({operatorSetId: operatorSets[i].id, strategies: strategies}); + } + + cheats.prank(operatorSets[0].avs); + allocationManager.createRedistributingOperatorSets(operatorSets[0].avs, createSetParams, redistributionRecipients); + } + function _registerForOperatorSet(address operator, OperatorSet memory operatorSet) internal { cheats.prank(operator); allocationManager.registerForOperatorSets( @@ -1755,9 +1786,9 @@ contract AllocationManagerUnitTests_ModifyAllocations is AllocationManagerUnitTe allocationManager.modifyAllocations(address(this), new AllocateParams[](0)); } - function test_revert_invalidCaller() public { + function test_revert_invalidPermissions() public { address invalidOperator = address(0x2); - cheats.expectRevert(InvalidCaller.selector); + cheats.expectRevert(InvalidPermissions.selector); allocationManager.modifyAllocations(invalidOperator, new AllocateParams[](0)); } @@ -3242,7 +3273,7 @@ contract AllocationManagerUnitTests_SetAllocationDelay is AllocationManagerUnitT } function test_revert_callerNotAuthorized() public { - cheats.expectRevert(InvalidCaller.selector); + cheats.expectRevert(InvalidPermissions.selector); allocationManager.setAllocationDelay(operatorToSet, 1); } @@ -3454,7 +3485,7 @@ contract AllocationManagerUnitTests_deregisterFromOperatorSets is AllocationMana address randomOperator = address(0x1); defaultDeregisterParams.operator = randomOperator; - cheats.expectRevert(InvalidCaller.selector); + cheats.expectRevert(InvalidPermissions.selector); allocationManager.deregisterFromOperatorSets(defaultDeregisterParams); } @@ -3462,7 +3493,7 @@ contract AllocationManagerUnitTests_deregisterFromOperatorSets is AllocationMana address randomAVS = address(0x1); cheats.prank(randomAVS); - cheats.expectRevert(InvalidCaller.selector); + cheats.expectRevert(InvalidPermissions.selector); allocationManager.deregisterFromOperatorSets(defaultDeregisterParams); } @@ -3655,6 +3686,79 @@ contract AllocationManagerUnitTests_createOperatorSets is AllocationManagerUnitT } } +contract AllocationManagerUnitTests_createRedistributingOperatorSets is AllocationManagerUnitTests { + using ArrayLib for *; + + function testRevert_createRedistributingOperatorSets_InvalidOperatorSet() public { + cheats.prank(defaultAVS); + cheats.expectRevert(InvalidOperatorSet.selector); + allocationManager.createRedistributingOperatorSets( + defaultAVS, CreateSetParams(defaultOperatorSet.id, defaultStrategies).toArray(), address(this).toArray() + ); + } + + function testRevert_createRedistributingOperatorSets_NonexistentAVSMetadata(Randomness r) public rand(r) { + address avs = r.Address(); + address redistributionRecipient = r.Address(); + cheats.expectRevert(NonexistentAVSMetadata.selector); + cheats.prank(avs); + allocationManager.createRedistributingOperatorSets( + avs, CreateSetParams(defaultOperatorSet.id, defaultStrategies).toArray(), redistributionRecipient.toArray() + ); + } + + function testFuzz_createRedistributingOperatorSets_Correctness(Randomness r) public rand(r) { + address avs = r.Address(); + uint numOpSets = r.Uint256(1, FUZZ_MAX_OP_SETS); + uint numStrategies = r.Uint256(1, FUZZ_MAX_STRATS); + + cheats.prank(avs); + allocationManager.updateAVSMetadataURI(avs, "https://example.com"); + + CreateSetParams[] memory createSetParams = new CreateSetParams[](numOpSets); + address[] memory redistributionRecipients = new address[](numOpSets); + + for (uint i; i < numOpSets; ++i) { + createSetParams[i].operatorSetId = r.Uint32(1, type(uint32).max); + createSetParams[i].strategies = r.StrategyArray(numStrategies); + redistributionRecipients[i] = r.Address(); + + cheats.expectEmit(true, true, true, true, address(allocationManager)); + emit OperatorSetCreated(OperatorSet(avs, createSetParams[i].operatorSetId)); + cheats.expectEmit(true, true, true, true, address(allocationManager)); + emit RedistributionAddressSet(OperatorSet(avs, createSetParams[i].operatorSetId), redistributionRecipients[i]); + for (uint j; j < numStrategies; ++j) { + cheats.expectEmit(true, true, true, true, address(allocationManager)); + emit StrategyAddedToOperatorSet(OperatorSet(avs, createSetParams[i].operatorSetId), createSetParams[i].strategies[j]); + } + } + + cheats.prank(avs); + allocationManager.createRedistributingOperatorSets(avs, createSetParams, redistributionRecipients); + + for (uint k; k < numOpSets; ++k) { + OperatorSet memory opSet = OperatorSet(avs, createSetParams[k].operatorSetId); + assertTrue(allocationManager.isOperatorSet(opSet), "should be operator set"); + assertTrue(allocationManager.isRedistributingOperatorSet(opSet), "should be redistributing operator set"); + assertEq( + allocationManager.getRedistributionRecipient(opSet), + redistributionRecipients[k], + "should have correct redistribution recipient" + ); + + IStrategy[] memory strategiesInSet = allocationManager.getStrategiesInOperatorSet(opSet); + assertEq(strategiesInSet.length, numStrategies, "strategiesInSet length should be numStrategies"); + for (uint l; l < numStrategies; ++l) { + assertTrue( + allocationManager.getStrategiesInOperatorSet(opSet)[l] == createSetParams[k].strategies[l], "should be strat of set" + ); + } + } + + assertEq(createSetParams.length, allocationManager.getOperatorSetCount(avs), "should be correct number of sets"); + } +} + contract AllocationManagerUnitTests_setAVSRegistrar is AllocationManagerUnitTests { function test_getAVSRegistrar() public { address randomAVS = random().Address(); @@ -4135,3 +4239,19 @@ contract AllocationManagerUnitTests_getAllocatedStake is AllocationManagerUnitTe assertEq(allocatedStake[0][0], DEFAULT_OPERATOR_SHARES.mulWad(5e17), "allocated stake should remain same after deregistration"); } } + +contract AllocationManagerUnitTests_isRedistributingOperatorSet is AllocationManagerUnitTests { + function testFuzz_isRedistributingOperatorSet(Randomness r) public rand(r) { + assertEq(allocationManager.isRedistributingOperatorSet(defaultOperatorSet), false, "operator set should not be redistributing"); + + OperatorSet memory redistributingOpSet = OperatorSet(defaultAVS, defaultOperatorSet.id + 1); + address redistributionRecipient = r.Address(); + _createRedistributingOperatorSet(redistributingOpSet, defaultStrategies, redistributionRecipient); + assertEq(allocationManager.isRedistributingOperatorSet(redistributingOpSet), true, "operator set should be redistributing"); + assertEq( + allocationManager.getRedistributionRecipient(redistributingOpSet), + redistributionRecipient, + "redistribution recipient should be correct" + ); + } +}