はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、EIP7702のコード委任を活用し、EOA(Externally Owned Account)にバッチ実行やガススポンサーシップ機能を付与する最小構成のスマートウォレットインターフェースを提案しているERC7988についてまとめていきます!

以下にまとめられているものを解説しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC7988とは、Minimal Avatar Smart Wallet(MASW)と呼ばれる、EIP7702のコード委任メカニズムを利用した最小構成のスマートウォレット規格です。

EIP7702については以下の記事を参考にしてください。

EIP7702を使うと、EOAに対して任意のスマートコントラクトのコードを一時的に紐付けることができます。 ERC7988はその仕組みを利用して、EOAを「avatar(アバター)」として、MASWのロジックを「delegate(委任先)」として同じアドレスで動作させます。 つまり、EOAのアドレスはそのままで、スマートコントラクトウォレットとしての機能が使えるようになります。

以下の図は、avatar(EOA)とdelegate(MASW)の関係を示しています。

fig2-avatar-delegate.png

MASWはexecuteBatchによるアトミックなバッチ実行、ETHまたはERC20トークンでのガススポンサーシップ、そしてPolicy ModuleとRecovery Moduleという2つのモジュール拡張ポイントを提供します。

ERC20については以下の記事を参考にしてください。

動機

ウォレットの断片化問題

EIP7702はEOAに任意のコントラクトコードを紐付けられるため、実装の自由度が非常に高いです。 しかし、自由度の高さはそのまま断片化につながります。 ウォレットごとにABI(コントラクトの関数インターフェース)や手数料の精算方法が異なると、dAppsやリレイヤーはウォレットごとに個別のアダプターを用意する必要が生じます。

なぜ標準化が必要か

断片化の影響は複数の領域に及びます。

領域 断片化による影響
相互運用性 ウォレットごとに異なるABIにdAppsが対応する必要があり、統合の複雑さと障害パターンが増加する。
ガススポンサーシップ 手数料の精算方法がウォレットごとに異なると、リレイヤーのインセンティブが低下し、スポンサード・トランザクションの普及が遅れる。
ツーリング インデクサー、デバッガー、静的解析ツールは、単一のバイトコードと7フィールドの固定スキーマを対象にすることで、イベントデコードやガス見積もりの精度が向上する。
拡張性 コア部分の多様性を抑え、拡張ポイントを2つのモジュール境界に限定することで、セキュリティの強化やリサーチにリソースを集中できる。

この提案による解決策

ERC7988は「不変のバイトコード」、「固定のEIP712署名ドメイン」、「最小限のABI」を標準化しつつ、Policy ModuleとRecovery Moduleという2つの拡張ポイントを公開することで、断片化を抑えつつ拡張性を確保します。 ウォレットのコアロジックは変更できない代わりに、モジュールを差し替えることでカスタムロジックを実装できる設計です。

仕様

デリゲーションの流れ

ERC7988のウォレットは以下の手順でセットアップされます。

  1. デプロイ MASWコントラクトをデプロイし、コンストラクタ引数に所有者のEOAアドレスを指定します。
  2. EIP-7702トランザクション 所有者がタイプ0x04のトランザクション(EIP7702で定義)を送信し、MASWのバイトコードハッシュを参照先として指定します。
  3. アバターウォレットとして動作 以降のトランザクションでは、EOAがavatar、MASWのコードがdelegateとして同じアドレスで動作します。 所有者が新たに0x04トランザクションを送信してクリアまたは置換するまで、この状態が維持されます。
sequenceDiagram
    participant Owner as EOA(所有者)
    participant MASW as MASWコントラクト
    participant Chain as ブロックチェーン

    Owner->>Chain: MASWをデプロイ(_owner = EOA)
    Owner->>Chain: EIP-7702 tx(0x04)でコード委任
    Note over Owner,Chain: 以降、EOAアドレスでMASWロジックが実行可能
    Owner->>MASW: executeBatch(署名付き)
    MASW->>Chain: targets[0].call → targets[1].call → ...

