Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Jan 30, 2026 - 日記

EIP-7702時代のAccount Abstraction: 4337が必要なケース

English version

EIP-7702時代のAccount Abstraction: 4337が必要なケース cover image

Ethereum でdApp(分散型アプリケーション)を作ると、ユーザーに「ウォレットを用意して」「ETHを買って」「ガス代を払って」とお願いすることになります。これが普及の壁です。

Account Abstraction(アカウント抽象化)は、この壁を崩すための仕組みです。EIP-4337EIP-7702 という2つの仕様があります。

結論から言うと、EIP-7702の登場で、4337の出番は大幅に減りました。単一ユーザーのバッチ実行や権限管理は7702だけで実現できます。4337が必要なのは「パーミッションレスなガススポンサー」と「複数ユーザー集約」の2つのケースに限られます。

本記事では、まず「なぜAccount Abstractionが必要か」を整理し、その上で7702と4337の使い分けを具体的に示します。

そもそもAccount Abstractionとは何か

従来のEOA(普通のウォレット)の問題

Ethereumには2種類のアカウントがあります:

  1. EOA(Externally Owned Account): 秘密鍵で操作する普通のウォレット(MetaMaskなど)
  2. コントラクトアカウント: コードで動くスマートコントラクト

ほとんどのユーザーはEOAを使いますが、EOAには厳しい制約があります:

制約具体的な問題
秘密鍵が全て秘密鍵を失くしたら資産は永久に失われる。リカバリー手段がない
ガス代は自分持ちETHを持っていないと何もできない。新規ユーザーは「まずETHを買え」と言われる
1トランザクション1操作approve→swapのような2段階操作は2回署名が必要。UXが悪い
署名方式が固定ECDSA一択。生体認証やマルチシグを使いたくても無理

Account Abstractionが解決すること

Account Abstractionは、「アカウントの振る舞いをプログラム可能にする」ことでこれらを解決します:

解決策できるようになること
リカバリー信頼できる友人やサービスを使って、秘密鍵なしでアカウントを復旧
ガススポンサーdApp運営者がガス代を肩代わり。ユーザーはETHなしで操作可能
バッチ実行複数操作を1トランザクションにまとめて、署名1回で完了
柔軟な署名パスキー(指紋/顔認証)、マルチシグ、セッションキーなど自由に選べる

要するに、「EOAの不便さをコントラクトの柔軟さで補う」のがAccount Abstractionです。

2つのアプローチ:EIP-4337とEIP-7702

Account Abstractionを実現する方法は1つではありません。EIP-4337とEIP-7702は、異なるアプローチで同じゴールを目指しています。

EIP-4337: コントラクト+周辺インフラの解決策

EIP-4337は、チェーンのトランザクション形式を増やさずに、Account Abstractionを実現します。仕組みはこうです:

  • EntryPoint(実行ゲートウェイ): UserOperationを検証・実行するスマートコントラクト。代表的な実装はeth-infinitism/account-abstraction
  • Bundler(束ね役): UserOperationを集約してEntryPointに送信するオフチェーンサービス
  • Paymaster(スポンサー): ガス代(手数料)を肩代わりするスマートコントラクト

ユーザーは従来のトランザクションではなく、UserOperationという疑似トランザクションを作成します。Bundlerがこれを集約し、EntryPointに送信することで、ガススポンサーシップやバッチ実行が可能になります。

UserOperationの構造

UserOperationは、従来のトランザクションを拡張した構造体です。主要なフィールドを見てみます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct UserOperation {
    address sender;           // スマートアカウントのアドレス
    uint256 nonce;            // リプレイ攻撃(同じ署名の使い回し)防止用のナンス
    bytes initCode;           // アカウント未作成時のデプロイコード
    bytes callData;           // 実行したい操作のデータ
    uint256 callGasLimit;     // 実行フェーズのガス上限
    uint256 verificationGasLimit; // 検証フェーズのガス上限
    uint256 preVerificationGas;   // Bundlerへの補償ガス
    uint256 maxFeePerGas;     // EIP-1559のmaxFeePerGas
    uint256 maxPriorityFeePerGas; // EIP-1559のmaxPriorityFeePerGas
    bytes paymasterAndData;   // Paymaster情報(オプション)
    bytes signature;          // 署名
}

