Account Abstraction Tutorial Advanced: Build Production-Ready ERC-4337 Smart Wallets

11 min read

ERC-4337 advanced account abstraction architecture diagram showing EntryPoint, Paymaster, and smart wallet nodes
  • Key Takeaway 1: Mastering validateUserOp is the foundation of every advanced ERC-4337 smart account — get it wrong and your wallet is either locked or wide open.
  • Key Takeaway 2: A paymaster is what makes gasless transactions real. You can write one in about 30 lines of Solidity once you understand its two validation functions.
  • Key Takeaway 3: Session keys and multi-call batching are the UX breakthroughs that separate advanced wallets from basic ones — and both are achievable without changing your core account logic.
  • Key Takeaway 4: Social recovery protects users from the biggest risk in self-custody — losing their key. Building it into your wallet at the start is far easier than adding it later.
  • Key Takeaway 5: If you know Chialisp puzzles, you already understand the mental model. ERC-4337’s validateUserOp is Ethereum’s version of a coin puzzle — both decide what makes a spend valid.

This advanced account abstraction tutorial walks you through building production-grade ERC-4337 smart contract wallets from scratch — covering custom signature validation, paymaster contracts, session keys, multi-call batching, and social recovery. If you haven’t read Account Abstraction Tutorial: Build Your First ERC-4337 Smart Wallet yet, start there before continuing here.

What This Advanced Account Abstraction Tutorial Covers

The beginner guide got you through the concepts: UserOperation, EntryPoint, bundler, and the alt mempool. This guide is where things get interesting. Now we’re going to build — real Solidity code, real patterns, real security traps to dodge. By the end, you will have a working advanced smart account with a paymaster and session keys, tested in Foundry, and ready to deploy on a testnet.

Ethereum co-founder Vitalik Buterin put the core idea simply when introducing ERC-4337: “Instead of modifying the logic of the consensus layer itself, we replicate the functionality of the transaction mempool in a higher-level system.”1 That shift — moving wallet logic to a higher layer — is what gives you the flexibility to build everything in this guide. The consensus layer stays untouched. You program the rules.

One more thing before we dive in. If you have Chialisp experience, you are already halfway there. The ERC-4337 mental model maps cleanly onto Chia’s coin-puzzle-solution model. Chialisp puzzles decide what makes a coin spend valid. ERC-4337’s validateUserOp decides what makes a UserOperation valid. Same idea, different language. We’ll point out those parallels as we go. And if you want a deeper look at Chialisp’s smart contract design philosophy, this ChiaTribe guide on Chialisp and next-gen smart contracts is a great companion read.

Setting Up Your Foundry Environment for Advanced ERC-4337 Work

Foundry is the right tool for this. It has fast testing loops, inline debugging, and works well with the official eth-infinitism/account-abstraction library2 that gives you the audited base contracts you’ll inherit from.

Start a fresh Foundry project and install two dependencies:

forge init my-advanced-wallet
cd my-advanced-wallet
forge install eth-infinitism/account-abstraction
forge install OpenZeppelin/openzeppelin-contracts

Set your foundry.toml to use the correct remappings:

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[profile.default.optimizer]

enabled = true runs = 200

The optimizer matters here. The EntryPoint contract is large, and without optimization enabled you’ll hit bytecode size limits.3 With your environment ready, let’s write the account.

The Advanced ERC-4337 Feature Map: What to Build and When

FeatureWhat It DoesBest ForComplexity
Custom validateUserOpDefines what signatures or conditions make a UserOp validAll advanced wallets — this is always your starting pointMedium
Paymaster ContractPays gas on behalf of users or accepts ERC-20 paymentdApps wanting gasless onboarding; gaming platformsMedium
Session KeysGrants a temporary key limited signing rightsGames, subscriptions, automated DeFi actionsMedium–High
Multi-Call BatchingExecutes multiple contract calls in one UserOpDeFi flows (approve + swap in one step)Low
Social RecoveryLets guardians replace a lost keyConsumer wallets where seed-phrase loss is a real riskHigh
Multisig ValidationRequires M-of-N approvals before executingTreasury wallets, DAO operations, corporate accountsHigh

