Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getSharesFromQueuedWithdrawal #1078

Merged
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");
}
}
Loading