Summary
The Liquidity Provider
isn't always able to redeem the maximum amount of deposited tokens (type(uint256).max
).
In the ThunderLoan::deposit()
function, after the IERC20 token is deposited, the ThunderLoan::updateExchangeRate()
adjusts based on the deposited token amount.
During the redemption process (ThunderLoan::redeem
), the token amount to be redeemed is determined using the adjusted exchangeRate
.
Consequently, the Liquidity Provider
is entitled to receive the deposited token amount multiplied by exchangeRate
.
If no flash loan has happen providing the required fee, or if the taken flashloan hasn't provided sufficient fees, the smart contract won't have enough tokens available for the Liquidity Provider
.
Vulnerability Details
function redeem(
IERC20 token,
uint256 amountOfAssetToken
)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
{
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 exchangeRate = assetToken.getExchangeRate();
@> if (amountOfAssetToken == type(uint256).max) {
@> amountOfAssetToken = assetToken.balanceOf(msg.sender);
}
@> uint256 amountUnderlying = (amountOfAssetToken * exchangeRate) / assetToken.EXCHANGE_RATE_PRECISION();
emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
assetToken.burn(msg.sender, amountOfAssetToken);
assetToken.transferUnderlyingTo(msg.sender, amountUnderlying);
}
Impact
function testCantRedeemMaxDepositedAmount() public setAllowedToken {
AssetToken assetToken = thunderLoan.getAssetFromToken(tokenA);
uint256 initilaExchangeRate = assetToken.getExchangeRate();
console.log("The initial exchangeRate is: %s", initilaExchangeRate);
tokenA.mint(liquidityProvider, DEPOSIT_AMOUNT);
vm.startPrank(liquidityProvider);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
uint256 calculatedFee = thunderLoan.getCalculatedFee(tokenA, DEPOSIT_AMOUNT);
console.log("The calculatedFee is: %s", calculatedFee);
uint256 newExchangeRate = assetToken.getExchangeRate();
console.log("The newExchangeRate after deposit is: %s", newExchangeRate);
uint256 initialBalance = tokenA.balanceOf(liquidityProvider);
uint256 maxAmountToRedeem = assetToken.balanceOf(liquidityProvider);
uint256 expectedBalance =
initialBalance + (maxAmountToRedeem * newExchangeRate) / assetToken.EXCHANGE_RATE_PRECISION();
console.log("The Liquidity Provided expectedBalance is: %s", expectedBalance);
uint256 assetTokenBalance = tokenA.balanceOf(address(assetToken));
console.log("The balance of the tokenA on the contract is: %s", assetTokenBalance);
assetToken.approve(address(thunderLoan), maxAmountToRedeem);
vm.expectRevert();
thunderLoan.redeem(tokenA, maxAmountToRedeem);
vm.stopPrank();
}
Running 1 test for test/unit/ThunderLoanTest.t.sol:ThunderLoanTest
[PASS] testCantRedeemMaxDepositedAmount() (gas: 1205905)
Logs:
The Liquidity Provider expectedBalance is: 1003000000000000000000
The balance of the tokenA in the contract is: 1000000000000000000000
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.74ms
Tools Used
Manual review
Recommendations
Delete the updateExchangeRate()
in the deposit()
function and leave it only in the flashloan()
function.
function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) {
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 exchangeRate = assetToken.getExchangeRate();
uint256 mintAmount = (amount * assetToken.EXCHANGE_RATE_PRECISION()) / exchangeRate;
emit Deposit(msg.sender, token, amount);
assetToken.mint(msg.sender, mintAmount);
- uint256 calculatedFee = getCalculatedFee(token, amount);
- assetToken.updateExchangeRate(calculatedFee);
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}