LayerZeroFault
passkey recovery Updated:

ERC-4337 AA13 initCode Failed Factory Gas Revert Fix

VV

Written by

Updated:

Technical Guide

The ERC-4337 bundler simulation error AA13 initCode failed or OOG occurs during counterfactual smart account deployment when the account factory reverts or exhausts the allocated verificationGasLimit. In passkey smart wallets, heavy P-256 cryptographic registration and ERC-1967 initialization routinely exceed default gas estimates. Elevating verificationGasLimit and verifying that the counterfactual CREATE2 salt has not already been deployed eliminates the AA13 revert.

To parse packed initCode bytes and inspect factory parameters offline, paste your transaction payload into our interactive ERC-4337 Calldata Decoder Tool. For subsequent signature validation issues, consult our companion guide on ERC-4337 Passkey P256 AA23 signature verification reverts.


Architectural Mechanics: How EntryPoint Executes initCode

In EntryPoint v0.7 (0x0000000071727De22E5E9d8BAf0edAc6f37da032), the deployment logic is decoupled through a specialized helper contract named SenderCreator.

// EntryPoint v0.7: SenderCreator execution
contract SenderCreator {
    function createSender(bytes calldata initCode) external returns (address sender) {
        address factory = address(bytes20(initCode[0:20]));
        bytes memory initCallData = initCode[20:];
        bool success;
        /* assembly call executing factory.call(initCallData) */
        require(success, "AA13 initCode failed or OOG");
        require(sender != address(0), "AA14 initCode must return an address");
    }
}

When a bundler validates the incoming UserOperation:

  1. It reads userOp.verificationGasLimit.
  2. It assigns a fraction of this gas budget to SenderCreator.createSender().
  3. If the factory call runs out of gas, or if any internal opcode inside the factory (e.g. CREATE2, ERC-1967 proxy initialization, or P-256 public key registration) reverts, the entire call returns false, surfacing as AA13.

Root Cause 1: verificationGasLimit Underestimation for P-256 Public Key Storage

Standard ECDSA wallets (like SimpleAccount) only write a single 20-byte Ethereum address into storage during initialization, requiring $\approx 65,000$ gas.

In contrast, a WebAuthn passkey smart account must initialize:

  • A 32-byte $q_x$ coordinate
  • A 32-byte $q_y$ coordinate
  • A variable-length WebAuthn credentialId (typically 32 to 128 bytes)
  • An optional WebAuthn P256Verifier precompile or RIP-7212 fallback address pointer

Writing 3 to 5 new non-zero 32-byte words into cold storage costs up to $5 \times 20,000 = 100,000$ gas in raw EVM storage writes (SSTORE), excluding proxy contract creation overhead!

Standard EOA Proxy Init:        ~65,000 gas
WebAuthn Passkey Proxy Init:    ~185,000 - 240,000 gas
Default Bundler Estimate Cap:   ~150,000 gas  <-- CAUSES OUT OF GAS (OOG)!

If the client application uses standard bundler gas estimation without accounting for the high initialization gas of the passkey factory, createSender exhausts its gas allocation midway through the SSTORE execution.

The Fix: Explicit Deployment Gas Buffer in Viem / Permissionless

When constructing the initial deployment UserOperation, inspect if initCode.length > 2 (indicating a first-time deployment). If true, append a mandatory $150,000$ gas buffer to verificationGasLimit:

// passkey-userop-gas-fix.ts
import { type UserOperation } from 'viem/account-abstraction';

export function patchPasskeyDeploymentGas(userOp: UserOperation<'0.7'>): UserOperation<'0.7'> {
  // Check if account is undergoing counterfactual deployment
  const isDeploying = userOp.factory && userOp.factory !== '0x' && userOp.factoryCalldata && userOp.factoryCalldata !== '0x';

  if (isDeploying) {
    // Standard verification needs ~100k for P-256 verification (RIP-7212 or Solady)
    // Factory deployment needs ~220k for CREATE2 + storage writes
    const MIN_DEPLOYMENT_VERIFICATION_GAS = 350_000n;

    const currentLimit = BigInt(userOp.verificationGasLimit);
    if (currentLimit < MIN_DEPLOYMENT_VERIFICATION_GAS) {
      console.warn(
        `[AA13 Prevention] Elevating verificationGasLimit from ${currentLimit} to ${MIN_DEPLOYMENT_VERIFICATION_GAS}`
      );
      return {
        ...userOp,
        verificationGasLimit: MIN_DEPLOYMENT_VERIFICATION_GAS,
      };
    }
  }

  return userOp;
}

Root Cause 2: Factory Salt Collision & Re-Initialization Revert

Another frequent cause of AA13 occurs when the user or backend attempts to deploy an account with an existing salt, or when the account contract already exists at the computed address:

[Bundler Check]
1. Computes CREATE2 address: 0x1234...abcd
2. Reads code at 0x1234...abcd: EXTCODESIZE > 0 (Already deployed!)
3. Factory executes CREATE2 with salt: Reverts because contract exists!

This occurs in multi-tab applications or when onboarding retries submit a new UserOperation with the same initCode after a previous transaction has already been mined.

The Fix: Pre-Flight Code Check Before Injecting initCode

Before sending any UserOperation to the bundler, query the account’s bytecode on-chain. If bytecode already exists, set factory and factoryCalldata (or initCode) to 0x:

// preflight-code-check.ts
import { type PublicClient, type Address } from 'viem';

