Thunder Loan

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

Exchange Rate Division Truncation Burns Small Fees and Underpays Depositors

Exchange Rate Division Truncation Burns Small Fees and Underpays Depositors

Description

  • In AssetToken::updateExchangeRate(), accrued fees from flash loans are meant to increase the exchange rate so that liquidity providers earn yield proportional to pool activity.

  • The formula s_exchangeRate = s_exchangeRate + (fee * s_exchangeRatePrecision) / totalSupply() suffers from integer division truncation in Solidity. When (fee * s_exchangeRatePrecision) < totalSupply(), the fee increment truncates to zero, effectively burning the fee without updating the exchange rate.

// AssetToken.sol: integer division truncates small fees to zero
function updateExchangeRate(uint256 fee) external onlyThunderLoan {
@> uint256 newExchangeRate = s_exchangeRate + (fee * s_exchangeRatePrecision) / totalSupply();
if (newExchangeRate <= s_exchangeRate) {
revert AssetToken__ExhangeRateCanOnlyIncrease(s_exchangeRate, newExchangeRate);
}
s_exchangeRate = newExchangeRate;
}

Risk

Likelihood:

  • Occurs whenever flash loan fees generated by a transaction are small relative to the total share supply.

  • Occurs with high frequency in deep liquidity pools where totalSupply() is large.

Impact:

  • Permanent loss of yield for liquidity providers due to fee burn.

  • Flash loans revert unexpectedly when newExchangeRate <= s_exchangeRate, causing Denial of Service.

Proof of Concept

The exploit operates through the following steps:

  1. Pool has a large liquidity supply (e.g., 10,000e18 shares).

  2. A borrower takes a flash loan that generates a small fee (e.g., 1 wei).

  3. (1 * 1e18) / 10,000e18 evaluates to 0 due to integer division truncation.

  4. newExchangeRate <= s_exchangeRate triggers, reverting the flash loan and burning the fee.

function test_exchangeRateTruncationRevertsSmallFees() public setAllowedToken {
tokenA.mint(user, 10000e18);
vm.startPrank(user);
tokenA.approve(address(thunderLoan), 10000e18);
thunderLoan.deposit(tokenA, 10000e18);
vm.stopPrank();
AssetToken assetToken = thunderLoan.getAssetFromToken(tokenA);
vm.prank(address(thunderLoan));
vm.expectRevert();
assetToken.updateExchangeRate(1); // 1 wei fee causes unexpected revert
}

Recommended Mitigation

Track accrued fees in an accumulator variable or scale s_exchangeRatePrecision to 1e27 ray precision to prevent precision loss.

- uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;
+ uint256 private constant EXCHANGE_RATE_PRECISION = 1e27;
function updateExchangeRate(uint256 fee) external onlyThunderLoan {
+ if (fee == 0) return;
uint256 newExchangeRate = s_exchangeRate + (fee * s_exchangeRatePrecision) / totalSupply();
- if (newExchangeRate <= s_exchangeRate) {
- revert AssetToken__ExhangeRateCanOnlyIncrease(s_exchangeRate, newExchangeRate);
- }
+ if (newExchangeRate > s_exchangeRate) {
+ s_exchangeRate = newExchangeRate;
+ }
}
Updates

Lead Judging Commences

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