Account Abstraction (ERC-4337) in Production: LA Tech Week 2025 Adoption Report

Just attended the “Account Abstraction in Production” workshop at LA Tech Week 2025, and I’m convinced: ERC-4337 is finally ready for mainstream adoption.

We’re seeing real traction - over 12 million smart accounts deployed across chains, and the UX improvements are massive.

What is Account Abstraction (ERC-4337)?

Traditional EOA (Externally Owned Accounts):

  • Controlled by single private key
  • Must hold ETH for gas
  • No transaction batching
  • No social recovery
  • Lost key = lost funds forever

Smart Contract Accounts (ERC-4337):

  • Programmable account logic
  • Gasless transactions (paymasters)
  • Batch multiple operations
  • Social recovery (guardians)
  • Session keys (limited permissions)
  • Hardware 2FA support

Key difference: Your wallet is a smart contract, not just a key pair.

The ERC-4337 Architecture

Core Components

1. UserOperation (UserOp)

  • Pseudo-transaction object (not a real Ethereum transaction)
  • Contains: sender, nonce, calldata, gas limits, paymaster info, signature

2. Bundler

  • Collects UserOps from mempool
  • Bundles multiple UserOps into single transaction
  • Sends to EntryPoint contract

3. EntryPoint Contract

  • Single global contract (0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789)
  • Validates and executes UserOps
  • Manages gas accounting

4. Paymaster

  • Optional contract that sponsors gas fees
  • Allows gasless transactions for users
  • Can implement custom gas policies

5. Smart Account

  • User’s wallet contract
  • Implements validation and execution logic
  • Can have custom features (multisig, recovery, etc.)

Transaction Flow

User → Create UserOp → Send to Bundler Pool
                              ↓
                    Bundler collects UserOps
                              ↓
                    Bundle → EntryPoint.handleOps()
                              ↓
                    EntryPoint validates each UserOp
                              ↓
                    EntryPoint executes account.execute()
                              ↓
                    Paymaster pays gas (if present)

Real-World Use Cases from LA Tech Week

1. Gasless Onboarding (Coinbase Wallet)

Problem: New users don’t have ETH for gas fees.

Solution: Paymaster sponsors first N transactions.

// Paymaster contract
contract OnboardingPaymaster is BasePaymaster {
    mapping(address => uint256) public sponsoredTxCount;
    uint256 constant MAX_SPONSORED = 10;

    function validatePaymasterUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external override returns (bytes memory context, uint256 validationData) {
        address sender = userOp.sender;

        // Check if user still eligible for sponsored gas
        require(sponsoredTxCount[sender] < MAX_SPONSORED, "Sponsorship limit reached");

        sponsoredTxCount[sender]++;

        // Approve payment
        return ("", 0);
    }

    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost
    ) external override {
        // Paymaster pays the gas cost
        // (deducted from paymaster deposit in EntryPoint)
    }
}

Results (Coinbase Wallet, Q3 2025):

  • 2.3M users onboarded with gasless transactions
  • 94% retention vs 67% with traditional gas-required flow
  • $1.2M in sponsored gas costs → $15M in user deposits

2. Social Recovery (Argent, Safe)

Problem: Lost seed phrase = lost funds.

Solution: Guardians can recover account.

contract SocialRecoveryAccount is BaseAccount {
    address public owner;
    address[] public guardians;
    uint256 public recoveryThreshold; // e.g., 2 of 3 guardians

    mapping(address => uint256) public recoveryVotes;
    address public proposedOwner;

    function proposeRecovery(address newOwner) external {
        require(isGuardian(msg.sender), "Not a guardian");

        if (proposedOwner != newOwner) {
            proposedOwner = newOwner;
            recoveryVotes[newOwner] = 0;
        }

        recoveryVotes[newOwner]++;

        if (recoveryVotes[newOwner] >= recoveryThreshold) {
            owner = newOwner;
            delete proposedOwner;
            emit RecoveryExecuted(newOwner);
        }
    }

    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external override returns (uint256 validationData) {
        bytes32 hash = userOpHash.toEthSignedMessageHash();
        address signer = hash.recover(userOp.signature);

        // Validate signature from current owner
        require(signer == owner, "Invalid signature");

        return 0; // Valid
    }
}