パブリックインターフェース

executeBatch

MASWの中核となる関数で、複数のコントラクト呼び出しを1つのトランザクションで実行します。

javascript
function executeBatch(
    address[] calldata targets,
    uint256[] calldata values,
    bytes[]   calldata calldatas,
    address token,
    uint256 fee,
    uint256 expiry,
    bytes   calldata signature
) external;

この関数は7つのパラメータを受け取ります。

パラメータ 型 説明
targets address[] 呼び出し先のコントラクトアドレスの配列。
values uint256[] 各呼び出しに送信するETHの量(wei単位)。
calldatas bytes[] 各呼び出しのcalldata(関数の引数データ)。
token address 手数料の支払いに使うトークンのアドレス。address(0)の場合はETHで支払う。
fee uint256 リレイヤーに支払う手数料の金額。0の場合はスポンサーシップなし。
expiry uint256 署名の有効期限(UNIXタイムスタンプ)。この時刻を過ぎるとトランザクションは無効になる。
signature bytes EIP712形式の署名。所有者が上記パラメータとmetaNonceを署名したもの。
バッチ実行の各ステージ

以下の図は、executeBatchの各ステージを示したフロー図です。

fig1-batch-execution-flow.png

executeBatchは以下のステージを順番に実行します。

  1. 検証 targets、values、calldatasの配列長が一致し、0より大きいことを確認します。 block.timestampがexpiry以下であること、metaNonceが現在の値と一致することを検証します。 EIP712ダイジェストから復元されたアドレスが所有者と一致するか、Recovery Moduleが署名を承認するかを確認します。 metaNonceは一致確認後にこのステージ内でインクリメントされます。
  2. Policyプリフック Policy Moduleが設定されている場合、preCheckを呼び出します。 falseを返すかrevertすると、バッチ全体が拒否されます。
  3. 呼び出し実行 配列の各インデックスに対してtargets[i].call{value:values[i]}(calldatas[i])を実行します。 1つでも失敗するとバッチ全体がrevertします。
  4. Policyポストフック postCheckを呼び出し、プリフックと同様にバッチを拒否できます。
  5. 手数料の精算 feeが0より大きい場合、tokenがaddress(0)ならETHで、それ以外ならERC20のtransferでリレイヤーに手数料を送ります。 ERC20のTransfer時にはOpenZeppelinのSafeERC20パターンで戻り値をチェックします。
  6. イベント発行 BatchExecuted(structHash)イベントを発行します。

setPolicyModule / setRecoveryModule

モジュールを設定・変更するための関数です。

javascript
function setPolicyModule(address newModule)   external;
function setRecoveryModule(address newModule) external;

モジュールの変更には所有者の署名付きトランザクションが必要です。

イベント

MASWは以下の2つのイベントを定義しています。

javascript
event BatchExecuted(bytes32 indexed structHash);
event ModuleChanged(bytes32 indexed kind, address oldModule, address newModule);

BatchExecutedはexecuteBatchの実行完了時に発行され、structHash(indexed)でバッチの内容を特定できます。 ModuleChangedはモジュールの設定・変更時に発行され、kind(indexed)でPolicy ModuleかRecovery Moduleかを識別します。

ストレージレイアウト

MASWは最小限の4スロットのみをストレージに使用します。

スロット 変数名 型 説明
0 metaNonce uint256 単調増加するメタnonce。リプレイ攻撃を防止する。
1 _entered uint256 リエントランシーガードフラグ。
2 policyModule address Policy Moduleのアドレス。ゼロアドレスの場合はモジュールなし。
3 recoveryModule address Recovery Moduleのアドレス。ゼロアドレスの場合はモジュールなし。

ownerとDOMAIN_SEPARATORはimmutable(デプロイ時に固定)であるため、ストレージスロットを消費しません。

EIP712署名

EIP712

