Securite·11 min de lecture·Par Solingo

Integer Overflow Before and After Solidity 0.8 — What Changed

A deep dive into integer overflow vulnerabilities in Solidity, the SafeMath era, and how Solidity 0.8 changed everything with built-in overflow checks.

# Integer Overflow Before and After Solidity 0.8 — What Changed

Integer overflow bugs have caused some of the most devastating exploits in blockchain history. The BEC token exploit in 2018 wiped out over $1 billion in market cap due to a single overflow vulnerability. Understanding how Solidity's handling of integer overflow evolved is critical for every smart contract developer.

What Is Integer Overflow?

Integer overflow occurs when an arithmetic operation produces a result outside the range of the integer type:

// With uint8 (max value: 255)

uint8 value = 255;

value = value + 1; // What happens here?

// Before Solidity 0.8: wraps to 0

// After Solidity 0.8: reverts with panic

The Binary Explanation

uint8 max value: 11111111 (255 in decimal)

Add 1: + 1

Result: 100000000 (9 bits)

Truncated: 00000000 (back to 8 bits = 0)

Underflow works the same way in reverse:

uint8 value = 0;

value = value - 1; // Before 0.8: wraps to 255

Historical Overflow Exploits

The BEC Token Exploit (April 2018)

The Beauty Chain (BEC) token had this vulnerable code:

function batchTransfer(address[] _receivers, uint256 _value) public {

uint256 cnt = _receivers.length;

// THE BUG: amount overflows

uint256 amount = uint256(cnt) * _value;

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

balances[msg.sender] -= amount;

for (uint i = 0; i < cnt; i++) {

balances[_receivers[i]] += _value;

}

}

The exploit:

// Attacker calls with:

// _receivers = [addr1, addr2] (cnt = 2)

// _value = 2^255 (very large number)

// Calculation:

// amount = 2 * 2^255 = 2^256 = 0 (overflow!)

// require(balances[msg.sender] >= 0) passes

// balances[msg.sender] -= 0 (no change)

// Each receiver gets 2^255 tokens!

The attacker created billions of tokens from nothing, immediately crashing the token's value.

The SMT Token Exploit (Same Day!)

Similar overflow in transferProxy:

function transferProxy(

address _from,

address _to,

uint256 _value,

uint256 _fee

) public returns (bool) {

uint256 total = _value + _fee; // OVERFLOW HERE

require(balances[_from] >= total);

balances[_from] -= total;

balances[_to] += _value;

balances[msg.sender] += _fee;

}

The exploit:

// _value = 2^256 - 100

// _fee = 200

// total = (2^256 - 100) + 200 = 100 (overflow!)

// require passes with just 100 tokens

// But _to receives 2^256 - 100 tokens!

The SafeMath Era (Solidity 0.4.x - 0.7.x)

After these exploits, the community adopted OpenZeppelin's SafeMath library as standard practice:

library SafeMath {

function add(uint256 a, uint256 b) internal pure returns (uint256) {

uint256 c = a + b;

require(c >= a, "SafeMath: addition overflow");

return c;

}

function sub(uint256 a, uint256 b) internal pure returns (uint256) {

require(b <= a, "SafeMath: subtraction underflow");

uint256 c = a - b;

return c;

}

function mul(uint256 a, uint256 b) internal pure returns (uint256) {

if (a == 0) return 0;

uint256 c = a * b;

require(c / a == b, "SafeMath: multiplication overflow");

return c;

}

function div(uint256 a, uint256 b) internal pure returns (uint256) {

require(b > 0, "SafeMath: division by zero");

uint256 c = a / b;

return c;

}

}

Using SafeMath

pragma solidity ^0.7.0;

import "@openzeppelin/contracts/math/SafeMath.sol";

contract Token {

using SafeMath for uint256;

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) public {

balances[msg.sender] = balances[msg.sender].sub(amount);

balances[to] = balances[to].add(amount);

}

// The BEC exploit would now fail:

function batchTransfer(address[] memory receivers, uint256 value) public {

uint256 cnt = receivers.length;

uint256 amount = uint256(cnt).mul(value); // Reverts on overflow!

require(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);

}

}

}

