Skip to Content
Advanced Mini CoursesGasless Onboarding (ERC4337, Bundlers, Paymasters, EIP7702)

Fixing Web3’s Biggest Onboarding Problem

Work in Progress

Please Note: This tutorial is currently under active development. The content, code examples, and explanations may be incomplete, contain errors, or change significantly without notice. Use with caution!

Are you losing users before they even start?

Have you ever built a dApp, only to watch new users disappear when they hit the “Connect Wallet & Get Gas” wall?

complicated meme

It’s a common and frustrating roadblock. Before anyone can use your app, they have to:

  • Figure out what a wallet is and install one.
  • Buy crypto on an exchange, a process filled with KYC, fees, and delays.
  • Transfer the right amount of a native token like ETH just to pay for gas.

This isn’t a small hurdle. It’s one of the main reasons Web3 struggles with adoption. It’s like building a great theme park but forcing everyone to solve a calculus problem to get a ticket.

What if you could remove that friction? What if you could onboard users as smoothly as the best Web2 apps, letting them experience your dApp’s value without the wallet and gas headache?

This tutorial is a playbook for creating seamless, gasless onboarding experiences. We’ll start with a simple scenario: giving users 100 free Platform Credits (CRED) when they sign up. Then we’ll look at why the standard Web3 approach makes this painful, and explore patterns to fix it, from off-chain signatures to Account Abstraction.

Who is this for?

  • For beginners: You see the potential of Web3 but are frustrated by the user experience. You need the foundational knowledge to build apps people actually use.
  • For developers: You know the basics, but you’re ready for advanced strategies like meta-transactions, ERC-2612 permits, ERC-4337 Account Abstraction, and a look at the future with EIP-7702.

Our goal: We’ll take a simple action-a user signing up and getting 100 free CRED to buy an NFT-and eliminate the hidden complexities of the standard Web3 flow, one technique at a time.

Let’s get started.

1. The Problem: Why the Standard Web3 Flow Fails Users

Companion Code

You can find the complete code for this section on GitHub in the example1 branch.

Let’s walk through the normal way someone might try to use their free 100 Platform Credits (CRED) to buy an NFT.

Imagine a user, Sarah, signs up for your platform with her email and sees “100 Free Credits Added!” in her account. These credits are on the blockchain as CRED tokens, so to use them, she needs a wallet.

  1. The Wallet Setup: First, she sees a prompt: “Install MetaMask to use your credits.” She installs the browser extension and writes down a secret phrase she doesn’t fully understand, hoping she won’t lose it. The platform links her email to this new wallet address, and the 100 CRED tokens appear. This initial step introduces technical setup and security anxiety.
  2. The Gas Wall: Sarah sees her 100 CRED in her wallet. She finds an NFT she likes for 100 credits and clicks “Buy NFT”, but gets an error: “Insufficient funds for gas.” She needs ETH to pay for the transaction, but she only has CRED tokens. Even with the right credits, she’s blocked.
  3. The On-Ramp: To solve this, you might add an on-ramp button: “Buy ETH with Card”. This seems easier, but it introduces its own problems. She might need to go through KYC, and she’ll likely face a minimum purchase of $50 or $100, even if she only needs $0.50 of ETH for gas. After fees, her $50 purchase might only get her $45 of ETH, and she still has to wait for the transaction to process.
  4. Connecting Again: After buying the ETH, Sarah returns to your site and has to connect her wallet again.
  5. The “Approve” Step: She clicks “Buy NFT”, but another popup appears: “Grant permission for the NFT contract to access your PlatformCredits?” She has to sign a transaction-and pay gas with the ETH she just bought-just to allow the purchase to happen later.
  6. The Final Purchase: After the ‘approve’ transaction confirms, Sarah clicks “Buy NFT” again. She signs another transaction and pays more gas. She might have spent over $50 just to use her 100 free credits for a transaction that cost pennies in gas.

This process involves at least six steps, multiple gas payments, confusing jargon, and a lot of waiting. It’s no surprise that many users give up.

no thanks

Feeling the Pain?

If this flow seems complicated, you’re right. It’s the frustrating reality we’re trying to fix.

Feel free to skim the code breakdown below if you’re already convinced this is a problem. The solutions start in Section 2.

Let’s Look Under the Hood (The Code):

