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: 14/43
Findings: 1
Award: $1,443.93
🌟 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 src/contracts/core/StrategyManager.sol#L536 parameter indicesToSkip
per documentation: """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""".
However, the maliciuos strategies are not skipped because the loop index is incremented incorrectly, which allows one malicious strategy to effectively block withdrawal from all staker's strategies.
The following test (modified testSlashQueuedWithdrawalNotBeaconChainETH
with non-empty indicesToSkip
, added at src/test/unit/StrategyManagerUnit.t.sol#L1575) passes, although it should not:
function testSlashQueuedWithdrawalWithSkipping() external { address recipient = address(333); uint256 depositAmount = 1e18; uint256 withdrawalAmount = depositAmount; bool undelegateIfPossible = false; (IStrategyManager.QueuedWithdrawal memory queuedWithdrawal, /*IERC20[] memory tokensArray*/, bytes32 withdrawalRoot) = testQueueWithdrawal_ToSelf_NotBeaconChainETH(depositAmount, withdrawalAmount, undelegateIfPossible); uint256 balanceBefore = dummyToken.balanceOf(address(recipient)); // slash the delegatedOperator slasherMock.freezeOperator(queuedWithdrawal.delegatedAddress); uint256[] memory indicesToSkip = new uint256[](1); indicesToSkip[0] = 0; cheats.startPrank(strategyManager.owner()); strategyManager.slashQueuedWithdrawal(recipient, queuedWithdrawal, _arrayWithJustDummyToken(), indicesToSkip); cheats.stopPrank(); uint256 balanceAfter = dummyToken.balanceOf(address(recipient)); require(balanceAfter == balanceBefore + withdrawalAmount, "balanceAfter != balanceBefore + withdrawalAmount"); require(!strategyManager.withdrawalRootPending(withdrawalRoot), "withdrawalRootPendingAfter is true!"); }
Increment the loop index in every loop cycle:
diff --git a/src/contracts/core/StrategyManager.sol b/src/contracts/core/StrategyManager.sol index fdf224a..58404a1 100644 --- a/src/contracts/core/StrategyManager.sol +++ b/src/contracts/core/StrategyManager.sol @@ -571,9 +571,9 @@ contract StrategyManager is // 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; } } }
Loop
#0 - c4-pre-sort
2023-05-09T13:09:13Z
0xSorryNotSorry marked the issue as duplicate of #205
#1 - c4-judge
2023-06-08T12:22:52Z
GalloDaSballo marked the issue as satisfactory
#2 - c4-judge
2023-06-08T12:23:27Z
GalloDaSballo changed the severity to 3 (High Risk)