Without external donated funds, the protocol's assetToken.totalSupply * assetToken.exchangeRate will always be less than the underlying token balance in assetToken. Users will not be able to completely destroy assetToken to withdraw funds.
In the protocol, the number of assetToken multiplied by assetToken.exchangeRate indicates the amount of funds that the user can withdraw.
In the deposit function, fees are calculated for the funds deposited by the user, which are then used to increase the exchange rate. However, deposited funds will be fully converted into assetTokens at the current exchange rate, and no additional funds will be added to the protocol to match exchange rate increases. When users subsequently attempt to redeem, the increase in the exchange rate means they are unable to completely destroy their asset tokens to withdraw their funds.
The root cause of the issue is that users depositing funds into the protocol should not affect assetToken.exchangeRate.
pragma solidity 0.8.20;
import { Test, console } from "forge-std/Test.sol";
import { BaseTest, ThunderLoan } from "./BaseTest.t.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ThunderLoanUserLossTest is BaseTest {
uint256 constant AMOUNT = 10e18;
uint256 constant DEPOSIT_AMOUNT = AMOUNT * 1000;
address liquidityProvider = address(123);
address user = address(456);
function setUp() public override {
super.setUp();
vm.prank(user);
}
modifier setAllowedToken() {
vm.prank(thunderLoan.owner());
thunderLoan.setAllowedToken(tokenA, true);
_;
}
modifier hasDeposits() {
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, DEPOSIT_AMOUNT);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
vm.stopPrank();
_;
}
function testExploitUserLoss() public setAllowedToken hasDeposits {
vm.startPrank(liquidityProvider);
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
uint256 redeemAmount = asset.balanceOf(address(liquidityProvider));
vm.expectRevert("ERC20: transfer amount exceeds balance");
thunderLoan.redeem(tokenA, redeemAmount);
vm.stopPrank();
uint256 exchangeRate = asset.getExchangeRate();
uint256 redeemRequireAmount = asset.totalSupply() * exchangeRate / 1e18;
uint256 redeemAbleAmount = tokenA.balanceOf(address(asset));
emit log_named_decimal_uint("Difference between redeemAbleAmount and redeemRequireAmount", redeemRequireAmount - redeemAbleAmount, tokenA.decimals());
}
}
Users will not be able to completely burn assetToken to withdraw funds.
Remove the assetToken.updateExchangeRate(calculatedFee) in function deposit().