Hardcoded decimal value leads to incorrect conversion when ERC20 does not use 18 decimals.
Description
The convenience function below calls getOutputAmountBasedOnInput() with a hardcoded value of 1e18 for the parameter inputAmount. This assumes the ERC token will use 18 decimals which is not true for all ERC tokens. eg. USDC uses 6 decimals and WBTC uses 8.
function getPriceOfOnePoolTokenInWeth() external view returns (uint256) {
return
getOutputAmountBasedOnInput(
1e18,
i_poolToken.balanceOf(address(this)),
i_wethToken.balanceOf(address(this))
);
}
This leads to an incorrect calculation of the value of one token in the pool.
The documentation states "Any ERC20 token":
Audit Scope Details
Impact
The protocol does not correctly support all ERC tokens even though the documentation states all ERC tokens are supported. When tokens that utilise a decimal value other than 18 are used in the pool the conversion calculations above will be incorrect.
Severity
This function would likely be used to make decisions based on expected pricing and therefore could pose a significant risk to end-users.
Medium Impact
High Likelihood
Severity: Medium
Proof of Concept
The following standalone unit test demonstrates the issue:
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 testDecimals() public {
uint256 oneToken = 1e6;
uint256 liquidity = oneToken * 1000;
weth.mint(liquidityProvider, liquidity);
poolToken.mint(liquidityProvider, liquidity);
weth.mint(user, oneToken);
vm.startPrank(liquidityProvider);
weth.approve(address(pool), liquidity);
poolToken.approve(address(pool), liquidity);
pool.deposit(liquidity, liquidity, liquidity, uint64(block.timestamp));
vm.stopPrank();
uint256 priceInWethForOnePoolToken = pool.getPriceOfOnePoolTokenInWeth();
uint256 oneWeth = 1e18;
console.log("One Pool Token: ", oneToken);
console.log("One Weth Token: ", oneWeth);
console.log("priceInWethForOnePoolToken: ",priceInWethForOnePoolToken);
}
}
pragma solidity 0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract USDCMock is ERC20 {
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
constructor() ERC20("USDCMock", "USDCM") { }
function decimals() public view virtual override returns (uint8) {
return 18;
}
}
pragma solidity 0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract WETHMock is ERC20 {
constructor() ERC20("WETHMock", "WETHM") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
Result with current code:
Ran 1 test for test/unit/ExperimentTest.t.sol:ExperimentTest
[PASS] testDecimals() (gas: 239736)
Logs:
One Pool Token: 1000000
One Weth Token: 1000000000000000000
priceInWethForOnePoolToken: 999999998
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.07ms (1.62ms CPU time)
Result with implemenation using 1e6 is shown below:
function getPriceOfOneUSDCPoolTokenInWeth() external view returns (uint256) {
return
getOutputAmountBasedOnInput(
1e6,
i_poolToken.balanceOf(address(this)),
i_wethToken.balanceOf(address(this))
);
}
Ran 1 test for test/unit/ExperimentTest.t.sol:ExperimentTest
[PASS] testDecimals() (gas: 242930)
Logs:
One Pool Token: 1000000
One Weth Token: 1000000000000000000
priceInWethForOnePoolToken: 999999998
priceInWethForOneUSDCPoolToken: 996006
Recommended mitigation
References
https://github.com/Cyfrin/2024-06-t-swap/blob/d1783a0ae66f4f43f47cb045e51eca822cd059be/src/TSwapPool.sol#L452
https://github.com/Cyfrin/2024-06-t-swap/blob/d1783a0ae66f4f43f47cb045e51eca822cd059be/src/TSwapPool.sol#L461
Tools Used