First Flight #18: T-Swap

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

The function `TSwapPool::sellPoolTokens` incorrectly calls `swapExactOutput` rather than `swapExactInput`

Impact

The user will receive poolTokenAmount of Weth rather than actually sell the poolTokenAmount of Pool Tokens.

Description

The function TSwapPool::sellPoolTokens incorrectly calls TSwapPool::swapExactOutput where it should call
TSwapPool::swapExactInput as the user has specified the poolTokenAmount as input tokens to sell.

Recommendation
The following is indicative of possible solution.

/**
* @notice wrapper function to facilitate users
* selling pool tokens in exchange of WETH
* @param poolTokenAmount amount of pool tokens to sell
* @return wethAmount amount of WETH received by caller
*/
function sellPoolTokens(
uint256 poolTokenAmount
) external returns (uint256 wethAmount) {
return
- swapExactOutput(
- i_poolToken,
- i_wethToken,
- poolTokenAmount,
- uint64(block.timestamp)
- );
+ swapExactInput(
+ i_poolToken,
+ poolTokenAmount,
+ i_wethToken,
+ poolTokenAmount - 1, // we'll loose a little in fees
+ uint64(block.timestamp)
+ );
}

The NatSpec is as follows:

/*
* @notice figures out how much you need to input based on how much
* output you want to receive.
*
* Example: You say "I want 10 output WETH, and my input is DAI"
* The function will figure out how much DAI you need to input to get 10 WETH
* And then execute the swap
* @param inputToken ERC20 token to pull from caller
* @param outputToken ERC20 token to send to caller
* @param outputAmount The exact amount of tokens to send to caller
*/
function swapExactOutput(
IERC20 inputToken,
IERC20 outputToken,
uint256 outputAmount,
uint64 deadline
)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import { Test, console } from "forge-std/Test.sol";
import { PoolFactory } from "../../src/PoolFactory.sol";
import { TSwapPool } from "../../src/TSwapPool.sol";
import { ERC20Mock } from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {USDCMock} from "./USDCMock.sol"; //usdc
import {WETHMock} from "./WETHMock.sol"; //weth
contract ExperimentTest is Test {
TSwapPool pool;
USDCMock poolToken;
WETHMock weth;
PoolFactory poolFactory;
address liquidityProvider = makeAddr("liquidityProvider");
address user = makeAddr("user");
address poolAddress;
function setUp() public {
poolToken = new USDCMock();
weth = new WETHMock();
poolFactory = new PoolFactory( address(weth) );
poolAddress = poolFactory.createPool( address(poolToken) );
pool = TSwapPool(poolFactory.getPool( address(poolToken) ));
//pool = new TSwapPool(address(poolToken), address(weth), "LTokenA", "LA");
}
function testSellPoolTokens() public {
uint256 hundred = 1_000_000_000;
// generate some liquidity
weth.mint(liquidityProvider, hundred*100);
weth.mint(liquidityProvider, hundred*100);
poolToken.mint(liquidityProvider, hundred);
poolToken.mint(liquidityProvider, hundred);
// pool has liquidity
vm.startPrank(liquidityProvider);
weth.approve(address(pool), hundred);
poolToken.approve(address(pool), hundred);
pool.deposit(hundred, hundred, hundred, uint64(block.timestamp));
vm.stopPrank();
// verify the user has zero tokens
uint256 userBal = poolToken.balanceOf(address(user));
assertEq(0, userBal);
// give the user some tokens
poolToken.mint(user, 100);
vm.startPrank(user);
//pre-approve selling of weth and pool tokens...
weth.approve(address(pool), hundred*100);
poolToken.approve(address(pool), hundred*100);
uint256 ptBalBefore = poolToken.balanceOf(address(user));
// sell 9 tokens
console.log("Sell 9 pool tokens");
pool.sellPoolTokens(9);
vm.stopPrank();
uint256 ptBalAfter = poolToken.balanceOf(address(user));
console.log("pt before sell ", ptBalBefore);
console.log("pt after sell ", ptBalAfter);
}
}

Result

forge test --mt testSellPoolTokens -vvv
Ran 1 test for test/unit/ExperimentTest.t.sol:ExperimentTest
[PASS] testSellPoolTokens() (gas: 363232)
Logs:
Sell 9 pool tokens
pt before sell 100
pt after sell 10

The result of attempting to sell 9 (nine) pool tokens resulted in the user selling 90 (ninety pool tokens).

Running the test with the correct implementation as shown in the diff above yields the following result, which looks correct.

Ran 1 test for test/unit/ExperimentTest.t.sol:ExperimentTest
[PASS] testSellPoolTokens() (gas: 363362)
Logs:
Sell 9 pool tokens
pt before sell 100
pt after sell 91

Note: the bug might be easier to see if you use a Pool Token to Weth ratio that's not 1:1, such as the following:

pool.deposit(hundred, hundred, hundred/2, uint64(block.timestamp));

Furthermore, the implementation of the getInputAmountBasedOnOutput which is called by swapExactOutput incorrectly calculates the inputAmount return value by multiplying by 10_000 rather than 1_000 as described in the documentation.

References

Tools Used

  • Manual Review

Updates

Appeal created

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

`sellPoolTokens` mismatches input and output tokens causing users to receive the incorrect amount of tokens

Support

FAQs

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