重要なフィールドの役割:

  • initCode: アカウントが存在しない場合、このコードでデプロイします。既存アカウントなら空にします
  • callData: execute(address dest, uint256 value, bytes data) のようなスマートアカウントの関数呼び出しをエンコードしたもの
  • paymasterAndData: 最初の20バイトがPaymasterアドレス、残りがPaymasterに渡すデータ。空ならユーザー自身がガス代を払います
  • preVerificationGas: calldata のサイズに応じたBundlerへの補償。Bundlerがトランザクションを送信するコストをカバーします

実行フロー

EntryPointのhandleOps関数が呼ばれると、以下の順序で処理されます:

sequenceDiagram
    participant B as Bundler
    participant E as EntryPoint
    participant A as SmartAccount
    participant P as Paymaster
    participant T as Target Contract

    B->>E: handleOps(userOps)
    loop 各UserOperation
        E->>A: validateUserOp(userOp)
        A-->>E: 検証結果
        opt Paymaster使用時
            E->>P: validatePaymasterUserOp(userOp)
            P-->>E: 検証結果
        end
        E->>A: callData実行
        A->>T: 実際の操作
        T-->>A: 結果
        opt Paymaster使用時
            E->>P: postOp(実行結果)
        end
    end
    E-->>B: 完了
  1. 検証フェーズ: スマートアカウントのvalidateUserOpで署名を検証
  2. Paymaster検証: Paymasterがある場合、validatePaymasterUserOpでスポンサー条件を確認
  3. 実行フェーズ: callDataに基づいてスマートアカウントが操作を実行
  4. 後処理: PaymasterのpostOpで課金処理など

ユーザーの操作はシンプルにできますが、その裏側でBundler/Paymasterを運用する「提供側の複雑さ」が発生します。

EIP-7702: トランザクション形式の解決策

EIP-7702は、新しいトランザクションタイプ(Type 4)を導入します。これにより、EOA(authority)宛の呼び出しが、別アドレスに置かれたコードへ「委任」され、EOAの状態(残高・ストレージ)を使って実行されるようになります。

この「委任」は、EIP本文ではDelegation Indicator(委任マーカー)と呼ばれる値(0xef0100 + delegate address)をauthorityのaccount codeに書き込むことで実現します。本記事ではこの値を「委任マーカー」と呼びます。

登場人物の整理

EIP-7702では、以下の3つの役割を区別することが重要です:

役割説明
tx.origin(送信者)Type 4トランザクションをネットワークに送信する人。ガス代を払う
authority(委任元EOA)authorization_listに署名するEOA。自分のアカウントに委任マーカーを設定される
delegate(委任先コントラクト)実際のコードが置かれているアドレス。authorityのコンテキストで実行される

重要: tx.originとauthorityは同一でも別人でも構いません。スポンサーがtx.originとなり、別のEOAのauthority委任を設定することも可能です。

委任の仕組み

