Ir al contenido principal

Abstracción de cuentas y ERC-4337 - Parte 2

Actualizado el
26 de noviembre de 2025

13 minutos de lectura

Resumen

En nuestra guía «Abstracción de cuentas y ERC-4337 - Parte 1», sentamos las bases para comprender el EIP-4337. En esta guía complementaria, pasaremos a la práctica y profundizaremos en los pasos concretos para crear e implementar un contrato inteligente compatible con ERC-4337 utilizando Stackup. ¿Listo para profundizar más? ¡Manos a la obra!

Lo que necesitarás


Tus funciones


  • Repasa lo que sabes sobre qué es la «abstractación de cuentas» (ERC-4337).
  • Crear un contrato inteligente ERC-4337 con Stackup
  • Enviar una transacción utilizando una cuenta de contrato inteligente y ERC-4337

Una nueva mirada a la abstracción de cuentas en Ethereum: conceptos básicos del ERC-4337

¿Qué es la abstracción de cuentas (ERC-4337)?

Antes de profundizar en la implementación del ERC-4337, repasemos los conceptos fundamentales de la abstracción de cuentas en Ethereum, tal y como se explicaron en la primera parte de esta serie de guías:


  • Retos del sistema de cuentas de Ethereum: Hemos analizado los problemas actuales del sistema de cuentas de Ethereum. Las cuentas de propiedad externa (EOA) presentan limitaciones en cuanto a la experiencia del usuario, especialmente a la hora de interactuar con contratos inteligentes, realizar operaciones de varios pasos o gestionar frases de semilla. Las cuentas de contratos inteligentes ofrecen algunas soluciones, pero también plantean sus propios retos.
  • Introducción al ERC-4337: El ERC-4337, conocido a menudo como «Abstracción de cuentas mediante Alt Mempool», surgió como una solución prometedora a los retos mencionados. Esta Propuesta de Mejora de Ethereum (EIP) se centra en mejorar la experiencia del usuario de los monederos.

Componentes clave del ERC-4337


  • Operaciones de usuario: Esta es la acción que el usuario desea realizar. Podría tratarse de transferir fondos desde la cuenta del contrato inteligente, interactuar con otro contrato inteligente o realizar una llamada de recuperación social. Operación de usuario Los objetos tienen campos similares a los de los objetos de transacción que vemos hoy en día en Ethereum. Sin embargo, campos como «nonce» y «signature» son específicos de cada cuenta (tal y como se implementa en ERC-4337).
  • Agrupadores: Entidades incluidas en la lista blanca que recopilan y envían «UserOperations» a la red Ethereum a través de la Punto de entrada contrato. Dado que los «bundlers» tienen incentivos para mantenerse activos, reciben comisiones y establecen prioridades en cuanto a qué Operación de usuario los agrupan para obtener la máxima rentabilidad.
  • Punto de entrada: Un único contrato inteligente que valida y ejecuta las operaciones de los usuarios (UserOperations). Este es el contrato con el que interactuarán la mayoría o la totalidad de los «Bundlers» de la red para enviar lotes de Operación de usuario objetos.
  • Cuentas contractuales: son cuentas contractuales que controlan las entidades.
  • Pagador: Entidades opcionales que pueden hacerse cargo de las comisiones de las transacciones (por ejemplo, otra entidad puede pagar las comisiones de tu transacción).
  • Agregadores: ayudan a validar conjuntamente las firmas de varias operaciones de usuario.

Ahora que hemos refrescado la memoria sobre los conceptos del ERC-4337, veamos cómo crear contratos inteligentes que cumplan con el estándar ERC-4337 e interactuar con ellos.

Análisis de SimpleAccount.sol: un ejemplo de contrato ERC-4337

La Fundación Ethereum ha implementado un ejemplo minimalista de un contrato compatible con el estándar ERC-4337 denominado SimpleAccount.sol.

Dediquemos unos minutos a repasar el código que aparece a continuación. No hace falta que creemos un archivo con este código, sino que basta con revisarlo para comprender su funcionamiento.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

