Integer over/underflow (Solidity)
TL;DR: Pre-0.8.0 Solidity wraps silently on arithmetic overflow; 0.8+ reverts. Still a live class of bugs in legacy contracts, in
unchecked { ... }blocks, in inline assembly, and in downcasts (uint256 -> uint96).
What it is
EVM integers are fixed-width (uint8 … uint256, signed variants). Arithmetic that exceeds the type’s range either wraps modulo 2^N (legacy / unchecked) or reverts (Solidity ≥ 0.8.0 default). When wrap happens silently, an attacker can mint near-infinite balance with balance - 1 underflowing or pass a length check by overflowing a multiplication used in array-size math.
Preconditions / where it applies
- Solidity compiler
< 0.8.0without OpenZeppelin SafeMath - Solidity ≥ 0.8.0 inside
unchecked { ... }blocks (used for gas optimisation) - Inline
assemblyblocks — Yul does not check - Downcasts:
uint96(largeUint256)truncates without revert - Multiplication of user-controlled values (fees, prices, supply caps)
Technique
- Classic underflow withdraw. Pre-0.8 token:
function transfer(address to, uint256 v) public { balances[msg.sender] -= v; // underflows if v > balance balances[to] += v; }Calling with
v = 1while balance is 0 wrapsbalances[msg.sender]to2^256 - 1. uncheckedblock in modern contract. Devs adduncheckedaround loop counters; if any user input slips into the block (a length or index calculation), wrap returns.- Downcast laundering. Vault stores
uint256balance but a withdraw helper casts touint96for packing:uint96 amt = uint96(_amount); // truncates high bits token.transfer(msg.sender, amt);Attacker passes
_amount = 2^96 + smallValueto withdrawsmallValuewhile debiting the full_amountagainst accounting that uses the un-truncateduint256. - Signed shenanigans.
int256minimum (-2^255) negated stays negative; comparisons that assume positivity fail.
PoC harness with Foundry:
function testUnderflow() public {
vm.prank(attacker);
vulnToken.transfer(address(0xdead), 1);
assertEq(vulnToken.balanceOf(attacker), type(uint256).max);
}
Detection and defence
- Pin compiler to
pragma solidity ^0.8.20;or newer and avoiduncheckedunless the bound is proven. - Use OpenZeppelin
SafeCastfor downcasts (SafeCast.toUint96(x)reverts on truncation). - Static analysis: Slither
safe-math,incorrect-downcast; Mythril symbolic exec; Halmos / Certora for formal bounds. - Code review: any
assembly, anyunchecked, any cast to a narrower type is a hotspot.
Related: reentrancy, access-control-bugs, solidity-basics.
References
- SWC-101 Integer Overflow and Underflow — registry entry
- Solidity 0.8.0 release notes — checked arithmetic
- OpenZeppelin SafeCast — checked downcasts
- Trail of Bits — unchecked math pitfalls — modern bug patterns