standards/erc-account-standards

ERC Account & Proxy Standards (ERC-4337, ERC-6551, EIP-1967)

ethereumstandard-guide🏛️ Officialconfidence highhealth -2%
v1.0.0·Updated 3/26/2026

Overview

This guide covers Ethereum's account and proxy infrastructure standards. These are critical for building smart wallets, upgradeable contracts, gasless transactions, and NFT-bound account systems.

Standards at a Glance

StandardPurposeComplexityAdoption
ERC-4337Account Abstraction (smart wallets)HighGrowing rapidly
ERC-6551Token Bound Accounts (NFT has wallet)MediumEmerging
EIP-1967Standard proxy storage slotsLowUniversal
ERC-1167Minimal proxy (clone factory)LowUniversal
ERC-2771Meta-transactions (gasless)MediumMature

ERC-4337 — Account Abstraction

ERC-4337 enables smart contract wallets without protocol changes. Users submit UserOperation structs instead of transactions, processed by a decentralized network of Bundlers.

Architecture

User → UserOperation → Bundler → EntryPoint (0x0000000071727De22E5E9d8BAf0edAc6f37da032)
                                      ↓
                              Smart Account (validateUserOp)
                                      ↓
                              Paymaster (optional, pay fees)

UserOperation Structure

struct UserOperation {
    address sender;           // Smart account address
    uint256 nonce;            // Anti-replay
    bytes initCode;           // Factory calldata for new accounts (empty if existing)
    bytes callData;           // What the account should execute
    uint256 callGasLimit;     // Gas for execution phase
    uint256 verificationGasLimit; // Gas for validation phase
    uint256 preVerificationGas;   // Bundler overhead gas
    uint256 maxFeePerGas;     // EIP-1559 max fee
    uint256 maxPriorityFeePerGas; // EIP-1559 priority fee
    bytes paymasterAndData;   // Paymaster address + data (empty = user pays)
    bytes signature;          // Signature over the UserOp hash
}

Smart Account Interface

interface IAccount {
    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external returns (uint256 validationData);
    // validationData: 0 = valid, 1 = invalid, or SIG_VALIDATION_FAILED (1)
    // Upper bits can encode valid time range
}

Sending a UserOperation (ethers.js v6)

import { ethers } from "ethers";

const ENTRY_POINT = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";

async function sendUserOp(
  smartAccountAddress: string,
  callData: string,
  signer: ethers.Signer,
  bundlerUrl: string
) {
  const provider = new ethers.JsonRpcProvider(bundlerUrl);

  // Get current nonce from EntryPoint
  const entryPoint = new ethers.Contract(
    ENTRY_POINT,
    ["function getNonce(address sender, uint192 key) view returns (uint256)"],
    provider
  );
  const nonce = await entryPoint.getNonce(smartAccountAddress, 0);

  const userOp = {
    sender: smartAccountAddress,
    nonce: nonce.toString(),
    initCode: "0x",
    callData,
    callGasLimit: "0x40000",
    verificationGasLimit: "0x40000",
    preVerificationGas: "0x10000",
    maxFeePerGas: ethers.parseUnits("10", "gwei").toString(),
    maxPriorityFeePerGas: ethers.parseUnits("2", "gwei").toString(),
    paymasterAndData: "0x",
    signature: "0x",
  };

  // Sign the UserOp hash
  const userOpHash = await provider.send("eth_getUserOperationByHash", [userOp]);
  userOp.signature = await signer.signMessage(ethers.getBytes(userOpHash));

  // Submit via bundler
  const hash = await provider.send("eth_sendUserOperation", [userOp, ENTRY_POINT]);
  return hash;
}

Paymaster Pattern (Gas Sponsorship)

interface IPaymaster {
    function validatePaymasterUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external returns (bytes memory context, uint256 validationData);

    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost
    ) external;
}

ERC-4337 key facts for AI agents:

  • EntryPoint is a singleton deployed at the same address on all chains
  • initCode = factory address + calldata to create the account
  • Bundlers require a stake in the EntryPoint to prevent DoS
  • ERC-4337 v0.6 and v0.7 have different UserOp structures (v0.7 splits gas fields)

ERC-6551 — Token Bound Accounts

ERC-6551 gives every ERC-721 NFT its own smart contract wallet. The NFT is the account owner.

