First Flight #18: T-Swap

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

In `TswapPool::_swap` the extra tokens given to users after every `swapCount` breaks the protocol invariant of `x * y = k`

Summary

In TswapPool::_swap the extra tokens given to users after every swapCount breaks the protocol invariant of x * y = k

Vulnerability Details

The protocol follows a strict invariant of x * y = k, where

  • x: The balance of the pool token in the pool

  • y: The balance of WETH in the pool

  • k: The constant product of the 2 balances

This means that whenever the balances change in the protocol, the ratio between the two amounts should remain constant, hence the k. However, this is broken due to the extra incentive (a full extra token after every 10 swaps) in the _swap function, meaning that over time the protocol funds would be drained.

The following block of code is responsible for the issue.

swap_count++;
if (swap_count >= SWAP_COUNT_MAX) {
swap_count = 0;
outputToken.safeTransfer(msg.sender, 1_000_000_000_000_000_000);
}
  1. A user swaps 10 times and collects the extra incentive of 1 token (1_000_000_000_000_000_000)

  2. The usercontinues to swap until all the protocol funds are drained.

Consider the following proof of code:

Proof of Code
function test_InvariantBreaks() public {
// providing liquidity
vm.startPrank(liquidityProvider);
weth.approve(address(pool), 100e18);
poolToken.approve(address(pool), 100e18);
pool.deposit(100e18, 100e18, 100e18, uint64(block.timestamp));
vm.stopPrank();
uint256 outputWeth = 1e17;
// set up user, than perform 9 swaps
vm.startPrank(user);
poolToken.mint(user, 100e18);
poolToken.approve(address(pool), type(uint256).max);
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
// from the invariant test (the handler).
// We use these here because the interesting thing happens at the 10th swap
int256 startingY = int256(weth.balanceOf(address(pool)));
int256 expectedDeltaY = int256(-1) * int256(outputWeth); // this is deltaY
// and then do a swap for the 10th time
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
vm.stopPrank();
// from the invariant test (the handler)
uint256 endingY = weth.balanceOf(address(pool));
int256 actualDeltaY = int256(endingY) - int256(startingY); // this could be negative
assertEq(expectedDeltaY, actualDeltaY);
}

Impact

A user could maliciously drain the protocol of funds by doing a lot of swaps and collecting the extra incentive (a full extra token after every 10 swaps) given out by the protocol.

More simply put, the core invariant of the protocol is broken.

Tools Used

Manual review, Foundry.

Recommendations

Remove the extra incentive mechanism. If you want to keep this nonetheless, you should account for the change in the x * y = k invariant. Alternatively, you could set aside tokens the same way you did with fees.

- swap_count++;
- if (swap_count >= SWAP_COUNT_MAX) {
- swap_count = 0;
- outputToken.safeTransfer(msg.sender, 1_000_000_000_000_000_000);
- }
Updates

Appeal created

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

In `TSwapPool::_swap` the extra tokens given to users after every swapCount breaks the protocol invariant of x * y = k

Support

FAQs

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