Tiempo estimado: 5 minutos de lectura

Las transacciones sin gas eliminan la barrera del pago de los costes en la blockchain, mejorando la experiencia de uso en las DApps. Se explora la abstracción de cuentas mediante los estándares ERC-4337 y ERC-7677 para patrocinar transacciones y facilitar la adopción de la web3 reduciendo la fricción para nuevos usuarios.

¿Qué son las transacciones sin gas?

La ejecución de transacciones sobre los Smart Contracts en la blockchain pública conlleva unos costos, denominados gas, que deben asumirse como contraprestación por el uso de la infraestructura. Para profundizar en este tema, recomiendo leer mi artículo sobre la estimación del gas consumido en los Smart Contracts

Se trata de un concepto clave para entender y asegurar la descentralización de las redes blockchain no permisionadas.

Sin embargo, desde el punto de vista de la experiencia de usuario, constituye uno de los principales elementos de fricción que frena la adopción y uso de servicios web3 entre el público general.

Esto es así porque habitualmente se obliga al usuario a disponer de moneda nativa de la blockchain en su wallet autocustodiado para hacer frente a los costes de gas. A día de hoy la adquisión de criptomonedas en autocustodia no es un proceso sencillo.

Las transacciones sin gas son aquellas en que el usuario queda libre de tener que pagar los costes de la transacción, siendo estos afrontados por un tercero.

Esto mejora enormemente la experiencia de usuario y favorece el embarque de público no habituado a web3, al no requerir del aprovisionamiento previo de criptomonedas.

¿Cómo se ejecutan las transacciones sin gas?

El patrocionio de los costes de gas en las transacciones de la blockchain es algo sobre lo que se viene trabajando hace años.

Sin embargo, no ha sido hasta fechas relativamente recientes cuando se han desarrollado los estándares adecuados que están impulsando su uso en distintas aplicaciones descentralizadas

El más extendido es el relativo a la abstracción de cuentas, el ERC-4337. Dentro de ese estándar, el pago de los costos de gas recae en el componente llamado «paymaster«.  

Se trata de un smart contract que entre otras funciones puede permitir:

  • Patrocinar las transacciones a partir de un conjunto de reglas propias del servicio.
  • Aceptar un token ERC-20 para pagar el gas.
  • Desarrollar esquemas de pago a través de un modelo de suscripción.

Integrar las transacciones sin gas en una DApp

Una de las formas más versátiles y flexibles de integrar transacciones sin gas en una DApp consiste en seguir las pautas del estándar ERC-7677.

Se trata de una norma que define las interacciones entre la DApp, el bundler y el paymaster mediante llamadas normalizadas a métodos RPC facilitados por los endpoints de los proveedores de servicio.

De este modo, aunque se introduzca un elemento centralizado dentro de la arquitectura web3, se dota a la solución de cierta independencia con el proveedor, puesto que se podrían intercambiar endpoints e incluso mezclar bundlers y paymasters de distintos vendedores.

La propuesta planteada hace uso de las funciones de abstracción de cuentas de la librería viem. Se trata de un paquete que está ganando mucha tracción en el mundo del desarrollo web3 como alternativa a «ethers.js» y a la obsoleta «web3.js». 

El núcleo de la solución radica en el método createBundlerClient. Permite definir toda la secuencia de llamadas a los endpoints del bundler y el paymaster para componer adecuadamente el userOperation.

Se esquematiza en el siguiente extracto de código fuente: 

