Thunder Loan

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

Fee Calculation Uses 18-Decimal WETH Precision — Non-18-Decimal Token Pools Suffer Catastrophic Exchange Rate Inflation

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

# [H-#] Fee Calculation Uses 18-Decimal WETH Precision — Non-18-Decimal Token Pools Suffer Catastrophic Exchange Rate Inflation
## Summary
The `getCalculatedFee` function calculates operational flash loan fees with an absolute precision of 18 decimals based on its underlying WETH oracle configuration. When this 18-decimal fee is passed directly into `AssetToken.updateExchangeRate`, the contract interprets it as a raw token unit quantity. For asset pools using tokens with fewer than 18 decimals (such as USDC or USDT with 6 decimals), this raw value injection scales the exchange rate exponentially. This causes subsequent user deposits to mint zero shares and permanently ruins the asset pool.
## Vulnerability Details
In `ThunderLoan.sol`, the fee calculation triggers `OracleUpgradeable.getPriceInWeth`, returning an 18-decimal formatted valuation. The calculated fee is directly routed into the pool state:
```solidity
assetToken.updateExchangeRate(fee);
```
Inside `AssetToken.sol`, this parameter updates pool share values via the following formula:
```solidity
newExchangeRate = s_exchangeRate * (totalSupply() + fee) / totalSupply();
```
Because the `fee` variable is evaluated assuming an 18-decimal base scale, passing it to an asset pool tracking a 6-decimal token scales the calculation by a factor of $10^{12}$ ($10^{18-6}$). The math adds an astronomical scale value to the true `totalSupply()`. The resulting bloated exchange rate value drives the asset-to-share conversion factor so high that typical user deposit amounts completely round down to zero, locking out future depositors.
## Impact
**High.** Any newly launched asset pool built around tokens using anything other than 18 decimals suffers immediate, catastrophic exchange rate corruption on its very first flash loan. The pool becomes permanently insolvent, new shares cannot be minted, and current liquidity providers are blocked from executing safe token redemptions.
## Proof of Concept
Add the following test case to your suite to confirm the decimal-scaling issue:
```solidity
function test_PoC9_UnitMismatch_Non18Decimals() public {
// 1. Setup a 6-decimal token asset pool (similar to USDC)
ERC20Mock usdc = new ERC20Mock("USDC", "USDC", 6);
MockPoolFactory poolFactory = new MockPoolFactory();
poolFactory.createPool(address(usdc));
ThunderLoan tl = ThunderLoan(address(new ERC1967Proxy(address(new ThunderLoan()), "")));
tl.initialize(address(poolFactory));
tl.setAllowedToken(IERC20(address(usdc)), true);
AssetToken at = tl.getAssetFromToken(IERC20(address(usdc)));
// 2. LP deposits 1,000,000 USDC into the asset pool
usdc.mint(lp, 1_000_000e6);
vm.startPrank(lp);
usdc.approve(address(tl), 1_000_000e6);
tl.deposit(IERC20(address(usdc)), 1_000_000e6);
vm.stopPrank();
uint256 exchangeRateBefore = at.getExchangeRate();
// 3. Process a flash loan where the 18-decimal WETH fee is scaled incorrectly into AssetToken
FlashLoanReceiver receiver = new FlashLoanReceiver(address(tl), address(usdc));
usdc.mint(address(receiver), 3e17);
vm.startPrank(user);
tl.flashloan(address(receiver), IERC20(address(usdc)), 1000e6, "");
vm.stopPrank();
// 4. Assert that the exchange rate was corrupted by a massive scaling factor
uint256 exchangeRateAfter = at.getExchangeRate();
assertGt(exchangeRateAfter, exchangeRateBefore * 1000);
// 5. Secondary depositor attempts a normal entry but receives zero shares due to rounding
usdc.mint(lp2, 1_000_000e6);
vm.startPrank(lp2);
usdc.approve(address(tl), 1_000_000e6);
uint256 sharesMinted = tl.deposit(IERC20(address(usdc)), 1_000_000e6);
assertEq(sharesMinted, 0); // Confirmation that the pool is broken
vm.stopPrank();
}
```
## Tools Used
* Manual Code Review
## Recommendations
Normalize the computed fee amount downward based on the decimals of the underlying asset before passing it to `updateExchangeRate`:
```solidity
uint256 fee = getCalculatedFee(token, amount);
uint256 tokenDecimals = ERC20(address(token)).decimals();
// @audit-issue Scale fee from 18 decimals down to match the pool token's native precision
if (tokenDecimals < 18) {
fee = fee / (10 ** (18 - tokenDecimals));
}
assetToken.updateExchangeRate(fee);
```
// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Recommended Mitigation

- remove this code
+ add this code
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!