Thunder Loan

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

Exchange Rate Updated Before Flash Loan Repayment is Confirmed

VULNERABILITY-04 — Exchange Rate Updated Before Flash Loan Repayment is Confirmed

Severity: Medium
File: src/protocol/ThunderLoan.sol

Summary

flashloan() calls assetToken.updateExchangeRate(fee) before the borrower's executeOperation runs and before repayment is verified. An attacker who is also an existing LP can redeem at the inflated exchange rate during the flash loan callback, extracting excess underlying.

Vulnerability Details

function flashloan(...) external {
...
uint256 fee = getCalculatedFee(token, amount);
assetToken.updateExchangeRate(fee); // ← rate inflated HERE (before repayment)
s_currentlyFlashLoaning[token] = true;
assetToken.transferUnderlyingTo(receiverAddress, amount);
receiverAddress.functionCall(...); // ← receiver runs with already-inflated rate
uint256 endingBalance = token.balanceOf(address(assetToken));
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(...);
}
s_currentlyFlashLoaning[token] = false;
}

Inside executeOperation, a flash loan receiver who also holds AssetTokens can call redeem() and receive underlying calculated at the inflated exchange rate. They then repay the flash loan normally. The result: they extracted the fee value from other LPs by front-running the exchange rate update.

PoC

function executeOperation(
address _token, uint256 amount, uint256 fee, address, bytes calldata
) external returns (bool) {
// Exchange rate is already inflated by fee
// Redeem existing LP position at the inflated rate
uint256 myAssets = assetToken.balanceOf(address(this));
if (myAssets > 0) {
thunderLoan.redeem(IERC20(_token), myAssets); // extra profit here
}
// Repay flash loan normally
IERC20(_token).approve(address(thunderLoan), amount + fee);
thunderLoan.repay(IERC20(_token), amount + fee);
return true;
}

Impact

  • Medium — An LP who is also a flash loan receiver extracts the fee profit before other LPs benefit.

  • The fee income is effectively stolen from the LP pool on each such call.

  • Individual impact is small per transaction but compounds over time.

Tools Used

  • Manual analysis

Recommendations

Move updateExchangeRate() to after repayment is confirmed:

function flashloan(...) external {
...
- uint256 fee = getCalculatedFee(token, amount);
- assetToken.updateExchangeRate(fee);
s_currentlyFlashLoaning[token] = true;
assetToken.transferUnderlyingTo(receiverAddress, amount);
receiverAddress.functionCall(...);
uint256 endingBalance = token.balanceOf(address(assetToken));
+ uint256 fee = getCalculatedFee(token, amount);
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}
+ assetToken.updateExchangeRate(fee); // Update AFTER confirming repayment
s_currentlyFlashLoaning[token] = false;
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 6 days 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!