/* solhint-disable avoid-low-level-calls */
/* solhint-disable no-inline-assembly */
/* solhint-disable reason-string */

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

import "../core/BaseAccount.sol";
import "./callback/TokenCallbackHandler.sol";

/**
* minimal account.
* this is sample minimal account.
* has execute, eth handling methods
* has a single signer that can send requests through the entryPoint.
*/
contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
using ECDSA for bytes32;

address public owner;

IEntryPoint private immutable _entryPoint;

event SimpleAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner);

modifier onlyOwner() {
_onlyOwner();
_;
}

/// @inheritdoc BaseAccount
function entryPoint() public view virtual override returns (IEntryPoint) {
return _entryPoint;
}


// solhint-disable-next-line no-empty-blocks
receive() external payable {}

constructor(IEntryPoint anEntryPoint) {
_entryPoint = anEntryPoint;
_disableInitializers();
}

function _onlyOwner() internal view {
//directly from EOA owner, or through the account itself (which gets redirected through execute())
require(msg.sender == owner || msg.sender == address(this), "only owner");
}

/**
* execute a transaction (called directly from owner, or by entryPoint)
*/
function execute(address dest, uint256 value, bytes calldata func) external {
_requireFromEntryPointOrOwner();
_call(dest, value, func);
}

/**
* execute a sequence of transactions
* @dev to reduce gas consumption for trivial case (no value), use a zero-length array to mean zero value
*/
function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external {
_requireFromEntryPointOrOwner();
require(dest.length == func.length && (value.length == 0 || value.length == func.length), "wrong array lengths");
if (value.length == 0) {
for (uint256 i = 0; i < dest.length; i++) {
_call(dest[i], 0, func[i]);
}
} else {
for (uint256 i = 0; i < dest.length; i++) {
_call(dest[i], value[i], func[i]);
}
}
}

/**
* @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint,
* a new implementation of SimpleAccount must be deployed with the new EntryPoint address, then upgrading
* the implementation by calling `upgradeTo()`
*/
function initialize(address anOwner) public virtual initializer {
_initialize(anOwner);
}

function _initialize(address anOwner) internal virtual {
owner = anOwner;
emit SimpleAccountInitialized(_entryPoint, owner);
}

// Require the function call went through EntryPoint or owner
function _requireFromEntryPointOrOwner() internal view {
require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint");
}

/// implement template method of BaseAccount
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
internal override virtual returns (uint256 validationData) {
bytes32 hash = userOpHash.toEthSignedMessageHash();
if (owner != hash.recover(userOp.signature))
return SIG_VALIDATION_FAILED;
return 0;
}

function _call(address target, uint256 value, bytes memory data) internal {
(bool success, bytes memory result) = target.call{value : value}(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}

/**
* check current account deposit in the entryPoint
*/
function getDeposit() public view returns (uint256) {
return entryPoint().balanceOf(address(this));
}

/**
* deposit more funds for this account in the entryPoint
*/
function addDeposit() public payable {
entryPoint().depositTo{value : msg.value}(address(this));
}

/**
* withdraw value from the account's deposit
* @param withdrawAddress target to send to
* @param amount to withdraw
*/
function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner {
entryPoint().withdrawTo(withdrawAddress, amount);
}

function _authorizeUpgrade(address newImplementation) internal view override {
(newImplementation);
_onlyOwner();
}
}

Repasemos el código.

El SimpleAccount El contrato anterior está controlado por una dirección de propietario externa y está diseñado para interactuar con un Punto de entrada contrato (según el estándar ERC-4337), lo que permite al propietario realizar transacciones sin tener que pagar él mismo el gas. El contrato se integra con la biblioteca de OpenZeppelin para ofrecer otras funcionalidades, como la validación de firmas criptográficas (ECDSA) y patrones de contrato actualizables (UUPSUpgradeable e Initializable). También importa el Cuenta básica y el controlador de devolución de llamada. El Cuenta básica es un componente fundamental que lleva un registro del nonce del contrato inteligente y ayuda a Operación de usuario validación de la carga útil, Punto de entrada interacción, pago por ejecución (es decir, payPrefund()), y su capacidad de ampliación, lo que permite implementaciones personalizadas para funciones como, por ejemplo, _validateSignature(), _validateNonce(), y _payPrefund().

