Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Guías de red y protocolo12 min de lectura

Anulación de estado en eth_call: Simulando llamadas a Ethereum contra un estado hipotético

Aprende a usar el conjunto de anulación de estado de eth_call para simular llamadas contra un estado modificado, sin necesidad de transacciones. Incluye ejemplos con viem y ethers.

TL;DR

El conjunto de anulación de estado de eth_call es un cuarto parámetro opcional en la solicitud JSON-RPC que permite ejecutar una llamada contra una versión temporal y modificada del estado de la blockchain. Nunca persiste cambios, lo que lo hace ideal para simular escenarios hipotéticos como un gran saldo de tokens o un propietario suplantado. Esta guía explica cada campo de anulación, muestra cómo calcular ranuras de almacenamiento y proporciona ejemplos ejecutables con viem y ethers.

¿Qué es el conjunto de anulación de estado de eth_call?

El método estándar eth_call ejecuta una llamada de solo lectura contra el estado actual de la blockchain. Pero, ¿qué sucede si quieres probar cómo se comporta un contrato cuando una dirección posee un millón de tokens, o cuando el propietario es otra persona? Enviar una transacción real sería costoso e irreversible. La solución es el conjunto de anulación de estado, un cuarto parámetro opcional en el objeto de solicitud de eth_call. Es un mapa de direcciones a objetos de anulación que el cliente aplica solo para esa llamada. Las anulaciones son efímeras: nunca tocan el estado persistido y se descartan inmediatamente después de que la llamada regresa.

Esta característica es compatible con los principales clientes de Ethereum, incluidos geth, reth y Erigon, y está expuesta por la mayoría de los proveedores de RPC que utilizan eth_call. No forma parte de eth_sendRawTransaction: no puedes usar anulaciones para alterar el estado en una transacción real. La especificación oficial está en las APIs de ejecución de Ethereum y en la documentación JSON-RPC de geth.

El objeto de anulación para cada dirección puede contener hasta cinco campos: balance, nonce, code, state y stateDiff. Cada campo te permite manipular un aspecto diferente de la cuenta o contrato en esa dirección. Cubriremos cada uno en detalle, con ejemplos concretos y trampas.

  • balance: establece el saldo de ETH de una dirección, en wei (hexadecimal o decimal).
  • nonce: establece el número de transacciones de una cuenta.
  • code: reemplaza el bytecode en una dirección de contrato, suplantando efectivamente su lógica.
  • state: sobrescribe ranuras de almacenamiento específicas de un contrato.
  • stateDiff: aplica un diff a las ranuras de almacenamiento, similar a state pero con diferencias sutiles entre clientes (verifica según el cliente).

Cómo funciona el conjunto de anulación de estado bajo el capó

Cuando envías un eth_call con un conjunto de anulación de estado, el cliente crea un trie temporal en memoria que comienza como una copia del estado actual. Luego aplica tus anulaciones a esa copia. La llamada se ejecuta contra este trie modificado y se devuelve el resultado. Debido a que el trie es efímero, no se escriben cambios en el disco ni se transmiten a la red. Por eso las anulaciones de estado son perfectas para análisis de 'qué pasaría si' y pruebas.

Los campos state y stateDiff operan en ranuras de almacenamiento. Una ranura de almacenamiento es una clave de 256 bits en el trie de almacenamiento del contrato. Para variables públicas simples, el número de ranura suele ser un entero (por ejemplo, la ranura 0 para la primera variable). Para mapeos, la ranura se calcula usando keccak256(abi.encode(key, uint256(slot))). Te mostraremos cómo calcular esto en la sección de ejemplos.

Una trampa crítica: el campo stateDiff se implementa de manera diferente entre clientes. En geth, se trata como un conjunto transitorio que se confirma después de la llamada, pero otros clientes pueden interpretarlo de manera diferente. Siempre verifica el comportamiento en tu cliente o proveedor objetivo. En caso de duda, usa state, que tiene un soporte más universal.

  • Las anulaciones se aplican a una copia temporal del trie de estado, no al canónico.
  • La llamada se ejecuta exactamente como si el estado anulado fuera real, incluida la lógica del contrato y el manejo de errores.
  • No se consume gas por los cambios de estado, pero la llamada en sí puede requerir estimación de gas.
  • Las anulaciones de estado no se persisten y no se pueden usar en eth_sendRawTransaction.

Casos de uso comunes para la simulación con anulación de estado

Los desarrolladores suelen recurrir a las anulaciones de estado en tres escenarios. Primero, simular un gran saldo de tokens: quieres probar un swap o un guard de transferencia que requiere que el llamador tenga una cantidad mínima de un token ERC-20. En lugar de transferir tokens realmente, anulas el mapeo de saldo del contrato del token para tu dirección.

Segundo, suplantar a un propietario o rol privilegiado: muchos contratos tienen modificadores onlyOwner que restringen ciertas funciones de lectura. Para probar la ruta de lectura como propietario, puedes anular la ranura de almacenamiento del propietario del contrato con tu dirección, o anular el código del contrato con un mock que devuelva true para isOwner().

