skills/

account-abstraction

Infrastructuremultichain|#account-abstraction#erc-4337#eip-7702#smart-accounts#paymasters#bundlers#session-keys#permissionless#zerodev
Target:

Install this skill:

$ npx cryptoskills install account-abstraction

Install all 95 skills:

$ npx cryptoskills install --all

Account Abstraction

Account abstraction replaces the rigid EOA transaction model with programmable accounts that support arbitrary verification logic, gas sponsorship, batched operations, and session-based access control. ERC-4337 introduces an out-of-protocol smart account infrastructure (EntryPoint, bundlers, paymasters), while EIP-7702 adds a protocol-level mechanism for EOAs to temporarily or persistently delegate their execution to smart contract code. Together they form a complete stack: 7702 upgrades existing EOAs without migration, and 4337 provides the off-chain infrastructure for UserOperation bundling, gas abstraction, and paymaster sponsorship.

What You Probably Got Wrong

  • EIP-7702 does NOT replace ERC-4337 -- they are complementary. EIP-7702 is a protocol-level mechanism that lets EOAs point their code to an implementation contract. ERC-4337 is an off-chain infrastructure layer (bundlers, paymasters, EntryPoint) that processes UserOperations. You need both for a complete account abstraction stack: 7702 makes the EOA programmable, 4337 provides gas sponsorship and bundling.

  • Existing EOAs can upgrade in-place with EIP-7702 -- users do NOT need to deploy a new smart contract wallet and migrate assets. A single Type 0x04 transaction sets a delegation designator on the EOA, making it behave like a smart account while keeping the same address, balances, and history.

  • Paymasters are NOT free gas -- someone always pays. A verifying paymaster requires off-chain signature approval from the sponsor. An ERC-20 paymaster charges the user in tokens at a markup. A sponsoring paymaster has a deposit in the EntryPoint that drains with each sponsored operation. "Gasless" means gasless for the end user, not gasless for the system.

  • UserOperations are NOT wrapped transactions -- they are a completely different execution model. A UserOp goes from the client to a bundler (off-chain), the bundler packages multiple UserOps into a single handleOps transaction to the EntryPoint contract (on-chain), and the EntryPoint calls each account's validateUserOp then execute. The account contract itself is the msg.sender for the inner calls, not the EOA signer.

  • Session keys are NOT private keys you hand out -- they are scoped authorizations with explicit constraints: which contracts can be called, which functions, maximum value per call, time window, and total usage count. The session key validator module enforces these constraints on-chain. An expired or over-limit session key is rejected at validation time, not at execution time.

  • EntryPoint v0.7 changed the UserOp struct -- v0.6 and v0.7 are not compatible. v0.7 packs gas fields (verificationGasLimit + callGasLimit into a single bytes32), uses accountGasLimits instead of separate fields, and adds paymasterAndData packing. If your bundler returns "invalid UserOp" errors, check which EntryPoint version you are targeting.

  • Nonces are 2D in ERC-4337 -- the nonce field is a uint256 where the upper 192 bits are the "key" and the lower 64 bits are the sequential nonce for that key. This allows parallel UserOp submission across different nonce keys without blocking. Most SDKs handle this automatically, but raw UserOp construction requires understanding this.

ERC-4337 Core Architecture

EntryPoint v0.7

The EntryPoint is a singleton contract deployed at a deterministic address across all EVM chains. It serves as the central coordinator: validating UserOperations, calling smart account contracts, handling paymaster interactions, and managing gas accounting.

Last verified: February 2026

Contract Address
EntryPoint v0.7 0x0000000071727De22E5E9d8BAf0edAc6f37da032
EntryPoint v0.6 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789

UserOperation Struct (v0.7)

