Skip to content

Commit 768f160

Browse files
committed
feat: prevent queuing withdrawals to other addresses (#438)
1 parent eaadd48 commit 768f160

File tree

4 files changed

+29
-78
lines changed

4 files changed

+29
-78
lines changed

docs/core/DelegationManager.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,20 +226,18 @@ function queueWithdrawals(
226226

227227
Allows the caller to queue one or more withdrawals of their held shares across any strategy (in either/both the `EigenPodManager` or `StrategyManager`). If the caller is delegated to an Operator, the `shares` and `strategies` being withdrawn are immediately removed from that Operator's delegated share balances. Note that if the caller is an Operator, this still applies, as Operators are essentially delegated to themselves.
228228

229-
`queueWithdrawals` works very similarly to `undelegate`, except that the caller is not undelegated, and also may:
230-
* Choose which strategies and how many shares to withdraw (as opposed to ALL shares/strategies)
231-
* Specify a `withdrawer` to receive withdrawn funds once the withdrawal is completed
229+
`queueWithdrawals` works very similarly to `undelegate`, except that the caller is not undelegated, and also may choose which strategies and how many shares to withdraw (as opposed to ALL shares/strategies).
232230

233231
All shares being withdrawn (whether via the `EigenPodManager` or `StrategyManager`) are removed while the withdrawals are in the queue.
234232

235-
Withdrawals can be completed by the `withdrawer` after max(`minWithdrawalDelayBlocks`, `strategyWithdrawalDelayBlocks[strategy]`) such that `strategy` represents the queued strategies to be withdrawn. Withdrawals do not require the `withdrawer` to "fully exit" from the system -- they may choose to receive their shares back in full once the withdrawal is completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).
233+
Withdrawals can be completed by the caller after max(`minWithdrawalDelayBlocks`, `strategyWithdrawalDelayBlocks[strategy]`) such that `strategy` represents the queued strategies to be withdrawn. Withdrawals do not require the caller to "fully exit" from the system -- they may choose to receive their shares back in full once the withdrawal is completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details).
236234

237-
Note that for any `strategy` s.t `StrategyManager.thirdPartyTransfersForbidden(strategy) == true` the `withdrawer` must be the same address as the `staker` as this setting disallows users to deposit or withdraw on behalf of other users. (see [`thirdPartyTransfersForbidden`](./StrategyManager.md) for details).
235+
Note that the `QueuedWithdrawalParams` struct has a `withdrawer` field. Originally, this was used to specify an address that the withdrawal would be credited to once completed. However, `queueWithdrawals` now requires that `withdrawer == msg.sender`. Any other input is rejected.
238236

