Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: high
Valid

Unchecked = in _burn allows creating infinite tokens

Root + Impact

Description

  • The _burn function in ERC20Internals.sol uses assembly blocks to efficiently update token balances and total supply through direct storage operations.

  • Inside the assembly block, the sub instruction is used without explicit validation, allowing arithmetic underflow when burning more tokens than available. In Solidity 0.8+, arithmetic operations are checked by default, but this safety feature is disabled inside assembly blocks, creating a critical vulnerability.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
let supply := sload(supplySlot)
sstore(supplySlot, sub(supply, value)) @> // @audit-issue : NO UNDERFLOW CHECK
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
sstore(accountBalanceSlot, sub(accountBalance, value)) @> // @audit-issue : NO UNDERFLOW CHECK
}
}

Risk

Likelihood:

  • Any user with access to burn functionality (public or through exposed internal functions) can trigger this vulnerability

  • The underflow condition is deterministic and guaranteed when value > balance

Impact:

  • Infinite token creation - Attackers can receive approximately 2^256 tokens by burning more than their balance

  • Total supply manipulation - The token economics can be completely destroyed, rendering the token worthless

Proof of Concept

// Attack scenario demonstrating the vulnerability
function testUnderflowAttack() public {
address attacker = makeAddr("attacker");
// Mint initial tokens to attacker
_mint(attacker, 100 ether);
uint256 initialBalance = balanceOf(attacker);
uint256 initialSupply = totalSupply();
// Attack: Burn more than balance
uint256 burnAmount = initialBalance + 1 ether;
// Before fix: This would succeed and give attacker near-infinite tokens
_burn(attacker, burnAmount);
// Result:
// balanceOf(attacker) = 2^256 - 1 (approx. 1.15e77 tokens)
// totalSupply() = 2^256 - 100 (underflowed)
}

Recommended Mitigation

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
let supply := sload(supplySlot)
+ if lt(supply, value) {
+ mstore(0x00, 0x4e487b71) // Panic(uint256)
+ mstore(add(0x00, 0x04), 0x11) // Arithmetic underflow
+ revert(0x00, 0x24)
+ }
sstore(supplySlot, sub(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
+ if lt(accountBalance, value) {
+ mstore(0x00, shl(224, 0xe450d38c)) // ERC20InsufficientBalance
+ mstore(add(0x00, 4), account)
+ mstore(add(0x00, 0x24), accountBalance)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

overflow & underflow

missing checks for overflow and underflow.

Support

FAQs

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

Give us feedback!