struct PackedUserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    bytes32 accountGasLimits;      // verificationGasLimit (16 bytes) || callGasLimit (16 bytes)
    uint256 preVerificationGas;
    bytes32 gasFees;               // maxPriorityFeePerGas (16 bytes) || maxFeePerGas (16 bytes)
    bytes paymasterAndData;        // paymaster (20 bytes) || paymasterVerificationGasLimit (16 bytes) || paymasterPostOpGasLimit (16 bytes) || paymasterData
    bytes signature;
}
Field Purpose
sender Smart account address that will execute the operation
nonce Anti-replay value (upper 192 bits = key, lower 64 bits = sequence)
initCode Factory address + calldata for first-time account deployment (empty if account exists)
callData Encoded call to the account's execute function
accountGasLimits Packed verification and call gas limits
preVerificationGas Gas to compensate bundler for calldata and overhead costs
gasFees Packed EIP-1559 gas price fields
paymasterAndData Paymaster address + gas limits + custom data (empty if self-paying)
signature Signature validated by the account's validateUserOp

Smart Account Interface

Every ERC-4337 smart account must implement:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
interface IAccount {
    /// @notice Validates a UserOperation
    /// @param userOp The packed user operation
    /// @param userOpHash Hash of the user operation (used for signature verification)
    /// @param missingAccountFunds Amount the account must prefund the EntryPoint
    /// @return validationData 0 for success, 1 for failure, or packed (authorizer, validUntil, validAfter)
    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external returns (uint256 validationData);
}

UserOperation Lifecycle

1. Client constructs UserOp (callData, gas limits, signature)
         |
2. Client sends UserOp to bundler via eth_sendUserOperation RPC
         |
3. Bundler validates UserOp locally (simulation, gas checks, signature)
         |
4. Bundler packages UserOps into a bundle transaction
         |
5. Bundler calls EntryPoint.handleOps(ops[], beneficiary)
         |
6. EntryPoint loops through each UserOp:
   a. If initCode present -> deploy account via factory
   b. Call account.validateUserOp() -> must return 0 for success
   c. If paymaster present -> call paymaster.validatePaymasterUserOp()
   d. Call account with callData (the actual operation)
   e. If paymaster present -> call paymaster.postOp()
         |
7. EntryPoint settles gas: refunds excess, pays bundler from account/paymaster deposit

Validation Phase

The validation phase runs with a restricted opcode set (no TIMESTAMP, BLOCKHASH, etc. relative to other accounts) to ensure simulation determinism. The account's validateUserOp must:

  1. Verify the signature over userOpHash
  2. Pay missingAccountFunds to the EntryPoint (or have sufficient deposit)
  3. Return 0 for valid, 1 for invalid, or a packed value with time-range validity

Execution Phase

After all UserOps in a bundle pass validation, the EntryPoint calls each account with the callData. The account contract is msg.sender for any downstream calls. If execution reverts, the gas is still consumed and paid for.

Bundler Role

Bundlers are off-chain services that collect UserOperations from users, validate them, and submit them as bundle transactions to the EntryPoint. They earn gas refunds as compensation.

Bundler RPC Methods

Method Purpose
eth_sendUserOperation Submit a UserOp for inclusion
eth_estimateUserOperationGas Estimate gas limits for a UserOp
eth_getUserOperationByHash Look up a UserOp by its hash
eth_getUserOperationReceipt Get execution receipt for a UserOp
eth_supportedEntryPoints List EntryPoint addresses the bundler supports
eth_chainId Return the chain ID

Major Bundler Providers

Provider Endpoint Format Notes
Pimlico https://api.pimlico.io/v2/{chain}/rpc?apikey={key} Alto bundler, widest chain support
Alchemy https://{chain}.g.alchemy.com/v2/{key} Rundler, integrated with Alchemy platform
Stackup https://api.stackup.sh/v1/node/{key} Stackup bundler, ERC-4337 pioneers

EIP-7702 EOA Delegation

EIP-7702 introduces Type 0x04 transactions that let EOAs set a delegation designator, making the EOA's code point to a smart contract implementation. This is a protocol-level change (Pectra upgrade) that does not require ERC-4337 infrastructure.

How Delegation Works

A Type 0x04 transaction includes an authorizationList -- an array of signed authorization tuples. When processed, the EOA's code is set to 0xef0100 || address, a special delegation designator prefix. Any call to the EOA now delegates execution to the implementation contract at address.

Authorization Tuple

interface Authorization {
  chainId: bigint;    // 0 for chain-agnostic, or specific chain ID
  address: Address;   // Implementation contract to delegate to
  nonce: bigint;      // EOA's current nonce (prevents replay)
  yParity: number;    // Signature recovery parameter
  r: `0x${string}`;   // Signature component
  s: `0x${string}`;   // Signature component
}

