Account Abstraction Bundler Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Account Abstraction Bundler Development
Complex
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Development of Account Abstraction Bundler

Bundler is the central infrastructure component of the ERC-4337 ecosystem. It's what enables Account Abstraction to work: accepts UserOperations from users, validates them, groups into batch and sends on-chain via EntryPoint.handleOps(). Without bundler, Account Abstraction doesn't work — there's no mechanism to deliver UserOps to blockchain.

Developing your own bundler is relevant if: you need custom mempool logic, MEV optimization for UserOps, private bundler for specific application, or need to understand and control entire ERC-4337 infrastructure.

What Bundler Does: Detailed Flow

User creates UserOperation and sends it to bundler via JSON-RPC method eth_sendUserOperation. Bundler performs series of checks, keeps UserOp in its alt mempool, and periodically sends batch on-chain.

Phase 1: Validation

Bundler calls EntryPoint.simulateValidation(userOp). This is view-function (reverts with result via custom error) that:

  1. If initCode not empty — deploys Account contract via factory
  2. Calls account.validateUserOp() — checks signature, nonce
  3. If Paymaster specified — calls paymaster.validatePaymasterUserOp()
  4. Returns ValidationResult with gas data, Paymaster stake info, time constraints
interface ValidationResult {
  returnInfo: {
    preOpGas: bigint;
    prefund: bigint;  // how much ETH account/paymaster deposited in EntryPoint
    sigFailed: boolean;
    validAfter: number;
    validUntil: number;
  };
  senderInfo: StakeInfo;
  factoryInfo?: StakeInfo;
  paymasterInfo?: StakeInfo;
}

prefund — key moment. Account or Paymaster must have deposit in EntryPoint sufficient to cover maxFeePerGas * (verificationGasLimit + callGasLimit). Bundler checks this before adding to mempool.

Storage Access Rules: Why This is Hard

ERC-4337 enforces strict restrictions on what storage can be read/written by validateUserOp. Goal — prevent situation where one UserOp makes others invalid (griefing attack).

Forbidden in validation:

  • Read storage of other contracts, except Account itself and related entities
  • Call block.timestamp, block.number (except limited use via validAfter/validUntil)
  • Access storage that might be changed by another UserOp in same batch

Bundler tracks storage slots accessed during validation via debug_traceCall with EVM tracer. This is expensive operation — one of main performance bottlenecks of bundler.

// Simplified: tracer for tracking storage access
async function traceValidation(userOp: UserOperation): Promise<StorageMap> {
  const trace = await provider.send('debug_traceCall', [{
    to: ENTRY_POINT_ADDRESS,
    data: entryPoint.interface.encodeFunctionData('simulateValidation', [userOp])
  }, 'latest', {
    tracer: bundlerCollectorTracer, // custom JS tracer
    tracerConfig: { /* ... */ }
  }])
  
  return parseStorageAccess(trace)
}

bundlerCollectorTracer — JavaScript tracer for go-ethereum's debug_traceCall. Tracks every SLOAD/SSTORE opcode and links to calling contract. This is most technically non-trivial part of bundler.

Phase 2: Alt Mempool Management

UserOp accepted to mempool must remain valid. Bundler monitors:

Nonce invalidation. If on-chain Account nonce changes (other UserOp passed) — pending UserOp with stale nonce is removed.

Deposit insufficiency. If deposit balance in EntryPoint decreased (other UserOp sponsored by same Paymaster passed) — need to recalculate if enough for all pending UserOps of this Paymaster.

Gas price changes. UserOp with maxFeePerGas below current basefee — won't pass, bundler can temporarily defer or drop.

class UserOpMempool {
  private pool: Map<string, MempoolEntry> = new Map()
  
  async add(userOp: UserOperation): Promise<string> {
    const hash = getUserOpHash(userOp)
    
    // Reputation system: limit by sender/paymaster/factory
    this.reputationManager.checkReputation(userOp)
    
    this.pool.set(hash, {
      userOp,
      prefund: await this.calculatePrefund(userOp),
      addedAt: Date.now()
    })
    
    return hash
  }
  
  getBundle(maxGas: bigint): UserOperation[] {
    // Greedy algorithm: select UserOps with highest priority fee
    // accounting for gas limit and storage conflicts
    return this.selectNonConflicting(
      [...this.pool.values()]
        .sort((a, b) => Number(b.userOp.maxPriorityFeePerGas - a.userOp.maxPriorityFeePerGas)),
      maxGas
    )
  }
}

