reedem() function in ThunderLoan contrat miscalculate amount of deposit token to withdraw
Description
-
The redeem() function is intended to allow a user to burn their AssetTokens and receive the corresponding underlying ERC20 tokens.
-
reedem() function makes wrong calculation, leading to overestimate balance to withdraw, reverting the transferUnderling function() in AssetToken function
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();
@> assetToken.burn(msg.sender, amountOfAssetToken);
@> assetToken.transferUnderlyingTo(msg.sender, amountOfAssetToken);
emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
}
Risk
Likelihood:
Any time liquidity providers withdraw funds
Impact:
Users are unable to redeem their deposits correctly.
Proof of Concept
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/ThunderLoan.sol";
import "../src/AssetToken.sol";
contract RedeemPoC is Test {
ThunderLoan thunderLoan;
AssetToken assetToken;
IERC20 tokenA;
address liquidityProvider = address(0x1234);
uint256 DEPOSIT_AMOUNT = 1e18;
function setUp() public {
tokenA = new ERC20Mock("TokenA", "TKA", liquidityProvider, DEPOSIT_AMOUNT * 10);
assetToken = new AssetToken(tokenA, address(thunderLoan));
thunderLoan = new ThunderLoan();
thunderLoan.setAllowedToken(tokenA, true);
}
function testRedeemFailsWithOldLogic() public {
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, DEPOSIT_AMOUNT);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
vm.expectRevert("ERC20: transfer amount exceeds balance");
thunderLoan.redeem(tokenA, DEPOSIT_AMOUNT);
vm.stopPrank();
}
function testRedeemWorksAfterFix() public {
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, DEPOSIT_AMOUNT);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
thunderLoan.redeem(tokenA, DEPOSIT_AMOUNT);
uint256 balance = tokenA.balanceOf(liquidityProvider);
assertEq(balance, DEPOSIT_AMOUNT);
vm.stopPrank();
}
}
Recommended Mitigation
- remove this code
+ add this code
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);
}
// Correct underlying calculation using exchange rate
+ uint256 amountUnderlying = (amountOfAssetToken * assetToken.EXCHANGE_RATE_PRECISION()) / exchangeRate;
// Burn the user's AssetTokens
+ assetToken.burn(msg.sender, amountOfAssetToken);
// Transfer the correct underlying amount
+ assetToken.transferUnderlyingTo(msg.sender, amountUnderlying);
+ emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
}