Delegation with viem

import { createWalletClient, http, parseEther } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { eip7702Actions } from "viem/experimental";
 
const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);
 
const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL),
}).extend(eip7702Actions());
 
// BatchExecutor is a contract that implements execute(Call[]) for batching
const BATCH_EXECUTOR = "0x..." as const; // Your batch executor implementation
 
async function delegateAndBatch() {
  const authorization = await walletClient.signAuthorization({
    contractAddress: BATCH_EXECUTOR,
  });
 
  const hash = await walletClient.sendTransaction({
    authorizationList: [authorization],
    to: account.address,
    data: encodeFunctionData({
      abi: batchExecutorAbi,
      functionName: "execute",
      args: [
        [
          { target: TOKEN_A, value: 0n, data: approveCalldata },
          { target: ROUTER, value: 0n, data: swapCalldata },
        ],
      ],
    }),
  });
 
  return hash;
}

Per-Transaction vs Persistent Delegation

  • Per-transaction: Sign an authorization with chainId: 0 or the specific chain. The delegation persists until explicitly cleared or overwritten by another Type 0x04 transaction. There is no automatic expiry.
  • Clearing delegation: Send a Type 0x04 transaction with an authorization pointing to address(0) to remove the delegation designator and restore the EOA to its original state.
  • Key insight: The EOA retains its private key and can always send a new Type 0x04 transaction to change or clear delegation. The delegated code cannot lock out the EOA owner.

ERC-4337 + EIP-7702 Combined

The most powerful pattern combines both standards: EIP-7702 makes the EOA behave like a smart account, and ERC-4337 provides bundler infrastructure and paymaster sponsorship for that account.

How It Works

  1. User signs an EIP-7702 authorization delegating their EOA to a smart account implementation (e.g., SimpleAccount, Kernel)
  2. The UserOp includes the eip7702Auth in its authorization list
  3. The bundler submits a Type 0x04 transaction that both sets the delegation and processes the UserOp through the EntryPoint
  4. The EOA now has smart account capabilities (validation logic, execute function) and can use paymasters
import { toSmartAccount } from "permissionless/accounts";
import { createSmartAccountClient } from "permissionless";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { http } from "viem";
import { mainnet } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";
import { eip7702Actions } from "viem/experimental";
 
const owner = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);
 
const pimlicoClient = createPimlicoClient({
  transport: http(
    `https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
  ),
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
});
 
// The EOA becomes the smart account via 7702 delegation
const smartAccount = await toSmartAccount({
  client: publicClient,
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
  owner,
  // 7702 delegation target -- the EOA will delegate to this implementation
  implementation: SMART_ACCOUNT_IMPLEMENTATION,
});
 
const smartAccountClient = createSmartAccountClient({
  account: smartAccount,
  chain: mainnet,
  bundlerTransport: http(
    `https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
  ),
  paymaster: pimlicoClient,
});

Benefits of Combined Approach

Capability 4337 Only 7702 Only Combined
Same EOA address No (new contract) Yes Yes
Gas sponsorship Yes (paymasters) No Yes
Batched calls Yes Yes Yes
Session keys Yes (modules) Requires custom impl Yes (modules)
Works without bundler No Yes Flexible
Cross-chain replay protection Via nonce keys Via chainId in auth Both

Paymaster Patterns

Verifying Paymaster

A verifying paymaster sponsors gas for UserOps that carry a valid off-chain signature from the paymaster operator. The operator controls who gets sponsored.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
import {IPaymaster} from "account-abstraction/interfaces/IPaymaster.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
 
