Thunder Loan

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

Reentrancy in `flashloan()` Allows Borrowers to Bypass Repayment Validation

Reentrancy in flashloan() Allows Borrowers to Bypass Repayment Validation

Summary

The flashloan() function is vulnerable to reentrancy because it performs an external call to an attacker-controlled contract before validating that the flash loan has been repaid.

An attacker can reenter flashloan() from within executeOperation() and manipulate the repayment accounting logic so that only the nested flash loan repayment is validated, while the initial flash loan remains unpaid.

This can result in direct loss of funds from the protocol.


Vulnerability Details

The vulnerability originates from the following external call:

receiverAddress.functionCall(
abi.encodeWithSignature(
"executeOperation(address,uint256,uint256,address,bytes)",
address(token),
amount,
fee,
msg.sender,
params
)
);

Before this external interaction, the protocol transfers the borrowed funds to the receiver:

assetToken.transferUnderlyingTo(receiverAddress, amount);

Because receiverAddress is fully attacker-controlled, the attacker can reenter flashloan() during the execution of executeOperation().

The issue is that repayment validation only occurs after the external call returns:

uint256 endingBalance = token.balanceOf(address(assetToken));
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}

During reentrancy, a second flash loan invocation creates a new startingBalance snapshot and performs its own repayment validation independently.

This allows the attacker to repay only the nested flash loan while leaving the original borrowed amount unpaid.

Since the outer call relies on balance-based accounting after control flow has already been manipulated, the repayment check can incorrectly succeed.


Impact

An attacker can exploit this vulnerability to steal funds from the protocol by bypassing repayment requirements for the initial flash loan.

Because flash loans can access the full available liquidity of the pool, this issue can lead to significant or total loss of funds.


Proof of Concept

Attack Flow

  1. Attacker calls flashloan()

  2. Protocol transfers funds to attacker contract

  3. Protocol calls executeOperation()

  4. Inside executeOperation(), attacker reenters flashloan()

  5. Nested flash loan completes successfully

  6. Attacker repays only the nested flash loan

  7. Original flash loan repayment validation is bypassed

Example Attacker Contract

contract Attacker {
ThunderLoan protocol;
IERC20 token;
bool reentered;
constructor(address _protocol, address _token) {
protocol = ThunderLoan(_protocol);
token = IERC20(_token);
}
function attack(uint256 amount) external {
protocol.flashloan(address(this), token, amount, "");
}
function executeOperation(
address,
uint256 amount,
uint256 fee,
address,
bytes calldata
) external {
if (!reentered) {
reentered = true;
// Reenter flashloan
protocol.flashloan(address(this), token, amount, "");
}
// Repay only the nested flashloan
token.transfer(address(protocol), amount + fee);
}
}

Recommended Mitigation

Add a nonReentrant modifier to the flashloan() function.

function flashloan(...) external nonReentrant {
...
}
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!