EIP712とは、構造化されたデータに対する署名方式を定めた規格です。 署名対象のデータに型情報を含めることで、ユーザーが署名する内容を検証可能にします。

バッチ型ハッシュ

executeBatchの署名に使われる型ストリングは以下の通りです。

javascript
bytes32 constant BATCH_TYPEHASH = keccak256(
  "Batch(address[] targets,uint256[] values,bytes[] calldatas,address token,uint256 fee,uint256 exp,uint256 metaNonce)"
);

型ストリング内のフィールド名expは、関数引数ではexpiryに対応します。 metaNonceは現在のnonce値が署名に含まれることで、同じ署名を再利用するリプレイ攻撃を防止します。

ドメインセパレータ

リプレイ攻撃を防ぐために、MASWはEIP712のドメインセパレータを使用します。

javascript
DOMAIN_SEPARATOR = keccak256(
  abi.encode(
    keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
    keccak256("MASW"),
    keccak256("1"),
    block.chainid,
    _owner
  )
);

chainIdにはライブのチェーンIDが使われ、0は許可されません。 これにより同じ署名を別のチェーンで再利用するクロスチェーンリプレイが防止されます。 verifyingContractに所有者のアドレスを使用しているのは、コード委任の前後でドメインセパレータが変わらないようにするためです。

ガススポンサーシップ

ガススポンサーシップは、リレイヤーが所有者の代わりにトランザクションを送信し、その対価として手数料を受け取る仕組みです。

リレイヤーと所有者はオフチェーンで(token, fee)について合意します。 手数料は署名されたバッチデータの一部であるため、リレイヤーが一方的に手数料を引き上げることはできません。

別のリレイヤーが同じ署名済みバッチを先にブロードキャストした場合、そのリレイヤーが手数料を獲得し、元のリレイヤーのトランザクションはrevertします。 これにより、最も早く送信したリレイヤーが報酬を得るという自然なインセンティブ構造が実現されています。

リレイヤーはアバターウォレットの残高を事前に確認する必要があります。 残高が不足している場合、トランザクションはmempoolの段階で無効と判定されます。

モジュール

MASWは2つのモジュール拡張ポイントを提供します。 コアロジックは不変のまま、モジュールの差し替えだけでカスタム機能を追加できる設計です。

Policy Module

javascript
interface IPolicyModule {
  function preCheck(address sender, bytes calldata rawData, uint256 value) external view returns (bool);
  function postCheck(address sender, bytes calldata rawData, uint256 value) external view returns (bool);
}

バッチ実行の前後でチェックを行うモジュールです。 preCheckはバッチ実行前、postCheckはバッチ実行後に呼ばれます。 falseを返すかrevertすることでバッチを拒否できます。

valueパラメータはトランザクションで送信されたETHの合計量(msg.value)を表します。 Policy Moduleはこの値をrawDataに含まれるバッチ要件と照合して検証できます。

例えば、バッチのサイズ(呼び出し回数)を制限するポリシーや、特定のコントラクトへの呼び出しのみを許可するポリシーを実装できます。 複数のポリシーを組み合わせるアグリゲーター設計も推奨されており、子ポリシーに順番に転送して最初の失敗で停止する方式が使えます。

Recovery Module

javascript
interface IRecoveryModule {
  function isValidSignature(bytes32 hash, bytes calldata sig) external view returns (bytes4);
}

所有者の秘密鍵を紛失した場合の代替署名検証を提供するモジュールです。 0x1626ba7eを返すと署名が有効と判定されます。 例えば、ソーシャルリカバリー(信頼できる複数の連絡先の合意で復旧する方式)や、マルチシグによる署名検証を実装できます。

補足

なぜ不変ロジックなのか

MASWのコアロジックは不変(イミュータブル)に設計されています。 新しいバージョンが必要な場合、所有者は明示的にEIP7702の0x04トランザクションを送信して、委任先を新しいコントラクトに切り替える必要があります。

