Estimated time: 5 minutes read
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.