Thunder Loan

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

Division by Zero Vulnerability in AssetToken Contract

Description

Normal behavior: The updateExchangeRate function calculates a new exchange rate based on collected fees and total token supply. This mechanism ensures asset token holders receive their proportional share of protocol fees through appreciation of their tokens.

The issue: The function divides by totalSupply() without checking if it's zero. When totalSupply() = 0, the division operation s_exchangeRate * (totalSupply() + fee) / totalSupply() will revert due to division by zero, permanently disabling the fee mechanism.

https://github.com/CodeHawks-Contests/ai-thunder-loan/blob/ef8516ae0c0b683dab7928eab1490ac2d9b09209/src/protocol/AssetToken.sol#L89

The vulnerable code is in the updateExchangeRate function:

function updateExchangeRate(uint256 fee) external onlyThunderLoan {
uint256 newExchangeRate = s_exchangeRate * (totalSupply() + fee) / totalSupply();

Risk

Likelihood:

  • This occurs when all asset tokens have been burned/redeemed, bringing total supply to zero

  • While uncommon in active protocols, it can happen during protocol shutdown, migration, or if an attacker deliberately manipulates supply

  • Requires coordinated action by all token holders or specific protocol conditions

Impact:

  • Permanent disablement of the updateExchangeRate function - core protocol functionality breaks

  • Protocol fees cannot be distributed, disrupting economic incentives

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {AssetToken} from "../src/AssetToken.sol";
contract MockUnderlying is ERC20 {
constructor() ERC20("Mock Token", "MTK") {
_mint(msg.sender, 1000 ether);
}
}
contract AssetTokenTest is Test {
AssetToken private assetToken;
MockUnderlying private underlying;
address private thunderLoan = address(0x123);
address private user = address(0x456);
function setUp() public {
underlying = new MockUnderlying();
assetToken = new AssetToken(
thunderLoan,
underlying,
"Asset Token",
"ATK"
);
}
function test_DivisionByZeroInUpdateExchangeRate() public {
vm.prank(thunderLoan);
assetToken.mint(thunderLoan, 100 ether);
assertEq(assetToken.totalSupply(), 100 ether, "Initial supply should be 100 ether");
vm.prank(thunderLoan);
assetToken.burn(thunderLoan, 100 ether);
assertEq(assetToken.totalSupply(), 0, "Supply should be 0 after burning all tokens");
vm.prank(thunderLoan);
vm.expectRevert();
assetToken.updateExchangeRate(1 ether);
}
function test_UpdateExchangeRateWithMinimalSupply() public {
vm.prank(thunderLoan);
assetToken.mint(thunderLoan, 1);
assertEq(assetToken.totalSupply(), 1, "Supply should be 1 wei");
vm.prank(thunderLoan);
assetToken.updateExchangeRate(1 ether);
uint256 newRate = assetToken.getExchangeRate();
assertGt(newRate, 1e18, "Exchange rate should increase");
}
function test_NormalUpdateExchangeRate() public {
vm.prank(thunderLoan);
assetToken.mint(thunderLoan, 100 ether);
vm.prank(thunderLoan);
assetToken.updateExchangeRate(10 ether);
uint256 newRate = assetToken.getExchangeRate();
assertEq(newRate, 1.1e18, "Exchange rate should be 1.1e18");
}
}
[FAIL. Reason: division by zero] test_DivisionByZeroInUpdateExchangeRate() (gas: [gas_used])

Recommended Mitigation

- uint256 newExchangeRate = s_exchangeRate * (totalSupply() + fee) / totalSupply();
+Add a zero-supply check at the beginning of the function:
function updateExchangeRate(uint256 fee) external onlyThunderLoan {
uint256 supply = totalSupply();
if (supply == 0) {
s_exchangeRate = s_exchangeRate * (fee + EXCHANGE_RATE_PRECISION) / EXCHANGE_RATE_PRECISION;
revert AssetToken__ZeroSupply();
}
uint256 newExchangeRate = s_exchangeRate * (supply + fee) / supply;
if (newExchangeRate <= s_exchangeRate) {
revert AssetToken__ExhangeRateCanOnlyIncrease(s_exchangeRate, newExchangeRate);
}
s_exchangeRate = newExchangeRate;
emit ExchangeRateUpdated(s_exchangeRate);
}
Updates

Lead Judging Commences

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