Back

SC08: Integer Overflow and Underflow - Overview

What is Integer Overflow and Underflow?

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 2n instead of being the mathematically correct number. On the EVM every integer is fixed-width—uint256 holds 0 to 2256−1, uint8 holds 0 to 255—and there is no arbitrary-precision fallback. When a computation steps past the edge of that range, the bits that don't fit are simply discarded, and the surviving value can be wildly wrong.

Two directions of the same defect:

Core Concept

Fixed-width wrap-around (modulo 2^n):

  uint8  range: 0 .. 255            uint256 range: 0 .. 2^256 - 1

  UNDERFLOW (subtract past zero):
    uint8:  0 - 1      -> 255           // not -1
    token:  balance(5) - amount(10)     -> 2^256 - 5   (huge)

  OVERFLOW (add/multiply past max):
    uint8:  255 + 1    -> 0             // wraps to zero
    supply: MAX_UINT + 1               -> 0
    mul:    a * b when a*b > 2^256 - 1  -> truncated

  DOWNCAST TRUNCATION (narrowing a wide value):
    uint256 x = 300;
    uint8   y = uint8(x);              // y == 44  (300 mod 256)

Correct arithmetic would revert or carry; wrapped arithmetic
keeps running with a value the invariants never anticipated.

Why It's Critical for Smart Contracts

The same wrap-around exists in C and countless other languages, but on a blockchain the stakes and the finality are different:

Why Does This Matter?

Business Impact

Technical Impact

Technical Context

Common Overflow / Underflow Scenarios

1. Balance Underflow (the classic)

// Pre-0.8 or inside unchecked: NO automatic check
function transfer(address to, uint256 amount) public {
    // If amount > balances[msg.sender], this subtraction underflows
    balances[msg.sender] -= amount;      // wraps to ~2^256
    balances[to] += amount;
}

Risk: A sender with balance 0 can send a huge amount; their balance wraps to a near-maximum value instead of reverting, minting tokens from nothing.

2. Supply / Cap Overflow

// A counter or total that wraps past the maximum
uint256 public totalSupply;

function mint(uint256 amount) public {
    // If totalSupply + amount overflows, the stored total wraps small,
    // defeating any later require(totalSupply <= CAP) check.
    totalSupply += amount;               // pre-0.8 / unchecked: wraps
}

Risk: An attacker overflows a total or counter so a cap check sees a small number and lets minting continue past the intended limit.

3. Downcast / Truncation

uint256 bigAmount = 4_294_967_296;       // 2^32
uint32  small = uint32(bigAmount);        // truncates to 0

// Bookkeeping in uint256 but stored/compared as a narrower type
mapping(address => uint64) public shares;
shares[user] = uint64(computedShares);    // high bits silently dropped

Risk: Narrowing a wide value drops the high bits, so a large amount can become a small (or zero) stored value, corrupting accounting.

4. Multiplication Before Division

// Order matters: multiply first can overflow; divide first loses precision
uint256 reward = (userStake * rewardPerToken) / PRECISION;
// If userStake * rewardPerToken exceeds 2^256 - 1, it wraps (pre-0.8 / unchecked)

Risk: An intermediate product overflows even though the final result would fit, producing a wildly wrong reward or price.

5. Unchecked Blocks and Inline Assembly

unchecked {
    counter += userInput;    // gas-optimised, but NO overflow protection
}

assembly {
    let z := add(x, y)       // Yul add() never checks for overflow
}

Risk: Modern code deliberately turns checks off for gas; if the operation can actually wrap, the vulnerability is reintroduced on an up-to-date compiler.

Where Arithmetic Risk Hides

LocationTypical MistakeConsequence
Token transfer/mintUnchecked balance - amount / supply + amountUnlimited minting, cap bypass
unchecked { } blockWrap possible on attacker-controlled inputSilent overflow on modern compiler
Type castsDowncast uint256 -> uint64/uint8Truncated amounts, IDs, timestamps
Reward / price mathMultiply before divide, large intermediatesWrong payouts, mispriced shares
Inline assembly (Yul)Raw add/mul/subNo checks at any compiler version
Legacy contracts (<0.8)No SafeMath around arithmeticEvery operation can wrap

The Crucial Version Nuance

Solidity ≥ 0.8.0 inserts overflow and underflow checks by default. Since 0.8.0, +, -, and * on integers automatically revert (with Panic(0x11)) when they would wrap. This changed the shape of the risk—it did not eliminate it.

