Description
The function TSwapPool::getInputAmountBasedOnOutput
incorrectly calculates the return value, based on the description provided in the documentation.
The documentation states:
The TSwap protocol accrues fees from users who make swaps. Every swap has a 0.3 fee, represented in getInputAmountBasedOnOutput and getOutputAmountBasedOnInput. Each applies a 997 out of 1000 multiplier. That fee stays in the protocol.
The function is shown below for reference, with a suggested change.
function getInputAmountBasedOnOutput(
uint256 outputAmount,
uint256 inputReserves,
uint256 outputReserves
)
public
pure
revertIfZero(outputAmount)
revertIfZero(outputReserves)
returns (uint256 inputAmount)
{
return
- ((inputReserves * outputAmount) * 10000) /
+ ((inputReserves * outputAmount) * 1000) /
((outputReserves - outputAmount) * 997);
}
Impact
The protocol incorrectly calculates the input amount of tokens required to return the required output token.
Proof of Concept
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";
import {WETHMock} from "./WETHMock.sol";
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) ));
}
function testGetInputAmountBasedOnOutput() public {
uint256 hundred = 1_000_000_000;
weth.mint(liquidityProvider, hundred);
poolToken.mint(liquidityProvider, hundred);
vm.startPrank(liquidityProvider);
weth.approve(address(pool), hundred);
poolToken.approve(address(pool), hundred);
pool.deposit(hundred, hundred, hundred/2, uint64(block.timestamp));
vm.stopPrank();
poolToken.mint(user, 100);
weth.mint(user, 100);
vm.startPrank(user);
uint256 inputReserves = poolToken.balanceOf(address(pool));
uint256 outputReserves = weth.balanceOf(address(pool));
uint256 inputAmount = pool.getInputAmountBasedOnOutput(
10,
inputReserves,
outputReserves
);
vm.stopPrank();
console.log("inputAmount ", inputAmount);
}
Result with current implemetation ((inputReserves * outputAmount) * 10000)
Ran 1 test for test/unit/ExperimentTest.t.sol:ExperimentTest
[PASS] testGetInputAmountBasedOnOutput() (gas: 290707)
Logs:
inputAmount 50
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.41ms (670.25µs CPU time)
This incorrect results requires the user to input 50 tokens to receive 10 - clearly this is incorrect for a 2:1 token ratio.
Changing the code as shown yeilds the following results
- ((inputReserves * outputAmount) * 10000) /
+ ((inputReserves * outputAmount) * 1000) /
Result with ((inputReserves * outputAmount) * 1000)
- The suggested code change.
forge test --mt testGetInputAmountBasedOnOutput -vvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/unit/ExperimentTest.t.sol:ExperimentTest
[PASS] testGetInputAmountBasedOnOutput() (gas: 290707)
Logs:
inputAmount 5
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.83ms (441.88µs CPU time)
This result requires the user to input 5 tokens to get 10 which is what you'd expect for a 2:1 token ratio. Input 5 to receive 10.
References
https://github.com/Cyfrin/2024-06-t-swap/blob/d1783a0ae66f4f43f47cb045e51eca822cd059be/src/TSwapPool.sol#L292
Tools Used