export async function sanitizeUserOpInitCode(
  publicClient: PublicClient,
  accountAddress: Address,
  factory: Address,
  factoryCalldata: `0x${string}`
): Promise<{ factory: Address | undefined; factoryCalldata: `0x${string}` | undefined }> {
  // Query bytecode at counterfactual address
  const bytecode = await publicClient.getBytecode({ address: accountAddress });

  if (bytecode && bytecode !== '0x') {
    // Account is ALREADY deployed! Sending initCode will cause CREATE2 collision and AA13 revert.
    return {
      factory: undefined,
      factoryCalldata: undefined,
    };
  }

  // Account does not exist yet; safe to submit factory deployment
  return {
    factory,
    factoryCalldata,
  };
}

Root Cause 3: Malformed WebAuthn Coordinates in Factory Calldata

The factory initializer function typically accepts:

function createAccount(
    bytes calldata credentialId,
    uint256 qx,
    uint256 qy,
    uint256 salt
) external returns (address);

Common encoding pitfalls that cause the factory constructor to revert include:

  1. Unstripped 0x04 uncompressed point prefix: WebAuthn public keys are often encoded in ASN.1 or 65-byte uncompressed format (0x04 || X || Y). Passing the 65-byte buffer directly instead of splitting it into two 32-byte coordinates ($q_x, q_y$) causes ABI decoding reverts.
  2. Empty credentialId bytes: Many factory implementations enforce require(credentialId.length > 0, "InvalidCredential"). If the passkey registration callback failed or returned an empty array, the factory call fails.

WebAuthn Public Key Extraction Helper

Ensure your client-side WebAuthn parser strips the leading 0x04 byte and validates coordinate lengths:

// webauthn-coord-extractor.ts
export function parseWebAuthnCoordinates(uncompressedPublicKey: Uint8Array): {
  qx: bigint;
  qy: bigint;
} {
  // Uncompressed P-256 public key is exactly 65 bytes: 0x04 + 32 bytes X + 32 bytes Y
  if (uncompressedPublicKey.length === 65 && uncompressedPublicKey[0] === 0x04) {
    const xBytes = uncompressedPublicKey.slice(1, 33);
    const yBytes = uncompressedPublicKey.slice(33, 65);

    const qx = BigInt('0x' + Buffer.from(xBytes).toString('hex'));
    const qy = BigInt('0x' + Buffer.from(yBytes).toString('hex'));

    return { qx, qy };
  }

  throw new Error(`Invalid P-256 public key length: ${uncompressedPublicKey.length} (expected 65 bytes)`);
}

Step-by-Step CLI Debugging Guide

When faced with an AA13 initCode failed or OOG error, use Foundry cast to trace the factory call directly in your terminal:

# 1. Simulate the exact factory call via cast
cast call <FACTORY_ADDRESS> <FACTORY_CALLDATA> --rpc-url <RPC_URL>

# 2. If it reverts, view decoded error trace:
cast run <TX_HASH> --rpc-url <RPC_URL>

# 3. Check if target counterfactual address already has code:
cast code <COMPUTED_ACCOUNT_ADDRESS> --rpc-url <RPC_URL>

By ensuring that verificationGasLimit adequately covers cold storage writes, asserting account counterfactual states before injecting initCode, and strictly sanitizing WebAuthn coordinate parameters, passkey smart account deployments complete seamlessly without AA13 bundler rejections.

ERC-4337 EntryPoint Error Code Reference Matrix

Cross-reference standard ERC-4337 EntryPoint revert codes across account abstraction and smart wallet layers:

Error CodeStagePrimary Failure MechanismDiagnostic Guide
AA13Factory DeploymentInitCode execution reverted or exceeded factory gasERC-4337 AA13 InitCode Failed Fix
AA21Prefund ValidationSender account balance or deposit below required maxCostERC-4337 AA21 Prefund Payment Fix
AA22Timestamp ValidityClock drift or expired session key validity windowERC-4337 AA22 Clock Drift Fix
AA23User VerificationP-256 passkey signature or WebCrypto auth failedERC-4337 AA23 Signature Revert Fix
AA33Paymaster PostOpPost-execution token transfer or context revertedERC-4337 AA33 Paymaster PostOp Fix
PaymasterGas SponsorshipPaymaster rejected UserOp or sponsorship signature invalidERC-4337 Paymaster Revert Resolution

[!TIP] Unpack packed UserOperations, calculate prefund requirements, and isolate simulation hazards in our air-gapped /tools/erc4337-calldata-decoder/.

Interactive Diagnostic Sandbox

Local WebAuthn & Smart Account Sandboxes

Debug WebAuthn credential requests, PRF extensions, and simulate ERC-4337 passkey account validation in client WebCrypto.

Companion Diagnostic Guide ERC-4337 Passkey P256 AA23 Signature Revert Fix →

Resolve ERC-4337 EntryPoint AA23 signature validation reverts using WebAuthn P256 keys. Fix authenticatorData UV flags, high-s malleability, and SHA-256 hashes.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

What causes the ERC-4337 AA13 initCode failed or OOG error?

In ERC-4337 (EntryPoint v0.6 and v0.7), AA13 triggers when the EntryPoint executes the initCode field to deploy a counterfactual smart account, but the factory contract reverts or runs out of gas (OOG) before returning a deployed address.

Why is AA13 especially common during passkey / WebAuthn account deployment?

Passkey accounts register 64-byte P-256 public key coordinates during initialization and often configure modular validation hooks. These operations require substantial initialization gas. If verificationGasLimit is estimated using standard EOA proxies, the factory runs out of gas and triggers AA13.

How do I fix CREATE2 salt collision AA13 errors?

If the account was already deployed on-chain by a prior transaction, sending a UserOperation with non-empty initCode causes the factory call to fail. You must set initCode to '0x' on deployed accounts.

How can I inspect the packed initCode and factory calldata?

Use our interactive ERC-4337 UserOp Calldata Decoder tool to separate the factory address from initialization calldata and inspect deployment parameters.