SafeMath Limitations

  • Easy to forget: Developers had to remember to use it everywhere
  • Gas cost: Additional function calls and checks cost ~20-30% more gas
  • Verbose syntax: .add(), .sub() instead of +, -
  • Mixed usage bugs: Mixing SafeMath and raw operators was dangerous
  • Solidity 0.8.0: Built-in Overflow Checks

    In August 2020, Solidity 0.8.0 introduced automatic overflow/underflow checks:

    pragma solidity ^0.8.0;
    
    

    contract Modern {

    uint256 public value;

    function increment() public {

    value = value + 1; // Automatically reverts on overflow!

    }

    function decrement() public {

    value = value - 1; // Automatically reverts on underflow!

    }

    // No SafeMath needed!

    function multiply(uint256 a, uint256 b) public pure returns (uint256) {

    return a * b; // Safe by default

    }

    }

    How It Works

    The compiler inserts overflow checks directly into the bytecode:

    // Source code
    

    uint256 result = a + b;

    // Generated bytecode (simplified)

    result = a + b;

    if (result < a) revert Panic(0x11); // 0x11 = arithmetic overflow

    This happens at the opcode level, making it more gas-efficient than SafeMath:

    | Operation | Solidity 0.7 + SafeMath | Solidity 0.8 Built-in | Gas Saved |

    |-----------|------------------------|---------------------|-----------|

    | Addition | ~200 gas | ~150 gas | 25% |

    | Multiplication | ~300 gas | ~200 gas | 33% |

    | Subtraction | ~200 gas | ~150 gas | 25% |

    The unchecked Block

    Sometimes you know overflow is impossible or even desired. Solidity 0.8 provides the unchecked block:

    function efficientLoop() public {
    

    uint256 sum = 0;

    // Loop counter can't overflow in practice

    for (uint256 i = 0; i < 100; ) {

    sum += i;

    unchecked {

    i++; // Save gas by skipping overflow check

    }

    }

    }

    When to Use unchecked

    Safe uses:

    // 1. Loop counters with known bounds
    

    for (uint i = 0; i < array.length; ) {

    // ... process array[i]

    unchecked { i++; }

    }

    // 2. Calculations proven safe by prior checks

    function divide(uint256 a, uint256 b) public pure returns (uint256) {

    require(b != 0, "Division by zero");

    unchecked {

    return a / b; // Safe because b != 0

    }

    }

    // 3. Intentional wrapping behavior

    function hash(uint256 value) public pure returns (uint8) {

    unchecked {

    return uint8(value); // Intentional truncation

    }

    }

    // 4. Gas optimization in proven-safe math

    function calculateFee(uint256 amount) public pure returns (uint256) {

    // amount * 30 / 10000 for 0.3% fee

    // Can't overflow because amount <= type(uint256).max

    unchecked {

    return amount * 30 / 10000;

    }

    }

    Dangerous uses (NEVER do this):

    // DANGEROUS: User-controlled values
    

    function badTransfer(address to, uint256 amount, uint256 fee) public {

    unchecked {

    uint256 total = amount + fee; // Could overflow!

    balances[msg.sender] -= total;

    balances[to] += amount;

    }

    }

    // DANGEROUS: No bounds checking

    function badBatchTransfer(address[] memory to, uint256 amount) public {

    unchecked {

    uint256 total = to.length * amount; // Could overflow!

    require(balances[msg.sender] >= total);

    }

    }

    Gas Optimization Patterns

    Pattern 1: Unchecked Loop Increments

    // Before
    

    function sumArray(uint256[] memory arr) public pure returns (uint256) {

    uint256 sum = 0;

    for (uint256 i = 0; i < arr.length; i++) {

    sum += arr[i];

    }

    return sum;

    }

    // After (saves ~100 gas per iteration)

    function sumArrayOptimized(uint256[] memory arr) public pure returns (uint256) {

    uint256 sum = 0;

    for (uint256 i = 0; i < arr.length; ) {

    sum += arr[i];

    unchecked { i++; }

    }

    return sum;

    }

    Pattern 2: Safe Percentage Calculations

    function calculatePercentage(uint256 amount, uint256 bps)
    

    public

    pure

    returns (uint256)

    {

    // bps = basis points (1% = 100 bps)

    // This can't overflow if bps <= 10000

    require(bps <= 10000, "Invalid bps");

    unchecked {

    return (amount * bps) / 10000;

    }

    }

    Pattern 3: Decrements in Bounded Loops

    function processReversed(uint256[] memory arr) public {
    

    for (uint256 i = arr.length; i > 0; ) {

    unchecked { i--; }

    // Process arr[i]

    }

    }

    Migrating from SafeMath to 0.8

    If you're upgrading a Solidity 0.7 contract:

    // Before (0.7)
    

    pragma solidity ^0.7.0;

    import "@openzeppelin/contracts/math/SafeMath.sol";

    contract OldToken {

    using SafeMath for uint256;

    mapping(address => uint256) balances;

    function transfer(address to, uint256 amount) public {

    balances[msg.sender] = balances[msg.sender].sub(amount);

    balances[to] = balances[to].add(amount);

    }

    }

    // After (0.8) - simple replacement

    pragma solidity ^0.8.0;

    contract NewToken {

    mapping(address => uint256) balances;

    function transfer(address to, uint256 amount) public {

    balances[msg.sender] -= amount; // Built-in check

    balances[to] += amount; // Built-in check

    }

    }

    Testing for Overflow Vulnerabilities

    Always test edge cases:

    import "forge-std/Test.sol";
    
    

    contract OverflowTest is Test {

    function testOverflowReverts() public {

    uint256 max = type(uint256).max;

    vm.expectRevert(); // Expect panic

    uint256 result = max + 1;

    }

    function testUnderflowReverts() public {

    uint256 zero = 0;

    vm.expectRevert();

    uint256 result = zero - 1;

    }

    function testUncheckedAllowsOverflow() public {

    uint8 max = type(uint8).max; // 255

    uint8 result;

    unchecked {

    result = max + 1; // Wraps to 0

    }

    assertEq(result, 0);

    }

    }

    Conclusion

    Solidity 0.8's automatic overflow protection is one of the most important security improvements in smart contract development. Key takeaways:

  • Use Solidity 0.8+ for all new projects
  • Remove SafeMath — it's redundant and costs extra gas
  • Use unchecked carefully — only when provably safe
  • Test edge cases — especially with type(uint256).max
  • Audit carefullyunchecked blocks deserve extra scrutiny
  • The era of overflow exploits is over — if you use Solidity 0.8+ and avoid unnecessary unchecked blocks. Write safe code by default.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement