[ERC5267] EIP712ドメインの生成に必要な情報を渡す仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**EIP712署名を安全に使用するための、**EIP712**ドメインの記述と取得方法の仕組みを提案している規格である**ERC5267**についてまとめていきます！

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

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

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

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

# 概要

このEIPは、**EIP712**を補完する役割を果たし、コントラクトがそのドメインを記述するフィールドと値をどのように公開すべきかを標準化しています。
これにより、アプリケーションはこの説明を取得し、一般的な方法で適切なドメインセパレーターを生成し、それによって**EIP712**の署名を安全かつスケーラブルに統合できるようになります。

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

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

この標準化により、コントラクトはそのドメインに関する情報を提供し、アプリケーションはそれを取得して適切なドメインセパレーターを生成できるようになります。
これにより、**EIP712**の署名をより安全かつスケーラブルに統合できるようになります。

# 動機

**EIP712**は、複雑な構造を持つメッセージの署名スキームです。
リエントランシー攻撃を避けフィッシングを軽減するために、このスキームには「**ドメインセパレーター**」が含まれています。
これにより、生成された署名が特定のコントラクトなど特定のドメインに固有であることが保証され、ユーザーエージェントがエンドユーザーに署名の詳細と使用方法を通知できるようになります。
ドメインは、事前に定義されたフィールドのセットまたは拡張からのフィールドを持つデータ構造によって定義されます。
特に重要なのは、**EIP712**コントラクトがこれらのフィールドをどのように使用し、どの値を持つかを公開する方法を規定していないことです。
これが**EIP712**の採用を制限している可能性があり、一般的な統合を開発することができないため、アプリケーションは各**EIP712**ドメインに対してカスタムサポートを構築する必要があることがわかりました。

典型的な例として、**EIP2612**（許可）があります。
これはユーザーエクスペリエンスの向上に価値があると理解されているにもかかわらず、アプリケーションに広く採用されていません。
このEIPは、アプリケーションが**EIP712**署名を検証するためにコントラクトが使用するドメインの定義を取得するために使用できるインターフェースを定義しています。

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

https://qiita.com/cardene/items/6809173b2ac428b1ccd0

# 仕様

この規格に準拠するコントラクトは、以下に正確に宣言された`eip712Domain`を定義することが必要があり、指定された値は使用しない場合でも正確に返す必要があり、クライアント側で正しいデコードが行われるようにします。

```javascript
function eip712Domain() external view returns (
    bytes1 fields,
    string name,
    string version,
    uint256 chainId,
    address verifyingContract,
    bytes32 salt,
    uint256[] extensions
);
```

この関数の戻り値は、コントラクト内で**EIP712**署名の検証に使用されるドメインセパレーターを記述する必要があります。
**EIP712Domain**構造体の形式（つまり、どのオプションのフィールドと拡張が存在するか）および各フィールドの値を記述します。

- `fields`
    - ビットマップで、フィールド`i`が存在する場合にのみビット`i`が`1`に設定されます（`0 ≤ i ≤ 4`）。
    - ビットは最下位から最上位へ読まれ、フィールドは**EIP712**で指定された順序に従ってインデックスが付けられ、関数タイプにリストされている順序と同じです。
- `name`、`version`、`chainId`、`verifyingContract`、`salt`
    - `EIP712Domain`の対応するフィールドの値（`fields`に従って存在する場合）、フィールドが存在しない場合は値は未指定です。
    - 各フィールドのセマンティクスは**EIP712**で定義されています。
- `extensions`
    - EIP番号のリストで、それぞれが新しいドメインフィールドを持つ**EIP712**を拡張するEIPを参照する必要があります。
    - これには、それらのフィールドの値を取得するための方法と、含める条件が含まれる場合があります。
    - `fields`の値はそれらの含まれることに影響しません。

