Thunder Loan

AI First Flight #7
Beginner FriendlyFoundryDeFiOracle
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Flash-loan fees are quoted in WETH but collected in the borrowed token

Root + Impact

Description

A flash-loan fee paid in the borrowed asset should be calculated as a percentage of the borrowed asset amount. For example, at a 0.3% fee, borrowing 1 token requires repaying 1.003 tokens, independent of that token’s WETH price.

getCalculatedFee() first converts the borrowed amount into WETH value, then returns that WETH-denominated result as fee. flashloan() subsequently treats this value as an amount of the borrowed token and requires repayment in that token. Consequently, every asset whose price differs from exactly 1 WETH is charged an incorrect fee.

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;
}
function flashloan(address receiverAddress, IERC20 token, uint256 amount, bytes calldata params) external {
uint256 fee = getCalculatedFee(token, amount);
// ...
@> if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}
}

Risk

Likelihood:

  • Every flash loan of a supported token priced above or below 1 WETH uses an incorrectly denominated fee.

  • Assets such as stablecoins, WBTC, and other non-WETH assets naturally have prices and decimal conventions that differ from WETH.

Impact:

  • Borrowers of assets priced below 1 WETH pay less than the intended percentage fee, reducing or eliminating LP yield.

  • Borrowers of assets priced above 1 WETH are overcharged, potentially making flash loans unavailable; non-18-decimal assets can be charged by orders of magnitude incorrectly.

Proof of Concept

The following test demonstrates that a token worth 0.1 WETH is charged 0.0003 tokens for borrowing 1 token, instead of the intended 0.003 tokens.

Add this to [`test/unit/ThunderLoanTest.t.sol`](/home/jpn/code/defi/ai_auditing/2023-11-Thunder-Loan/test/unit/ThunderLoanTest.t.sol):

function testFlashLoanFeeUsesWethUnitsButCollectsTokenUnits() public {
// Set tokenA's oracle price to 0.1 WETH per token.
address manipulatedPool = address(0xBEEF);
vm.mockCall(
address(mockPoolFactory),
abi.encodeWithSignature("getPool(address)", address(tokenA)),
abi.encode(manipulatedPool)
);
vm.mockCall(
manipulatedPool,
abi.encodeWithSignature("getPriceOfOnePoolTokenInWeth()"),
abi.encode(0.1e18)
);
uint256 borrowedAmount = 1e18; // 1 token
uint256 actualFee = thunderLoan.getCalculatedFee(tokenA, borrowedAmount);
// Current implementation:
// 1 token * 0.1 WETH/token * 0.3% = 0.0003 WETH
// This raw number is later collected as tokenA.
assertEq(actualFee, 0.0003e18);
// A 0.3% fee charged in tokenA should be 0.003 tokenA.
uint256 expectedTokenDenominatedFee = (borrowedAmount * 3e15) / 1e18;
assertEq(expectedTokenDenominatedFee, 0.003e18);
// The borrower is undercharged by 10x.
assertEq(expectedTokenDenominatedFee / actualFee, 10);
}

For a token worth 2 WETH, the opposite occurs: the protocol collects 0.006 tokens for a 1 token loan, double the intended 0.3% fee.


Recommended Mitigation

Since repayment occurs in the borrowed token, calculate the fee directly from the borrowed token amount:

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

Remove the oracle from fee calculation. An oracle is only needed when fees are intentionally charged in a different asset, such as WETH; in that design, collect WETH separately rather than treating a WETH amount as an underlying-token amount.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!