/// @notice Sponsors UserOps that carry a valid signature from the verifying signer
contract VerifyingPaymaster is IPaymaster {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;
 
    address public immutable entryPoint;
    address public verifyingSigner;
 
    error InvalidSignature();
    error CallerNotEntryPoint();
 
    modifier onlyEntryPoint() {
        if (msg.sender != entryPoint) revert CallerNotEntryPoint();
        _;
    }
 
    constructor(address _entryPoint, address _signer) {
        entryPoint = _entryPoint;
        verifyingSigner = _signer;
    }
 
    function validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external onlyEntryPoint returns (bytes memory context, uint256 validationData) {
        // paymasterData layout: signature (65 bytes)
        bytes calldata paymasterData = userOp.paymasterAndData[52:];
 
        bytes32 hash = keccak256(
            abi.encode(userOp.sender, userOp.nonce, userOpHash)
        ).toEthSignedMessageHash();
 
        address recovered = hash.recover(paymasterData[:65]);
        if (recovered != verifyingSigner) revert InvalidSignature();
 
        return (abi.encode(userOp.sender), 0);
    }
 
    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost,
        uint256 actualUserOpFeePerGas
    ) external onlyEntryPoint {}
}

ERC-20 Paymaster

An ERC-20 paymaster lets users pay gas fees in ERC-20 tokens instead of ETH. The paymaster holds an ETH deposit in the EntryPoint and charges the user's tokens in postOp.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
import {IPaymaster} from "account-abstraction/interfaces/IPaymaster.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
 
/// @notice Charges users in ERC-20 tokens for gas sponsorship
/// @dev Users must approve this paymaster for the payment token
contract ERC20Paymaster is IPaymaster {
    address public immutable entryPoint;
    IERC20 public immutable token;
    // Price oracle would set this -- simplified for reference
    uint256 public tokenPricePerGas;
 
    error CallerNotEntryPoint();
    error InsufficientTokenBalance();
 
    modifier onlyEntryPoint() {
        if (msg.sender != entryPoint) revert CallerNotEntryPoint();
        _;
    }
 
    constructor(address _entryPoint, address _token, uint256 _tokenPricePerGas) {
        entryPoint = _entryPoint;
        token = IERC20(_token);
        tokenPricePerGas = _tokenPricePerGas;
    }
 
    function validatePaymasterUserOp(
        PackedUserOperation calldata userOp,
        bytes32,
        uint256 maxCost
    ) external onlyEntryPoint returns (bytes memory context, uint256 validationData) {
        uint256 maxTokenCost = maxCost * tokenPricePerGas;
        uint256 balance = token.balanceOf(userOp.sender);
        if (balance < maxTokenCost) revert InsufficientTokenBalance();
 
        return (abi.encode(userOp.sender, maxTokenCost), 0);
    }
 
    function postOp(
        PostOpMode,
        bytes calldata context,
        uint256 actualGasCost,
        uint256
    ) external onlyEntryPoint {
        (address sender, ) = abi.decode(context, (address, uint256));
        uint256 actualTokenCost = actualGasCost * tokenPricePerGas;
 
        // CEI: state is updated via token transfer, then no further external calls
        token.transferFrom(sender, address(this), actualTokenCost);
    }
}

Client-Side Paymaster Integration

import { createSmartAccountClient } from "permissionless";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { http } from "viem";
import { entryPoint07Address } from "viem/account-abstraction";
 
const pimlicoClient = createPimlicoClient({
  transport: http(
    `https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
  ),
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
});
 
const smartAccountClient = createSmartAccountClient({
  account: smartAccount,
  chain: mainnet,
  bundlerTransport: http(
    `https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
  ),
  paymaster: pimlicoClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      return (await pimlicoClient.getUserOperationGasPrice()).fast;
    },
  },
});
 
const txHash = await smartAccountClient.sendTransaction({
  to: "0xRecipient..." as `0x${string}`,
  value: parseEther("0.01"),
  data: "0x",
});

SDK: permissionless.js

permissionless.js is a TypeScript SDK built on viem for ERC-4337 smart accounts. It supports multiple smart account implementations, bundlers, and paymasters.

Setup

npm install permissionless viem

Create Smart Account Client

import { createSmartAccountClient } from "permissionless";
import { toSimpleSmartAccount } from "permissionless/accounts";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";
 
const publicClient = createPublicClient({
  chain: sepolia,
  transport: http(process.env.RPC_URL),
});
 
const owner = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);
 
const simpleAccount = await toSimpleSmartAccount({
  client: publicClient,
  owner,
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
});
 
const smartAccountClient = createSmartAccountClient({
  account: simpleAccount,
  chain: sepolia,
  bundlerTransport: http(
    `https://api.pimlico.io/v2/sepolia/rpc?apikey=${process.env.PIMLICO_API_KEY}`
  ),
});