Let’s define our basic contracts first.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title PlatformCredits * @dev Basic ERC20 token representing credits on our platform. * Includes a function for the owner (platform) to grant credits. */ contract PlatformCredits is ERC20, Ownable { // Assuming 18 decimals for credits, like ETH. Adjust if needed. constructor(address initialOwner) ERC20("PlatformCredits", "CRED") Ownable(initialOwner) {} /** * @dev Grants credits to a specific address. Only callable by the owner (platform). * Used for simulating the initial free credits grant upon signup. */ function grantCredits(address to, uint256 amount) public onlyOwner { _mint(to, amount); // Internally, granting credits is minting tokens } // Later, we will add ERC2612 permit functionality here! }

How the Frontend Orchestrates the Pain (Interactive Demo):

Even with helpful libraries like viem and wagmi, the frontend code reflects this awkward two-step dance (approve then buyNFT). Let’s see it in action.

Why This Flow is Broken:

This standard flow has several major problems:

  • The Wallet Mandate: Users need a wallet before they can receive anything of value.
  • The ETH Requirement: Users must acquire and hold ETH, even if your dApp uses other tokens. This is often the biggest barrier to use.
  • The Double Transaction: The ‘approve’ followed by the action is confusing and requires two separate gas payments.
  • Confusion and Delay: Multiple steps, blockchain confirmations, and unfamiliar concepts create a poor user experience.

This is the baseline we need to improve. Now that we’ve seen the problems, let’s look at how to solve them.

2. Solution 1: Sponsoring Gas with Meta-Transactions

The biggest hurdle for Sarah was the need for ETH to pay for gas. She had the 100 CRED tokens but couldn’t use them. This is where many users drop off.

What if you could make the gas fee invisible?

Imagine Sarah clicks “Buy NFT” and it just works, with no pop-ups asking for ETH. Behind the scenes, someone else pays the network fee. This is possible with meta-transactions.

The Core Idea: Using a Relayer

Instead of Sarah sending the transaction to the blockchain herself, she signs a message off-chain that describes what she wants to do. This signature costs nothing. The dApp sends this signed message to a relayer service.

The relayer takes Sarah’s signed message, verifies her signature, and then creates a real transaction. It pays the ETH gas fee and submits it to the blockchain.

The smart contract receives the transaction from the relayer. But how does it know Sarah was the one who wanted the NFT? This is solved by a standard called EIP-2771.

EIP-2771: Identifying the Original Sender

EIP-2771 provides a standard way for a contract to know the original sender of a request, even when it’s submitted by a trusted relayer (often called a “forwarder” in EIP-2771 terminology).

EIP2771

Here’s how it works with our contracts:

  • The Forwarder Contract: We need a trusted, on-chain “Forwarder” contract. The Relayer sends the transaction through this Forwarder. The Forwarder’s job is to verify Sarah’s signature on her original request and then call the actual target contract (MyNFT).
  • Modified MyNFT Contract: Our MyNFT contract needs a small but crucial modification. Instead of checking msg.sender (which would be the Forwarder address), it needs to ask, “Who is the original sender that the trusted Forwarder verified?” OpenZeppelin provides a handy helper contract, ERC2771Context, to make this easy.

Code Transformation: Making MyNFT EIP-2771 Aware

Let’s see how our MyNFT contract changes. We need to:

  1. Import and inherit ERC2771Context from OpenZeppelin.
  2. Add a constructor to store the address of the trustedForwarder contract.
  3. Replace all instances of msg.sender with _msgSender(). The _msgSender() function, provided by ERC2771Context, automatically checks if the call comes from the trusted forwarder. If it does, it extracts the original sender’s address (Sarah’s address) from the transaction data. Otherwise, it just returns the normal msg.sender.

Here’s the modified MyNFT contract:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; // Still Ownable for withdrawal etc. // Counters is no longer needed for simple increments in Solidity >=0.8.0 // Import the EIP-2771 context import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; // <--- Import /** * @title MyNFT (EIP-2771 Enabled) * @dev ERC721 contract allowing NFT purchase with PlatformCredits, * compatible with EIP-2771 meta-transactions. */ // Inherit ERC2771Context contract MyNFT is ERC721, Ownable, ERC2771Context { // <--- Inherit uint256 private _tokenIdCounter; // Start from 0, first token ID will be 1 IERC20 public immutable paymentCredits; uint256 public constant NFT_PRICE_IN_CREDITS = 100 * 10**18; event NFTMinted(address indexed buyer, uint256 indexed tokenId); // Constructor now takes the trusted forwarder address constructor( address initialOwner, address _paymentCreditsAddress, address trustedForwarder // <--- Add trusted forwarder param ) ERC721("MyNFT", "MYNFT") Ownable(initialOwner) ERC2771Context(trustedForwarder) // <--- Initialize ERC2771Context { require(_paymentCreditsAddress != address(0), "MyNFT: Invalid credits token address"); paymentCredits = IERC20(_paymentCreditsAddress); } /** * @dev Mints a new NFT after receiving payment in PlatformCredits. * Requires prior approval of PlatformCredits for this contract. * Uses _msgSender() to identify the original buyer in meta-tx scenarios. */ function buyNFT() public returns (uint256) { uint256 currentPrice = NFT_PRICE_IN_CREDITS; // Use _msgSender() instead of msg.sender address buyer = _msgSender(); // <--- Use _msgSender() // Check allowance for PlatformCredits (still needed!) uint256 allowance = paymentCredits.allowance(buyer, address(this)); require(allowance >= currentPrice, "MyNFT: Check credits allowance"); // Transfer PlatformCredits from buyer to owner/treasury // transferFrom uses the 'buyer' address obtained via _msgSender() bool success = paymentCredits.transferFrom(buyer, owner(), currentPrice); require(success, "MyNFT: Credits transfer failed"); // Mint the NFT to the original buyer _tokenIdCounter++; // Increment the counter uint256 newTokenId = _tokenIdCounter; // Assign the new ID _safeMint(buyer, newTokenId); // Mint to the original buyer emit NFTMinted(buyer, newTokenId); return newTokenId; } /** * @dev Withdraw collected credits. Only callable by the contract owner. * Note: Ownable's owner check uses the real msg.sender, which is fine here, * but the transfer target is the owner address derived potentially * from an initial deployment or transferOwnership call. * If owner actions were meant to be meta-tx compatible, Ownable would * also need adjustment or replacement with an EIP-2771 compatible version. */ function withdrawCredits() public onlyOwner { // onlyOwner uses actual msg.sender uint256 balance = paymentCredits.balanceOf(address(this)); if (balance > 0) { // Transfer to the owner address stored by Ownable bool success = paymentCredits.transfer(owner(), balance); require(success, "MyNFT: Credits withdrawal failed"); } } // --- EIP-2771 Overrides --- // Optional: If you need to override the trusted forwarder later, // you can override the trustedForwarder() function from ERC2771Context. // function trustedForwarder() public view virtual override returns (address) { // // return address(YOUR_NEW_FORWARDER); // return super.trustedForwarder(); // Keep original if not changing // } // The _msgSender() and _msgData() functions are inherited from ERC2771Context. }
The 'Approve' Problem Persists

There’s a key limitation here. While the buyNFT call can be sponsored by a relayer, the user still needs to call approve on the PlatformCredits contract first.

That approve transaction still costs gas and must be sent from the user’s account before the meta-transaction for buyNFT can work. This pattern doesn’t make the entire flow gasless. We still need to solve the initial approval hurdle.

Frontend: Talking to the Relayer

The frontend code changes slightly. Instead of preparing and sending a transaction directly to the blockchain using useWriteContract, it now:

  1. Gets the user (Sarah) to sign the intent (the action she wants the Forwarder to execute on her behalf) using useSignTypedData. This signature is specific to the Forwarder’s requirements (EIP-712).
  2. Sends this signed message and the action details to the Relayer’s API endpoint.
  3. Waits for the Relayer to process the request and return a transaction hash.

Here’s the conceptual frontend code again, highlighting the interaction with the Relayer:

import { encodeFunctionData, parseAbi } from 'viem'; import { useAccount, useSignTypedData, useReadContract } from 'wagmi'; // useReadContract for nonce // Assume Forwarder contract address and ABI are known const forwarderAddress = "0x..."; const forwarderABI = parseAbi([ 'function getNonce(address from) view returns (uint256)', // Example EIP-2771 execute function signature (adjust based on actual Forwarder) 'function execute(tuple(address from, address to, uint256 value, uint256 gas, uint256 nonce, bytes data) req, bytes signature) payable', ]); // Assume MyNFT ABI and address are known const myNFTAddress = "0x..."; const myNFTABI = [ /* ... MyNFT ABI including buyNFT ... */ ] as const; // Assume Relayer API endpoint is known const relayerApiEndpoint = '/api/relay'; // Your backend relayer service // EIP-712 Domain and Types specific to YOUR Forwarder contract's execute function // These define the structure the user needs to sign. const domain = { name: "MyForwarder", version: "1", chainId: 1, verifyingContract: forwarderAddress }; // Example const types = { ForwardRequest: [ // Example structure - MUST match your Forwarder's expected struct { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'gas', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'data', type: 'bytes' }, ] } as const; function MetaTxButton() { const { address } = useAccount(); const { signTypedDataAsync, isPending: isSigning } = useSignTypedData(); const [isRelaying, setIsRelaying] = useState(false); const [status, setStatus] = useState(''); // Fetch the current nonce for the user *from the Forwarder* const { data: nonce, isLoading: isLoadingNonce } = useReadContract({ address: forwarderAddress, abi: forwarderABI, functionName: 'getNonce', args: [address!], // Ensure address is not null enabled: !!address, // Only run query if address exists }); const handleMetaTxPurchase = async () => { if (!address || nonce === undefined) { setStatus("Error: Wallet not connected or nonce not loaded."); return; } setIsRelaying(true); setStatus("Preparing meta-transaction..."); // 1. Craft the inner function call data (calling MyNFT.buyNFT()) const targetCallData = encodeFunctionData({ abi: myNFTABI, functionName: 'buyNFT', args: [], }); // 2. Prepare the ForwardRequest message for signing // User signs this structure, authorizing the Forwarder to act const message = { from: address, to: myNFTAddress, // The target contract the Forwarder will call value: 0n, // We are not sending ETH in this inner call gas: 150000n, // Estimate gas needed for MyNFT.buyNFT() (Relayer might adjust) nonce: nonce, // Nonce from the Forwarder to prevent replays data: targetCallData, // The action for the Forwarder to execute }; try { // 3. Sign the typed data (EIP-712 signature) setStatus("Requesting signature from wallet..."); console.log("Signing message:", message); const signature = await signTypedDataAsync({ domain, types, primaryType: 'ForwardRequest', message, }); setStatus("Signature obtained. Sending to Relayer..."); console.log("Signature:", signature); // 4. Send the signed request and signature to YOUR Relayer backend console.log("Sending signed message to relayer API:", relayerApiEndpoint); const response = await fetch(relayerApiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, // The body structure depends entirely on YOUR Relayer API design body: JSON.stringify({ forwardRequest: message, // The message that was signed signature: signature, // The signature itself // You might include other info your relayer needs }), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`Relayer request failed: ${response.status} ${response.statusText} - ${errorBody}`); } const result = await response.json(); // Assuming relayer returns JSON { transactionHash: "0x..." } setStatus(`Relayer accepted! Tx Hash: ${result.transactionHash}. Waiting for confirmation...`); console.log("Relayer response:", result); // You would typically then use the txHash to monitor confirmation status } catch (error: any) { console.error("Meta-transaction failed:", error); setStatus(`Error: ${error.message}`); } finally { setIsRelaying(false); } }; return ( <div> <button onClick={handleMetaTxPurchase} disabled={!address || nonce === undefined || isLoadingNonce || isSigning || isRelaying} > {isLoadingNonce ? 'Loading Nonce...' : isSigning ? 'Waiting for Signature...' : isRelaying ? 'Relaying Transaction...' : 'Buy NFT (Meta-Transaction)'} </button> {status && <p style={{ marginTop: '10px', wordBreak: 'break-word' }}>Status: {status}</p>} </div> ); }
Interactive Demo Skipped for EIP-2771

While EIP-2771 meta-transactions were an important early solution, setting up a full interactive demo requires a dedicated Relayer backend service, which adds significant complexity beyond the scope of this tutorial’s focus on smart contract patterns and frontend interactions.

Furthermore, newer patterns like ERC-2612 Permits (for approvals) and especially ERC-4337 Account Abstraction (for full transaction abstraction) offer more standardized, flexible, and often preferred solutions today.

Therefore, we’ll skip the interactive demo for this specific pattern and focus our interactive efforts on Account Abstraction later in the tutorial, which better represents the current state-of-the-art for gasless experiences.

Trade-offs of the Relayer Model

This relayer model is a good solution, but it has some important trade-offs:

  1. Centralization and Trust: Your gasless flow depends on your relayer service being online, funded, and trustworthy. If the relayer goes down, your users can’t perform gasless actions. This is different from the ERC-4337 model, which uses a decentralized network of “bundlers” and removes single points of failure. You are often locked into the API and requirements of your chosen relayer.
  2. Contract Modification: This pattern only works if the target smart contract is designed to use EIP-2771 and _msgSender(). You cannot use this method to interact with existing contracts that haven’t been built this way, like a standard Uniswap router.
  3. The approve Problem: The user still needs to call approve on the PlatformCredits contract, which costs gas. While the buyNFT step becomes gasless for the user, the entire flow is not.
  4. Relayer Costs: Someone has to pay the gas. The relayer service pays the ETH, but they will likely pass this cost back to you, the dApp provider, potentially with a markup.
Complexity & Security

Implementing a secure and reliable meta-transaction system involves careful smart contract design (using ERC2771Context correctly, handling nonces) and robust backend infrastructure (the Relayer must validate signatures, manage its own nonces and gas, and protect against spam/replay attacks). It’s a significant undertaking.

Pros:

  • Hides Gas from User: The primary benefit – users with supported wallets (EOAs) don’t need native tokens (ETH) for the sponsored transaction itself.
  • Works with Existing Wallets: Users don’t need a new type of wallet; their standard EOA (like MetaMask) works for signing.

Cons:

  • Relayer Infrastructure: Requires building or relying on a centralized Relayer service (cost, uptime, trust, potential lock-in).
  • Contract Modification: Target contracts must be EIP-2771 compatible (_msgSender()). Not backward compatible with existing, unmodified contracts.
  • Doesn’t Solve approve Gas: The crucial initial token approval (PlatformCredits.approve(...)) still costs the user gas and must be done separately before this meta-transaction flow can be used for the main action.
  • Complexity: Requires careful coordination between frontend, relayer backend, and EIP-2771 aware contracts.

The Gas Station Network (GSN) was an early, more decentralized attempt at providing shared Relayer infrastructure, but implementing and using it also involves specific contract modifications and understanding its architecture.

A picture showing the contract changes needed to support the Gas station network

The problem is that GSN is becoming outdated. Many nodes are no longer working, according to its status page, and the project has not been updated recently.

A picture showing the broken GSN Relayer Network Status

You can run your own paymaster and relayer service, but that defeats the purpose of using a ready-made solution. At this stage, GSN does not work out of the box.

Meta-transactions were a crucial first step in solving the gas problem. They showed that gas could be abstracted away from the user, but the reliance on specific relayers and the need for contract changes showed the need for better solutions. Next, we’ll look at how to handle token approvals.

3. Solution 2: Gasless Approvals with ERC-2612 Permits

Companion Code

You can find the complete code for this section on GitHub in the example2 branch.

Meta-transactions can sponsor the gas for the buyNFT call, but the user still has to make a separate approve transaction and pay gas for it.

This is like giving someone a VIP pass but making them pay to get a wristband to use it.

What if we could eliminate the approve transaction? What if a user could grant permission without sending a transaction and paying for gas? This is possible with ERC-2612 permits.

Instead of sending two transactions, the user can sign a permission message off-chain.

This is a digitally signed message created using the EIP-712 standard. EIP-712 provides a way to create secure, off-chain messages that are easy to verify. The message states: “I, the signer, grant permission for the MyNFT contract to spend up to 100 of my CRED tokens, and this permission is valid until a specific time.”

Sarah signs this message in her wallet. It costs nothing because it’s an off-chain action.

How Permits Work:

  1. Sarah Signs the Slip: Sarah clicks “Buy NFT”. The dApp prepares the EIP-712 “Permit” message and asks her to sign it. Click, sign, done. No gas.
  2. Frontend Bundles the Request: The dApp now takes Sarah’s main request (“Buy the NFT!”) and attaches this signed permission slip (the permit signature) to it.
  3. One Transaction to Rule Them All: The dApp sends one single transaction to the MyNFT contract. This transaction calls a new function we’ll add, let’s call it buyNFTWithPermit.
  4. The Contract Uses the Slip: Inside the buyNFTWithPermit function, the first thing the MyNFT contract does is take Sarah’s signed permission slip and present it to the PlatformCredits contract by calling its special permit() function.
  5. PlatformCredits Verifies: The PlatformCredits contract (which we’ll upgrade slightly) looks at the signed slip. Using the magic of cryptography (specifically ecrecover on the EIP-712 signature), it verifies:
    • Was this really signed by Sarah?
    • Has this specific permission slip been used before (checking a ‘nonce’)?
    • Is it still valid (checking the ‘deadline’)?
    • If everything checks out, the PlatformCredits contract says, “Okay, permission granted!” and internally updates the allowance for the MyNFT contract without needing a separate transaction from Sarah.
  6. Purchase Proceeds: Now that the allowance is set within the same transaction, the rest of the buyNFTWithPermit function in MyNFT can proceed immediately. It calls transferFrom on PlatformCredits (which now succeeds!) and mints the NFT to Sarah.

One transaction. Zero gas spent by Sarah on approval. The annoying approve step? Obliterated!

Code Transformation: Giving Our Contracts Permit Power

This requires two key changes:

  1. Upgrade PlatformCredits.sol: It needs the ability to understand and process these magic permission slips. Thankfully, OpenZeppelin makes this easy! We just need to inherit their ERC20Permit contract.

    // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; // Import ERC20Permit import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; // <--- Import /** * @title PlatformCredits (with Permit) * @dev ERC20 token now including ERC-2612 permit functionality. */ // Inherit ERC20Permit contract PlatformCredits is ERC20, Ownable, ERC20Permit { // <--- Inherit constructor(address initialOwner) ERC20("PlatformCredits", "CRED") Ownable(initialOwner) // Initialize ERC20Permit with the token name (must match ERC20 name) ERC20Permit("PlatformCredits") // <--- Initialize Permit {} function grantCredits(address to, uint256 amount) public onlyOwner { _mint(to, amount); } // The permit() function and nonce tracking are inherited from ERC20Permit! }
  2. Add buyNFTWithPermit to MyNFT.sol: We add a new function that accepts the permit signature components (v, r, s, deadline) along with the purchase request.

    // Inside MyNFT contract... /** * @dev Mints a new NFT using PlatformCredits, leveraging ERC2612 permit. * Allows purchase without a prior separate 'approve' transaction. * @param deadline The permit deadline timestamp. * @param v The recovery byte of the permit signature. * @param r The r value of the permit signature. * @param s The s value of the permit signature. */ function buyNFTWithPermit( uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public returns (uint256) { uint256 currentPrice = NFT_PRICE_IN_CREDITS; address buyer = msg.sender; // The user sending this transaction // Step 1: Call permit on the PlatformCredits contract // This grants allowance if the signature is valid // It will revert if the signature is invalid, expired, or already used. paymentCredits.permit(buyer, address(this), currentPrice, deadline, v, r, s); // Step 2: Allowance is now set, proceed with transferFrom bool success = paymentCredits.transferFrom(buyer, owner(), currentPrice); require(success, "MyNFT: Credits transfer failed after permit"); // Step 3: Mint the NFT _tokenIdCounter++; // Increment the counter uint256 newTokenId = _tokenIdCounter; // Assign the new ID _safeMint(buyer, newTokenId); emit NFTMinted(buyer, newTokenId); return newTokenId; }

Frontend: The Slight Shift

The frontend now needs to:

  1. Prepare the EIP-712 Permit message.
  2. Get the user to sign it using signTypedDataAsync.
  3. Split the signature into v, r, s components.
  4. Call the new buyNFTWithPermit function via useWriteContract, passing the signature components and deadline.
Try It Yourself: The Two-Step Permit Flow

Connect your wallet to the Sepolia Testnet. This interactive demo walks you through the full Permit flow:

  1. Step 1 (Off-Chain & Gasless): First, you’ll sign the EIP-712 Permit message. This happens entirely in your wallet and costs zero gas. We’ll break down exactly what you’re signing.
  2. Step 2 (On-Chain & Requires Gas): Once you have the signature, you’ll use it to execute the buyNFTWithPermit transaction. This step submits the signature to the blockchain and requires gas (ETH), clearly demonstrating that while the approval is gasless, the main action still needs to be paid for.

You’ll need some Sepolia ETH for Step 2. Try a Sepolia Faucet if you’re out. Ensure the placeholder contract addresses in the component code are replaced with your actual deployed addresses.

With ERC-2612 permits, we’ve eliminated one of the most confusing and costly steps in the standard flow. The user signs one message off-chain for free, then submits one transaction on-chain. It’s smoother, cheaper, and closer to the seamless experience users expect.

Pros:

  • Eliminates approve Gas: The biggest win. Saves users one transaction fee.
  • Better UX: Reduces the number of steps and confusing pop-ups.
  • Standardized: EIP-2612 is a widely adopted standard for many tokens.
  • Atomic: Approval and action happen within the same transaction.

Cons:

  • Token Must Support It: The PlatformCredits token must implement ERC20Permit. You can’t use this on tokens that don’t have the permit function.
  • Main Transaction Still Costs Gas: While approval is gasless, the buyNFTWithPermit transaction itself still requires gas paid by the user.

We’ve removed the need for a separate approve transaction, which is a big improvement. But the user still needs ETH to pay for the main purchase. The next step is to make the entire flow gasless.

4. Solution 3: Combining Permits and Relayers

Companion Code

You can find the complete code for this section on GitHub in the example3 branch.

We’ve made the approve step gasless with ERC-2612 permits, but the user still has to submit the buyNFTWithPermit transaction and pay for gas. The final hurdle is to make the purchase itself gasless.

We can do this by combining gasless permits with the relayer model we saw earlier.

The Idea: A Double Signature

The user provides two off-chain signatures:

  1. Signature #1 (The Permit): A standard ERC-2612 Permit signature. This is Sarah’s permission slip for the MyNFT contract to access her CRED tokens. It’s off-chain and gasless.
  2. Signature #2 (The Action): A second, custom EIP-712 signature. This is Sarah’s explicit, off-chain command authorizing the MyNFT contract to execute the buyNFT logic on her behalf. It’s also off-chain and gasless.

The frontend collects both of these signatures. Now, instead of Sarah sending a transaction, she hands this “bundle of proof” to a Relayer.

The Relayer receives the two signatures and calls a new, specialized function on our MyNFT contract, something like buyNFTWithSignatureAndPermit. This function is designed to:

  1. Verify the Action Signature to confirm the user authorized this specific purchase.
  2. Use the Permit Signature to grant itself allowance for the CRED tokens.
  3. Execute the purchase by transferring the CRED and minting the NFT to the user.

From Sarah’s perspective, the entire flow is just two quick, free signature requests in her wallet. The transaction, the gas, the complexity – it’s all handled by the Relayer. This is the pinnacle of gasless UX for a standard wallet (EOA).

Code Transformation: The Relayer-Ready Contract

To make this work, we need to add the new buyNFTWithSignatureAndPermit function to our MyNFT contract. Let’s look at the code and break down the critical difference.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title MyNFT * @dev Contract now supports both user-submitted and relayer-submitted purchases. */ contract MyNFT is ERC721, Ownable, EIP712 { uint256 private _tokenIdCounter; IERC20Permit public immutable paymentCredits; uint256 public constant NFT_PRICE_IN_CREDITS = 100 * 10**18; // Nonce for the action signature to prevent replay attacks mapping(address => uint256) public actionNonces; // Hash of the EIP-712 struct for the action bytes32 private constant BUY_NFT_ACTION_TYPEHASH = keccak256( "BuyNFTAction(address user,uint256 nonce)" ); event NFTMinted(address indexed buyer, uint256 indexed tokenId); constructor(address initialOwner, address _paymentCreditsAddress) ERC721("MyNFT", "MYNFT") EIP712("MyNFT", "1") // Initialize EIP712 domain separator Ownable(initialOwner) { paymentCredits = IERC20Permit(_paymentCreditsAddress); } /** * @notice Mints an NFT using a permit, but requires the USER to be msg.sender. * @dev This function is NOT relayable because it trusts msg.sender implicitly. */ function buyNFTWithPermit( uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public returns (uint256) { // CRITICAL: 'buyer' is the one who sent the transaction. address buyer = msg.sender; // The permit must be from the 'buyer' (msg.sender). paymentCredits.permit(buyer, address(this), NFT_PRICE_IN_CREDITS, deadline, v, r, s); // Transfer credits from the 'buyer' (msg.sender). paymentCredits.transferFrom(buyer, owner(), NFT_PRICE_IN_CREDITS); _tokenIdCounter++; uint256 newTokenId = _tokenIdCounter; _safeMint(buyer, newTokenId); emit NFTMinted(buyer, newTokenId); return newTokenId; } /** * @notice Mints an NFT using a permit AND an action signature. * @dev This function IS relayable. It authenticates the 'user' via signature, * not by trusting msg.sender. A relayer can be msg.sender. */ function buyNFTWithSignatureAndPermit( address user, uint256 permitDeadline, uint8 permitV, bytes32 permitR, bytes32 permitS, uint8 actionV, bytes32 actionR, bytes32 actionS ) public returns (uint256) { // Step 1: Verify the action signature to prove 'user' wants to do this. // We no longer trust msg.sender. We trust the signature. bytes32 digest = _hashTypedDataV4(keccak256( abi.encode(BUY_NFT_ACTION_TYPEHASH, user, actionNonces[user]) )); address signer = ECDSA.recover(digest, actionV, actionR, actionS); require(signer == user, "MyNFT: Invalid action signature"); actionNonces[user]++; // Prevent replay of the action // Step 2: Use the user's permit to get approval for their tokens. paymentCredits.permit(user, address(this), NFT_PRICE_IN_CREDITS, permitDeadline, permitV, permitR, permitS); // Step 3: Transfer credits from the 'user', not the relayer. paymentCredits.transferFrom(user, owner(), NFT_PRICE_IN_CREDITS); // Step 4: Mint the NFT to the 'user', not the relayer. _tokenIdCounter++; uint256 newTokenId = _tokenIdCounter; _safeMint(user, newTokenId); emit NFTMinted(user, newTokenId); return newTokenId; } }
The Key Difference: `msg.sender` vs. Signature

Look closely at the two functions.

  • buyNFTWithPermit is simple, but it’s not relayable. It assumes address buyer = msg.sender;. This means the person who calls the function is the person buying the NFT. If a relayer called this, the relayer would be charged for the NFT and would receive it.
  • buyNFTWithSignatureAndPermit is relayable. It ignores msg.sender. Instead, it takes a user address as an argument and uses ECDSA.recover to prove that the user actually signed the request. This allows anyone-a relayer-to be msg.sender while the contract correctly identifies, charges, and delivers the NFT to the actual user.

This is the difference between a user-paid transaction and a fully sponsored, gasless experience.

Try It Yourself: The Double Signature Flow

This interactive demo walks you through the completely off-chain signing process. You will sign two messages, neither of which costs any gas. The demo will then show you the final JSON payload containing both signatures. This payload is what you would send to a relayer service to execute the transaction on your behalf, paying the gas fee for you.

The Problem with Custom Solutions

This solution provides a great user experience. The user signs two messages off-chain and gets their NFT without paying for gas. But this approach has a major limitation: it only works for our specific dApp. It’s a private road, not a public highway.

What happens when the user wants to use their CRED tokens on another platform? What if they want to interact with a contract you don’t control, like Uniswap? What if the token they want to use is USDT, which doesn’t support the ERC-2612 permit function?

Your custom solution breaks.

This is the central challenge of all custom relayer and signature-based models:

  • They are not universal. They only work for the specific contracts and tokens they were designed for.
  • They are not backward-compatible. You can’t force an existing contract like Uniswap’s router to understand your unique signature scheme.
  • They create a fragmented ecosystem. Each dApp has to invent its own off-chain agreement with its relayer.

This pattern solves the user’s gas problem for a single, controlled interaction, but it doesn’t provide a scalable or decentralized solution for the entire ecosystem.

The lack of a standard, open market for transaction sponsorship is a central problem. We need a system that can work with any contract. This is the motivation for our final solution: a public, permissionless system where users can submit transaction intents to an open market of “bundlers” (similar to relayers), with fees handled by “paymaster” contracts.

This system exists. It’s called ERC-4337 Account Abstraction.

5. Solution 4: Account Abstraction (ERC-4337)

Our previous solution works well, but it doesn’t scale or compose with other applications. It’s a custom solution, not a universal one.

We need a public system for transactions where gas sponsorship is a native feature.

This is the problem that ERC-4337 Account Abstraction solves. It’s a redesign of how transactions work on Ethereum.

The Big Idea: The Programmable Wallet

With ERC-4337, users get a Smart Contract Wallet (SCW) instead of a simple key (an EOA). Think of it as a programmable safe that becomes their on-chain identity.

Here are the new players on this superhighway:

  • Smart Contract Wallet (SCW): Your user’s new account. It’s a smart contract, a programmable safe that holds their assets and defines its own rules for what makes a transaction valid.
  • UserOperation: A “to-do list” for your safe. Instead of a rigid transaction, a user creates a flexible UserOperation object that says, “Here’s what I want to do.”
  • Bundler: A network of independent delivery drivers. They hang out in a special “mempool,” grab UserOperations, and compete to be the one to deliver them to the main office.
  • EntryPoint: The main office. A single, global, and trusted contract that receives all the to-do lists from Bundlers. It verifies everything and ensures the SCWs execute their tasks correctly.
  • Paymaster: The sponsorship office. A Paymaster can look at a UserOperation and tell the EntryPoint, “I’ll cover the delivery fee for this one.” This is how gas sponsorship becomes a standardized, open-market feature.
Important: Bundlers are Not Blockchain Nodes

You might be thinking this means more RPC URLs to manage, but a Bundler RPC endpoint is not a regular blockchain node RPC. You can’t ask it for the latest block number or a user’s ETH balance. It’s a specialized service with one job: to listen for UserOperation objects, validate them, and get them onto the blockchain.

  • A Blockchain Node RPC is like a public library where you can read any on-chain data.
  • A Bundler RPC is like a courier service. You give it a UserOperation, and its only job is to deliver it to the EntryPoint contract.

The same is true for a Paymaster RPC. It’s a sponsorship office. You send it a UserOperation and ask, “Will you pay for this?”

This separation of concerns is what makes the ERC-4337 ecosystem powerful and decentralized.

The Most Important Detail About Account Abstraction

When a Smart Contract Wallet performs an action, who does the final contract (like our MyNFT contract) see as the caller?

It sees the Smart Contract Wallet itself.

The msg.sender is no longer the user’s EOA. The EOA is just the remote control; the SCW is the one interacting with the MyNFT contract. This fundamental shift is the key to how Account Abstraction works.

5.1. The “Pure” AA Model: Your Smart Wallet is Your New Identity

Companion Code

You can find the complete code for this section on GitHub in the example4 branch.

In this model, we embrace the msg.sender shift completely. Sarah’s EOA is just for signing authorizations. Her real on-chain identity, the one that owns everything, becomes her Smart Contract Wallet.

Here’s the flow:

  1. When Sarah signs up, we give the 100 free CRED tokens directly to her SCW address.
  2. The dApp helps her create a UserOperation where the “to-do” is: “Call the buyNFT() function on the MyNFT contract.”
  3. A Paymaster agrees to sponsor it, and a Bundler submits it to the EntryPoint.
  4. The EntryPoint tells Sarah’s SCW to execute the buyNFT() call.
  5. The MyNFT contract sees the call coming in. It checks msg.sender… and sees the SCW’s address. It checks the CRED balance of the SCW, takes the payment from the SCW, and mints the new NFT… directly to the SCW’s address.

The EOA is the hand, the SCW is the arm. The arm does all the work and holds all the items. This is the cleanest, most forward-facing, and powerful way to use Account Abstraction. The user’s assets and identity live in one secure, programmable place.

Building a UserOperation From Scratch: A Step-by-Step Autopsy

Most tutorials show you a library function like prepareUserOperation that magically creates the whole object for you. That’s great for production, but terrible for learning. To truly understand ERC-4337, we must build a UserOperation from the ground up, piece by piece.

A UserOperation is just a JSON object. It’s a structured request that says, “I want this smart contract wallet to do this specific thing.” Let’s dissect each field, following the logical order of construction as shown in the interactive demo below.

  1. callData: The “to-do list” for the SCW. This is the most important and often most complex part, as it represents the user’s intent. It’s the encoded instruction of what you want the sender wallet to do. This can be a single action or, more powerfully, a batch of multiple actions combined into one atomic operation.

    For our example, buying an NFT with credits requires three steps, which we will batch together:

    1. Call topUpCredits() on the PlatformCredits contract to give our Smart Wallet the necessary funds. In a real dApp, this might be handled by a trusted backend or a promotional faucet, but we include it in the batch for demonstration. It’s like a faucet to get Free Tokens.
    2. Call approve() on the PlatformCredits contract to allow the MyNFT contract to spend the credits.
    3. Call buyNFT() on the MyNFT contract.

    A Smart Contract Wallet can execute these in a single, atomic transaction. But how? It’s crucial to distinguish between two types of “batching” in the ERC-4337 ecosystem:

    • Bundler Batching: A user sends their UserOperation to a Bundler, not directly to the EntryPoint contract. The Bundler has the sole discretion to bundle multiple UserOperations (often from different users) into a single transaction that it sends to the EntryPoint.handleOps function. This batching does not guarantee atomicity or execution order between the different UserOperations. It’s a mechanism for the Bundler to optimize its on-chain submissions.
    • User-Controlled Batching (Multi-call): This is when a single user needs to execute multiple actions (e.g., approve then buyNFT) and guarantee they happen together, in order, and atomically. The user cannot simply send two UserOperations to the Bundler and hope for the best. Instead, they must encode all their desired actions into the callData of a single UserOperation. This is the batching that users control.

    To achieve this user-controlled batching, the logic must be implemented by the Smart Contract Wallet itself. Some account implementations have a native executeBatch function. However, a popular implementation like Safe does not support this out of the box. To batch transactions for a Safe account, the user’s UserOperation must contain callData for a delegatecall to a separate, trusted MultiSend contract. This MultiSend contract then executes the multiple actions on the user’s behalf, guaranteeing atomicity and order. This is the approach used by Safe{Wallet} and is what we demonstrate in the interactive demo.

    However, this layered approach is complex. The real magic of modern libraries like viem is that they abstract this entire process away. As you’ll see in the final step of the demo, instead of manually encoding everything, you can simply provide a calls array to a function like prepareUserOperation. The library handles all the complex encoding behind the scenes, giving you the final UserOperation object.

  2. sender: The address of the Smart Contract Wallet (SCW) that will execute the transaction. Now that we know what we want to do (callData), we need to specify who will do it. This is the address of the programmable safe we’re giving the user, not their EOA address.

  3. Gas, Fees, and Payment: This is the most misunderstood part of a UserOperation. In a normal transaction, the gas fields are used to pay the block producer (miner/validator) directly. In ERC-4337, the payment flow is different:

    • The gas and fee fields in a UserOperation are instructions for the EntryPoint contract.
    • The Bundler first simulates the UserOperation to calculate the required gas and ensure it will get paid. This is what prepareUserOperation does behind the scenes.
    • When the Bundler submits the transaction to the EntryPoint, it pays the ETH gas fee for the entire transaction out of its own pocket.
    • The EntryPoint then executes the UserOperation and, at the end, uses the gas fields from the UserOperation to reimburse the Bundler.
    • This reimbursement comes from one of two places:
      1. The Smart Contract Wallet: If the paymasterAndData field is empty, the EntryPoint will withdraw ETH directly from the sender’s balance.
      2. A Paymaster: If paymasterAndData is populated, the EntryPoint will withdraw ETH from the Paymaster’s staked funds on the EntryPoint contract. The paymasterAndData field contains the Paymaster’s address and often a signature from the Paymaster authorizing this specific payment.

    This is why a Bundler will reject a UserOperation if the SCW has no ETH and there is no Paymaster. The simulation fails because the Bundler knows it won’t get reimbursed. Our interactive demo below will show this exact scenario.

  4. signature: The user’s “authorization slip.” This is the signature of the user (from their EOA) over the hash of the entire UserOperation (excluding the signature itself). This proves the user authorized this specific operation.

Now that we know the components, let’s build one interactively.

An Ecosystem of Tools

The beauty of a standard like ERC-4337 is that it creates a vibrant, competitive ecosystem. While our examples use the excellent viem and permissionless.js libraries for their modern design and performance, they are not your only option!

Because all these tools speak the same ERC-4337 language, they are largely interchangeable. You could swap out a bundler from Pimlico for one from Alchemy, or use a Paymaster from Biconomy. You could also use different client libraries like Candide’s AbstractionKit or Ethers-based libraries like ethers-core-5 and userop.js. This prevents vendor lock-in and lets you choose the best components for your specific needs.

Interactive Demo: Building a UserOperation Step-by-Step

Let’s see this in action. The interactive demo below first walks you through the complex, multi-layered process of constructing the callData for a batch transaction. Then, it shows how a modern library can abstract this away.

Crucially, it also demonstrates the ERC-4337 payment model by first attempting to prepare the UserOperation without a sponsor, which will fail. Then, it adds a Paymaster to successfully prepare the final, sponsored UserOperation.

5.2. The Hybrid Model: AA as a “Super-Relayer” for Your EOA

But what if you’re not ready to go all-in? What if your users (or your contracts) are deeply tied to their EOAs? What if Sarah insists on holding the final NFT in her good old MetaMask address?

Can we still use the beautiful, decentralized ERC-4337 highway just to pay for gas?

Absolutely. This is where we combine our previous secrets. We treat the entire AA system as the most sophisticated Relayer ever built.

Here’s how this mind-bending hybrid works:

  1. Sarah’s EOA still holds the 100 CRED tokens.
  2. She signs the exact same double signature from our “Ultimate Combo” pattern: one Permit for the CRED token, and one signature authorizing the buyNFT action.
  3. Now, we wrap these two signatures into a UserOperation. The “to-do list” for the SCW is: “Call this special buyNFTWithSignatureAndPermit function on the MyNFT contract and pass it these two signatures from Sarah’s EOA.”
  4. A Paymaster sponsors the UserOperation. A Bundler submits it.
  5. The MyNFT contract receives the call from the SCW. But the special function logic ignores msg.sender! Instead, it uses the signatures to cryptographically verify that the EOA authorized the action. It then pulls the CRED from the EOA (via the permit) and mints the NFT directly to Sarah’s EOA address.

In this model, the Smart Contract Wallet is just a temporary, gas-sponsored tool. A disposable execution environment. The EOA remains the star of the show and the ultimate owner of the assets. This shows the incredible flexibility of ERC-4337, but it comes at the cost of re-introducing the complexity of custom signature-verifying functions on your target contracts.

Interactive Demo Coming Soon!

An interactive demo for the Hybrid AA Model will also be added, showcasing how to use the AA infrastructure as a powerful relayer for EOA-centric operations.

6. The Future: EIP-7702

Companion Code

You can find the work-in-progress code for this section on GitHub in the example6 branch.

Concept: A new transaction type (0x04) allows an EOA to temporarily adopt the code of a smart contract for a single transaction. This enables features like batching and gas sponsorship for regular wallets.

How it Works (Simplified):

  1. User: Uses their regular EOA (e.g., MetaMask).
  2. Frontend:
    • Identifies the user wants to perform an action (e.g., buyNFT).
    • Uses a Delegate Contract: A pre-deployed contract designed to perform the required actions (e.g., call PlatformCredits.permit then MyNFT.buyNFT). This contract contains the logic the EOA will temporarily adopt.
    • Constructs an EIP-7702 transaction. This involves specifying the delegate contract address, the call data for that contract, and getting an authorization signature from the user.
    • Sends this transaction request to a Sponsor/Relayer service.
  3. Sponsor/Relayer Service:
    • Verifies the authorization.
    • Submits the valid EIP-7702 transaction to the network, paying the gas.
  4. On-Chain Execution (Post-Pectra Hardfork):
    • The EVM recognizes the EIP-7702 transaction and verifies the user’s signature.
    • It temporarily sets the user’s EOA code to the delegate contract’s code.
    • The delegate contract’s code executes in the context of the user’s EOA, performing the batch call (permit + buyNFT).
    • The EOA’s code reverts to normal after the transaction.

Implementation Sketch (Conceptual JS using wagmi & viem - EIP-7702):

// NOTE: This is highly conceptual as EIP-7702 SDKs/Wallet support are still emerging. import { sepolia } from 'viem/chains'; import { encodeFunctionData, parseAbi, parseEther, Hex } from 'viem'; import { useAccount, useWalletClient, useSignTypedData } from 'wagmi'; import { ethers } from 'ethers'; // For signature splitting (or use viem utils) // Assume an EIP-7702 compatible SDK/Relayer client exists // import { sendEip7702Transaction } from 'your-eip7702-sdk'; // Address of the pre-deployed Delegate Contract (must be trusted) const delegateContractAddress = "0x..."; // Replace with actual address // ABI of the Delegate Contract const delegateABI = parseAbi([ // Example: Function expects permit signature components 'function executePermitAndBuy(address creditsToken, address nftContract, uint256 price, uint256 deadline, uint8 v, bytes32 r, bytes32 s)', ]); // Assume PlatformCredits implements permit const platformCreditsAddress = "0x..."; // Replace with actual address const myNFTAddress = "0x..."; // Replace with actual address const nftPriceInCredits = parseEther("100"); // Example price in CRED // EIP-712 Domain/Types for PlatformCredits permit signature const permitDomain = { /* ... EIP712 domain for PlatformCredits ... */ }; // Replace with actual domain const permitTypes = { Permit: [ /* ... Permit fields ... */ ] }; // Replace with actual types function GaslessEip7702Button() { const { address } = useAccount(); const { data: walletClient } = useWalletClient(); // Needed for signing EIP-7702 auth const { signTypedDataAsync } = useSignTypedData(); // For signing the permit const handleEip7702Purchase = async () => { if (!address || !walletClient) { console.error("Wallet not connected"); return; } console.log("Starting gasless EIP-7702 NFT purchase..."); try { // --- Step 1: Get Permit Signature for PlatformCredits (Off-chain) --- const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour // Fetch actual permit nonce from PlatformCredits contract using viem/wagmi readContract const permitNonce = 0n; // Replace with actual fetched nonce const permitMessage = { owner: address, spender: delegateContractAddress, // The delegate contract needs the allowance value: nftPriceInCredits, // Use price in credits nonce: permitNonce, // Use actual nonce deadline: deadline, }; console.log("Requesting permit signature..."); const permitSignature = await signTypedDataAsync({ domain: permitDomain, types: permitTypes, primaryType: 'Permit', message: permitMessage, }); console.log("Permit signature obtained:", permitSignature); // Split signature using viem/ethers utility const { v, r, s } = ethers.Signature.from(permitSignature); // Replace with viem equivalent if preferred // --- Step 2: Prepare the Delegate Call Data --- const delegateCallData = encodeFunctionData({ abi: delegateABI, functionName: 'executePermitAndBuy', args: [ platformCreditsAddress, myNFTAddress, nftPriceInCredits, deadline, v, // Pass signature components r, s, ], }); // --- Step 3: Construct and Send EIP-7702 Transaction via Sponsor/Relayer --- // This part is highly dependent on wallet support and SDKs which are TBD. // It would involve: // 1. Getting an authorization signature from the user via walletClient (e.g., a modified eth_signTransaction) // that approves delegating to `delegateContractAddress` for this specific call (`delegateCallData`). // 2. Sending the transaction details (delegateAddress, delegateCallData, authorization) // to a Sponsor/Relayer service. console.log("Constructing EIP-7702 transaction (Conceptual)..."); // const authorizationSignature = await walletClient.signEip7702Authorization(...) // Speculative // Use an SDK function to send via relayer (API is speculative) // const relayerResponse = await sendEip7702Transaction({ // delegateAddress: delegateContractAddress, // delegateCallData: delegateCallData, // authorization: authorizationSignature, // Speculative // // Potentially other params like nonce, gas limits for the relayer // }); // console.log("EIP-7702 Transaction submitted via Relayer, Hash:", relayerResponse.transactionHash); // Speculative // // Wait for confirmation (SDK might provide a helper) // const receipt = await relayerResponse.wait(); // Speculative // console.log("EIP-7702 Transaction confirmed:", receipt.transactionHash); // Speculative console.log("Conceptual EIP-7702 flow complete. NFT Purchase initiated (if infra existed)."); alert("EIP-7702 flow is conceptual. Transaction not actually sent."); } catch (error) { console.error("EIP-7702 Purchase failed:", error); alert(`Error: ${error.message}`); } }; return ( <button onClick={handleEip7702Purchase} disabled={!address || !walletClient}> Buy NFT (EIP-7702 - Conceptual) </button> ); }
EIP-7702 is New!

EIP-7702 is currently included in the upcoming Pectra hardfork (expected late 2024/early 2025). Wallet support (for the new authorization signing), SDK implementations, relayer infrastructure, and security best practices for delegate contracts are still under development. The code above is highly conceptual and will not work until the ecosystem matures post-hardfork.

Pros:

  • Works directly with existing EOAs (massive user base).
  • Enables native EOA batching and sponsorship.
  • Potentially simpler migration path for dApps compared to full AA.
  • Designed for forward compatibility with ERC-4337.

Cons:

  • Very new, requires Pectra hardfork inclusion and widespread infra/wallet adoption.
  • Security of delegate contracts is paramount.
  • Introduces new complexities around transaction validity and state changes for EOAs.

7. Comparison and Choosing the Right Approach

FeatureMeta-Transactions (Relayer)ERC-2612 PermitsDirect Sig + PermitERC-4337 (AA)EIP-7702
Primary GoalGas SponsorshipGasless ApproveGas SponsorshipFull AA / Gasless TxEOA Batch/Sponsor
Account TypeEOAEOAEOASCWEOA
Needs ETH? (User)No (if Relayer pays)Yes (for main tx)No (if Relayer pays)No (if Paymaster)No (if Sponsor)
BatchingComplexNoYes (in Contract)Yes (Native in SCW)Yes (via Delegate)
Requires Contract Mod?Yes (EIP-2771)Yes (Token+Target)Yes (Target Contract)Yes (SCW logic)Yes (Delegate Contract)
InfrastructureRelayer BackendNoneRelayer BackendBundler, PaymasterSponsor/Relayer (7702)
StandardizationCustom / EIP-2771EIP-2612CustomERC-4337EIP-7702
MaturityMature ConceptMatureCustom / Less CommonGrowingNew / Future

When to Use What:

  • Simple ERC20 approve removal: Use ERC-2612 if the token supports it. It’s clean and standard.
  • Gas sponsorship for specific contract actions: Consider Direct Signature + Permit if you control the target contract and prefer embedding logic there.
  • Full gas sponsorship & advanced features for a new dApp: Go with ERC-4337. Build with Account Abstraction from the start.
  • Support existing EOA users with batching/sponsorship: EIP-7702 (once available and supported) offers a powerful bridge.
  • Basic gas sponsorship for EOAs: Traditional Meta-Transactions (EIP-2771) are still viable, but compare their overhead with AA SDKs.

Conclusion

We’ve seen how to move from clunky, gas-heavy interactions to smoother, gasless flows. While traditional transactions are still the foundation of Ethereum, solutions like ERC-2612, ERC-4337, and the upcoming EIP-7702 give developers powerful tools to improve the user onboarding experience.

  • ERC-2612 tackles the common approve pain point.
  • ERC-4337 offers a flexible, future-proof framework for gas abstraction and advanced wallet features.
  • EIP-7702 aims to bring batching and sponsorship capabilities directly to existing EOAs.

By using these patterns, you can build dApps that are more accessible and easier to use. Choose the right tool for your needs, focus on the user experience, and help build a better decentralized future.

Last updated on