sherlock-audit / 2024-06-leveraged-vaults-judging

9 stars 8 forks source link

Ironsidesec - `_sellStakedUSDe` is prone to slippage and MEV #85

Closed sherlock-admin3 closed 2 months ago

sherlock-admin3 commented 2 months ago

Ironsidesec

Medium

_sellStakedUSDe is prone to slippage and MEV

Summary

issue & likelihood: Executing the curve swap with 0 slippage when borrowToken == address(DAI) root cause : 0 slippage limit in curve sUSDe -> sDAI swap

Vulnerability Detail

Call flow : BaseStrategyVault.redeemFromNotional() -> BaseStakingVault._redeemFromNotional() -> EthenaVault._executeInstantRedemption() -> Ethena._sellStakedUSDe()

When _sellStakedUSDe swaps sUSDe into sDAI, the slippage is zero. But it is left 0 intended as the DAI to borrow token swap will check the slippage. But what if borrowToken == address(DAI), then no swap is executed and no slippage check is made in the second leg. Hence the sUSDe -> DAI  swap is prone to MEV / slippage issues.

If a private mempool is used, then the volatility and high slippage will impact here. If no private mempool is used, then the MEV attack is easy, frontrun this action by buying the maximum sDAI as possible, and it will increase the sDAI price compared to sUSDe in the curve pool. Then this action will run buying sDAI at a much higher price which will get backrun to sell the sDAI at a higher price than the price bought when frontrunning.

So, overall using 0 slippage when the borrow token is DAI, is prone to slippage. And using using 0 as a limit in the curve swap is an issue.

https://github.com/sherlock-audit/2024-06-leveraged-vaults/blob/14d3eaf0445c251c52c86ce88a84a3f5b9dfad94/leveraged-vaults-private/contracts/vaults/staking/protocols/Ethena.sol#L124-L167

    function _sellStakedUSDe(
        uint256 sUSDeAmount,
        address borrowToken,
        uint256 minPurchaseAmount,
        bytes memory exchangeData,
        uint16 dexId
    ) internal returns (uint256 borrowedCurrencyAmount) {
        Trade memory sDAITrade = Trade({
            tradeType: TradeType.EXACT_IN_SINGLE,
            sellToken: address(sUSDe),
            buyToken: address(sDAI),
            amount: sUSDeAmount,
    >>>>    limit: 0, // NOTE: no slippage guard is set here, it is enforced in the second leg
                      // of the trade.
            deadline: block.timestamp, 
            exchangeData: abi.encode(CurveV2Adapter.CurveV2SingleData({
                pool: 0x167478921b907422F8E88B43C4Af2B8BEa278d3A,
                fromIndex: 1, // sUSDe
                toIndex: 0 // sDAI
            }))
        });

        (/* */, uint256 sDAIAmount) = sDAITrade._executeTrade(uint16(DexId.CURVE_V2));

        // Unwraps the sDAI to DAI
        uint256 daiAmount = sDAI.redeem(sDAIAmount, address(this), address(this));

 >>>>   if (borrowToken != address(DAI)) {
            Trade memory trade = Trade({
                tradeType: TradeType.EXACT_IN_SINGLE,
                sellToken: address(DAI),
                buyToken: borrowToken,
                amount: daiAmount,
                limit: minPurchaseAmount,
                deadline: block.timestamp,
                exchangeData: exchangeData
            });

            // Trades the unwrapped DAI back to the given token.
            (/* */, borrowedCurrencyAmount) = trade._executeTrade(dexId);
        } else {
            borrowedCurrencyAmount = daiAmount;
        }
    }

Impact

If a private mempool is used, then the volatility and high slippage will impact here. If no private mempool is used, then the MEV attack is easy. Executing the curve swap with 0 slippage when borrowToken == address(DAI) is a loss of funds to protocol. So high impact with medium likelihood, due to borrowToken == address(DAI) condition.

Code Snippet

https://github.com/sherlock-audit/2024-06-leveraged-vaults/blob/14d3eaf0445c251c52c86ce88a84a3f5b9dfad94/leveraged-vaults-private/contracts/vaults/staking/protocols/Ethena.sol#L124-L167

Tool used

Manual Review

Recommendation

https://github.com/sherlock-audit/2024-06-leveraged-vaults/blob/14d3eaf0445c251c52c86ce88a84a3f5b9dfad94/leveraged-vaults-private/contracts/vaults/staking/protocols/Ethena.sol#L124-L167

    function _sellStakedUSDe(
        uint256 sUSDeAmount,
        address borrowToken,
        uint256 minPurchaseAmount,
        bytes memory exchangeData,
        uint16 dexId,
+       uint256 min_DaiAmount
    ) internal returns (uint256 borrowedCurrencyAmount) {
... SNIP ...

        (/* */, uint256 sDAIAmount) = sDAITrade._executeTrade(uint16(DexId.CURVE_V2));

        // Unwraps the sDAI to DAI
        uint256 daiAmount = sDAI.redeem(sDAIAmount, address(this), address(this));

        if (borrowToken != address(DAI)) {
            Trade memory trade = Trade({
                tradeType: TradeType.EXACT_IN_SINGLE,
                sellToken: address(DAI),
                buyToken: borrowToken,
                amount: daiAmount,
                limit: minPurchaseAmount,
                deadline: block.timestamp,
                exchangeData: exchangeData
            });

            // Trades the unwrapped DAI back to the given token.
            (/* */, borrowedCurrencyAmount) = trade._executeTrade(dexId);
        } else {
+           require(daiAmount >= min_DaiAmount);
            borrowedCurrencyAmount = daiAmount;
        }
    }

Duplicate of #18