sherlock-audit / 2024-02-rio-network-core-protocol-judging

4 stars 4 forks source link

Aymen0909 - Unable to Claim EigenLayer Withdrawals through `settleEpochFromEigenLayer` #353

Closed sherlock-admin closed 6 months ago

sherlock-admin commented 7 months ago

Aymen0909

medium

Unable to Claim EigenLayer Withdrawals through settleEpochFromEigenLayer

Summary

If the deposit pool contains enough funds to settle an epoch after that epoch was already queued into EigenLayer withdrawal through queueCurrentEpochSettlement, the settleEpochFromEigenLayer function will be DoSed, and it will be impossible to claim the EigenLayer withdrawals for that epoch.

Vulnerability Detail

In the RioLRTCoordinator contract, when the rebalance function is called, there are two ways in which a withdrawal can proceed (after invoking _processUserWithdrawalsForCurrentEpoch under the hood):

The _processUserWithdrawalsForCurrentEpoch function will handle all the withdrawal logic in the rebalancing call:

function _processUserWithdrawalsForCurrentEpoch(address asset, uint256 sharesOwed) internal {
    IRioLRTWithdrawalQueue withdrawalQueue_ = withdrawalQueue();
    (uint256 assetsSent, uint256 sharesSent) = depositPool().transferMaxAssetsForShares(
        asset,
        sharesOwed,
        address(withdrawalQueue_)
    );
    uint256 sharesRemaining = sharesOwed - sharesSent;

    // Exit early if all pending withdrawals were paid from the deposit pool.
    if (sharesRemaining == 0) {
        withdrawalQueue_.settleCurrentEpoch(asset, assetsSent, sharesSent);
        return;
    }

    address strategy = assetRegistry().getAssetStrategy(asset);
    bytes32 aggregateRoot = OperatorOperations.queueWithdrawalFromOperatorsForUserSettlement(
        operatorRegistry(),
        strategy,
        sharesRemaining
    );

    withdrawalQueue_.queueCurrentEpochSettlement(asset, assetsSent, sharesSent, aggregateRoot);
}

In this rebalance process, there is a scenario in which all the queued EigenLayer withdrawals can't be claimed at all through settleEpochFromEigenLayer and will be lost.

The following example illustrates that scenario:

function settleEpochFromEigenLayer(
    address asset,
    uint256 epoch,
    IDelegationManager.Withdrawal[] calldata queuedWithdrawals,
    uint256[] calldata middlewareTimesIndexes
) external {
    EpochWithdrawals storage epochWithdrawals = _getEpochWithdrawals(asset, epoch);
    if (epochWithdrawals.sharesOwed == 0) revert NO_SHARES_OWED_IN_EPOCH();
    // @audit will revert as epoch is already settled
    if (epochWithdrawals.settled) revert EPOCH_ALREADY_SETTLED();
    if (epochWithdrawals.aggregateRoot == bytes32(0)) revert WITHDRAWALS_NOT_QUEUED_FOR_EPOCH();
    ...
}
function queueWithdrawalForUserSettlement(address strategy, uint256 shares) external onlyCoordinator returns (bytes32 root) {
    if (strategy == BEACON_CHAIN_STRATEGY) {
        _increaseETHQueuedForUserSettlement(shares);
    }
    root = _queueWithdrawal(strategy, shares, address(withdrawalQueue()));
}

Impact

If the deposit pool contains enough funds to settle an epoch after that epoch was already queued into EigenLayer withdrawal through queueCurrentEpochSettlement, the settleEpochFromEigenLayer function will be DoSed, and it will be impossible to claim the EigenLayer withdrawals, resulting in a loss of funds for the protocol and the users, and making all the shares accounting logic wrong.

Code Snippet

https://github.com/sherlock-audit/2024-02-rio-network-core-protocol/blob/main/rio-sherlock-audit/contracts/restaking/RioLRTCoordinator.sol#L245-L267

https://github.com/sherlock-audit/2024-02-rio-network-core-protocol/blob/main/rio-sherlock-audit/contracts/restaking/RioLRTWithdrawalQueue.sol#L216-L271

Tool used

Manual Review

Recommendation

To address this issue, the simplest method is to increment the epoch currentEpochsByAsset when RioLRTWithdrawalQueue.queueCurrentEpochSettlement is called. In this case, the epoch will not be settled in a second rebalance call as highlighted in the scenario above.

Duplicate of #4