Ethereum: de historia a labs

Artículo 37 de 48

77%

completado al llegar aquí

Sistemas distribuidosNota de investigación17 min de lectura

ERC-4337: UserOperation, bundlers y EntryPoint

Cómo ERC-4337 crea smart accounts sin hard fork: UserOperation, bundler, EntryPoint y paymaster, con los riesgos que hereda un equipo de custodia.

Ruta Ethereum: 0/48

Imagina que tu equipo custodia fondos y quiere una regla simple: cualquier retiro mayor que cierto monto necesita dos firmas, hay un límite diario y la empresa paga el gas para que el usuario no tenga que comprar ETH. Con una cuenta normal de Ethereum esa regla no se puede expresar. La red solo acepta una transacción si trae una firma ECDSA válida de una clave, el nonce correcto y saldo suficiente. No hay lugar para "dos de tres" ni para "hasta 10.000 por día".

ERC-4337 resuelve eso sin tocar el protocolo base. En lugar de una nueva transacción, define un objeto llamado UserOperation, una red paralela donde circula y un contrato único, el EntryPoint, que decide qué operaciones se validan y ejecutan.

Brújula de lectura

Actor, evidencia y estado

Antes de avanzar, ubica quién actúa, qué prueba queda y qué parte del sistema cambia.

Actor

Cuenta, bundler y paymaster

La cuenta define su regla de firma; el bundler incluye; el paymaster puede pagar.

Evidencia

userOpHash y receipt de handleOps

El hash de la UserOperation no es el hash de la transacción que la incluye.

Estado

Depósitos y nonces en EntryPoint

El EntryPoint guarda depósitos, stake y la secuencia de nonces de cada cuenta.

Preguntas guía

  • ¿Quién paga el gas de una operación y en qué momento se le cobra?
  • ¿Qué contrato decide si una firma es válida: el protocolo, el EntryPoint o la cuenta?
  • ¿Qué identificador debe guardar tu sistema para rastrear una operación de punta a punta?
  • ¿Qué pasa con la operación si el único bundler que usas deja de responder?

El cuello de botella: la regla de validez fija de una EOA

Una EOA (externally owned account, cuenta controlada por una clave privada) tiene una sola forma de autorizar: firmar con esa clave. Quien tiene la clave puede hacer todo; quien la pierde no puede hacer nada. La página de ethereum.org lo resume así y agrega que una clave robada da acceso inmediato a todos los fondos.

Una smart account (cuenta inteligente) es un contrato que guarda los fondos y define en código qué cuenta como autorización válida. Puede pedir varias firmas, aceptar otro esquema criptográfico, limitar montos o permitir recuperar la cuenta con claves de respaldo. El problema histórico era que un contrato no puede iniciar una transacción: alguien con una EOA tenía que enviarla por él.

ERC-4337 ataca ese cuello de botella separando tres cosas que en una EOA vienen pegadas: validar, pagar y ejecutar. La especificación lo dice en su motivación: abstraer la validación permite multisig y recuperación; abstraer el pago permite que un tercero pague o que se pague con tokens; abstraer la ejecución permite agrupar llamadas. Y lo hace sin cambios de consenso, porque los autores no querían esperar a un hard fork.

Anatomía de una UserOperation

Una UserOperation es una "pseudo-transacción": describe lo que la cuenta quiere hacer, pero no es una transacción de Ethereum. Se envía a un bundler con el método JSON-RPC eth_sendUserOperation. Sus campos principales son:

CampoPara qué sirve
senderLa smart account que actúa.
nonceAnti-replay. Son 256 bits: 192 de "key" y 64 de "sequence".
factory y factoryDataDespliegan la cuenta si todavía no existe.
callDataLo que la cuenta ejecutará, por ejemplo una llamada execute.
callGasLimit, verificationGasLimitGas para ejecución y para validación.
preVerificationGasGas extra para compensar al bundler por costos que el contrato no mide.
maxFeePerGas, maxPriorityFeePerGasPrecio máximo, con semántica análoga a la de EIP-1559.
paymaster y sus camposContrato que paga en lugar de la cuenta, si existe.
signatureBytes que la cuenta interpreta como quiera.

El detalle más importante para custodia está en la firma. El protocolo no define qué es una firma válida: lo define el código de la cuenta. Lo que sí exige la especificación es que la firma dependa del chainId y de la dirección del EntryPoint, para que una operación firmada en una red no se pueda reproducir en otra ni contra otra versión del EntryPoint. El userOpHash es un hash tipado EIP-712 sobre la operación (sin la firma), el EntryPoint y el chainId.

