diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index ceacd1da2..ef940d4c5 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -776,6 +776,40 @@ contract DelegationManager is } } + /// @dev Get the shares from a queued withdrawal. + function _getSharesByWithdrawalRoot( + bytes32 withdrawalRoot + ) internal view returns (Withdrawal memory withdrawal, uint256[] memory shares) { + withdrawal = queuedWithdrawals[withdrawalRoot]; + shares = new uint256[](withdrawal.strategies.length); + + uint32 slashableUntil = withdrawal.startBlock + MIN_WITHDRAWAL_DELAY_BLOCKS; + + // If the slashableUntil block is in the past, read the slashing factors at that block. + // Otherwise, read the current slashing factors. Note that if the slashableUntil block is the current block + // or in the future, then the slashing factors are still subject to change before the withdrawal is completable, + // which may result in fewer shares being withdrawn. + uint256[] memory slashingFactors = slashableUntil < uint32(block.number) + ? _getSlashingFactorsAtBlock({ + staker: withdrawal.staker, + operator: withdrawal.delegatedTo, + strategies: withdrawal.strategies, + blockNumber: slashableUntil + }) + : _getSlashingFactors({ + staker: withdrawal.staker, + operator: withdrawal.delegatedTo, + strategies: withdrawal.strategies + }); + + for (uint256 j; j < withdrawal.strategies.length; ++j) { + shares[j] = SlashingLib.scaleForCompleteWithdrawal({ + scaledShares: withdrawal.scaledShares[j], + slashingFactor: slashingFactors[j] + }); + } + } + /// @dev Depending on the strategy used, determine which ShareManager contract to make external calls to function _getShareManager( IStrategy strategy @@ -914,6 +948,13 @@ contract DelegationManager is return queuedWithdrawals[withdrawalRoot]; } + /// @inheritdoc IDelegationManager + function getSharesFromQueuedWithdrawal( + bytes32 withdrawalRoot + ) external view returns (uint256[] memory shares) { + (, shares) = _getSharesByWithdrawalRoot(withdrawalRoot); + } + /// @inheritdoc IDelegationManager function getQueuedWithdrawals( address staker @@ -924,37 +965,8 @@ contract DelegationManager is withdrawals = new Withdrawal[](totalQueued); shares = new uint256[][](totalQueued); - address operator = delegatedTo[staker]; - for (uint256 i; i < totalQueued; ++i) { - withdrawals[i] = queuedWithdrawals[withdrawalRoots[i]]; - shares[i] = new uint256[](withdrawals[i].strategies.length); - - uint32 slashableUntil = withdrawals[i].startBlock + MIN_WITHDRAWAL_DELAY_BLOCKS; - - uint256[] memory slashingFactors; - // If slashableUntil block is in the past, read the slashing factors at that block - // Otherwise read the current slashing factors. Note that if the slashableUntil block is the current block - // or in the future then the slashing factors are still subject to change before the withdrawal is completable - // and the shares withdrawn to be less - if (slashableUntil < uint32(block.number)) { - slashingFactors = _getSlashingFactorsAtBlock({ - staker: staker, - operator: operator, - strategies: withdrawals[i].strategies, - blockNumber: slashableUntil - }); - } else { - slashingFactors = - _getSlashingFactors({staker: staker, operator: operator, strategies: withdrawals[i].strategies}); - } - - for (uint256 j; j < withdrawals[i].strategies.length; ++j) { - shares[i][j] = SlashingLib.scaleForCompleteWithdrawal({ - scaledShares: withdrawals[i].scaledShares[j], - slashingFactor: slashingFactors[j] - }); - } + (withdrawals[i], shares[i]) = _getSharesByWithdrawalRoot(withdrawalRoots[i]); } } diff --git a/src/contracts/interfaces/IDelegationManager.sol b/src/contracts/interfaces/IDelegationManager.sol index d606140b2..03df3ce3c 100644 --- a/src/contracts/interfaces/IDelegationManager.sol +++ b/src/contracts/interfaces/IDelegationManager.sol @@ -477,11 +477,27 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele bytes32 withdrawalRoot ) external view returns (Withdrawal memory); - /// @notice Returns a list of pending queued withdrawals for a `staker`, and the `shares` to be withdrawn. + /** + * @notice Returns all queued withdrawals and their corresponding shares for a staker. + * @param staker The address of the staker to query withdrawals for. + * @return withdrawals Array of Withdrawal structs containing details about each queued withdrawal. + * @return shares 2D array of shares, where each inner array corresponds to the strategies in the withdrawal. + * @dev The shares are what a user would receive from completing a queued withdrawal, assuming all slashings are applied. + */ function getQueuedWithdrawals( address staker ) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares); + /** + * @notice Returns the withdrawal details and corresponding shares for a specific queued withdrawal. + * @param withdrawalRoot The hash identifying the queued withdrawal. + * @return shares Array of shares corresponding to each strategy in the withdrawal. + * @dev The shares are what a user would receive from completing a queued withdrawal, assuming all slashings are applied. + */ + function getSharesFromQueuedWithdrawal( + bytes32 withdrawalRoot + ) external view returns (uint256[] memory shares); + /// @notice Returns a list of queued withdrawal roots for the `staker`. /// NOTE that this only returns withdrawals queued AFTER the slashing release. function getQueuedWithdrawalRoots( diff --git a/src/test/unit/DelegationUnit.t.sol b/src/test/unit/DelegationUnit.t.sol index 826bde4d3..1a3845fae 100644 --- a/src/test/unit/DelegationUnit.t.sol +++ b/src/test/unit/DelegationUnit.t.sol @@ -8813,4 +8813,162 @@ contract DelegationManagerUnitTests_getQueuedWithdrawals is DelegationManagerUni "block.number should be the completableBlock" ); } + + function test_getQueuedWithdrawals_UsesCorrectOperatorMagnitude() public { + // Alice deposits 100 shares into strategy + uint256 depositAmount = 100e18; + _depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256()); + + // Register operator with magnitude of 0.5 and delegate Alice to them + _registerOperatorWithBaseDetails(defaultOperator); + _delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator); + _setOperatorMagnitude(defaultOperator, strategyMock, 0.5 ether); + + // Alice queues withdrawal of all 100 shares while operator magnitude is 0.5 + // This means she should get back 50 shares (100 * 0.5) + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + bytes32 withdrawalRoot + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: defaultStaker, + strategy: strategyMock, + depositSharesToWithdraw: depositAmount + }); + + cheats.prank(defaultStaker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + + // Alice undelegates, which would normally update operator's magnitude to 1.0 + // This tests that the withdrawal still uses the original 0.5 magnitude from when it was queued + cheats.prank(defaultStaker); + delegationManager.undelegate(defaultStaker); + + // Get shares from withdrawal - should return 50 shares (100 * 0.5) using original magnitude + // rather than incorrectly returning 100 shares (100 * 1.0) using new magnitude + uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot); + assertEq(shares[0], 50e18, "shares should be 50e18 (100e18 * 0.5) using original magnitude"); + } +} + +contract DelegationManagerUnitTests_getSharesFromQueuedWithdrawal is DelegationManagerUnitTests { + using ArrayLib for *; + using SlashingLib for *; + + function test_getSharesFromQueuedWithdrawal_Correctness(Randomness r) public rand(r) { + // Set up initial deposit + uint256 depositAmount = r.Uint256(1 ether, 100 ether); + _depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256()); + + // Register operator and delegate + _registerOperatorWithBaseDetails(defaultOperator); + _delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator); + + // Queue withdrawal + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + bytes32 withdrawalRoot + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: defaultStaker, + strategy: strategyMock, + depositSharesToWithdraw: depositAmount + }); + + cheats.prank(defaultStaker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + + // Get shares from queued withdrawal + uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot); + + // Verify withdrawal details match + assertEq(shares.length, 1, "incorrect shares array length"); + assertEq(shares[0], depositAmount, "incorrect shares amount"); + } + + function test_getSharesFromQueuedWithdrawal_AfterSlashing(Randomness r) public rand(r) { + // Set up initial deposit + uint256 depositAmount = r.Uint256(1 ether, 100 ether); + _depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256()); + + // Register operator and delegate + _registerOperatorWithBaseDetails(defaultOperator); + _delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator); + + // Queue withdrawal + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + bytes32 withdrawalRoot + ) = _setUpQueueWithdrawalsSingleStrat({ + staker: defaultStaker, + strategy: strategyMock, + depositSharesToWithdraw: depositAmount + }); + + cheats.prank(defaultStaker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + + // Slash operator by 50% + _setOperatorMagnitude(defaultOperator, strategyMock, 0.5 ether); + cheats.prank(address(allocationManagerMock)); + delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0.5 ether); + + // Get shares from queued withdrawal + uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot); + + // Verify withdrawal details match and shares are slashed + assertEq(shares.length, 1, "incorrect shares array length"); + assertEq(shares[0], depositAmount / 2, "shares not properly slashed"); + } + + function test_getSharesFromQueuedWithdrawal_NonexistentWithdrawal() public { + bytes32 nonexistentRoot = bytes32(uint256(1)); + uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(nonexistentRoot); + assertEq(shares.length, 0, "shares array should be empty"); + } + + function test_getSharesFromQueuedWithdrawal_MultipleStrategies(Randomness r) public rand(r) { + // Set up multiple strategies with deposits + uint256 numStrategies = r.Uint256(2, 5); + uint256[] memory depositShares = r.Uint256Array({ + len: numStrategies, + min: 1 ether, + max: 100 ether + }); + + IStrategy[] memory strategies = _deployAndDepositIntoStrategies(defaultStaker, depositShares, false); + + // Register operator and delegate + _registerOperatorWithBaseDetails(defaultOperator); + _delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator); + + // Queue withdrawals for multiple strategies + ( + QueuedWithdrawalParams[] memory queuedWithdrawalParams, + Withdrawal memory withdrawal, + bytes32 withdrawalRoot + ) = _setUpQueueWithdrawals({ + staker: defaultStaker, + strategies: strategies, + depositWithdrawalAmounts: depositShares + }); + + cheats.prank(defaultStaker); + delegationManager.queueWithdrawals(queuedWithdrawalParams); + + // Get shares from queued withdrawal + uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot); + + // Verify withdrawal details and shares for each strategy + assertEq(shares.length, numStrategies, "incorrect shares array length"); + for (uint256 i = 0; i < numStrategies; i++) { + assertEq(shares[i], depositShares[i], "incorrect shares amount for strategy"); + } + } + + function testFuzz_getSharesFromQueuedWithdrawal_EmptyWithdrawal(bytes32 withdrawalRoot) public { + uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot); + assertEq(shares.length, 0, "sanity check"); + } }