¿Qué es Gas y por qué las tarifas cambian en EVM?
1. Resumen general
Si alguna vez enviaste una transacción en Ethereum y viste que las tarifas a veces eran baratas y otras 10 veces más caras, estás tocando el concepto de gas. Este artículo explica qué es el gas, cómo EVM calcula las tarifas antes y después de EIP-1559, por qué baseFee cambia en cada bloque. Después de leerlo, podrás calcular las tarifas de transacción con código y saber establecer maxFeePerGas de manera razonable para evitar que tus transacciones se queden colgadas o pagues de más.
2. ¿Qué es Gas?
Gas es la unidad de medida del trabajo computacional que EVM (Ethereum Virtual Machine) debe realizar para ejecutar una transacción. Cada opcode (instrucción de máquina virtual) tiene un costo de gas fijo — sumar enteros (ADD) es barato, escribir datos en storage (SSTORE) cuesta cientos de veces más.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 public value; // ranura de storage
function increment() public {
value = value + 1; // ADD (~3 gas) + SSTORE (~5000-20000 gas)
}
}
La línea value = value + 1 es en realidad dos pasos: cálculo aritmético (barato) y escribir en storage (caro). Esta es la razón por la que optimizar smart contracts generalmente se enfoca en reducir el número de lectura/escritura en storage, no en reducir operaciones de cálculo.
Nota: Gas no es dinero. Gas es “cantidad de trabajo”. La tarifa real = gas usado × precio por gas (medido en ETH).
3. Gas limit, gas price y gas used
Tres conceptos que se confunden fácilmente:
- Gas limit: el gas máximo que permites que la transacción gaste. Si lo estableces muy bajo → la transacción se revierte con un error
out of gas. - Gas used: la cantidad real de gas gastado, siempre ≤ gas limit.
- Gas price: el precio por unidad de gas, medido en
gwei(1 gwei = 10⁻⁹ ETH).
Tarifa de transacción (antes de EIP-1559) = gasUsed × gasPrice
Por ejemplo, una transferencia de ETH simple cuesta 21.000 gas, si gasPrice = 30 gwei:
Tarifa = 21.000 × 30 gwei = 630.000 gwei = 0.00063 ETH
4. Mecanismo antiguo: legacy gas price
Antes de EIP-1559 (antes del fork London, 2021), los usuarios establecían gasPrice por sí mismos. Un precio más alto significaba que los mineros priorizaban poner tu transacción en un bloque antes. Este mecanismo era como una subasta ciega: no sabías cuánto pagaba la competencia, fácilmente pagabas demasiado o muy poco y tenías que esperar.
5. EIP-1559: base fee + priority fee
EIP-1559 divide la tarifa en dos partes:
baseFee: calculado automáticamente por la red basándose en la congestión del bloque anterior, se quema (no va al validador).priorityFee(tip): pagas extra al validador para tener prioridad.
El usuario establece dos valores:
maxFeePerGas: el precio máximo que aceptas pagar por gas.maxPriorityFeePerGas: el tip máximo que aceptas pagar.
5.1. Fórmula para calcular la tarifa real
effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
tarifa real = gasUsed × effectiveGasPrice
5.2. Calcular tarifa 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); // nunca exceder maxFeePerGas
}
public BigInteger totalFee(BigInteger gasUsed, BigInteger effectiveGasPrice) {
return gasUsed.multiply(effectiveGasPrice);
}
}
effectiveGasPricetoma el menor entre (baseFee + tip deseado) y el límite máximomaxFeePerGasque estableces.totalFeemultiplica por el gas realmente usado para obtener el ETH que debes 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 khiBaseFeeCongTipNhoHonMax_thiDungTipDayDu() {
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 khiBaseFeeTangVuotMax_thiBiGhimOMaxFeePerGas() {
BigInteger baseFee = BigInteger.valueOf(60); // aumento fuerte
BigInteger tip = BigInteger.valueOf(2);
BigInteger maxFee = BigInteger.valueOf(50);
BigInteger price = calculator.effectiveGasPrice(baseFee, tip, maxFee);
assertThat(price).isEqualTo(maxFee); // pegado a 50, no pagar de más
}
}
El segundo test ilustra exactamente por qué tu transacción queda colgada (pending) cuando la red está congestionada: baseFee sube por encima de maxFeePerGas que estableciste, los validadores no tienen incentivo para aceptarla porque el tip real es demasiado bajo.
6. Por qué base fee cambia en cada bloque
baseFee no es fijo — se ajusta automáticamente según la congestión del bloque anterior, basándose en una fórmula aproximada:
baseFee_siguiente = baseFee_actual × (1 + 1/8 × (gasUsed - gasTarget) / gasTarget)
Donde gasTarget es el 50% del gasLimit del bloque (cada bloque de Ethereum tiene un gas limit aproximado de 30 millones, target aproximadamente 15 millones).
- Bloque más lleno que el target →
baseFeeaumenta máximo 12.5% por bloque. - Bloque más vacío que el target →
baseFeedisminuye máximo 12.5% por bloque.
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 es negativo
return next.max(BigInteger.ONE);
}
}
@Test
void khiBlockDayHonTarget_thiBaseFeeTang() {
BaseFeeAdjuster adjuster = new BaseFeeAdjuster();
BigInteger current = BigInteger.valueOf(100); // gwei
BigInteger next = adjuster.nextBaseFee(current, 30_000_000L, 15_000_000L);
// gasUsed = 2x target -> aumenta cerca de 12.5%
assertThat(next).isGreaterThan(current);
}
Este mecanismo de auto-ajuste explica por qué ves que baseFee salta continuamente cada ~12 segundos (el tiempo de un bloque): la red está equilibrando automáticamente la oferta y demanda de espacio en bloques, no es que los mineros/validadores establezcan el precio arbitrariamente.
7. Observar base fee y gas price a través de JSON-RPC
Puedes obtener baseFeePerGas directamente desde un nodo 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"
El valor devuelto está en hexadecimal (wei), lo conviertes a gwei dividiendo entre 10⁹.
8. Estimar gas antes de enviar la transacción con Web3j
En lugar de adivinar gasLimit, deja que el nodo lo calcule previamente usando eth_estimateGas — evita establecerlo demasiado bajo (revert) o demasiado alto (bloqueo temporal de ETH excedente).
<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("La estimación falló: " + response.getError().getMessage());
}
return response.getAmountUsed();
}
}
ethEstimateGassimula la transacción en el estado actual y devuelve el gas necesario.- En la práctica, debes agregar un buffer del 10-20% a este resultado antes de establecer
gasLimit, porque el estado puede cambiar entre la estimación y la ejecución real de la transacción.
9. Errores comunes
- Establecer
gasLimitexactamente al número estimado: si el layout del storage cambia ligeramente (por ejemplo, una slot de 0 → non-zero cuesta gas diferente), la transacción fácilmente se revierte conout of gas. Siempre agrega un buffer. - Establecer
maxFeePerGasdemasiado bajo mientras la red está congestionada: la transacción queda en el mempool indefinidamente, sin timeout automático — debes cancelarla tú mismo enviando una transacción con el mismo nonce pero tarifa más alta (replace-by-fee). - Confundir
gasPrice(legacy) conmaxFeePerGas(EIP-1559): enviar transacciones del tipo antiguo en una red que ya soporta 1559 funciona, pero pierdes el control del tip — tu billetera podría pagar más de lo necesario comparado con el baseFee real. - Establecer
priorityFeedemasiado bajo: si el tip es casi 0 mientras el bloque está lleno, los validadores no tienen incentivo para incluir tu transacción en un bloque pronto aunquemaxFeePerGassea alto.
Nota: No codifiques
gasPricefijo en código de producción. Siempre llama aeth_feeHistoryoeth_gasPricepara obtener el precio del mercado en el momento, luego aplica la fórmula de la sección 5.
10. Conclusión
Gas es la unidad de medida del trabajo computacional en EVM; la tarifa real = gas usado × precio por gas. Con EIP-1559, ese precio se divide en baseFee que se ajusta automáticamente según la congestión del bloque y priorityFee que tú eliges para prioridad. Entender bien esta fórmula te ayuda a establecer gasLimit/maxFeePerGas de manera razonable, evitando transacciones colgadas o tarifas innecesarias — y si lo necesitas, puedes escribir tu propia lógica de cálculo de tarifa en Java como en los ejemplos anteriores para integrarla en tu backend de monitoreo de billetera o dashboard de transacciones.