Thunder Loan

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

# `getCalculatedFee` rounds down to zero for small borrows, giving free flash loans

getCalculatedFee rounds down to zero for small borrows, giving free flash loans

Severity: Low · Impact: Low · Likelihood: Medium

Description

  • Every flash loan should pay a non-zero fee proportional to the amount borrowed.

  • getCalculatedFee divides by s_feePrecision (1e18) twice, and Solidity integer division truncates. For small borrow amounts the intermediate products are less than 1e18, so the fee rounds down to 0.

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; // truncates to 0 when small
}

Risk

Likelihood:

  • Occurs for any borrow small enough that amount * price * s_flashLoanFee < 1e36 — with the default 0.3% fee and a 1e18 price, any borrow below 334 base units pays no fee. It is also hit routinely by low-decimal / low-value in-scope tokens.

Impact:

  • Borrowers obtain flash loans for free (and can split large borrows into many tiny ones to reduce total fees), eroding LP/protocol revenue. Low severity as it does not risk principal.

Proof of Concept

Save the block below as test/PocL2.t.sol and run forge test --mt test_L2_fee_rounds_to_zero -vv. Borrowing 333 base units costs a fee of 0.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import { Test, console } from "forge-std/Test.sol";
import { ThunderLoan } from "../src/protocol/ThunderLoan.sol";
import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { MockPoolFactory } from "./mocks/MockPoolFactory.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract PocL2 is Test {
function test_L2_fee_rounds_to_zero() public {
ThunderLoan impl = new ThunderLoan();
MockPoolFactory factory = new MockPoolFactory();
ERC20Mock token = new ERC20Mock();
factory.createPool(address(token));
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), "");
ThunderLoan tl = ThunderLoan(address(proxy));
tl.initialize(address(factory));
tl.setAllowedToken(IERC20(address(token)), true);
// price 1e18, fee 3e15 -> fee = amount * 0.003, truncated. Below 334 => 0.
uint256 fee = tl.getCalculatedFee(IERC20(address(token)), 333);
console.log("fee for borrowing 333:", fee);
assertEq(fee, 0); // free flash loan
}
}

Recommended Mitigation

Enforce a minimum non-zero fee (revert if the computed fee is 0), or restructure the calculation to divide only once so precision is not lost, e.g. fee = amount * price * s_flashLoanFee / (s_feePrecision * s_feePrecision) with a require(fee > 0) guard.

Updates

Lead Judging Commences

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