Writing a Minimal Smart Account from Scratch

Every ERC-4337 smart account must implement the IAccount interface, which means providing exactly one required function: validateUserOp. This is the gatekeeper. The EntryPoint calls it before executing any operation. Think of it as the Chialisp puzzle — it says “here are the conditions for a valid spend.” If validation passes, the EntryPoint moves forward. If not, the whole operation reverts.

The validateUserOp Function

Here is a clean minimal account contract that validates single-owner ECDSA signatures. This is the correct starting point before adding any advanced features on top:

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

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 MinimalAccount is IAccount {
    using ECDSA for bytes32;

    address public immutable owner;
    IEntryPoint private immutable _entryPoint;

    constructor(address entryPointAddress, address _owner) {
        _entryPoint = IEntryPoint(entryPointAddress);
        owner = _owner;
    }

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

        address signer = MessageHashUtils
            .toEthSignedMessageHash(userOpHash)
            .recover(userOp.signature);

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

        if (missingAccountFunds > 0) {
            (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
            require(success, "Gas funding failed");
        }

        return 0; // SIG_VALIDATION_SUCCESS
    }

    receive() external payable {}
}

Two things matter most here. First, always check that msg.sender == address(_entryPoint). Any attacker could call validateUserOp directly and try to trick your wallet. The EntryPoint guard blocks that entirely. Second, the missingAccountFunds parameter tells you how much ETH the EntryPoint needs upfront to cover gas. Pay it unconditionally if it’s above zero — this is not optional if you want your account to work without a paymaster.

The userOpHash already includes the EntryPoint address and the chain ID. This is what makes replay attacks across different chains impossible by design — you don’t have to add extra replay protection manually.

The execute Function

Your account also needs a way to actually do things — call other contracts, send ETH, interact with DeFi. Add an execute function gated to the EntryPoint only:

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

The raw assembly revert bubble ensures the original revert reason from the target contract propagates back to you. This is critical for debugging — without it, all your transaction failures look identical.

Building a Paymaster: Real Gasless Transactions

A paymaster is a smart contract that takes on the gas cost for a UserOperation.4 This is the feature that lets your users interact with a dApp without ever holding ETH. The paymaster stakes ETH with the EntryPoint in advance and draws from that balance as it sponsors transactions.

There are two functions a paymaster must implement: _validatePaymasterUserOp (run before execution — decide whether to sponsor) and _postOp (run after execution — handle cleanup, ERC-20 charging, etc.).5

A Whitelist Paymaster in Solidity

Here is a simple but production-relevant paymaster pattern — it sponsors gas for a list of approved wallet addresses:

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

import "@account-abstraction/contracts/core/BasePaymaster.sol";

contract WhitelistPaymaster is BasePaymaster {
    mapping(address => bool) public sponsored;
    address public owner;

    constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {
        owner = msg.sender;
    }

    function addToWhitelist(address account) external {
        require(msg.sender == owner, "Not owner");
        sponsored[account] = true;
    }

    function _validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) internal override returns (bytes memory context, uint256 validationData) {
        require(sponsored[userOp.sender], "Sender not whitelisted");
        return (abi.encode(userOp.sender), 0);
    }

    function _postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost,
        uint256 actualUserOpFeePerGas
    ) internal override {
        // Optional: log gas per user, deduct from credit balance, etc.
    }
}

A critical security rule from the OtterSec security team’s paymaster research: always collect payment during validation, not during postOp. If you try to charge an ERC-20 during postOp and the user drains their token balance between validation and execution, you’ve sponsored a transaction for free with no recourse.5 Validate aggressively. Validate everything you need upfront.

