Thunder Loan

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

`getCalculatedFee` uses divide-before-multiply, causing the fee to round to zero for small loan amounts and allowing free flash loans of tiny units

Root + Impact

Description

  • getCalculatedFee computes the fee in two steps: first valueOfBorrowedToken = (amount * price) / feePrecision, then fee = (valueOfBorrowedToken * flashLoanFee) / feePrecision. The first division loses precision before the second multiplication, so for inputs where amount * price < feePrecision, the intermediate valueOfBorrowedToken rounds to 0 and the final fee is 0.

  • A flash loaner can request a tiny amount (e.g. 1 wei of an 18-decimal token) and the fee will be 0. The pool transfers the loaner the tokens for free; the loaner only needs to return what was borrowed. While the absolute amount per call is microscopic, the issue indicates a precision flaw in the fee calculation that may also produce undercharge for non-trivial loans with low-priced tokens.

// src/protocol/ThunderLoan.sol :: getCalculatedFee
function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) {
@> uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / s_feePrecision;
@> fee = (valueOfBorrowedToken * s_flashLoanFee) / s_feePrecision;
}

Risk

Likelihood:

  • Any caller can request a flash loan of 1 wei and receive a zero fee.

  • The precision-loss arithmetic also undercharges for genuinely-low-value tokens or for borrowers who fragment their loans into small parallel calls.
    Impact:

  • Direct revenue loss for LPs on small loans.

  • The bug is bounded in size for each individual call (only large enough to consume gas), but reflects a systemic mis-ordering of arithmetic operations that should be fixed for correctness across the wider input domain.

Proof of Concept

function test_BUG5_smallAmountsGetFreeFlashLoanDueToFeeRounding() public view {
uint256 fee = thunderLoan.getCalculatedFee(IERC20(address(tokenA)), 1); // 1 wei
assertEq(fee, 0, "1-wei flash loan has zero fee");
}

Output: fee for 1 wei of token: 0. A flash loan of 1 wei costs nothing.

Recommended Mitigation

Reorder to multiply before dividing, eliminating the intermediate truncation:

function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) {
- uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / s_feePrecision;
- fee = (valueOfBorrowedToken * s_flashLoanFee) / s_feePrecision;
+ fee = (amount * getPriceInWeth(address(token)) * s_flashLoanFee) / (s_feePrecision * s_feePrecision);
}

Optionally, enforce a minimum fee of 1 wei to guarantee non-zero billing for all positive-amount loans:

+ if (fee == 0) fee = 1;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 16 hours 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!