はじめに

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

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

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

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

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

概要

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

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

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

動機

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

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

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

仕様

この規格に準拠するコントラクトは、以下に正確に宣言された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フィールドは、基盤となるチェーンのEIP155IDを反映するために変更すべきです。 コントラクトは、ドメインが変更された可能性を示すために以下で定義された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を指定することが正当であり、ユーザーエージェントやアプリケーションはこれを受け入れる必要があるかもしれません。 したがって、この検証プロセスは柔軟で、具体的な状況に応じて適切な対応を取る必要があります。

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

引用

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などからお気軽に質問してください!

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