Skip to main content

Command Palette

Search for a command to run...

ERC-4337: Smart Wallets Without Changing Ethereum

Updated
9 min read

Ethereum wallets are still weirdly primitive.

For most users, a wallet means:

  • One private key
  • One seed phrase
  • One account
  • One scary signing popup
  • Zero forgiveness

Lose the key?

Gone.

Sign the wrong thing?

Pain.

Need to approve, swap, and stake?

Enjoy three transactions and a small identity crisis.

ERC-4337 is one of Ethereum’s cleaner answers to this problem.

It brings account abstraction to Ethereum without changing the base protocol.

Translation:

Users can use smart contract wallets as their main accounts.

And smart contracts can do more than “one private key signed this, therefore ship it.”


The Old Model: EOAs

Normal Ethereum wallets are Externally Owned Accounts, or EOAs.

An EOA is controlled by a private key.

The rule is simple:

Valid signature from private key = transaction allowed

That works.

But it is limited.

EOAs do not natively support:

  • Social recovery
  • Multisig rules
  • Gas sponsorship
  • Batched transactions
  • Session keys
  • Spending limits
  • Custom signature schemes

EOAs are basically keys with balances.

Very powerful.

Very unforgiving.

Very “hope you wrote down those 12 words correctly.”


The New Model: Smart Accounts

A smart account is a wallet built as a smart contract.

That means the wallet can have programmable rules.

For example:

2 of 3 guardians can recover this wallet

Or:

This app can spend up to 20 USDC per day

Or:

This game session key works for 2 hours and cannot move NFTs

Or:

The dapp sponsors gas for this new user

That is the big shift.

EOA = account controlled by a key
Smart account = account controlled by code

More flexible.

Also easier to mess up if the code is bad.

Because of course 🥀


So What Is ERC-4337?

ERC-4337 is a standard that lets smart accounts behave like first-class wallets.

The clever part:

It does this without requiring Ethereum consensus changes.

Instead of creating a new native transaction type, ERC-4337 creates a new flow using:

  • UserOperation
  • Bundlers
  • EntryPoint
  • Smart accounts
  • Paymasters
  • Factories

The user does not directly send a normal Ethereum transaction.

The user signs a UserOperation.

Then a bundler submits it onchain through the EntryPoint smart contract.


The ERC-4337 Flow

High level:

  1. User wants to do something
  2. Wallet creates a UserOperation
  3. User signs it
  4. Bundler receives it
  5. Bundler simulates it
  6. Bundler calls EntryPoint.handleOps(...)
  7. EntryPoint asks the smart account to validate it
  8. Smart account executes the action
  9. Bundler gets paid

Diagram:

User
  -> signs UserOperation
  -> Bundler
  -> EntryPoint
  -> Smart Account
  -> Target Contract

That is the whole machine.

A little more plumbing than EOAs.

A lot more flexibility.


UserOperation: The Almost-Transaction

A UserOperation is like a transaction, but for smart accounts.

A simplified version:

struct PackedUserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    bytes32 accountGasLimits;
    uint256 preVerificationGas;
    bytes32 gasFees;
    bytes paymasterAndData;
    bytes signature;
}

The main fields:

  • sender: the smart account
  • nonce: replay protection
  • initCode: optional account deployment data.
  • callData: what the account should execute
  • paymasterAndData: optional gas sponsorship data
  • signature: proof the operation is authorized

So the user signs this object.

The smart account decides whether the signature and operation are valid.


EntryPoint: The Traffic Controller

The EntryPoint is the central contract in ERC-4337.

It:

  • Receives UserOperations from bundlers
  • Validates them
  • Deploys accounts if needed
  • Executes calls
  • Handles gas accounting
  • Pays the bundler

Simplified interface:

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

interface IEntryPoint {
    function handleOps(
        PackedUserOperation[] calldata ops,
        address payable beneficiary
    ) external;

    function getUserOpHash(
        PackedUserOperation calldata userOp
    ) external view returns (bytes32);

    function depositTo(address account) external payable;

    function balanceOf(address account) external view returns (uint256);
}

The bundler calls:

entryPoint.handleOps(userOps, bundlerAddress);

The smart account trusts a specific EntryPoint.

That version matters.

Wrong EntryPoint, wrong day.


Bundlers

A bundler is a relayer.

It takes UserOperations and submits them to Ethereum.

The bundler:

  • Receives UserOperations
  • Simulates them offchain
  • Bundles valid ones together
  • Sends a normal transaction to the EntryPoint
  • Gets reimbursed for gas

So users do not need to submit normal Ethereum transactions directly.

They sign intents.

Bundlers get them onchain.

Very useful.

Slightly more moving parts.

Welcome to crypto.


Smart Accounts

The smart account is the user’s actual wallet contract.

It validates UserOperations.

A common validation function looks like this:

function validateUserOp(
    PackedUserOperation calldata userOp,
    bytes32 userOpHash,
    uint256 missingAccountFunds
) external returns (uint256 validationData);

The EntryPoint calls this.

The account checks things like:

  • Is the caller the trusted EntryPoint?
  • Is the signature valid?
  • Is the nonce correct?
  • Is this operation allowed?
  • Does the account need to send ETH to EntryPoint for gas?

