Thunder Loan

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

getCalculatedFee() returns 0 for small or low-priced token amounts, enabling free flash loans

Root + Impact

Description

  • getCalculatedFee() performs two sequential integer divisions that both truncate toward zero. For tokens with a low WETH price or for small loan amounts, the intermediate value valueOfBorrowedToken truncates to zero, making fee also zero.

  • A borrower can take a flash loan of a non-trivial amount and pay no fee at all if the product amount * getPriceInWeth(token) is less than s_feePrecision (1e18), causing the first division to round down to zero.

  • This is independent of the double-division precision loss issue (L-03 / 10-thunder-L2.md): L-01 concerns complete fee elimination, not just reduced fees.

// ThunderLoan.sol lines 246-251
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;
}

If amount * getPriceInWeth(token) < 1e18, then valueOfBorrowedToken = 0 and fee = 0.

Risk

Likelihood: Medium — tokens priced far below WETH (e.g. meme tokens, stablecoins on a different decimal scale, or tokens added with a very small TSwap price) create windows where small-to-medium amounts produce zero fees. The required loan size depends on the token's WETH price.

Impact: Low — the protocol loses fee revenue for those loans but LP principal is unaffected (the loan is still repaid). However, an attacker can arbitrage or manipulate state repeatedly at zero cost.

Proof of Concept

s_feePrecision = 1e18
s_flashLoanFee = 3e15 (0.3%)
Example: token WETH price = 1e10 (very cheap token), amount = 1e6
valueOfBorrowedToken = (1e6 * 1e10) / 1e18
= 1e16 / 1e18
= 0 ← truncated
fee = (0 * 3e15) / 1e18
= 0
Borrower pays zero fee on a 1e6-token flash loan.

Foundry test:

function testGetCalculatedFeeReturnsZero() public {
// Deploy a mock token with TSwap price 1e10
MockLowPriceToken lowToken = new MockLowPriceToken();
thunderLoan.setAllowedToken(lowToken, true);
mockTswap.setPrice(address(lowToken), 1e10);
uint256 fee = thunderLoan.getCalculatedFee(lowToken, 1e6);
assertEq(fee, 0, "Fee should not be zero for a non-zero loan");
}

Recommended Mitigation

Add a minimum fee floor so that any non-zero loan always incurs at least 1 wei in fees:

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;
+ if (fee == 0 && amount > 0) {
+ fee = 1; // minimum fee floor
+ }
}
Updates

Lead Judging Commences

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

[L-01] getCalculatedFee can be 0

## Description getCalculatedFee can be as low as 0 ## Vulnerability Details Any value up to 333 for "amount" can result in 0 fee based on calculation ``` function testFuzzGetCalculatedFee() public { AssetToken asset = thunderLoan.getAssetFromToken(tokenA); uint256 calculatedFee = thunderLoan.getCalculatedFee( tokenA, 333 ); assertEq(calculatedFee ,0); console.log(calculatedFee); } ``` ## Impact Low as this amount is really small ## Recommendations A minimum fee can be used to offset the calculation, though it is not that important.

Support

FAQs

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

Give us feedback!