Integer Overflow in Solidity 0.8+ — Why You Still Need to Care

Solidity 0.8.0 made overflow checks automatic, and most developers stopped thinking about it. That's the problem. Every `unchecked{}` block, every assembly snippet, every type cast, and every legacy SafeMath import is a place where SWC-101 quietly comes back. Here's where overflows still bite in…

Integer overflow (SWC-101) occurs when an arithmetic operation produces a value outside the range of its type, wrapping around silently. Solidity 0.8.0+ reverts on overflow by default, but developers can opt out via `unchecked{}` blocks for gas savings. Modern overflow bugs typically live in those unchecked regions, in inline assembly, or in unsafe type downcasting like `uint256` to `uint64`.
··7 min read

The Bug

An integer overflow happens when arithmetic produces a result larger than the maximum value a type can hold (or smaller than its minimum, for underflow). In a `uint8`, `255 + 1` wraps to `0`. In a `uint256`, `type(uint256).max + 1` wraps to `0`. Before Solidity 0.8.0, this was silent and catastrophic — entire token supplies could be minted by underflowing a balance check.

Solidity 0.8.0 fixed the default behavior: arithmetic on integer types now reverts on overflow or underflow. The compiler injects checks around `+`, `-`, `*`, `/`, and `**`. SafeMath, the library that defined an era of ERC-20 development, became redundant overnight.

So why are we still talking about this in 2025? Because the bug didn't die — it migrated.

Where Overflow Still Lives in Modern Solidity

There are four places SWC-101 still shows up in audits:

1. **`unchecked{}` blocks** — developers wrap arithmetic in `unchecked` to save ~30-40 gas per operation. If the bounds reasoning is wrong, you're back to pre-0.8 behavior.
2. **Inline assembly** — Yul has no overflow checks. Ever. `add(x, y)` wraps silently.
3. **Unsafe downcasting** — `uint256` to `uint128` (or smaller) is a truncation, not a check. The compiler does *not* revert. SafeCast exists for a reason.
4. **Legacy SafeMath in 0.8+ codebases** — usually harmless, but I've seen `SafeMath.sub` wrapped in `unchecked` because someone "optimized" it. That's a special kind of cursed.

A Vulnerable Modern Contract

Here's a staking contract that compiles cleanly on 0.8.24 and still has an exploitable overflow:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract RewardVault {
mapping(address => uint128) public stakedAmount;
mapping(address => uint128) public rewardDebt;
uint256 public accRewardPerShare; // scaled by 1e12

function stake(uint256 amount) external {
// bug 1: silent truncation from uint256 to uint128
stakedAmount[msg.sender] += uint128(amount);

unchecked {
// bug 2: pendingReward can exceed uint128
uint256 pending = (uint256(stakedAmount[msg.sender]) * accRewardPerShare) / 1e12;
rewardDebt[msg.sender] = uint128(pending);
}
}
}
```

Two separate failure modes:

`uint128(amount)` — if `amount > type(uint128).max`, the high bits are silently dropped. An attacker stakes `2^128`, the contract records `0`, but the token transfer (if added) moves the full amount. Funds vanish into accounting limbo.
- `unchecked { ... uint128(pending) }` — the inner multiplication can wrap in `unchecked`, and the final cast truncates regardless. Reward debt becomes wildly wrong.

The Fix

Use OpenZeppelin's `SafeCast` for any narrowing conversion, and only use `unchecked` when you can mathematically prove the operation cannot wrap:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/utils/math/SafeCast.sol";

contract RewardVault {
using SafeCast for uint256;

mapping(address => uint128) public stakedAmount;
mapping(address => uint128) public rewardDebt;
uint256 public accRewardPerShare;

function stake(uint256 amount) external {
// reverts if amount > type(uint128).max
stakedAmount[msg.sender] += amount.toUint128();

// checked arithmetic — reverts on overflow
uint256 pending = (uint256(stakedAmount[msg.sender]) * accRewardPerShare) / 1e12;
rewardDebt[msg.sender] = pending.toUint128();
}
}
```

When `unchecked` Is Actually Fine

I'm not anti-`unchecked`. Used correctly, it's free gas. The canonical safe pattern is a loop counter:

```solidity
for (uint256 i = 0; i < array.length;) {
// ... do work ...
unchecked { ++i; }
}
```

`i` is bounded by `array.length`, which is bounded by `2^256 - 1` in theory but by gas limits in practice. It cannot overflow. Same logic applies to decrementing a value you've just checked is greater than zero, or subtracting a balance from a total you know is larger.

The rule: write a one-line comment above every `unchecked` block explaining *why* it can't wrap. If you can't write that comment, remove the `unchecked`.

