EigenLayer Contest - Ruhum'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: 10/43

Findings: 2

Award: $1,972.18

🌟 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
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#L564

Vulnerability details

Impact

When slashing a queued withdrawal, the owner can specify an array of strategies that are supposed to be skipped, i.e. not withdrawn from. But, the implementation has a bug causing none of the strategies to be skipped.

The feature was implemented in case

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.

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

Proof of Concept

When you loop over the strategies to withdraw the funds, the function checks whether the current strategy is supposed to be skipped. If that's the case it doesn't execute the withdrawal:

        // keeps track of the index in the `indicesToSkip` array
        uint256 indicesToSkipIndex = 0;

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

The issue is that it only increments indiciesToSkipIndex and not i. Meaning, loop will be executed again with the same value for i but with a different value for inidicesToSkipIndex. In that execution indicesToSkip[indicesToSkipIndex] != i thus, the funds will be withdrawn from it.

Here's a PoC:

// StrategyManagerUnit.t.sol
    function testIssue() external {
        uint depositAmount = 1e18;
        address recipient = address(333);

        // add a deposit so we can withdraw it later on
        testDepositIntoStrategySuccessfully(/*staker*/ address(this), depositAmount);

        IStrategy[] memory strategyArray = new IStrategy[](2);
        IERC20[] memory tokensArray = new IERC20[](2);
        uint256[] memory shareAmounts = new uint256[](2);
        strategyArray[0] = dummyStrat;
        tokensArray[0] = dummyToken;
        shareAmounts[0] = 5e17;
        strategyArray[1] = dummyStrat;
        tokensArray[1] = dummyToken;
        shareAmounts[1] = 5e17;

        IStrategyManager.WithdrawerAndNonce memory withdrawerAndNonce = IStrategyManager.WithdrawerAndNonce({
            withdrawer: recipient,
            nonce: uint96(strategyManager.numWithdrawalsQueued(address(this)))
        });

        IStrategyManager.QueuedWithdrawal memory queuedWithdrawal = 
            IStrategyManager.QueuedWithdrawal({
                strategies: strategyArray,
                shares: shareAmounts,
                depositor: address(this),
                withdrawerAndNonce: withdrawerAndNonce,
                withdrawalStartBlock: uint32(block.number),
                delegatedAddress: strategyManager.delegation().delegatedTo(address(this))
            }
        );
        // calculate the withdrawal root
        bytes32 withdrawalRoot = strategyManager.calculateWithdrawalRoot(queuedWithdrawal);
        
        uint256[] memory strategyIndexes = new uint256[](2);
        strategyIndexes[0] = 0;
        strategyIndexes[1] = 0;

        strategyManager.queueWithdrawal(
            strategyIndexes,
            queuedWithdrawal.strategies,
            queuedWithdrawal.shares,
            /*withdrawer*/ recipient,
            false 
        );
        uint256 balanceBefore = dummyToken.balanceOf(address(recipient));

        slasherMock.freezeOperator(queuedWithdrawal.delegatedAddress);

        cheats.startPrank(strategyManager.owner());
        uint[] memory toSkip = new uint[](1);
        // skip first strategy
        toSkip[0] = 0;
        strategyManager.slashQueuedWithdrawal(recipient, queuedWithdrawal, tokensArray, toSkip);
        cheats.stopPrank();

        uint256 balanceAfter = dummyToken.balanceOf(address(recipient));

        assertEq(balanceAfter, balanceBefore + 5e17, "balanceAfter != balanceBefore + withdrawalAmount");
    }
 

Tools Used

Increment i as well:

    if (indicesToSkipIndex < indicesToSkip.length && indicesToSkip[indicesToSkipIndex] == i) {
                unchecked {
                    ++i;
                    ++indicesToSkipIndex;
                }

Assessed type

Loop

#0 - c4-pre-sort

2023-05-09T13:08:56Z

0xSorryNotSorry marked the issue as duplicate of #205

#1 - c4-judge

2023-06-08T12:22:04Z

GalloDaSballo marked the issue as satisfactory

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