Back to the engineering blog

Building with Account Abstraction: ERC-4337 Tutorial

Learn to build with account abstraction using ERC-4337. This tutorial covers smart contract wallets, UserOp bundlers, paymasters, and the best account a...

Building with Account Abstraction: ERC-4337 and Beyond

TL;DR

Account abstraction (AA) replaces externally owned accounts (EOAs) with smart contract wallets, enabling gasless transactions, social recovery, session keys, and batched operations. ERC-4337 introduced this without protocol-level changes, and newer standards like ERC-7702 (Pectra, 2025) and EIP-7560 push the ecosystem further. This account abstraction tutorial walks you through building a smart contract wallet, deploying a paymaster, and integrating with a UserOp bundler using production-ready SDKs.

---

What Is Account Abstraction?

Account abstraction decouples transaction validation from Ethereum's rigid EOA model. Instead of requiring every transaction to originate from a private-key-signed EOA, AA allows smart contract wallets to define their own authentication logic, gas payment strategies, and execution flows.

Why It Matters in 2026

  • User onboarding: Users can sign in with email, passkeys, or social login — no seed phrases.
  • Gasless UX: Paymasters sponsor gas fees, so users never need to hold ETH.
  • Programmable security: Multi-sig, time-locks, spending limits, and social recovery are native.
  • Batched transactions: Approve + swap in a single UserOperation.

---

ERC-4337 Architecture Overview

ERC-4337 introduced a higher-level mempool for UserOperation objects. Here's the architecture:

User → UserOperation → Bundler → EntryPoint Contract → Smart Contract Wallet
                                        ↕
                                   Paymaster (optional gas sponsorship)

Key Components

| Component | Role | |-----------|------| | UserOperation (UserOp) | A pseudo-transaction struct containing sender, calldata, gas limits, and signature | | Bundler | An off-chain service that batches UserOps and submits them to the EntryPoint | | EntryPoint | A singleton contract (v0.7+) that validates and executes UserOps | | Smart Contract Wallet | The user's on-chain account with custom validation logic | | Paymaster | An optional contract that sponsors gas fees on behalf of users | | Account Factory | Deploys new smart contract wallets via CREATE2 for deterministic addresses |

---

Step 1: Building a Minimal Smart Contract Wallet

Let's build a minimal ERC-4337-compatible wallet. The wallet must implement the IAccount interface from the EntryPoint:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@account-abstraction/contracts/interfaces/IAccount.sol";
import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

contract SimpleSmartWallet is IAccount {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;

    address public owner;
    IEntryPoint private immutable _entryPoint;

    constructor(IEntryPoint entryPoint_, address owner_) {
        _entryPoint = entryPoint_;
        owner = owner_;
    }

    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external override returns (uint256 validationData) {
        require(msg.sender == address(_entryPoint), "only EntryPoint");

        // Verify the signature
        bytes32 ethSignedHash = userOpHash.toEthSignedMessageHash();
        address recovered = ethSignedHash.recover(userOp.signature);

        if (recovered != owner) {
            return 1; // SIG_VALIDATION_FAILED
        }

        // Prefund the EntryPoint if needed
        if (missingAccountFunds > 0) {
            (bool success, ) = payable(msg.sender).call{
                value: missingAccountFunds
            }("");
            require(success, "prefund failed");
        }

        return 0; // SIG_VALIDATION_SUCCESS
    }

    function execute(
        address target,
        uint256 value,
        bytes calldata data
    ) external {
        require(msg.sender == address(_entryPoint), "only EntryPoint");
        (bool success, bytes memory result) = target.call{value: value}(data);
        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }
    }

    receive() external payable {}
}

This is a production-starting-point for ERC-4337 development. The validateUserOp function is where all custom authentication logic lives — you could replace ECDSA with WebAuthn/passkey verification, multi-sig, or any scheme.

---

Step 2: Writing a Paymaster in Solidity

A paymaster Solidity contract sponsors gas for users. Here's a simple verifying paymaster:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@account-abstraction/contracts/core/BasePaymaster.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

contract VerifyingPaymaster is BasePaymaster {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;

    address public verifyingSigner;

    constructor(
        IEntryPoint entryPoint_,
        address signer_
    ) BasePaymaster(entryPoint_) {
        verifyingSigner = signer_;
    }

    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) internal view override returns (bytes memory context, uint256 validationData) {
        // Extract the paymaster signature from paymasterAndData
        bytes calldata signature = userOp.paymasterAndData[20:];

        bytes32 ethSignedHash = userOpHash.toEthSignedMessageHash();
        address recovered = ethSignedHash.recover(signature);

        if (recovered != verifyingSigner) {
            return ("", 1); // Reject
        }

        return ("", 0); // Approved
    }
}

The paymaster must be staked and funded on the EntryPoint. When a UserOp includes paymasterAndData, the EntryPoint calls the paymaster to validate and deducts gas costs from the paymaster's deposit instead of the user's wallet.

---

Step 3: Integrating a UserOp Bundler

