Skip to content

Commit

Permalink
feat: add getSharesFromQueuedWithdrawal (#1078)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
0xClandestine authored Feb 13, 2025
1 parent 38f5faa commit 3ad7a0a
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 31 deletions.
72 changes: 42 additions & 30 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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]);
}
}

Expand Down
18 changes: 17 additions & 1 deletion src/contracts/interfaces/IDelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
158 changes: 158 additions & 0 deletions src/test/unit/DelegationUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

0 comments on commit 3ad7a0a

Please sign in to comment.