First Flight #18: T-Swap

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

`TSwapPool::_swap()` is giving away extra tokens, breaking system's invariant property

Summary

for every 10 swaps, there is an additional transfer of 1 ether to the swapper hence, the protocol invariant breaks

Vulnerability Details

code

Place below code in test/invariants/Invariant.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import { Test, StdInvariant } from "forge-std/Test.sol";
import { TSwapPool } from "../../src/TSwapPool.sol";
import { PoolFactory } from "../../src/PoolFactory.sol";
import { ERC20Mock } from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
import { Handler } from "./Handler.sol";
contract Invariant is StdInvariant, Test {
TSwapPool public pool;
PoolFactory public factory;
ERC20Mock public token;
ERC20Mock public weth;
Handler public handler;
address liquidityProvider = makeAddr("liquidityProvider");
uint256 public STARTING_WETH = 100 ether;
uint256 public STARTING_TOKEN = 50 ether;
function setUp() public {
factory = new PoolFactory(address(weth));
weth = new ERC20Mock();
token = new ERC20Mock();
pool = TSwapPool(factory.createPool(address(token)));
vm.startPrank(liquidityProvider);
weth.mint(liquidityProvider, STARTING_WETH);
token.mint(liquidityProvider, STARTING_TOKEN);
pool.deposit(STARTING_WETH, STARTING_WETH, STARTING_TOKEN, uint64(block.timestamp));
handler = new Handler(address(pool), address(weth), address(token));
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = handler.deposit.selector;
selectors[1] = handler.swap.selector;
targetSelector(FuzzSelector({ addr: address(handler), selectors: selectors }));
targetContract(address(handler));
}
function stateful_InvariantX() public view {
assertEq(handler.actualDeltaWeth(), handler.expectedDeltaWeth());
}
function stateful_InvariantY() public view {
assertEq(handler.actualDeltaToken(), handler.expectedDeltaToken());
}
}

Place below code in test/invariants/Handler.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import { Test } from "forge-std/Test.sol";
import { TSwapPool } from "../../src/TSwapPool.sol";
import { ERC20Mock } from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
contract Handler is Test {
TSwapPool public pool;
ERC20Mock public weth;
ERC20Mock public token;
address liquidityProvider = makeAddr("liquidityProvider");
address user = makeAddr("user");
uint256 public actualDeltaWeth;
uint256 public actualDeltaToken;
uint256 public expectedDeltaWeth;
uint256 public expectedDeltaToken;
uint256 startingWeth;
uint256 startingToken;
constructor(address _tswapPool, address _weth, address _token) {
pool = TSwapPool(_tswapPool);
weth = ERC20Mock(_weth);
token = ERC20Mock(_token);
}
function deposit(uint256 wethToDeposit) public {
bound(wethToDeposit, pool.getMinimumWethDepositAmount(), type(uint64).max);
uint256 poolTokenToDeposit = pool.getPoolTokensToDepositBasedOnWeth(wethToDeposit);
startingWeth = weth.balanceOf(address(pool));
startingToken = token.balanceOf(address(pool));
expectedDeltaToken = poolTokenToDeposit;
expectedDeltaWeth = wethToDeposit;
vm.startPrank(liquidityProvider);
weth.mint(liquidityProvider, wethToDeposit);
token.mint(liquidityProvider, poolTokenToDeposit);
weth.approve(address(pool), wethToDeposit);
token.approve(address(pool), poolTokenToDeposit);
pool.deposit(wethToDeposit, 0, poolTokenToDeposit, uint64(block.timestamp));
vm.stopPrank();
actualDeltaWeth = weth.balanceOf(address(pool)) - startingWeth;
actualDeltaToken = token.balanceOf(address(pool)) - startingToken;
}
function swap(uint256 outputWethAmount) public {
bound(outputWethAmount, 0, weth.balanceOf(address(pool)));
uint256 inputTokenAmount = pool.getInputAmountBasedOnOutput(
outputWethAmount, token.balanceOf(address(pool)), weth.balanceOf(address(pool))
);
startingWeth = weth.balanceOf(address(pool));
startingToken = token.balanceOf(address(pool));
expectedDeltaWeth = weth.balanceOf(address(pool)) - outputWethAmount;
expectedDeltaToken = token.balanceOf(address(pool)) + inputTokenAmount;
vm.prank(user);
token.mint(user, inputTokenAmount);
token.approve(address(pool), inputTokenAmount);
pool.swapExactOutput(token, weth, outputWethAmount, uint64(block.timestamp));
vm.stopPrank();
actualDeltaWeth = weth.balanceOf(address(pool)) - startingWeth;
actualDeltaToken = token.balanceOf(address(pool)) - startingToken;
}
}

Now run forge test --mt stateful_InvariantX and forge test --mt stateful_InvariantY to see if invariant breaks or no.

Impact

protocol breaks and becomes unusable if invariant breaks.

Tools Used

Foundry

Recommendations

Make below code changes in TSwapPool.sol

function _swap(
...
) private {
...
- swap_count++;
- if (swap_count >= SWAP_COUNT_MAX) {
- swap_count = 0;
- outputToken.safeTransfer(msg.sender, 1_000_000_000_000_000_000);
- }
emit Swap(msg.sender, inputToken, inputAmount, outputToken, outputAmount);
inputToken.safeTransferFrom(msg.sender, address(this), inputAmount);
outputToken.safeTransfer(msg.sender, outputAmount);
}
Updates

Appeal created

inallhonesty Lead Judge 11 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.