Integer Overflow and Underflow in Smart Contracts
Integer overflow is one of the oldest classes of software vulnerability, and Ethereum smart contracts have lost tens of millions of dollars to it. The mechanics are simple: an unsigned integer that exceeds its maximum value wraps around to zero; one that drops below zero wraps to its maximum. In a financial context, that arithmetic quirk can let an attacker mint unlimited tokens, drain funds, or bypass access controls. This guide explains how overflow and underflow work in Solidity, how the language's built-in protections function, and where the risk still lives in modern code.
The Mechanics of Overflow and Underflow
Every Solidity integer type has a fixed bit width. A uint8 can represent values from 0 to 255. A uint256 can represent values from 0 to 2256 - 1. When an arithmetic operation produces a result outside that range, the EVM does not throw an error by default; it silently discards the high bits and returns whatever fits in the type.
- Overflow:
uint8 x = 255; x += 1;yieldsx == 0. - Underflow:
uint8 x = 0; x -= 1;yieldsx == 255.
In practice the most dangerous patterns appear in token balance logic, staking reward calculations, and time-lock comparisons. The infamous BatchOverflow bug (2018) exploited overflow in an ERC-20 token to generate an astronomically large balance from a near-zero transfer amount, effectively breaking the token's entire economy overnight.
A Classic Vulnerable Pattern
// Solidity <0.8 — VULNERABLE
pragma solidity ^0.6.0;
contract OldToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
// If balances[msg.sender] < amount, subtraction underflows
// wrapping to a huge number instead of reverting
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
An attacker with a zero balance could call transfer with any non-zero amount. The subtraction underflows, leaving balances[msg.sender] at 2^256 - amount. The recipient receives tokens as expected. No revert occurs.
SafeMath: The Pre-0.8 Solution
Before Solidity 0.8, the community standard answer to overflow was OpenZeppelin's SafeMath library. It wraps every arithmetic operation in an explicit bounds check and reverts on overflow or underflow.
// Solidity <0.8 — SAFE with SafeMath
using SafeMath for uint256;
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
SafeMath added clarity and protection, but it came with gas overhead and verbal noise. Every arithmetic expression became a function call. Developers also had to remember to import and apply it consistently; forgetting a single operation was enough to reintroduce the bug.
Solidity 0.8 Checked Arithmetic
Solidity 0.8.0, released in December 2020, made overflow and underflow checks the default at the language level. Every addition, subtraction, multiplication, and exponentiation now automatically reverts if the result would overflow or underflow the target type. The compiler inserts the equivalent of SafeMath checks for you.
// Solidity >=0.8 — Safe by default
pragma solidity ^0.8.0;
contract ModernToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
// Reverts automatically if balances[msg.sender] < amount
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
This is a genuine improvement. The overwhelming majority of straightforward overflow bugs simply cannot be written without explicitly opting out. However, the protection is not unconditional, and assuming all 0.8+ code is safe is a dangerous shortcut.
Where the Risk Still Lives
The unchecked Block
Solidity 0.8 introduced the unchecked block to allow developers to opt out of overflow checks for gas optimization. Inside an unchecked block, arithmetic behaves exactly like pre-0.8 Solidity: wrapping occurs silently.
// Solidity >=0.8 — VULNERABLE inside unchecked
pragma solidity ^0.8.0;
contract GasOptimized {
uint256 public counter;
function increment() external {
unchecked {
// No revert on overflow — wraps to 0
counter += 1;
}
}
}
unchecked is legitimate when you can mathematically prove overflow cannot happen, for example when iterating a loop index bounded by an array length. The danger is copy-paste code or over-eager optimization applied to logic where the bounds are not actually guaranteed. Auditors treat every unchecked block as a region requiring manual proof of safety.
Unsafe Type Casting
Explicit downcasting truncates bits and does not revert in Solidity 0.8, even with checked arithmetic enabled. Casting a uint256 to a uint128 or uint8 drops the high bits silently.
// VULNERABLE — silent truncation
uint256 bigValue = 300;
uint8 small = uint8(bigValue); // small == 44, not 300
// SAFER — explicit bounds check before casting
uint256 bigValue = 300;
require(bigValue <= type(uint8).max, "Value too large");
uint8 small = uint8(bigValue);
This pattern is common in price oracles that receive int256 from Chainlink and cast to smaller types, in packed storage optimizations, and in ABI-decoding custom structs. OpenZeppelin's SafeCast library provides safe casting functions for all common type pairs and is the standard recommendation for any non-trivial conversion.
Signed Integer Edge Cases
Signed integers introduce one more footgun: the minimum value. For an int8, the minimum is -128. Negating -128 overflows because 128 cannot be represented in a signed 8-bit integer. Solidity 0.8 catches this in checked arithmetic, but it can appear in unchecked blocks or in assembly.
Inline Assembly
Any arithmetic performed inside a assembly { } block operates directly on the EVM's 256-bit words with no overflow protection whatsoever. Assembly is necessary for certain low-level optimizations, but any arithmetic there must be reasoned about explicitly.
Detecting Overflow Vulnerabilities in Practice
Static analysis tools look for several signals: use of compiler versions below 0.8.0 without SafeMath, arithmetic inside unchecked blocks that lacks obvious bounds proofs, and unsafe explicit casts. Symbolic execution engines such as Mythril can construct concrete inputs that trigger wrap-around and report them as reachable bugs. If you want to understand how Mythril approaches this class of problem, the guide What Is Mythril? Symbolic Execution for Smart Contract Security covers its methodology in detail.
Fuzzing with Echidna is particularly effective here: you define an invariant like assert(balances[user] <= totalSupply) and let the fuzzer search for arithmetic paths that violate it. For ERC-20 contracts specifically, overflow in balance tracking and allowance logic are priority targets; the guide How to Audit an ERC-20 Token Contract walks through the full checklist. If you are approaching a token launch, Is My Token Safe? A Pre-Launch Security Checklist covers overflow alongside the broader surface area you need to verify before going live.
Running multiple tools in combination catches more than any single one. You can run an automated scan to check your contracts with Slither, Mythril, Aderyn, and other tools simultaneously, with each finding attributed to the tool that produced it.
Practical Recommendations
- Use Solidity 0.8.0 or higher. The built-in checks eliminate the entire class of straightforward overflow bugs at no cost to readability.
- Audit every
uncheckedblock individually. Document why overflow cannot occur at that point. If you cannot articulate the proof, remove the optimization. - Use
SafeCastfor all non-trivial type conversions. Never downcast without a preceding bounds check. - Review all inline assembly arithmetic as if you are working in pre-0.8 Solidity.
- Do not apply
uncheckedto loop counters that depend on user-controlled length inputs. - If maintaining pre-0.8 code, ensure
SafeMathis applied to every arithmetic operation, not just the ones that seem dangerous at first glance.
Summary
Integer overflow is a well-understood vulnerability with a long history in smart contract security. Solidity 0.8's checked arithmetic addressed the default case effectively, but the risk migrated rather than disappeared. It now concentrates in unchecked blocks, explicit type casts, and inline assembly. Any codebase that uses these features warrants careful arithmetic reasoning, tool-assisted analysis, and where the stakes are high enough, a manual review of the specific regions where the compiler's safety net is absent.
Scan your contract before you ship
Run an automated, transparent security scan — seven industry tools in parallel, every finding labeled with its source tool. It is not a substitute for a full manual audit, but it is a fast first line of defense.
Scan a contractFrequently asked questions
Does Solidity 0.8 completely eliminate integer overflow vulnerabilities?
No. Solidity 0.8 enables checked arithmetic by default, which prevents overflow in ordinary addition, subtraction, and multiplication. However, arithmetic inside unchecked blocks, explicit type casts that downsize integers, and code written in inline assembly all remain unprotected. Overflow is still possible in any of those contexts.
Is SafeMath still needed in Solidity 0.8?
For basic arithmetic operators like +, -, and *, SafeMath is redundant in Solidity 0.8 and adds unnecessary gas cost. You should still use OpenZeppelin's SafeCast library when converting between integer types of different sizes, since explicit casts truncate silently and are not covered by the built-in checks.
What is the risk of using the unchecked block for gas optimization?
Inside an unchecked block, Solidity reverts to pre-0.8 behavior: arithmetic wraps on overflow or underflow with no revert. This is safe only when you can mathematically prove the inputs will never cause the boundary to be exceeded. Using unchecked on logic with user-controlled inputs or complex state dependencies reintroduces the original vulnerability.
How do automated tools detect integer overflow in smart contracts?
Static analyzers like Slither flag compiler versions below 0.8 that lack SafeMath, arithmetic in unchecked blocks, and unsafe casts. Symbolic execution tools like Mythril systematically explore execution paths to find concrete inputs that produce overflow. Fuzzers like Echidna test invariants such as total supply constraints against millions of randomized inputs to find arithmetic violations empirically.
Can signed integers overflow in Solidity 0.8?
Yes, in specific circumstances. The most notable edge case is negating the minimum value of a signed integer type, for example negating int8(-128), which produces 128, a value that cannot be represented in a signed 8-bit integer. Solidity 0.8 checked arithmetic catches this in normal code, but it can appear undetected inside unchecked blocks or inline assembly.