Real data (Argent, 2025):

  • 47,000 account recoveries performed
  • $180M in assets recovered
  • Average recovery time: 48 hours

3. Session Keys (Gaming)

Problem: Users don’t want to sign every transaction in games.

Solution: Grant limited permissions to session key.

contract SessionKeyAccount is BaseAccount {
    struct SessionKey {
        address key;
        uint48 validUntil;
        uint48 validAfter;
        address allowedContract; // Can only call this contract
        uint256 maxGasPrice;
    }

    mapping(address => SessionKey) public sessionKeys;

    function addSessionKey(
        address key,
        uint48 validUntil,
        address allowedContract,
        uint256 maxGasPrice
    ) external onlyOwner {
        sessionKeys[key] = SessionKey({
            key: key,
            validUntil: validUntil,
            validAfter: uint48(block.timestamp),
            allowedContract: allowedContract,
            maxGasPrice: maxGasPrice
        });
    }

    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external override returns (uint256 validationData) {
        bytes32 hash = userOpHash.toEthSignedMessageHash();
        address signer = hash.recover(userOp.signature);

        // Check if signer is session key
        SessionKey storage session = sessionKeys[signer];

        if (session.key == signer) {
            // Validate session constraints
            require(block.timestamp >= session.validAfter, "Session not started");
            require(block.timestamp <= session.validUntil, "Session expired");
            require(userOp.maxFeePerGas <= session.maxGasPrice, "Gas too high");

            // Validate target contract
            address target = address(bytes20(userOp.callData[0:20]));
            require(target == session.allowedContract, "Invalid contract");

            return 0; // Valid session key
        }

        // Fall back to owner validation
        require(signer == owner, "Invalid signature");
        return 0;
    }
}

Use case: Web3 Gaming

  • Player authorizes session key for 24 hours
  • Session key can only interact with game contract
  • Max 50 gwei gas price
  • Player doesn’t sign every action (better UX)

4. Batch Transactions

Problem: Multiple approvals required (approve token, then swap).

Solution: Batch into single UserOp.

// Create UserOp with batched calls
const userOp = {
  sender: accountAddress,
  nonce: await account.getNonce(),
  callData: account.interface.encodeFunctionData('executeBatch', [
    [
      // Call 1: Approve USDC
      {
        to: USDC_ADDRESS,
        value: 0,
        data: usdc.interface.encodeFunctionData('approve', [
          UNISWAP_ROUTER,
          ethers.parseUnits('1000', 6)
        ])
      },
      // Call 2: Swap on Uniswap
      {
        to: UNISWAP_ROUTER,
        value: 0,
        data: router.interface.encodeFunctionData('swapExactTokensForTokens', [
          ethers.parseUnits('1000', 6),
          ethers.parseUnits('990', 6),
          [USDC_ADDRESS, WETH_ADDRESS],
          accountAddress,
          deadline
        ])
      }
    ]
  ]),
  // Gas limits, paymaster, signature...
};

// Send to bundler
await bundler.sendUserOperation(userOp);

Result: Two transactions → One signature, one confirmation.

Production Adoption Numbers (LA Tech Week 2025)

Smart Account Deployments

Total deployed (all chains, Oct 2025): 12.4 million

By provider:

  • Safe (formerly Gnosis Safe): 4.2M accounts
  • Biconomy: 3.1M accounts
  • Alchemy Account Kit: 2.8M accounts
  • ZeroDev: 1.4M accounts
  • Coinbase Smart Wallet: 0.9M accounts

Transaction Volume

Monthly UserOps (Sept 2025): 28 million
Gas sponsored by paymasters: $4.2M/month
Average UserOp cost: $0.15 (vs $0.50 for regular tx)

Chain Distribution

  • Polygon: 35% of smart accounts (low gas costs)
  • Optimism: 22% (OP Stack adoption)
  • Base: 18% (Coinbase integration)
  • Arbitrum: 15%
  • Ethereum mainnet: 10% (high-value accounts only)

Developer Tools & SDKs

1. Alchemy Account Kit

import { AlchemyProvider } from '@alchemy/aa-alchemy';
import { LightSmartContractAccount } from '@alchemy/aa-accounts';

const provider = new AlchemyProvider({
  apiKey: 'your-api-key',
  chain: mainnet,
});

