From dd0293d8acab9a53b8fe1a3f0d257e382e16b6c6 Mon Sep 17 00:00:00 2001 From: wadealexc Date: Wed, 13 Nov 2024 19:56:48 +0000 Subject: [PATCH] feat: add getMinimumSlashableStake --- src/contracts/core/AllocationManager.sol | 38 ++++ .../interfaces/IAllocationManager.sol | 24 +++ src/test/mocks/DelegationManagerMock.sol | 17 ++ src/test/unit/AllocationManagerUnit.t.sol | 203 +++++++++++++++++- src/test/utils/SingleItemArrayLib.sol | 7 + 5 files changed, 283 insertions(+), 6 deletions(-) diff --git a/src/contracts/core/AllocationManager.sol b/src/contracts/core/AllocationManager.sol index 96223fdb6..fef94b769 100644 --- a/src/contracts/core/AllocationManager.sol +++ b/src/contracts/core/AllocationManager.sol @@ -738,4 +738,42 @@ contract AllocationManager is return strategies; } + + /// @inheritdoc IAllocationManager + function getMinimumSlashableStake( + OperatorSet memory operatorSet, + address[] memory operators, + IStrategy[] memory strategies, + uint32 futureBlock + ) external view returns (uint256[][] memory slashableStake) { + slashableStake = new uint256[][](operators.length); + uint256[][] memory delegatedStake = delegation.getOperatorsShares(operators, strategies); + + for (uint256 i = 0; i < operators.length; i++) { + address operator = operators[i]; + slashableStake[i] = new uint256[](strategies.length); + + for (uint256 j = 0; j < strategies.length; j++) { + IStrategy strategy = strategies[j]; + + // Fetch the max magnitude and allocation for the operator/strategy. + // Prevent division by 0 if needed. This mirrors the "FullySlashed" checks + // in the DelegationManager + uint64 maxMagnitude = _maxMagnitudeHistory[operator][strategy].latest(); + if (maxMagnitude == 0) { + continue; + } + + Allocation memory alloc = getAllocation(operator, operatorSet, strategy); + + // If the pending change takes effect before `futureBlock`, include it in `currentMagnitude` + if (alloc.effectBlock <= futureBlock) { + alloc.currentMagnitude = _addInt128(alloc.currentMagnitude, alloc.pendingDiff); + } + + uint256 slashableProportion = uint256(alloc.currentMagnitude).divWad(maxMagnitude); + slashableStake[i][j] = delegatedStake[i][j].mulWad(slashableProportion); + } + } + } } diff --git a/src/contracts/interfaces/IAllocationManager.sol b/src/contracts/interfaces/IAllocationManager.sol index e3b447046..4079962af 100644 --- a/src/contracts/interfaces/IAllocationManager.sol +++ b/src/contracts/interfaces/IAllocationManager.sol @@ -517,4 +517,28 @@ interface IAllocationManager is ISignatureUtils, IAllocationManagerErrors, IAllo function getStrategiesInOperatorSet( OperatorSet memory operatorSet ) external view returns (IStrategy[] memory strategies); + + /** + * @notice Returns the minimum amount of stake that will be slashable as of some future block, + * according to each operator's allocation from each strategy to the operator set. + * @dev This method queries actual delegated stakes in the DelegationManager and applies + * each operator's allocation to the stake to produce the slashable stake each allocation + * represents. + * @dev This minimum takes into account `futureBlock`, and will omit any pending magnitude + * diffs that will not be in effect as of `futureBlock`. NOTE that in order to get the true + * minimum slashable stake as of some future block, `futureBlock` MUST be greater than block.number + * @dev NOTE that `futureBlock` should be fewer than `DEALLOCATION_DELAY` blocks in the future, + * or the values returned from this method may not be accurate due to deallocations. + * @param operatorSet the operator set to query + * @param operators the list of operators whose slashable stakes will be returned + * @param strategies the strategies that each slashable stake corresponds to + * @param futureBlock the block at which to get allocation information. Should be a future block. + * @return slashableStake a list of slashable stakes, indexed by [operator][strategy] + */ + function getMinimumSlashableStake( + OperatorSet memory operatorSet, + address[] memory operators, + IStrategy[] memory strategies, + uint32 futureBlock + ) external view returns (uint256[][] memory slashableStake); } diff --git a/src/test/mocks/DelegationManagerMock.sol b/src/test/mocks/DelegationManagerMock.sol index e8c94c260..dc0c82281 100644 --- a/src/test/mocks/DelegationManagerMock.sol +++ b/src/test/mocks/DelegationManagerMock.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "src/contracts/interfaces/IDelegationManager.sol"; import "src/contracts/interfaces/IStrategyManager.sol"; +import "src/contracts/libraries/SlashingLib.sol"; contract DelegationManagerMock is Test { receive() external payable {} @@ -24,11 +25,27 @@ contract DelegationManagerMock is Test { isOperator[operator] = _isOperatorReturnValue; } + function decreaseOperatorShares(address operator, IStrategy strategy, uint256 wadSlashed) external { + uint256 amountSlashed = SlashingLib.calcSlashedAmount({ + operatorShares: operatorShares[operator][strategy], + wadSlashed: wadSlashed + }); + + operatorShares[operator][strategy] -= amountSlashed; + } + /// @notice returns the total number of shares in `strategy` that are delegated to `operator`. function setOperatorShares(address operator, IStrategy strategy, uint256 shares) external { operatorShares[operator][strategy] = shares; } + /// @notice returns the total number of shares in `strategy` that are delegated to `operator`. + function setOperatorsShares(address operator, IStrategy[] memory strategies, uint256 shares) external { + for (uint i = 0; i < strategies.length; i++) { + operatorShares[operator][strategies[i]] = shares; + } + } + function delegateTo(address operator, ISignatureUtils.SignatureWithExpiry memory /*approverSignatureAndExpiry*/, bytes32 /*approverSalt*/) external { delegatedTo[msg.sender] = operator; } diff --git a/src/test/unit/AllocationManagerUnit.t.sol b/src/test/unit/AllocationManagerUnit.t.sol index 08162b783..8cf7ef3eb 100644 --- a/src/test/unit/AllocationManagerUnit.t.sol +++ b/src/test/unit/AllocationManagerUnit.t.sol @@ -26,6 +26,8 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag uint32 constant ALLOCATION_CONFIGURATION_DELAY = 21 days / ASSUMED_BLOCK_TIME; uint32 constant DEFAULT_OPERATOR_ALLOCATION_DELAY = 1 days / ASSUMED_BLOCK_TIME; + uint256 constant DEFAULT_OPERATOR_SHARES = 1e18; + /// ----------------------------------------------------------------------- /// Mocks /// ----------------------------------------------------------------------- @@ -68,6 +70,7 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag _registerOperator(defaultOperator); _setAllocationDelay(defaultOperator, DEFAULT_OPERATOR_ALLOCATION_DELAY); _registerForOperatorSet(defaultOperator, defaultOperatorSet); + _grantDelegatedStake(defaultOperator, defaultOperatorSet, DEFAULT_OPERATOR_SHARES); } /// ----------------------------------------------------------------------- @@ -138,6 +141,11 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag ); } + function _grantDelegatedStake(address operator, OperatorSet memory operatorSet, uint stake) internal { + IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet); + delegationManagerMock.setOperatorsShares(operator, strategies, stake); + } + function _registerForOperatorSets(address operator, OperatorSet[] memory operatorSets) internal { cheats.startPrank(operator); for (uint256 i; i < operatorSets.length; ++i) { @@ -159,6 +167,43 @@ contract AllocationManagerUnitTests is EigenLayerUnitTestSetup, IAllocationManag assertEq(expectedEffectBlock, allocation.effectBlock, "effectBlock != expected"); } + function _checkSlashableStake( + OperatorSet memory operatorSet, + address operator, + IStrategy[] memory strategies, + uint expectedStake + ) internal view { + uint[] memory slashableStake = allocationManager.getMinimumSlashableStake({ + operatorSet: operatorSet, + operators: operator.toArray(), + strategies: strategies, + futureBlock: uint32(block.number) + })[0]; + + for (uint i = 0; i < strategies.length; i++) { + assertEq(slashableStake[i], expectedStake, "slashableStake != expected"); + } + } + + function _checkSlashableStake( + OperatorSet memory operatorSet, + address operator, + IStrategy[] memory strategies, + uint expectedStake, + uint futureBlock + ) internal view { + uint[] memory slashableStake = allocationManager.getMinimumSlashableStake({ + operatorSet: operatorSet, + operators: operator.toArray(), + strategies: strategies, + futureBlock: uint32(futureBlock) + })[0]; + + for (uint i = 0; i < strategies.length; i++) { + assertEq(slashableStake[i], expectedStake, "slashableStake != expected"); + } + } + function _checkAllocationEvents( address operator, OperatorSet memory operatorSet, @@ -367,6 +412,7 @@ contract AllocationManagerUnitTests_Initialization_Setters is AllocationManagerU contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests { using SingleItemArrayLib for *; + using SlashingLib for *; /// ----------------------------------------------------------------------- /// slashOperator() @@ -495,6 +541,12 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests expectedPendingDiff: 0, expectedEffectBlock: 0 }); + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(75e16) + }); } /// @notice Same test as above, but fuzzes the allocation @@ -510,10 +562,13 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests SlashingParams memory slashingParams = _randSlashingParams(defaultOperator, defaultOperatorSet.id); + uint64 allocatedMagnitude = allocateParams[0].newMagnitudes[0]; uint64 expectedSlashedMagnitude = - uint64(SlashingLib.mulWadRoundUp(allocateParams[0].newMagnitudes[0], slashingParams.wadToSlash)); - uint64 expectedEncumberedMagnitude = allocateParams[0].newMagnitudes[0] - expectedSlashedMagnitude; + uint64(SlashingLib.mulWadRoundUp(allocatedMagnitude, slashingParams.wadToSlash)); + uint64 expectedEncumberedMagnitude = allocatedMagnitude - expectedSlashedMagnitude; uint64 maxMagnitudeAfterSlash = WAD - expectedSlashedMagnitude; + uint slashedStake = DEFAULT_OPERATOR_SHARES.mulWad(expectedSlashedMagnitude); + uint newSlashableMagnitude = uint(expectedEncumberedMagnitude).divWad(maxMagnitudeAfterSlash); _checkSlashEvents({ operator: defaultOperator, @@ -544,6 +599,12 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests expectedPendingDiff: 0, expectedEffectBlock: 0 }); + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: (DEFAULT_OPERATOR_SHARES - slashedStake).mulWad(newSlashableMagnitude) + }); } /** @@ -563,12 +624,37 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests allocationManager.modifyAllocations(allocateParams); cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY); + // Check slashable stake after the first allocation + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(5e17) + }); + // Allocate the other half AllocateParams[] memory allocateParams2 = _newAllocateParams(defaultOperatorSet, WAD); cheats.prank(defaultOperator); allocationManager.modifyAllocations(allocateParams2); uint32 secondAllocEffectBlock = uint32(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY); + // Check slashable stake hasn't changed after the second allocation + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(5e17) + }); + + // Check slashable stake would change after the second allocation becomes effective + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES, + futureBlock: secondAllocEffectBlock + }); + // Slash operator for 50% SlashingParams memory slashingParams = SlashingParams({ operator: defaultOperator, @@ -576,10 +662,16 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests wadToSlash: 50e16, description: "test" }); - // // avsDirectoryMock.setIsOperatorSlashable(slashingParams.operator, defaultAVS, slashingParams.operatorSetId, true); + uint64 expectedEncumberedMagnitude = 75e16; // 25e16 from first allocation, 50e16 from second uint64 magnitudeAfterSlash = 25e16; uint64 maxMagnitudeAfterSlash = 75e16; + + uint64 expectedSlashedMagnitude = + uint64(SlashingLib.mulWadRoundUp(5e17, slashingParams.wadToSlash)); + uint newSlashableMagnitude = uint(magnitudeAfterSlash).divWad(maxMagnitudeAfterSlash); + uint slashedStake = DEFAULT_OPERATOR_SHARES.mulWad(expectedSlashedMagnitude); + uint newTotalStake = DEFAULT_OPERATOR_SHARES - slashedStake; _checkSlashEvents({ operator: defaultOperator, @@ -612,6 +704,14 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests expectedEffectBlock: secondAllocEffectBlock }); + // Slashable stake should include first allocation and slashed magnitude + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: newTotalStake.mulWad(newSlashableMagnitude) + }); + cheats.roll(secondAllocEffectBlock); assertEq( @@ -626,6 +726,14 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests expectedPendingDiff: 0, expectedEffectBlock: 0 }); + + newSlashableMagnitude = allocateParams2[0].newMagnitudes[0]; + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: newTotalStake.mulWad(newSlashableMagnitude) + }); } /** @@ -641,7 +749,7 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests * 4. Calculations for `getAllocatableMagnitude` and `getAllocation` are correct * 5. Slashed amounts are rounded up to ensure magnitude is always slashed */ - function test_slashTwoOperatorSets() public { + function test_repeatUntilFullSlash() public { // Generate allocation for `strategyMock`, we allocate 100% to opSet 0 AllocateParams[] memory allocateParams = _newAllocateParams(defaultOperatorSet, WAD); @@ -649,6 +757,14 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests allocationManager.modifyAllocations(allocateParams); cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY); + // Check slashable amount after initial allocation + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES + }); + // 1. Slash operator for 99% in opSet 0 bringing their magnitude to 1e16 SlashingParams memory slashingParams = SlashingParams({ operator: defaultOperator, @@ -656,7 +772,7 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests wadToSlash: 99e16, description: "test" }); - // // avsDirectoryMock.setIsOperatorSlashable(slashingParams.operator, defaultAVS, slashingParams.operatorSetId, true); + uint64 expectedEncumberedMagnitude = 1e16; // After slashing 99%, only 1% expected encumberedMagnitude uint64 magnitudeAfterSlash = 1e16; uint64 maxMagnitudeAfterSlash = 1e16; // 1e15 is maxMagnitude @@ -688,6 +804,14 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock); assertEq(magnitudeAfterSlash, allocation.currentMagnitude, "currentMagnitude not updated"); + // Check slashable amount after first slash + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(1e16) + }); + // 2. Slash operator again for 99.99% in opSet 0 bringing their magnitude to 1e14 slashingParams = SlashingParams({ operator: defaultOperator, @@ -724,6 +848,14 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests allocation = allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock); assertEq(magnitudeAfterSlash, allocation.currentMagnitude, "currentMagnitude not updated"); + // Check slashable amount after second slash + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(1e12) + }); + // 3. Slash operator again for 99.9999999999999% in opSet 0 slashingParams = SlashingParams({ operator: defaultOperator, @@ -762,6 +894,14 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests ); allocation = allocationManager.getAllocation(defaultOperator, defaultOperatorSet, strategyMock); assertEq(magnitudeAfterSlash, allocation.currentMagnitude, "currentMagnitude not updated"); + + // Check slashable amount after final slash + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: 0 + }); } /** @@ -783,12 +923,37 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests allocationManager.modifyAllocations(allocateParams); cheats.roll(block.number + DEFAULT_OPERATOR_ALLOCATION_DELAY); + // Check slashable stake after initial allocation + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES + }); + // Deallocate half AllocateParams[] memory deallocateParams = _newAllocateParams(defaultOperatorSet, initialMagnitude / 2); cheats.prank(defaultOperator); allocationManager.modifyAllocations(deallocateParams); uint32 deallocationEffectBlock = uint32(block.number + DEALLOCATION_DELAY); + // Check slashable stake after deallocation (still pending; no change) + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES + }); + + // Check slashable stake after deallocation takes effect, before slashing + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(5e17), + futureBlock: deallocationEffectBlock + }); + // Slash operator for 25% SlashingParams memory slashingParams = SlashingParams({ operator: defaultOperator, @@ -796,7 +961,7 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests wadToSlash: 25e16, description: "test" }); - // // avsDirectoryMock.setIsOperatorSlashable(slashingParams.operator, defaultAVS, slashingParams.operatorSetId, true); + uint64 magnitudeAfterDeallocationSlash = 375e15; // 25% is slashed off of 5e17 uint64 expectedEncumberedMagnitude = 75e16; // 25e16 is slashed. 75e16 is encumbered uint64 magnitudeAfterSlash = 75e16; @@ -834,8 +999,26 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests expectedEffectBlock: deallocationEffectBlock }); + // Check slashable stake after slash + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(expectedEncumberedMagnitude) + }); + + // Check slashable stake after deallocation takes effect + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(magnitudeAfterDeallocationSlash), + futureBlock: deallocationEffectBlock + }); + // Check storage after complete modification cheats.roll(deallocationEffectBlock); + allocationManager.clearDeallocationQueue(defaultOperator, defaultStrategies, _maxNumToClear()); assertEq( @@ -850,6 +1033,14 @@ contract AllocationManagerUnitTests_SlashOperator is AllocationManagerUnitTests expectedPendingDiff: 0, expectedEffectBlock: 0 }); + + // Check slashable stake after slash and deallocation + _checkSlashableStake({ + operatorSet: defaultOperatorSet, + operator: defaultOperator, + strategies: defaultStrategies, + expectedStake: DEFAULT_OPERATOR_SHARES.mulWad(magnitudeAfterDeallocationSlash) + }); } /** diff --git a/src/test/utils/SingleItemArrayLib.sol b/src/test/utils/SingleItemArrayLib.sol index 8fbadbacd..ad9d47fdf 100644 --- a/src/test/utils/SingleItemArrayLib.sol +++ b/src/test/utils/SingleItemArrayLib.sol @@ -82,6 +82,13 @@ library SingleItemArrayLib { /// EigenLayer Types /// ----------------------------------------------------------------------- + function toArray( + address a + ) internal pure returns (address[] memory array) { + array = new address[](1); + array[0] = a; + } + function toArray( IERC20 token ) internal pure returns (IERC20[] memory array) {