Send a UserOp

const txHash = await smartAccountClient.sendTransaction({
  to: "0xRecipient..." as `0x${string}`,
  value: parseEther("0.01"),
  data: "0x",
});
 
console.log(`Transaction hash: ${txHash}`);

Batch Transactions

const txHash = await smartAccountClient.sendUserOperation({
  calls: [
    {
      to: TOKEN_ADDRESS,
      abi: erc20Abi,
      functionName: "approve",
      args: [SPENDER, parseEther("100")],
    },
    {
      to: PROTOCOL_ADDRESS,
      abi: protocolAbi,
      functionName: "deposit",
      args: [parseEther("100")],
    },
  ],
});
 
const receipt = await smartAccountClient.waitForUserOperationReceipt({
  hash: txHash,
});
 
if (!receipt.success) {
  throw new Error(`UserOp failed: ${receipt.reason}`);
}

SDK: ZeroDev

ZeroDev provides the Kernel smart account -- a modular ERC-7579 compatible smart account with plugin support for session keys, passkeys, and recovery.

Kernel Account Setup

npm install @zerodev/sdk @zerodev/ecdsa-validator permissionless viem
import { createKernelAccount, createKernelAccountClient } from "@zerodev/sdk";
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";
 
const publicClient = createPublicClient({
  chain: sepolia,
  transport: http(process.env.RPC_URL),
});
 
const signer = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);
 
const ecdsaValidator = await signerToEcdsaValidator(publicClient, {
  signer,
  entryPoint: entryPoint07Address,
});
 
const kernelAccount = await createKernelAccount(publicClient, {
  plugins: {
    sudo: ecdsaValidator,
  },
  entryPoint: entryPoint07Address,
});
 
const kernelClient = createKernelAccountClient({
  account: kernelAccount,
  chain: sepolia,
  bundlerTransport: http(process.env.BUNDLER_URL),
  paymaster: {
    getPaymasterData: async (userOperation) => {
      // Return paymaster data from your paymaster service
      return { paymasterAndData: "0x" };
    },
  },
});

Send Transaction with Kernel

const txHash = await kernelClient.sendTransaction({
  to: "0xRecipient..." as `0x${string}`,
  value: parseEther("0.01"),
  data: "0x",
});

Session Keys

Session keys are scoped signing keys that allow limited actions without requiring the main account owner's signature for every operation. They are implemented as ERC-7579 validator modules.

Session Key Constraints

Constraint Description
Contract whitelist Only specific contract addresses can be called
Function whitelist Only specific function selectors are allowed
Value limit Maximum ETH value per call
Time window Valid only between validAfter and validUntil timestamps
Usage count Maximum number of times the session key can be used
Spending limit Maximum total token spend across all calls

Creating a Session Key with ZeroDev

import {
  createKernelAccount,
  createKernelAccountClient,
} from "@zerodev/sdk";
import {
  toPermissionValidator,
  toSudoPolicy,
  toCallPolicy,
  CallPolicyVersion,
  ParamCondition,
} from "@zerodev/permissions";
import { toECDSASigner } from "@zerodev/permissions/signers";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
 
// Generate a temporary session key
const sessionPrivateKey = generatePrivateKey();
const sessionKeySigner = privateKeyToAccount(sessionPrivateKey);
 
const ecdsaSigner = toECDSASigner({
  signer: sessionKeySigner,
});
 
const permissionValidator = await toPermissionValidator(publicClient, {
  entryPoint: entryPoint07Address,
  signer: ecdsaSigner,
  policies: [
    toCallPolicy({
      policyVersion: CallPolicyVersion.V0_0_4,
      permissions: [
        {
          target: TOKEN_ADDRESS,
          abi: erc20Abi,
          functionName: "transfer",
          args: [
            {
              condition: ParamCondition.EQUAL,
              value: ALLOWED_RECIPIENT,
            },
            null, // Any amount (or add a condition)
          ],
        },
      ],
    }),
  ],
});
 
const sessionKeyAccount = await createKernelAccount(publicClient, {
  plugins: {
    sudo: ecdsaValidator,
    regular: permissionValidator,
  },
  entryPoint: entryPoint07Address,
});

