Estimated time: 5 minutes read

Gasless transactions remove the barrier of paying blockchain fees, enhancing the user experience in DApps. Account abstraction is explored through the ERC-4337 and ERC-7677 standards to sponsor transactions and facilitate web3 adoption by reducing friction for new users.

What are gasless transactions?

Executing transactions on smart contracts in a public blockchain incurs costs, known as gas, which must be covered as compensation for using the infrastructure. To delve deeper into this topic, I recommend reading my article on estimating gas consumption in smart contracts.

This is a key concept for understanding and ensuring the decentralization of permissionless blockchain networks.

However, from a user experience perspective, gas fees represent one of the main friction points hindering the adoption and use of web3 services among the general public.

This is because users are typically required to have the blockchain’s native currency in their self-custodial wallet to cover gas costs. As of today, acquiring cryptocurrencies in self-custody is not a straightforward process.

Gasless transactions allow users to execute transactions without having to pay the associated gas fees, as these costs are covered by a third party.

This significantly improves the user experience and facilitates the onboarding of non-web3-native users, as they no longer need to pre-fund their wallets with cryptocurrencies.

How are gasless transactions executed?

Gas sponsorship for blockchain transactions has been an area of research for several years.

However, it was only recently that suitable standards were developed, driving its adoption in various decentralized applications.

The most widely adopted approach is account abstraction, specifically the ERC-4337 standard. Within this framework, gas fees are covered by a component called the paymaster.

This component is a smart contract that can:

  • Sponsor transactions based on service-specific rules.
  • Accept an ERC-20 token to pay for gas.
  • Implement subscription-based payment models.

Integrating gasless transactions into a DApp

One of the most versatile and flexible ways to integrate gasless transactions into a DApp is by following the ERC-7677 standard.

This standard defines how the DApp, bundler, and paymaster interact using standardized RPC method calls supplied by service provider endpoints.

While this introduces a centralized element into the web3 architecture, it also grants some degree of independence from the provider, as different endpoints can be swapped or even mixed across multiple bundlers and paymasters.

The proposed approach leverages account abstraction features from the Viem library. This package is gaining significant traction in the web3 development ecosystem as an alternative to “ethers.js” and the now deprecated “web3.js”.

At the core of this solution lies the createBundlerClient method, which defines the entire sequence of calls to the bundler and paymaster endpoints to properly compose the userOperation.

This process is illustrated in the following code snippet:

// 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;
}

The returned bundlerClient embeds the calls to “pm_getPaymasterStubData”, “eth_estimateUserOperationGas”, and “pm_getPaymasterData”, following the ERC-7677 standard framework.

The “smartAccount” parameter represents the smart wallet account that will interact with the smart contract.

The following code snippet illustrates how to use the Safe factory to create a smart account from the user’s EOA wallet, leveraging Pimlico’s permissionless library:

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

Finally, the submission of the userOperation to the blockchain is summarized using the bundlerClient for an ERC-721 token burn transaction:

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);    
});

Conclusion: Sponsoring your users’ transactions

Paying gas fees for blockchain transactions is one of the biggest entry barriers when onboarding new users to decentralized web3 applications.

Whenever possible, sponsoring gas fees so that users experience gasless transactions can make a significant difference in the adoption of a service.

This article has demonstrated a sponsorship approach based on the ERC-4337 account abstraction standard, while also following the ERC-7677 guidelines to achieve a provider-independent implementation.


Have you sponsored your users’ transactions in any DApp? What techniques or standards have you used? I invite you to share your experiences.

If you’re struggling to onboard users to your web3 applications, sponsoring transactions might be a solution worth considering. Tell me about your case, and let’s find a solution together.