Back to Cheat Sheets

🔢 Integer Overflow & Underflow

OWASP Smart Contract Top 10 (2025) - SC08

HIGH RISK

📋 What Is It?

Integer Overflow and Underflow occur when an arithmetic operation produces a result outside the range a fixed-size integer can hold, so the value wraps around modulo 2ⁿ instead of being the mathematically correct number. On the EVM every integer is fixed-width-uint256 holds 0 to 2²⁵⁶−1, uint8 holds 0 to 255-with no arbitrary-precision fallback. Underflow subtracts past zero (uint8(0) - 1 wraps to 255), so balance - amount can mint a near-infinite balance. Overflow adds or multiplies past the maximum (uint8(255) + 1 wraps to 0), so a supply counter or cap check can silently reset and let an attacker slip past a limit.

SC08 OWASP Rank
Wrap Failure Mode
0.8+ Checked by Default

⚠️ Common Exploits

  • Balance underflow: balance - amount where amount > balance wraps to ~2²⁵⁶ - mint from nothing
  • Multiplication overflow: count * amount wraps a batch total small so a balance check passes (batchOverflow class)
  • Supply / cap overflow: overflowing total slips under a require(total <= CAP) check
  • Downcast truncation: uint64(amount) silently drops high bits - never checked, any version
  • unchecked block: gas optimisation re-enables wrapping on attacker-controlled input
  • Inline assembly: Yul add/mul/sub never check for overflow at any version
  • Multiply-before-divide: intermediate product wraps (pre-0.8) or reverts as a DoS (0.8+)

🔴 Attack Flow

1. Locate unprotected arithmetic (pre-0.8, unchecked, cast, assembly)
↓
2. Model the wrap - choose an input past 0 or past 2ⁿ−1
↓
3. Call the function with the boundary value in one transaction
↓
4. Balance/total wraps; invariant the code trusted is now false
↓
5. IMPACT: Unlimited minting, cap bypass, pool drained!

❌ Vulnerable Code

// Bad: pre-0.8, no SafeMath - subtraction underflows and wraps up pragma solidity ^0.6.0; // arithmetic wraps silently function transfer(address to, uint256 amount) external { balances[msg.sender] -= amount; ← 0 - 1 => 2^256 - 1 (mint from nothing) balances[to] += amount; } // Bad: multiplication overflow in a batch total (batchOverflow class) function batchTransfer(address[] memory to, uint256 amount) public { uint256 total = to.length * amount; ← overflows for a crafted amount require(balances[msg.sender] >= total); ← passes: total wrapped small for (uint i = 0; i < to.length; i++) { balances[to[i]] += amount; ← each gets the full huge amount } } // Bad: supply overflow defeats the cap check function mint(uint256 amount) external { totalSupply += amount; ← wraps past 2^256 - 1 (pre-0.8) require(totalSupply <= CAP); ← wrapped-small total slips under cap balances[msg.sender] += amount; } // Bad: silent downcast truncation - NEVER checked, even on 0.8+ mapping(address => uint64) public shares; shares[msg.sender] += uint64(amount); ← uint64(2^64) == 0, high bits dropped // Bad: unchecked re-enables wrapping on attacker input unchecked { accrued[msg.sender] += userInput; ← 0.8 protection OFF here } // Bad: inline assembly never checks overflow at any version assembly { z := add(x, y) } ← wraps silently, no Panic(0x11)

✅ Secure Code

