Thunder Loan

AI First Flight #7
Beginner FriendlyFoundryDeFiOracle
EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

Exchange rate update mixes units (fee in underlying vs totalSupply in asset units)

Root + Impact

Description

  • Normal behavior: Exchange rate should represent underlying per AssetToken, increasing only by actual underlying fees.

  • Issue: `updateExchangeRate adds fee (underlying units) to totalSupply()` (AssetToken units), inflating the exchange rate once it deviates from 1

function updateExchangeRate(uint256 fee) external {
...
@> uint256 newExchangeRate = s_exchangeRate * (totalSupply() + fee) / totalSupply();
...
}

Risk

Likelihood:

  • Triggers after any fee update once the exchange rate is no longer exactly 1.

  • Active flashloan usage causes repeated fee updates over time.

Impact:

  • Exchange rate drifts above actual backing, enabling over‑redemption.

  • Pool insolvency can accumulate over multiple fee updates.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
import { Test } from "forge-std/Test.sol";
import { ThunderLoan } from "../../src/protocol/ThunderLoan.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol";
import { MockPoolFactory } from "../../test/mocks/MockPoolFactory.sol";
import { MockFlashLoanReceiver } from "../../test/mocks/MockFlashLoanReceiver.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract ExchangeRateUnitMismatchPoC is Test {
ThunderLoan private thunderLoan;
MockPoolFactory private poolFactory;
ERC20Mock private token;
MockFlashLoanReceiver private receiver;
address private lp = address(0x3333);
function setUp() public {
ThunderLoan impl = new ThunderLoan();
poolFactory = new MockPoolFactory();
token = new ERC20Mock();
poolFactory.createPool(address(token));
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), "");
thunderLoan = ThunderLoan(address(proxy));
thunderLoan.initialize(address(poolFactory));
vm.prank(thunderLoan.owner());
thunderLoan.setAllowedToken(token, true);
receiver = new MockFlashLoanReceiver(address(thunderLoan));
}
function testExchangeRateDriftsAboveBackingAfterMultipleFees() public {
uint256 depositAmount = 100e18;
uint256 amountBorrow = 100e18;
token.mint(lp, depositAmount);
vm.startPrank(lp);
token.approve(address(thunderLoan), depositAmount);
thunderLoan.deposit(token, depositAmount);
vm.stopPrank();
token.mint(address(receiver), 10e18);
thunderLoan.flashloan(address(receiver), token, amountBorrow, "");
thunderLoan.flashloan(address(receiver), token, amountBorrow, "");
AssetToken asset = thunderLoan.getAssetFromToken(token);
uint256 expectedRate =
(token.balanceOf(address(asset)) * asset.EXCHANGE_RATE_PRECISION()) / asset.totalSupply();
uint256 actualRate = asset.getExchangeRate();
assertGt(actualRate, expectedRate);
}
}

Recommended Mitigation

- uint256 newExchangeRate = s_exchangeRate * (totalSupply() + fee) / totalSupply();
+ uint256 totalUnderlying = (s_exchangeRate * totalSupply()) / EXCHANGE_RATE_PRECISION;
+ uint256 newExchangeRate = ((totalUnderlying + fee) * EXCHANGE_RATE_PRECISION) / totalSupply();
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 18 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!