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

Open
wants to merge 8 commits into
base: slashing-magnitudes-fixes
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 43 additions & 30 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,41 @@ 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];
0xClandestine marked this conversation as resolved.
Show resolved Hide resolved
shares = new uint256[](withdrawal.strategies.length);

address operator = delegatedTo[withdrawal.staker];
uint32 slashableUntil = withdrawal.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: withdrawal.staker,
operator: operator,
strategies: withdrawal.strategies,
blockNumber: slashableUntil
});
} else {
slashingFactors =
_getSlashingFactors({staker: withdrawal.staker, operator: operator, strategies: withdrawal.strategies});
}

for (uint256 j; j < withdrawal.strategies.length; ++j) {
shares[j] = SlashingLib.scaleForCompleteWithdrawal({
scaledShares: withdrawal.scaledShares[j],
slashingFactor: slashingFactors[j]
});
}
}
0xClandestine marked this conversation as resolved.
Show resolved Hide resolved

/// @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 +949,13 @@ contract DelegationManager is
return queuedWithdrawals[withdrawalRoot];
}

/// @inheritdoc IDelegationManager
function getSharesFromQueuedWithdrawal(
bytes32 withdrawalRoot
) external view returns (Withdrawal memory withdrawal, uint256[] memory shares) {
(withdrawal, shares) = _getSharesByWithdrawalRoot(withdrawalRoot);
0xClandestine marked this conversation as resolved.
Show resolved Hide resolved
}

/// @inheritdoc IDelegationManager
function getQueuedWithdrawals(
address staker
Expand All @@ -924,37 +966,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
19 changes: 18 additions & 1 deletion src/contracts/interfaces/IDelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -477,11 +477,28 @@ 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(
0xClandestine marked this conversation as resolved.
Show resolved Hide resolved
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 withdrawal The Withdrawal struct containing details about 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 (Withdrawal memory withdrawal, 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
134 changes: 134 additions & 0 deletions src/test/unit/DelegationUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8814,3 +8814,137 @@ contract DelegationManagerUnitTests_getQueuedWithdrawals is DelegationManagerUni
);
}
}

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
(Withdrawal memory returnedWithdrawal, uint256[] memory shares) = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);

// Verify withdrawal details match
assertEq(keccak256(abi.encode(returnedWithdrawal)), withdrawalRoot, "returned withdrawal root mismatch");
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
(Withdrawal memory returnedWithdrawal, uint256[] memory shares) = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);

// Verify withdrawal details match and shares are slashed
assertEq(keccak256(abi.encode(returnedWithdrawal)), withdrawalRoot, "returned withdrawal root mismatch");
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));

(Withdrawal memory withdrawal, uint256[] memory shares) = delegationManager.getSharesFromQueuedWithdrawal(nonexistentRoot);

// For a nonexistent withdrawal, we expect empty arrays and default values
assertEq(withdrawal.staker, address(0), "withdrawal staker should be zero address");
assertEq(withdrawal.strategies.length, 0, "withdrawal strategies should be empty");
assertEq(withdrawal.scaledShares.length, 0, "withdrawal scaledShares should be empty");
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
(Withdrawal memory returnedWithdrawal, uint256[] memory shares) = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);

// Verify withdrawal details and shares for each strategy
assertEq(keccak256(abi.encode(returnedWithdrawal)), withdrawalRoot, "returned withdrawal root mismatch");
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 {
(
Withdrawal memory withdrawal,
uint256[] memory shares
) = delegationManager.getSharesFromQueuedWithdrawal(withdrawalRoot);
assertEq(shares.length, 0, "sanity check");
}
}
Loading