この設計にはアップグレードリスクの最小化というメリットがあります。 プロキシパターンのように実装を暗黙的に差し替えられる仕組みでは、悪意のあるアップグレードやバグの混入リスクがあります。 MASWではコアロジックが変わらないため、ユーザーは常に同じコードが実行されることを確信できます。

カスタムロジックが必要な場合は、Policy ModuleとRecovery Moduleの2つの拡張ポイントを使います。 これにより、コアの安全性を維持したままバッチサイズ制限やソーシャルリカバリーのような高レベルの機能を追加できます。

nonce競合の考慮

MASWはグローバルなmetaNonceを使用するため、2つのリレイヤーが同じnonceで同時にトランザクションを送信すると、1つは成功し、もう1つはrevertします。

この問題はexpiryフィールド(通常30秒以下に設定)によって軽減されます。 短い有効期限により、リプレイウィンドウが狭くなり、競合の影響が限定されます。 ただし、UIはこの失敗をユーザーに通知する必要があります。

バッチサイズの上限

MASWはバッチに含められる呼び出し数(targetsの長さ)にハードコードされた上限を設けていません。 上級ユーザーは多数の呼び出しをバンドルでき、保守的なユーザーはPolicy Moduleでサイズ制限を設けることができます。 これにより、コアの柔軟性を維持しつつ、ユーザーごとに適切な制約を課すことが可能です。

参考実装

参考実装のSolidityコードは以下のリポジトリで公開されています。

セキュリティ

リプレイ攻撃の防止

リプレイ攻撃とは、過去に成功したトランザクションを再送信して不正に実行する攻撃です。 MASWは2つの仕組みでこれを防止します。 同一チェーン上のリプレイに対しては、metaNonceが単調増加するため同じ署名は1回しか使えません。 異なるチェーンへのリプレイに対しては、ドメインセパレータにチェーンIDが含まれるため、別のチェーンで署名を再利用できません。

リエントランシー対策

executeBatch内で外部コントラクトを呼び出す時、呼び出し先から再度executeBatchが呼ばれるリエントランシーのリスクがあります。 MASWはnonReentrantガードでこれを防止します。 さらに、状態変更(metaNonceのインクリメント)は外部呼び出しの前に完了し、手数料の支払いは呼び出しの後に行われるため、状態の不整合が発生しません。

手数料のフロントランニング

ガススポンサーシップでは、リレイヤーが所有者の署名済みバッチを受け取ってトランザクションを送信します。 手数料(tokenとfee)は署名データの一部であるため、リレイヤーが一方的に手数料を引き上げることはできません。 ただし、別のリレイヤーが同じ署名済みバッチを先にブロードキャストするフロントランニングのリスクがあります。 このリスクは手数料を受け取るリレイヤー側が負うため、所有者には影響しません。

以下のテーブルは、脅威と対策の全体像をまとめたものです。

脅威 対策
同一チェーンでのリプレイ グローバルなmetaNonceで防止。
クロスチェーンリプレイ チェーンIDにバインドされたドメインセパレータで防止。
手数料の不正引き上げ 手数料は署名データの一部。フロントランニングのリスクはリレイヤー側が負う。
バッチによるガス攻撃 Policy Moduleで過大なバッチを拒否可能。
ERC20の非標準戻り値 OpenZeppelinのSafeERC20による戻り値チェック。
リエントランシー nonReentrantガード + 状態変更の順序設計。
悪意あるモジュール コアロジックは不変。モジュール変更には所有者署名が必要。

引用

最後に

今回は「ERC7988によるEIP-7702ベースの最小構成スマートウォレットMASW」についてまとめてきました。 いかがだったでしょうか?

EIP7702のコード委任は強力な仕組みですが、各ウォレットが独自のABIや手数料モデルを採用するとエコシステムが断片化します。 ERC7988は「7つのパラメータの固定ABI」と「2つのモジュール拡張ポイント」というシンプルな制約で、互換性と拡張性のバランスを実現する設計になっています。

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!