integer-overflow
solidity
security

Integer Overflow in Solidity: Why SafeMath Still Matters in 2026

$8M evaporated from BeautyChain's BECToken because a single multiplication overflowed a uint256. That was 2018. The scary part? Developers are still shipping the same bug — just with different variable names.

Updated October 4, 2026
Duron Epps, Founder
14 min read
Share:X / TwitterLinkedIn

$8,000,000 gone because 2 × 1057 wrapped back to zero. The BeautyChain (BECToken) exploit in April 2018 is textbook integer overflow — an attacker called batchTransfer() with a crafted value, the internal multiplication silently overflowed, and they minted tokens from thin air. The BEC price collapsed to near-zero within hours. One bad multiplication. No safety check. Eight million dollars.

Fast forward to 2026. Solidity 0.8.x has built-in overflow protection. Everyone "knows" about this. And yet — overflow-adjacent logic bugs are still making it into production contracts every single month. Some are in custom assembly blocks that bypass the compiler check. Some are in unchecked blocks devs added to save gas without thinking. Some are just the old pre-0.8 pattern copy-pasted from a three-year-old tutorial.

This isn't ancient history. This is your next deployment.


Here's What Actually Happened with BECToken

The vulnerable function looked roughly like this:

// Solidity ~0.4.x — the BECToken era
function batchTransfer(address[] _receivers, uint256 _value) public {
    uint cnt = _receivers.length;
    uint256 amount = uint256(cnt) * _value; // ← the bomb

    require(_value > 0 && balances[msg.sender] >= amount);

    balances[msg.sender] = balances[msg.sender].sub(amount);
    for (uint i = 0; i < cnt; i++) {
        balances[_receivers[i]] = balances[_receivers[i]].add(_value);
        Transfer(msg.sender, _receivers[i], _value);
    }
}

The attacker passed 2 receiver addresses and a _value of 0x8000000000000000000000000000000000000000000000000000000000000000 — exactly half of the uint256 max. Multiply that by 2? You get 2^256, which overflows back to zero. The require check sees amount == 0, passes happily, and both receivers each get 2^255 tokens conjured from nothing.

This is why the crypto community collectively screamed about SafeMath for years. OpenZeppelin's SafeMath library would have reverted on that overflow. Instead, BECToken used it in some places — just not the right place.


The False Confidence of Solidity 0.8.x

Here's the take that'll surprise you even if you've been in DeFi for years: Solidity's built-in overflow checks are not a complete solution. They're a floor, not a ceiling. Three specific patterns still blow past them:

1. The unchecked Block — Gas Optimization's Dark Side

// Solidity 0.8.24 — looks safe, isn't
function distributeRewards(uint256 totalReward, uint256 participants) external {
    unchecked {
        uint256 share = totalReward / participants; // division is fine
        uint256 totalDistributed = share * participants; // overflow possible here
        // if share is large and participants is large...
    }
}

Developers wrap loops in unchecked to save gas on the iterator — totally reasonable. Then they add one more line of arithmetic inside the block without thinking. Now that one line has zero overflow protection. Slither will flag unchecked blocks, but it won't tell you which specific line inside is actually dangerous.

2. Downcasting — The Silent Truncator

// Solidity 0.8.24 — compiles clean, logic is broken
function setFee(uint256 basisPoints) external onlyOwner {
    fee = uint8(basisPoints); // truncates silently if basisPoints > 255
}

You can overflow a uint8 by downcasting from uint256 without using any arithmetic operator. The compiler doesn't catch this as an "overflow" — it's just a cast. If an owner (or attacker with owner access) calls setFee(256), the stored fee becomes 0. Suddenly your protocol takes zero fees on every trade. Or worse, your fee-gating logic breaks entirely.

OpenZeppelin 5.x has SafeCast for exactly this. Use it.

3. Inline Assembly — You're on Your Own

// Solidity 0.8.24 — assembly bypasses ALL compiler checks
function unsafeAdd(uint256 a, uint256 b) internal pure returns (uint256 result) {
    assembly {
        result := add(a, b) // no overflow check. none.
    }
}

Any arithmetic inside a Yul/assembly block is raw EVM opcodes. No overflow protection. No bounds checking. This is where a lot of gas-optimized DeFi code lives, and it's where auditors spend serious time.


The Vulnerable Pattern vs. The Fixed Version

Let's build a realistic example — a token vesting contract that calculates claimable amounts:

Vulnerable Version

// SPDX-License-Identifier: MIT
// Solidity 0.8.24 — still broken despite compiler version
pragma solidity ^0.8.24;

contract VestingVulnerable {
    mapping(address => uint256) public allocation; // tokens allocated
    mapping(address => uint256) public claimed;    // tokens already claimed
    uint256 public vestingRate; // tokens per second (set by admin)
    uint256 public startTime;

    // PROBLEM 1: No cap on vestingRate — owner could set astronomically high value
    function setVestingRate(uint256 rate) external onlyOwner {
        vestingRate = rate;
    }

    function claimable(address user) public view returns (uint256) {
        uint256 elapsed = block.timestamp - startTime;
        unchecked {
            // PROBLEM 2: This multiplication is inside unchecked
            // If vestingRate is large and elapsed is large, this overflows
            uint256 totalVested = vestingRate * elapsed;
            if (totalVested > allocation[user]) totalVested = allocation[user];
            return totalVested - claimed[user];
        }
    }

    // PROBLEM 3: No check that claim amount doesn't exceed allocation
    function claim() external {
        uint256 amount = claimable(msg.sender);
        claimed[msg.sender] += amount; // could overflow if claimable() is broken
        _transfer(msg.sender, amount);
    }

    modifier onlyOwner() { _; } // simplified
    function _transfer(address to, uint256 amount) internal {} // simplified
}