What this means in practice, and why the bug class is still very much alive:

The modern lesson is not “overflow is solved.” It is: know which of your operations are actually unprotected—old compilers, unchecked blocks, casts, and assembly—and prove those specific operations cannot wrap.

Real-World Impact

Case Study 1: The ERC-20 batchTransfer Overflow Class (2018)

Vulnerability:

Impact:

Root Cause: An unchecked multiplication feeding a balance check on a pre-0.8 compiler without SafeMath.

Case Study 2: The proxyOverflow ERC-20 Class (2018)

Vulnerability:

Impact:

Root Cause: The same family of defect—fixed-width arithmetic wrapping in token bookkeeping compiled without overflow protection.

Case Study 3: Underflow-Based Balance Exploits

Vulnerability:

Impact:

Root Cause: Unchecked subtraction on an unsigned integer, allowing an underflow where a revert was expected.

Note: these are described as classes of incident. The durable lesson is the pattern—unchecked fixed-width arithmetic in value-bearing bookkeeping—not any single token's exact numbers.

Prevalence and Significance

Integer overflow and underflow is a foundational smart-contract vulnerability: it was among the most exploited bug classes in the 2016–2018 era and is the reason SafeMath became ubiquitous and, ultimately, why checked arithmetic was baked into the Solidity language at 0.8.0.

The defensible picture today:

Common Misunderstandings

Myth 1: "Solidity 0.8 fixed integer overflow, so I don't need to think about it"

Reality: 0.8 protects default +/-/*. It does not protect unchecked blocks, type casts, or inline assembly, and it does nothing for the huge base of pre-0.8 contracts. You still have to audit every unprotected operation.

Myth 2: "A require(balance >= amount) before the subtraction makes me safe"

Reality: A correct guard can prevent underflow, but only if it truly runs first and covers every path. Historically many contracts either omitted it or had a computation that wrapped before the check was meaningful. Relying on checked arithmetic (0.8) or SafeMath is more robust than hand-written guards.

Myth 3: "unchecked is just a gas optimisation, it's harmless"

Reality: unchecked re-enables wrap-around. It is safe only when you have proven the operation cannot overflow (for example a loop counter bounded by array length). On attacker-influenced values it reintroduces the classic bug on a modern compiler.

Myth 4: "Casting between integer types is safe"

Reality: Narrowing casts (uint256 -> uint64) silently truncate in every version. uint8(300) is 44, not a revert. Validate ranges before downcasting or use a safe-cast helper.

Myth 5: "Overflow only matters for addition"

Reality: Subtraction underflows, multiplication overflows (often via multiply-before-divide), and downcasts truncate. Division and modulo have their own edge cases (division by zero reverts; be careful with rounding). The whole arithmetic surface needs attention.

Myth 6: "If it compiles and the tests pass, the math is fine"

Reality: Overflow bugs live at the extreme edges of the input range that ordinary unit tests never reach. You need boundary tests and fuzzing (Foundry/Echidna) that push values to type(uint256).max and zero to surface them.

How Overflow/Underflow Differs from Related Issues

AspectInteger Overflow/Underflow (SC08)Lack of Input Validation (SC04)Logic Errors (SC03)
Root causeFixed-width arithmetic wraps modulo 2^nUntrusted input used without bounds/checksCorrect types, wrong business rule
Where it livesAny add/sub/mul, cast, or assemblyFunction entry points and parametersControl flow and accounting design
Typical fixSolidity ≥0.8 / SafeMath, audit unchecked, safe castsValidate and bound inputsRework the invariant/flow
DetectionBoundary tests, fuzzing invariantsInput fuzzing, reviewSpec review, property testing

Key Takeaways

  1. Integers are fixed-width—past the edge they wrap modulo 2n rather than growing, and a wrapped balance or supply is real, spendable value.
  2. 0.8 changed the default, not the whole story—checked arithmetic protects plain +/-/*, but not unchecked, casts, or assembly.
  3. The residual risk is specific—legacy contracts, unchecked blocks, downcasts, and inline Yul are where wraps still happen.
  4. Underflow mints, overflow bypasses—subtracting past zero creates value; overflowing a total defeats a cap.
  5. Prove it can't wrap—for every unprotected operation, bound the inputs and fuzz the extremes before you trust it.

How to Identify if You're Vulnerable

Ask these questions about your contract:

If you answered "no" or "not sure" to several of these, your contract may harbour an exploitable overflow or underflow.

Next Steps