Phase 3: Bundle Submission

Bundler forms batch from valid UserOps and sends EntryPoint.handleOps(ops, beneficiary). beneficiary — address where EntryPoint sends collected gas (bundler's priority fee).

Critical moment: bundler sends regular EOA transaction. Pays gas upfront, EntryPoint reimburses from Account/Paymaster deposits. If handleOps reverts — bundler loses gas. That's why simulation before sending is mandatory.

Protection from reverting bundle: EntryPoint in handleOps skips UserOps that revert in execution phase (not validation). For validation phase — if revert, entire handleOps fails. Bundler must ensure validation will pass for certain.

Reputation System

To prevent spam and DoS attacks, ERC-4337 introduces reputation system for unbanned entities (Paymaster, Factory, Aggregator). Logic:

class ReputationManager {
  // For each entity track: ops included vs ops unsuccessful
  
  updateIncluded(entity: string): void {
    this.entries[entity].opsSeen++
    this.entries[entity].opsIncluded++
  }
  
  updateFailed(entity: string): void {
    this.entries[entity].opsIncluded-- // if bundle was reverted
  }
  
  getStatus(entity: string): 'ok' | 'throttled' | 'banned' {
    const entry = this.entries[entity]
    if (!entry) return 'ok'
    
    const ratio = entry.opsIncluded / Math.max(1, entry.opsSeen)
    if (ratio < MIN_INCLUSION_RATE_DENOMINATOR) return 'banned'
    if (entry.opsSeen > THROTTLE_THRESHOLD) return 'throttled'
    return 'ok'
  }
}

Staking in EntryPoint raises limits: entity with stake can have more UserOps in mempool. This is anti-spam mechanism: can't freely flood mempool.

P2P Mempool

For decentralized bundler you need P2P alt mempool — network for exchanging UserOps between bundler nodes. ERC-4337 specifies protocol based on libp2p with gossipsub:

  • Topic: user_ops/{chainId}/{entryPointAddress}
  • Message: RLP-encoded UserOperation
  • Validation: each node independently validates before relay
import { createLibp2p } from 'libp2p'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'

const libp2p = await createLibp2p({
  /* ... transport, identify, etc */
  services: {
    pubsub: gossipsub({
      allowPublishToZeroPeers: true,
      msgIdFn: (msg) => computeUserOpHash(msg.data)
    })
  }
})

libp2p.services.pubsub.subscribe(userOpsTopic)
libp2p.services.pubsub.addEventListener('message', async (event) => {
  const userOp = decodeUserOp(event.detail.data)
  await mempool.add(userOp) // with all checks
})

MEV and Bundle Construction

Bundler has unique position: it selects order of UserOps in bundle, opening MEV opportunities. Two strategies:

Honest FIFO bundler — includes UserOps in order received, maximizes priority fee. Simple implementation, good for permissioned bundler for specific application.

MEV-aware bundler — analyzes UserOps callData, finds arbitrage opportunities, constructs bundle optimally. Integration with Flashbots MEV-boost for sending bundle via private mempool.

Implementations for Study and Fork

  • Infinitism/bundler (TypeScript) — reference implementation from ERC-4337 creators
  • Stackup bundler (Go) — production bundler from Stackup
  • Silius (Rust) — high-performance bundler
  • Rundler (Rust) — bundler from Alchemy

For custom development: TypeScript reference is easier for understanding, Rust/Go better for production throughput.

Development Stack

Component Technology
RPC server Node.js / Go / Rust
EVM tracing debug_traceCall + custom JS tracer
Mempool storage Redis / in-memory + persistence
P2P (optional) libp2p + gossipsub
Monitoring Prometheus + Grafana
Testing Foundry + Hardhat (local EntryPoint)

Timeline

Basic centralized bundler with RPC, validation, mempool and bundle submission: 6-8 weeks. Main complexity — correct EVM tracer for storage access rules.

Production bundler with reputation system, P2P mempool, MEV optimization, monitoring: 3-4 months.

Key warning: incorrect storage access rules implementation leads to either accepting dangerous UserOps (DoS risk) or rejecting valid ones (poor UX). Thorough testing on all edge cases is mandatory.