Fixed Version

// SPDX-License-Identifier: MIT
// Solidity 0.8.24 + OpenZeppelin 5.x
pragma solidity ^0.8.24;

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

contract VestingSecure {
    mapping(address => uint256) public allocation;
    mapping(address => uint256) public claimed;
    uint256 public vestingRate;
    uint256 public startTime;

    uint256 public constant MAX_VESTING_RATE = 1e18; // sensible upper bound

    // FIX 1: Cap the vesting rate at a sane maximum
    function setVestingRate(uint256 rate) external onlyOwner {
        require(rate <= MAX_VESTING_RATE, "Rate exceeds maximum");
        vestingRate = rate;
    }

    function claimable(address user) public view returns (uint256) {
        uint256 elapsed = block.timestamp - startTime;

        // FIX 2: Arithmetic is outside unchecked, so 0.8.x overflow protection applies
        // Math.mulDiv handles overflow-safe multiplication + division in one step
        uint256 totalVested = Math.mulDiv(vestingRate, elapsed, 1); // safe

        // FIX 3: Cap at user's allocation — never return more than allocated
        totalVested = Math.min(totalVested, allocation[user]);

        // FIX 4: Ensure claimed never exceeds totalVested (shouldn't happen, but defensive)
        if (claimed[user] >= totalVested) return 0;
        return totalVested - claimed[user];
    }

    function claim() external {
        uint256 amount = claimable(msg.sender);
        require(amount > 0, "Nothing to claim");

        // FIX 5: Update state before external call (reentrancy pattern)
        // claimed[user] can never exceed allocation[user] because claimable() enforces it
        claimed[msg.sender] += amount;
        require(claimed[msg.sender] <= allocation[msg.sender], "Overclaim");

        _transfer(msg.sender, amount);
    }

    modifier onlyOwner() { _; }
    function _transfer(address to, uint256 amount) internal {}
}

Notice what changed: the unchecked block is gone from the sensitive calculation, there's a hard cap on the vesting rate, and Math.mulDiv from OpenZeppelin 5.x handles the multiplication safely using 512-bit intermediate values. No truncation risk. No silent wrap-around.


How I'd Catch This Before It Ships

When I'm auditing a contract, integer overflow isn't the first thing I look for — it's the context around arithmetic that tells me where the risk is. Here's the actual mental model:

Step 1: Find every unchecked block. Grep for it. Read every single line of arithmetic inside each one. Ask: "Is there any way an input to this calculation could be attacker-influenced?" If yes, that's a finding.

Step 2: Hunt for downcasts. Specifically uint256 to uint128, uint64, uint32, uint16, or uint8. If the contract uses OpenZeppelin's SafeCast.toUint128() etc., that's fine — it'll revert on truncation. If it uses a raw cast like uint128(someValue), that's a flag every time.

Step 3: Look for assembly blocks doing math. Inline assembly is a red flag by default. Not because it's always wrong — sometimes it's the right call — but because every line needs manual verification. There's no compiler net.

Step 4: Check economic bounds. Static analysis tools catch syntax-level overflow patterns. They don't know that a vestingRate of 1e30 would cause overflow given 6 months of elapsed time. That requires understanding what the values actually mean in the real world. This is where most automated tools fall short and where manual review — or AI analysis that understands contract semantics — adds real value.

Step 5: Cross-reference with OpenZeppelin 5.x utilities. If the contract does multiplication before division anywhere, it should be using Math.mulDiv. If it accumulates values over time, there should be a max cap. If it accepts user-supplied amounts, there should be a sanity bound. The absence of these patterns is a smell.


The Tool Reality

Slither and Mythril will catch obvious overflow patterns — raw arithmetic without SafeMath in pre-0.8 code, unguarded casting in some cases. Run them. They're table stakes. But they flag based on patterns, not meaning. An unchecked block that's genuinely safe (incrementing a loop counter that's bounded by array length) looks identical to one that's dangerous (multiplying two user-controlled values) from a static analysis perspective. You need semantic understanding to know the difference.

That's the gap SmartContractAuditor.ai fills. It doesn't just pattern-match — it analyzes the economic logic of what your contract does and explains why a specific arithmetic operation is dangerous in your specific context. Run your contract before you deploy. Not after. SmartContractAuditor.ai flags overflow-prone patterns — including unsafe casts, unguarded unchecked blocks, and assembly arithmetic — and explains the actual exploit scenario in plain English, not just a warning code.


What to Do Right Now

1. Audit your unchecked blocks. Open your contracts and search for unchecked {. For each one, manually verify that every arithmetic operation inside cannot overflow given maximum realistic inputs. If you're not sure, take it out. The gas savings are rarely worth it.

2. Replace raw casts with SafeCast. Import @openzeppelin/contracts/utils/math/SafeCast.sol and replace every uint128(x), uint64(x), etc. with SafeCast.toUint128(x). It'll revert instead of truncating silently.

3. Use Math.mulDiv for multiply-then-divide. Any pattern like (a * b) / c should become Math.mulDiv(a, b, c) from OpenZeppelin 5.x. It uses 512-bit intermediates and handles overflow correctly.

4. Set explicit bounds on admin-settable values. Every function that lets an owner set a rate, a fee, a multiplier — add a require with a sensible maximum. An unbounded admin parameter is an overflow waiting to happen.

5. Run SmartContractAuditor.ai before mainnet. Paste your contract in, get a structured breakdown of overflow risks, logic issues, and access control problems before you deploy. Try the free audit here — it takes less than a minute and has caught exactly the patterns described in this post in real contracts.

The BECToken exploit was six years ago. The lesson still hasn't fully landed. Don't be the dev who learns it the expensive way.

Further Reading

Published on
June 19, 2026