[ERC7679] ERC4337のUserOperationを標準化する仕組みを理解しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、異なる**ERC4337**で提案されているコントラクトアカウントのエンコードロジックを標準化し、コントラクトアカウントの実装に依存しない**UserOp**ビルダーを使用する仕組みを提案している**ERC7679**についてまとめていきます！

:::note info
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-7679

他にも様々なEIPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

# 概要

**ERC4337**コントラクトアカウントの実装では、署名、`nonce`、`calldata`（呼び出しデータ）のエンコード方法が異なります。
この違いにより、DApps、ウォレット、コントラクトアカウントツールが、特定のアカウントのSDK（ソフトウェア開発キット）を使用せずにコントラクトアカウントと連携することが難しくなります。
これにより、特定のベンダーに依存することになり、コントラクトアカウントの普及に悪影響を及ぼします。

この問題を解決するために、コントラクトアカウントの実装がアカウント固有のエンコードロジックをオンチェーンで実装できる標準的な方法を提案します。
これは、生の署名、`nonce`、`calldata`を入力として受け取り、適切にフォーマットされた形式で出力するメソッドの実装を行います。

:::note info
**ERC4337**については以下の記事を参考にしてください。
:::

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58/-qoixMc413uT4FFM61uhXYUiL05Hw-wahq9hV_JtjJs

https://qiita.com/cardene/items/8c97ca7c93c4557ebfc2

# 動機

**ERC4337**の**UserOperation**をコントラクトアカウント用に構築するには、各コントラクトアカウント実装の詳細な理解が必要です。
これは、各実装が`nonce`、`calldata`、署名を異なる方法でエンコードすることが許容されているためです。

## 具体例

- あるコントラクトアカウントでは`executeFoo`という関数を実行する可能性があります。
- 別のコントラクトアカウントは`executeBar`という関数を実行する可能性があります。

このように、同じ操作を実行していても、使用する関数が異なるため`calldata`も異なります。

## UserOp送信時の課題

**UserOperation**を特定のコントラクトアカウントに送信するためには、以下のステップが必要です。

1. **どのコントラクトアカウント実装を使用しているかを特定**
    - 各アカウントの実装方法を理解する必要があります。
2. **正しくエンコード**
    - 署名、`nonce`、`calldata`を正しくエンコードする。
    - もしくは、エンコード方法を知っている特定のアカウント専用のSDKを使用する。

## 問題点

多くのDApps、ウォレット、アカウントアブストラクション（AA）ツールは特定のコントラクトアカウント実装に依存しているため、断片化が発生します。
これにより、特定のベンダーに依存してしまい、異なる実装同士での相互運用性が損なわれます。

# 仕様

## UserOperation Builder

この規格に準拠するコントラクトアカウントの実装は、以下で定義されている**IUserOperationBuilder**インターフェースを実装する必要があります。

### IUserOperationBuilderインターフェース

```js
struct Execution {
    address target;
    uint256 value;
    bytes callData;
}

interface IUserOperationBuilder {
    /**
     * @dev Returns the ERC-4337 EntryPoint that the account implementation
     * supports.
     */
    function entryPoint() external view returns (address);
    
    /**
     * @dev Returns the nonce to use for the UserOp, given the context.
     * @param smartAccount is the address of the UserOp sender.
     * @param context is the data required for the UserOp builder to
     * properly compute the requested field for the UserOp.
     */
    function getNonce(
        address smartAccount,
        bytes calldata context
    ) external view returns (uint256);
	
    /**
     * @dev Returns the calldata for the UserOp, given the context and
     * the executions.
     * @param smartAccount is the address of the UserOp sender.
     * @param executions are (destination, value, callData) tuples that
     * the UserOp wants to execute.  It's an array so the UserOp can
     * batch executions.
     * @param context is the data required for the UserOp builder to
     * properly compute the requested field for the UserOp. 
     */
    function getCallData(
        address smartAccount,
        Execution[] calldata executions,
        bytes calldata context
    ) external view returns (bytes memory);
    
    /**
     * @dev Returns a correctly encoded signature, given a UserOp that
     * has been correctly filled out except for the signature field.
     * @param smartAccount is the address of the UserOp sender.
     * @param userOperation is the UserOp.  Every field of the UserOp should
     * be valid except for the signature field.  The "PackedUserOperation"
     * struct is as defined in ERC-4337.
     * @param context is the data required for the UserOp builder to
     * properly compute the requested field for the UserOp.
     */
    function formatSignature(
        address smartAccount,
        PackedUserOperation calldata userOperation,
        bytes calldata context
    ) external view returns (bytes memory signature);
}
```

### `Execution`

