# Overflow d'Entiers Avant et Après Solidity 0.8 — Ce Qui a Changé
L'overflow d'entiers a été l'une des vulnérabilités les plus coûteuses de l'histoire des smart contracts. Puis, en décembre 2020, Solidity 0.8.0 a tout changé. Retour sur cette révolution silencieuse.
L'Ère Pré-0.8 : Le Far West des Overflows
Avant Solidity 0.8, les opérations arithmétiques "wrappaient" silencieusement :
// Solidity 0.7.6
contract UnsafeArithmetic {
uint8 public value = 255;
function increment() public {
value = value + 1; // ❌ Becomes 0 (silent overflow!)
}
function decrement() public {
uint8 zero = 0;
zero = zero - 1; // ❌ Becomes 255 (silent underflow!)
}
}
Ce comportement venait de l'EVM elle-même : l'opcode ADD ne revert pas en cas d'overflow, il wrappe modulo 2^256.
Le Hack DAO 2016 : Pas un Overflow, Mais Presque
Bien que le fameux hack du DAO ne soit pas strictement un overflow, il illustre le danger des uint non vérifiés :
// Simplified vulnerable reentrancy + unchecked balance
mapping(address => uint) public balances;
function withdraw(uint amount) public {
// ❌ No underflow check!
balances[msg.sender] -= amount;
msg.sender.call{value: amount}("");
}
Si amount > balances[msg.sender], le solde devenait astronomique (wrap autour de 2^256).
L'Exploit BEC Token 2018 : Le Vrai Overflow
En avril 2018, le token BeautyChain (BEC) a été exploité via un overflow classique :
// Vulnerable code (simplified)
function batchTransfer(address[] _receivers, uint256 _value) public {
uint256 cnt = _receivers.length;
// ❌ VULNERABLE: amount can overflow!
uint256 amount = uint256(cnt) * _value;
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
for (uint256 i = 0; i < cnt; i++) {
balances[_receivers[i]] += _value;
}
}
L'exploit :
cnt = 2
_value = 2^255(la moitié de uint256)
amount = 2 * 2^255 = 2^256 = 0(overflow!)
- Le check
balances[msg.sender] >= 0passe toujours
- L'attaquant crée des tokens à l'infini
Impact : BEC a perdu 100% de sa valeur en quelques heures.
La Solution Pré-0.8 : SafeMath
OpenZeppelin a publié SafeMath, utilisé par 90% des projets :
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 overflow");
return a - b;
}
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;
}
}
contract SafeToken {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
balances[msg.sender] = balances[msg.sender].sub(amount); // ✅ Reverts on underflow
balances[to] = balances[to].add(amount); // ✅ Reverts on overflow
}
}
Problèmes de SafeMath :
- Verbeux (chaque opération devient un appel de fonction)
- Coût en gas (+20-50 gas par opération)
- Oublier
.add()une seule fois = vulnérabilité
Solidity 0.8.0 : Checked Arithmetic par Défaut
Le 16 décembre 2020, Solidity 0.8.0 introduit les checks automatiques :
// Solidity 0.8.0+
contract ModernArithmetic {
uint8 public value = 255;
function increment() public {
value = value + 1; // ✅ Reverts with "Arithmetic operation underflow or overflow"
}
function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // ✅ Automatic overflow check
}
}
Le compilateur insère automatiquement des checks après chaque opération arithmétique.
Bytecode Comparison
Regardons le bytecode généré :
// Solidity 0.7.6 (no SafeMath)
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
// Bytecode: ADD (1 opcode, 3 gas)
// Solidity 0.7.6 (with SafeMath)
function add(uint a, uint b) public pure returns (uint) {
return a.add(b);
}
// Bytecode: ~15 opcodes, ~50 gas (function call overhead)
// Solidity 0.8.0+
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
// Bytecode: ADD DUP1 DUP3 LT ISZERO PUSH2 JUMPI INVALID
// (7 opcodes, ~25 gas)
Résultat : 0.8.0+ est 2x plus rapide que SafeMath, mais un peu plus lent que 0.7.6 sans check.
Le Bloc unchecked : Quand Désactiver les Checks
Pour les cas où l'overflow est impossible (ou souhaité), utilisez unchecked :
contract GasOptimized {
// Example: loop counter (can't overflow in practice)
function sumArray(uint256[] memory arr) public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < arr.length;) {
sum += arr[i];
unchecked { i++; } // ✅ Save ~25 gas per iteration
}
return sum;
}
// Example: known safe arithmetic
function calculateFee(uint256 amount) public pure returns (uint256) {
unchecked {
// Fee is 0.3%, amount is capped at 1e24
// Max result: 1e24 * 3 / 1000 = 3e21 (safe)
return amount * 3 / 1000;
}
}
}
Gas saved : ~25 gas par opération non-checkée.
Cas Dangereux : Quand unchecked Devient une Vulnérabilité
Attention aux pièges :
contract VulnerableUnchecked {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
unchecked {
// ❌ VULNERABLE: can underflow!
balances[msg.sender] -= amount;
}
payable(msg.sender).transfer(amount);
}
}
Si amount > balances[msg.sender], le solde wrappe à un nombre énorme. L'utilisateur peut ensuite retirer tout l'Ether du contrat.
Règle d'or : n'utilisez unchecked que si vous pouvez prouver mathématiquement qu'un overflow est impossible.
Pattern Sûr : Checks Explicites + Unchecked
Le meilleur des deux mondes :
contract SafeUnchecked {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
unchecked {
balances[msg.sender] -= amount; // ✅ Safe: checked above
}
payable(msg.sender).transfer(amount);
}
}
Économie : ~20 gas par transaction (le require suffit, pas besoin du check automatique).
Cas Spéciaux : int256 et les Signed Integers
Les signed integers peuvent overflow dans les deux sens :
contract SignedOverflow {
function overflow() public pure returns (int8) {
int8 max = 127;
return max + 1; // ✅ Reverts in 0.8.0+ (was -128 in 0.7.6)
}
function underflow() public pure returns (int8) {
int8 min = -128;
return min - 1; // ✅ Reverts in 0.8.0+ (was 127 in 0.7.6)
}
}
Solidity 0.8 protège aussi contre ces cas.
Migration 0.7 → 0.8 : Checklist
Si vous migrez un projet existant :
/ et % (par design)// ⚠️ Division by zero still NOT checked!
function divide(uint256 a, uint256 b) public pure returns (uint256) {
return a / b; // Reverts, but with generic "Division or modulo by zero"
}
// Better: explicit check
function safeDivide(uint256 a, uint256 b) public pure returns (uint256) {
require(b > 0, "Division by zero");
return a / b;
}
Impact sur l'Écosystème
Depuis 0.8.0 (décembre 2020) :
- Baisse de 95% des exploits liés aux overflows
- Adoption de 0.8+ par 80% des nouveaux projets (source : Etherscan verified contracts)
- OpenZeppelin 5.0 a retiré SafeMath de son export par défaut
Les 5% d'exploits restants viennent de :
- Contrats legacy 0.7.6 non migrés
- Utilisation incorrecte de
unchecked
- Overflows dans des librairies assembly
Conclusion : Une Révolution Silencieuse
Solidity 0.8 a éliminé l'une des classes de bugs les plus dangereuses, sans que les développeurs aient à changer leur code. C'est un exemple rare de "secure by default" qui fonctionne.
Règles en 2026 :
- ✅ Utilisez toujours Solidity 0.8.0+
- ✅ Profitez des checks automatiques gratuits
- ✅ Utilisez
uncheckeduniquement avec preuve mathématique
- ❌ N'importez plus SafeMath (sauf pour div/mod avec messages custom)
L'ère des overflows silencieux est révolue. Bonne débarras.