diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index 348add787..f5ce928fc 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -29,6 +29,7 @@ contract DelegationManager is SignatureUtils { using SlashingLib for *; + using EnumerableSet for EnumerableSet.Bytes32Set; // @notice Simple permission for functions that are only callable by the StrategyManager contract OR by the EigenPodManagerContract modifier onlyStrategyManagerOrEigenPodManager() { @@ -275,6 +276,8 @@ contract DelegationManager is sharesToWithdraw: params[i].shares, maxMagnitudes: maxMagnitudes }); + + pendingWithdrawals[withdrawalRoots[i]] = true; } return withdrawalRoots; @@ -295,11 +298,26 @@ contract DelegationManager is IERC20[][] calldata tokens, bool[] calldata receiveAsTokens ) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant { - for (uint256 i = 0; i < withdrawals.length; ++i) { + uint256 n = withdrawals.length; + for (uint256 i; i < n; ++i) { _completeQueuedWithdrawal(withdrawals[i], tokens[i], receiveAsTokens[i]); } } + /// @inheritdoc IDelegationManager + function completeQueuedWithdrawals( + IERC20[][] calldata tokens, + bool[] calldata receiveAsTokens, + uint256 numToComplete + ) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant { + EnumerableSet.Bytes32Set storage withdrawalRoots = _stakerQueuedWithdrawalRoots[msg.sender]; + uint256 totalQueued = withdrawalRoots.length(); + numToComplete = numToComplete > totalQueued ? totalQueued : numToComplete; + for (uint256 i; i < numToComplete; ++i) { + _completeQueuedWithdrawal(queuedWithdrawals[withdrawalRoots.at(i)], tokens[i], receiveAsTokens[i]); + } + } + /// @inheritdoc IDelegationManager function increaseDelegatedShares( address staker, @@ -494,7 +512,7 @@ contract DelegationManager is * and added back to the operator's delegatedShares. */ function _completeQueuedWithdrawal( - Withdrawal calldata withdrawal, + Withdrawal memory withdrawal, IERC20[] calldata tokens, bool receiveAsTokens ) internal { @@ -541,8 +559,12 @@ contract DelegationManager is } } - // Remove `withdrawalRoot` from pending roots + _stakerQueuedWithdrawalRoots[withdrawal.staker].remove(withdrawalRoot); + + delete queuedWithdrawals[withdrawalRoot]; + delete pendingWithdrawals[withdrawalRoot]; + emit SlashingWithdrawalCompleted(withdrawalRoot); } @@ -678,10 +700,12 @@ contract DelegationManager is bytes32 withdrawalRoot = calculateWithdrawalRoot(withdrawal); - // Place withdrawal in queue - pendingWithdrawals[withdrawalRoot] = true; + _stakerQueuedWithdrawalRoots[staker].add(withdrawalRoot); + + queuedWithdrawals[withdrawalRoot] = withdrawal; emit SlashingWithdrawalQueued(withdrawalRoot, withdrawal, sharesToWithdraw); + return withdrawalRoot; } @@ -824,6 +848,33 @@ contract DelegationManager is return (strategies, shares); } + + /// @inheritdoc IDelegationManager + function getQueuedWithdrawals( + address staker + ) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares) { + bytes32[] memory withdrawalRoots = _stakerQueuedWithdrawalRoots[staker].values(); + uint256 totalQueued = withdrawalRoots.length; + + withdrawals = new Withdrawal[](totalQueued); + shares = new uint256[][](totalQueued); + + address operator = delegatedTo[staker]; + + for (uint256 i; i < totalQueued; ++i) { + withdrawals[i] = queuedWithdrawals[withdrawalRoots[i]]; + + uint64[] memory operatorMagnitudes = + allocationManager.getMaxMagnitudes(operator, withdrawals[i].strategies); + + for (uint256 j; j < withdrawals[i].strategies.length; ++j) { + StakerScalingFactors memory ssf = stakerScalingFactor[staker][withdrawals[i].strategies[j]]; + + shares[i][j] = + withdrawals[i].scaledShares[j].scaleSharesForCompleteWithdrawal(ssf, operatorMagnitudes[i]); + } + } + } /// @inheritdoc IDelegationManager function calculateWithdrawalRoot( diff --git a/src/contracts/core/DelegationManagerStorage.sol b/src/contracts/core/DelegationManagerStorage.sol index 350d70936..0a020aa44 100644 --- a/src/contracts/core/DelegationManagerStorage.sol +++ b/src/contracts/core/DelegationManagerStorage.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import "../libraries/SlashingLib.sol"; import "../interfaces/IDelegationManager.sol"; import "../interfaces/IAVSDirectory.sol"; @@ -86,7 +88,8 @@ abstract contract DelegationManagerStorage is IDelegationManager { /// @dev Do not remove, deprecated storage. uint256 private __deprecated_minWithdrawalDelayBlocks; - /// @notice Returns whether a given `withdrawalRoot` has a pending withdrawal. + /// @dev Returns whether a withdrawal is pending for a given `withdrawalRoot`. + /// @dev This variable will be deprecated in the future, values should only be read or deleted. mapping(bytes32 withdrawalRoot => bool pending) public pendingWithdrawals; /// @notice Returns the total number of withdrawals that have been queued for a given `staker`. @@ -104,6 +107,15 @@ abstract contract DelegationManagerStorage is IDelegationManager { /// @dev We do not need the `beaconChainScalingFactor` for non-beaconchain strategies, but it's nicer syntactically to keep it. mapping(address staker => mapping(IStrategy strategy => StakerScalingFactors)) public stakerScalingFactor; + /// @notice Returns a list of queued withdrawals for a given `staker`. + /// @dev Entrys are removed when the withdrawal is completed. + /// @dev This variable only reflects withdrawals that were made after the slashing release. + mapping(address staker => EnumerableSet.Bytes32Set withdrawalRoots) internal _stakerQueuedWithdrawalRoots; + + /// @notice Returns the details of a queued withdrawal for a given `staker` and `withdrawalRoot`. + /// @dev This variable only reflects withdrawals that were made after the slashing release. + mapping(bytes32 withdrawalRoot => Withdrawal withdrawal) public queuedWithdrawals; + // Construction constructor( @@ -125,5 +137,5 @@ abstract contract DelegationManagerStorage is IDelegationManager { * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[38] private __gap; + uint256[36] private __gap; } diff --git a/src/contracts/interfaces/IDelegationManager.sol b/src/contracts/interfaces/IDelegationManager.sol index a6b3029f4..31aeb6dea 100644 --- a/src/contracts/interfaces/IDelegationManager.sol +++ b/src/contracts/interfaces/IDelegationManager.sol @@ -326,10 +326,22 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele ) external returns (bytes32[] memory); /** - * @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer` - * Withdrawals remain slashable during the withdrawal delay period and the actual withdrawn shares are calculated - * based off the scaledShares. - * @param withdrawal The Withdrawal to complete. + * @notice Used to complete the all queued withdrawals. + * Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer` + * @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array. + * @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean. + * @param numToComplete The number of withdrawals to complete. This must be less than or equal to the number of queued withdrawals. + * @dev See `completeQueuedWithdrawal` for relevant dev tags + */ + function completeQueuedWithdrawals( + IERC20[][] calldata tokens, + bool[] calldata receiveAsTokens, + uint256 numToComplete + ) external; + + /** + * @notice Used to complete the lastest queued withdrawal. + * @param withdrawal The withdrawal to complete. * @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `withdrawal.strategies` array. * @param receiveAsTokens If true, the shares calculated to be withdrawn will be withdrawn from the specified strategies themselves * and sent to the caller, through calls to `withdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies @@ -345,9 +357,9 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele ) external; /** - * @notice Array-ified version of `completeQueuedWithdrawal`. + * @notice Used to complete the all queued withdrawals. * Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer` - * @param withdrawals The Withdrawals to complete. + * @param withdrawals Array of Withdrawals to complete. See `completeQueuedWithdrawal` for the usage of a single Withdrawal. * @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array. * @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean. * @dev See `completeQueuedWithdrawal` for relevant dev tags @@ -541,6 +553,11 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele */ function MIN_WITHDRAWAL_DELAY_BLOCKS() external view returns (uint32); + /// @notice Returns a list of pending queued withdrawals for a `staker`, and the `shares` to be withdrawn. + function getQueuedWithdrawals( + address staker + ) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares); + /// @notice Returns the keccak256 hash of `withdrawal`. function calculateWithdrawalRoot( Withdrawal memory withdrawal diff --git a/src/contracts/libraries/SlashingLib.sol b/src/contracts/libraries/SlashingLib.sol index ece39f704..ae861418d 100644 --- a/src/contracts/libraries/SlashingLib.sol +++ b/src/contracts/libraries/SlashingLib.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/SafeCastUpgradeable.sol"; /// @dev the stakerScalingFactor and operatorMagnitude have initial default values to 1e18 as "1" /// to preserve precision with uint256 math. We use `WAD` where these variables are used @@ -22,12 +23,10 @@ uint64 constant WAD = 1e18; * Note that `withdrawal.scaledShares` is scaled for the beaconChainETHStrategy to divide by the beaconChainScalingFactor upon queueing * and multiply by the beaconChainScalingFactor upon withdrawal */ - struct StakerScalingFactors { - uint256 depositScalingFactor; - // we need to know if the beaconChainScalingFactor is set because it can be set to 0 through 100% slashing - bool isBeaconChainScalingFactorSet; + uint184 depositScalingFactor; uint64 beaconChainScalingFactor; + bool isBeaconChainScalingFactorSet; } using SlashingLib for StakerScalingFactors global; @@ -36,6 +35,7 @@ using SlashingLib for StakerScalingFactors global; library SlashingLib { using Math for uint256; using SlashingLib for uint256; + using SafeCastUpgradeable for uint256; // WAD MATH @@ -119,7 +119,8 @@ library SlashingLib { /// forgefmt: disable-next-item ssf.depositScalingFactor = uint256(WAD) .divWad(ssf.getBeaconChainScalingFactor()) - .divWad(maxMagnitude); + .divWad(maxMagnitude) + .toUint184(); return; } /** @@ -150,10 +151,11 @@ library SlashingLib { // Step 3: Calculate newStakerDepositScalingFactor /// forgefmt: disable-next-item - uint256 newStakerDepositScalingFactor = newShares + uint184 newStakerDepositScalingFactor = newShares .divWad(existingDepositShares + addedShares) .divWad(maxMagnitude) - .divWad(uint256(ssf.getBeaconChainScalingFactor())); + .divWad(uint256(ssf.getBeaconChainScalingFactor())) + .toUint184(); ssf.depositScalingFactor = newStakerDepositScalingFactor; } diff --git a/src/test/unit/DelegationUnit.t.sol b/src/test/unit/DelegationUnit.t.sol index 16e08df6e..306498c7a 100644 --- a/src/test/unit/DelegationUnit.t.sol +++ b/src/test/unit/DelegationUnit.t.sol @@ -426,7 +426,7 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag strategyArray[0] = strategy; // Set scaling factors - (uint256 depositScalingFactor, bool isBeaconChainScalingFactorSet, uint64 beaconChainScalingFactor) = delegationManager.stakerScalingFactor(staker, strategy); + (uint184 depositScalingFactor, uint64 beaconChainScalingFactor, bool isBeaconChainScalingFactorSet) = delegationManager.stakerScalingFactor(staker, strategy); StakerScalingFactors memory stakerScalingFactor = StakerScalingFactors({ depositScalingFactor: depositScalingFactor, isBeaconChainScalingFactorSet: isBeaconChainScalingFactorSet, @@ -3933,6 +3933,15 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage cheats.expectRevert(IPausable.CurrentlyPaused.selector); delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false); + + IERC20[][] memory tokensArray = new IERC20[][](1); + tokensArray[0] = tokens; + + bool[] memory receiveAsTokens = new bool[](1); + receiveAsTokens[0] = false; + + cheats.expectRevert(IPausable.CurrentlyPaused.selector); + delegationManager.completeQueuedWithdrawals(tokensArray, receiveAsTokens, 1); } function test_Revert_WhenInputArrayLengthMismatch() public { @@ -3952,10 +3961,21 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage // resize tokens array tokens = new IERC20[](0); + cheats.prank(defaultStaker); cheats.expectRevert(IDelegationManagerErrors.InputArrayLengthMismatch.selector); delegationManager.completeQueuedWithdrawal(withdrawal, tokens, false); - } + IERC20[][] memory tokensArray = new IERC20[][](1); + tokensArray[0] = tokens; + + bool[] memory receiveAsTokens = new bool[](1); + receiveAsTokens[0] = false; + + cheats.prank(defaultStaker); + cheats.expectRevert(IDelegationManagerErrors.InputArrayLengthMismatch.selector); + delegationManager.completeQueuedWithdrawals(tokensArray, receiveAsTokens, 1); + } + function test_Revert_WhenWithdrawerNotCaller(address invalidCaller) filterFuzzedAddressInputs(invalidCaller) public { cheats.assume(invalidCaller != defaultStaker); @@ -4031,6 +4051,16 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage cheats.expectRevert(IDelegationManagerErrors.WithdrawalDelayNotElapsed.selector); cheats.prank(defaultStaker); delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens); + + IERC20[][] memory tokensArray = new IERC20[][](1); + tokensArray[0] = tokens; + + bool[] memory receiveAsTokensArray = new bool[](1); + receiveAsTokensArray[0] = false; + + cheats.expectRevert(IDelegationManagerErrors.WithdrawalDelayNotElapsed.selector); + cheats.prank(defaultStaker); + delegationManager.completeQueuedWithdrawals(tokensArray, receiveAsTokensArray, 1); } /**