239237
*Effects*:
240238
* For each withdrawal:
241239
* If the caller is delegated to an Operator, that Operator's delegated balances are decreased according to the `strategies` and `shares` being withdrawn.
242-
* A `Withdrawal` is queued for the `withdrawer`, tracking the strategies and shares being withdrawn
240+
* A `Withdrawal` is queued for the caller, tracking the strategies and shares being withdrawn
243241
* The caller's withdrawal nonce is increased
244242
* The hash of the `Withdrawal` is marked as "pending"
245243
* See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares)
@@ -250,8 +248,7 @@ Note that for any `strategy` s.t `StrategyManager.thirdPartyTransfersForbidden(s
250248
* For each withdrawal:
251249
* `strategies.length` MUST equal `shares.length`
252250
* `strategies.length` MUST NOT be equal to 0
253-
* The `withdrawer` MUST NOT be 0
254-
* For all strategies being withdrawn, the `withdrawer` MUST be the same address as the `staker` if `StrategyManager.thirdPartyTransfersForbidden(strategy) == true`
251+
* The `withdrawer` MUST equal `msg.sender`
255252
* See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares)
256253
* See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares)
257254

src/contracts/core/DelegationManager.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg
272272

273273
for (uint256 i = 0; i < queuedWithdrawalParams.length; i++) {
274274
require(queuedWithdrawalParams[i].strategies.length == queuedWithdrawalParams[i].shares.length, "DelegationManager.queueWithdrawal: input length mismatch");
275-
require(queuedWithdrawalParams[i].withdrawer != address(0), "DelegationManager.queueWithdrawal: must provide valid withdrawal address");
275+
require(queuedWithdrawalParams[i].withdrawer == msg.sender, "DelegationManager.queueWithdrawal: withdrawer must be staker");
276276

277277
// Remove shares from staker's strategies and place strategies/shares in queue.
278278
// If the staker is delegated to an operator, the operator's delegated shares are also reduced

src/test/Withdrawals.t.sol

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ contract WithdrawalTests is EigenLayerTestHelper {
2222
function testWithdrawalWrapper(
2323
address operator,
2424
address depositor,
25-
address withdrawer,
2625
uint96 ethAmount,
2726
uint96 eigenAmount,
2827
bool withdrawAsTokens,
2928
bool RANDAO
30-
) public fuzzedAddress(operator) fuzzedAddress(depositor) fuzzedAddress(withdrawer) {
29+
) public fuzzedAddress(operator) fuzzedAddress(depositor) {
3130
cheats.assume(depositor != operator);
3231
cheats.assume(ethAmount >= 1 && ethAmount <= 1e18);
3332
cheats.assume(eigenAmount >= 1 && eigenAmount <= 1e18);
3433

34+
address withdrawer = depositor;
35+
3536
if (RANDAO) {
3637
_testWithdrawalAndDeregistration(operator, depositor, withdrawer, ethAmount, eigenAmount, withdrawAsTokens);
3738
} else {
@@ -244,14 +245,13 @@ contract WithdrawalTests is EigenLayerTestHelper {
244245
function testRedelegateAfterWithdrawal(
245246
address operator,
246247
address depositor,
247-
address withdrawer,
248248
uint96 ethAmount,
249249
uint96 eigenAmount,
250250
bool withdrawAsShares
251-
) public fuzzedAddress(operator) fuzzedAddress(depositor) fuzzedAddress(withdrawer) {
251+
) public fuzzedAddress(operator) fuzzedAddress(depositor) {
252252
cheats.assume(depositor != operator);
253253
//this function performs delegation and subsequent withdrawal
254-
testWithdrawalWrapper(operator, depositor, withdrawer, ethAmount, eigenAmount, withdrawAsShares, true);
254+
testWithdrawalWrapper(operator, depositor, ethAmount, eigenAmount, withdrawAsShares, true);
255255

256256
cheats.prank(depositor);
257257
delegation.undelegate(depositor);
@@ -261,50 +261,6 @@ contract WithdrawalTests is EigenLayerTestHelper {
261261
_initiateDelegation(operator, depositor, ethAmount, eigenAmount);
262262
}
263263

264-
// onlyNotFrozen modifier is not used in current DelegationManager implementation.
265-
// commented out test case for now
266-
// /// @notice test to see if an operator who is slashed/frozen
267-
// /// cannot be undelegated from by their stakers.
268-
// /// @param operator is the operator being delegated to.
269-
// /// @param staker is the staker delegating stake to the operator.
270-
// function testSlashedOperatorWithdrawal(address operator, address staker, uint96 ethAmount, uint96 eigenAmount)
271-
// public
272-
// fuzzedAddress(operator)
273-
// fuzzedAddress(staker)
274-
// {
275-
// cheats.assume(staker != operator);
276-
// testDelegation(operator, staker, ethAmount, eigenAmount);
277-
278-
// {
279-
// address slashingContract = slasher.owner();
280-
281-
// cheats.startPrank(operator);
282-
// slasher.optIntoSlashing(address(slashingContract));
283-
// cheats.stopPrank();
284-
285-
// cheats.startPrank(slashingContract);
286-
// slasher.freezeOperator(operator);
287-
// cheats.stopPrank();
288-
// }
289-
290-
// (IStrategy[] memory updatedStrategies, uint256[] memory updatedShares) =
291-
// strategyManager.getDeposits(staker);
292-
293-
// uint256[] memory strategyIndexes = new uint256[](2);
294-
// strategyIndexes[0] = 0;
295-
// strategyIndexes[1] = 1;
296-
297-
// IERC20[] memory tokensArray = new IERC20[](2);
298-
// tokensArray[0] = weth;
299-
// tokensArray[0] = eigenToken;
300-
301-
// //initiating queued withdrawal
302-
// cheats.expectRevert(
303-
// bytes("StrategyManager.onlyNotFrozen: staker has been frozen and may be subject to slashing")
304-
// );
305-
// _testQueueWithdrawal(staker, strategyIndexes, updatedStrategies, updatedShares, staker);
306-
// }
307-
308264
// Helper function to begin a delegation
309265
function _initiateDelegation(
310266
address operator,

src/test/unit/DelegationUnit.t.sol

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2828,14 +2828,16 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes
28282828
delegationManager.queueWithdrawals(queuedWithdrawalParams);
28292829
}
28302830

2831-
function test_Revert_WhenZeroAddressWithdrawer() public {
2831+
function test_Revert_WhenNotStakerWithdrawer(address withdrawer) public {
2832+
cheats.assume(withdrawer != defaultStaker);
2833+
28322834
(IDelegationManager.QueuedWithdrawalParams[] memory queuedWithdrawalParams, , ) = _setUpQueueWithdrawalsSingleStrat({
28332835
staker: defaultStaker,
2834-
withdrawer: address(0),
2836+
withdrawer: withdrawer,
28352837
strategy: strategyMock,
28362838
withdrawalAmount: 100
28372839
});
2838-
cheats.expectRevert("DelegationManager.queueWithdrawal: must provide valid withdrawal address");
2840+
cheats.expectRevert("DelegationManager.queueWithdrawal: withdrawer must be staker");
28392841
delegationManager.queueWithdrawals(queuedWithdrawalParams);
28402842
}
28412843

@@ -2977,6 +2979,7 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes
29772979
uint256 randSalt
29782980
) public filterFuzzedAddressInputs(staker){
29792981
cheats.assume(depositAmounts.length > 0 && depositAmounts.length <= 32);
2982+
cheats.assume(staker != defaultOperator);
29802983
uint256[] memory withdrawalAmounts = _fuzzWithdrawalAmounts(depositAmounts);
29812984

29822985
IStrategy[] memory strategies = _deployAndDepositIntoStrategies(staker, depositAmounts);
@@ -3033,7 +3036,7 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes
30333036
uint256[] memory depositAmounts,
30343037
uint256 randSalt
30353038
) public filterFuzzedAddressInputs(staker) {
3036-
cheats.assume(staker != withdrawer && withdrawer != address(0));
3039+
cheats.assume(staker != withdrawer);
30373040
cheats.assume(depositAmounts.length > 0 && depositAmounts.length <= 32);
30383041
uint256[] memory withdrawalAmounts = _fuzzWithdrawalAmounts(depositAmounts);
30393042

@@ -3052,8 +3055,11 @@ contract DelegationManagerUnitTests_queueWithdrawals is DelegationManagerUnitTes
30523055
});
30533056

30543057
// queueWithdrawals
3058+
// NOTE: Originally, you could queue a withdrawal to a different address, which would fail with a specific error
3059+
// if third party transfers were forbidden. Now, withdrawing to a different address is forbidden regardless
3060+
// of third party transfer status.
30553061
cheats.expectRevert(
3056-
"DelegationManager._removeSharesAndQueueWithdrawal: withdrawer must be same address as staker if thirdPartyTransfersForbidden are set"
3062+
"DelegationManager.queueWithdrawal: withdrawer must be staker"
30573063
);
30583064
cheats.prank(staker);
30593065
delegationManager.queueWithdrawals(queuedWithdrawalParams);
@@ -3247,12 +3253,10 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage
32473253
*/
32483254
function test_completeQueuedWithdrawal_SingleStratWithdrawAsTokens(
32493255
address staker,
3250-
address withdrawer,
32513256
uint256 depositAmount,
32523257
uint256 withdrawalAmount
32533258
) public filterFuzzedAddressInputs(staker) {
32543259
cheats.assume(staker != defaultOperator);
3255-
cheats.assume(withdrawer != address(0));
32563260
cheats.assume(withdrawalAmount > 0 && withdrawalAmount <= depositAmount);
32573261
_registerOperatorWithBaseDetails(defaultOperator);
32583262
(
@@ -3261,7 +3265,7 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage
32613265
bytes32 withdrawalRoot
32623266
) = _setUpCompleteQueuedWithdrawalSingleStrat({
32633267
staker: staker,
3264-
withdrawer: withdrawer,
3268+
withdrawer: staker,
32653269
depositAmount: depositAmount,
32663270
withdrawalAmount: withdrawalAmount
32673271
});
@@ -3271,7 +3275,7 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage
32713275

32723276
// completeQueuedWithdrawal
32733277
cheats.roll(block.number + delegationManager.getWithdrawalDelay(withdrawal.strategies));
3274-
cheats.prank(withdrawer);
3278+
cheats.prank(staker);
32753279
cheats.expectEmit(true, true, true, true, address(delegationManager));
32763280
emit WithdrawalCompleted(withdrawalRoot);
32773281
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, 0 /* middlewareTimesIndex */, true);
@@ -3291,21 +3295,20 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage
32913295
*/
32923296
function test_completeQueuedWithdrawal_SingleStratWithdrawAsShares(
32933297
address staker,
3294-
address withdrawer,
32953298
uint256 depositAmount,
32963299
uint256 withdrawalAmount
32973300
) public filterFuzzedAddressInputs(staker) {
32983301
cheats.assume(staker != defaultOperator);
3299-
cheats.assume(withdrawer != defaultOperator && withdrawer != address(0));
33003302
cheats.assume(withdrawalAmount > 0 && withdrawalAmount <= depositAmount);
33013303
_registerOperatorWithBaseDetails(defaultOperator);
3304+
33023305
(
33033306
IDelegationManager.Withdrawal memory withdrawal,
33043307
IERC20[] memory tokens,
33053308
bytes32 withdrawalRoot
33063309
) = _setUpCompleteQueuedWithdrawalSingleStrat({
33073310
staker: staker,
3308-
withdrawer: withdrawer,
3311+
withdrawer: staker,
33093312
depositAmount: depositAmount,
33103313
withdrawalAmount: withdrawalAmount
33113314
});
@@ -3315,19 +3318,14 @@ contract DelegationManagerUnitTests_completeQueuedWithdrawal is DelegationManage
33153318

33163319
// completeQueuedWithdrawal
33173320
cheats.roll(block.number + delegationManager.getWithdrawalDelay(withdrawal.strategies));
3318-
cheats.prank(withdrawer);
3321+
cheats.prank(staker);
33193322
cheats.expectEmit(true, true, true, true, address(delegationManager));
33203323
emit WithdrawalCompleted(withdrawalRoot);
33213324
delegationManager.completeQueuedWithdrawal(withdrawal, tokens, 0 /* middlewareTimesIndex */, false);
33223325

33233326
uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, withdrawal.strategies[0]);
3324-
if (staker == withdrawer) {
3325-
// Since staker is delegated, operatorShares get incremented
3326-
assertEq(operatorSharesAfter, operatorSharesBefore + withdrawalAmount, "operator shares not increased correctly");
3327-
} else {
3328-
// Since withdrawer is not the staker and isn't delegated, staker's oeprator shares are unchanged
3329-
assertEq(operatorSharesAfter, operatorSharesBefore, "operator shares should be unchanged");
3330-
}
3327+
// Since staker is delegated, operatorShares get incremented
3328+
assertEq(operatorSharesAfter, operatorSharesBefore + withdrawalAmount, "operator shares not increased correctly");
33313329
assertFalse(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRoot should be completed and marked false now");
33323330
}
33333331
}

0 commit comments

Comments
 (0)