この関数の戻り値（または**EIP712**のドメイン）は、コントラクトのライフタイム全体で変更する可能性がありますが、変更は頻繁ではあってはなりません。
使用される場合、`chainId`フィールドは、基盤となるチェーンの**EIP155**IDを反映するために変更すべきです。
コントラクトは、ドメインが変更された可能性を示すために以下で定義された`EIP712DomainChanged`イベントを発行することができます。

```javascript
event EIP712DomainChanged();
```

# 補足

**EIP712**署名の注目すべき応用例は、**EIP2612**（許可）に見られます。
**EIP2612**は、`DOMAIN_SEPARATOR`関数を指定しています。
この関数は`bytes32`値を返します（具体的なドメインセパレータ、つまり `hashStruct(eip712Domain)`の結果です）。
ただし、この値だけでは**EIP712**と統合するためには不十分です。
なぜなら、**EIP712**で定義されたRPCメソッドは、セパレータの単なるハッシュ形式ではなくドメインを記述したオブジェクトを受け取るからです。
これはRPCメソッドの欠陥ではなく、むしろセキュリティの一環としてドメインを検証し、ユーザーに通知する必要がある提案の一部です。ハッシュ単体では、これを実現するのが不可能であるためです。
現在のEIPは、このギャップを**EIP712**と**EIP2612**の両方で埋めるものです。

拡張機能は、**EIP712**が述べているように、そのEIP番号によって説明されます。
「**この標準への将来の拡張機能は新しいフィールドを追加することができる...新しいフィールドはEIPプロセスを通じて提案されるべきである**」と**EIP712**で述べられているためです。

# 後方互換性

このEIPは、**EIP712**に対するオプションの拡張機能であり、既存のコントラクトとの後方互換性の問題を引き起こしません。

**EIP712**署名を使用しているアップグレード可能なコントラクトは、このEIPを実装することができます。
つまり、新しい機能を導入するためにコントラクトをアップグレードする選択肢が提供されています。

一方、このEIPを使用するユーザーエージェントやアプリケーションは、アップグレードできないコントラクトもサポートするべきです。
アップグレードできないコントラクトの場合、共通のドメインをコントラクトのアドレスとチェーンIDに基づいてハードコードする方法があります。
ただし、より一般的な方法として、利用可能な情報を使用して一般的なドメインパターンを推測し、それに合致するドメインセパレーターまたは`domainSeparator`関数をコントラクト内で選択することも考えられます。

# 参考実装

## Solidity

```javascript
pragma solidity 0.8.0;

contract EIP712VerifyingContract {
  function eip712Domain() external view returns (
      bytes1 fields,
      string memory name,
      string memory version,
      uint256 chainId,
      address verifyingContract,
      bytes32 salt,
      uint256[] memory extensions
  ) {
      return (
          hex"0d", // 01101
          "Example",
          "",
          block.chainid,
          address(this),
          bytes32(0),
          new uint256[](0)
      );
  }
}
```

このコントラクトのドメインは、`field`、`chainId`、`verifyingContract`のみを使用していますしたがって、`fields`の値は`01101`、または16進数で`0d`となります。

このコントラクトがEthereumメインネット上に存在し、そのアドレスが`0x0000000000000000000000000000000000000001`であると仮定すると、このコントラクトが説明するドメインは以下のようになります。

```javascript
{
  name: "Example",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000001"
}
```

この情報は、**EIP712**署名に関連するドメインを明確に定義し、署名の検証や他のアプリケーションとの統合に使用されます。

## JavaScript

ドメイン・オブジェクトは、`eip712Domain()`関数の呼び出しの戻り値に基づいて構築することができます。

```javascript
/** Retrieves the EIP-712 domain of a contract using EIP-5267 without extensions. */
async function getDomain(contract) {
  const { fields, name, version, chainId, verifyingContract, salt, extensions } =
    await contract.eip712Domain();

  if (extensions.length > 0) {
    throw Error("Extensions not implemented");
  }

  return buildBasicDomain(fields, name, version, chainId, verifyingContract, salt);
}

const fieldNames = ['name', 'version', 'chainId', 'verifyingContract', 'salt'];

/** Builds a domain object without extensions based on the return values of `eip712Domain()`. */
function buildBasicDomain(fields, name, version, chainId, verifyingContract, salt) {
  const domain = { name, version, chainId, verifyingContract, salt };

  for (const [i, field] of fieldNames.entries()) {
    if (!(fields & (1 << i))) {
      delete domain[field];
    }
  }

  return domain;
}
```