Real-World Damage

The pre-0.8 era was brutal:

**BeautyChain (BEC), April 2018** — an overflow in `batchTransfer` let an attacker mint `2 * (2^255)` tokens. The token's market cap collapsed in minutes; trading was suspended. Losses estimated north of $900M in notional value.
- **SMT (SmartMesh), April 2018** — same week, same bug class, different contract. Trading halted on multiple exchanges.
- **Proxy Overflow (multiple ERC-20s)** — a class of bugs where `transferFrom` allowance subtraction underflowed, granting infinite spending.

Post-0.8, the public incidents have shifted to `unchecked` misuse and downcasting bugs in DeFi protocols. They get less press because the dollar amounts per incident are smaller — but they're still six- and seven-figure losses.

Audit Checklist for SWC-101 in 0.8+

When I'm reviewing a contract, I grep for these patterns first:

Every `unchecked` block — read the math, prove the bound
- Every `uintN(x)` cast where `N < 256` — should be `SafeCast`
- Every `assembly` block touching arithmetic — Yul never checks
- Any imported `SafeMath` — flag it; the codebase is either old or confused
- Fixed-point math libraries (PRBMath, ABDKMath) — check they're using the correct mode

If you're shipping a protocol that handles real value, run it through our [free AI audit](https://www.cryptohawking.com/audit) before deployment — it catches the obvious downcasting and `unchecked` patterns automatically. For anything holding more than six figures in TVL, a [manual audit](https://www.cryptohawking.com/audit/manual) is the right call: deeper invariant analysis, three business days, paid in ETH, SOL, or USDT.

TL;DR

Solidity 0.8 didn't kill integer overflow. It moved it from "every line of arithmetic" to "every `unchecked` block, every downcast, every assembly snippet." The surface area shrunk by ~95%, but what's left is concentrated in exactly the places developers add for gas optimization — which means it correlates with code that was already trying to be clever. Be paranoid in those regions. SafeCast everywhere. Comment your `unchecked` blocks. And if you're still importing SafeMath in 2025, we need to talk.

FAQ

Do I still need SafeMath in Solidity 0.8+?

No. Solidity 0.8.0 and later check every arithmetic operation by default and revert on overflow or underflow. SafeMath is redundant and just wastes gas — every `add`, `sub`, `mul` already does what SafeMath used to do. Remove the import. The only legitimate reason to keep SafeMath-style libraries around is if you're maintaining a multi-version codebase that still compiles against 0.7.x or earlier. For new contracts on 0.8.x, drop it.

When is `unchecked{}` safe to use?

Only when you can mathematically prove the operation cannot overflow or underflow. The classic safe case is incrementing a loop counter that's bounded by an array length. Another is subtraction immediately after an explicit `require` that the minuend is larger. Unsafe cases include any arithmetic on user-supplied values, multiplication of two large numbers, and anything inside a function whose preconditions you haven't fully audited. Rule of thumb: if you can't write a one-line proof of safety in a comment, don't use `unchecked`.

Does Solidity 0.8 check integer overflow in inline assembly?

No. Yul and inline assembly have no overflow protection — `add`, `sub`, `mul` all wrap silently like pre-0.8 Solidity. If you drop into assembly for gas optimization or low-level access, you're responsible for every bound. This is one of the most common sources of modern overflow bugs in protocols that use assembly for math-heavy operations like fixed-point arithmetic. Audit assembly blocks line by line, and prefer well-tested libraries like Solady or PRBMath over hand-rolled Yul.

Is downcasting from uint256 to uint128 safe in Solidity 0.8?

No, and this surprises people. Explicit type conversions like `uint128(x)` are truncations, not checked casts — the compiler silently drops the high bits if `x` exceeds `type(uint128).max`. There is no revert. Use OpenZeppelin's `SafeCast` library (`x.toUint128()`) which performs an explicit bounds check and reverts on overflow. This bug class has caused real losses in DeFi protocols that pack values into smaller types for storage efficiency without checking the source range.

What was the BEC token overflow hack?

In April 2018, an attacker exploited an integer overflow in BeautyChain's (BEC) `batchTransfer` function. By passing two recipient addresses and a transfer amount of `2^255`, the multiplication `amount * recipients.length` overflowed to zero, bypassing the balance check. The attacker then minted approximately `2 * 2^255` tokens to two addresses they controlled. The token's price crashed instantly and exchanges suspended trading. It's the textbook SWC-101 case and a major reason Solidity 0.8 made overflow checks the default.

One Solidity tip + 1 case study per month

Integer Overflow in Solidity 0.8+ — Why You Still Need to Care | Crypto Hawking