const account = new LightSmartContractAccount({
  chain: mainnet,
  owner: ownerAddress,
  factoryAddress: '0x...',
  rpcClient: provider,
});

// Send UserOp
const result = await provider.sendUserOperation({
  target: '0x...',
  data: '0x...',
  value: 0n,
});

console.log(`UserOp hash: ${result.hash}`);

2. Biconomy SDK

import { BiconomySmartAccount } from '@biconomy/account';
import { Bundler, Paymaster } from '@biconomy/modules';

const bundler = new Bundler({
  bundlerUrl: 'https://bundler.biconomy.io/api/v2/80001/...',
});

const paymaster = new Paymaster({
  paymasterUrl: 'https://paymaster.biconomy.io/api/v1/80001/...',
});

const smartAccount = await BiconomySmartAccount.create({
  signer: signer,
  bundler: bundler,
  paymaster: paymaster,
});

// Gasless transaction
const tx = await smartAccount.sendTransaction({
  to: '0x...',
  data: '0x...',
});

3. ZeroDev Kernel

import { createKernelAccount, createKernelAccountClient } from '@zerodev/sdk';

const account = await createKernelAccount(publicClient, {
  signer: signer,
});

const client = createKernelAccountClient({
  account,
  chain: mainnet,
  transport: http('https://rpc.url'),
  sponsorUserOperation: async (userOp) => {
    // Custom paymaster logic
  },
});

// Send transaction
const hash = await client.sendTransaction({
  to: '0x...',
  value: parseEther('0.1'),
});

Security Considerations

1. Signature Validation Vulnerabilities

Risk: Weak signature validation allows unauthorized access.

Best practice:

function validateUserOp(...) external override returns (uint256) {
    bytes32 hash = userOpHash.toEthSignedMessageHash();

    // ALWAYS use EIP-191 or EIP-712 signed message hash
    // NEVER use raw userOpHash (vulnerable to replay)

    address signer = hash.recover(userOp.signature);
    require(signer == owner, "Invalid signature");

    return 0;
}

2. Paymaster Abuse

Risk: Malicious users drain paymaster funds.

Mitigation:

  • Rate limiting per address
  • Whitelist allowed operations
  • Max gas price caps
  • Circuit breakers for unusual activity

3. Upgradeability Risks

Risk: Malicious upgrade steals funds.

Best practice:

  • Use timelock for upgrades (24-48 hours)
  • Require multisig approval
  • Transparent proxy pattern
  • Immutable critical functions (ownership transfer)

My Questions for the Community

  1. Which provider are you using? Alchemy, Biconomy, ZeroDev, or custom?

  2. Paymaster economics: How much should apps spend on sponsored gas? Is it worth it for retention?

  3. Session keys: What’s the right permission model? Time-based, transaction count, or spend limit?

  4. Recovery mechanisms: Social recovery (guardians) vs hardware 2FA vs both?

Account abstraction is finally here. The UX improvements are real and measurable.

David Kim
DAO Tooling Developer


Resources:

David, great overview of ERC-4337 adoption! From a security perspective, I need to highlight the new attack vectors that smart accounts introduce. Account abstraction is powerful, but power requires careful security design.

Critical Security Risks

1. Signature Validation Bugs

The most dangerous vulnerability. If validation is weak, anyone can control the account.

Common mistake: Raw hash signing

// VULNERABLE
function validateUserOp(...) external returns (uint256) {
    address signer = userOpHash.recover(userOp.signature);
    require(signer == owner, "Invalid");
    return 0;
}

Attack: Replay signatures across chains or accounts.

Fix: Use EIP-712 structured signing

// SAFE
bytes32 domainSeparator = keccak256(abi.encode(
    DOMAIN_TYPEHASH,
    block.chainid,
    address(this)
));

bytes32 structHash = keccak256(abi.encode(
    USEROP_TYPEHASH,
    userOp.sender,
    userOp.nonce,
    keccak256(userOp.callData)
));

bytes32 digest = keccak256(abi.encodePacked(
    "\x19\x01",
    domainSeparator,
    structHash
));

address signer = digest.recover(userOp.signature);

2. Paymaster Draining

Attack scenario:

  1. Attacker creates many smart accounts
  2. Submits gas-expensive UserOps to public paymaster
  3. Paymaster pays gas, funds drained

Defense:

