イン , スマートアカウントが展開され、最初の UserOperation が EntryPoint を通じて成功して実行されました. その時点で、すべてが機能しました - しかし、システムの重要な部分はほとんど見えなくなりました: バンドラー。 パート1 バンドラーは、アカウント抽象化とEthereumの実行層の間の橋です。彼らは、ユーザーオペレーションを別々のメプポールから取って、ガスコストを事前に支払い、プロトコルを通じて補償されます。それらがどのように機能するか - 検証規則、評判システム、経済的な奨励金 - を理解することは、問題をバッグ化し、信頼できるアプリケーションを構築するために不可欠です。 User ライフサイクル UserOperation は、スマートアカウントの名義でアクションを実行するために必要なすべてをカプセル化します - 権限、ガス制限、実行コールデータ、およびオプションの paymaster ロジック。 EntryPoint v0.8 では、UserOperations は、パッケージ化された、ガス最適化された形式でチェーン上で処理されます。 type UserOperation = { sender: Address nonce: bigint factory?: Address // Account factory (for first-time deployment) factoryData?: Hex // Factory calldata callData: Hex callGasLimit: bigint verificationGasLimit: bigint preVerificationGas: bigint maxFeePerGas: bigint maxPriorityFeePerGas: bigint paymaster?: Address paymasterVerificationGasLimit?: bigint paymasterPostOpGasLimit?: bigint paymasterData?: Hex signature: Hex } On-chain, the EntryPoint uses a packed format for gas efficiency (combining fields like SDK はこのパッケージを自動的に処理します - 心配する必要はありません。 accountGasLimits = verificationGasLimit | callGasLimit UserOperationのライフサイクルは以下のようになります。 作成:ユーザーはスマートアカウントのSDK( permissionless.js のように)で UserOp を構築します。 署名:ユーザーが UserOp ハッシュに署名し、アクションを承認したことを証明します。 Submission: UserOp は eth_sendUserOperation 経由でバンドラーに送信されます。 検証:Bundler は UserOp をシミュレートして、成功するかどうかをチェックします。 Mempool: If valid, the UserOp enters the bundler's mempool バンドラーの mempool Bundling: Bundler は複数の UserOps を 1 つの handleOps コールにパッケージ化します。 実行: EntryPoint 契約は、チェーン上の各 UserOp を検証し、それらを実行します。 : EntryPoint collects gas costs from each account (or their paymaster). Payment 重要な洞察は、検証が2回起こるということです:バンドラーによってオフチェーンの一度(UserOpを受け入れるかどうかを決定する)、そしてEntryPointによってチェーン上で一度(実際に実行する前に)。 すべての検証ルールは、UserOpがシミュレーションを通過するがチェーン上で失敗する攻撃のクラスを排除するために存在します。 Validation: Why Bundlers Are Paranoid バンドラーがなぜパラノイドなのか バンドラーはガスコストを事前に支払います。バンドルに含まれた後にユーザーオペレーションがチェーン上で失敗した場合、損失は彼らのものです。 That single fact defines the entire bundler threat model. シミュレーションと包摂の間では、Ethereumの状態は静的ではありません。ブロックパラメータは変化し、バランスは変化し、対立的な取引はその間に着陸することができます。慎重に設計されたUserOperationはチェーンシミュレーションを過ぎ去ることができ、まだチェーン検証中に失敗します。 ERC-4337は、認証コードが許可されていることを強く制限することによって反応します。 The EntryPoint enforces a strict separation of concerns: : アカウントのvalidateUserOp機能は、署名を検証し、操作を許可するために実行されます。この段階では、コードがアクセスできるオプコードやストレージに厳しい制限があります。 Validation Phase : アカウントの実行機能は実際の操作を実行します. No restrictions here — full EVM capabilities. Execution Phase Banned Opcodes 特定のオプションコードは、シミュレーションと実行の間に値が変化する可能性があるため、検証中に禁止されています。 TIMESTAMP、NUMBER、COINBASE、PREVRANDAO:ブロック依存値. アカウントは、(block.timestamp > deadline)が戻ってくるかどうかをチェックし、シミュレーションを通過し、バンドルが後にブロックに着陸したときに失敗します。 BLOCKHASH: 異なるブロックの異なる値を返します. 同じ攻撃ベクター。 GASPRICE、BASEFEE:ネットワーク条件に基づく変更。 BALANCE, SELFBALANCE: Only allowed from staked entities (see Staking below) per ERC-7562 [OP-080]. , : Could vary between simulation environment and actual execution. GASLIMIT ORIGIN BLOBHASH、BLOBBASEFEE: EIP-4844 ブロックごとに異なるブロック関連のオプコード。 SELFDESTRUCT, INVALID: 検証中に破壊的なオプコードは許可されません。 GAS:CALL、DELEGATECALL、STATICCALL、またはCALLCODE(他の契約へのガス配送のための)の *CALL指示に直ちに従う場合にのみ許可されます。 CREATE: 一般的に禁止されているのは、予測できないアドレスで契約を作成するためです. しかし、CREATE は未開封の工場を使用する場合に発送者契約のために許可されます. CREATE2 は発送者契約の展開中に正確に一度許可されます. あなたのアカウントが禁止されたオプコードを使用する場合に起こることは以下の通りです。 // This validateUserOp will be rejected by bundlers function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 validationData) { // BANNED: block.timestamp can change require(block.timestamp < deadline, "Expired"); // ... rest of validation } バンドラーはシミュレーション中にトラックを実行します(使用) ERC-7562 コンプライアンスのトラッカー) で、禁止されたオプコードに触れるユーザーOp を拒否します。現代のバンドラーは、JavaScript トラッカーまたは EntryPoint v0.8 で導入されたネイティブ Go 実装を使用することができます。 debug_traceCall ストレージアクセス規則 オプコードを超えて、バンドラーは、どのストレージスロットの検証コードが読み書きできるかを制限します。 can only access: Unstaked entities 自分のストレージ(アカウントのストレージ) Storage slots that are "associated" with the account address A, defined as: the slot value equals A, OR was calculated as where x is bytes32 and n is in range 0 to 128 keccak256(A || x) + n In practice, this means your account can read/write to mappings where the account address is the key. For example, an ERC-20 token stores balances in a mapping like 実際の倉庫は、 あなたのアカウントアドレスがキーである場合、そのスロットはあなたと「関連」されています。 offset (0-128) allows access to struct members stored after the mapping entry—useful when the mapping value is a struct. mapping(address => uint256) balances keccak256(address || slot_of_mapping) +n (アカウント、支払いマスター、またはEntryPointに投資した工場) は、より多くの自由を得ます。 Staked entities Can access any slot in their own storage (STO-031) UserOp(STO-032)のいずれかのエンティティの関連ストレージにアクセスできます。 非団体契約におけるストレージへの読み込みのみのアクセスを有することができる(STO-033) 完全なストレージアクセスのルールを参照 . エルフ7562 Why do these rules exist? Consider two UserOperations that read from the same storage slot during validation. If the first operation mutates that slot, the second operation’s validation assumptions may no longer hold. By restricting storage access during validation, bundlers can safely include multiple UserOperations in the same bundle without risking cross-operation interference or non-deterministic failures. 評判制度 認証規則でさえ、エンティティは悪い行動をとることがあります。アカウントはシミュレーションを通過するユーザーオプションを継続的に提出する可能性がありますが、微妙な状態の変更によりチェーン上で失敗する可能性があります。 Bundlers tracks these entities with a 各エンティティ(アカウントアドレス、paymasterアドレス、工場アドレス)に対して、バンドラーは次のように追跡します。 reputation system opsSeen: このエンティティに関わる UserOps がバンドラーがどれだけ見たか : How many of those actually got included on-chain opsIncluded The reputation status is determined by slack-based thresholds (defined in ) : エルフ7562 maxSeen = opsSeen / MIN_INCLUSION_RATE_DENOMINATOR (10 for bundlers) status = BANNED if maxSeen > opsIncluded + BAN_SLACK (50) status = THROTTLED if maxSeen > opsIncluded + THROTTLING_SLACK (10) status = OK otherwise 「スラック」値は、正常な操作変数から偽ポジティブを防ぐ寛容バッファです。 つまり、あるエンティティは、投与される前に、実際のインクルージョンよりも予想されるインクルージョンが10倍以上あることを意味します。 provides an even larger buffer before permanent banning. This design acknowledges that some UserOps legitimately fail (network conditions, race conditions) without indicating malicious behavior. THROTTLING_SLACK = 10 BAN_SLACK = 50 ある entity が , the bundler limits how many UserOps from that entity can be in the mempool. このバンドラーは、そのエンティティからどれだけの UserOps が mempoolに含まれるかを制限します。 , all UserOps involving that entity are rejected immediately. throttled banned ストライキ Entities can improve their standing by staking ETH in the EntryPoint contract: entryPoint.addStake{ value: 1 ether }(unstakeDelaySec); 賭けはカットされません - それは単に閉じ込められていますが、それはコミットメントと助成を示しています: リラックスしたストレージアクセスのルール(上記のように) ・MEPOOL LIMIT より軽い評判の限界 paymastersと工場、特に、ストックは、生産の使用のためにほとんど義務付けられています。それなしでは、単一の失敗したUserOpは、迅速にエンティティティを投げ込むことができます。 (chain-specific, typically 1 ETH or equivalent) of 86400 seconds (1 day). The exact stake amount varies by chain and is defined in the mempool metadata—check the bundler documentation for your target network. MIN_STAKE_VALUE MIN_UNSTAKE_DELAY ガス経済 彼らはバンドルを提出するためのガスコストを支払い、彼らが含むUserOpsから補償されます。 The Bundlerの視点 Revenue = Σ (each UserOp's payment to beneficiary) Cost = Gas used × Gas price paid for handleOps tx Profit = Revenue - Cost 各 UserOp は、ガスの使用量と指定したガス価格に基づいて支払います。 Payment = (actualGasUsed + preVerificationGas) × min(maxFeePerGas, baseFee + maxPriorityFeePerGas) The bundler sets the address in the call to receive these payments. beneficiary handleOps PreVerificationガス説明 The フィールドは、直接測定できないコストをカバーします: preVerificationGas : 16 gas per non-zero byte, 4 gas per zero byte Calldata cost バンドルオーバーヘッド:HandOpsコールあたりの固定コストは、UserOpsを通じて削減されます。 L2 データ料金: Optimism や Arbitrum などの L2 では、L1 に通話データを送信するには追加のコストが発生します。 ガスを推定する際に、バンドラーは UserOp のサイズに基づいて preVerificationGas を計算します。 // Simplified preVerificationGas calculation const calldataCost = userOpBytes.reduce((sum, byte) => sum + (byte === 0 ? 4n : 16n), 0n ); const overhead = 38000n; // ~21000 tx base + ~10000 bundle overhead + ~7000 per-op preVerificationGas = calldataCost + overhead + l2DataFee; Overhead values vary by bundler. For reference, Alto uses , で、 . Always use rather than hardcoding. transactionGasStipend: 21000 fixedGasOverhead: 9830 perUserOp: 7260 eth_estimateUserOperationGas The Unused Gas Penalty ユーザーがガス(ブロックスペースを無駄にしている)の過払いを防止するために、EntryPointは未使用の実行ガスに罰金を課します。 (アカウントの実行)および (PayMasterの手続きについて): callGasLimit paymasterPostOpGasLimit どちらのフィールドでも未使用のガスがPENALTY_GAS_THRESHOLD(40,000)を超える場合、アカウントは未使用の金額の10%(UNUSED_GAS_PENALTY_PERCENT)を支払います(検証GasLimitまたはpreVerificationGasに適用されません) これは「安全のためだけ」に荒々しく高い執行限界を設定することを妨げます。 ガス評価エラーが発生した場合、制限が合理的かどうかを確認してください。 エンドポイントは合理的な欠陥を生み出す。 eth_estimateUserOperationGas 一般的なエラーとデバッグ バンドラーは、操作が失敗した理由を正確に説明する構造化エラーコードを返しますが、それらを読み取る方法を知っている場合にのみ。 AA1x:工場エラー These occur when deploying a new account via the factory. In EntryPoint v0.8, you specify and 別々のフィールドとして(EntryPoint はそれらをパッケージします。 内部): factory factoryData initCode AA10: "sender already constructed" - 送信アドレスにはすでにコードが展開されている。 AA13: "initCode failed or OOG" - 工場のCreateAccount 呼び出しが失敗またはガスが切れた。 AA14: "initCode must return sender" - 工場は予想以上に異なるアドレスを返しました。 AA15: "initCode must create sender" - 工場通話が完了しましたが、送信アドレスにコードを展開しませんでした。 : Check that your factory's function returns the expected address. Verify the factory is deployed and funded. Debugging createAccount AA2x: Account Validation Errors 最も一般的なカテゴリ: AA20: "account not deployed" - 送信アドレスにはコードがなく、 initCode が提供されていません。 : "didn't pay prefund" - The account doesn't have enough ETH to cover the maximum possible gas cost. Fund the account or use a paymaster. AA21 AA22: "expired or not due" - The UserOp has a validUntil timestamp that has passed, or a validAfter timestamp that has not arrived yet. ユーザオプは、まだ到着していない有効なUntil timestampを有効にしました。 AA23: "reverted" - アカウントの validateUserOp 関数は reverted. Check your signature validation logic. : "signature error" - The returned validation data indicates an invalid signature. AA24 AA25: "invalid account nonce" - The nonce does not match. The nonce in ERC-4337 is a 256-bit value with two parts: nonce = (key << 64) Iain sequence. The key (upper 192 bits) identifies the "lane"—you can have multiple parallel UserOps with different keys. The sequence (lower 64 bits) must increment sequentially within each lane. 一般的な原因: Reusing a nonce that was already included Using the wrong nonce key Another UserOp with the same sender is pending in the mempool AA26: 「over verificationGasLimit」 - アカウント認証で割り当てられたガスよりも多くのガスを使用しました。 AA3x:Paymasterエラー : "paymaster not deployed" - The paymaster address has no code deployed. AA30 : "paymaster deposit too low" - The paymaster's deposit in the EntryPoint can't cover the gas cost. Top it up: AA31 entryPoint.depositTo{ value: 1 ether }(paymasterAddress); AA32: "paymaster expires or not due" - AA22と同様ですが、paymasterの検証データです。 : "paymaster reverted" - The paymaster's function reverted. AA33 validatePaymasterUserOp : "paymaster signature error" - Bad signature in the paymaster data. AA34 AA36: "over paymasterVerificationGasLimit" - Paymaster 認証で割り当てられたガスよりも多くのガスを使用しました。 バンドラーとの協力 RPC方法 各ERC-4337バンドラーは、以下の標準手法を実装しています。 : Submit a UserOp for inclusion を参照 eth_sendUserOperation const userOpHash = await bundler.request({ method: 'eth_sendUserOperation', params: [userOp, entryPointAddress] }); : Get gas limit estimates eth_estimateUserOperationGas const gasEstimate = await bundler.request({ method: 'eth_estimateUserOperationGas', params: [userOp, entryPointAddress] }); // Returns: { preVerificationGas, verificationGasLimit, callGasLimit, ... } : Look up a UserOp by its hash eth_getUserOperationByHash const userOp = await bundler.request({ method: 'eth_getUserOperationByHash', params: [userOpHash] }); : Get the receipt after inclusion eth_getUserOperationReceipt const receipt = await bundler.request({ method: 'eth_getUserOperationReceipt', params: [userOpHash] }); // Returns: { success, actualGasUsed, receipt: { transactionHash, ... } } : Discover which EntryPoint versions the bundler supports eth_supportedEntryPoints const entryPoints = await bundler.request({ method: 'eth_supportedEntryPoints', params: [] }); // Returns: ['0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108'] 『The Shared Mempool』 当初、各バンドラーは独自のプライベートメプポールを維持していました。 : A single bundler could refuse to include certain UserOps Censorship risk : Users had to know which bundlers to submit to Fragmentation : If your bundler went down, your UserOps were stuck Single points of failure 解決策は、The , バンドルが互いに UserOps を噂する P2P ネットワーク. It works similarly to how Ethereum nodes gossip transactions: ERC-4337 shared mempool User submits UserOp to any participating bundler Bundler validates and adds to the local mempool Bundler broadcasts to connected peers Any bundler on the network can include the UserOp. The protocol uses libp2p for networking. Bundlers advertise which mempools they support (identified by IPFS CIDs that reference mempool metadata files), and only propagate UserOps that pass validation. For example, a mempool metadata file looks like: chainId: '1' entryPointContract: '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108' description: Canonical ERC-4337 mempool for Ethereum Mainnet minimumStake: '1000000000000000000' このファイルの IPFS CID は、P2P トピック名で使用されるメプポール識別子になります。メプポールメタデータは、検証ルールを定義します:どのオプコードが禁止されているか、ストレージアクセスのパターン、ガス制限、評判の限界です。バンドラーがP2P スピーチを通じて UserOp を受け取ると、ローカルメプポールに追加する前に、独自のルールに反して再検証します。 Advanced テーマ アグレクター Signature verification is expensive on-chain. The ecrecover precompile costs 3,000 gas per call, but smart account signature verification typically costs more due to additional validation logic—often 6,000-10,000 gas total. For 100 UserOps in a bundle, that's 600,000+ gas just for signatures. Aggregators enable batch signature verification—verify all 100 signatures in a single operation for a fraction of the cost. What problem do aggregators solve? 各アカウントが独自の署名を検証する代わりに、アカウントは、集計契約に委任することができる。 How it works: アカウントのvalidateUserOp は、その検証データに集計者アドレスを返します。 Bundler groups all UserOps using the same aggregator Bundler calls once for the group aggregator.validateSignatures(userOps, aggregatedSignature) If verification passes, all UserOps in that group are considered valid : The return value from packs three pieces of information into a single 256-bit value: The validationData encoding validateUserOp validationData = uint160(aggregator) | // bits 0-159: aggregator address (uint256(validUntil) << 160) | // bits 160-207: expiration timestamp (uint256(validAfter) << 208) // bits 208-255: earliest valid time (bits 0-159): Address of the aggregator contract, or special values: 0 = signature valid, 1 = signature invalid aggregator validUntil (bits 160-207): Timestamp after which this UserOp expires (0 = no expiry) (bits 208-255): Timestamp before which this UserOp is not valid (0 = immediately valid) validAfter This encoding lets accounts specify both signature verification delegation and time-bounded validity in a single return value. ペイマスター Paymasters abstract gas payment from users. Instead of the account paying for gas, a paymaster can: : Pay on behalf of users (gasless UX) Sponsor transactions : Let users pay in stablecoins or other tokens Accept ERC-20 tokens : Rate limiting, subscription models, etc. Implement custom logic The paymaster's validation flow runs during the validation phase: function validatePaymasterUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost ) external returns (bytes memory context, uint256 validationData); THE returned here is passed to after execution completes, allowing the paymaster to perform final accounting (like charging an ERC-20 token): context postOp function postOp( PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas ) external; ストライキはリラックスされたストレージアクセスのルールとより良い評判を提供します - unstaked paymasters face strict limitations and can be quickly throttled by bundlers. While unstaked paymasters technically function with basic operations, staking is practically required for any serious paymaster implementation. can 地元テスト このセクションでは、EntryPoint v0.8 で Anvil を実行していると仮定します。 , Pimlico's TypeScript bundler, and , a viem-based library for ERC-4337 interactions. Alto オリジナル.js SimpleAccountFactory Part 1では、最低限のスマートアカウントを構築しましたが、ユーザーはどのようにそれを展開しますか? 彼らは通常の取引を送信できません - 彼らはまだガス用ETHを持っていません。 のために 参照実施には、 以下の例を実行する前にEntryPointと一緒に展開します。 SimpleAccount SimpleAccountFactory Account Deployment via UserOp When the EntryPoint receives a UserOp with factory and factoryData fields: Checks if has code—if yes, skip deployment sender calls factory.createAccount(owner, salt) via the factoryData Verifies the deployed address matches sender Continues with validation on the newly-deployed account 走り高く alto \ --rpc-url http://localhost:8545 \ --entrypoints 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 \ --executor-private-keys 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d \ --utility-private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ --safe-mode false \ --api-version v1,v2 \ --bundle-mode auto 主な旗: : Key for submitting bundles (must have ETH) --executor-private-keys --safe-mode false: Anvil が完全な ERC-7562 認証のための JavaScript トラッカーを欠いている --api-version v1,v2: 両方の UserOp フォーマットを受け入れる (v1 for 0.6, v2 for 0.7/0.8) Sending UserOperations with permissionless.js Install dependencies: npm install viem permissionless Step 1: Set up clients We need three clients: one for reading chain state, one for bundler-specific RPCs, and one for the smart account owner. import { http, createPublicClient, createWalletClient, parseEther } from "viem" import { privateKeyToAccount } from "viem/accounts" import { foundry } from "viem/chains" import { toSimpleSmartAccount } from "permissionless/accounts" import { createSmartAccountClient } from "permissionless/clients" import { createPimlicoClient } from "permissionless/clients/pimlico" const ENTRYPOINT = "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108" const publicClient = createPublicClient({ chain: foundry, transport: http("http://localhost:8545") }) const pimlicoClient = createPimlicoClient({ chain: foundry, transport: http("http://localhost:4337"), entryPoint: { address: ENTRYPOINT, version: "0.8" } }) const owner = privateKeyToAccount(process.env.PRIVATE_KEY) THE connects to Alto's RPC and provides gas estimation via . pimlicoClient pimlico_getUserOperationGasPrice Step 2: Create the smart account instance const simpleAccount = await toSimpleSmartAccount({ client: publicClient, owner, entryPoint: { address: ENTRYPOINT, version: "0.8" } }) const accountAddress = await simpleAccount.getAddress() console.log("Account:", accountAddress) これは、工場の反事実アドレスを使用して計算します。 function. The account doesn't exist yet—but we know exactly where it will be deployed. getAddress Step 3: Fund the account The smart account needs ETH to pay for gas (or use a paymaster). We can send ETH to the counterfactual address: const walletClient = createWalletClient({ account: owner, chain: foundry, transport: http("http://localhost:8545") }) await walletClient.sendTransaction({ to: accountAddress, value: parseEther("1") }) ETHはそのアドレスに座っており、アカウントが展開されるとすぐにその資金にアクセスできます。 Step 4: Create the smart account client const smartAccountClient = createSmartAccountClient({ client: publicClient, account: simpleAccount, bundlerTransport: http("http://localhost:4337"), userOperation: { estimateFeesPerGas: async () => (await pimlicoClient.getUserOperationGasPrice()).fast } }) THE handles UserOp construction, nonce management, gas estimation, and signing. The コールバックはバンドラーから現在のガス価格を取得します。 smartAccountClient estimateFeesPerGas Step 5: Send a UserOperation const hash = await smartAccountClient.sendUserOperation({ calls: [{ to: "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720", value: parseEther("0.01"), data: "0x" }] }) const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash }) console.log("Success:", receipt.success) 最初の UserOp の場合、SDK は自動的に and fields. The EntryPoint deploys the account, then executes the transfer—all in one transaction. factory factoryData What We've Learned バンドラーはERC-4337の実行層です。それらは、アカウント抽象を仕様から生産準備のメカニズムに変えるものです。 Understanding their constraints — validation rules, gas economics, and reputation mechanics — is critical when designing reliable smart accounts. Mistakes here don’t surface in Solidity code, but in mempool behavior, simulations, and execution economics. ERC-4337は、複雑さをプロトコルからインフラストラクチャに移し、開発者が取引ではなくバンドラーで考え始めるほど、彼らのシステムは生産においてより強力になります。