code-423n4 / 2024-05-bakerfi-findings

4 stars 4 forks source link

MEV bots can sandwich withdrawals from a Vault to extract funds from user's withdrawals #44

Closed c4-bot-6 closed 5 months ago

c4-bot-6 commented 5 months ago

Lines of code

https://github.com/code-423n4/2024-05-bakerfi/blob/main/contracts/core/strategies/StrategyLeverage.sol#L612 https://github.com/code-423n4/2024-05-bakerfi/blob/main/contracts/core/strategies/StrategyAAVEv3WSTETH.sol#L96 https://github.com/code-423n4/2024-05-bakerfi/blob/main/contracts/core/hooks/UseSwapper.sol#L72

Vulnerability details

Impact

User's funds are at risk because the swaps don't enforce a correct slippage to prevent MEV manipulation.

Proof of Concept

When withdrawing from a Vault, the Vault will burn shares from the withdrawer and will send him native ETH.

The withdrawal process looks like this:

  1. The Vault computes the amount of ETH to withdraw for the specified amount of shares to be burnt
  2. The Vault calls the StrategyLeverage.undeploy() function to proceed with the withdrawal of the corresponding amount of collateral from Aave.
  3. The Strategy computes the delta of the Collateral and Debt expressed in their ETH value that is required to accommodate the withdrawal in Aave
  4. The Strategy takes a flashloan in Balancer to borrow the deltaDebtInETH.
  5. Using the flashloaned WETH, the strategy repays WETH debt in Aave
  6. The Strategy withdraws (if enough deposits) the deltaCollateralInETH amount of collateral from Aave.
  7. All the withdrawn collateral is swapped for WETH using a Uniswap.
  8. The Strategy unwraps the difference between all the received WETH from the previous step and all the debt owed to balancer (flashloned amount + fees)
  9. The native received from unwrapping WETH in the previous step is then sent to the Vault
  10. Finally, the received native ETH from the Strategy is forwarded to the withdrawer.

The problem that allows MEV bots to steal from users is located in step 8.

When requesting the swap to the UniRouter to swap the withdrawn collateral for WETH, the Strategy doesn't compute a minimum amount of tokensOut to receive, it simply sets the amountOutMinimum parameter as 0.

> StrategyLeverage.sol

function _convertToWETH(uint256 amount) internal virtual returns (uint256) {
 // 1.Swap cbETH -> WETH/wstETH/rETH
 return
 //@audit-info => Swaps a collateral for WETH
 _swap(
 ISwapHandler.SwapParams(
 ierc20A(), // Asset In
 wETHA(), // Asset Out

 //@audit-info => swap of type EXACT_INPUT
 ISwapHandler.SwapType.EXACT_INPUT, // Swap Mode

 //@audit-info => Swapp all the withdrawn collateral
 amount, // Amount In

 //@audit-info => amountOut is set as 0
 0, // Amount Out

 _swapFeeTier, // Fee Pair Tier
 bytes("") // User Payload
 )
 );
}

> UseSwapper.sol
function _swap(
 ISwapHandler.SwapParams memory params
) internal override returns (uint256 amountOut) {
 ...

 // Exact Input
 if (params.mode == ISwapHandler.SwapType.EXACT_INPUT) {
 amountOut = _uniRouter.exactInputSingle(
 IV3SwapRouter.ExactInputSingleParams({
 tokenIn: params.underlyingIn,
 tokenOut: params.underlyingOut,
 amountIn: params.amountIn,

 //@audit-issue => Setting `amountOutMinimum` as 0. No slippage protection
 amountOutMinimum: 0,

 fee: fee,
 recipient: address(this),
 sqrtPriceLimitX96: 0
 })
 );
 ...
 } 
 ...
 ...
 ...

 ...

}

The problem is the type of swap used to swap Collateral for WETH is an EXACT_INPUT swap, which means, the UniRouter will swaps amountIn of one token for as much as possible of another token

For example, if 9 WETH were borrowed as a flashloan, and 9.5 wstETH were withdrawn from Aave. The Strategy only requires at the minimum to have 9 WETH + fees to repay the flashloan.

Tools Used

Manual Audit & UniswapRouter Documentation

Recommended Mitigation Steps

The recommendation to prevent this problem is to specify a value for the parameter amountOutMinimum, do not leave it as 0, and make sure to apply a reasonable slippage to the amountOutMinimum of WETH the Strategy wants to receive for swapping the withdrawn collateral from Aave, as a reference, the automatic slippage when swapping directly on the Uniswap app ranges from 0.1% to 5%

And, on the UseSwapper._swap() function, make sure to set the amountOutMinimum as the value passed on the params.amountOut

Assessed type

Uniswap

c4-judge commented 5 months ago

0xleastwood marked the issue as duplicate of #32

c4-judge commented 5 months ago

0xleastwood marked the issue as satisfactory