Skip to content

Commit 13fb017

Browse files
0xClandestineypatil12
authored andcommitted
feat: add getSharesFromQueuedWithdrawal (#1078)
* feat: add `getSharesFromQueuedWithdrawal` * test: passing * refactor(review): improve natspec * refactor(review): maintain original interface * test(review): add unit tests * refactor(review): test empty * refactor(review): test empty * refactor(review): remove returned `Withdrawal` * fix: use operator from `Withdrawal` * test: use operator from `Withdrawal` * chore: forge fmt
1 parent 9779fae commit 13fb017

File tree

3 files changed

+217
-31
lines changed

3 files changed

+217
-31
lines changed

src/contracts/core/DelegationManager.sol

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,40 @@ contract DelegationManager is
776776
}
777777
}
778778

779+
/// @dev Get the shares from a queued withdrawal.
780+
function _getSharesByWithdrawalRoot(
781+
bytes32 withdrawalRoot
782+
) internal view returns (Withdrawal memory withdrawal, uint256[] memory shares) {
783+
withdrawal = queuedWithdrawals[withdrawalRoot];
784+
shares = new uint256[](withdrawal.strategies.length);
785+
786+
uint32 slashableUntil = withdrawal.startBlock + MIN_WITHDRAWAL_DELAY_BLOCKS;
787+
788+
// If the slashableUntil block is in the past, read the slashing factors at that block.
789+
// Otherwise, read the current slashing factors. Note that if the slashableUntil block is the current block
790+
// or in the future, then the slashing factors are still subject to change before the withdrawal is completable,
791+
// which may result in fewer shares being withdrawn.
792+
uint256[] memory slashingFactors = slashableUntil < uint32(block.number)
793+
? _getSlashingFactorsAtBlock({
794+
staker: withdrawal.staker,
795+
operator: withdrawal.delegatedTo,
796+
strategies: withdrawal.strategies,
797+
blockNumber: slashableUntil
798+
})
799+
: _getSlashingFactors({
800+
staker: withdrawal.staker,
801+
operator: withdrawal.delegatedTo,
802+
strategies: withdrawal.strategies
803+
});
804+
805+
for (uint256 j; j < withdrawal.strategies.length; ++j) {
806+
shares[j] = SlashingLib.scaleForCompleteWithdrawal({
807+
scaledShares: withdrawal.scaledShares[j],
808+
slashingFactor: slashingFactors[j]
809+
});
810+
}
811+
}
812+
779813
/// @dev Depending on the strategy used, determine which ShareManager contract to make external calls to
780814
function _getShareManager(
781815
IStrategy strategy
@@ -914,6 +948,13 @@ contract DelegationManager is
914948
return queuedWithdrawals[withdrawalRoot];
915949
}
916950

951+
/// @inheritdoc IDelegationManager
952+
function getSharesFromQueuedWithdrawal(
953+
bytes32 withdrawalRoot
954+
) external view returns (uint256[] memory shares) {
955+
(, shares) = _getSharesByWithdrawalRoot(withdrawalRoot);
956+
}
957+
917958
/// @inheritdoc IDelegationManager
918959
function getQueuedWithdrawals(
919960
address staker
@@ -924,37 +965,8 @@ contract DelegationManager is
924965
withdrawals = new Withdrawal[](totalQueued);
925966
shares = new uint256[][](totalQueued);
926967

927-
address operator = delegatedTo[staker];
928-
929968
for (uint256 i; i < totalQueued; ++i) {
930-
withdrawals[i] = queuedWithdrawals[withdrawalRoots[i]];
931-
shares[i] = new uint256[](withdrawals[i].strategies.length);
932-
933-
uint32 slashableUntil = withdrawals[i].startBlock + MIN_WITHDRAWAL_DELAY_BLOCKS;
934-
935-
uint256[] memory slashingFactors;
936-
// If slashableUntil block is in the past, read the slashing factors at that block
937-
// Otherwise read the current slashing factors. Note that if the slashableUntil block is the current block
938-
// or in the future then the slashing factors are still subject to change before the withdrawal is completable
939-
// and the shares withdrawn to be less
940-
if (slashableUntil < uint32(block.number)) {
941-
slashingFactors = _getSlashingFactorsAtBlock({
942-
staker: staker,
943-
operator: operator,
944-
strategies: withdrawals[i].strategies,
945-
blockNumber: slashableUntil
946-
});
947-
} else {
948-
slashingFactors =
949-
_getSlashingFactors({staker: staker, operator: operator, strategies: withdrawals[i].strategies});
950-
}
951-
952-
for (uint256 j; j < withdrawals[i].strategies.length; ++j) {
953-
shares[i][j] = SlashingLib.scaleForCompleteWithdrawal({
954-
scaledShares: withdrawals[i].scaledShares[j],
955-
slashingFactor: slashingFactors[j]
956-
});
957-
}
969+
(withdrawals[i], shares[i]) = _getSharesByWithdrawalRoot(withdrawalRoots[i]);
958970
}
959971
}
960972

