Thunder Loan

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

CEI violation — exchange rate updated before token transfer lets LP-borrowers redeem at inflated rate during callback

Root + Impact

Description

  • ThunderLoan increases the AssetToken exchange rate when a flash loan fee is collected, distributing the fee yield to LPs. This rate update should happen only after confirming repayment.

  • flashloan calls assetToken.updateExchangeRate(fee) before the token transfer and before the callback fires. An attacker who is both an LP and a flash loan borrower can call redeem() during the callback at the fee-inflated rate — extracting yield that has not yet been earned — then repay the flash loan at face value.

function flashloan(...) external ... {
uint256 fee = getCalculatedFee(token, amount);
@> assetToken.updateExchangeRate(fee); // EFFECT: rate bumped before any interaction
emit FlashLoan(receiverAddress, token, amount, fee, params);
s_currentlyFlashLoaning[token] = true;
@> assetToken.transferUnderlyingTo(receiverAddress, amount); // INTERACTION: transfer
@> receiverAddress.functionCall(...); // INTERACTION: callback
uint256 endingBalance = token.balanceOf(address(assetToken));
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}
// Rate was already updated — cannot be rolled back
}

Risk

Likelihood:

  • Any LP who also calls flashloan — a natural combination in a flash loan protocol — can exploit this during their callback.

  • Read-only reentrancy via the inflated exchange rate affects any external protocol that reads the AssetToken rate during the callback window.

Impact:

  • LPs who are also flash loan borrowers extract fee yield before it is earned, stealing from other LPs who have not yet accumulated their share.

  • External protocols reading the AssetToken exchange rate during a flash loan callback receive an incorrect inflated value, enabling a class of read-only reentrancy attacks.

Proof of Concept

Place this test in test/ and run forge test --match-test testLpBorrowerAttack. The test demonstrates that an LP holder can call redeem() inside a flash loan callback after the exchange rate is inflated by the fee, withdrawing more underlying tokens than they originally deposited.

contract LPBorrowerAttack is IFlashLoanReceiver {
ThunderLoan thunderLoan;
IERC20 token;
AssetToken assetToken;
function attack(uint256 amount) external {
// Precondition: attacker has deposited and holds AssetTokens
thunderLoan.flashloan(address(this), token, amount, "");
}
function executeOperation(
address _token, uint256 amount, uint256 fee,
address, bytes calldata
) external returns (bool) {
// Exchange rate already inflated by fee — redeem at inflated rate
// Receive more underlying tokens than originally deposited
thunderLoan.redeem(IERC20(_token), assetToken.balanceOf(address(this)));
// Repay flash loan at face value
IERC20(_token).approve(address(thunderLoan), amount + fee);
// ... repayment logic ...
return true;
}
}

Recommended Mitigation

Move updateExchangeRate(fee) to after the repayment balance check so the rate is only bumped once repayment is confirmed.

uint256 fee = getCalculatedFee(token, amount);
- assetToken.updateExchangeRate(fee);
emit FlashLoan(receiverAddress, token, amount, fee, params);
s_currentlyFlashLoaning[token] = true;
assetToken.transferUnderlyingTo(receiverAddress, amount);
receiverAddress.functionCall(...);
uint256 endingBalance = token.balanceOf(address(assetToken));
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}
+ assetToken.updateExchangeRate(fee);
s_currentlyFlashLoaning[token] = false;
Updates

Lead Judging Commences

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