graph TB
    subgraph "1. 委任マーカーの設定"
        Sender[tx.origin
送信者] -->|Type 4 Tx送信| EVM[EVM] EVM -->|authorization_list検証| Auth[authorityの署名検証] Auth -->|委任マーカーを書き込み| EOA_Code["authorityのcode領域
0xef0100 + delegate address"] end
graph TB
    subgraph "2. 委任先コードの実行"
        Caller[任意の呼び出し元] -->|"CALL(authority)"| EOA[authority EOA]
        EOA -->|委任マーカーを検出| Lookup[delegateのコードを参照]
        Lookup -->|コードをロード| Delegate[delegate contract
のバイトコード] Delegate -->|authorityのコンテキストで実行| Exec["実行環境:
address(this) = authority
storage = authorityのもの
balance = authorityのもの"] end

具体例で理解する

Alice(EOA)が、BatchExecutorコントラクトのコードを「借りて」複数操作を1トランザクションで実行する例です:

sequenceDiagram
    participant Alice as Alice (authority)
0xAlice... participant Bob as Bob (tx.origin)
0xBob... participant EVM as EVM participant Batch as BatchExecutor
0xBatch... participant DEX as DEX Contract Note over Alice,Bob: 1. Aliceが委任に署名(オフチェーン) Alice->>Bob: authorization署名を渡す Note over Bob,EVM: 2. BobがType 4 Txを送信 Bob->>EVM: Type 4 Tx
to=Alice, data=executeBatch(...), authorization_list EVM->>EVM: authorization_listを処理(Alice署名を検証) Note right of EVM: 状態更新: Aliceのcodeに委任マーカーを設定 Note right of EVM: code = 0xef0100 + 0xBatch... Note over EVM,DEX: 3. Aliceへの呼び出しが委任実行される Note over EVM: トランザクションの宛先はAlice(to=Alice) EVM->>Batch: delegateのコードをロード Note right of Batch: 実行コンテキストはAlice Note right of Batch: address(this)=Alice, msg.sender=Bob Batch->>DEX: swap() [Aliceとして] DEX-->>Batch: 結果

この例のポイント:

  • Bobがガス代を払う(tx.origin)
  • Aliceのアカウントに委任マーカーが設定される(authority)
  • BatchExecutorのコードが使われる(delegate)
  • 実行時のaddress(this)Alicemsg.senderBob

7702時代に4337が必要なケース

EIP-7702の登場で、4337の出番は大幅に減りました。単一ユーザーのバッチ実行、権限委譲、セッションキーなどは7702だけで実現できます。

では4337は不要になったのか? 2つの具体的なケースで、まだ4337が必要です。

ケース1: パーミッションレスなガススポンサー

ガススポンサー自体は7702でも実現できます。運営者がtx.originとしてトランザクションを送信し、運営者が管理するdelegateコントラクトを使えば、revertリスクも事前検証で管理できます。

4337が必要になるのは「パーミッションレス」なスポンサーを作る場合です。

Open Paymaster のような「誰でも使えるPaymaster」をDeFiとして運用する場合、7702では成り立ちません。

7702の問題:

1. 不特定多数がスポンサーに「実行してほしい操作」を投げられる
2. 悪意あるユーザーがrevertするトランザクションを送信
3. スポンサーはガス代だけ取られ、操作は失敗
4. これを繰り返されると破綻

事前にシミュレーション(eth_call)はできますが、送信までの間に状態が変わる可能性があります。パーミッションレスな環境では、悪意あるユーザーを排除できません。

4337の解決策: 検証フェーズと実行フェーズが分離しています。

1
2
3
4
5
6
7
8
9
contract OpenPaymaster {
    function validatePaymasterUserOp(UserOperation calldata userOp, ...)
        external returns (bytes memory context, uint256 validationData) {
        // 検証に失敗 → UserOperationは実行されない → ガス損失なし
        // ここで「このUserOperationはスポンサーすべきか」を判断
        require(hasEnoughTokenBalance(userOp.sender), "insufficient balance");
        return ("", 0);
    }
}

Bundlerが事前にUserOperationをシミュレーションし、検証に通るものだけをEntryPointに送ります。検証段階で弾けるものはガス損失なしで弾けるため、パーミッションレスでも運用が成り立ちます。

Open Paymasterの設計(リバランサーのインセンティブで持続性を確保する仕組み)については「Open Paymaster: 誰も運営しないのに回るリバランス設計 」を参照してください。

ケース2: 複数ユーザーの操作を1トランザクションに集約

4337の場合:

1
2
3
4
5
6
7
// Bundlerが1回のtxで3人のUserOperationを実行
entryPoint.handleOps([
    userOpAlice,  // AliceのSwap
    userOpBob,    // BobのTransfer  
    userOpCharlie // CharlieのMint
], beneficiary);
// → 1トランザクション、1回のbase fee(21000 gas)で3人分を処理

7702の場合:

// Aliceの操作 → 1 tx(21000 base fee)
// Bobの操作 → 1 tx(21000 base fee)
// Charlieの操作 → 1 tx(21000 base fee)
// → 3トランザクション、3回のbase fee

7702のauthorization_listには複数のauthorityを入れられますが、それは「複数EOAに委任マーカーを設定する」だけです。設定後の実行は各authorityへの個別CALLが必要で、1 txにまとめるには「全員の署名を集めて呼ぶコントラクト」を自前で作ることになります。つまり4337のEntryPoint相当を再発明することになります。

このケースが重要な場面:

  • 高頻度取引プラットフォーム(多数ユーザーの注文を効率的に処理)
  • ガス代の「まとめ買い」でコスト削減したいリレイヤー運用

レアケース: 7702非対応チェーン

2025年現在、主要チェーン(Ethereum mainnet、Arbitrum、Optimism、Base、Polygon等)はEIP-7702に対応済みです。ただし、一部のEVM互換チェーンやプライベートチェーンでは未対応の場合があります。

4337はEntryPointコントラクトをデプロイすれば動くため、7702非対応チェーンでも同じ設計を持ち込めます。マルチチェーン展開で「全チェーンで同じコードベース」を維持したい場合は、4337の方が安全な選択です。

比較まとめ

ユースケース77024337
単一ユーザーのバッチ実行
運営者によるガススポンサー
パーミッションレスなスポンサー
複数ユーザー集約△ 自前実装◎ Bundler標準
既存EOAの継続利用△ 新アドレス
ガスオーバーヘッド◎ 小さい△ EntryPoint経由
7702非対応チェーン

コード例(最小コンパイル): 4337 vs 7702

比較のために、4337側(スマートアカウント)と7702側(delegate)の最小サンプルを分けて置いておきます。

4337: 最小のスマートアカウント

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

struct UserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    uint256 callGasLimit;
    uint256 verificationGasLimit;
    uint256 preVerificationGas;
    uint256 maxFeePerGas;
    uint256 maxPriorityFeePerGas;
    bytes paymasterAndData;
    bytes signature;
}

interface IEntryPoint {
    function handleOps(UserOperation[] calldata ops, address payable beneficiary) external;
}

contract MinimalSmartAccount {
    IEntryPoint public immutable entryPoint;
    address public owner;

    constructor(IEntryPoint _entryPoint, address _owner) {
        entryPoint = _entryPoint;
        owner = _owner;
    }

    // EntryPointからのみ呼ばれる想定(実物はEIP/実装に合わせて調整)
    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 /* missingAccountFunds */
    ) external returns (uint256 validationData) {
        require(msg.sender == address(entryPoint), "only EntryPoint");
        require(userOp.sender == address(this), "bad sender");
        require(_recover(userOpHash, userOp.signature) == owner, "bad signature");
        return 0;
    }

    function execute(address dest, uint256 value, bytes calldata data) external {
        require(msg.sender == address(entryPoint), "only EntryPoint");
        (bool ok,) = dest.call{value: value}(data);
        require(ok, "call failed");
    }

    function _recover(bytes32 hash, bytes calldata sig) internal pure returns (address) {
        require(sig.length == 65, "bad sig len");
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            r := calldataload(sig.offset)
            s := calldataload(add(sig.offset, 32))
            v := byte(0, calldataload(add(sig.offset, 64)))
        }
        if (v < 27) v += 27;
        require(v == 27 || v == 28, "bad v");
        return ecrecover(hash, v, r, s);
    }
}