Paymaster TypeHow It WorksRisk LevelBest Use Case
Whitelist PaymasterSponsors gas for approved addresses onlyLowControlled beta launches, allowlists
ERC-20 PaymasterUser pays gas in a token (e.g. USDC) instead of ETHMedium — token price exposureWallets where users hold tokens but no ETH
Signature PaymasterOff-chain signer approves each sponsorshipMedium — off-chain dependencydApps needing flexible sponsorship logic
Open PaymasterSponsors anyone unconditionallyHigh — DoS and drain riskOnly for testnets or strictly rate-limited scenarios

Session Keys: Scoped Signing for Advanced Wallet UX

Session keys are temporary signing keys you grant to a specific action or service — with hard limits baked in. A game can hold a session key that lets it call one specific function on one specific contract, for a capped amount, until an expiry timestamp. The main owner key never touches the game server. If the session key is compromised, the damage is contained by design.

Here is how to add session key storage and validation to your smart account:

struct SessionKey {
    uint48 validUntil;       // Unix timestamp when key expires
    uint256 spendingLimit;   // Max ETH value per UserOp
    address allowedTarget;   // Only this contract can be called
}

mapping(address => SessionKey) public sessionKeys;

function addSessionKey(
    address key,
    uint48 validUntil,
    uint256 spendingLimit,
    address allowedTarget
) external {
    require(
        msg.sender == address(entryPoint()) || msg.sender == owner,
        "Not authorized"
    );
    sessionKeys[key] = SessionKey(validUntil, spendingLimit, allowedTarget);
}

function revokeSessionKey(address key) external {
    require(msg.sender == owner, "Not owner");
    delete sessionKeys[key];
}

In validateUserOp, after checking the owner’s signature, check whether the recovered signer is a valid session key. If the session key exists, hasn’t expired, and the call target matches the allowed contract, approve it. This layered check is what makes session keys safe to use in production.

Session keys bring blockchain UX to parity with how web apps already work — scoped tokens, expiry timestamps, limited permissions. If you’ve seen OAuth scopes, you already understand the pattern.

Multi-Call Batching: One UserOp, Many Actions

One of the most practical advantages of ERC-4337 for DeFi users is batching. Instead of signing three separate transactions — approve token, swap token, deposit into a yield vault — a user signs one UserOperation that does all three atomically. No partial state, no failed mid-flow, no three rounds of MetaMask popups.

Add a batch execute function alongside your single-call execute:

struct Call {
    address target;
    uint256 value;
    bytes data;
}

function executeBatch(Call[] calldata calls) external {
    require(msg.sender == address(_entryPoint), "Not EntryPoint");
    for (uint256 i = 0; i < calls.length; i++) {
        (bool success, bytes memory result) = calls[i].target.call{
            value: calls[i].value
        }(calls[i].data);
        if (!success) {
            assembly { revert(add(result, 32), mload(result)) }
        }
    }
}

The callData in the UserOperation will simply encode the executeBatch selector along with the array of Call structs. On the client side, encode this with ethers.js or viem’s encodeFunctionData and pass it directly into the UserOp’s callData field. The bundler and EntryPoint don’t care what the callData says — they only care that validateUserOp passed. The execution logic is entirely yours.

Social Recovery: Protecting Users from Themselves

Social recovery is the feature that makes a smart wallet worth using over a plain EOA. If a user loses their private key, a set of pre-approved guardians can collectively authorize a new owner. No seed phrase. No lost funds. The guardians don’t get access to the wallet — they only get the power to nominate a replacement key after a timelock period.

A basic social recovery module needs three things: a list of trusted guardian addresses, a threshold (how many must agree), and a timelock (delay between vote initiation and key replacement). The timelock is critical — it gives the real owner time to cancel a malicious recovery attempt they didn’t authorize.

Social recovery is what Chialisp builders might recognize as a “multi-sig puzzle with a timelock output condition.” The pattern is the same across both ecosystems: multiple valid signers, a minimum quorum, and a delay before state changes. Chia’s native puzzle composition and Ethereum’s ERC-4337 module pattern both solve the same human problem — keys get lost.

“Instead of modifying the logic of the consensus layer itself, we replicate the functionality of the transaction mempool in a higher-level system.”

— Vitalik Buterin, co-founder of Ethereum, original ERC-4337 proposal1

