Thunder Loan

AI First Flight #7
Beginner FriendlyFoundryDeFiOracle
EXP
View results
Submission Details
Severity: high
Valid

getCalculatedFee() assumes 18 decimals, causing fees to be ~1e12x smaller for non-standard tokens like USDT

Root + Impact

getCalculatedFee() hard-codes s_feePrecision = 1e18 as the scaling factor when converting token amounts to a WETH-equivalent value, so tokens with fewer than 18 decimals (e.g. USDT at 6 decimals) produce fees that are orders of magnitude smaller than the protocol intends.

Description

  • Flash-loan fees are computed by converting the borrowed amount to a WETH-denominated value using the token's price, then multiplying by the flash-loan fee rate. The formula is designed for 18-decimal tokens and divides by s_feePrecision (= 1e18) to normalize the result.

  • For a token with 6 decimals the raw amount is already 1e12 times smaller than the equivalent 18-decimal amount, but the formula divides by the same 1e18, so the computed fee is roughly 1e12 times smaller than the fee a user borrowing the same USD value of an 18-decimal token would pay.

// src/protocol/ThunderLoan.sol
// @> s_feePrecision = 1e18 is applied regardless of token decimals
function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) {
// @> amount for a 6-decimal token is 1e12x smaller than an 18-decimal equivalent
uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / s_feePrecision;
// @> fee inherits the 1e12x underestimation
fee = (valueOfBorrowedToken * s_flashLoanFee) / s_feePrecision;
}

Risk

Likelihood:

  • ThunderLoan is designed to support any ERC20 token; USDT, USDC, and WBTC — all of which have fewer than 18 decimals — are among the most common flash-loan tokens in DeFi.

  • Every flash loan of a non-18-decimal token triggers the undercharge, requiring no special setup or attack; normal protocol usage is sufficient.

Impact:

  • Liquidity providers earn a fraction of the fee they are owed — for USDT borrowers the shortfall is a factor of ~1e12 compared to an ETH borrower of equivalent USD value.

  • The protocol accumulates far less revenue than modeled, undermining the economic security and sustainability of the liquidity pool.

Proof of Concept

Consider two flash loans of equivalent USD value: 1 ETH and 2000 USDT (priced at 2e9 WETH-per-USDT in the oracle, representing 1 ETH = 2000 USDT).

// --- ETH flash loan ---
// amount = 1e18 (18 decimals), price = 1e18 (1 ETH per ETH), s_flashLoanFee = 3e15 (0.3%)
// valueOfBorrowedToken = (1e18 * 1e18) / 1e18 = 1e18
// fee = (1e18 * 3e15) / 1e18 = 3e15 wei → 0.003 ETH ✓
// --- USDT flash loan (same USD value) ---
// amount = 2000e6 = 2e9 (6 decimals), price = 2e9 (WETH-per-USDT at 6-decimal scale)
// valueOfBorrowedToken = (2e9 * 2e9) / 1e18 = 4e18 / 1e18 = 4 (rounds to 4 wei)
// fee = (4 * 3e15) / 1e18 = 0 (rounds to zero for small amounts; ~6e-12 ETH at best)
// Fee ratio: 3e15 / ~6e6 ≈ 5e8x — effectively zero revenue for USDT loans

The USDT flash loan of the same USD value produces a fee that is effectively zero, while the ETH loan correctly charges 0.003 ETH. At scale the revenue loss is catastrophic for liquidity providers.

Recommended Mitigation

Normalize amount to 18 decimals before applying the fee formula by scaling up using the token's actual decimal count. This ensures the intermediate valueOfBorrowedToken is always expressed in 18-decimal units regardless of the underlying token.

+ import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) {
+ uint8 decimals = IERC20Metadata(address(token)).decimals();
+ uint256 normalizedAmount = amount * (10 ** (18 - decimals));
- uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / s_feePrecision;
+ uint256 valueOfBorrowedToken = (normalizedAmount * getPriceInWeth(address(token))) / s_feePrecision;
fee = (valueOfBorrowedToken * s_flashLoanFee) / s_feePrecision;
}

Apply the same fix to ThunderLoanUpgraded.sol.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-03] fee are less for non standard ERC20 Token

## 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.

Support

FAQs

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

Give us feedback!