La variable de estado propietario almacena la dirección del titular de la cuenta, y _entryPoint es una referencia inmutable a un contrato externo que sirve como el Punto de entrada.

Dos funciones principales, ejecutar y executeBatch, permite que el propietario o el punto de entrada del sistema de retransmisión para enviar transacciones o una secuencia de transacciones, respectivamente. Ambas funciones comprueban primero si el remitente es el Punto de entrada o al propietario antes de su procesamiento.

El contrato también permite la actualización en caso de cambio de titular, pero cualquier modificación del EntryPoint (por ejemplo, _entryPoint) requerirá el despliegue de una nueva cuenta de contrato inteligente.

Ahora, para simplificar las cosas, utilizaremos el SDK de Stackup para implementar un contrato compatible con ERC-4337 y empezar a realizar operaciones como la aprobación de tokens ERC-20 y la transferencia de ETH y tokens.

Desarrollo de contratos inteligentes ERC-4337 con Stackup

El contrato compatible con ERC-4337 que vamos a desarrollar en esta sección procede de Stackup. Es una buena plantilla de partida para los desarrolladores que se inician en la abstracción de cuentas.

Configuración para desarrolladores

1. En primer lugar, abre la ventana de la terminal y ejecuta el siguiente comando:

git clone https://github.com/stackup-wallet/erc-4337-examples.git
cd erc-4337-examples
yarn install

El comando anterior clona e instala las dependencias del repositorio de GitHub asociado.

2. A continuación, configuraremos nuestro contrato ERC-4337 utilizando el init comando:

Inicialización de «yarn run»

Esto creará un config.json archivo, con valores como:


  • rpcUrl: Esta URL de RPC admitirá los métodos que llamemos desde nuestro contrato ERC-4337. Para este campo se necesita una clave API de Stackup.
  • clave de firma: La clave para generar un Operación de usuario firma. La cuenta del contrato también la utiliza para validar las transacciones
  • paymaster.rpcUrl: La clave utilizada para generar una firma de UserOperation. La cuenta del contrato utiliza esta firma para validar las transacciones
  • paymaster.context: Campo arbitrario que depende del responsable de pagos con el que estés interactuando

Crear una clave API de Stackup

3. Ahora, con el config.json Una vez generado, tendremos que introducir valores como la URL de RPC. Para ello, ve a https://app.stackup.sh/sign-in, crea una cuenta y, a continuación, se te pedirá que selecciones una cadena. Selecciona la Ethereum Sepolia cadena (a efectos de esta guía) y, a continuación, haz clic en «Siguiente». A continuación, selecciona la instancia de Bundler que has creado y haz clic en el Clave API botón. Una vez copiada tu clave API, vuelve a tu config.json archivo e introduce la clave API en todos los rpcUrl campos.

Tu config.json debería tener un aspecto similar a este:

{
"rpcUrl": "https://api.stackup.sh/v1/node/cd9af3b13c47d203af0b48513615bef69ec8c9072c24bbf2fd9ed9c8f97d6428",
"signingKey": "0xbeacd206e9870af02243e2c1cd253a1440966f04c553d7e696c0271a17edd9e",
"paymaster": {
"rpcUrl": "https://api.stackup.sh/v1/paymaster/cd9af3b13c47d203af0b48513615bef69ec8c9072c24bbf2fd9ed9c8f97d6428",
"context": {}
}
}

¡No te olvides de guardar el archivo!

Crear la dirección de la cuenta del contrato inteligente

4. Una vez realizada la configuración, podemos crear una cuenta de contrato inteligente tal y como se define en nuestro archivo de configuración. Ejecuta el siguiente comando en tu terminal y se mostrará una dirección. La cuenta de contrato inteligente aún no se ha implementado, pero se generará la dirección para que la conozcamos de antemano.

Dirección de la cuenta de yarn run simpleAccount

