Back

SC08: Integer Overflow and Underflow - Code Examples

Each pair below shows a vulnerable contract and the secure version. The examples focus on the wraps that dominate real findings: balance underflow, multiplication overflow in a total, downcast truncation, unchecked misuse, and inline-assembly arithmetic.

1. Balance Underflow

Vulnerable

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;               // pre-0.8: arithmetic wraps silently

contract VulnerableToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        // No SafeMath, no guard: if amount > balance, this underflows.
        balances[msg.sender] -= amount;   // 0 - 1 => 2^256 - 1
        balances[to]        += amount;
    }
}
// Attacker with balance 0 calls transfer(x, 1) and wraps to a near-max balance.

Secure

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;              // checked arithmetic reverts on wrap

contract SecureToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        // Underflow now reverts with Panic(0x11) instead of wrapping.
        balances[msg.sender] -= amount;
        balances[to]        += amount;
    }
}
// Pre-0.8 equivalent: use SafeMath -> balances[msg.sender].sub(amount)

2. Multiplication Overflow in a Total (batchOverflow class)

Vulnerable

pragma solidity ^0.4.24;              // pre-0.8, no SafeMath

contract VulnerableBatch {
    mapping(address => uint256) public balances;

    function batchTransfer(address[] memory to, uint256 amount) public {
        uint256 total = to.length * amount;      // overflows for a crafted amount
        require(balances[msg.sender] >= total);  // passes: total wrapped small
        balances[msg.sender] -= total;
        for (uint i = 0; i < to.length; i++) {
            balances[to[i]] += amount;           // each gets the full huge amount
        }
    }
}

Secure

pragma solidity ^0.8.20;

contract SecureBatch {
    mapping(address => uint256) public balances;

    function batchTransfer(address[] calldata to, uint256 amount) external {
        // Checked multiply reverts on overflow, so `total` is always truthful.
        uint256 total = to.length * amount;
        require(balances[msg.sender] >= total, "insufficient balance");
        balances[msg.sender] -= total;
        for (uint256 i = 0; i < to.length; i++) {
            balances[to[i]] += amount;
        }
    }
}

3. Supply / Cap Overflow

Vulnerable

pragma solidity ^0.7.0;               // pre-0.8

contract VulnerableCap {
    uint256 public totalSupply;
    uint256 constant CAP = 1_000_000e18;
    mapping(address => uint256) public balances;

    function mint(uint256 amount) external {
        totalSupply += amount;            // can overflow and wrap small
        require(totalSupply <= CAP);       // wrapped-small total slips under cap
        balances[msg.sender] += amount;
    }
}

Secure

pragma solidity ^0.8.20;

contract SecureCap {
    uint256 public totalSupply;
    uint256 constant CAP = 1_000_000e18;
    mapping(address => uint256) public balances;

    function mint(uint256 amount) external {
        require(amount > 0, "zero amount");
        // Check the cap BEFORE mutating; checked add can't wrap the total.
        require(totalSupply + amount <= CAP, "cap exceeded");
        totalSupply += amount;
        balances[msg.sender] += amount;
    }
}

4. Downcast Truncation

Vulnerable

pragma solidity ^0.8.20;              // 0.8 does NOT check casts

contract VulnerableCast {
    mapping(address => uint64) public shares;

    function deposit(uint256 amount) external {
        // Silent truncation: amount > type(uint64).max loses its high bits.
        shares[msg.sender] += uint64(amount);   // uint64(2^64) == 0
    }
}

Secure

pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/math/SafeCast.sol";

contract SecureCast {
    using SafeCast for uint256;
    mapping(address => uint64) public shares;

    function deposit(uint256 amount) external {
        // Reverts if amount does not fit in uint64 instead of truncating.
        shares[msg.sender] += amount.toUint64();
    }
    // Manual equivalent:
    // require(amount <= type(uint64).max, "downcast overflow");
    // shares[msg.sender] += uint64(amount);
}

5. Misused unchecked Block

Vulnerable

pragma solidity ^0.8.20;

contract VulnerableUnchecked {
    mapping(address => uint256) public accrued;

    function reward(uint256 userInput) external {
        unchecked {
            // Protection OFF on attacker-controlled input: this can wrap.
            accrued[msg.sender] += userInput;
        }
    }
}

Secure

pragma solidity ^0.8.20;

contract SecureUnchecked {
    mapping(address => uint256) public accrued;

    function reward(uint256 userInput) external {
        // Keep checked arithmetic on untrusted values.
        accrued[msg.sender] += userInput;
    }

    // unchecked is fine ONLY for provably-bounded quantities:
    function sum(uint256[] calldata xs) external pure returns (uint256 total) {
        for (uint256 i = 0; i < xs.length; ) {
            total += xs[i];          // checked: values are untrusted
            unchecked { ++i; }       // safe: i bounded by xs.length
        }
    }
}

6. Inline Assembly Arithmetic

Vulnerable

pragma solidity ^0.8.20;

contract VulnerableAsm {
    function addBalances(uint256 x, uint256 y) external pure returns (uint256 z) {
        assembly {
            z := add(x, y)          // Yul add() never checks: wraps silently
        }
    }
}

Secure

pragma solidity ^0.8.20;

contract SecureAsm {
    function addBalances(uint256 x, uint256 y) external pure returns (uint256 z) {
        assembly {
            z := add(x, y)
            if lt(z, x) { revert(0, 0) }   // overflow iff sum < an operand
        }
    }
    // Better still: prefer high-level `x + y`, which is checked on 0.8+.
}

7. Multiply-Before-Divide

Vulnerable

pragma solidity ^0.8.20;

contract VulnerableReward {
    uint256 constant PRECISION = 1e18;

    function reward(uint256 stake, uint256 rate) external pure returns (uint256) {
        // stake * rate can exceed 2^256 - 1 -> reverts (DoS) or, pre-0.8, wraps.
        return stake * rate / PRECISION;
    }
}

Secure

pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/math/Math.sol";

contract SecureReward {
    uint256 constant PRECISION = 1e18;

    function reward(uint256 stake, uint256 rate) external pure returns (uint256) {
        // 512-bit intermediate: no overflow, no divide-first precision loss.
        return Math.mulDiv(stake, rate, PRECISION);
    }
}

What Changed, and Why

IssueVulnerableSecure
Balance underflowPre-0.8 balance - amount wraps0.8 checked arithmetic / SafeMath reverts
Total overflowUnchecked length * amountChecked multiply, cap checked before mutate
Downcastuint64(amount) truncatesSafeCast.toUint64 reverts on overflow
uncheckedWraps attacker inputChecked on untrusted values; wrap only bounded counters
AssemblyRaw add, no checkManual overflow check or high-level +
Reward mathMultiply-before-divide overflowsMath.mulDiv full-precision

Next Steps