← Back to Blog

Integer Overflow in Solidity 0.8+: Are We Really Safe?

2026-04-09 solidity smart contract security integer overflow SafeMath arithmetic audit solidity 0.8 unchecked type casting

The Beauty Chain (BEC) token overflow in April 2018 minted 10^58 tokens from nothing, instantly collapsing the token's value to near zero. The vulnerability was one line: a multiplication that overflowed uint256 silently rolled over to a small number, bypassing the balance check. This class of bug felt so fundamental — and so catastrophic — that the Solidity team made overflow protection a language default.

Solidity 0.8.0 shipped in December 2020 with checked arithmetic. Every addition, subtraction, and multiplication reverts on overflow or underflow by default. SafeMath became largely redundant. The community declared overflow solved.

It wasn't.


What 0.8 Actually Fixed

The default checked arithmetic in Solidity 0.8 catches the most obvious class: direct +, -, and * operations on uint and int types where the result exceeds the type's range.

uint256 x = type(uint256).max;
uint256 y = x + 1; // reverts in 0.8, silently overflows in 0.7

This is genuinely useful. The BEC-style bug — multiply two large numbers, get zero, skip a require — can't happen by accident anymore.

What 0.8 doesn't fix: everything else.


The unchecked Block

Solidity 0.8 introduced unchecked {} precisely because checked arithmetic has gas overhead, and some patterns (loop counters, ERC-20 balance deltas) are safe to compute without checks. The unchecked block explicitly disables overflow protection for its contents.

This is legitimate. It's also a footgun when used carelessly.

// Gas optimization — fine if i is bounded by array length
for (uint256 i = 0; i < arr.length;) {
    process(arr[i]);
    unchecked { ++i; }
}

// Dangerous — arithmetic on user-controlled values inside unchecked
function calculateReward(uint256 userBalance, uint256 multiplier) external view returns (uint256) {
    unchecked {
        return userBalance * multiplier / 1e18; // overflows silently if inputs are large
    }
}

The second example is a real pattern in yield protocols. If userBalance is close to type(uint256).max and multiplier is greater than 1, the multiplication overflows — silently — producing a nonsense reward value that may underflow further downstream.

When auditing, every unchecked block is a code review checkpoint. The question isn't "is this optimization valid" but "can any input path reach this block with values that would overflow?"


Downcasting: The Silent Truncation

Casting from a larger integer type to a smaller one truncates the high bits. Solidity 0.8 doesn't revert on downcast truncation — it silently drops the data.

uint256 largeValue = 256; // 0x100
uint8 small = uint8(largeValue); // becomes 0x00 — silently truncated to 0

This creates a reliable exploit path: find a value that gets downcast, craft an input where the truncation produces a favorable (usually small or zero) result, and exploit the logic that reads the downcasted value.

// Realistic example: fee calculation
function calculateFee(uint256 amount) internal pure returns (uint256) {
    uint8 feeRate = uint8(amount / 100); // truncated if amount > 25500
    return amount * feeRate / 100;
}

If amount is 25600 (0x6400), amount / 100 is 256, which downcasts to uint8(0). Fee is zero. An attacker who controls amount can choose values that truncate to zero, paying no fees.

OpenZeppelin's SafeCast library provides safe downcast operations that revert if the value doesn't fit:

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

uint8 feeRate = SafeCast.toUint8(amount / 100); // reverts if > 255

Type Mismatch and Expression Evaluation Order

Solidity evaluates arithmetic expressions using the type of the operands, not the result variable. When both operands are smaller types, the intermediate result may overflow even if the final variable is large enough to hold the correct answer.

uint64 a = 2e18;
uint64 b = 2e18;
uint256 result = a * b; // WRONG: computed as uint64 * uint64, overflows before assignment

The fix is explicit casting before the operation:

uint256 result = uint256(a) * uint256(b); // computed as uint256, no overflow

This is especially common when working with timestamps, token amounts from older contracts with uint64 balances, or packed structs where fields are smaller integer types.


Division by Zero and Rounding Direction

Solidity division reverts on division by zero (both in checked and unchecked contexts). But rounding behavior is less obvious and can be exploited.

Solidity integer division truncates toward zero. In financial calculations, this means:

// User stakes 999 tokens, pool requires 1000 per unit
uint256 units = userAmount / unitsSize; // 999 / 1000 = 0
// User gets 0 units but their 999 tokens are still locked

More critically, truncation can be weaponized. If a fee calculation rounds down in the user's favor on every operation, repeatedly executing small operations can drain accumulated fees. This is the "dust attack" pattern in fee-collecting contracts.

Direction matters: when calculating amounts the protocol keeps (fees, penalties), round up. When calculating amounts users receive, round down. Mixing these can create slowly-exploitable value leakage.


Where the Bugs Actually Are in 2026

The straight integer overflow (BEC-style) is rare. The bugs that appear in audits now are subtler:

Pattern Example Still a Live Bug?
Direct overflow a + b on uint256 Rare (0.8 catches it)
unchecked overflow Yield reward calculation Yes
Downcast truncation uint8(largeValue) Yes
Cross-type expression uint64 * uint64 → uint256 Yes
Rounding direction Fee rounds toward user Yes
Intermediate overflow a * b / c where a*b overflows Yes

The intermediate overflow pattern deserves special mention. A calculation like shares * pricePerShare / totalShares is mathematically safe if the final result fits in uint256. But if shares * pricePerShare overflows in the intermediate step, the checked arithmetic reverts even though the answer would have been valid. The correct approach uses mulDiv from libraries like OpenZeppelin (Math.mulDiv) or Solmate, which compute the full 512-bit product before dividing.


Detection

Slither includes detectors for unchecked arithmetic in pre-0.8 code and flags explicit unchecked blocks. Semgrep custom rules can target downcast patterns. ContractScan's AI engine is specifically prompted to examine unchecked blocks, intermediate multiplication in financial calculations, and type cast chains — the patterns where 0.8's protection doesn't reach.

Manual review remains essential for rounding direction bugs: these require understanding the economic intent of the calculation, not just the syntax.


Solidity 0.8's checked arithmetic was a genuine improvement. It eliminated a class of bugs that had drained hundreds of millions. But treating it as a complete solution to integer arithmetic risk is what creates the next generation of vulnerabilities — ones that are harder to spot because they don't look like overflow bugs.


ContractScan is a multi-engine smart contract security scanner. QuickScan is free and unlimited — no sign-up required. Try it now.

Scan your contract now
Slither + AI analysis — Unlimited QuickScans, no signup required
Try Free Scan →