// Good: Solidity >= 0.8 - checked +, -, * revert with Panic(0x11) on wrap pragma solidity ^0.8.20; function transfer(address to, uint256 amount) external { balances[msg.sender] -= amount; ← underflow now reverts, never wraps balances[to] += amount; } // Good: legacy (<0.8) - OpenZeppelin SafeMath reverts on every op using SafeMath for uint256; balances[msg.sender] = balances[msg.sender].sub(amount); ← reverts on underflow balances[to] = balances[to].add(amount); ← reverts on overflow // Good: check the cap BEFORE mutating; checked add can't wrap the total function mint(uint256 amount) external { require(amount > 0, "zero amount"); require(totalSupply + amount <= CAP, "cap exceeded"); totalSupply += amount; balances[msg.sender] += amount; } // Good: SafeCast reverts instead of truncating on downcast using SafeCast for uint256; shares[msg.sender] += amount.toUint64(); ← reverts if amount > type(uint64).max // Good: unchecked ONLY for provably-bounded quantities for (uint256 i = 0; i < xs.length; ) { total += xs[i]; // checked: values untrusted unchecked { ++i; } ← safe: i bounded by xs.length } // Good: manual overflow check inside assembly; or prefer high-level x + y assembly { z := add(x, y) if lt(z, x) { revert(0, 0) } ← overflow iff sum < an operand } // Good: Math.mulDiv uses a 512-bit intermediate - no overflow, no precision loss return Math.mulDiv(stake, rate, PRECISION);

✓ Prevention Checklist

  • Compile with Solidity ≥ 0.8.0 so checked arithmetic is the default
  • Pin the pragma to a fixed, recent compiler version
  • Wrap every add/sub/mul in SafeMath for legacy <0.8 code
  • Treat every unchecked { } block as a proof obligation
  • Never unchecked raw attacker input - only bounded quantities
  • Range-check or use OpenZeppelin SafeCast before any downcast
  • Add manual overflow checks to any inline assembly arithmetic
  • Validate and bound inputs so values can't reach the extremes
  • Order operations to avoid intermediate overflow; use Math.mulDiv
  • Add boundary tests at 0 and type(uint256).max
  • Fuzz arithmetic invariants with Foundry / Echidna
  • Make "is this arithmetic protected?" a review line item

🔍 Detection & Tools

Analysis Tools:

Slither Mythril Echidna Foundry Manticore

Secure Libraries:

OpenZeppelin SafeMath OpenZeppelin SafeCast Math.mulDiv solc ^0.8

How to Test:

  • Run Slither to flag overflow-prone patterns, dangerous casts, and risky unchecked
  • Run Mythril / MythX symbolic execution for arithmetic edge cases
  • Fuzz invariants in Foundry: e.g. transfers never change total supply
  • Assert Echidna properties: sum of balances == total supply; no mint past cap
  • Add explicit boundary tests: adding to type(uint256).max must revert
  • Push inputs to 0 and the type maximum that unit tests never reach

🌍 Real-World Incident Classes

  • batchTransfer overflow class (2018): ERC-20 tokens computed amount * count and checked it against the sender's balance. On a pre-0.8 compiler with no SafeMath, a crafted amount overflowed the product to a tiny value, so the balance check passed while each recipient was credited the full enormous amount - minting far beyond supply. Widely referred to as the batchOverflow class; it affected multiple tokens and forced exchanges to suspend deposits and trading.
  • proxyOverflow ERC-20 class (2018): a related family exposed transfer paths where overflow in amount/allowance arithmetic let attackers credit balances the supply accounting never authorised - again prompting exchanges to halt trading of affected tokens.
  • Underflow-based balance exploits: a recurring pre-0.8 pattern where a transfer or burn subtracts an attacker-controlled amount without guaranteeing the balance is large enough. Subtracting past zero wraps the balance to a near-maximum value, effectively minting unlimited supply.

Note: these are described as classes of incident. The durable lesson is the mechanism - unchecked fixed-width arithmetic in value-bearing bookkeeping - not any single token's exact numbers. This is why SafeMath became ubiquitous and why checked arithmetic was baked into Solidity at 0.8.0.

📌 Quick Tips

  • DO NOT assume 0.8 protects casts, unchecked, or assembly
  • DO NOT unchecked raw attacker-controlled input
  • DO NOT downcast a wide value without a range check
  • DO compile with Solidity ≥ 0.8 (or SafeMath below it)
  • DO use SafeCast for narrowing conversions
  • DO check caps before mutating, and bound inputs
  • DO fuzz the extremes with Foundry / Echidna