contract RateLimitedPaymaster is BasePaymaster {
    mapping(address => uint256) public gasSponsored;
    mapping(address => uint256) public lastReset;

    uint256 constant DAILY_LIMIT = 0.1 ether;
    uint256 constant RESET_PERIOD = 1 days;

    function validatePaymasterUserOp(...) external override {
        address sender = userOp.sender;

        // Reset counter if 24h passed
        if (block.timestamp - lastReset[sender] > RESET_PERIOD) {
            gasSponsored[sender] = 0;
            lastReset[sender] = block.timestamp;
        }

        // Check limit
        require(
            gasSponsored[sender] + maxCost <= DAILY_LIMIT,
            "Daily limit exceeded"
        );

        gasSponsored[sender] += maxCost;
    }
}

3. Session Key Privilege Escalation

Risk: Session key gains more permissions than intended.

Example vulnerability:

// VULNERABLE: Session key can call ANY contract
function validateUserOp(...) external override {
    if (isSessionKey(signer)) {
        require(block.timestamp < sessionExpiry, "Expired");
        return 0; // Valid - but no contract restriction!
    }
}

Attack: Session key calls account’s addOwner() function, takes full control.

Fix:

// SAFE: Restrict session key to specific contracts
if (isSessionKey(signer)) {
    require(block.timestamp < sessionExpiry, "Expired");

    address target = address(bytes20(userOp.callData[16:36]));
    require(
        sessionKeyAllowedContracts[signer][target],
        "Contract not allowed"
    );
}

Audit Checklist for Smart Accounts

Before deploying, verify:

  • EIP-712 signature validation (not raw hash)
  • Nonce increment enforced (prevent replays)
  • Upgrade mechanism secured (timelock + multisig)
  • Recovery mechanism rate-limited (prevent guardian attacks)
  • Session keys scoped to specific contracts
  • Gas limits enforced (prevent DoS)
  • Paymaster has abuse protection
  • EntryPoint deposit sufficient for operations

Recommend audits: OpenZeppelin, Trail of Bits, or Certora for formal verification.

Brian Zhang
Protocol Architect @ LayerZero

David, Brian - excellent coverage! Let me add a practical integration guide. I just shipped ERC-4337 for a production dapp at LA Tech Week, and here’s what worked.

Quick Start: Add Account Abstraction in 1 Hour

Goal: Convert existing dapp to support gasless transactions.

Step 1: Choose SDK (Alchemy Account Kit)

npm install @alchemy/aa-alchemy @alchemy/aa-accounts ethers@6

Step 2: Create Smart Account

import { AlchemyProvider } from '@alchemy/aa-alchemy';
import { LightSmartContractAccount } from '@alchemy/aa-accounts';
import { sepolia } from 'viem/chains';

// 1. Initialize provider
const provider = new AlchemyProvider({
  apiKey: process.env.ALCHEMY_API_KEY,
  chain: sepolia,
});

// 2. Create account from EOA signer
const account = new LightSmartContractAccount({
  chain: sepolia,
  owner: signer, // Your existing ethers signer
  factoryAddress: '0x...', // LightAccount factory
  rpcClient: provider,
});

// 3. Send gasless transaction
const result = await provider.sendUserOperation({
  target: contractAddress,
  data: contract.interface.encodeFunctionData('transfer', [to, amount]),
  value: 0n,
});

console.log(`UserOp: ${result.hash}`);

Result: Gasless transaction sent. Alchemy paymaster sponsors gas.

Step 3: Fund Paymaster

Paymasters need ETH deposits in EntryPoint:

const entryPoint = new ethers.Contract(
  '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
  ENTRYPOINT_ABI,
  signer
);

// Deposit 0.5 ETH to sponsor ~500 transactions
await entryPoint.depositTo(paymasterAddress, {
  value: ethers.parseEther('0.5')
});

Monitoring: Track paymaster balance, auto-refill when low.

Production Tips

1. Gas estimation: UserOps have different gas structure (verificationGas, callGas, preVerificationGas).

2. Bundler selection: Use multiple bundlers for redundancy (Alchemy, Pimlico, StackUp).

3. Fallback: If AA fails, fall back to regular transaction.

Integration is straightforward - most complexity handled by SDKs.

Chris Anderson
Full-Stack Crypto Developer