El nonce partido en dos también tiene uso práctico. Para cada "key" de 192 bits, el EntryPoint exige una secuencia que crece de uno en uno. Una cuenta puede usar la key 0 para operaciones normales y otra key para un canal administrativo, por ejemplo rotar firmantes, sin que una cola bloquee a la otra. Si la cuenta quiere el comportamiento clásico de un solo nonce secuencial, debe exigir que la key sea cero.

Traza de ejecución: del bundler a handleOps

Sigamos una operación concreta: la cuenta quiere transferir 500 USDC y la empresa paga el gas con un paymaster.

El bundler es un nodo que recibe operaciones, las agrupa y envía una transacción normal que llama a EntryPoint.handleOps(ops, beneficiary). La especificación le pide validar tres veces: al recibir la operación, al armar el bundle y justo antes de enviarlo. Cada validación es una simulación, es decir, una llamada de solo lectura que ejecuta la parte de validación de la cuenta y del paymaster. Si la simulación revierte, la operación se descarta y no se propaga a otros bundlers.

Dentro de handleOps hay dos bucles. En el bucle de verificación, para cada operación el EntryPoint crea la cuenta si hace falta, calcula el costo máximo posible, llama a validateUserOp en la cuenta y, si hay paymaster, llama a validatePaymasterUserOp y revisa que el depósito alcance. En el bucle de ejecución llama a la cuenta con su callData, invoca postOp en el paymaster si este lo pidió, devuelve el gas cobrado de más y al final paga todas las comisiones a la dirección beneficiary del bundler.

Hay una penalización que conviene conocer antes de estimar gas: si queda sin usar al menos 40.000 de gas de callGasLimit o de paymasterPostOpGasLimit, se cobra el 10 % de ese sobrante. Existe para que nadie reserve mucho espacio en el bundle y deje fuera a otras operaciones. Sobrestimar "por si acaso" tiene costo.

Figura 1

Recorrido de una UserOperation hasta el EntryPoint

Fuentes:

[1]
Recorrido de una UserOperation hasta el EntryPoint
Cómo leer esta figura

La figura tiene dos franjas. La franja superior, fuera de la cadena, muestra la wallet que firma la UserOperation y la envía con eth_sendUserOperation al bundler, que la simula tres veces: al recibirla, al armar el bundle y antes de enviarlo. Una flecha baja a la franja inferior, en la cadena, donde una transacción llama a EntryPoint.handleOps. El EntryPoint ejecuta primero un bucle de verificación con tres pasos: desplegar la cuenta si hay initCode, llamar a validateUserOp en la cuenta y llamar a validatePaymasterUserOp revisando el depósito del paymaster. Luego ejecuta un bucle de ejecución: llamar a la cuenta con callData, llamar a postOp del paymaster y reembolsar el gas sobrante. Al final paga las comisiones a la dirección beneficiary del bundler.

Visual original de TrautsLab. La operación vive fuera de la cadena hasta que el bundler la incluye en una transacción handleOps; dentro del EntryPoint, primero se valida todo el lote y después se ejecuta.

Para tu sistema esto significa que hay dos identificadores. El userOpHash identifica la operación; el hash de la transacción identifica el bundle que la incluyó. Varias operaciones pueden compartir una misma transacción, y una operación puede fallar en ejecución aunque la transacción del bundle tenga éxito. La especificación introduce eth_getUserOperationReceipt para consultar el resultado por userOpHash, aunque deja fuera de su alcance la definición completa de la API JSON-RPC.

La interfaz mínima que implementa la cuenta

La cuenta solo necesita una función obligatoria. Según la especificación debe comprobar que quien llama es un EntryPoint de confianza, verificar la firma sobre userOpHash, revertir ante cualquier error que no sea una firma incorrecta y pagar al EntryPoint al menos missingAccountFunds. Ante una firma incorrecta, la recomendación (un "SHOULD" del estándar) es devolver 1 (SIG_VALIDATION_FAILED) sin revertir y sin cortar el flujo antes de tiempo, para que la estimación de gas de la validación siga funcionando. El valor devuelto también puede empaquetar una ventana de validez (validAfter, validUntil).

El siguiente contrato es ilustrativo: muestra la forma de esa interfaz con un único dueño. No verifica la maleabilidad de firmas ni tiene recuperación, así que no debe usarse con fondos reales.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
struct PackedUserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    bytes32 accountGasLimits;
    uint256 preVerificationGas;
    bytes32 gasFees;
    bytes paymasterAndData;
    bytes signature;
}
 
