# 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
.add(), .sub() instead of +, -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:
unchecked carefully — only when provably safetype(uint256).maxunchecked blocks deserve extra scrutinyThe era of overflow exploits is over — if you use Solidity 0.8+ and avoid unnecessary unchecked blocks. Write safe code by default.