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: 9/43
Findings: 2
Award: $1,978.72
π Selected for report: 0
π Solo Findings: 0
π Selected for report: juancito
Also found by: MiloTruck, Ruhum, SpicyMeatball, bin2chen, evmboi32, pontifex, sashik_eth, volodya, yjrwkk
1443.9307 USDC - $1,443.93
In StrategyManager.sol
, the owner can call slashQueuedWithdrawal()
to slash an existing queued withdrawal that is delegated to a "frozen" operator.
If the withdrawal contains a "malicious" strategy (ie. reverts when withdraw()
is called) in its strategies
array, the owner can include its index in indicesToSkip
to skip it. However, this is implemented incorrectly in slashQueuedWithdrawal()
:
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 { // Some code to slash queuedWithdrawal here... unchecked { ++i; } } }
In the code above, i
is only incremented in the else
body. Therefore, if indicesToSkip[indicesToSkipIndex]
matches i
, only indicesToSkipIndex
is incremented, and i
remains the same for the next iteration. This makes indicesToSkip
redundant as the malicious strategy's index is never actually skipped.
If a queued withdrawal:
its shares can never be withdrawn, causing assets to be permanently stuck in their respecptive strategies. This is because:
slashQueuedWithdrawal()
cannot skip malicious strategies due to the bug described above, and will therefore revert.completeQueuedWithdrawal()
only works for queued withdrawal delegated to non-"frozen" operators.Always increment i
for every iteration in slashQueuedWithdrawal()
:
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 (queuedWithdrawal.strategies[i] == beaconChainETHStrategy){ //withdraw the beaconChainETH to the recipient _withdrawBeaconChainETH(queuedWithdrawal.depositor, recipient, queuedWithdrawal.shares[i]); } else { // tell the strategy to send the appropriate amount of funds to the recipient queuedWithdrawal.strategies[i].withdraw(recipient, tokens[i], queuedWithdrawal.shares[i]); } - unchecked { - ++i; - } } + unchecked { + ++i; + } }
#0 - c4-pre-sort
2023-05-09T13:09:00Z
0xSorryNotSorry marked the issue as duplicate of #205
#1 - c4-judge
2023-06-08T12:22:16Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: ABA
Also found by: 0xWaitress, 8olidity, ABA, MiloTruck, ToonVH, bughunter007, bytes032, juancito, rvierdiiev
534.7892 USDC - $534.79
In StrategyManager.sol
, if users want to withdraw their tokens from strategies, they have to queue up withdrawals using queueWithdrawal()
.
After withdrawalDelayBlocks
blocks has passed, they can call completeQueuedWithdrawal()
to complete their queued withdrawal. This withdraws tokens from strategies in a loop:
// actually withdraw the funds for (uint256 i = 0; i < strategiesLength;) { if (queuedWithdrawal.strategies[i] == beaconChainETHStrategy) { // if the strategy is the beaconchaineth strat, then withdraw through the EigenPod flow _withdrawBeaconChainETH(queuedWithdrawal.depositor, msg.sender, queuedWithdrawal.shares[i]); } else { // tell the strategy to send the appropriate amount of funds to the depositor queuedWithdrawal.strategies[i].withdraw( msg.sender, tokens[i], queuedWithdrawal.shares[i] ); } unchecked { ++i; } }
As seen from above, if withdraw()
reverts for any strategy in the strategies
array, _completeQueuedWithdrawal()
will also revert.
In StrategyBase.sol
, withdraw()
reverts if the total amount of shares is below MIN_NONZERO_TOTAL_SHARES
after the withdrawal:
// check to avoid edge case where share rate can be massively inflated as a 'griefing' sort of attack require(updatedTotalShares >= MIN_NONZERO_TOTAL_SHARES || updatedTotalShares == 0, "StrategyBase.withdraw: updated totalShares amount would be nonzero but below MIN_NONZERO_TOTAL_SHARES");
By manipulating the total number of shares in a strategy, an attacker can force the check shown above to revert when a user calls completeQueuedWithdrawal()
, making him unable to withdraw his tokens.
Note that withdraw()
also reverts if withdrawals are paused by a strategy's pauser.
An attacker can manipulate a strategy's total shares to make a user unable to withdraw their shares for tokens.
If the user still wants withdraw his tokens from strategies, his only option is to complete the queued withdrawal by calling completeQueuedWithdrawal()
with receiveAsTokens = false
, and then creating a new queued withdrawal.
Consider the following scenario:
deposit()
to deposit 1000 USDC into strategy B, gaining 1e9
shares.queueWithdrawal()
with both strategies A and B.withdrawalDelayBlocks
blocks passes.completeQueuedWithdrawal()
with the queued withdrawal to withdraw his shares for tokens.deposit()
to deposit 1 USDC into strategy B. She gains 1e6
shares as a result.completeQueuedWithdrawal()
:
withdraw()
reverts as the total amount of shares is 1e6
after the withdrawal, which is less than MIN_NONZERO_TOTAL_SHARES
.completeQueuedWithdrawal()
reverts.In completeQueuedWithdrawal()
, allow users to specify which strategies to skip, similar to the strategyIndexes
array in slashQueuedWithdrawal()
. Strategies that are skipped should have their shares added again using _addShares()
.
#0 - c4-pre-sort
2023-05-09T13:33:20Z
0xSorryNotSorry marked the issue as primary issue
#1 - Sidu28
2023-05-12T04:43:18Z
Due to the way the Beacon Chain works, there is no way for us to stop people making new Beacon Chain deposits. There is pausability on the level of deploying new EigenPods already. We consider this informational
#2 - c4-sponsor
2023-05-12T04:43:21Z
Sidu28 marked the issue as disagree with severity
#3 - GalloDaSballo
2023-05-31T18:44:25Z
Tough to dedoup:
#4 - GalloDaSballo
2023-05-31T18:44:58Z
Because the warden sent #121 I think the best course of action is:
#5 - GalloDaSballo
2023-05-31T18:45:10Z
TODO: This is a doup of Withdrawals being reverted if strat reverts
#6 - c4-judge
2023-06-01T13:56:20Z
GalloDaSballo marked the issue as duplicate of #297
#7 - c4-judge
2023-06-01T13:56:43Z
GalloDaSballo marked the issue as not a duplicate
#8 - c4-judge
2023-06-01T13:57:02Z
GalloDaSballo marked the issue as duplicate of #132
#9 - c4-judge
2023-06-01T13:57:10Z
GalloDaSballo marked the issue as satisfactory