That principle applies to social recovery too. You’re not changing Ethereum. You’re programming your wallet’s rules at a higher layer, in code, with no protocol change required.

How This Maps to Chialisp’s Puzzle-Solution Model

If you’ve worked with Chialisp, the ERC-4337 architecture has an almost one-to-one mapping to concepts you already know.6

In Chia, a coin holds value and is locked by a Chialisp puzzle. To spend it, you provide a solution — the set of conditions that satisfies the puzzle. The puzzle script runs, checks the conditions, and if everything passes, the coin is spent and new coins are created. The blockchain enforces the rules; the puzzle defines them.

In ERC-4337, your smart account holds ETH (and tokens). To do anything, a user creates a UserOperation — the “solution.” The EntryPoint calls validateUserOp — the “puzzle.” If validation passes (sig checks, nonce checks, gas payment), the EntryPoint executes the operation. Same mental model. Different language, different chain, same underlying logic of intent + proof + conditional execution.

The Chia singleton pattern (a unique, persistent coin identity) is analogous to how a smart account deployed via CREATE2 maintains a stable address across chains. Both achieve the same goal: a stable identity for a wallet that can be found and interacted with predictably. Understanding one helps you build better with the other.

Security Hardening Before You Go to Mainnet

Advanced features mean more attack surface. Here is what every account abstraction tutorial advanced guide needs to address that beginner guides skip over:

Replay Protection

The userOpHash includes the EntryPoint address and chain ID, which prevents cross-chain replay by default.7 But if you build custom aggregators or cross-chain features, audit your hash construction carefully. Any signature that doesn’t bind to a specific chain and a specific contract is a replay attack waiting to happen.

Gas Griefing Defense

A malicious user could craft a UserOperation that passes validation but uses excessive gas during execution — draining a paymaster’s ETH stake. Defend against this by setting tight gas limits in your paymaster’s _validatePaymasterUserOp and requiring off-chain signature approval for large-value sponsorships. Your paymaster’s stake is on the line, not just your user’s funds.

Storage Access Restrictions

During the validation phase, the EntryPoint enforces strict storage access rules — your validateUserOp cannot read from arbitrary storage slots of other contracts. Violating these rules means bundlers will reject your UserOp before it ever reaches the chain. If your validation logic needs external data, pre-cache it in your account’s own storage during an earlier transaction.

Upgradeability Decisions

A proxy-upgradeable smart account is more flexible but adds risk — a compromised upgrade path is catastrophic. An immutable minimal account is harder to fix if a bug is found, but the attack surface is smaller. Most production wallets use a delegatecall proxy pattern with a timelocked owner as the upgrade controller. Pick your trade-off deliberately, not by default.

Testing Your Advanced Wallet with Foundry

Testing ERC-4337 contracts is different from testing regular smart contracts because the call flow runs through the EntryPoint, not directly from your test. Foundry handles this well when you deploy a local EntryPoint in your test setup.

// In your test file
import "@account-abstraction/contracts/core/EntryPoint.sol";

EntryPoint entryPoint = new EntryPoint();
MinimalAccount account = new MinimalAccount(address(entryPoint), owner);

// Fund the account on EntryPoint
entryPoint.depositTo{value: 1 ether}(address(account));

// Build a UserOperation
PackedUserOperation memory userOp = PackedUserOperation({
    sender: address(account),
    nonce: entryPoint.getNonce(address(account), 0),
    initCode: bytes(""),
    callData: abi.encodeWithSelector(
        account.execute.selector,
        targetAddress,
        0,
        abi.encodeWithSignature("doSomething()")
    ),
    // ... fill in gas fields
    signature: signUserOp(userOpHash, ownerPrivateKey)
});

PackedUserOperation[] memory ops = new PackedUserOperation[](1);
ops[0] = userOp;
entryPoint.handleOps(ops, payable(bundler));

Use Foundry’s built-in vm.sign cheatcode to sign the userOpHash with your test private key. Step through validateUserOp with --debug to watch the ECDSA recovery happen in real time. This is the fastest way to catch a wrong hash construction before it ever touches a testnet.

