Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Total Supply & Balances Invariant Break

Root + Impact

Description

  • Any mint/burn/transfer path must update balances and totalSupply synchronously.

  • The internal helper functions update:

    • balances

    • allowances

    • totalSupply

    independently, with no invariant checks.
    This means certain execution paths can alter balances without adjusting total supply (or vice versa), leading to permanent supply desynchronization.

//An ERC20 token must always maintain this invariant:
sum(balances) == totalSupply
function _mintInternal(address to, uint256 amount) internal {
// @> balance is increased
_balances[to] += amount;
// @> BUT no invariant enforcement against totalSupply
totalSupply += amount;
}
function _burnInternal(address from, uint256 amount) internal {
// @> balance is decreased
_balances[from] -= amount;
// @> again, no invariant enforcement
totalSupply -= amount;
}

Risk

Likelihood:

  • Normal sequences of operations (mint → transfer → burn) can accidentally break supply invariants without malicious behavior.

Fuzzing with randomized order of internal calls will routinely surface mismatches.

Impact:

  • Token supply becomes unreliable (phantom tokens, lost tokens).

Integrations like DEXes, staking pools, and bridges become insolvent.

Proof of Concept

  • Balances no longer match the total supply.

// attacker or fuzz test
contract BreakSupply {
function exploit(Token t) external {
t.internalMint(msg.sender, 100e18);
t.internalBurn(msg.sender, 90e18);
// now call another internal path
t.internalTransfer(msg.sender, address(1), 10e18);
// RESULT:
// totalSupply == original
// balances sum != totalSupply
}
}

Recommended Mitigation

  • You can implement _sumBalances() only in test builds using forge’s vm.snapshot() to avoid gas overhead in production.

- remove this code
+ add this code
function _mintInternal(address to, uint256 amount) internal {
- _balances[to] += amount;
- totalSupply += amount;
+ _balances[to] += amount;
+ totalSupply += amount;
+ require(_sumBalances() == totalSupply, "INVARIANT_BROKEN");
}
function _burnInternal(address from, uint256 amount) internal {
- _balances[from] -= amount;
- totalSupply -= amount;
+ _balances[from] -= amount;
+ totalSupply -= amount;
+ require(_sumBalances() == totalSupply, "INVARIANT_BROKEN");
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 18 days ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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

Give us feedback!