src/contracts/interfaces/IDelegationManager.sol

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,11 +477,27 @@ interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDele
477477
bytes32 withdrawalRoot
478478
) external view returns (Withdrawal memory);
479479

480-
/// @notice Returns a list of pending queued withdrawals for a `staker`, and the `shares` to be withdrawn.
480+
/**
481+
* @notice Returns all queued withdrawals and their corresponding shares for a staker.
482+
* @param staker The address of the staker to query withdrawals for.
483+
* @return withdrawals Array of Withdrawal structs containing details about each queued withdrawal.
484+
* @return shares 2D array of shares, where each inner array corresponds to the strategies in the withdrawal.
485+
* @dev The shares are what a user would receive from completing a queued withdrawal, assuming all slashings are applied.
486+
*/
481487
function getQueuedWithdrawals(
482488
address staker
483489
) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares);
484490

491+
/**
492+
* @notice Returns the withdrawal details and corresponding shares for a specific queued withdrawal.
493+
* @param withdrawalRoot The hash identifying the queued withdrawal.
494+
* @return shares Array of shares corresponding to each strategy in the withdrawal.
495+
* @dev The shares are what a user would receive from completing a queued withdrawal, assuming all slashings are applied.
496+
*/
497+
function getSharesFromQueuedWithdrawal(
498+
bytes32 withdrawalRoot
499+
) external view returns (uint256[] memory shares);
500+
485501
/// @notice Returns a list of queued withdrawal roots for the `staker`.
486502
/// NOTE that this only returns withdrawals queued AFTER the slashing release.
487503
function getQueuedWithdrawalRoots(

src/test/unit/DelegationUnit.t.sol

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8813,4 +8813,162 @@ contract DelegationManagerUnitTests_getQueuedWithdrawals is DelegationManagerUni
88138813
"block.number should be the completableBlock"
88148814
);
88158815
}
8816+
8817+
function test_getQueuedWithdrawals_UsesCorrectOperatorMagnitude() public {
8818+
// Alice deposits 100 shares into strategy
8819+
uint256 depositAmount = 100e18;
8820+
_depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256());
8821+
8822+
// Register operator with magnitude of 0.5 and delegate Alice to them
8823+
_registerOperatorWithBaseDetails(defaultOperator);
8824+
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
8825+
_setOperatorMagnitude(defaultOperator, strategyMock, 0.5 ether);
8826+
8827+
// Alice queues withdrawal of all 100 shares while operator magnitude is 0.5
8828+
// This means she should get back 50 shares (100 * 0.5)
8829+
(
8830+
QueuedWithdrawalParams[] memory queuedWithdrawalParams,
8831+
Withdrawal memory withdrawal,
8832+
bytes32 withdrawalRoot
8833+
) = _setUpQueueWithdrawalsSingleStrat({
8834+
staker: defaultStaker,
8835+
strategy: strategyMock,
8836+
depositSharesToWithdraw: depositAmount
8837+
});
8838+
8839+
cheats.prank(defaultStaker);
8840+
delegationManager.queueWithdrawals(queuedWithdrawalParams);
8841+
8842+
// Alice undelegates, which would normally update operator's magnitude to 1.0
8843+
// This tests that the withdrawal still uses the original 0.5 magnitude from when it was queued
8844+
cheats.prank(defaultStaker);
8845+
delegationManager.undelegate(defaultStaker);
8846+
8847+
// Get shares from withdrawal - should return 50 shares (100 * 0.5) using original magnitude
8848+
// rather than incorrectly returning 100 shares (100 * 1.0) using new magnitude
8849+
uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);
8850+
assertEq(shares[0], 50e18, "shares should be 50e18 (100e18 * 0.5) using original magnitude");
8851+
}
8852+
}
8853+
8854+
contract DelegationManagerUnitTests_getSharesFromQueuedWithdrawal is DelegationManagerUnitTests {
8855+
using ArrayLib for *;
8856+
using SlashingLib for *;
8857+
8858+
function test_getSharesFromQueuedWithdrawal_Correctness(Randomness r) public rand(r) {
8859+
// Set up initial deposit
8860+
uint256 depositAmount = r.Uint256(1 ether, 100 ether);
8861+
_depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256());
8862+
8863+
// Register operator and delegate
8864+
_registerOperatorWithBaseDetails(defaultOperator);
8865+
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
8866+
8867+
// Queue withdrawal
8868+
(
8869+
QueuedWithdrawalParams[] memory queuedWithdrawalParams,
8870+
Withdrawal memory withdrawal,
8871+
bytes32 withdrawalRoot
8872+
) = _setUpQueueWithdrawalsSingleStrat({
8873+
staker: defaultStaker,
8874+
strategy: strategyMock,
8875+
depositSharesToWithdraw: depositAmount
8876+
});
8877+
8878+
cheats.prank(defaultStaker);
8879+
delegationManager.queueWithdrawals(queuedWithdrawalParams);
8880+
8881+
// Get shares from queued withdrawal
8882+
uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);
8883+
8884+
// Verify withdrawal details match
8885+
assertEq(shares.length, 1, "incorrect shares array length");
8886+
assertEq(shares[0], depositAmount, "incorrect shares amount");
8887+
}
8888+
8889+
function test_getSharesFromQueuedWithdrawal_AfterSlashing(Randomness r) public rand(r) {
8890+
// Set up initial deposit
8891+
uint256 depositAmount = r.Uint256(1 ether, 100 ether);
8892+
_depositIntoStrategies(defaultStaker, strategyMock.toArray(), depositAmount.toArrayU256());
8893+
8894+
// Register operator and delegate
8895+
_registerOperatorWithBaseDetails(defaultOperator);
8896+
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
8897+
8898+
// Queue withdrawal
8899+
(
8900+
QueuedWithdrawalParams[] memory queuedWithdrawalParams,
8901+
Withdrawal memory withdrawal,
8902+
bytes32 withdrawalRoot
8903+
) = _setUpQueueWithdrawalsSingleStrat({
8904+
staker: defaultStaker,
8905+
strategy: strategyMock,
8906+
depositSharesToWithdraw: depositAmount
8907+
});
8908+
8909+
cheats.prank(defaultStaker);
8910+
delegationManager.queueWithdrawals(queuedWithdrawalParams);
8911+
8912+
// Slash operator by 50%
8913+
_setOperatorMagnitude(defaultOperator, strategyMock, 0.5 ether);
8914+
cheats.prank(address(allocationManagerMock));
8915+
delegationManager.slashOperatorShares(defaultOperator, strategyMock, WAD, 0.5 ether);
8916+
8917+
// Get shares from queued withdrawal
8918+
uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);
8919+
8920+
// Verify withdrawal details match and shares are slashed
8921+
assertEq(shares.length, 1, "incorrect shares array length");
8922+
assertEq(shares[0], depositAmount / 2, "shares not properly slashed");
8923+
}
8924+
8925+
function test_getSharesFromQueuedWithdrawal_NonexistentWithdrawal() public {
8926+
bytes32 nonexistentRoot = bytes32(uint256(1));
8927+
uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(nonexistentRoot);
8928+
assertEq(shares.length, 0, "shares array should be empty");
8929+
}
8930+
8931+
function test_getSharesFromQueuedWithdrawal_MultipleStrategies(Randomness r) public rand(r) {
8932+
// Set up multiple strategies with deposits
8933+
uint256 numStrategies = r.Uint256(2, 5);
8934+
uint256[] memory depositShares = r.Uint256Array({
8935+
len: numStrategies,
8936+
min: 1 ether,
8937+
max: 100 ether
8938+
});
8939+
8940+
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(defaultStaker, depositShares, false);
8941+
8942+
// Register operator and delegate
8943+
_registerOperatorWithBaseDetails(defaultOperator);
8944+
_delegateToOperatorWhoAcceptsAllStakers(defaultStaker, defaultOperator);
8945+
8946+
// Queue withdrawals for multiple strategies
8947+
(
8948+
QueuedWithdrawalParams[] memory queuedWithdrawalParams,
8949+
Withdrawal memory withdrawal,
8950+
bytes32 withdrawalRoot
8951+
) = _setUpQueueWithdrawals({
8952+
staker: defaultStaker,
8953+
strategies: strategies,
8954+
depositWithdrawalAmounts: depositShares
8955+
});
8956+
8957+
cheats.prank(defaultStaker);
8958+
delegationManager.queueWithdrawals(queuedWithdrawalParams);
8959+
8960+
// Get shares from queued withdrawal
8961+
uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);
8962+
8963+
// Verify withdrawal details and shares for each strategy
8964+
assertEq(shares.length, numStrategies, "incorrect shares array length");
8965+
for (uint256 i = 0; i < numStrategies; i++) {
8966+
assertEq(shares[i], depositShares[i], "incorrect shares amount for strategy");
8967+
}
8968+
}
8969+
8970+
function testFuzz_getSharesFromQueuedWithdrawal_EmptyWithdrawal(bytes32 withdrawalRoot) public {
8971+
uint256[] memory shares = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);
8972+
assertEq(shares.length, 0, "sanity check");
8973+
}
88168974
}

0 commit comments

Comments
 (0)