DEV Community

metadevdigital
metadevdigital

Posted on

Understanding ERC-4337: Account Abstraction Without Protocol Changes

Understanding ERC-4337: Account Abstraction Without Protocol Changes

cover

When I first read the ERC-4337 spec, I had one dumb question: if we can't change Ethereum itself, how do we actually abstract away the account model that's baked into the protocol?

Turns out, the answer is elegant but unintuitive. You don't abstract the account model—you route around it.

The Problem Nobody Solved Until 2021

EOAs suck. ecrecover, nonces, sequential ordering, fixed gas costs, the inability to batch transactions—they're all baked into the protocol as immutable constraints. Vitalik and others spent years sketching solutions that required consensus-layer changes. Then Yoichi Hirai and the team realized: you don't need to fork Ethereum. You can build the abstraction layer on top using contract-based smart wallets.

But here's the gotcha: if your smart wallet is just a contract, who pays for its creation? Who orders its transactions? That's where EntryPoint comes in.

How It Actually Works

Instead of sending regular tx objects to the network, users sign UserOperation objects and send them to bundlers. Bundlers collect UserOps, validate them, wrap them in a bundle, and execute them all at once through a single EntryPoint contract deployed at a fixed address.

The EntryPoint validates that each UserOp has a valid signature (delegated to the account contract via validateUserOp) and executes the UserOp's calldata through the account contract.

Here's a minimal UserOp:

const userOp = {
  sender: mySmartWalletAddress,
  nonce: 0n,
  initCode: "0x", // only needed if wallet doesn't exist yet
  callData: "0x", // the actual transaction
  callGasLimit: 100000n,
  verificationGasLimit: 100000n,
  preVerificationGas: 21000n,
  maxFeePerGas: ethers.parseUnits("10", "gwei"),
  maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"),
  paymasterAndData: "0x", // optional, for gas sponsoring
  signature: "0x", // signed by the wallet owner
};
Enter fullscreen mode Exit fullscreen mode

The paymasterAndData field is where things get interesting. A paymaster is another contract that can sponsor gas costs. Instead of the user paying for gas, the bundler fronts it, and the paymaster reimburses. Gas-free UX without touching the protocol layer.

// IAccount interface that all smart wallets must implement
interface IAccount {
    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external returns (uint256 validationData);

    function execute(address dest, uint256 value, bytes calldata func) external;
}
Enter fullscreen mode Exit fullscreen mode

Your wallet just implements this. Nothing else needed.

The Bundler's Job (and Why Centralization Matters)

Bundlers listen to the userOp mempool, validate each UserOp without executing it (merkle proofs, signature checks, nonce validation), group UserOps into a handleOps call, and execute the bundle on-chain. The bundler needs to ensure a UserOp is profitable to include. If a user sets maxFeePerGas too low, bundlers reject it. If a paymaster is sketchy and might fail, bundlers reject it. This is fundamentally a private operation, not consensus-based.

Tbh, this is a weak point. Mainnet has very few bundlers (last I checked, maybe 5-10 reliable ones), which creates centralization risk. If bundlers collude or all go down, the network seizes.

Gotchas I've Actually Hit

Nonce ordering is NOT enforced by EntryPoint. Your wallet manages its own nonce. If you send UserOps with nonces [0, 2], EntryPoint might execute them out of order. You have to implement sequential nonce validation in validateUserOp or use the special "key-based" nonce scheme and handle your own ordering. Took me two hours to debug this in staging.

verificationGasLimit is NOT an EVM limit. It's just a value bundlers check. The actual gas your wallet uses can exceed this, and the transaction will fail mid-execution. Bundlers are supposed to reject UserOps that'll fail, but they're not perfect. Test with realistic gas budgets.

Paymaster validation happens twice: once by bundlers (off-chain), once by EntryPoint (on-chain). If the second validation fails, the entire bundle reverts. A single sketchy UserOp kills the whole batch. This is why mainstream paymasters are conservative.

You need initCode to bootstrap. If the wallet doesn't exist, you must provide the factory bytecode plus constructor args. This gets concatenated and called during UserOp validation. First-time UX is noticeably slower because of this extra bytecode.

Cross-chain attacks. If you're using the same signer for multiple chains, replay is theoretically possible unless you include the chainId in your signature. Always include it.

Real-World Example

Look at Alchemy's light account implementation (https://github.com/alchemyplatform/light-account). They've shipped a production ERC-4337 wallet. One thing that stands out: they use OpenZeppelin's Ownable pattern but override it to ensure owner changes go through the execute flow, not directly. Prevents someone from signing a malicious UserOp that changes the owner.

The spec is implemented live on mainnet at 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 (the reference EntryPoint v0.6).

The Real Answer to My Original Question

ERC-4337 doesn't change Ethereum—it works within Ethereum's constraints by treating smart wallets as first-class citizens via contract execution. The account abstraction isn't in the protocol; it's in the social consensus that bundlers and EntryPoint operators agree to a new execution model. It's pragmatic, not elegant, which is probably why it actually shipped.

Top comments (0)