/// Ilustrativo: cuenta de un solo dueño para leer la interfaz de ERC-4337.
contract MinimalAccount {
    uint256 internal constant SIG_VALIDATION_FAILED = 1;
    address public immutable entryPoint;
    address public immutable owner;
 
    constructor(address entryPoint_, address owner_) {
        entryPoint = entryPoint_;
        owner = owner_;
    }
 
    modifier onlyEntryPoint() {
        require(msg.sender == entryPoint, "solo EntryPoint");
        _;
    }
 
    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external onlyEntryPoint returns (uint256 validationData) {
        validationData = _recover(userOpHash, userOp.signature) == owner ? 0 : SIG_VALIDATION_FAILED;
        if (missingAccountFunds > 0) {
            (bool ok, ) = payable(msg.sender).call{value: missingAccountFunds}("");
            (ok); // el EntryPoint comprueba después que el depósito alcance
        }
    }
 
    function execute(address target, uint256 value, bytes calldata data) external onlyEntryPoint {
        (bool ok, bytes memory ret) = target.call{value: value}(data);
        if (!ok) {
            assembly {
                revert(add(ret, 32), mload(ret))
            }
        }
    }
 
    function _recover(bytes32 digest, bytes calldata sig) internal pure returns (address) {
        if (sig.length != 65) return address(0);
        bytes32 r = bytes32(sig[0:32]);
        bytes32 s = bytes32(sig[32:64]);
        uint8 v = uint8(sig[64]);
        return ecrecover(digest, v, r, s);
    }
 
    receive() external payable {}
}
62 líneas

Fíjate en onlyEntryPoint sobre execute. Si una cuenta deja execute abierta a cualquiera, toda la validación anterior sobra: cualquier contrato podría mover los fondos sin pasar por validateUserOp. La sección de seguridad de la especificación pide justamente que las demás funciones de la cuenta solo acepten llamadas del EntryPoint (o de la propia cuenta).

Paymasters, depósitos y stake

Un paymaster es un contrato que acepta pagar el gas de otra cuenta. La empresa del ejemplo despliega uno que patrocina solo a sus usuarios, o uno que cobra el equivalente en un token ERC-20. Durante la verificación, el EntryPoint llama a validatePaymasterUserOp(userOp, userOpHash, maxCost). Si el paymaster devuelve un context no vacío, después de ejecutar se llama a postOp con el costo real, aun cuando la operación revirtió: en ese caso el paymaster igual paga el gas.

Hay dos saldos distintos en el EntryPoint y conviene no confundirlos. El depósito es ETH que el EntryPoint descuenta para pagar gas; se gestiona con depositTo, balanceOf y withdrawTo. El stake es ETH bloqueado con addStake(unstakeDelaySec), que solo se retira tras unlockStake y la espera fijada. El stake no paga gas: sirve como garantía para que los bundlers le permitan al paymaster usar más almacenamiento durante la validación.

Desde el punto de vista operativo, un paymaster es una cuenta corriente con política. Si su depósito se agota, todas las operaciones que dependen de él dejan de validarse al mismo tiempo. Si su política de firma vive en un servidor, ese servidor pasa a ser parte del camino crítico de cada retiro patrocinado.

Por qué el bundler desconfía: reglas de validación

En una transacción normal, el nodo puede decidir si es válida con datos estáticos: firma, nonce, saldo. Una UserOperation no: su validez depende de código EVM que lee estado, y otra operación del mismo bundle podría cambiar ese estado e invalidarla después de simulada. Si eso pasa, el bundler paga el gas del bundle sin cobrar.

Por eso existe ERC-7562, todavía en estado de revisión, que fija las reglas que deben cumplir la cuenta, la factory y el paymaster durante la validación: qué opcodes pueden usar, a qué almacenamiento pueden acceder y cuándo se exige stake. Su principio es que, una vez admitida en el mempool, una operación debe garantizar el pago de comisiones, como ocurre con una EOA.

ERC-4337 añade límites concretos que el bundler revisa antes de simular: verificationGasLimit y el de verificación del paymaster deben ser menores que MAX_VERIFICATION_GAS (500.000), y preVerificationGas debe cubrir el costo del calldata más PRE_VERIFICATION_OVERHEAD_GAS (50.000). Además, solo se acepta una operación por sender en el mempool y en cada bundle, salvo que la cuenta tenga stake. Para un servicio que procesa muchos retiros desde la misma cuenta, esta regla define el throughput real.

Riesgo operativo para un equipo de custodia

El EntryPoint concentra la confianza. La propia especificación dice que debe auditarse y verificarse formalmente porque es el punto central de todo el sistema, y enumera dos propiedades mínimas: que solo llame a una cuenta con su callData si validateUserOp de esa cuenta pasó, y que si cobra la validación también ejecute la llamada. Elegir una versión del EntryPoint es elegir el contrato del que depende cada fondo.

La dirección de una cuenta se conoce antes de desplegarla. Se espera que la factory use CREATE2, y es importante que la dirección dependa de las credenciales iniciales; así nadie puede adelantarse y desplegar en esa dirección una cuenta con otro dueño. Un detalle que confunde: si la cuenta ya existe y la operación trae initCode, ese código se ignora.

Las claves de sesión no forman parte del estándar. La documentación de ERC-4337 aclara que se implementan wallet por wallet, dentro de validateUserOp, mediante módulos o plugins. Auditar una smart account significa auditar ese módulo, no solo la interfaz.

ERC-4337 también admite EOAs que delegaron código con EIP-7702. En ese caso el campo factory lleva el marcador 0x7702, el bundler envía la autorización en una transacción de tipo set-code y el userOpHash incluye la dirección del contrato delegado. Lo vemos en el capítulo siguiente.

Revisión operativa

Antes de aprobar una smart account para custodia

0 de 6 revisados

Cada punto se puede verificar en código, en la configuración del bundler o en los receipts.

Lab propuesto: simular una UserOperation patrocinada

Objetivo: desplegar localmente un EntryPoint, una cuenta mínima y un paymaster de prueba, y observar cada etapa. La documentación de ERC-4337 explica cómo desplegar el EntryPoint en una red local desde el repositorio oficial de referencia, con una sal fija de CREATE2 para obtener la misma dirección que en otras redes.

La validación observable es concreta: una operación con firma correcta deja un evento del EntryPoint y descuenta el depósito del paymaster; la misma operación con la firma de otra clave devuelve SIG_VALIDATION_FAILED en la simulación y nunca llega al bundle. Como teardown, basta detener la red local. El riesgo de llevarlo tal cual a mainnet es claro: la cuenta de ejemplo no tiene recuperación ni defensas contra firmas maleables.

Conexiones dentro de la serie

Referencias

Bibliografía académica

Formato IEEENumerado para ingeniería, sistemas y protocolos.
  1. [1]

    V. Buterin, Y. Weiss, D. Tirosh, S. Nacson, A. Forshtat, K. Gazso y T. Hess, "ERC-4337: Account Abstraction Using Alt Mempool," Ethereum Improvement Proposals, no. 4337, 2021.

    Especificación final: UserOperation, EntryPoint, paymasters, simulación y consideraciones de seguridad.

  2. [2]

    ethereum.org, "Account abstraction," Ethereum roadmap, actualizado el 24 de junio de 2026.

    official docsID: ETH-ACCOUNT-ABSTRACTIONAbrir fuente (abre en otra pestaña)

    Límites de las EOAs y beneficios de las smart contract wallets.

  3. [3]

    Y. Weiss, D. Tirosh, A. Forshtat y S. Nacson, "ERC-7562: Account Abstraction Validation Scope Rules [Review]," Ethereum Improvement Proposals, no. 7562, 2023.

    Reglas de opcodes, almacenamiento, stake y reputación para la fase de validación.

  4. [4]

    ERC-4337 Documentation, "Session Keys & Delegation."

    docsID: ERC4337-DOCS-SESSIONSAbrir fuente (abre en otra pestaña)

    Las claves de sesión no están estandarizadas y dependen de cada wallet.

  5. [5]

    V. Buterin, S. Wilson, A. Dietrichs y lightclient, "EIP-7702: Set Code for EOAs," Ethereum Improvement Proposals, no. 7702, 2024.

    Transacción de tipo 0x04 que escribe un indicador de delegación en el código de una EOA.

  6. [6]

    ERC-4337 Documentation, "The EntryPoint Contract."

    docsID: ERC4337-DOCS-ENTRYPOINTAbrir fuente (abre en otra pestaña)

    Responsabilidades del EntryPoint y despliegue local con CREATE2.

Fuentes primarias, papers seminales y especificaciones técnicas citadas en esta investigación.

Rutas vecinas

Si este tema te abrió hambre, sigue por aquí.

Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.

Sigue leyendo

Ideas conectadas con este artículo

Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.

Los comentarios se cargan al final de la lectura para no meter peso antes de tiempo.