7702: delegate側コード(authorityコンテキストで実行)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// ポイントは「tx.originで縛らず、authority署名で縛る」ことです。
contract BatchExecutor {
    struct Call {
        address to;
        uint256 value;
        bytes data;
    }

    uint256 public nonce;

    function executeBatch(Call[] calldata calls, uint256 expectedNonce, bytes calldata sig) external {
        require(expectedNonce == nonce, "bad nonce");

        // authorityの署名で縛る(delegated executionでは address(this) == authority)
        bytes32 digest = keccak256(
            abi.encode(
                block.chainid,
                address(this),
                msg.sender,
                expectedNonce,
                _hashCalls(calls)
            )
        );
        require(_recover(digest, sig) == address(this), "bad authority sig");
        nonce = expectedNonce + 1;

        for (uint256 i = 0; i < calls.length; i++) {
            (bool ok,) = calls[i].to.call{value: calls[i].value}(calls[i].data);
            require(ok, "call failed");
        }
    }

    function _hashCalls(Call[] calldata calls) internal pure returns (bytes32) {
        bytes32 h = keccak256("");
        for (uint256 i = 0; i < calls.length; i++) {
            h = keccak256(abi.encode(h, calls[i].to, calls[i].value, keccak256(calls[i].data)));
        }
        return h;
    }

    function _recover(bytes32 hash, bytes calldata sig) internal pure returns (address) {
        require(sig.length == 65, "bad sig len");
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            r := calldataload(sig.offset)
            s := calldataload(add(sig.offset, 32))
            v := byte(0, calldataload(add(sig.offset, 64)))
        }
        if (v < 27) v += 27;
        require(v == 27 || v == 28, "bad v");
        return ecrecover(hash, v, r, s);
    }
}

結論: ほとんどのdAppは7702だけで十分