Using a Session Key

const sessionClient = createKernelAccountClient({
  account: sessionKeyAccount,
  chain: sepolia,
  bundlerTransport: http(process.env.BUNDLER_URL),
});
 
// This transaction is signed by the session key, validated by the permission module
const txHash = await sessionClient.sendTransaction({
  to: TOKEN_ADDRESS,
  abi: erc20Abi,
  functionName: "transfer",
  args: [ALLOWED_RECIPIENT, parseEther("10")],
});

Revoking a Session Key

Session keys can be revoked by the account owner by disabling the permission validator or removing the specific session from the validator's storage:

const revokeTx = await kernelClient.sendUserOperation({
  calls: [
    {
      to: kernelAccount.address,
      data: encodeFunctionData({
        abi: kernelAccountAbi,
        functionName: "disablePlugin",
        args: [permissionValidatorAddress],
      }),
    },
  ],
});

Gas Abstraction in Practice

To build a fully gasless flow, create a smartAccountClient with a paymaster parameter. The paymaster's EntryPoint deposit covers all gas costs. See the examples/simple-smart-account/ example for a complete working implementation.

Gas Estimation for UserOps

const gasEstimate = await pimlicoClient.estimateUserOperationGas({
  sender: account.address,
  nonce: await account.getNonce(),
  callData: await account.encodeCalls([
    { to: recipient, value: parseEther("0.01"), data: "0x" },
  ]),
  signature: await account.getDummySignature(),
});
 
// gasEstimate contains:
// - preVerificationGas: bundler overhead compensation
// - verificationGasLimit: gas for validateUserOp
// - callGasLimit: gas for the actual execution

preVerificationGas Calculation

preVerificationGas compensates the bundler for calldata costs and fixed overhead. On L2s with L1 data posting costs (Arbitrum, Optimism, Base), this value is significantly higher because it includes the L1 data fee.

// L1 chains: ~21000 + calldata_cost
// L2 chains: ~21000 + calldata_cost + l1_data_fee
// Always use bundler estimation -- manual calculation is error-prone on L2s
const gasPrice = await pimlicoClient.getUserOperationGasPrice();

Choosing Your Stack

Use Case Recommendation Why
New app, new users, gasless onboarding ERC-4337 + permissionless.js + Pimlico Full gas abstraction, paymaster sponsoring, widest tooling
Existing app, users have EOAs EIP-7702 only No migration needed, no new addresses, works with existing wallets
DeFi power users wanting batching EIP-7702 only Minimal overhead, batch calls in single tx, keep same address
Gaming with session-based actions ERC-4337 + ZeroDev Kernel Session keys for in-game actions, modular validators
Enterprise with compliance needs ERC-4337 + Safe Multisig, role-based access, audit trail
Maximum flexibility ERC-4337 + EIP-7702 combined EOA upgrades in-place, gets paymaster + session key support
Mobile wallet ERC-4337 + passkey signer WebAuthn/passkey as account signer, no seed phrase

When to Use 4337 Only

  • You need paymaster gas sponsorship
  • You need modular validator/executor plugins (ERC-7579)
  • Your users are creating new accounts (onboarding flow)
  • You need bundler infrastructure (high throughput, MEV protection)

When to Use 7702 Only

  • Your users already have EOAs with assets
  • You need batching but not gas sponsorship
  • You want minimal infrastructure overhead
  • You are building a wallet upgrade feature

When to Combine

  • You want EOAs to get paymaster sponsorship
  • You need session keys on existing EOA addresses
  • You want the "best of both worlds" without address migration

Contract Addresses

Last verified: February 2026. EntryPoint is deployed at deterministic addresses via CREATE2 across all EVM chains.

Contract Address Chains
EntryPoint v0.7 0x0000000071727De22E5E9d8BAf0edAc6f37da032 All EVM chains
EntryPoint v0.6 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 All EVM chains
SimpleAccountFactory (v0.7) 0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985 Ethereum, Arbitrum, Base, Optimism, Polygon
# Verify EntryPoint deployment on any chain
cast code 0x0000000071727De22E5E9d8BAf0edAc6f37da032 --rpc-url $RPC_URL

References