First Flight #18: T-Swap

First Flight #18
Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Lack of slippage protection in `TSwapPool::swapExactOutput` exposes user to MEV and causes to spend more than expected

Summary

The TSwapPool::swapExactOutput function does not include any sort of slippage protection. This function is similar to what is done in TSwapPool::swapExactInput, where the function specifies a minOutputAmount, the swapExactOutput function should specify a maxInputAmount to limit the amount of tokens the user is willing to pay.

Impact

This vulnerability makes the user succeptible to MEV (sandwich attacks and frontrunning attacks).

Any user on the Ethereum network has the ability to watch for new transactions being sent to the network. When the attacker sees a large victim transaction that they want to front run come in, they can create a similar transaction that would move the market up. They then increase their gas fees to ensure that their order gets executed first. The attacker transaction executes, raising the price of the asset, and then the victim transaction executes at the higher price. The attacker is then free to exit the position immediately, pocketing the difference, having never exposed themselves to any risk.

Sophisicated front-runners will likely call these transactions from their own contract addresses to make sure they end up with the prices they expect, and don't collide with other front-runners.

For example:

  1. The price of 1 WETH right now is 1000 USDC

  2. User inputs a swapExactOutput looking for 1 WETH

    1. inputToken = USDC

    2. outputToken = WETH

    3. outputAmount = 1

    4. deadline = whatever

  3. The function does not offer a maxInput Amount

  4. As the transaction is pending in the mempool, the market changes! And the price moves HUGE -> 1 WETH is now 10000 USDC. 10x more than the user expected.

  5. The transaction completes, but the user sent the protocol 10000 USDC instead of the expected 1000 USDC.

Proof of Code

Add this test to TSwapPool.t.sol:

function testSwapExactOutputLackOfSlippageProtection() public {
// Liquidity provider adds liquidity to the pool
testDeposit();
// This is what the user estimates that will receive after the swap
uint256 OUTPUT_WETH = 1e17;
uint256 expectedPoolTokenToSpend = pool.getInputAmountBasedOnOutput(
OUTPUT_WETH, poolToken.balanceOf(address(pool)), weth.balanceOf(address(pool))
);
// mevAttacker sees the transaction of the user and puts this before on the mempool
address mevAttacker = makeAddr("mevAttacker");
vm.startPrank(mevAttacker);
// We assume the attack comes from a whale or after a flashloan
uint256 MEV_POOL_TOKENS = 50e18;
poolToken.mint(mevAttacker, MEV_POOL_TOKENS);
poolToken.approve(address(pool), type(uint256).max);
pool.swapExactInput(poolToken, MEV_POOL_TOKENS, weth, 0, uint64(block.timestamp));
vm.stopPrank();
// User transaction goes through
vm.startPrank(user);
poolToken.approve(address(pool), type(uint256).max);
poolToken.mint(user, 100e18);
uint256 poolTokensSpentByUser = pool.swapExactOutput(poolToken, weth, OUTPUT_WETH, uint64(block.timestamp));
vm.stopPrank();
// mevAttacker completes the sandwich and takes profits
vm.startPrank(mevAttacker);
weth.approve(address(pool), type(uint256).max);
uint256 wethBalanceOfMevAttacker = weth.balanceOf(address(mevAttacker));
pool.swapExactInput(weth, wethBalanceOfMevAttacker, poolToken, 0, uint64(block.timestamp));
vm.stopPrank();
uint256 poolTokenBalanceOfMevAttackerAfterSandwich = poolToken.balanceOf(address(mevAttacker));
// MEV attacker has more pool tokens than before the sandwich
assert(poolTokenBalanceOfMevAttackerAfterSandwich > MEV_POOL_TOKENS);
// User spends more pool tokens than expected
assert(poolTokensSpentByUser > expectedPoolTokenToSpend);
}

Tools Used

Foundry test suite and manual review

Recommendations

Include a maxInputAmount so the user only has to spend up to a specific amount, and can predict how much they will spend on the protocol.

function swapExactOutput(
IERC20 inputToken,
IERC20 outputToken,
uint256 outputAmount,
+ uint256 maxInputAmount
uint64 deadline
)
public
revertIfZero(outputAmount)
revertIfDeadlinePassed(deadline)
returns (uint256 inputAmount)
{
uint256 inputReserves = inputToken.balanceOf(address(this));
uint256 outputReserves = outputToken.balanceOf(address(this));
+ if (inputAmount > maxInputAmount) {
+ revert TSwapPool__InputTooHigh(inputAmount, maxInputAmount);
+ }
inputAmount = getInputAmountBasedOnOutput(outputAmount, inputReserves, outputReserves);
_swap(inputToken, inputAmount, outputToken, outputAmount);
}
Updates

Appeal created

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Lack of slippage protection in `TSwapPool::swapExactOutput` causes users to potentially receive way fewer tokens

Support

FAQs

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