EIP-7702 and What It Means for Your Advanced Wallet

With the Pectra upgrade in May 2025, Ethereum introduced EIP-7702, which lets existing EOAs temporarily execute smart contract code without migrating to a new address.8 For your ERC-4337 wallet, this is an additive change, not a replacement. EIP-7702 handles the migration problem — getting existing EOA users into smart account features — while ERC-4337 continues to be the right foundation for full smart account functionality including paymasters, session keys, and social recovery.

Major wallets like Ambire have already rolled out EIP-7702 support. The practical impact: users can adopt AA features like batching and gas sponsorship from their current EOA without creating a new smart account address, then migrate to a full ERC-4337 account when they want all the features.

Conclusion

This account abstraction tutorial advanced guide has taken you from environment setup through a fully working minimal account, paymaster, session keys, batching, and social recovery — with the security considerations you need before going to mainnet. The ERC-4337 stack is no longer experimental. Over 40 million smart accounts have been deployed across Ethereum and its L2 networks, with over 100 million UserOperations processed since March 2023.8 The tooling is production-ready and the patterns are battle-tested. Your next step is to deploy your MinimalAccount and whitelist paymaster on a testnet like Base Sepolia, send a gasless UserOp, and then start layering on the advanced features one at a time. Build incrementally, audit your validation logic before mainnet, and keep the Chialisp mental model close — if you can describe a puzzle and a solution, you can build an ERC-4337 wallet.

account abstraction tutorial advanced FAQs

What is the most important function to understand in an account abstraction tutorial advanced guide?

validateUserOp is the most important function in any advanced account abstraction tutorial. It is the gatekeeping function the EntryPoint calls before executing any UserOperation — your signature check, nonce validation, and gas payment logic all live here. Getting it wrong means your wallet is either frozen or exploitable.

How does a paymaster work in ERC-4337?

A paymaster is a smart contract that stakes ETH with the EntryPoint and agrees to cover gas costs for qualifying UserOperations. It implements _validatePaymasterUserOp to decide whether to sponsor a transaction, and _postOp to handle any post-execution cleanup like charging an ERC-20 token.

Can I use account abstraction tutorial advanced patterns on Layer 2 networks?

Yes — ERC-4337 works on any EVM-compatible network without protocol changes, including Base, Optimism, Arbitrum, and Polygon. The EntryPoint contract is deployed at the same address on every supported network, which means your smart account contracts and paymaster contracts can be deployed identically across chains.

What are session keys in account abstraction tutorial advanced development?

Session keys in an account abstraction tutorial advanced context are temporary signing keys you embed into your smart account with scoped permissions — a specific target contract, a spending cap, and an expiry timestamp. They let a game or dApp sign limited actions on a user’s behalf without ever accessing the user’s main owner key.

How does ERC-4337 social recovery work at a code level?

Social recovery in ERC-4337 works by storing a list of trusted guardian addresses inside your smart account, along with an approval threshold and a timelock delay. When enough guardians submit a recovery vote, the wallet waits for the timelock to expire and then replaces the owner key — giving the legitimate owner time to cancel any unauthorized recovery attempt.

account abstraction tutorial advanced Citations

  1. Vitalik Buterin — ERC-4337: Account Abstraction Without Ethereum Protocol Changes (Medium/Infinitism)
  2. eth-infinitism/account-abstraction — Official ERC-4337 GitHub Repository
  3. ERC-4337: Account Abstraction Using Alt Mempool — Ethereum Improvement Proposals
  4. ERC-4337 Official Documentation
  5. OtterSec — ERC-4337 Paymasters: Better UX, Hidden Risks (December 2025)
  6. ChiaTribe — Chialisp Transforming Blockchain: Unlocking Next-Gen Smart Contracts
  7. Hacken — ERC-4337 & Account Abstraction: A Comprehensive Overview
  8. Alchemy — What Is Account Abstraction? ERC-4337 and EIP-7702 Explained