Thunder Loan

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

[H-01] Flash loan reentrancy via stale balance check

Root + Impact

Description

  • flashloan() reads startingBalance at line 182 before making three external calls (updateExchangeRate, transferUnderlyingTo, executeOperation). The repayment check at line 213 compares endingBalance against this stale startingBalance.

  • Additionally, the reentrancy guard s_currentlyFlashLoaning[token] = true is set at line 198 — after the first external call at line 194 — violating the Checks-Effects-Interactions (CEI) pattern.

  • During the executeOperation callback (line 201), a malicious receiver can call deposit() on the same token. This increases the pool's token balance, making endingBalance >= startingBalance + fee pass — even though the actual loan principal was never repaid.

// flashloan() execution order (ThunderLoan.sol:180-217)
182: startingBalance = token.balanceOf(assetToken) // snapshot
194: assetToken.updateExchangeRate(fee) // external call #1
198: s_currentlyFlashLoaning[token] = true // guard SET TOO LATE
199: assetToken.transferUnderlyingTo(receiver, amt) // external call #2
201: receiver.executeOperation(...) // external call #3
// ↑ attacker calls deposit() here — balance increases
212: endingBalance = token.balanceOf(assetToken)
213: endingBalance < startingBalance + fee // passes with stale startingBalance
216: s_currentlyFlashLoaning[token] = false

Risk

Likelihood:

  • The attack requires no privileged access; any address that deploys a contract implementing IFlashLoanReceiver can execute it. The vulnerable path is triggered on every flashloan() call.

Impact:

  • A malicious borrower can take a flash loan and never repay the principal, draining the entire token balance of the pool. All LP funds for that token are at risk.

Proof of Concept

contract AttackContract is IFlashLoanReceiver {
ThunderLoan thunderLoan;
IERC20 token;
function executeOperation(
address _token, uint256 amount, uint256 fee,
address, bytes calldata
) external returns (bool) {
// Instead of repaying the loan, deposit the borrowed amount back
// This satisfies the balance check without repaying
IERC20(_token).approve(address(thunderLoan), amount + fee);
thunderLoan.deposit(IERC20(_token), amount + fee);
return true;
}
}

Recommended Mitigation

  • Follow the CEI strictly set s_currentlyFlashLoaning[token] = true before any external call.

  • Move updateExchangeRate(fee) to after the repayment check, so the balance snapshot is taken immediately before the check.

  • Consider using OpenZeppelin ReentrancyGuard (nonReentrant) on flashloan(), deposit(), and redeem() to prevent cross-function reentrancy.

Updates

Lead Judging Commences

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