[ERC7677] アプリが特定のPaymasterサービスと通信する仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC4337**で提案されているPaymasterサービスと通信するための標準化されたAPIを提案し、アプリ側が指定したPaymasterサービスとウォレットを経由して通信する仕組みを提案している**ERC7677**についてまとめていきます！

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

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

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

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

# 概要

**EIP5792**は、アプリがウォレットと通信して高度な機能を利用できるようにする提案です。
この提案では、アプリが**ERC4337**ウォレットに特定のPaymasterサービスと通信するようにリクエストできる機能を定義しています。
この機能をサポートするために、Paymasterサービスの標準化されたAPIも定義しています。

:::note info
Paymasterは、コントラクトウォレットからのトランザクション手数料を肩代わりしてくれるアドレスです。
これにより、ユーザーはガス代の負担をせずにトランザクションを実行できます。
このPaymasterの機能を提供するサービスとウォレットが通信するためのAPIについて提案しています。
:::

:::note info
**EIP5792**ではバッチトランザクションをはじめとする、複数の新しいJSON-RPCメソッドを定義しています。
より詳しくは以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/45da7ebe31ff0ee860a2

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

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

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

# 動機

現在、アプリ開発者はがユーザーのトランザクションのガス代をPaymasterに負担させたいと思っている時、アプリがウォレットに特定のPaymasterサービスと通信するよう指示する方法がありません。
また、ウォレットがPaymasterサービスとどのように通信するかの標準も存在しません。
そのため、アプリがウォレットに特定のPaymasterサービスと通信するように指示する方法と、ウォレットがPaymasterサービスと通信するための標準を両方必要としています。

# 仕様

新しい**EIP5792**ウォレットの機能が1つ定義されています。
また、Paymasterサービスの標準インターフェースについても定義しています。

## Paysmaster Webサービスインターフェース

### `pm_getPaymasterStubData`

このメソッドは、Paymasterが負担するガス代の見積もり値を返します。
以下のデータを受け取って実行します。
- 署名されていないユーザー操作 (unsigned user operation)
- エントリーポイントアドレス (entrypoint address)
- チェーンID (chain id)
- コンテキストオブジェクト (context object)

Paymasterサービスプロバイダーは、アプリ開発者に対してウォレットと通信する時にどのような情報が必要かのフィールドを定義することができます。

このメソッドは、提供されたEntoryPointのバージョンに適用される場合、Paymaster固有のガス値を返すことがあります。
例えば、エントリーポイントv0.7が提供された場合、このメソッドは`paymasterVerificationGasLimit`と`paymasterPostOpGasLimit`の値を返すことがあります。
ウォレットは、ガス見積もりのために**UserOperation**をバンドラーへ送信されたガス値を使用する必要があります。

:::note info
EntoryPointは署名の検証などを行うコントラクトのことです。
UserOperationは実行するトランザクションの内容のことです。
Bundler（バンドラー）は複数のコントラクトウォレットからのトランザクションをまとめて実行してくれるEOAアドレスのことです。
より詳しくは以下の記事を参考にしてください。
:::

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

