Platform: Code4rena
Start Date: 27/04/2023
Pot Size: $90,500 USDC
Total HM: 4
Participants: 43
Period: 7 days
Judge: GalloDaSballo
Id: 233
League: ETH
Rank: 21/43
Findings: 2
Award: $1,063.04
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: ABA
Also found by: 0xWaitress, 8olidity, ABA, MiloTruck, ToonVH, bughunter007, bytes032, juancito, rvierdiiev
534.7892 USDC - $534.79
Funds can be locked if one strategy withdrawal fails, preventing users from withdrawing their funds from other strategies in the queued withdrawal.
When queuing a withdrawal, the caller can specify a list of strategies to withdraw from.
function queueWithdrawal( uint256[] calldata strategyIndexes, IStrategy[] calldata strategies, uint256[] calldata shares, address withdrawer, bool undelegateIfPossible ) external onlyWhenNotPaused(PAUSED_WITHDRAWALS) onlyNotFrozen(msg.sender) nonReentrant returns (bytes32) {
The actual withdrawal takes place in the _completeQueuedWithdrawal
function when certain conditions are met.
If the staker is not withdrawing from the BeaconChain and has chosen to receive the funds as tokens, the withdrawal from strategies occurs in the StrategyManager
contract.
// tell the strategy to send the appropriate amount of funds to the depositor // @audit paused/bricked strategy underlying will throw here queuedWithdrawal.strategies[i].withdraw( msg.sender, tokens[i], queuedWithdrawal.shares[i] );
This, in turn, calls the withdraw
function of the strategy.
function withdraw(address depositor, IERC20 token, uint256 amountShares) external virtual override onlyWhenNotPaused(PAUSED_WITHDRAWALS) onlyStrategyManager { require(token == underlyingToken, "StrategyBase.withdraw: Can only withdraw the strategy token"); ... // @audit can dos underlyingToken.safeTransfer(depositor, amountToSend); }
However, this can be problematic because if a single strategy withdrawal fails, this means the user's funds are locked until the withdrawal is possible again. As there are multiple ERC20's with pausing functionality, this makes the scenario very likely.
Consider the following scenario:
queuedWithdrawal.strategies[i].withdraw(...)
reverts, and the user cannot withdraw from any of the strategies in the queued withdrawal.Manual Review
The slashQueuedWithdrawal
function elegantly mitigates this issue with the indicesToSkip
parameter, explicitly used for scenarios where a single strategy withdrawal fails. This allows malicious or problematic strategies to be skipped, while the non-malicious strategies are still called as normal.
My suggestion is to add the same possibility for users when completing withdrawals.
* @param indicesToSkip Optional input parameter -- indices in the `strategies` array to skip (i.e. not call the 'withdraw' function on). This input exists * so that, e.g., if the slashed QueuedWithdrawal contains a malicious strategy in the `strategies` array which always reverts on calls to its 'withdraw' function, * then the malicious strategy can be skipped (with the shares in effect "burned"), while the non-malicious strategies are still called as normal. */
uint256 strategiesLength = queuedWithdrawal.strategies.length; for (uint256 i = 0; i < strategiesLength;) { // check if the index i matches one of the indices specified in the `indicesToSkip` array if (indicesToSkipIndex < indicesToSkip.length && indicesToSkip[indicesToSkipIndex] == i) { unchecked { ++indicesToSkipIndex; } } else {
If the team is not comfortable with this solution, as alternative, they can implement functionality where the user can cancel individual withdrawals.
#0 - c4-pre-sort
2023-05-09T13:39:01Z
0xSorryNotSorry marked the issue as duplicate of #132
#1 - c4-judge
2023-06-08T12:27:16Z
GalloDaSballo marked the issue as satisfactory