### 拡張機能

**EIPXYZ**が新しい`bytes32`型のフィールド`subdomain`と、その値を取得するための`getSubdomain()`関数を定義している場合を考えてみましょう。

```javascript
/** Retrieves the EIP-712 domain of a contract using EIP-5267 with support for EIP-XYZ. */
async function getDomain(contract) {
  const { fields, name, version, chainId, verifyingContract, salt, extensions } =
    await contract.eip712Domain();

  const domain = buildBasicDomain(fields, name, version, chainId, verifyingContract, salt);

  for (const n of extensions) {
    if (n === XYZ) {
      domain.subdomain = await contract.getSubdomain();
    } else {
      throw Error(`EIP-${n} extension not implemented`);
    }
  }

  return domain;
}
```

上記の`getDomain`関数は、コントラクトの**EIP712**ドメイン情報を取得するための関数であり、**EIP5267**と**EIPXYZ**のサポートを追加したものです。

この関数の動作は以下の通りです。

1. コントラクトの`eip712Domain()`関数を呼び出し、その結果から**EIP712**ドメインに必要な情報を取得します。
    - これには、`fields`、`name`、`version`、`chainId`、`verifyingContract`、`salt`、および`extensions`が含まれます。

2. `buildBasicDomain`関数を使用して、基本的な**EIP712**ドメイン情報を構築します。
    - これには、`fields`、`name`、`version`、`chainId`、`verifyingContract`、`salt`が含まれます。

3. 拡張機能`extensions`に対してループを実行し、各拡張機能に対する処理を行います。
    - もし拡張機能が**XYZ**である場合、コントラクトの`getSubdomain()`関数を呼び出して`subdomain`の値を取得し、**EIP712**ドメインに追加します`。
    - それ以外の場合、エラーメッセージをスローして**EIP-n**（nは拡張機能の番号）の拡張機能が実装されていないことを示します。

4. 最終的に、構築した**EIP712**ドメイン情報を返します。

ただし、`EIP712Domain`構造体に`subdomain`フィールドを追加する部分は、この参考実装のスコープ外です。
この部分は具体的な実装に応じて行う必要があります。

# セキュリティ考慮事項

このEIPによれば、コントラクトは自身以外の検証コントラクトや異なるチェーンのチェーンIDを指定することができます。
しかし、ユーザーエージェントやアプリケーションは、ユーザーから署名を要求する前に、これらが実際にコントラクトとチェーンに対応していることを確認すべきです。
つまり、コントラクトが提供するドメイン情報が正当であることを確認しなければなりません。

ただし、常にこれが適用できる前提条件ではありません。
特定の状況や使用ケースにおいては、コントラクトが異なる検証コントラクトや異なるチェーンIDを指定することが正当であり、ユーザーエージェントやアプリケーションはこれを受け入れる必要があるかもしれません。
したがって、この検証プロセスは柔軟で、具体的な状況に応じて適切な対応を取る必要があります。

:::note info
ユーザーエージェントやアプリケーションは、コントラクトが提供するドメイン情報を検証し、その情報が正当であると確信する前にユーザーから署名を要求すべきですが、これはすべての場面で適用できるルールではなく、柔軟性が必要な場合もあるということです。
:::

# 引用

Francisco Giordano (@frangio), "ERC-5267: Retrieval of EIP-712 domain," Ethereum Improvement Proposals, no. 5267, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5267.

# 最後に

今回は「**EIP712署名を安全に使用するための、**EIP712**ドメインの記述と取得方法の仕組みを提案している規格である**ERC5267**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


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