interface IERC6551Registry {
    function createAccount(
        address implementation,
        bytes32 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external returns (address account);

    function account(
        address implementation,
        bytes32 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external view returns (address account);
}

// Registry deployed at: 0x000000006551c19487814612e58FE06813775758

TBA (Token Bound Account) Interface

interface IERC6551Account {
    receive() external payable;

    function token() external view returns (
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    );

    function state() external view returns (uint256);

    function isValidSigner(address signer, bytes calldata context)
        external view returns (bytes4 magicValue);
}

interface IERC6551Executable {
    function execute(
        address to,
        uint256 value,
        bytes calldata data,
        uint8 operation  // 0=CALL, 1=DELEGATECALL
    ) external payable returns (bytes memory result);
}

TBA Usage Pattern

const REGISTRY = "0x000000006551c19487814612e58FE06813775758";
const IMPLEMENTATION = "0x..."; // Your TBA implementation

// Compute TBA address (deterministic, no deployment needed yet)
const tbaAddress = await registry.account(
  IMPLEMENTATION, salt, chainId, nftContract, tokenId
);

// Create the TBA (deploys if not exists)
await registry.createAccount(IMPLEMENTATION, salt, chainId, nftContract, tokenId);

// TBA executes transactions as the NFT's identity
await tba.execute(targetContract, value, callData, 0 /* CALL */);

Use cases: Gaming characters that own their inventory, NFT galleries that hold their art, DAO memberships with associated assets.

EIP-1967 — Standard Proxy Storage Slots

EIP-1967 defines standard storage slots for proxy contracts to avoid storage collisions with the implementation contract.

// Implementation slot: keccak256('eip1967.proxy.implementation') - 1
bytes32 constant IMPLEMENTATION_SLOT =
    0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

// Admin slot: keccak256('eip1967.proxy.admin') - 1
bytes32 constant ADMIN_SLOT =
    0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

// Beacon slot: keccak256('eip1967.proxy.beacon') - 1
bytes32 constant BEACON_SLOT =
    0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

Reading proxy implementation (ethers.js)

const IMPL_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";

async function getProxyImplementation(proxyAddress: string, provider: ethers.Provider) {
  const raw = await provider.getStorage(proxyAddress, IMPL_SLOT);
  return ethers.getAddress("0x" + raw.slice(26)); // Last 20 bytes = address
}

Proxy patterns using EIP-1967:

  • Transparent Proxy: Admin can upgrade, users cannot call admin functions
  • UUPS Proxy: Upgrade logic in implementation, more gas efficient
  • Beacon Proxy: Multiple proxies share one implementation via beacon

ERC-1167 — Minimal Proxy (Clone Factory)

ERC-1167 defines a bytecode template for creating cheap clones of a contract. The clone delegates all calls to the implementation.

Minimal proxy bytecode: 45 bytes
Cost to deploy: ~42,000 gas (vs ~300,000+ for full contract)
// OpenZeppelin Clones library
import "@openzeppelin/contracts/proxy/Clones.sol";

contract Factory {
    address public immutable implementation;

    constructor(address _implementation) {
        implementation = _implementation;
    }

    function createClone(bytes32 salt) external returns (address clone) {
        clone = Clones.cloneDeterministic(implementation, salt);
        IMyContract(clone).initialize(msg.sender); // Initialize instead of constructor
    }

    function predictAddress(bytes32 salt) external view returns (address) {
        return Clones.predictDeterministicAddress(implementation, salt);
    }
}

When to use: Creating many instances of the same contract (e.g., per-user vaults, DAO proposals, escrow contracts). Savings are massive at scale.

ERC-2771 — Meta-Transactions (Gasless)

ERC-2771 defines a protocol for trusted forwarders to relay transactions on behalf of users, enabling gasless UX.

abstract contract ERC2771Context {
    address private immutable _trustedForwarder;

    constructor(address trustedForwarder) {
        _trustedForwarder = trustedForwarder;
    }

    function isTrustedForwarder(address forwarder) public view returns (bool) {
        return forwarder == _trustedForwarder;
    }

    // Override msg.sender to extract original sender from calldata
    function _msgSender() internal view virtual override returns (address sender) {
        if (isTrustedForwarder(msg.sender)) {
            assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) }
        } else {
            return super._msgSender();
        }
    }
}

ERC-2771 vs ERC-4337:

  • ERC-2771: Simpler, works with existing EOA wallets, requires trusted forwarder
  • ERC-4337: More powerful, full smart wallet, no trusted third party required

Architecture Decision Guide

GoalSolution
Smart wallet with custom validationERC-4337
Gasless transactions (simple)ERC-2771 + Gelato/Biconomy
NFT that owns assetsERC-6551 TBA
Upgradeable contractEIP-1967 proxy (UUPS or Transparent)
Deploy many identical contracts cheaplyERC-1167 minimal proxy
Smart wallet + gas sponsorshipERC-4337 + Paymaster