```js
struct Execution {
    address target;
    uint256 value;
    bytes callData;
}
```
- **target**
    - 呼び出し先のアドレス。
- **value**
    - 送金するEtherの量。
- **callData**
    - 実行する関数の呼び出しデータ。

### `entryPoint`

```js
function entryPoint() external view returns (address);
```

コントラクトアカウント実装がサポートする**ERC4337**の**EntryPoint**のアドレスを返す関数。

### `getNonce`

```js
function getNonce(
    address smartAccount,
    bytes calldata context
) external view returns (uint256);
```

指定されたコントラクトアカウントアドレスとコンテキストに基づいて、UserOpに使用する`nonce`を返す関数。

- **引数**
    - `smartAccount`
        - UserOpの送信者のアドレス。
    - `context`
        - UserOpビルダーがUserOpのフィールドを正しく計算するために必要なデータ。

### `getCallData`

```js
function getCallData(
    address smartAccount,
    Execution[] calldata executions,
    bytes calldata context
) external view returns (bytes memory);
```

指定されたコントラクトアカウントアドレス、実行内容、コンテキストに基づいてUserOpの`calldata`を返す関数。

- **引数**
    - `smartAccount`
        - UserOpの送信者のアドレス。
    - `executions`
        - 実行したい（ターゲット、送金額、呼び出しデータ）のタプルの配列。
        - バッチ実行が可能。
    - `context`
        - UserOpビルダーがUserOpのフィールドを正しく計算するために必要なデータ。

### `formatSignature`

```js
function formatSignature(
    address smartAccount,
    PackedUserOperation calldata userOperation,
    bytes calldata context
) external view returns (bytes memory signature);
```

署名フィールド以外の全てのフィールドが正しく入力されたUserOpに対して、正しい形式の署名を返す関数。

- **引数**
    - `smartAccount`
        - UserOpの送信者のアドレス。
    - `userOperation`
        - UserOp。
        - 署名フィールド以外の全てのフィールドが有効である必要があります。
    - `context`
        - UserOpビルダーがUserOpのフィールドを正しく計算するために必要なデータ。

## UserOperation Builderの使用

以下の手順に従って、UserOp Builderを使用してUserOpを構築します。

### 1. UserOpBuilderのアドレスとコンテキストを取得
アカウントオーナーから　**UserOpBuilder**のアドレスとコンテキストを取得します。
**コンテキスト**は、構築する側から見ると内容がわからないバイト配列です。
**UserOpBuilder**の実装が**UserOp**の各値を正しく決定するためにコンテキストが必要になる場合があります。

### 2. マルチコール（バッチeth_call）の実行
**getNonce**と**getCallData**をコンテキストと実行内容（executions）を使ってマルチコール（バッチ化された`eth_call`呼び出します。

- **getNonce**
    - 指定されたコンテキストとアカウントアドレスに基づいて`nonce`を取得。
- **getCallData**
    - 指定されたコンテキストと実行内容に基づいて`calldata`を取得。

### 3. UserOpの作成
取得したデータ（`nonce`と`calldata`）を使用してUserOpを作成します。
ガス値はランダムに設定しても良いですし、低く設定しても良いです。
この**UserOp**を使用してガス見積もり用のダミー署名を取得して、**UserOp**のハッシュに署名します。

### 4. formatSignatureの呼び出し
**eth_call**を使用して、**formatSignature**を呼び出し、**UserOp**とコンテキストを使用して適切にフォーマットされたダミー署名を取得します。
この**UserOp**をガス見積もりに使用します。

### 5. ガス値の調整
**UserOp**内の既存のガス値を、適切なガス見積もりから得た値に変更します。
この**UserOp**は署名フィールド以外は有効である必要があります。
**UserOp**のハッシュに署名し、署名を`UserOp.signature`フィールドに設定します。

### 6. formatSignatureの再呼び出し
`eth_call`を使用して、`formatSignature`を再度呼び出し、完全に有効な**UserOp**を取得します。

### 7. 完全に有効なUserOpの取得
この時点で、完全に有効な**UserOp**が取得できています。
これをバンドラーに提出するために必要な操作を実行します。

### 注意点
**UserOp**には`nonce`、`calldata`、署名以外にも多くのフィールドがありますが、これらはコントラクトアカウントの実装に大きく依存します。
コントラクトアカウントを構築する側がこれらの他のフィールドをどのように取得するかは実装次第です。。

## アカウントがデプロイされていない時にUserOp Builderを使用

### デプロイされていないアカウントに対してUserOp Builderを使用する方法

まだデプロイされていないアカウントに対してUserOpを構築する場合、UserOp Builderを利用する際の手順を以下のように変更します：

