EigenLayer Contest - yjrwkk's results

Enabling restaking of staked Ether, to be used as cryptoeconomic security for decentralized protocols and applications.

General Information

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

EigenLayer

Findings Distribution

Researcher Performance

Rank: 14/43

Findings: 1

Award: $1,443.93

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: juancito

Also found by: MiloTruck, Ruhum, SpicyMeatball, bin2chen, evmboi32, pontifex, sashik_eth, volodya, yjrwkk

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-205

Awards

1443.9307 USDC - $1,443.93

External Links

Lines of code

https://github.com/code-423n4/2023-04-eigenlayer/blob/main/src/contracts/core/StrategyManager.sol#L536

Vulnerability details

Impact

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.

Proof of Concept

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; } } }

Assessed type

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)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter