Skip to content

Commit 2323207

Browse files
Gajesh2007ChaoticWalruswadealexc
authored
made forceUndelegate queue a withdrawal for each strategy (#345)
* changes * added back comments * chore: fix tests to work with modified behavior (#378) * chore: fix tests to work with modified behavior integration tests in particular are now slightly more flexible * fix: remove memory overwrite * docs: update dmgr docs --------- Co-authored-by: wadealexc <[email protected]> --------- Co-authored-by: ChaoticWalrus <[email protected]> Co-authored-by: wadealexc <[email protected]>
1 parent 9e04c92 commit 2323207

10 files changed

+123
-75
lines changed

docs/core/DelegationManager.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,24 +229,24 @@ function undelegate(
229229
)
230230
external
231231
onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE)
232-
returns (bytes32 withdrawalRoot)
232+
returns (bytes32[] memory withdrawalRoots)
233233
```
234234

235-
`undelegate` can be called by a Staker to undelegate themselves, or by a Staker's delegated Operator (or that Operator's `delegationApprover`). Undelegation (i) queues a withdrawal on behalf of the Staker for all their delegated shares, and (ii) decreases the Operator's delegated shares according to the amounts and strategies being withdrawn.
235+
`undelegate` can be called by a Staker to undelegate themselves, or by a Staker's delegated Operator (or that Operator's `delegationApprover`). Undelegation (i) queues withdrawals on behalf of the Staker for all their delegated shares, and (ii) decreases the Operator's delegated shares according to the amounts and strategies being withdrawn.
236236

237-
If the Staker has active shares in either the `EigenPodManager` or `StrategyManager`, they are removed while the withdrawal is in the queue.
237+
If the Staker has active shares in either the `EigenPodManager` or `StrategyManager`, they are removed while the withdrawal is in the queue - and an individual withdrawal is queued for each strategy removed.
238238

239-
The withdrawal can be completed by the Staker after `withdrawalDelayBlocks`, and does not require the Staker to "fully exit" from the system -- the Staker may choose to receive their shares back in full once the withdrawal is completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).
239+
The withdrawals can be completed by the Staker after `withdrawalDelayBlocks`. This does not require the Staker to "fully exit" from the system -- the Staker may choose to receive their shares back in full once withdrawals are completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).
240240

241241
Note that becoming an Operator is irreversible! Although Operators can withdraw, they cannot use this method to undelegate from themselves.
242242

243243
*Effects*:
244244
* Any shares held by the Staker in the `EigenPodManager` and `StrategyManager` are removed from the Operator's delegated shares.
245245
* The Staker is undelegated from the Operator
246246
* If the Staker has no delegatable shares, there is no withdrawal queued or further effects
247-
* A `Withdrawal` is queued for the Staker, tracking the strategies and shares being withdrawn
248-
* The Staker's withdrawal nonce is increased
249-
* The hash of the `Withdrawal` is marked as "pending"
247+
* For each strategy being withdrawn, a `Withdrawal` is queued for the Staker:
248+
* The Staker's withdrawal nonce is increased by 1 for each `Withdrawal`
249+
* The hash of each `Withdrawal` is marked as "pending"
250250
* See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares)
251251
* See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares)
252252

src/contracts/core/DelegationManager.sol

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
217217
* a staker from their operator. Undelegation immediately removes ALL active shares/strategies from
218218
* both the staker and operator, and places the shares and strategies in the withdrawal queue
219219
*/
220-
function undelegate(address staker) external onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) returns (bytes32) {
220+
function undelegate(address staker) external onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) returns (bytes32[] memory withdrawalRoots) {
221221
require(isDelegated(staker), "DelegationManager.undelegate: staker must be delegated to undelegate");
222222
require(!isOperator(staker), "DelegationManager.undelegate: operators cannot be undelegated");
223223
require(staker != address(0), "DelegationManager.undelegate: cannot undelegate zero address");
@@ -231,8 +231,7 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
231231

232232
// Gather strategies and shares to remove from staker/operator during undelegation
233233
// Undelegation removes ALL currently-active strategies and shares
234-
(IStrategy[] memory strategies, uint256[] memory shares)
235-
= getDelegatableShares(staker);
234+
(IStrategy[] memory strategies, uint256[] memory shares) = getDelegatableShares(staker);
236235

237236
// emit an event if this action was not initiated by the staker themselves
238237
if (msg.sender != staker) {
@@ -243,19 +242,28 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
243242
emit StakerUndelegated(staker, operator);
244243
delegatedTo[staker] = address(0);
245244

246-
// if no delegatable shares, return zero root, and don't queue a withdrawal
245+
// if no delegatable shares, return an empty array, and don't queue a withdrawal
247246
if (strategies.length == 0) {
248-
return bytes32(0);
247+
withdrawalRoots = new bytes32[](0);
249248
} else {
250-
// Remove all strategies/shares from staker and operator and place into queue
251-
return _removeSharesAndQueueWithdrawal({
252-
staker: staker,
253-
operator: operator,
254-
withdrawer: staker,
255-
strategies: strategies,
256-
shares: shares
257-
});
249+
withdrawalRoots = new bytes32[](strategies.length);
250+
for (uint256 i = 0; i < strategies.length; i++) {
251+
IStrategy[] memory singleStrategy = new IStrategy[](1);
252+
uint256[] memory singleShare = new uint256[](1);
253+
singleStrategy[0] = strategies[i];
254+
singleShare[0] = shares[i];
255+
256+
withdrawalRoots[i] = _removeSharesAndQueueWithdrawal({
257+
staker: staker,
258+
operator: operator,
259+
withdrawer: staker,
260+
strategies: singleStrategy,
261+
shares: singleShare
262+
});
263+
}
258264
}
265+
266+
return withdrawalRoots;
259267
}
260268

261269
/**

src/contracts/interfaces/IDelegationManager.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ interface IDelegationManager is ISignatureUtils {
249249
* @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's specified "delegationApprover"
250250
* @dev Reverts if the `staker` is already undelegated.
251251
*/
252-
function undelegate(address staker) external returns (bytes32 withdrawalRoot);
252+
function undelegate(address staker) external returns (bytes32[] memory withdrawalRoot);
253253

254254
/**
255255
* Allows a staker to withdraw some shares. Withdrawn shares/strategies are immediately removed

src/test/integration/IntegrationChecks.t.sol

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,16 @@ contract IntegrationCheckUtils is IntegrationBase {
5252
// ... check that each withdrawal was successfully enqueued, that the returned roots
5353
// match the hashes of each withdrawal, and that the staker and operator have
5454
// reduced shares.
55-
assert_AllWithdrawalsPending(withdrawalRoots, "staker withdrawals should now be pending");
56-
assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots");
57-
assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length");
58-
assert_Snap_Removed_OperatorShares(operator, strategies, shares, "failed to remove operator shares");
59-
assert_Snap_Removed_StakerShares(staker, strategies, shares, "failed to remove staker shares");
55+
assert_AllWithdrawalsPending(withdrawalRoots,
56+
"check_QueuedWithdrawal_State: staker withdrawals should now be pending");
57+
assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots,
58+
"check_QueuedWithdrawal_State: calculated withdrawals should match returned roots");
59+
assert_Snap_Added_QueuedWithdrawals(staker, withdrawals,
60+
"check_QueuedWithdrawal_State: staker should have increased nonce by withdrawals.length");
61+
assert_Snap_Removed_OperatorShares(operator, strategies, shares,
62+
"check_QueuedWithdrawal_State: failed to remove operator shares");
63+
assert_Snap_Removed_StakerShares(staker, strategies, shares,
64+
"check_QueuedWithdrawal_State: failed to remove staker shares");
6065
}
6166

6267
function check_Undelegate_State(
@@ -72,13 +77,18 @@ contract IntegrationCheckUtils is IntegrationBase {
7277
// ... check that the staker is undelegated, all strategies from which the staker is deposited are unqeuued,
7378
// that the returned root matches the hashes for each strategy and share amounts, and that the staker
7479
// and operator have reduced shares
75-
assertEq(withdrawalRoots.length, 1, "should only be one withdrawal root");
76-
assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated");
77-
assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawl should match returned root");
78-
assert_AllWithdrawalsPending(withdrawalRoots, "stakers withdrawal should now be pending");
79-
assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by 1");
80-
assert_Snap_Removed_OperatorShares(operator, strategies, shares, "failed to remove operator shares");
81-
assert_Snap_Removed_StakerShares(staker, strategies, shares, "failed to remove staker shares");
80+
assertFalse(delegationManager.isDelegated(address(staker)),
81+
"check_Undelegate_State: staker should not be delegated");
82+
assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots,
83+
"check_Undelegate_State: calculated withdrawl should match returned root");
84+
assert_AllWithdrawalsPending(withdrawalRoots,
85+
"check_Undelegate_State: stakers withdrawal should now be pending");
86+
assert_Snap_Added_QueuedWithdrawals(staker, withdrawals,
87+
"check_Undelegate_State: staker should have increased nonce by withdrawals.length");
88+
assert_Snap_Removed_OperatorShares(operator, strategies, shares,
89+
"check_Undelegate_State: failed to remove operator shares");
90+
assert_Snap_Removed_StakerShares(staker, strategies, shares,
91+
"check_Undelegate_State: failed to remove staker shares");
8292
}
8393

8494
function check_Withdrawal_AsTokens_State(

src/test/integration/User.t.sol

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,26 @@ contract User is Test {
171171
function undelegate() public createSnapshot virtual returns(IDelegationManager.Withdrawal[] memory){
172172
emit log(_name(".undelegate"));
173173

174-
IDelegationManager.Withdrawal[] memory withdrawal = new IDelegationManager.Withdrawal[](1);
175-
withdrawal[0] = _getExpectedWithdrawalStructForStaker(address(this));
174+
IDelegationManager.Withdrawal[] memory expectedWithdrawals = _getExpectedWithdrawalStructsForStaker(address(this));
176175
delegationManager.undelegate(address(this));
177-
return withdrawal;
176+
177+
for (uint i = 0; i < expectedWithdrawals.length; i++) {
178+
emit log("expecting withdrawal:");
179+
emit log_named_uint("nonce: ", expectedWithdrawals[i].nonce);
180+
emit log_named_address("strat: ", address(expectedWithdrawals[i].strategies[0]));
181+
emit log_named_uint("shares: ", expectedWithdrawals[i].shares[0]);
182+
}
183+
184+
return expectedWithdrawals;
178185
}
179186

180187
/// @dev Force undelegate staker
181188
function forceUndelegate(User staker) public createSnapshot virtual returns(IDelegationManager.Withdrawal[] memory){
182189
emit log_named_string(_name(".forceUndelegate: "), staker.NAME());
183190

184-
IDelegationManager.Withdrawal[] memory withdrawal = new IDelegationManager.Withdrawal[](1);
185-
withdrawal[0] = _getExpectedWithdrawalStructForStaker(address(staker));
191+
IDelegationManager.Withdrawal[] memory expectedWithdrawals = _getExpectedWithdrawalStructsForStaker(address(staker));
186192
delegationManager.undelegate(address(staker));
187-
return withdrawal;
193+
return expectedWithdrawals;
188194
}
189195

190196
/// @dev Queues a single withdrawal for every share and strategy pair
@@ -317,20 +323,33 @@ contract User is Test {
317323
return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(pod));
318324
}
319325

326+
/// @notice Gets the expected withdrawals to be created when the staker is undelegated via a call to `DelegationManager.undelegate()`
320327
/// @notice Assumes staker and withdrawer are the same and that all strategies and shares are withdrawn
321-
function _getExpectedWithdrawalStructForStaker(address staker) internal view returns (IDelegationManager.Withdrawal memory) {
322-
(IStrategy[] memory strategies, uint[] memory shares)
328+
function _getExpectedWithdrawalStructsForStaker(address staker) internal returns (IDelegationManager.Withdrawal[] memory) {
329+
(IStrategy[] memory strategies, uint256[] memory shares)
323330
= delegationManager.getDelegatableShares(staker);
324331

325-
return IDelegationManager.Withdrawal({
326-
staker: staker,
327-
delegatedTo: delegationManager.delegatedTo(staker),
328-
withdrawer: staker,
329-
nonce: delegationManager.cumulativeWithdrawalsQueued(staker),
330-
startBlock: uint32(block.number),
331-
strategies: strategies,
332-
shares: shares
333-
});
332+
IDelegationManager.Withdrawal[] memory expectedWithdrawals = new IDelegationManager.Withdrawal[](strategies.length);
333+
address delegatedTo = delegationManager.delegatedTo(staker);
334+
uint256 nonce = delegationManager.cumulativeWithdrawalsQueued(staker);
335+
336+
for (uint256 i = 0; i < strategies.length; ++i) {
337+
IStrategy[] memory singleStrategy = new IStrategy[](1);
338+
uint256[] memory singleShares = new uint256[](1);
339+
singleStrategy[0] = strategies[i];
340+
singleShares[0] = shares[i];
341+
expectedWithdrawals[i] = IDelegationManager.Withdrawal({
342+
staker: staker,
343+
delegatedTo: delegatedTo,
344+
withdrawer: staker,
345+
nonce: (nonce + i),
346+
startBlock: uint32(block.number),
347+
strategies: singleStrategy,
348+
shares: singleShares
349+
});
350+
}
351+
352+
return expectedWithdrawals;
334353
}
335354

336355
function _name(string memory s) internal view returns (string memory) {

src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationCheckUtils {
5656
// Fast forward to when we can complete the withdrawal
5757
cheats.roll(block.number + delegationManager.withdrawalDelayBlocks());
5858

59-
for (uint i = 0; i < withdrawals.length; i++) {
60-
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].shares);
59+
for (uint256 i = 0; i < withdrawals.length; i++) {
60+
uint256[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].shares);
6161
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
6262
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], strategies, shares, tokens, expectedTokens);
6363
}
@@ -115,7 +115,7 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationCheckUtils {
115115
// Fast forward to when we can complete the withdrawal
116116
cheats.roll(block.number + delegationManager.withdrawalDelayBlocks());
117117

118-
for (uint i = 0; i < withdrawals.length; i++) {
118+
for (uint256 i = 0; i < withdrawals.length; i++) {
119119
staker.completeWithdrawalAsShares(withdrawals[i]);
120120
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], strategies, shares);
121121
}
@@ -182,8 +182,8 @@ contract Integration_Deposit_Delegate_Queue_Complete is IntegrationCheckUtils {
182182
// 4. Complete withdrawals
183183
// Fast forward to when we can complete the withdrawal
184184
cheats.roll(block.number + delegationManager.withdrawalDelayBlocks());
185-
for (uint i = 0; i < withdrawals.length; i++) {
186-
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].shares);
185+
for (uint256 i = 0; i < withdrawals.length; i++) {
186+
uint256[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].shares);
187187
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
188188
check_Withdrawal_AsTokens_State(staker, operator, withdrawals[i], withdrawStrats, withdrawShares, tokens, expectedTokens);
189189
}

src/test/integration/tests/Deposit_Delegate_Redelegate_Complete.t.sol

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ contract Integration_Deposit_Delegate_Redelegate_Complete is IntegrationCheckUti
5555
// 4. Complete withdrawal as shares
5656
// Fast forward to when we can complete the withdrawal
5757
cheats.roll(block.number + delegationManager.withdrawalDelayBlocks());
58-
staker.completeWithdrawalAsShares(withdrawals[0]);
59-
check_Withdrawal_AsShares_Undelegated_State(staker, operator1, withdrawals[0], strategies, shares);
58+
for (uint256 i = 0; i < withdrawals.length; ++i) {
59+
staker.completeWithdrawalAsShares(withdrawals[i]);
60+
check_Withdrawal_AsShares_Undelegated_State(staker, operator1, withdrawals[i], withdrawals[i].strategies, withdrawals[i].shares);
61+
}
6062

6163
// 5. Delegate to a new operator
6264
staker.delegateTo(operator2);
@@ -76,7 +78,7 @@ contract Integration_Deposit_Delegate_Redelegate_Complete is IntegrationCheckUti
7678
for (uint i = 0; i < withdrawals.length; i++) {
7779
uint[] memory expectedTokens = _calculateExpectedTokens(withdrawals[i].strategies, withdrawals[i].shares);
7880
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[i]);
79-
check_Withdrawal_AsTokens_State(staker, operator2, withdrawals[i], strategies, shares, tokens, expectedTokens);
81+
check_Withdrawal_AsTokens_State(staker, operator2, withdrawals[i], withdrawals[i].strategies, withdrawals[i].shares, tokens, expectedTokens);
8082
}
8183
}
8284

0 commit comments

Comments
 (0)