Tercero, probar la lógica de liquidación: los protocolos de préstamo descentralizados a menudo dependen de oráculos de precios. Al anular el precio almacenado en el contrato del oráculo, puedes simular una caída de precios y verificar que tu bot de liquidación se activa correctamente, todo sin bifurcar la red ni esperar una caída real de precios.

  • Prueba funciones restringidas por tokens sin tener el token.
  • Simula llamadas solo para propietarios sin cambiar al propietario real.
  • Manipula precios de oráculos para probar umbrales de liquidación.
  • Depura interacciones complejas entre múltiples contratos de forma aislada.

Ejemplo ejecutable: Simulando un saldo de tokens y anulación de almacenamiento

A continuación se muestra un script completo de Node.js usando viem (v2.x) que demuestra tres cosas: (1) leer el saldo de ETH de una dirección con un eth_call simple, (2) volver a ejecutar la misma llamada con una anulación de estado que le da a la dirección un gran saldo de ETH, y (3) anular una ranura de almacenamiento en un contrato ERC-20 para simular un saldo de tokens. El script imprime ambos resultados lado a lado.

Usaremos el contrato USDC en la red principal de Ethereum (dirección 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) y una dirección de usuario hipotética. La ranura de almacenamiento para el mapeo de saldo se calcula usando keccak256(abi.encode(userAddress, uint256(0))) porque el mapeo es la primera variable de estado (ranura 0).

Antes de ejecutar, instala viem y asegúrate de tener un endpoint RPC. Puedes usar cualquier endpoint público o privado, pero ten en cuenta que algunos proveedores pueden no soportar anulaciones de estado: consulta la documentación de tu proveedor. Para un endpoint confiable, considera el RPC de Ethereum de OnFinality o tu propio nodo.

// npm install viem
import { createPublicClient, http, keccak256, encodeAbiParameters, parseEther, hexToBigInt } from 'viem';
import { mainnet } from 'viem/chains';

const client = createPublicClient({
  chain: mainnet,
  transport: http('https://eth-mainnet.public.blastapi.io'), // replace with your endpoint
});

const user = '0xYourAddressHere'; // replace with your address
const usdc = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';

// 1. Plain eth_call to read ETH balance
const ethBalancePlain = await client.getBalance({ address: user });
console.log('Plain ETH balance:', ethBalancePlain.toString());

// 2. eth_call with state override to set ETH balance to 1000 ETH
const ethBalanceOverridden = await client.getBalance({
  address: user,
  stateOverride: [
    {
      address: user,
      balance: parseEther('1000'),
    },
  ],
});
console.log('Overridden ETH balance:', ethBalanceOverridden.toString());

// 3. Override USDC balance (storage slot 0 mapping)
// Compute slot: keccak256(abi.encode(user, uint256(0)))
const slot = keccak256(encodeAbiParameters(
  [{ type: 'address' }, { type: 'uint256' }],
  [user, 0n]
));

// Simulate a call to balanceOf(user) with override
const balanceOfSelector = '0x70a08231'; // balanceOf(address)
const callData = balanceOfSelector + user.slice(2).padStart(64, '0');

const result = await client.call({
  account: user,
  to: usdc,
  data: callData,
  stateOverride: [
    {
      address: usdc,
      state: {
        [slot]: '0x' + (1000000n * 10n ** 6n).toString(16).padStart(64, '0'), // 1,000,000 USDC (6 decimals)
      },
    },
  ],
});

console.log('Simulated USDC balance:', hexToBigInt(result.data).toString());

// Fill in the table below with your results.

Salida esperada y tabla de resultados

Cuando ejecutes el script, deberías ver una salida similar a la siguiente (los valores reales dependen de tu dirección y endpoint):

El saldo de ETH anulado debería ser exactamente 1000 ETH (1e21 wei). El saldo de USDC simulado debería ser 1,000,000 USDC (1e12 unidades base, ya que USDC tiene 6 decimales). Si ves valores diferentes, verifica el cálculo de la ranura y los decimales.

Registra tus propios resultados en la tabla a continuación para verificar:

  • | Llamada | Resultado simple | Resultado anulado | Esperado |
  • |---------|------------------|-------------------|----------|
  • | Saldo ETH | ... | ... | 1000 ETH |
  • | Saldo USDC balanceOf | ... | ... | 1,000,000 USDC |
Plain ETH balance: 1234567890123456789
Overridden ETH balance: 1000000000000000000000
Simulated USDC balance: 1000000000000000000000000

Cálculo de ranuras de almacenamiento para mapeos y arrays

El ejemplo anterior usa la fórmula estándar de ranura de mapeo de Solidity: para un mapeo declarado en la ranura p, el valor para la clave k se almacena en keccak256(abi.encode(k, uint256(p))). Esto está definido en la documentación de Solidity. Para un mapeo en la ranura 0, la fórmula se simplifica a keccak256(abi.encode(key, uint256(0))).

