Qu'est-ce que le gas et pourquoi les frais varient dans l'EVM
1. Aperçu
Si vous avez déjà envoyé une transaction sur Ethereum et constaté que les frais varient de 1 à 10 fois, vous avez affaire au concept de gas. Cet article explique ce qu’est le gas, comment l’EVM calcule les frais avant et après EIP-1559, et pourquoi baseFee change à chaque bloc. Après la lecture, vous pourrez calculer les frais de transaction avec du code et savoir définir maxFeePerGas de manière raisonnable pour éviter que votre transaction soit suspendue ou que vous ne payiez trop.
2. Qu’est-ce que le gas ?
Le gas est une unité de mesure du travail de calcul que l’EVM (Ethereum Virtual Machine) doit effectuer pour exécuter une transaction. Chaque opcode (instruction machine virtuelle) a un coût en gas fixe — l’addition de deux entiers (ADD) est bon marché, écrire des données dans le storage (SSTORE) coûte des centaines de fois plus cher.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 public value; // position de storage
function increment() public {
value = value + 1; // ADD (~3 gaz) + SSTORE (~5000-20000 gaz)
}
}
La ligne value = value + 1 est en fait deux étapes : calcul arithmétique (bon marché) et écriture dans le storage (cher). C’est pourquoi l’optimisation des smart contracts se concentre généralement sur la réduction des opérations de lecture/écriture en storage, plutôt que sur la réduction des calculs.
Note : Le gas n’est pas de l’argent. Le gas est la « quantité de travail ». Les frais réels = gas utilisé × prix par gas (en ETH).
3. Gas limit, gas price et gas used
Trois concepts souvent confondus :
- Gas limit : le nombre maximum de gaz que vous autorisez la transaction à consommer. Le définir trop bas → la transaction revient avec l’erreur
out of gas. - Gas used : le nombre de gaz réellement consommé, toujours ≤ gas limit.
- Gas price : le prix par unité de gas, exprimé en
gwei(1 gwei = 10⁻⁹ ETH).
Frais de transaction (avant EIP-1559) = gasUsed × gasPrice
Par exemple, un simple transfert d’ETH coûte 21 000 gaz, si gasPrice = 30 gwei :
Frais = 21 000 × 30 gwei = 630 000 gwei = 0,00063 ETH
4. Ancien mécanisme : legacy gas price
Avant EIP-1559 (avant le fork London, 2021), les utilisateurs définissaient eux-mêmes gasPrice. Un prix élevé incitait les mineurs à inclure votre transaction dans un bloc plus tôt. Ce mécanisme ressemblait à une enchère à l’aveugle : vous ne saviez pas ce que les autres payaient, vous risquiez soit de trop payer, soit de trop peu payer et de devoir attendre.
5. EIP-1559 : base fee + priority fee
EIP-1559 sépare les frais en deux parties :
baseFee: calculé automatiquement par le réseau en fonction du taux d’utilisation du bloc précédent, est brûlé (détruit, n’va pas aux validateurs).priorityFee(pourboire) : montant supplémentaire que vous payez au validateur pour être priorisé.
L’utilisateur définit deux valeurs :
maxFeePerGas: le prix maximum que vous acceptez de payer par gas.maxPriorityFeePerGas: le pourboire maximum que vous acceptez de payer.
5.1. Formule pour calculer les frais effectifs
effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
frais_effectifs = gasUsed × effectiveGasPrice
5.2. Calculer les frais en Java
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
package dev.nonla.evm;
import java.math.BigInteger;
public class FeeCalculator {
public BigInteger effectiveGasPrice(
BigInteger baseFee,
BigInteger maxPriorityFeePerGas,
BigInteger maxFeePerGas) {
BigInteger desired = baseFee.add(maxPriorityFeePerGas);
return desired.min(maxFeePerGas); // ne dépasse jamais maxFeePerGas
}
public BigInteger totalFee(BigInteger gasUsed, BigInteger effectiveGasPrice) {
return gasUsed.multiply(effectiveGasPrice);
}
}
effectiveGasPriceprend la valeur minimale entre (baseFee + pourboire souhaité) et le plafondmaxFeePerGasque vous avez défini.totalFeemultiplie par le gas réellement consommé pour obtenir le montant en ETH à payer.
package dev.nonla.evm;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import static org.assertj.core.api.Assertions.assertThat;
class FeeCalculatorTest {
private final FeeCalculator calculator = new FeeCalculator();
@Test
void quandBaseFeeEtTipSontInferieursAuMax_alorsTipEstPaye() {
BigInteger baseFee = BigInteger.valueOf(20); // gwei
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(BigInteger.valueOf(22));
}
@Test
void quandBaseFeeAugmenteDePlusQueMax_alorsBloqueeAMaxFeePerGas() {
BigInteger baseFee = BigInteger.valueOf(60); // augmentation forte
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(maxFee); // bloquée à 50, pas de surpaiement
}
}
Le deuxième test illustre exactement pourquoi votre transaction reste suspendue (pending) lorsque le réseau est congestionné : baseFee augmente au-delà de maxFeePerGas que vous avez défini, les validateurs n’ont plus d’incitation à l’inclure car le pourboire réel reçu est trop faible.
6. Pourquoi base fee change à chaque bloc
baseFee n’est pas fixe — il s’ajuste automatiquement en fonction du taux d’utilisation du bloc précédent, selon une formule approximée :
baseFee_suivant = baseFee_actuel × (1 + 1/8 × (gasUsed - gasTarget) / gasTarget)
Où gasTarget est 50% du gasLimit du bloc (chaque bloc Ethereum a une limite de gaz d’environ 30 millions, une cible d’environ 15 millions).
- Si un bloc est plus plein que la cible →
baseFeeaugmente jusqu’à 12,5% maximum par bloc. - Si un bloc est plus vide que la cible →
baseFeebaisse jusqu’à 12,5% maximum par bloc.
package dev.nonla.evm;
import java.math.BigInteger;
public class BaseFeeAdjuster {
private static final BigInteger EIGHT = BigInteger.valueOf(8);
public BigInteger nextBaseFee(BigInteger currentBaseFee, long gasUsed, long gasTarget) {
long delta = gasUsed - gasTarget;
BigInteger change = currentBaseFee
.multiply(BigInteger.valueOf(delta))
.divide(BigInteger.valueOf(gasTarget))
.divide(EIGHT);
BigInteger next = currentBaseFee.add(change);
// baseFee ne peut jamais être négatif
return next.max(BigInteger.ONE);
}
}
@Test
void quandBlockEstPlusPleinQueTarget_alorsBaseFeeAugmente() {
BaseFeeAdjuster adjuster = new BaseFeeAdjuster();
BigInteger current = BigInteger.valueOf(100); // gwei
BigInteger next = adjuster.nextBaseFee(current, 30_000_000L, 15_000_000L);
// gasUsed = 2x cible -> augmentation proche de 12,5%
assertThat(next).isGreaterThan(current);
}
Ce mécanisme d’auto-ajustement explique pourquoi vous voyez baseFee fluctuer constamment tous les ~12 secondes (le temps d’un bloc) : le réseau s’équilibre lui-même en termes d’offre et de demande d’espace de bloc, ce ne sont pas les mineurs/validateurs qui définissent le prix arbitrairement.
7. Observer base fee et gas price via JSON-RPC
Vous pouvez récupérer baseFeePerGas directement depuis un nœud Ethereum :
curl -s -X POST https://rpc.ankr.com/eth \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}' \
| jq '.result.baseFeePerGas'
"0x9502f9000"
La valeur renvoyée est en hexadécimal (wei), vous la convertissez en gwei en divisant par 10⁹.
8. Estimer le gas avant d’envoyer une transaction avec Web3j
Au lieu de deviner gasLimit, laissez le nœud le calculer d’abord avec eth_estimateGas — évitez de le définir trop bas (revert) ou trop haut (geler temporairement l’ETH excédentaire).
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.12.2</version>
</dependency>
package dev.nonla.evm;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.EthEstimateGas;
import org.web3j.protocol.core.methods.request.Transaction;
import java.math.BigInteger;
public class GasEstimator {
private final Web3j web3j;
public GasEstimator(Web3j web3j) {
this.web3j = web3j;
}
public BigInteger estimate(String from, String to, String data) throws Exception {
Transaction tx = Transaction.createFunctionCallTransaction(
from, null, null, null, to, data);
EthEstimateGas response = web3j.ethEstimateGas(tx).send();
if (response.hasError()) {
throw new IllegalStateException("L'estimation a échoué : " + response.getError().getMessage());
}
return response.getAmountUsed();
}
}
ethEstimateGassimule la transaction sur l’état actuel et retourne le gas à utiliser.- En pratique, vous devriez ajouter un buffer de 10–20% à ce résultat avant de définir
gasLimit, car l’état peut changer entre le moment de l’estimation et le moment où la transaction s’exécute réellement.
9. Erreurs courantes
- Définir
gasLimitexactement égal à l’estimation : si la disposition du storage change légèrement (par exemple, un slot passe de 0 à une valeur non-zéro et consomme du gas différemment), la transaction risque facilement de revenir avecout of gas. Ajoutez toujours un buffer. - Définir
maxFeePerGastrop bas lors d’une congestion du réseau : votre transaction reste indéfiniment dans le mempool, sans timeout automatique — vous devez l’annuler en envoyant une nouvelle transaction avec le même nonce mais des frais plus élevés (replace-by-fee). - Confondre
gasPrice(legacy) avecmaxFeePerGas(EIP-1559) : envoyer une transaction de l’ancien style sur un réseau supportant 1559 fonctionne toujours, mais vous perdez le contrôle séparé du pourboire — vous risquez de trop payer par rapport à baseFee réel. - Ne pas négliger
priorityFee: si le pourboire est proche de 0 lors d’une congestion des blocs, les validateurs n’ont aucune incitation à inclure votre transaction rapidement, même simaxFeePerGasest élevé.
Note : Ne codez jamais en dur une
gasPricefixe dans le code de production. Appelez toujourseth_feeHistoryoueth_gasPricepour obtenir la valeur marchande du moment, puis appliquez la formule de la section 5.
10. Conclusion
Le gas est une unité de mesure du travail de calcul dans l’EVM ; les frais réels = gas utilisé × prix par gas. Avec EIP-1559, ce prix se scinde en baseFee qui s’ajuste automatiquement selon le taux d’utilisation du bloc et priorityFee que vous choisissez pour être priorisé. Comprendre cette formule vous aide à définir gasLimit/maxFeePerGas de manière raisonnable, éviter que votre transaction soit suspendue ou que vous ne payiez trop — et si besoin, vous pouvez entièrement écrire votre propre logique de calcul des frais en Java comme dans l’exemple ci-dessus pour l’intégrer dans un backend de suivi de portefeuille ou un tableau de bord de transactions.