Sécurité·7 min de lecture·Par Solingo

Débordement et Sous-débordement d'Entiers en Solidity — Protection des Calculs

Les débordements d'entiers ont causé des pertes de millions de dollars. Apprenez comment Solidity 0.8+ les prévient automatiquement et pourquoi vous devez quand même comprendre ce risque.

# Débordement et Sous-débordement d'Entiers en Solidity — Protection des Calculs

Les bugs de débordement d'entiers ont historiquement été l'une des vulnérabilités les plus coûteuses en développement de smart contracts. Bien que Solidity 0.8.0 ait introduit une protection par défaut, comprendre comment et pourquoi ces vulnérabilités se produisent reste essentiel pour tout développeur.

Qu'est-ce qu'un Débordement/Sous-débordement d'Entiers ?

En Solidity, les entiers ont des tailles fixes (uint8, uint256, etc.). Quand un calcul dépasse la valeur maximale ou descend en dessous de la valeur minimale, il "boucle" vers l'autre extrémité.

Débordement (Overflow)

uint8 public maxValue = 255; // Valeur max pour uint8

function overflow() public {

maxValue = maxValue + 1; // Solidity < 0.8.0 : boucle vers 0

}

Avant Solidity 0.8.0 : 255 + 1 = 0

Avec Solidity 0.8.0+ : ❌ Transaction revert

Sous-débordement (Underflow)

uint8 public minValue = 0;

function underflow() public {

minValue = minValue - 1; // Solidity < 0.8.0 : boucle vers 255

}

Avant Solidity 0.8.0 : 0 - 1 = 255

Avec Solidity 0.8.0+ : ❌ Transaction revert

Cas Réel : Exploit BeautyChain (BEC)

En avril 2018, le token BeautyChain (BEC) a subi un débordement catastrophique.

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

uint256 cnt = _receivers.length;

uint256 amount = uint256(cnt) * _value; // ❌ DÉBORDEMENT ICI

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

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

balances[_receivers[i]] += _value;

}

balances[msg.sender] -= amount;

}

L'attaque :

// cnt = 2

// _value = 2^255

// amount = 2 * 2^255 = 2^256 (déborde vers 0)

L'attaquant a créé des tokens à partir de rien car amount = 0 a passé le require, mais le transfert réel a transféré 2^255 tokens à chaque destinataire.

Impact : Le trading du token BEC a été suspendu, valeur de marché anéantie.

Protection dans Solidity 0.8.0+

Depuis Solidity 0.8.0, toutes les opérations arithmétiques revert automatiquement en cas de débordement/sous-débordement.

// Solidity ^0.8.0

uint8 x = 255;

x = x + 1; // ❌ Revert : "Arithmetic operation underflowed or overflowed"

Mode Unchecked

Si vous avez besoin d'arithmétique bouclante pour l'optimisation, utilisez unchecked {}.

function safeIncrement(uint8 x) public pure returns (uint8) {

unchecked {

return x + 1; // Boucle vers 0 si x = 255

}

}

⚠️ N'utilisez unchecked que si vous avez vérifié que le débordement est impossible ou intentionnel.

Cas d'usage légitime :

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

// ...

unchecked { ++i; } // Sûr car i ne dépassera jamais uint256.max

}

Protection dans Solidity < 0.8.0 : SafeMath

Avant 0.8.0, les développeurs utilisaient la bibliothèque SafeMath d'OpenZeppelin.

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

contract OldToken {

using SafeMath for uint256;

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) public {

balances[msg.sender] = balances[msg.sender].sub(amount); // Revert en cas de sous-débordement

balances[to] = balances[to].add(amount); // Revert en cas de débordement

}

}

SafeMath ajoute des vérifications à chaque opération :

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");

return a - b;

}

Depuis Solidity 0.8.0, SafeMath n'est plus nécessaire car la protection est intégrée.

Types d'Entiers en Solidity

| Type | Taille | Plage (unsigned) | Plage (signed) |

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