Para ver qué es lo que realmente se está ejecutando, ve a la scripts/simpleAccount/address.ts archivo.

Deberías ver un resultado como este:

$ ts-node scripts/simpleAccount/index.ts address
Dirección de SimpleAccount: 0xD4494616f04ebd65E407330672c4C5A07BA5270F
✨ Hecho en 1,75 s.

En la siguiente sección, ingresaremos fondos en la dirección de SimpleAccount que acabamos de generar. Ten en cuenta que el contrato aún no se ha desplegado.

Recargar la cuenta del contrato inteligente

Ahora, vamos a ingresar fondos en la dirección de nuestra cuenta de contrato inteligente (por ejemplo, SimpleAccount) que generamos en la sección anterior.

Puedes utilizar el grifo Quicknode Multi-Chain y enviar algo de ETH de la red de pruebas a tu monedero personal para, a continuación, transferirlo a la dirección de SimpleAccount. Ten en cuenta que el grifo requiere que tengas un saldo en la red principal en la dirección a la que se va a realizar la transferencia. Si ya dispones de ETH de prueba en otro monedero, también puedes transferirlo a la dirección de tu contrato inteligente (SimpleAccount) en lugar de utilizar primero el grifo.

Grifo multicadena de Quicknode

Aquí también puedes encontrar un grifo Sepolia alternativo. Úsalo bajo tu propia responsabilidad.

Iniciar una transferencia desde SimpleAccount a otra dirección

Una vez que hayamos ingresado fondos en nuestra cuenta de contrato inteligente (por ejemplo, SimpleAccount), ya podemos iniciar una transferencia desde dicha cuenta. Recomendamos disponer de al menos 0,01 ETH para probar una transferencia de ETH (más las comisiones de gas). Pega el siguiente comando en la ventana de tu terminal, pero recuerda sustituir los valores de marcador de posición, como dirección y eth con valores reales.

yarn run simpleTransferencia de cuenta --a {dirección} --importe {eth}

Para ver qué código se ejecuta al ejecutar este comando, ve a la scripts/simpleAccount/transfer.ts archivo.

En términos sencillos, el comando anterior implementa el SimpleAccount contrato, crea una carga útil UserOperation, la firma y, a continuación, la envía al Bundler (tal y como se define en config.json).

En términos técnicos, esto:

  • Toma la transferencia a la dirección (t) y la cantidad de éter (amt) en el principal función
  • A continuación, la función comprobará si hay middleware (en el caso de un pagador)
  • Inicializa un contrato SimpleAccount con los ajustes especificados en el archivo config.json
  • Analiza los valores de la dirección y el importe
  • Señala y llama al ejecutar función que recibe los valores anteriores como parámetros
  • Devuelve el hash de UserOperation y el hash de la transacción

Transferencia desde una cuenta de contrato inteligente - ERC-4337

Ahora, tómate un momento para revisar la transacción en Etherscan introduciendo el hash de la transacción. En la siguiente sección, analizaremos en detalle qué ocurrió exactamente durante estos pasos y qué podemos hacer a partir de ahora.

Análisis del ciclo de vida de las transacciones de la cuenta de un contrato inteligente

Ahora que la transferencia desde nuestra cuenta de contrato inteligente se ha realizado con éxito, veamos qué ha ocurrido realmente.

Volviendo a Etherscan, revisamos el hash de la transacción en la que se realizó la transferencia:

Etherscan

En el De campo, vemos que la transacción fue iniciada por una dirección diferente (es decir, 0x6892BEF4aE1b5cb33F9A175Ab822518c9025fc3C); esta es la Bundler tomando el Operación de usuario el objeto que hemos creado.

El A La dirección de campo (por ejemplo, 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789) hace referencia a la Punto de entrada contrato. Se trata del contrato al que recurre el Bundler incluido en la lista blanca para ejecutar paquetes de Operaciones de usuario.

