O que é Gas e por que as taxas mudam no EVM
1. Visão geral
Se você já enviou uma transação na Ethereum e viu as taxas variar 10 vezes de uma hora para outra, você está tocando no conceito de gas. Este artigo explica o que é gas, como a EVM calcula as taxas antes e depois do EIP-1559, e por que o baseFee muda a cada bloco. Depois de ler, você conseguirá calcular a taxa de transação via código e saberá definir maxFeePerGas adequadamente para evitar que a transação fique presa ou você pague acima da conta.
2. O que é Gas
Gas é a unidade que mede o trabalho computacional que a EVM (Ethereum Virtual Machine) precisa realizar para executar uma transação. Cada opcode (instrução de máquina virtual) tem um custo de gas fixo — uma soma inteira (ADD) é barata, mas escrever dados no storage (SSTORE) custa centenas de vezes mais.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 public value; // slot de armazenamento
function increment() public {
value = value + 1; // ADD (~3 gas) + SSTORE (~5000-20000 gas)
}
}
A linha value = value + 1 na verdade é dois passos: cálculo aritmético (barato) e escrita no storage (caro). Por isso otimizações em smart contracts costumam focar em reduzir o número de leitura/escrita no storage, não em reduzir operações aritméticas.
Nota: Gas não é dinheiro. Gas é “quantidade de trabalho”. Taxa real = gas usado × preço por gás (em ETH).
3. Gas limit, gas price e gas used
Três conceitos frequentemente confundidos:
- Gas limit: o máximo de gas que você permite a transação gastar. Se definir muito baixo → a transação sofre revert com erro
out of gas. - Gas used: o gas realmente gasto, sempre ≤ gas limit.
- Gas price: o preço por unidade de gas, medido em
gwei(1 gwei = 10⁻⁹ ETH).
Taxa de transação (antes do EIP-1559) = gasUsed × gasPrice
Por exemplo, uma transferência simples de ETH custa 21.000 gas, se gasPrice = 30 gwei:
Taxa = 21.000 × 30 gwei = 630.000 gwei = 0.00063 ETH
4. Mecanismo antigo: legacy gas price
Antes do EIP-1559 (antes do fork Londres em 2021), o usuário definia gasPrice. Preço mais alto significava que miners priorizavam colocar a transação no bloco antes. Esse mecanismo era como um leilão às cegas: você não sabia quanto os concorrentes pagavam, era fácil pagar demais ou de menos e ter que esperar.
5. EIP-1559: base fee + priority fee
EIP-1559 divide a taxa em duas partes:
baseFee: calculada automaticamente pela rede baseada no preenchimento do bloco anterior, é queimada (não vai para o validador).priorityFee(tip): você paga extra ao validador para ter prioridade.
O usuário define dois valores:
maxFeePerGas: o preço máximo por gás que você aceita pagar.maxPriorityFeePerGas: o tip máximo que você aceita pagar.
5.1. Fórmula para calcular a taxa real
effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
taxa real = gasUsed × effectiveGasPrice
5.2. Calcular taxa em 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); // nunca vai exceder maxFeePerGas
}
public BigInteger totalFee(BigInteger gasUsed, BigInteger effectiveGasPrice) {
return gasUsed.multiply(effectiveGasPrice);
}
}
effectiveGasPricepega o menor valor entre (baseFee + tip desejado) e o limitemaxFeePerGasque você definiu.totalFeemultiplica pelo gas realmente usado para calcular o ETH a pagar.
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 quandoBaseFeeComTipMenorQueMax_entaoUsaTipCompleto() {
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 quandoBaseFeeExcedeMax_entaoFixaEmMaxFeePerGas() {
BigInteger baseFee = BigInteger.valueOf(60); // sobe bastante
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(maxFee); // fixado em 50, não paga além
}
}
O segundo teste ilustra exatamente por que sua transação fica presa (pending) quando a rede está congestionada: o baseFee sobe acima de maxFeePerGas que você definiu, e o validador não tem incentivo para aceitar porque o tip real fica muito baixo.
6. Por que base fee muda a cada bloco
baseFee não é fixo — ajusta-se automaticamente conforme o preenchimento do bloco anterior, baseado na fórmula aproximada:
baseFee_próximo = baseFee_atual × (1 + 1/8 × (gasUsed - gasTarget) / gasTarget)
Onde gasTarget é 50% do gasLimit do bloco (cada bloco Ethereum tem um gas limit em torno de 30 milhões, com target em torno de 15 milhões).
- Bloco mais cheio que o target →
baseFeesobe no máximo 12,5% por bloco. - Bloco mais vazio que o target →
baseFeecai no máximo 12,5% por bloco.
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 nunca fica negativo
return next.max(BigInteger.ONE);
}
}
@Test
void quandoBlocoMaisCheioqueMetas_entaoBaseFeeAumenta() {
BaseFeeAdjuster adjuster = new BaseFeeAdjuster();
BigInteger current = BigInteger.valueOf(100); // gwei
BigInteger next = adjuster.nextBaseFee(current, 30_000_000L, 15_000_000L);
// gasUsed = 2x target -> sobe perto de 12.5%
assertThat(next).isGreaterThan(current);
}
Esse mecanismo de auto-ajuste explica por que você vê baseFee pulando continuamente a cada ~12 segundos (o tempo de um bloco): a rede está balanceando automaticamente a oferta e demanda de espaço em bloco, não é o miner/validador definindo preço arbitrariamente.
7. Observar base fee e gas price via JSON-RPC
Você pode obter baseFeePerGas diretamente de um nó 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"
O valor retornado está em hex (wei), você converte para gwei dividindo por 10⁹.
8. Estimar gas antes de enviar transação com Web3j
Em vez de adivinhar gasLimit, deixe o nó calcular antecipadamente com eth_estimateGas — evita definir muito baixo (revert) ou muito alto (bloqueia ETH em excesso temporariamente).
<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("Estimativa falhou: " + response.getError().getMessage());
}
return response.getAmountUsed();
}
}
ethEstimateGassimula a transação no estado atual e retorna o gas necessário.- Na prática, você deve adicionar um buffer de 10–20% a esse resultado antes de definir
gasLimit, porque o estado pode mudar entre o momento da estimativa e a execução real da transação.
9. Erros comuns
- Definir
gasLimitexatamente igual ao resultado de estimate: se o layout de storage mudar levemente (por exemplo, slot de 0 → non-zero custa gas diferente), a transação pode sofrer revert porout of gas. Sempre adicione um buffer. - Definir
maxFeePerGasmuito baixo quando a rede está congestionada: a transação fica no mempool indefinidamente, sem timeout automático — você precisa cancelá-la manualmente enviando uma transação com o mesmo nonce mas fee mais alta (replace-by-fee). - Confundir
gasPrice(legacy) commaxFeePerGas(EIP-1559): enviar uma transação do tipo antigo em uma rede que já suporta 1559 ainda funciona, mas você perde o controle sobre o tip separado — a carteira pode pagar acima dobaseFeereal. - Não considerar
priorityFeemuito baixo: se a taxa de prioridade está perto de 0 enquanto o bloco está cheio, o validador não tem incentivo para incluir a transação logo mesmo quemaxFeePerGasseja alto.
Nota: Não faça hard-code de um
gasPricefixo no código de produção. Sempre chameeth_feeHistoryoueth_gasPricepara obter valores de mercado no momento, depois aplique a fórmula da seção 5.
10. Conclusão
Gas é a unidade que mede o trabalho computacional na EVM; a taxa real = gas usado × preço por gás. Com EIP-1559, esse preço se divide em baseFee que se auto-ajusta conforme o preenchimento do bloco e priorityFee que você escolhe para prioridade. Entender bem essa fórmula ajuda você a definir gasLimit/maxFeePerGas adequadamente, evitando transações presas ou pagamento excessivo — e se necessário, você pode completamente escrever sua própria lógica de cálculo de taxa em Java como nos exemplos acima para integrar no backend de monitoramento de carteira ou painel de transações.