// Inits Viem bundlerClient using standarized RPC calls according to ERC-7677
const initBundler = async (smartAccount, publicClient) => {
  
    // viem createPaymasterClient
    const paymasterClient = createPaymasterClient({
        transport: http(paymasterEndpointURL),
    });

    // viem createBundlerClient
    const bundlerClient = createBundlerClient({
        account: smartAccount,   // smart wallet account 
        client: publicClient,    // viem publicClient created through createPublicClient    
        paymaster: {             
                       
            async getPaymasterStubData(userOperation) {
                console.log("getPaymasterStubData userOperation", userOperation);   

                const { sender, nonce, factory, factoryData, callData, maxFeePerGas, 
                        maxPriorityFeePerGas, signature } = userOperation;

                let partialUserOperation = {
                    sender,
                    nonce: numberToHex(nonce),
                    callData,
                    signature: signature,                
                    factory,
                    factoryData,
                    maxFeePerGas: numberToHex(maxFeePerGas),
                    maxPriorityFeePerGas: numberToHex(maxPriorityFeePerGas),
                    paymasterVerificationGasLimit: "0x0",
                    paymasterPostOpGasLimit: "0x0"
                }    
        
                // Calls pm_getPaymasterStubData RPC method
                let paymasterAndData = await paymasterClient.getPaymasterStubData(userOperation);
                                 
                partialUserOperation = {
                    ...partialUserOperation,
                    ...paymasterAndData.paymasterData,
                    ...paymasterAndData.paymaster
                }
                               
                const requestData = {
                    jsonrpc: '2.0',
                    method: 'eth_estimateUserOperationGas',
                    id: Date.now(),
                    params: [
                        partialUserOperation,
                        entryPoint07Address
                    ],
                };

                // Calls eth_estimateUserOperationGas
                const result = await postMethod(bundlerEndpointURL, requestData, "POST", 30000);
                let gasEstimates = await result.json();                
                gasEstimates = gasEstimates.result;                                  
                
                let paymasterStubData = {                    
                    paymaster: undefined,
                    maxFeePerGas: BigInt(gasEstimates.maxFeePerGas),
                    maxPriorityFeePerGas: BigInt(gasEstimates.maxPriorityFeePerGas),
                    callGasLimit: BigInt(gasEstimates.callGasLimit),
                    verificationGasLimit: BigInt(gasEstimates.verificationGasLimit),
                    preVerificationGas: BigInt(gasEstimates.preVerificationGas),                
                    paymasterVerificationGasLimit: BigInt(gasEstimates.paymasterVerificationGasLimit),
                    paymasterPostOpGasLimit: BigInt(gasEstimates.paymasterPostOpGasLimit)                    
                }            

                return paymasterStubData;                
            },
            async getPaymasterData(userOperation) {
                console.log("getPaymasterData userOperation", userOperation);
        
                // Calls pm_getPaymasterData RPC method
                paymasterAndData = await paymasterClient.getPaymasterData(userOperation);   

                return paymasterAndData;                    
                
            }
        },    
        
        // Paymaster context information, vendor dependant
        paymasterContext: PAYMASTER_CONTEXT,
        
        transport: http(bundlerEndpointURL),
    });

    return bundlerClient;
}

El bundlerClient devuelto lleva embebidas las llamadas a «pm_getPaymasterStubData», «eth_estimateUserOperationGas» y «pm_getPaymasterData», siguiendo el equema del estándar ERC-7677.

El parámetro «smartAccount» representa el smart wallet account que interaccionará con el smart contract. 

En el siguiente fragmento de código se ilustra el uso de la factoría de Safe para crear una smart account a partir de la EOA del wallet del usuario usando la librería permissionless de Pimlico:

smartAccount = await toSafeSmartAccount({
        client: publicClient, // viem publicClient
        entryPoint: {
            address: ENTRYPOINT_ADDRESS,
            version: "0.7",
        },
        owners: [walletClient], // viem walletClient       
        version: "1.4.1"
});

Por último se resume el envío de la userOperation a la blockchain utilizando el bundlerClient para una transacción de quemado de un token ERC-721:

const inputParams = {
    abi: erc721MintableAbi,
    functionName: 'burn',
    args: [tokenId],
}

const plainCallData = {
    to: contractAddress,
    data: encodeFunctionData(inputParams), // encodeFunctionData from viem
    value: 0n
}; 

// 2D nonce           
let nonce = await smartAccount.getNonce({key: BigInt(nonceKey)});

userOperation = {
    calls: [
        { ...plainCallData }
    ],
    nonce,
}

const hash = await bundlerClient.sendUserOperation(userOperation);

await bundlerClient.waitForUserOperationReceipt({
    hash,
}).then(async (result) => {
    console.log("UserOperation receipt", result);
    let boolResult = (/true/i).test(result.success);
    // Transaction is confirmed
    if (boolResult) {
        console.log("Success");
    }
    // Transaction failed
    else {
        console.log("Failure");
    }    
}).catch(async (error) => {
    console.log("Error getting transaction receipt", error);    
});

Conclusión: patrocinando las transacciones de tus usuarios

El pago de los costes de gas de las transacciones con la blockchain supone una de las mayores barreras de entrada a la hora de embarcar a nuevos usuarios en el uso de aplicaciones descentralizadas web3.

Cuando sea posible, patrocinar los costes de modo que los usuarios experimenten transacciones sin gas, puede suponer una gran diferencia en la adopción de un servicio.

En este artículo se ha ejemplificado una vía de patrocinio a partir del estándar de abstracción de cuentas, ERC-4337, siguiendo además las pautas de la norma ERC-7677 para conseguir una implementación independiente del proveedor.


¿Has patrocinado las transacciones de tus usuarios en alguna DApp? ¿Qué técnicas o estándares has seguido? Te invito a que comentes tus experiencias.

Si tienes dificultades para que los usuarios se embarquen en tus aplicaciones web3, tal vez te interese patrocinar las transacciones. Cuéntame tu caso y busquemos una solución.