# Écrire des Boucles Gas-Efficient en Solidity
Les boucles sont l'une des principales sources de consommation de gas dans les smart contracts. Une boucle mal optimisée peut rendre votre contrat inutilisable en production.
Le Problème des SLOAD Répétés
Chaque lecture depuis le storage coûte 2100 gas (cold) ou 100 gas (warm). Dans une boucle, cela s'accumule rapidement :
// ❌ Mauvais : SLOAD à chaque itération
uint256 sum;
for (uint256 i = 0; i < values.length; i++) {
sum += values[i];
}
Pattern 1 : Cache la Longueur du Tableau
// ✅ Bon : cache la longueur
uint256 length = values.length;
for (uint256 i = 0; i < length; i++) {
sum += values[i];
}
Économie : ~2000 gas pour 100 itérations.
Pattern 2 : Incrémentation Non-Checkée
Depuis Solidity 0.8.0, les débordements sont vérifiés par défaut. Dans une boucle avec un maximum connu, désactivez-les :
// ✅ Optimal
uint256 length = values.length;
for (uint256 i = 0; i < length;) {
sum += values[i];
unchecked { ++i; }
}
Économie : ~40 gas par itération.
Pattern 3 : Préférez ++i à i++
// ✅ ++i est plus économe (pas de variable temporaire)
unchecked { ++i; }
Pattern 4 : Évitez les Boucles Dynamiques
Ne bouclez JAMAIS sur un tableau dont la taille est contrôlée par les utilisateurs :
// ❌ DANGER : DoS possible
function distributeRewards(address[] calldata recipients) external {
for (uint256 i = 0; i < recipients.length; i++) {
// Un attaquant envoie 10000 adresses → out of gas
}
}
Solution : pagination ou pull pattern.
Pattern 5 : Accumulez Puis Écrivez
// ❌ Mauvais : SSTORE à chaque itération (20000 gas × n)
for (uint256 i = 0; i < length; i++) {
totalBalance += balances[i];
}
// ✅ Bon : une seule écriture finale
uint256 temp;
for (uint256 i = 0; i < length; i++) {
temp += balances[i];
}
totalBalance = temp;
Exemple Complet : Token Batch Transfer
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length <= 100, "Batch too large");
uint256 length = recipients.length;
uint256 totalAmount;
// Première passe : calcul du total
for (uint256 i = 0; i < length;) {
totalAmount += amounts[i];
unchecked { ++i; }
}
require(balanceOf[msg.sender] >= totalAmount, "Insufficient balance");
balanceOf[msg.sender] -= totalAmount;
// Deuxième passe : distribution
for (uint256 i = 0; i < length;) {
balanceOf[recipients[i]] += amounts[i];
unchecked { ++i; }
}
}
Benchmark
Pour un batch de 50 transferts :
- Version naïve : ~1.2M gas
- Version optimisée : ~450K gas
- Économie : 62%
Quand NE PAS Optimiser
Si votre boucle itère sur moins de 10 éléments fixes, l'optimisation ne vaut souvent pas la perte de lisibilité.
Ressources
- Testez avec
forge snapshot --diff
Maîtrisez ces patterns et vos utilisateurs vous remercieront avec leurs wallets.