ERC Account & Proxy Standards (ERC-4337, ERC-6551, EIP-1967)
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
| Standard | Purpose | Complexity | Adoption |
|---|---|---|---|
| ERC-4337 | Account Abstraction (smart wallets) | High | Growing rapidly |
| ERC-6551 | Token Bound Accounts (NFT has wallet) | Medium | Emerging |
| EIP-1967 | Standard proxy storage slots | Low | Universal |
| ERC-1167 | Minimal proxy (clone factory) | Low | Universal |
| ERC-2771 | Meta-transactions (gasless) | Medium | Mature |
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
| Goal | Solution |
|---|---|
| Smart wallet with custom validation | ERC-4337 |
| Gasless transactions (simple) | ERC-2771 + Gelato/Biconomy |
| NFT that owns assets | ERC-6551 TBA |
| Upgradeable contract | EIP-1967 proxy (UUPS or Transparent) |
| Deploy many identical contracts cheaply | ERC-1167 minimal proxy |
| Smart wallet + gas sponsorship | ERC-4337 + Paymaster |