#### 1. 追加情報の取得
- UserOpBuilderのアドレスとコンテキストに加えて、ERC-4337で定義されている**ファクトリー（factory）**と**ファクトリーデータ（factoryData）**も取得します。
- **factory**: アカウントをデプロイするためのコントラクト。
- **factoryData**: ファクトリーがアカウントをデプロイするために必要なデータ。

#### 2. CounterfactualCallコントラクトの使用
- UserOpBuilderのビュー関数を呼び出す際に、eth_callを使用して**CounterfactualCall**コントラクトをデプロイします。このコントラクトは、アカウントをデプロイし、UserOpBuilderを呼び出します。

#### 3. UserOpの構築
- UserOpを構築する際に、factoryとfactoryDataを含めます。

### CounterfactualCallコントラクトの動作

**CounterfactualCall**コントラクトは次のように動作します。

1. **アカウントのデプロイ**
    - building party（**UserOp**を構築するDappsやウォレット）が提供した**ERC4337**で定義されている`factory`（新しいアカウントをデプロイするためのコントラクト）と`factoryData`（アカウントをデプロイするために必要な追加データ）を使用してアカウントをデプロイします。
    - デプロイが成功しなかった場合は、`revert`します。

2. **UserOpBuilderの呼び出し**
    - アカウントが正常にデプロイされた場合、**UserOpBuilder**を呼び出し、そのデータをbuilding partyに返します。

# 補足

## Context（コンテキスト）

**コンテキスト**は、**UserOp**ビルダーが`nonce`、`calldata`、署名を正しく決定するために必要なデータをエンコードしたバイト配列です。
通常、このコンテキストはアカウントオーナーがウォレットソフトウェアを使って作成します。

### 委任
アカウントオーナーがトランザクションの実行をbuilding party（コントラクトアカウントを構築する側）に委任したい場合を考えます。
この場合、アカウントオーナーはbuilding partyの公開鍵に対する署名をコンテキスト内にエンコードします。
この署名を「`authorization`」と呼びます。

1. **認可のエンコード**
    - アカウントオーナーがbuilding partyの公開鍵に署名してコンテキストに含めます。
2. **UserOpの署名フィールド**
    - building partyは自身の秘密鍵で署名を生成し、**UserOp**の署名フィールドに入力します。
3. **getSignatureの呼び出し**
    - **UserOp**ビルダーがコンテキストから`authorization`を抽出し、building partyの署名と連結します。
4. **署名の検証**
    - コントラクトアカウントは署名からbuilding partyの公開鍵を復元し、その公開鍵が`authorization`によって署名されていることを確認します。
    - 確認が成功すれば、UserOpを実行します。

## Dummy Signature（ダミー署名）

**ダミー署名**は、bundler（バンドラー）が**UserOp**のガスを見積もるために使用される署名です（`eth_estimateUserOperationGas`を使用）。
この段階では有効な署名はまだ存在しません。
なぜなら、有効な署名自体が**UserOp**のガス値に依存しているため、循環依存が発生するからです。
この循環依存を解消するために、ダミー署名が使用されます。

### ダミー署名の作成
ダミー署名は単なる固定値ではなく、コントラクトアカウントの署名検証ロジックに応じて構築されます。
ダミー署名は、実際の署名とほぼ同じガスを消費するように設計されます。
そのため、ダミー署名はコントラクトアカウントの実装に依存します。

# 互換性

この規格は、**EntryPoint**のバージョン0.7の全ての**ERC4337**コントラクトアカウントとの互換性があります。

**EntryPoint**のバージョン0.6以下に対しては、`PackedUserOperation`が異なるため、バージョンに従って**IUserOperationBuilder**インターフェースを修正する必要があります。

# 実装

### Counterfactual Call Contract

**Counterfactual Call Contract**は、まだデプロイされていないコントラクトアカウントに対して処理の実行を行うメカニズムを提供します。
これは、**ERC6492**に影響されていて、**ERC1271**の`isValidSignature`をプレデプロイされたコントラクトに対して実行する仕組みを利用しています。

:::note info
**ERC6492**については以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/019072f96b73939a047d

:::note info
**ERC1271**については以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/da237b1b8cc185491512

## コントラクト