また、このメソッドは、スポンサーオブジェクト（｀name`フィールドとオプションの`icon`フィールド）を返すことがあります。
`name`フィールドはトランザクションをスポンサーしてくれる主体の名前で、`icon`フィールドは画像へのURIです。
ウォレット開発者は、ユーザーにスポンサー情報を表示することを選ぶことができます。
`icon`のURIは[RFC-2397](https://www.ietf.org/rfc/rfc2397.txt)で定義されているデータURIの必要があり、画像は最低96x96pxの解像度を持つ正方形であるべきです。
画像フォーマットは、レンダリングが容易なPNG、WebP、SVGなどが推奨されます。
SVG画像はJavaScriptを実行できるため、ウォレットは信頼されていないJavaScriptの実行を防ぐために`<img>`タグを使用してSVG画像をレンダリングする必要があります。

場合によっては、Paymaterデータが署名を含まない場合など、`pm_getPaymasterStubData`の呼び出しだけで必要な情報が取得できる場合があります。
この場合、RPC呼び出しで`isFinal: true`を返すことにより、2番目のRPC呼び出しである`pm_getPaymasterData`をスキップすることができます。

Paymaster Web Serviceは、`pm_getPaymasterStubData`の実行中に受け取った**UserOperation**に対して検証を行い、スポンサーしなのであればこの段階でリクエストを拒否するべきです。

#### 具体例

1. **ガス見積もりのためのスタブ値の取得**
    - アプリがウォレットに署名されていないユーザー操作とともにこのメソッドを呼び出す。
    - Paymasterサービスが適用可能なガス値を返す。
    - ウォレットがこれらの値を使用してガス見積もりを行う。

2. **スポンサー情報の表示**
    - メソッドがスポンサーの名前とアイコンURIを含むオブジェクトを返す。
    - ウォレットがこれらの情報をユーザーに表示する。

3. **署名が不要な場合**
    - `isFinal: true`を返すことで、追加のRPC呼び出しをスキップする。

#### `pm_getPaymasterStubData`RPC仕様

ユーザーは、他のすべてのフィールドが入力された後に署名するため、**UserOperation**パラメータには署名が含まれません。

##### 入力パラメータ

入力パラメータは以下の4つです。

- **userOp**
    - `sender`
        - 送信者のアドレス。
    - `nonce`
        - ユニークなトランザクションID。
    - `initCode`
        - 初期化コード。
    - `callData`
        - 呼び出しデータ。
    - `callGasLimit`
        - 呼び出しのガスリミット。
    - `verificationGasLimit`
        - 検証のガスリミット。
    - `preVerificationGas`
        - 事前検証のガス量。
    - `maxFeePerGas`
        - ガスあたりの最大料金。
    - `maxPriorityFeePerGas`
        - ガスあたりの優先料金。
- **EntryPoint (エントリーポイント)**
    - エントリーポイントアドレス。
- **Chain ID (チェーンID)**
    - チェーンのID。
- **Context (コンテキスト)**
    - サービスプロバイダーが指定する追加情報（例: `policyId`など）。

##### 出力結果

出力結果は、Paymasterサービスがどの**EntryPoint**バージョンを使用しているかに依存します。

- **共通フィールド**
    - `sponsor`
        - スポンサー情報（名前とオプションでアイコンURI）。
- **EntryPoint v0.6**の場合
    - `paymasterAndData`
        - Paymasterとデータ。
- **EntryPoint v0.7**の場合
    - `paymaster`
        - Paymasterアドレス。
    - `paymasterData`
        - Paymasterデータ。
    - `paymasterVerificationGasLimit`
        - Paymaster検証ガスリミット（オプション）。
    - `paymasterPostOpGasLimit`
        - Paymasterポストオペレーションガスリミット（オプション）。
    - `isFinal`
        - 追加のRPC呼び出しが不要であることを示すフラグ（オプション）。

#### 例

##### 入力例

```json
[
  {
    "sender": "0x...",
    "nonce": "0x...",
    "initCode": "0x",
    "callData": "0x...",
    "callGasLimit": "0x...",
    "verificationGasLimit": "0x...",
    "preVerificationGas": "0x...",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  },
  "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
  "0x2105",
  {
    "policyId": "962b252c-a726-4a37-8d86-333ce0a07299"
  }
]
```

##### 出力例

- **EntryPoint v0.6**の場合

```json
{
  "sponsor": {
    "name": "My App",
    "icon": "https://..."
  },
  "paymasterAndData": "0x..."
}
```

- **EntryPoint v0.7**の場合

```json
{
  "sponsor": {
    "name": "My App",
    "icon": "https://..."
  },
  "paymaster": "0x...",
  "paymasterData": "0x..."
}
```

- **EntryPoint v0.7**でペイマスターガス見積もりも含む場合

```json
{
  "sponsor": {
    "name": "My App",
    "icon": "https://..."
  },
  "paymaster": "0x...",
  "paymasterData": "0x...",
  "paymasterVerificationGasLimit": "0x...",
  "paymasterPostOpGasLimit": "0x..."
}
```

- **追加のRPC呼び出しが不要であることを示す場合**

```json
{
  "sponsor": {
    "name": "My App",
    "icon": "https://..."
  },
  "paymaster": "0x...",
  "paymasterData": "0x...",
  "isFinal": true
}
```


### `pm_getPaymasterData`

署名された**UserOperation**においてPaymaster関連フィールドに使用される値を返すJSON-RPCメソッドで、実際のトランザクション送信時に使用されます。
このメソッドは、署名されていない**UserOperation**、**EntryPoint**アドレス、チェーンID、コンテキストオブジェクトを受け取ります。


#### `pm_getPaymasterData` RPC 仕様

##### 入力パラメータ

入力パラメータは以下の4つです。

- **userOp (ユーザー操作)**
    - `sender`
        - 送信者のアドレス。
    - `nonce`
        - ユニークなトランザクションID。
    - `initCode`
        - 初期化コード。
    - `callData`
        - 呼び出しデータ。
    - `callGasLimit`
        - 呼び出しのガスリミット。
    - `verificationGasLimit`
        - 検証のガスリミット。
    - `preVerificationGas`
        - 事前検証のガス量。
    - `maxFeePerGas`
        - ガスあたりの最大料金。
    - `maxPriorityFeePerGas`
        - ガスあたりの優先料金。
- **EntryPoint (エントリーポイント)**
    - エントリーポイントアドレス。
- **Chain ID (チェーンID)**
    - チェーンのID。
- **Context (コンテキスト)**
    - サービスプロバイダーが指定する追加情報（例: `policyId`など）。

##### 出力結果

出力結果は、Paymasterサービスがどの**EntryPoint**バージョンを使用しているかに依存します。

- **EntryPoint v0.6**の場合
    - `paymasterAndData`
        - ペイマスターとデータ。
- **EntryPoint v0.7**の場合
    - `paymaster`
        - ペイマスターアドレス。
    - `paymasterData`
        - ペイマスターデータ。

#### 例

##### 入力例

```json
[
  {
    "sender": "0x...",
    "nonce": "0x...",
    "initCode": "0x",
    "callData": "0x...",
    "callGasLimit": "0x...",
    "verificationGasLimit": "0x...",
    "preVerificationGas": "0x...",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  },
  "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
  "0x2105",
  {
    "policyId": "962b252c-a726-4a37-8d86-333ce0a07299"
  }
]
```

##### 出力例

- **EntryPoint v0.6**の場合

```json
{
  "paymasterAndData": "0x..."
}
```

- **EntryPoint v0.7**の場合

```json
{
  "paymaster": "0x...",
  "paymasterData": "0x..."
}
```

## `paymasterService`

この`paymasterService`はアプリとウォレットの両方で実装されます。

### App実装

#### PaymasterService Capability

`paymasterService`機能は、アプリとウォレットの両方に実装されます。この機能を利用して、アプリはウォレットに対して特定のペイマスターサービスURLを提供し、ウォレットはそのURLに対してRPCコールを行います。

### アプリの実装

アプリは、ウォレットにPaymasterサービスのURLを渡す必要があります。
これを実現するために、アプリは`paymasterService`機能を使って**EIP5792**で提案されている`wallet_sendCalls`呼び出しの一部としてウォレットに情報を送ります。

##### wallet_sendCalls Paymaster Capability 仕様

- **PaymasterCapabilityParams**
    - `url`
        - ペイマスターサービスのURL。
    - `context`
        - サービスプロバイダーが指定する追加情報。

```js
type PaymasterCapabilityParams = {
  url: string;
  context: Record<string, any>;
}
```

##### wallet_sendCalls パラメータの例

以下は、`wallet_sendCalls`呼び出しの例です。

```json
[
  {
    "version": "1.0",
    "chainId": "0x01",
    "from": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
    "calls": [
      {
        "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
        "value": "0x9184e72a",
        "data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"
      },
      {
        "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
        "value": "0x182183",
        "data": "0xfbadbaf01"
      }
    ],
    "capabilities": {
      "paymasterService": {
        "url": "https://...",
        "context": {
          "policyId": "962b252c-a726-4a37-8d86-333ce0a07299"
        }
      }
    }
  }
]
```

- **version**
    - APIのバージョン。
- **chainId**
    - チェーンのID。
- **from**
    - トランザクションの送信元アドレス。
- **calls**
    - 複数のトランザクションの詳細情報。
    - 各トランザクションは以下のフィールドを持ちます。
        - `to`
            - トランザクションの宛先アドレス。
        - `value`
            - トランザクションの値（wei単位）。
        - `data`
            - トランザクションのデータ。
- **capabilities**
    - 使用する機能を指定。
    - ここでは`paymasterService`機能を指定します。
- `url`
    - ペイマスターサービスのURL。
- `context`
    - サービスプロバイダーが指定する追加情報（例：`policyId`）。

##### ウォレットの動作

ウォレットは、アプリから受け取ったPaymasterサービスのURLに対して、上記のPaymasterRPC`call`を行います。
具体的には、ウォレットは`pm_getPaymasterStubData`や`pm_getPaymasterData`の呼び出しを指定されたURLに対して実行して必要なデータを取得します。

### Wallet実装

この仕様に準拠するために、Appスポンサー付きのトランザクションを活用したコントラクトウォレットは以下の要件を満たす必要があります。

- ウォレットはアプリに対してPaymasterウェブサービスと通信できることを示す必要があります。
    - これは、**EIP-5792 wallet_getCapabilities**コールのレスポンスとして判別できます。
- ウォレットは、アプリが指定したPaymasterサービスの値を使用する必要があります。
    - これは、**EIP-5792 wallet_sendCalls**コールの`capabilities`フィールドで指定されたPaymasterサービスの値を使用することを意味します。
    - ただし、ウォレットがユーザーにPaymasterを選ばせる場合などの例外もあります。
    - Paymasterサービスが最終的に使用されない場合があるため、アプリケーションは提供されたPaymasterが必ずしもトランザクション手数料を支払うことを前提としてはいけません。

#### `wallet_getCapabilities`レスポンス仕様

ウォレットは`wallet_getCapabilities`コールに対して以下の形式でレスポンスします。

```typescript
type PaymasterServiceCapability = {
  supported: boolean;
}
```

#### `wallet_getCapabilities`応答例

```json
{
  "0x2105": {
    "paymasterService": {
      "supported": true
    }
  },
  "0x14A34": {
    "paymasterService": {
      "supported": true
    }
  }
}
```

#### フロー図

![0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/8d659e2f-709f-e56f-a709-49ef2e0434e8.png)

1. **アプリからのリクエスト**
    - アプリは、`wallet_sendCalls`コールを使用してウォレットに対してトランザクションを送信します。
    - このコールには、PaymasterサービスのURLとコンテキスト情報が含まれています。

2. **ウォレットの応答**
    - ウォレットは、指定されたPaymasterサービスのURLに対して`pm_getPaymasterStubData`や`pm_getPaymasterData`のRPCコールを行い必要なデータを取得します。

3. **トランザクションの処理**
    - ウォレットは取得したデータを使用してトランザクションを処理し、必要に応じてユーザーにスポンサー情報を表示します。

# 補足

## ガス見積もり

現在、Paymasterサービスは`pm_sponsorUserOperation`メソッドを使用して、**UserOperation**に関連するフィールドとガス代の見積もり値を返します。
しかし、各Paymasterサービスプロバイダーはガス見積もりの方法が異なるため、見積もり値が一定でなく見積もりが不十分なこともあります。

そこで、ガス見積もりをウォレットに任せる方が良いと考えています。
ウォレットは**UserOperation**がどのようにバンドラーに渡されるかについての詳細情報を持っているため、より正確な見積もりが可能です。
そして、ウォレットはその見積もりに基づいてPaymasterサービスにガススポンサーシップを依頼します。

### `pm_getPaymasterStubData`メソッド

`pm_getPaymasterStubData`メソッドはPaymaster固有のガス見積もりを返すことができ、これによりバンドラーが見積もりを不足するリスクを軽減します。

## チェーンIDパラメータ

現在、Paymasterサービスプロバイダーは通常チェーンごとにURLを渡し、マルチチェーン対応はしていません。
しかし、ウォレットがどのチェーンのリクエストであるかをPaymasterサービスと通信できるようにするために、チェーンIDパラメータが必要です。

- **オプション1**
    - 現在の標準を正式化し、PaymasterサービスURLがチェーンごとに1対1対応するようにする。
- **オプション2**
    - Paymasterサービスリクエストの一部としてチェーンIDパラメータを要求する。

オプション2はより柔軟であり、マルチチェーンURLを提供するサービスプロバイダーにも対応できます。

## ガスの見積もりの課題

ガス見積もりに柔軟性を持たせることで、Paymasterサービスが信頼性の高いガス見積もりを生成するために以下の点に注意が必要です。

### preVerificationGas

`preVerificationGas`の値は**UserOperation**のサイズやゼロバイトと非ゼロバイトの比率に大きく影響されます。
これにより、`pm_getPaymasterStubData`が返す値が最終的な`pm_getPaymasterData`で必要な`preVerificationGas`よりも低く見積もられる場合があります。
この場合、バンドラーは`eth_sendUserOperationの`の送信中に不十分な`preVerificationGas`エラーを返します。

このシナリオを避けるために、Paymasterサービスは以下の条件を満たすガス見積もり値を返す必要があります。

- 最終データと同じ長さであること。
- ゼロバイト（`0x00`）の量が最終データよりも少ないか等しいこと。

### 一貫したコードパス

**UserOperation**に使用されるデータと同じの長さの無意味なデータの繰り返し（例：`0x01`）で構成された値（スタブデータ）は、`preVerificationGas`の計算においては有効ですがシミュレーションに失敗する可能性があります。

また、有効なスタブデータであってもガス見積もりが不十分になることがあります。
これは、スタブデータが`validatePaymasterUserOp`や`postOp`関数で異なるコードパスをシミュレートする場合に起こります。
例えば、シミュレーション中にコードが早期に終了した場合、予想よりも少ないガス見積もりが返され、その結果バンドラーに提出された**UserOperation**がガス不足エラーを引き起こす可能性があります。

そのため、Paymasterサービスは実行される**UserOperation**と同じコードパスを実行するシミュレーション結果をもたらすスタブデータを返す必要があります。

# セキュリティ

Paymasterサービスプロバイダーがアプリ開発者に提供するURLには、通常APIキーが含まれています。
アプリ開発者はこれらのAPIキーをウォレットに渡したくない場合があります。
これを解決するために、以下の方法を推奨します。

- アプリ開発者は、Paymasterサービスへの呼び出しを仲介するアプリのバックエンドURLを渡します。
- アプリのバックエンドがPaymasterサービスへのリクエストを処理し、必要なAPIキーを使用してペイマスターサービスにリクエストを送信します。

## フロー図

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570269/54153c42-7c01-48c9-81d0-a899dd184e3d.png)


1. **アプリからバックエンドへのリクエスト**
    - アプリはウォレットにペイマスターサービスのURLではなく、バックエンドのURLを渡します。
    - ウォレットは、このバックエンドURLに対してPaymasterサービスリクエスト（例：`pm_getPaymasterStubData`や`pm_getPaymasterData`）を送信します。

2. **バックエンドの処理**
    - アプリのバックエンドは、受信したリクエストを元にPaymasterサービスに対して実際のAPIリクエストを行います。
    - この時に必要なAPIキーを付与します。
    - バックエンドはPaymasterサービスからのレスポンスを受け取り、必要に応じて追加のシミュレーションや検証を行います。

3. **バックエンドからウォレットへの応答**
   - バックエンドは、Paymasterサービスのレスポンスを適切な形式でウォレットに返します。

## 詳細フロー

1. **アプリの設定**
    - アプリはウォレットに次のようなURLを提供します。
    
     ```
     https://app-backend.example.com/proxy-paymaster
     ```

2. **ウォレットのリクエスト**
    - ウォレットは`pm_getPaymasterStubData`を次のようにバックエンドに送信します。
    
     ```json
     {
       "sender": "0x...",
       "nonce": "0x...",
       "initCode": "0x...",
       "callData": "0x...",
       "callGasLimit": "0x...",
       "verificationGasLimit": "0x...",
       "preVerificationGas": "0x...",
       "maxFeePerGas": "0x...",
       "maxPriorityFeePerGas": "0x...",
       "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
       "chainId": "0x2105",
       "context": {
         "policyId": "962b252c-a726-4a37-8d86-333ce0a07299"
       }
     }
     ```

3. **バックエンドの処理とペイマスターサービスへのリクエスト**
    - バックエンドはリクエストを受け取り、APIキーを付与してPaymasterサービスに次のようなリクエストを送信します。
     ```json
     {
       "sender": "0x...",
       "nonce": "0x...",
       "initCode": "0x...",
       "callData": "0x...",
       "callGasLimit": "0x...",
       "verificationGasLimit": "0x...",
       "preVerificationGas": "0x...",
       "maxFeePerGas": "0x...",
       "maxPriorityFeePerGas": "0x...",
       "entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
       "chainId": "0x2105",
       "context": {
         "policyId": "962b252c-a726-4a37-8d86-333ce0a07299"
       }
     }
     ```

4. **Paymasterサービスからのレスポンスとバックエンドの処理**
    - Paymasterサービスからのレスポンスを受け取り、必要に応じて追加の検証を行います。

5. **バックエンドからウォレットへのレスポンス**
    - 最終的なレスポンスをウォレットに返します。

# 引用

Lukas Rosario (@lukasrosario), Dror Tirosh (@drortirosh), Wilson Cusack (@wilsoncusack), Kristof Gazso (@kristofgazso), Hazim Jumali (@hazim-j), "ERC-7677: Paymaster Web Service Capability [DRAFT]," Ethereum Improvement Proposals, no. 7677, April 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7677.

# 最後に

今回は「**ERC4337**で提案されているPaymasterサービスと通信するための標準化されたAPIを提案し、アプリ側が指定したPaymasterサービスとウォレットを経由して通信する仕組みを提案している**ERC7677**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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