The UserOp bundler is the off-chain infrastructure that collects UserOperations and submits them on-chain. Popular bundler options in 2026:

| Bundler | Language | Notes | |---------|----------|-------| | Rundler (Alchemy) | Rust | Production-grade, high throughput | | Stackup Bundler | Go | Open-source, easy to self-host | | Infinitism Bundler | TypeScript | Reference implementation | | Pimlico Alto | TypeScript | Feature-rich, widely adopted |

Sending a UserOp via an Account Abstraction SDK

Here's how to construct and send a UserOp using permissionless.js (by Pimlico), one of the leading account abstraction SDK options:

import { createSmartAccountClient } from "permissionless";
import { toSimpleSmartAccount } from "permissionless/accounts";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// 1. Set up clients
const publicClient = createPublicClient({
  chain: sepolia,
  transport: http("https://rpc.sepolia.org"),
});

const pimlicoClient = createPimlicoClient({
  transport: http("https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_KEY"),
  entryPoint: { address: "0x0000000071727De22E5E9d8BAf0edAc6f37da032", version: "0.7" },
});

// 2. Create smart account
const signer = privateKeyToAccount("0xYOUR_PRIVATE_KEY");

const smartAccount = await toSimpleSmartAccount({
  client: publicClient,
  owner: signer,
  entryPoint: { address: "0x0000000071727De22E5E9d8BAf0edAc6f37da032", version: "0.7" },
});

// 3. Create smart account client with bundler + paymaster
const smartAccountClient = createSmartAccountClient({
  account: smartAccount,
  chain: sepolia,
  bundlerTransport: http("https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_KEY"),
  paymaster: pimlicoClient,
});

// 4. Send a gasless transaction
const txHash = await smartAccountClient.sendTransaction({
  to: "0xRecipientAddress",
  value: 0n,
  data: "0x",
});

console.log("Transaction hash:", txHash);

This sends a gasless transaction — the paymaster covers the fees, and the bundler handles submission.

---

Beyond ERC-4337: What's New in 2026

ERC-7702 (Live Since Pectra, 2025)

ERC-7702 allows EOAs to temporarily delegate to smart contract code within a single transaction. This means existing MetaMask users can get AA features (batching, sponsorship) without migrating to a new address.

// ERC-7702 authorization tuple (signed by EOA)
// Allows the EOA to behave as a smart contract wallet for one tx
{
  chainId: 1,
  address: "0xSmartWalletImplementation",
  nonce: 42,
  yParity: 0, r: "0x...", s: "0x..."
}

EIP-7560: Native Account Abstraction

EIP-7560 proposes enshrining AA into the protocol itself, eliminating the need for a separate UserOp mempool. While still in development, it represents the long-term direction for Ethereum.

Key Differences

| Feature | ERC-4337 | ERC-7702 | EIP-7560 | |---------|----------|----------|----------| | Protocol change required | No | Yes (Pectra) | Yes (future) | | Works with existing EOAs | No | Yes | Yes | | Separate mempool | Yes | No | No | | Production ready | Yes | Yes | No |

---

People Also Ask

What is the difference between ERC-4337 and ERC-7702?

ERC-4337 uses a separate mempool and EntryPoint contract to enable smart contract wallets without protocol changes. ERC-7702 (shipped with Pectra in 2025) lets existing EOAs temporarily adopt smart contract logic, enabling AA features without changing your address. They're complementary — many wallets use both.

Do users need ETH to use account abstraction?

No. With a paymaster, gas fees can be sponsored by a dApp or paid in ERC-20 tokens (like USDC). This is one of the primary UX benefits of account abstraction.

Which account abstraction SDK should I use?

The most popular options in 2026 are permissionless.js (Pimlico), Alchemy's aa-sdk, ZeroDev SDK, and Biconomy SDK. Choose based on your bundler preference and feature needs (session keys, multi-chain support, passkey integration).

Is account abstraction safe for production?

Yes. The ERC-4337 EntryPoint v0.7 has been audited multiple times and secures billions in assets. However, custom wallet and paymaster implementations should always be independently audited.

---

Actionable Takeaways

  1. Start with an SDK: Don't build from scratch. Use permissionless.js or aa-sdk to handle UserOp construction, bundler communication, and paymaster integration.
  2. Use EntryPoint v0.7: It's the current standard. Avoid v0.6 for new projects.
  3. Consider ERC-7702 for EOA users: If your users already have MetaMask wallets, ERC-7702 lets them access AA features without migration.
  4. Always audit your paymaster: A misconfigured paymaster can be drained. Implement spending limits, whitelisting, and rate limiting.
  5. Test on Sepolia first: Use Pimlico or Alchemy's free testnet bundlers before deploying to mainnet.
  6. Plan for multi-chain: Account abstraction works across EVM chains. Use CREATE2 factories for deterministic addresses across networks.

---

Last updated: August 1, 2026. Built and tested against EntryPoint v0.7 and Solidity 0.8.24+.

Related Reading