Debajo del campo «Para», verás transferencias como: «Transferencia de 0,00000000069379401 ETH desde...»; repasemos cada una de ellas:


  • Transferir 0,00000000069379401 ETH desde 0xD44946...7BA5270F a 0x5FF137...026d2789: Esta es la transferencia desde nuestra cuenta de contrato inteligente al contrato EntryPoint
  • Transferir 0,02 ETH desde 0xD44946...7BA5270F a 0x115c2A...BE1a1529: Se trata de la transferencia de nuestros 0,2 ETH desde la dirección del contrato inteligente a la dirección que hemos introducido en el transferir --a {dirección} comando.
  • Transferir 0,000000000672934136 ETH desde 0x5FF137...026d2789 a 0x6892BE...9025fc3C: Se trata de la transferencia del EntryPoint al Bundler en concepto de comisión.

En el Datos de entrada campo, vemos que el handleOps El método fue invocado por nuestra cuenta de contrato inteligente, y se le pasaron datos como:

Función: handleOps((dirección, uint256, bytes, bytes, uint256, uint256, uint256, uint256, uint256, bytes, bytes)[], dirección)
# Nombre Tipo Datos
0 ops.sender dirección 0xD4494616f04ebd65E407330672c4C5A07BA5270F
0 ops.nonce uint256 0
0 ops.initCode bytes 0x9406cc6185a346906296840746125a0e449764545fbfb9cf0000000000000000000000001ca0e2981c4abd1c9aa20af4e5142cdf8ac68c4f0000000000000000000000000000000000000000000000000000000000000000
0 operaciones.callData bytes 0xb61d27f6000000000000000000000000115c2ac736dc0fe31b8e08e1c7475b08be1a152900000000000000000000000000000000000000000000000000470de4df82000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000
0 ops.callGasLimit uint256 17475
0 ops.verificationGasLimit uint256 341795
0 ops.preVerificationGas uint256 50048
0 ops.maxFeePerGas uint256 1695
0 ops.maxPriorityFeePerGas uint256 1649
0 ops.paymasterAndData bytes 0x
0 ops.firma bytes 0xc9fd2edd94f242be428591627ce921ae1e9aa66497fe7ae76e8c28afe719dc933bb85738173cb3dcc1f901a4e788c088043f9b7d95cac22b42bfb45db916b4f71c
2 dirección del beneficiario 0x6892BEF4aE1b5cb33F9A175Ab822518c9025fc3C

Lo que ocurrió fue que enviamos una UserOperation (tal y como se define en scripts/transfer.ts), a un mempool de Bundlers (tal y como se define en nuestra configuración de URL de RPC), tras lo cual los Bundlers toman nuestro Operación de usuario objeto y creó una transacción realizando una llamada al handleOps función en el Punto de entrada contrato, lo que permite llevar a cabo de forma efectiva la transferencia de ETH desde nuestra cuenta de contrato inteligente a la dirección que hayamos definido.

Pon a prueba tus conocimientos

¡Pon a prueba tus conocimientos sobre ERC-4337 con este breve cuestionario de 6 preguntas!

🧠Prueba de conocimientos
Para que se pueda implementar el ERC-4337, será necesario realizar cambios a nivel del protocolo

Conclusión

¡Enhorabuena! Has creado una cuenta de contrato inteligente utilizando ERC-4337 y Stackup, y a continuación has transferido fondos desde la dirección de tu contrato inteligente a otra dirección. Este proceso puede parecer sencillo y factible tanto con ERC-4337 como sin él; sin embargo, ten en cuenta que has abierto la puerta a muchas otras posibilidades que ofrece ERC-4337, como las comisiones de gas patrocinadas y las transacciones por lotes. Ahora que hemos realizado una transferencia sencilla, ¡podemos explorar otras funcionalidades!

Si tienes alguna duda, echa un vistazo al foro de Quicknode para obtener ayuda. Mantente al día de las últimas novedades siguiéndonos en Twitter (@Quicknode) o en Discord.

¡Nos encanta recibir comentarios!

Si tienes algún comentario o sugerencia sobre nuevos temas, no dudes en hacérnoslo saber. Nos encantaría saber tu opinión.

Recursos y ejemplos


Comparte esta guía