Integer Overflow and Underflow: Complete Guide, Examples, Risks and Best Practices
Integer overflow and underflow are common arithmetic bugs that happen when a number becomes too large or too small for the space reserved to store it. The result can be a wrong value, a program crash, a security weakness, or in blockchain applications, a loss of funds.
The idea sounds technical, but the core concept is simple: every integer type has a limit. When a calculation goes beyond that limit, the computer must decide what to do. Some languages stop the operation, some wrap the value around, and some behave differently depending on settings, compiler version, or runtime mode.
This guide explains integer overflow and underflow from the ground up. It covers how they work, where they appear, practical examples, risks, smart contract concerns, testing methods, and best practices developers can use to avoid costly mistakes.
Quick answer: Integer overflow happens when a calculation exceeds the maximum value an integer type can store. Integer underflow happens when a calculation goes below the minimum value. In unsafe or unchecked arithmetic, the result may wrap around to the opposite end of the range. In checked arithmetic, the program usually throws an error or reverts.
1. What Is an Integer?
An integer is a whole number without decimals, such as 0, 15, -3, or 1,000,000. Programming languages store integers in fixed-size containers, often measured in bits. The number of bits determines the range of values the integer can safely hold.
| Integer type | Common range example | What it means |
|---|---|---|
| uint8 | 0 to 255 | Unsigned 8-bit integer. It cannot store negative numbers. |
| int8 | -128 to 127 | Signed 8-bit integer. It can store negative and positive numbers. |
| uint256 | 0 to 2^256 - 1 | Large unsigned integer commonly used in Solidity smart contracts. |
| int256 | -2^255 to 2^255 - 1 | Large signed integer commonly available in Solidity. |
The important point is that integers are not unlimited. Even very large types have boundaries.
2. What Is Integer Overflow?
Integer overflow happens when a calculation produces a value greater than the maximum value the integer type can store.
Example: uint8 can store values from 0 to 255. If unchecked arithmetic tries to calculate 255 + 1, the value cannot fit. In wrapping arithmetic, it becomes 0.
// Conceptual example using an 8-bit unsigned integer
uint8 max = 255;
uint8 result = max + 1;
// In unchecked/wrapping arithmetic, result becomes 0.
// In checked arithmetic, the operation fails.
This is called overflow because the value has gone over the top of the allowed range.
3. What Is Integer Underflow?
Integer underflow happens when a calculation produces a value lower than the minimum value the integer type can store.
Example: uint8 cannot store a negative number. If unchecked arithmetic tries to calculate 0 - 1, the value cannot fit. In wrapping arithmetic, it becomes 255.
// Conceptual example using an 8-bit unsigned integer
uint8 min = 0;
uint8 result = min - 1;
// In unchecked/wrapping arithmetic, result becomes 255.
// In checked arithmetic, the operation fails.
This is called underflow because the value has gone below the bottom of the allowed range.
4. Simple Diagram: How Overflow and Underflow Wrap Around
Figure: In unchecked wrapping arithmetic, going above the maximum may return to the minimum, and going below the minimum may return to the maximum. In checked arithmetic, the operation should fail instead of wrapping.
5. Overflow vs Underflow: Quick Comparison
| Issue | What happens | Simple example | Typical danger |
|---|---|---|---|
| Overflow | The result is higher than the maximum value. | uint8: 255 + 1 | A balance, counter, supply, or price may become unexpectedly small. |
| Underflow | The result is lower than the minimum value. | uint8: 0 - 1 | A balance or allowance may become unexpectedly huge. |
6. Why Do Overflow and Underflow Happen?
They happen because computers usually allocate a fixed amount of memory for each numeric type. A fixed-size integer cannot grow forever. When a calculation exceeds the type range, the language or runtime must handle it somehow.
| Arithmetic behavior | Meaning | Risk level |
|---|---|---|
| Checked arithmetic | The program detects overflow or underflow and throws an error, reverts, or aborts. | Safer by default. |
| Unchecked arithmetic | The program does not automatically check whether the result fits. | Risky unless carefully proven safe. |
| Wrapping arithmetic | The value wraps around the range, like an odometer. | Dangerous when not intentional. |
| Saturating arithmetic | The value stays at the nearest limit instead of wrapping. | Useful in some systems, but not always available. |
7. Real-World Analogy: The Odometer Problem
Imagine an old car odometer that can display only 999,999 miles. If the car drives one more mile, the display may roll over to 000,000. The car did not become new; the display ran out of space. Integer overflow is similar. The calculation may be mathematically correct, but the storage type cannot represent the answer.
8. Why Integer Overflow and Underflow Matter
In small demo programs, overflow may look like a harmless curiosity. In real systems, it can cause serious problems because software often uses integers for balances, lengths, permissions, counters, prices, IDs, timestamps, token supplies, and limits.
- A token balance can become much larger or smaller than intended.
- A counter can reset and allow duplicate IDs or repeated actions.
- A length calculation can become too small and lead to memory corruption in low-level languages.
- A price, fee, or reward formula can produce the wrong amount.
- A safety check can be bypassed because the checked value is not the real mathematical result.
- A smart contract can violate its economic rules and lose funds.
9. Beginner Example: Overflow in a Counter
Suppose an application uses a small integer for a counter. The counter is expected to increase by one each time a user performs an action.
uint8 counter = 255;
counter = counter + 1;
// Expected by a beginner: 256
// Actual in unchecked uint8 wrapping arithmetic: 0
If the application depends on the counter always increasing, this wraparound can break logic. For example, a system may think the counter has restarted or that an action has not happened yet.
10. Beginner Example: Underflow in a Balance
Underflow is especially dangerous when subtracting from a balance.
uint8 balance = 0;
uint8 withdrawal = 1;
balance = balance - withdrawal;
// In unchecked wrapping arithmetic, balance becomes 255.
A user with zero balance should not be able to withdraw one unit. If the subtraction underflows and wraps, the user may appear to have a huge balance instead.
11. Integer Overflow and Underflow in Smart Contracts
Overflow and underflow are well-known smart contract risks because smart contracts often manage money-like assets. A small arithmetic mistake can permanently affect token balances, liquidity pools, rewards, debt accounting, or access limits.
In Solidity 0.8.0 and later, arithmetic operations revert on overflow and underflow by default. Solidity also allows unchecked blocks for developers who deliberately want old wrapping behavior or need gas optimization in a proven-safe section. The Solidity documentation describes this as a breaking semantic change introduced in version 0.8.0. OWASP also lists integer overflow and underflow as a smart contract security concern, noting that unchecked blocks, assembly, and custom libraries still require review.
| Solidity version / context | Default arithmetic behavior | What developers should do |
|---|---|---|
| Solidity before 0.8.0 | Overflow and underflow can wrap silently. | Use SafeMath or upgrade where possible. |
| Solidity 0.8.0+ | Standard arithmetic reverts on overflow and underflow. | Avoid unnecessary unchecked blocks; still test edge cases. |
| unchecked { ... } | Arithmetic inside the block can wrap. | Use only when a condition or invariant proves it is safe. |
| Assembly / low-level code | May bypass high-level safety assumptions. | Review manually and test with extreme values. |
11.1 Smart Contract Example: Vulnerable Pre-0.8 Token Transfer
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
contract VulnerableToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
// If the sender has less than amount, this can underflow.
balances[msg.sender] -= amount;
// If the recipient balance is near the maximum, this can overflow.
balances[to] += amount;
}
}
The dangerous line is not visually obvious. A beginner may assume subtraction simply fails when the sender does not have enough tokens. In older Solidity versions, it could wrap instead unless protected by checks or a safe math library.
11.2 Safer Solidity 0.8+ Version
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SaferToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount; // Reverts on underflow by default
balances[to] += amount; // Reverts on overflow by default
}
}
The require statement is still useful because it gives a clear business-rule error. Solidity 0.8+ also protects the arithmetic operation itself.
11.3 What About SafeMath?
SafeMath is a library pattern that checks arithmetic operations before returning a result. It was widely used in Solidity before version 0.8.0. In modern Solidity, SafeMath is usually unnecessary for normal checked arithmetic because the compiler already checks overflow and underflow.
| Situation | Use SafeMath? | Reason |
|---|---|---|
| Solidity 0.7.x or older | Usually yes | Arithmetic can wrap silently without it. |
| Solidity 0.8.x or newer | Usually no | Compiler checks arithmetic by default. |
| Inside unchecked blocks | Maybe, but usually avoid the unchecked block instead | Manual checks are required if wrapping is not intended. |
| Custom numeric libraries | Depends | Review the library behavior carefully. |
12. Common Places Overflow and Underflow Bugs Appear
| Area | Risky operation | Example problem |
|---|---|---|
| Balances and withdrawals | balance - amount | Underflow if amount is greater than balance. |
| Token supply | totalSupply + mintedAmount | Overflow if minting is not capped or checked. |
| Counters and IDs | counter + 1 | Counter wraps and reuses an old ID. |
| Fees and rewards | amount * rate / denominator | Multiplication overflows before division. |
| Array lengths and indexes | length - 1 or index + 1 | Underflow or out-of-range access. |
| Time calculations | timestamp + duration | Overflow in systems with smaller timestamp types. |
| Type casting | uint256 to uint128 | Downcast truncates high bits if not checked. |
13. Important Mistake: Multiplication Before Division
A common formula in finance and smart contracts is: amount * rate / scale. Even if the final answer would fit, the intermediate multiplication may overflow first.
// Risky if amount * rate is too large
uint256 fee = amount * rate / 10_000;
Safer approaches include limiting input ranges, using full-precision math libraries designed for multiplication and division, or reworking the formula only when the math remains correct. Do not casually divide first unless you understand the rounding effect.
14. Casting and Downcasting Risks
Overflow is not limited to addition and multiplication. Type conversion can also create unexpected values. For example, converting a large uint256 into uint8 cannot preserve the full value.
uint256 big = 300;
uint8 small = uint8(big);
// In many low-level contexts, only the lower bits are kept.
// 300 does not fit into uint8, whose maximum is 255.
Best practice: before downcasting, check that the value is within the target type range or use a safe casting library.
15. How Attackers Exploit Overflow and Underflow
Attackers look for arithmetic that uses user-controlled input and then affects value, permissions, or state. They try extreme values, boundary values, and combinations that make intermediate calculations wrap.
- Find a variable with a known limit, such as a small integer, balance, supply, or counter.
- Submit an input that causes addition, subtraction, multiplication, or casting to exceed the valid range.
- Use the wrapped result to bypass a check, inflate a balance, reduce a fee, reuse an ID, or break an invariant.
- Repeat the action before the system detects the abnormal state.
16. Best Practices to Prevent Integer Overflow and Underflow
16.1 Use Checked Arithmetic by Default
Prefer language versions, compiler settings, or libraries that fail safely when arithmetic exceeds the valid range. In Solidity, use version 0.8.0 or newer unless you have a strong reason not to.
16.2 Avoid unchecked Blocks Unless You Can Prove Safety
Unchecked arithmetic may save gas in some cases, but it removes automatic protection. Use it only when the surrounding code clearly proves the operation cannot overflow or underflow.
// Reasonable use only if you can prove i < length and length is bounded.
unchecked {
++i;
}
16.3 Validate Inputs Before Arithmetic
Check user-controlled values before using them in calculations. This is especially important for withdrawals, minting, price calculations, and loops.
require(amount <= balances[msg.sender], "Insufficient balance");
require(amount <= maxMintAmount, "Amount too large");
16.4 Check Business Invariants, Not Just Arithmetic
A calculation can be arithmetically safe but still economically wrong. For example, a DeFi pool should maintain its expected accounting relationships. Test and assert important invariants such as total balances, total supply, share ratios, and fee limits.
16.5 Be Careful with Type Sizes
Smaller integer types can save storage space in some smart contract layouts, but they also have smaller limits. Do not use uint8, uint16, or uint32 merely because the current expected value is small. Think about future growth, upgrades, migrations, and edge cases.
16.6 Use Safe Casting for Downcasts
When converting from a larger integer type to a smaller one, use explicit range checks or safe casting utilities. Silent truncation can be as dangerous as arithmetic overflow.
16.7 Test Boundary Values
Normal tests often use normal numbers. Overflow bugs hide near limits. Include tests for zero, one, maximum values, maximum minus one, very large user inputs, and values just above allowed limits.
| Test case | Why it matters |
|---|---|
| 0 | Catches subtraction and division edge cases. |
| 1 | Catches off-by-one mistakes. |
| maximum value | Catches addition and multiplication overflow. |
| maximum value - 1 | Catches near-boundary behavior. |
| amount greater than balance | Catches withdrawal underflow. |
| large amount * large rate | Catches intermediate multiplication overflow. |
16.8 Use Static Analysis and Fuzz Testing
Static analyzers can scan code for risky arithmetic patterns. Fuzz testing generates many random and boundary inputs to find cases humans may miss. For smart contracts, combine unit tests, property-based tests, fuzz tests, and manual review.
16.9 Review Assembly and Custom Libraries Carefully
High-level safety checks may not apply inside low-level assembly, custom math libraries, or intentionally unchecked sections. Treat these areas as security-sensitive and document why each operation is safe.
16.10 Document Assumptions
If a calculation is safe because a value is capped elsewhere, document that relationship. Future developers may remove or change the cap without realizing it protects arithmetic safety.
17. Developer Checklist
- Are all arithmetic operations using checked arithmetic by default?
- Are any unchecked blocks clearly justified and covered by tests?
- Can any user input reach addition, subtraction, multiplication, exponentiation, or casting?
- Are there tests for zero, maximum values, and values just over allowed limits?
- Could multiplication overflow before division?
- Are downcasts checked before conversion?
- Do important business invariants hold after every state-changing operation?
- Have static analysis and fuzz tests been run?
- Has low-level assembly or custom math code received extra review?
18. Common Misconceptions
| Misconception | Reality |
|---|---|
| Only old code has overflow risks. | Modern checked arithmetic helps, but unchecked blocks, assembly, casting, and custom libraries can still be risky. |
| Using uint256 means overflow is impossible. | uint256 is very large, but not infinite. Bugs can still occur with multiplication, exponentiation, or wrong assumptions. |
| SafeMath solves every numeric bug. | SafeMath checks arithmetic, but it does not prove business logic, formulas, rounding, or economic invariants are correct. |
| Overflow always causes a crash. | In some environments it wraps silently; in others it reverts or throws an error. Behavior depends on language and context. |
| Unchecked arithmetic is always bad. | It can be acceptable for proven-safe micro-optimizations, but it should be rare, documented, and tested. |
19. Pros and Cons of Different Prevention Approaches
| Approach | Pros | Cons |
|---|---|---|
| Checked arithmetic | Simple, safe default; catches many bugs automatically. | May add some runtime cost in certain environments. |
| Safe math libraries | Useful for older languages or compiler versions; clear intent. | Can be redundant in modern Solidity; still does not fix flawed formulas. |
| Manual require checks | Allows custom errors and business-specific validation. | Easy to miss edge cases or check the wrong condition. |
| Fuzz testing | Finds unexpected edge cases and boundary failures. | Requires setup and good properties to be effective. |
| Formal verification | Can prove important safety properties. | More complex and usually reserved for high-value systems. |
20. Practical Secure Coding Pattern
A strong pattern is to combine business checks, checked arithmetic, and invariant tests.
function withdraw(uint256 amount) external {
require(amount > 0, "Amount must be positive");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
totalDeposits -= amount;
// Continue with transfer logic after state updates.
}
This pattern makes the intended rule clear: users can withdraw only a positive amount that they actually own. Checked arithmetic provides a second layer of protection if the rule is accidentally broken elsewhere.
21. How to Review Existing Code for Overflow and Underflow
- Identify the language, compiler version, and arithmetic behavior.
- Search for addition, subtraction, multiplication, exponentiation, increments, decrements, and casts.
- Mark every calculation that uses user input or affects money, limits, indexes, or permissions.
- Review unchecked blocks, assembly, and custom math libraries first.
- Check whether preconditions actually guarantee safe ranges.
- Add boundary tests around every risky operation.
- Add invariant tests for accounting, supply, balances, and limits.
- Document any arithmetic that is intentionally allowed to wrap.
22. FAQs About Integer Overflow and Underflow
22.1 What is integer overflow in simple words?
Integer overflow happens when a number becomes too large for its integer type. For example, if a type can store only 0 to 255, then 255 + 1 is outside the range.
22.2 What is integer underflow in simple words?
Integer underflow happens when a number becomes too small for its integer type. For an unsigned integer that cannot go below zero, 0 - 1 is an underflow.
22.3 Are overflow and underflow always security bugs?
Not always. Sometimes wrapping is intentional, such as in certain cryptographic or low-level operations. It becomes a bug when the developer did not intend the wraparound or when it breaks program rules.
22.4 Does Solidity still have overflow and underflow problems?
Solidity 0.8.0 and later check standard arithmetic by default, but risks remain in unchecked blocks, assembly, older contracts, custom libraries, unsafe casting, and flawed business logic.
22.5 Is SafeMath still needed in Solidity?
For normal arithmetic in Solidity 0.8.0 and later, SafeMath is usually not needed because the compiler checks overflow and underflow. For Solidity 0.7.x and older, SafeMath is commonly used.
22.6 Why is multiplication risky?
Multiplication grows values quickly. A formula like amount * rate / scale can overflow during multiplication before the division reduces the result.
22.7 Can integer overflow happen with uint256?
Yes. uint256 has a huge range, but it is still finite. Extreme multiplication, exponentiation, or unbounded user input can exceed it.
22.8 How do I test for overflow and underflow?
Test boundary values: zero, one, maximum values, maximum minus one, values greater than balances, and large multiplication inputs. Fuzz testing is also very useful.
22.9 What is an unchecked block?
In Solidity, unchecked { ... } disables automatic overflow and underflow checks for arithmetic inside the block. It should be used only when safety is proven and documented.
22.10 What is the best prevention strategy?
Use checked arithmetic by default, validate inputs, avoid unnecessary unchecked code, test boundaries, use safe casting, and review business invariants.
23. Final Takeaway
Integer overflow and underflow are simple ideas with serious consequences. They happen when arithmetic goes outside the range an integer type can store. In unsafe contexts, the result may wrap around and produce a value that looks valid but is completely wrong.
The safest approach is to use checked arithmetic, avoid unnecessary unchecked operations, validate inputs, test boundary cases, and review any calculation that affects balances, limits, counters, prices, or permissions. For smart contracts, arithmetic safety is not just a code-quality issue; it is part of protecting user funds and preserving trust.
Sources Consulted and Checked
The following sources were consulted and checked while preparing this article to support technical accuracy and clarity.
- Solidity documentation: Solidity v0.8.0 breaking changes - arithmetic operations revert on underflow and overflow; unchecked blocks can use wrapping behavior.
- OWASP Smart Contract Top 10: Integer Overflow and Underflow - notes that Solidity 0.8+ checks arithmetic by default, while unchecked blocks, assembly, and custom libraries still need review.
- OpenZeppelin Contracts documentation and common SafeMath usage history for pre-0.8 Solidity codebases.
Reader Advice
This article is provided for educational and informational purposes only. It explains general programming and smart-contract security concepts and is not personalized legal, financial, investment, cybersecurity, or professional advice. Software behavior, compiler defaults, security guidance, laws, policies, standards, and statistics can change over time and may vary by language, platform, version, project, and region.
Before relying on any example in production or making a decision involving code, funds, or user data, verify current requirements through official documentation and qualified professionals, test thoroughly, and obtain an independent security review where appropriate. Arithmetic flaws and incorrect assumptions can cause program failures, security vulnerabilities, or financial loss, so readers should assess the risks for their own circumstances.