ガスレス体験も7702で実現できます。運営者がtx.originとしてトランザクションを送信し、運営者が提供するdelegateコントラクトを使えば、revertリスクも管理できます。

4337が必要になるのは以下のケースだけです:

  1. パーミッションレスなスポンサー: 不特定多数が使う公開型Paymaster(Open Paymaster のような)では、検証フェーズで弾く仕組みが必須
  2. 複数ユーザーの集約: リレイヤーとして多数ユーザーのトランザクションを1つにまとめたい場合

ガスコストの見積もり方(実務)

「4337と7702、どっちが安い?」はケース依存です。なので、最初から固定の数値を置くより、まずは見積もりルートを用意するのが安全です。

7702(通常トランザクションとして見積もる)

7702は基本的に通常トランザクションの延長なので、まずはeth_estimateGasで見積もります。

1
2
3
4
# 例: JSON-RPCでgas見積もり(RPC_URLは自分のノード/プロバイダに置き換え)
curl -s "$RPC_URL" \
  -H 'content-type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"eth_estimateGas","params":[{"from":"0x...","to":"0x...","data":"0x..."}]}'

4337(Bundler RPCで見積もる)

4337はUserOperationの検証/実行コストが絡むので、BundlerのRPCで見積もるのが現実的です。

1
2
3
4
# 例: Bundlerの eth_estimateUserOperationGas を叩く(BUNDLER_RPCはBundlerのRPC)
curl -s "$BUNDLER_RPC" \
  -H 'content-type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"eth_estimateUserOperationGas","params":[{"sender":"0x...","nonce":"0x0","initCode":"0x","callData":"0x...","callGasLimit":"0x0","verificationGasLimit":"0x0","preVerificationGas":"0x0","maxFeePerGas":"0x...","maxPriorityFeePerGas":"0x...","paymasterAndData":"0x","signature":"0x..."},"0xEntryPoint..."]}'

この見積もりで出た値を元に、操作の種類ごとに「スポンサーあり/なし」「高頻度/低頻度」で分岐させると、実装と運用が安定します。

セキュリティ考慮事項

EIP-4337

ユーザーにとってのリスク: Bundlerの順序操作

Bundlerが悪意を持つ場合、トランザクションの順序を操作できます(MEV抽出)。ただし資金を直接盗むことはできません。対策として、複数のBundlerを使用するか、レピュテーションのあるBundlerを選びます。

Paymaster運営者/LPにとってのリスク: ガス吸い取り

検証ロジックが甘いと、悪意あるユーザーにガスを吸い取られます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 危険: 検証なしで誰でもスポンサー
contract UnsafePaymaster {
    function validatePaymasterUserOp(UserOperation calldata userOp, ...)
        external returns (bytes memory, uint256) {
        return ("", 0); // 無条件で許可 → ガス吸い取り放題
    }
}

// 安全: 上限設定
contract SafePaymaster {
    mapping(address => uint256) public dailyLimit;
    mapping(address => uint256) public dailySpent;
    
    function validatePaymasterUserOp(UserOperation calldata userOp, ..., uint256 maxCost)
        external returns (bytes memory, uint256) {
        require(dailySpent[userOp.sender] + maxCost <= dailyLimit[userOp.sender], "limit");
        dailySpent[userOp.sender] += maxCost;
        return ("", 0);
    }
}

EIP-7702

ユーザーにとってのリスク: 悪意ある委任先

委任先コントラクトはEOAの全権限(残高・ストレージ)で実行されます。悪意あるコードや脆弱性があると、資金を全て失います。信頼できる監査済みのdelegateのみを使うこと。

まとめ: 7702がデフォルト、4337は特定用途

EIP-7702の登場で、Account Abstractionの実装方針は明確になりました。

基本方針:

  • デフォルトは7702: 単一ユーザーのバッチ実行、権限管理、ガススポンサー(運営者による)は7702で十分
  • 4337は2つの用途に限定: パーミッションレスなスポンサー、複数ユーザー集約

4337が「obsolete」かと言われると、上記2つの用途がある限りは生き残ります。ただし、「とりあえず4337」という時代は終わり、7702で済むなら7702を選ぶのが合理的です。

個人的には、7702の登場でAccount Abstractionの敷居が大きく下がったと感じています。「4337特有の実装・運用の重さ(ユーザーには見せない部分)」を避けられるケースが増えたのは、エコシステム全体にとって良いことです。

参考リンク