| uint8 / int8 | 8 bits | 0 à 255 | -128 à 127 |

| uint16 / int16 | 16 bits | 0 à 65,535 | -32,768 à 32,767 |

| uint32 / int32 | 32 bits | 0 à 4,294,967,295 | -2,147,483,648 à 2,147,483,647 |

| uint256 / int256 | 256 bits | 0 à 2^256-1 | -2^255 à 2^255-1 |

uint est un alias pour uint256.

Vulnérabilités Communes

1. Multiplication Avant Division

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

return (amount * REWARD_MULTIPLIER) / PRECISION; // ❌ Peut déborder

}

Solution :

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

return (amount / PRECISION) * REWARD_MULTIPLIER; // ✅ Diviser d'abord

}

Ou utilisez des mathématiques de point fixe (bibliothèques comme PRBMath).

2. Cast Non Sécurisé vers un Type Plus Petit

function unsafeCast(uint256 x) public pure returns (uint8) {

return uint8(x); // ❌ Tronque silencieusement

}

// unsafeCast(256) = 0

// unsafeCast(257) = 1

Solution :

function safeCast(uint256 x) public pure returns (uint8) {

require(x <= type(uint8).max, "Value too large");

return uint8(x);

}

Ou utilisez la bibliothèque SafeCast d'OpenZeppelin :

import "@openzeppelin/contracts/utils/math/SafeCast.sol";

function safeCast(uint256 x) public pure returns (uint8) {

return SafeCast.toUint8(x); // Revert si > 255

}

3. Soustraction d'Horodatages

uint256 public lastUpdate;

function timeSinceUpdate() public view returns (uint256) {

return block.timestamp - lastUpdate; // ❌ Sous-déborde si lastUpdate > block.timestamp

}

Solution :

function timeSinceUpdate() public view returns (uint256) {

if (block.timestamp < lastUpdate) return 0;

return block.timestamp - lastUpdate;

}

Tests pour Débordement/Sous-débordement

Avec Foundry

// test/Overflow.t.sol

import "forge-std/Test.sol";

contract OverflowTest is Test {

function testOverflowReverts() public {

uint8 x = 255;

vm.expectRevert(); // Attend un revert

x = x + 1;

}

function testUncheckedOverflow() public {

uint8 x = 255;

unchecked {

x = x + 1;

}

assertEq(x, 0); // Boucle vers 0

}

}

Avec Hardhat

const { expect } = require("chai");

describe("Overflow", function () {

it("Should revert on overflow", async function () {

const Contract = await ethers.getContractFactory("MyContract");

const contract = await Contract.deploy();

await expect(

contract.overflow()

).to.be.revertedWith("Arithmetic operation overflowed");

});

});

Bonnes Pratiques

Utilisez Solidity 0.8.0 ou supérieur — protection automatique

Évitez unchecked sauf cas d'usage prouvé — documentez pourquoi c'est sûr

Utilisez SafeCast pour les conversions de type

Divisez avant de multiplier lorsque c'est possible

Testez les valeurs limites (0, max, max-1, max+1)

Utilisez des bibliothèques de point fixe pour les mathématiques de précision (PRBMath)

Auditez le code legacy < 0.8.0 — ajoutez SafeMath si nécessaire

Outils de Détection

  • Slither : Détecte les opérations arithmétiques non vérifiées
  • Mythril : Analyse symbolique pour débordements
  • Echidna : Fuzzing pour trouver des valeurs d'entrée déclenchant des débordements
slither contracts/MyContract.sol --detect unchecked-lowlevel

Conclusion

Bien que Solidity 0.8.0+ ait largement éliminé les débordements accidentels, comprendre cette vulnérabilité reste crucial pour :

  • Auditer du code legacy
  • Utiliser unchecked en toute sécurité
  • Écrire des conversions de type sécurisées
  • Comprendre les mathématiques de point fixe

Chez Solingo, vous pratiquez la détection de débordements à travers des exercices interactifs basés sur des exploits réels comme BeautyChain et PoWHC.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement