test_fee_compounding_creates_insolvency_after_repeated_flashloans() passes and shows requiredUnderlying > actualUnderlying, followed by a redeem revert.
pragma solidity 0.8.20;
import { BaseTest } from "../unit/BaseTest.t.sol";
import { ThunderLoanUpgraded } from "../../src/upgradedProtocol/ThunderLoanUpgraded.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { MockFlashLoanReceiver } from "../mocks/MockFlashLoanReceiver.sol";
contract F004_FlashloanFeeCompoundingInsolvency is BaseTest {
address internal liquidityProvider = address(123);
address internal user = address(456);
uint256 internal constant LIQUIDITY = 1_000e18;
uint256 internal constant BORROW_AMOUNT = 100e18;
MockFlashLoanReceiver internal receiver;
function setUp() public override {
super.setUp();
ThunderLoanUpgraded newImplementation = new ThunderLoanUpgraded();
vm.prank(thunderLoan.owner());
thunderLoan.upgradeTo(address(newImplementation));
ThunderLoanUpgraded upgraded = ThunderLoanUpgraded(address(thunderLoan));
vm.prank(upgraded.owner());
upgraded.updateFlashLoanFee(3e15);
vm.prank(upgraded.owner());
upgraded.setAllowedToken(tokenA, true);
vm.startPrank(liquidityProvider);
tokenA.mint(liquidityProvider, LIQUIDITY);
tokenA.approve(address(upgraded), LIQUIDITY);
upgraded.deposit(tokenA, LIQUIDITY);
vm.stopPrank();
vm.prank(user);
receiver = new MockFlashLoanReceiver(address(upgraded));
}
function test_fee_compounding_creates_insolvency_after_repeated_flashloans() public {
ThunderLoanUpgraded upgraded = ThunderLoanUpgraded(address(thunderLoan));
uint256 fee = upgraded.getCalculatedFee(tokenA, BORROW_AMOUNT);
tokenA.mint(address(receiver), fee * 2);
vm.prank(user);
upgraded.flashloan(address(receiver), tokenA, BORROW_AMOUNT, "");
vm.prank(user);
upgraded.flashloan(address(receiver), tokenA, BORROW_AMOUNT, "");
AssetToken asset = upgraded.getAssetFromToken(tokenA);
uint256 actualUnderlying = tokenA.balanceOf(address(asset));
uint256 requiredUnderlying =
(asset.totalSupply() * asset.getExchangeRate()) / asset.EXCHANGE_RATE_PRECISION();
assertLt(actualUnderlying, requiredUnderlying, "exchange rate exceeds actual backing");
uint256 shares = asset.balanceOf(liquidityProvider);
vm.prank(liquidityProvider);
vm.expectRevert();
upgraded.redeem(tokenA, shares);
}
function test_fuzz_fee_compounding_creates_insolvency(uint96 rawAmount) public {
ThunderLoanUpgraded upgraded = ThunderLoanUpgraded(address(thunderLoan));
uint256 amount = bound(uint256(rawAmount), 1e18, BORROW_AMOUNT);
uint256 fee = upgraded.getCalculatedFee(tokenA, amount);
tokenA.mint(address(receiver), fee * 2);
vm.prank(user);
upgraded.flashloan(address(receiver), tokenA, amount, "");
vm.prank(user);
upgraded.flashloan(address(receiver), tokenA, amount, "");
AssetToken asset = upgraded.getAssetFromToken(tokenA);
uint256 actualUnderlying = tokenA.balanceOf(address(asset));
uint256 requiredUnderlying =
(asset.totalSupply() * asset.getExchangeRate()) / asset.EXCHANGE_RATE_PRECISION();
assertLt(actualUnderlying, requiredUnderlying);
}
}