```js
contract CounterfactualCall {
    
    error CounterfactualDeployFailed(bytes error);

    constructor(
        address smartAccount,
        address create2Factory, 
        bytes memory factoryData,
        address userOpBuilder, 
        bytes memory userOpBuilderCalldata
    ) { 
        // スマートアカウントがデプロイされていない場合、デプロイを試みる
        if (address(smartAccount).code.length == 0) {
            (bool success, bytes memory ret) = create2Factory.call(factoryData);
            // デプロイに失敗した場合はエラーをスロー
            if (!success || address(smartAccount).code.length == 0) revert CounterfactualDeployFailed(ret);
        }

        // userOpBuilderを呼び出し、結果を返す
        assembly {
            let success := call(gas(), userOpBuilder, 0, add(userOpBuilderCalldata, 0x20), mload(userOpBuilderCalldata), 0, 0)
            let ptr := mload(0x40)
            returndatacopy(ptr, 0, returndatasize())
            if iszero(success) {
                revert(ptr, returndatasize())
            }
            return(ptr, returndatasize())
        }
    }
    
}
```

### コントラクトの動作

1. **スマートアカウントのデプロイ確認**
    - `constructor`内で、指定された`smartAccount`がまだデプロイされていない場合、`create2Factory`を使用してデプロイを実行します。
    - デプロイが成功しなければ、`CounterfactualDeployFailed`エラーを投げます。
2. **userOpBuilderの呼び出し**
    - アセンブリコードを使用して、`userOpBuilder`を呼び出し、指定された`userOpBuilderCalldata`を実行します。
    - 呼び出しが成功しなければ、エラーを投げて、成功すれば結果を返します。

## ethersとviemライブラリを使用した呼び出し例

以下は、ethersとviemライブラリを使用してこのコントラクトを呼び出す例です。

### ethers

```js
const nonce = await provider.call({
  data: ethers.utils.concat([
    counterfactualCallBytecode,
    (
      new ethers.utils.AbiCoder()).encode(['address','address', 'bytes', 'address','bytes'], 
      [smartAccount, userOpBuilder, getNonceCallData, factory, factoryData]
    )
  ])
});
```

- **`counterfactualCallBytecode`**
    - **CounterfactualCall**コントラクトのバイトコード。
- **`smartAccount`**
    - スマートアカウントのアドレス。
- **`userOpBuilder`**
    - **UserOpBuilder**のアドレス。
- **`getNonceCallData`**
    - `getNonce`関数を呼び出すためのデータ。
- **`factory`**
    - ファクトリーコントラクトのアドレス。
- **`factoryData`**
    - ファクトリーコントラクトに渡すデータ。

### viem

```js
const nonce = await client.call({
  data: encodeDeployData({
    abi: parseAbi(['constructor(address, address, bytes, address, bytes)']),
    args: [smartAccount, userOpBuilder, getNonceCalldata, factory, factoryData],
    bytecode: counterfactualCallBytecode,
  })
});
```

- **`encodeDeployData`**
    - デプロイデータをエンコードする関数。
- **`parseAbi`**
    - ABIを解析する関数。
- **`counterfactualCallBytecode`**
    - **CounterfactualCall**コントラクトのバイトコード。
- **`smartAccount`**
    - コントラクトアカウントのアドレス。
- **`userOpBuilder`**
    - **UserOpBuilder**のアドレス。
- **`getNonceCalldata`**
    - `getNonce`関数を呼び出すためのデータ。
- **`factory`**
    - ファクトリーコントラクトのアドレス。
- **`factoryData`**
    - ファクトリーコントラクトに渡すデータ。

# セキュリティ

## ダミー署名のセキュリティ

ダミー署名は、ガス見積もりに使用されますが、中間者攻撃によって通信内容が盗みられる可能性があります。
しかし、ダミー署名は最終的な**UserOp**が提出された後には使えなくなるため、リスクは非常に低いです（両方の**UserOp**が同じ`nonce`を使用するため）。

この小さなリスクをさらに軽減するために、ダミー署名を得るために署名される**UserOp**には非常に低いガス値を設定しておくことが推奨されます。
これにより、ダミー署名が公開されたとしても、その**UserOp**には低いガス値が設定された状態でトランザクションが実行されるようになります。

:::note info
ガス代が低いと実行に失敗したり、トランザクションが長時間pendingの状態になります。
:::

# 引用

Derek Chiang (@derekchiang), Garvit Khatri (@plusminushalf), Fil Makarov (@filmakarov), Kristof Gazso (@kristofgazso), Derek Rein (@arein), Tomas Rocchi (@tomiir), bumblefudge (@bumblefudge), "ERC-7679: UserOperation Builder [DRAFT]," Ethereum Improvement Proposals, no. 7679, April 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7679.


# 最後に

今回は「異なる**ERC4337**で提案されているコントラクトアカウントのエンコードロジックを標準化し、コントラクトアカウントの実装に依存しない**UserOp**ビルダーを使用する仕組みを提案している**ERC7679**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4