Para arrays dinámicos, la longitud se almacena en la ranura p, y el elemento en el índice i está en keccak256(abi.encode(uint256(p))) + i. Para mapeos anidados o estructuras, aplica la fórmula recursivamente. Siempre verifica el diseño de ranuras del contrato específico que estás atacando, ya que las optimizaciones del compilador pueden cambiarlo.

Si no estás seguro del número de ranura, puedes usar una herramienta como cast storage (Foundry) o eth_getStorageAt para inspeccionar el estado actual. Por ejemplo, para encontrar la ranura del propietario de un contrato, podrías leer la ranura 0 y ver si coincide con la dirección del propietario esperada.

  • Ranura de mapeo: keccak256(abi.encode(key, uint256(slot)))
  • Elemento de array: keccak256(abi.encode(uint256(slot))) + index
  • Siempre confirma el diseño de ranuras con el código fuente del contrato o inspeccionando el almacenamiento.

Solución de problemas de fallos comunes

Las anulaciones de estado pueden fallar por varias razones. La más común es que tu proveedor de RPC no soporte el parámetro stateOverride. Algunos proveedores lo eliminan o devuelven un error. Si obtienes un error como missing value for required argument 4, tu proveedor puede no soportarlo. Prueba con otro proveedor o ejecuta tu propio nodo.

Otro problema es el cálculo incorrecto de la ranura. Si anulas la ranura equivocada, la llamada devolverá resultados inesperados. Verifica dos veces el número de ranura y la codificación. Además, asegúrate de usar la dirección correcta para el contrato y el usuario.

Finalmente, ten en cuenta el comportamiento específico del cliente con stateDiff. Como se mencionó, geth lo trata de manera diferente a otros clientes. Si dependes de stateDiff, pruébalo en tu cliente objetivo. Para máxima compatibilidad, usa state en su lugar.

  • El proveedor no soporta anulaciones de estado: cambia a un nodo que controles o a un proveedor que lo soporte explícitamente.
  • Ranura de almacenamiento incorrecta: verifica con eth_getStorageAt o el código fuente del contrato.
  • Tipo de dato incorrecto: asegúrate de que los saldos estén en wei y los valores estén codificados en hexadecimal.
  • stateDiff no funciona: usa state en su lugar, o consulta la documentación del cliente.

Anulación de estado vs. bifurcación: ¿cuál deberías usar?

Las anulaciones de estado no son la única forma de simular un estado hipotético. También puedes usar una bifurcación (por ejemplo, una bifurcación de la red principal de Hardhat) para crear una copia local de la blockchain y luego modificar el estado libremente. Las bifurcaciones son más potentes porque te permiten enviar transacciones y probar cambios de estado, pero requieren ejecutar un nodo local y son más lentas para simples comprobaciones de solo lectura.

Las anulaciones de estado son ideales para simulaciones rápidas y sin estado donde solo necesitas probar una sola llamada o un pequeño conjunto de llamadas. También son útiles en entornos de producción donde no puedes permitirte ejecutar una bifurcación. Sin embargo, están limitadas a llamadas de solo lectura: no puedes simular una transacción que cambie el estado.

Un eth_call simple sin anulaciones es la opción más sencilla cuando solo necesitas leer el estado actual. Úsalo cuando no necesites modificar nada. La tabla a continuación resume las ventajas y desventajas:

  • | Método | Ventajas | Desventajas | Mejor para |
  • |--------|----------|-------------|------------|
  • | Anulación de estado | Rápido, sin configuración, sin persistencia de estado | Solo lectura, soporte variable del proveedor | Comprobaciones rápidas de 'qué pasaría si', depuración en producción |
  • | Bifurcación | Control total, puede enviar transacciones | Requiere nodo local, más lento | Pruebas complejas, pruebas de integración |
  • | eth_call simple | Simple, soporte universal | No puede modificar el estado | Lectura del estado actual |

Próximos pasos y lecturas adicionales

Ahora que entiendes las anulaciones de estado, puedes aplicarlas a tus propios flujos de trabajo de prueba y depuración. Para profundizar tu conocimiento, explora temas relacionados en el centro de aprendizaje de OnFinality. Por ejemplo, aprende a decodificar razones de reversión cuando tu llamada simulada falle, o cómo consultar datos históricos para entender el estado pasado. Si haces muchas llamadas, revisa las mejores prácticas de agrupación JSON-RPC para mejorar la eficiencia.

Al elegir un proveedor de RPC, considera las opciones de precios de RPC y el servicio de API. Para una comparación de proveedores, consulta la guía del asistente de RPC para elegir una API RPC de Ethereum. Si encuentras límites de velocidad, lee sobre límites de velocidad de RPC de Ethereum y errores 429.

Finalmente, consulta siempre la especificación oficial de las APIs de ejecución de Ethereum y la documentación de geth para obtener los detalles más actualizados sobre las anulaciones de estado.

  • Experimenta anulando code para suplantar la lógica de un contrato.
  • Usa anulaciones de estado en tu suite de pruebas para evitar bifurcaciones en casos simples.
  • Comparte tus hallazgos con la comunidad: las anulaciones de estado están subutilizadas.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar