Title: Fee ignores token decimals — non-18-decimal tokens pay 10^12× less
Impact: High. USDC/USDT flash loan fees are negligible compared to equivalent ETH loans.
Likelihood: High. Deterministic — applies to every non-18-decimal token in scope.
Reference Files: repos/src/protocol/ThunderLoan.sol:246-251
getCalculatedFee multiplies the raw token amount by getPriceInWeth without normalizing for the token's decimals. The price oracle returns a value in 18-decimal WETH precision, but when amount has 6 decimals (USDC) or 8 decimals (WBTC), the multiplication produces a value 10^12 or 10^10× smaller than intended. The contract imports IERC20Metadata which exposes decimals() but never calls it in the fee calculation path.
The fee disparity is not a rounding artifact — it is a dimensional mismatch where the formula assumes all tokens have 18 decimals.
Impact: High. A $1M USDC flash loan pays approximately 0.000000006 USD in fees instead of the intended ~$3,000. LPs providing USDC, USDT, or WBTC liquidity earn effectively zero yield because the fee numerator is 10^12× too small.
Likelihood: High. The bug is deterministic — every non-18-decimal token flash loan undercharges by 10^9–10^12× without exception. The fix requires only importing IERC20Metadata.decimals() which is already imported but unused.
With USDC, USDT, and WBTC all explicitly in scope, the majority of token pairs on the protocol are affected from the moment they are allowed.
The fee for a $2000 USDC loan is less than one-billionth of the fee for an equivalent ETH loan, despite both representing the same dollar value.
Normalize amount to 18 decimals before multiplying by the oracle price, ensuring consistent units regardless of the token's native precision.
## Description Within the functions `ThunderLoan::getCalculatedFee()` and `ThunderLoanUpgraded::getCalculatedFee()`, an issue arises with the calculated fee value when dealing with non-standard ERC20 tokens. Specifically, the calculated value for non-standard tokens appears significantly lower compared to that of standard ERC20 tokens. ## Vulnerability Details //ThunderLoan.sol ```solidity function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) { //slither-disable-next-line divide-before-multiply @> uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / s_feePrecision; @> //slither-disable-next-line divide-before-multiply fee = (valueOfBorrowedToken * s_flashLoanFee) / s_feePrecision; } ``` ```solidity //ThunderLoanUpgraded.sol function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) { //slither-disable-next-line divide-before-multiply @> uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / FEE_PRECISION; //slither-disable-next-line divide-before-multiply @> fee = (valueOfBorrowedToken * s_flashLoanFee) / FEE_PRECISION; } ``` ## Impact Let's say: - user_1 asks a flashloan for 1 ETH. - user_2 asks a flashloan for 2000 USDT. ```solidity function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) { //1 ETH = 1e18 WEI //2000 USDT = 2 * 1e9 WEI uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / s_feePrecision; // valueOfBorrowedToken ETH = 1e18 * 1e18 / 1e18 WEI // valueOfBorrowedToken USDT= 2 * 1e9 * 1e18 / 1e18 WEI fee = (valueOfBorrowedToken * s_flashLoanFee) / s_feePrecision; //fee ETH = 1e18 * 3e15 / 1e18 = 3e15 WEI = 0,003 ETH //fee USDT: 2 * 1e9 * 3e15 / 1e18 = 6e6 WEI = 0,000000000006 ETH } ``` The fee for the user_2 are much lower then user_1 despite they asks a flashloan for the same value (hypotesis 1 ETH = 2000 USDT). ## Recommendations Adjust the precision accordinly with the allowed tokens considering that the non standard ERC20 haven't 18 decimals.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.