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

Poor validation in `ThunderLoan::flashloan` allows malicious users to reenter `ThunderLoan::deposit` and never repay loan

Summary

This vulnerability allows malicious actors to execute a reentrancy attack, which could result in the theft of underlying tokens. The impact of this issue is assessed as high, as it poses a significant risk to the protocol's funds.

Vulnerability Details

The vulnerability pertains to the interaction between the flashloan() and deposit() functions within ThunderLoan.sol. It was observed that in cases where the assetToken has a supply greater than zero, an attacker can exploit the following sequence of actions:

  • Initiate a flashloan() to obtain underlying tokens from the assetToken.

  • Instead of returning these tokens using the repay() function, execute a deposit(). This action includes depositing the borrowed tokens along with the associated fee.

  • Subsequently, initiate a withdrawal of underlying tokens by providing the assetTokens generated during the deposit process.

As a result of this sequence, the attacker gains access to the protocol's tokens. The key issue is that the flashloan() function solely checks the assetToken balance, which does not account for the complex interaction between deposits and withdrawals.

Proof of Concept

function testFlashLoanAttackIncorrectValidation() public {
/**
* 1. Get some WETH to pay FL fees
* 2. Send that to BadFlashLoanReceiver
* 3. Execute FL from receiver
* 4. FL amount is deposited into thunderLoan (to bypass repay check)
* 5. FL finished, we never repay, as thunderLoan only checks token balance in assetToken contract
* 6. Redeem from thunderLoan, get WETH back
* 7. Profit
*/
// Before
thunderLoan.setAllowedToken(weth, true);
_deposit(address(weth), ALICE, DEPOSIT_AMOUNT);
_deposit(address(weth), BOB, DEPOSIT_AMOUNT);
// Start with the fee amount based on FL 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);
// When
receiver.hack(address(weth), DEPOSIT_AMOUNT);
// Checks
uint256 endingBalance = weth.balanceOf(address(this));
assertGt(endingBalance, startingBalance);
assertGt(endingBalance, DEPOSIT_AMOUNT);
console.log("Profit amount: %s", endingBalance - startingBalance);
}

Impact

The impact of this vulnerability is rated as high. The protocol's funds are at significant risk, as malicious actors can execute the described reentrancy attack at any time. Such an attack could lead to the theft of underlying tokens and, consequently, financial losses for the protocol.

Tools Used

Manual Review, Foundry Testing

Recommendations

To address this critical issue and mitigate the risk of reentrancy attacks, the following recommendation is made:

Consolidate the Logic: Merge the logic of the repay() function into the flashloan() function. This approach ensures that the repayment is guaranteed to occur at the end of the flashloan() transaction. This consolidation prevents attackers from bypassing the repayment phase by directly depositing funds.

By implementing this recommendation, the security of the protocol will be significantly enhanced, and the risk of reentrancy attacks will be mitigated.

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.