If valid, execution continues.

If not, the operation fails.

Basically, this is where the programmability lives


Tiny Simple Account Example

This is a stripped-down educational account.

Do not ship this directly.

Please do not become a postmortem🙂

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

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract SimpleAccount {
    using ECDSA for bytes32;

    address public owner;
    IEntryPoint public immutable entryPoint;

    uint256 internal constant SIG_VALIDATION_SUCCESS = 0;
    uint256 internal constant SIG_VALIDATION_FAILED = 1;

    modifier onlyEntryPoint() {
        require(msg.sender == address(entryPoint), "not EntryPoint");
        _;
    }

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

    receive() external payable {}

    function execute(
        address target,
        uint256 value,
        bytes calldata data
    ) external onlyEntryPoint {
        (bool ok, bytes memory result) = target.call{value: value}(data);

        if (!ok) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }
    }

    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external onlyEntryPoint returns (uint256 validationData) {
        bytes32 hash = ECDSA.toEthSignedMessageHash(userOpHash);
        address recovered = ECDSA.recover(hash, userOp.signature);

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

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

        return SIG_VALIDATION_SUCCESS;
    }
}

This account is still simple:

Valid owner signature = allowed

But because it is a contract, you can replace that logic with something better.

Multisig.

Guardians.

Passkeys.

Session keys.

Spending limits.

Whatever your wallet needs.


Factories

Smart accounts are contracts, so they need to be deployed.

A factory handles that.

With CREATE2, a factory can compute the account address before deployment.

That means a user can have a wallet address before the smart account exists onchain.

Very weird.

Very useful.

Remember the initCode field in a userop? it's useful with CREATE2 since we compute the address then the entrypoint will call the factory with the initcode that deploys to the address

Simplified idea:

contract SimpleAccountFactory {
    IEntryPoint public immutable entryPoint;

    constructor(IEntryPoint _entryPoint) {
        entryPoint = _entryPoint;
    }

    function createAccount(address owner, bytes32 salt)
        external
        returns (SimpleAccount account)
    {
        account = new SimpleAccount{salt: salt}(owner, entryPoint);
    }
}

The first UserOperation can deploy the account and perform an action.

That removes a lot of onboarding friction.


Paymasters

Paymasters let someone else pay gas.

That someone might be:

  • A dapp
  • A protocol
  • A sponsor
  • A service that accepts ERC-20 payment instead

This enables:

User has no ETH
User signs action
Paymaster sponsors gas
User still uses the app

That is a big deal.

Because “please go buy ETH first” is terrible onboarding.

A paymaster can choose when to sponsor:

  • New users only
  • Certain contracts only
  • Certain actions only
  • Users with valid offchain approval
  • Users paying fees in tokens

Gas abstraction is one of ERC-4337’s sharpest UX wins.


Why ERC-4337 Matters

ERC-4337 unlocks better wallet behavior.

Things like:

Gas sponsorship

Users can use an app without already holding ETH.

Batched transactions

One signature can trigger multiple actions.

approve + swap + stake

Much better than three popups and a prayer.

Social recovery

Lose your key?

Guardians can help recover the account.

No seed phrase treasure hunt required.

Session keys

Give temporary permissions to an app.

Useful for games, trading, subscriptions, and anything that should not require a wallet popup every 12 seconds.

Custom security

Smart accounts can support:

  • Multisig
  • Spending limits
  • Passkeys
  • Hardware policies
  • Different signature schemes

The account defines the rules.

Not the protocol.


What ERC-4337 Does Not Fix

ERC-4337 is not magic dust.

It does not automatically solve:

  • Phishing
  • Bad wallet design
  • Buggy smart accounts
  • Bad paymaster rules
  • Centralized infrastructure
  • Users signing things they do not understand

It gives builders better tools.

You still have to build carefully.

A smart wallet bug is not a normal bug.

It is often a “funds are gone” bug.

Different flavor of sadness.


Quick Recap

ERC-4337 gives Ethereum account abstraction without changing Ethereum consensus.

The main pieces:

  • UserOperation: what the user signs
  • Bundler: submits UserOperations onchain
  • EntryPoint: validates and executes operations
  • Smart account: the user’s programmable wallet
  • Factory: deploys smart accounts
  • Paymaster: optionally pays gas

The mental model:

EOA:
  key signs transaction
  Ethereum validates it

ERC-4337:
  user signs UserOperation
  EntryPoint asks smart account to validate it
  smart account executes if allowed

Shorter:

EOA = key-controlled account
Smart account = code-controlled account

Final Thoughts

ERC-4337 matters because wallet UX matters.

If the account layer is painful, every crypto app inherits that pain.

Smart accounts give us room to build better defaults:

  • Recovery
  • Gas sponsorship
  • Safer permissions
  • Fewer popups
  • Better onboarding

It is not perfect.

It adds new infrastructure and new risks.

But it gives Ethereum a practical path beyond private-key survival mode.

And honestly, that is overdue.

Users should not need to understand gas, nonces, seed phrases, approvals, and mempool drama just to try an app.

ERC-4337 does not remove all the complexity.

But it lets builders hide more of it behind better wallet logic.

That is the point.

Less raw crypto pain.

More usable accounts.

Finally.

Further reading: