# [ERC7412] オフチェーンデータを安全かつ効率的に活用する仕組みを理解しよう！

> スマートコントラクトがオフチェーン（ブロックチェーン外）のデータを必要とする場合の対応策を提案しています。

- URL: https://cardene.dev/articles/c0ee8e94802318b2d949
- 公開日: 2024-03-20
- 更新日: 2024-03-20
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、複数のブロックチェーンやオラクルネットワーク間でマルチコールを利用して、安全かつ効率的にやりとりする仕組みを提案している**ERC7412**についてまとめていきます！

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

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

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

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

## 概要

スマートコントラクトがオフチェーン（ブロックチェーン外）のデータを必要とする場合の対応策を提案しています。
具体的には、オラクルという仕組みを使用してオフチェーンのデータを取得し、そのデータをブロックチェーン上で利用可能にする方法について説明しています。
ステップごとにそのプロセスを確認していきます。

#### 1. コントラクトの実行時にオフチェーンデータが必要になる状況
スマートコントラクトは、そのロジックを実行する過程で、ブロックチェーン外のデータ（例えば、天候情報や株価など）が必要になる場合があります。

#### 2. エラーメッセージの提案
提案されている標準では、スマートコントラクトがオフチェーンデータが必要であることを示すために、`OracleDataRequired(address oracleContract, bytes oracleQuery)`というエラーを発生させます。
これは、「**オフチェーンデータが必要です**」とスマートコントラクトが明示的に通知している状態です。

#### 3. クライアントの役割
このエラーメッセージを受け取ったクライアント（例えば、ユーザーのウォレットやアプリケーション）は、指定されたオラクルネットワークにクエリを投げて、必要なオフチェーンデータを取得します。

#### 4. データの取得と検証
クライアントは、オラクルネットワークから取得したデータを、ブロックチェーン上で検証可能な形で（つまり、データが改ざんされていないことを確認できるように）スマートコントラクトに提供します。

#### 5. マルチコールの使用
クライアントは、オフチェーンデータの検証と元のスマートコントラクト関数の呼び出しを、一つのトランザクション内で連続して実行するためにマルチコールを使用します。
これにより、データが検証された直後にスマートコントラクトのロジックが正常に実行されるようになります。

#### 6. オフチェーンデータのオンチェーンへの書き込み
このプロセスを通じて、オフチェーンのデータがブロックチェーン上に書き込まれ、スマートコントラクトがそれを読み取って使用できるようになります。

:::message
この提案はスマートコントラクトが必要とするオフチェーンデータを効率的に、かつ安全に取得し使用するための方法を提案しています。
オラクルを使ってデータを取得し、そのデータをブロックチェーン上に記録してスマートコントラクトがアクセスできるようにすることで、より柔軟かつ実用的なスマートコントラクトの実装が可能になります。
:::

## 動機

イーサリアムのスケーリング計画では、スマートコントラクトのコードを実行するために、複数の実行コンテキスト（レイヤー2やレイヤー3のスケーリングソリューションを含む）を導入しています。
これにより、複数のチェーンにわたってデータを読み取る能力が、スケーラブルなアプリケーションの構築にとって重要になってきます。
特に、価格データに依存する分散型金融（DeFi）プロトコルでは、オラクルネットワークが無数のレイヤー2やレイヤー3ネットワークに継続的に新鮮なデータをプッシュすることは現実的ではありません。

この問題に対処するために、スマートコントラクトの関数が他のチェーンにデータを書き込むことができるクロスチェーンブリッジが開発されています。
しかし、これと同様に他のチェーンからデータを読み取ることを可能にする標準も必要です。
この標準は、価格フィードを含むあらゆるオフチェーンデータを分散型オラクルネットワークから読み取るために一般化することができます。

クロスチェーンデータの書き込みと読み取りの両方に対する標準があると、プロトコル開発者は非同期性に関する抽象化を作成できるようになります（このトピックは他のソフトウェアエンジニアリングの文脈で徹底的に探求されています）。
これにより、スケーリングの制約に悩まされることなく、高度に洗練されたプロトコルの開発が可能になります。

**ERC3668**は、オフチェーンデータが必要な場合に`revert`（エラーを発生させること）を使用することを導入しました。
ここで提案されている標準は、コールバック関数ではなくマルチコールを活用することで、これらの制約の一部を克服することが可能です。

:::message
このテキストは、イーサリアムのスケーリング問題に対処するために、複数のチェーン間でデータを読み書きする標準の必要性について説明しています。
このような標準により、スケーリング制約の影響を受けずに動作する洗練されたプロトコルを開発するための道が開かれます。
:::

## 仕様

オフチェーンデータを必要とするスマートコントラクトが従うべき標準について説明しています。
この標準は、特定のオフチェーンデータを要求し、検証するプロセスを構築するためのガイドラインを提供します。

#### OracleDataRequiredエラー

```ts
error OracleDataRequired(address oracleContract, bytes oracleQuery)
```

- **オフチェーンデータが必要な場合に発生するエラー**
    - コントラクトがオフチェーンデータを必要とする場合、`OracleDataRequired(address oracleContract, bytes oracleQuery)`というエラーを返します。
- **oracleQuery**
    - 要求されるオフチェーンデータを指定します。
    - このデータのフォーマットは、オラクルコントラクトによって指定されるオラクルIDに依存します。
    - これにはチェーンID、コントラクトアドレス、関数の署名、ペイロード、タイムスタンプや「**最新**」の指定が含まれる可能性があります。
- **oracleContract**
    - オフチェーンデータを検証し、`OracleDataRequired`エラーを回避するためにデータを提供するコントラクトのアドレスです。

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

```ts
interface IERC7412 {
  function oracleId() view external returns (bytes32 oracleId);
  function fulfillOracleQuery(bytes signedOffchainData) payable external;
}
```

- **IERC7412**
    - オフチェーンデータのクエリを満たすために実装する必要があるインターフェースです。
- **oracleId()関数**
    - 分散型オラクルネットワークを参照する一意の識別子（オラクルID）を返します。
    - このIDは、イーサリアムエコシステムのチェーンIDに類似しています。
- **fulfillOracleQuery(bytes signedOffchainData)関数**
    - オフチェーンデータを検証し、クエリを満たすために実行されます。この関数は支払いを必要とする場合があります。

#### FeeRequiredエラー

```ts
error FeeRequired(uint amount)
```

- **FeeRequired(uint amount)**
    - `fulfillOracleQuery`関数を実行するために必要な手数料（ガス代）を指定します。
    - このエラーは、呼び出し側が十分な手数料（`msg.value`）を提供することで解決されます。

#### 要点

この標準は、オフチェーンデータを安全かつ効率的に扱うための明確なフレームワークを提供します。
スマートコントラクトは、必要なオフチェーンデータを指定し、それを検証・利用できるオラクルコントラクトにリクエストを投げることができます。
オラクルコントラクトは、データを検証し、必要な手数料を支払うことで、そのデータを提供します。
これにより、スマートコントラクトがオフチェーンデータにアクセスし、それを用いて操作を行うことが可能になります。

:::message
スマートコントラクトが外部のデータ（例えば価格情報）を必要とする際に、安全かつ効率的にそのデータを取得し、利用するための方法を定義しています。
:::

オフチェーンデータを必要とするスマートコントラクト操作において、クライアントがどのようにしてマルチコール（複数の関数呼び出しを一つのトランザクションで実行すること）を構築すべきかについて説明しています。

#### マルチコールの構築

- **クライアントの責任**
    - オフチェーンデータを検証する`fulfillOracleQuery`関数を、意図された関数呼び出しの前に、アトミックトランザクション内で実行するようにマルチコールを構築する責任がクライアントにあります。
- **アトミックトランザクション**
    - 複数の操作を1つのトランザクションで実行し、すべてが成功するか、1つでも失敗した場合はすべてがロールバックされることを保証します。

#### アカウント抽象化とEOAサポート

- **アカウント抽象化**
    - **ERC4337**に従うウォレットは、アトミックマルチオペレーションを生成する能力をすでに持っているべきです。

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

[\[ERC4337\] プロトコルの変更をせずにAAを実装する仕組みを理解しよう！](/articles/8c97ca7c93c4557ebfc2)

- **EOA（外部所有アカウント）サポート**
    - プロトコルは、EOAがこの標準をサポートするために**ERC2771**を実装することができます。

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

[\[ERC2771\] ガス代を他のアドレスに負担させるメタトランザクションの仕組みを理解しよう！](/articles/e84f53471b7a79240f33)

#### データの新鮮さの保持

- **OracleDataUsedイベントの発行**
    - データがリクエストと実行の間で古くなりすぎないように、コントラクトは`OracleDataUsed`イベントを発行することが理想的です。
    - このイベントには、`oracleContract`、`oracleQuery`、そしてデータが古くなるまでの`expirationTime`が含まれます。
- **実用性の欠如**
    - 現時点でのほとんどのJSON-RPC APIではトランザクションシミュレーション中にイベントデータを取得する実用的な方法がないため、この機能は標準から省略されています。

#### URIの使用

- **URIとしてのoracleId**
    - `oracleId`にURIを使用し、`oracleQuery`として特定のURIを指定することで、この標準は任意のオンチェーンURIに対応でき、クライアントライブラリの更新を必要としません。
    - これは**ERC3668**と同様のアプローチです。

:::message
オフチェーンデータを効率的に扱うためのマルチコールトランザクションの構築方法、データの新鮮さを保持するためのイベント発行の提案、およびURIを利用したオラクルIDの柔軟な指定方法について説明しています。
これらのメカニズムは、スマートコントラクトがオフチェーンデータに依存する操作をより効率的かつ安全に行うための基盤を提供します。
:::

## 補足

この提案は、既存の**ERC3668**といくつかの点で異なる代替案を提供しています。

### ERC3668との違い

#### オンチェーンでのURIエンコード
**ERC3668**はオンチェーンでURIをエンコードすることを要求しますが、これは静的なアセット（NFT関連のアセットやマークルツリーのIPFSハッシュなど）には適しています。
しかし、クロスチェーンデータの取得や価格フィードのように、最新のデータが必要な場合には理想的ではありません。
HTTP URLを用いることで動的なデータを参照することは可能ですが、これにより中央集権化や保守のリスクが高まります。

#### マルチコールの使用
この提案は、コールバックを使うのではなく、マルチコールに依存します。
これにより、異なるオフチェーンデータを必要とするネストされたコールを扱う場面が格段に簡単になります。
提案された標準により、エンドユーザー（アカウント抽象化を実装するクライアントを使用するユーザーを含む）は、実行されるコールの内部構造の複雑さに関わらず、トランザクションに単に署名するだけで済みます。
クライアントは、コールが成功するために必要なオフチェーンデータをトランザクションに自動的に追加することができます。

#### エラーの構築の簡易さ
この標準を実装する開発者は、選択したオラクルネットワーク、このネットワークが受け入れるクエリの形式、およびデータを取得する予定のコントラクトについてのみ認識する必要があります。

#### オラクルプロバイダーのスケーラビリティと互換性
この標準では、オラクルプロバイダーは無制限の数のネットワークをスケーラブルにサポートできるだけでなく、プロトコル開発のためのローカル/フォークされたネットワークとも互換性があります。

#### 追加の利点

オラクルは、オンチェーンでのデータ検証中にネイティブガストークンの形で手数料を請求することができます。
これにより、データの消費者から手数料を集め、分散型オラクルネットワークのノード運営者に提供する経済的インセンティブが生まれます。

この提案は、**ERC3668**に対する洗練された代替案を提供し、オフチェーンデータを必要とするスマートコントラクトの実装と運用をより柔軟で効率的にすることを目的としています。

## 実装

### prepareTransaction関数

```ts
function prepareTransaction(originalTx) {
  let multicallTx = [originalTx];
  while (true) {
    try {
      const simulationResult = simulateTx(multicallTx);
      return multicallTx;
    } catch (error) {
      if (error instanceof OracleDataRequired) {
        const signedRequiredData = fetchOffchainData(
          error.oracleContract,
          error.oracleQuery
        );
        const dataVerificationTx = generateDataVerificationTx(
          error.oracleContract,
          signedRequiredData
        );
        multicallTx.unshift(dataVerificationTx);
      }
    }
  }
}
```

この疑似コードは、クライアントSDKの簡略化されたバージョンを示しています。
理想的には、この機能はウォレットに実装されることが想定されていますが、アプリケーション層に組み込むことも可能です。
この関数は、望ましいトランザクションを受け取り、それをマルチコールに変換します。
このマルチコールは、`OracleDataRequired`エラーを回避するために必要なデータ検証トランザクションを先頭に追加したものです。

#### 機能の説明

- **prepareTransaction関数**
    - この関数は、元のトランザクション(`originalTx`)を引数として受け取ります。
- **multicallTxの初期化**
    - まず、元のトランザクションを含むマルチコールトランザクション配列`multicallTx`を作成します。
- **無限ループの開始**
    - トランザクションのシミュレーションを試み、必要なオフチェーンデータを取得するまでループします。
    - **シミュレーションの実行**
        - `simulateTx`関数を用いて`multicallTx`のシミュレーションを行います。
        - シミュレーションが成功すれば、`multicallTx`を返して終了します。
    - **エラー処理**
        - シミュレーション中に`OracleDataRequired`エラーが発生した場合、エラーからオラクルコントラクトのアドレス(`error.oracleContract`)と必要なクエリ(`error.oracleQuery`)を取得します。
- **オフチェーンデータの取得**
    - `fetchOffchainData`関数を用いて、必要なオフチェーンデータを取得します。
- **データ検証トランザクションの生成**
    - `generateDataVerificationTx`関数を用いて、取得したオフチェーンデータを検証するトランザクションを生成します。
- **マルチコールトランザクションへの追加**
    - 生成したデータ検証トランザクションを`multicallTx`配列の先頭に追加します。
- **ループの継続**
    - ループを続け、`multicallTx`が成功するまで上記のプロセスを繰り返します。

#### 目的と利点

このプロセスの目的は、スマートコントラクトが必要とするオフチェーンデータを効率的に取得し、そのデータを検証することで`OracleDataRequired`エラーを事前に回避し、トランザクションが成功するようにすることです。
この方法により、エンドユーザーは複雑なデータ依存性を意識することなく、スムーズにトランザクションを行うことができます。

### OracleContract

```ts
contract OracleContract is IERC7412 {
  address public constant VERIFIER_CONTRACT = 0x0000;
  uint public constant STALENESS_TOLERANCE = 86400; // One day
  mapping(bytes32 => bytes) public latestVerifiedData;

  function oracleId() external pure returns (bytes32){
    return bytes32(abi.encodePacked("MY_ORACLE_ID"));
  }

  function fulfillOracleQuery(bytes calldata signedOffchainData) payable external {
    bytes memory oracleQuery = _verify(signedOffchainData);
    latestVerifiedData[keccak256(oracleQuery)] = signedOffchainData;
  }

  function retrieveCrossChainData(uint chainId, address contractAddress, bytes payload) internal returns (bytes) {
    bytes memory oracleQuery = abi.encode(chainId, contractAddress, payload);
    (uint timestamp, bytes response) = abi.decode(latestVerifiedData[oracleQuery], (uint, bytes));

    if(timestamp < block.timestamp - STALENESS_TOLERANCE){
      revert OracleDataRequired(address(this), oracleQuery);
    }

    return response;
  }

  function _verify(bytes memory signedOffchainData) payable internal returns (bytes oracleQuery) {
    // Insert verification code here
    // This may revert with error FeeRequired(uint amount)
  }

}
```

このコードは、オフチェーンデータをリクエストし、検証し、格納するためのスマートコントラクトの例を示しています。
このコントラクトは`IERC7412`インターフェースを実装しており、オラクルプロバイダーが使用することができます。

#### コントラクトの変数と機能

- **VERIFIER_CONTRACT**
    - 検証コントラクトのアドレスを定義していますが、この例では仮のアドレス`0x0000`が使用されています。
- **STALENESS_TOLERANCE**
    - 最新データの範囲を定義しています。
    - この例では、データが24時間（86400秒）より古い場合、古いと見なされます。
- **latestVerifiedData**
    - 最新の検証済みオフチェーンデータを格納するマッピングです。
    - キーはクエリのハッシュ値、値は署名されたオフチェーンデータです。

#### インターフェースの実装

- **oracleId関数**
    - オラクルの一意のIDを返します。
    - このIDは、オラクルプロバイダーによって固有に定義されます。
- **fulfillOracleQuery関数**
    - オフチェーンデータを受け取り、検証し、`latestVerifiedData`マッピングに格納します。
    - この関数は、署名されたオフチェーンデータを引数として受け取り、そのデータのクエリを検証します。

#### クロスチェーンデータの取得

- **retrieveCrossChainData関数**
    - 特定のチェーンID、コントラクトアドレス、およびペイロードに基づいてクロスチェーンデータを取得します。
    - この関数は、検証済みデータが新鮮であるかどうかをチェックし、古い場合は`OracleDataRequired`エラーを発生させます。
    - 新しいデータがある場合は、そのデータを返します。

#### データの検証

- **_verify関数**
    - 署名されたオフチェーンデータを検証するための内部関数です。
    - この関数は検証プロセスを実行し、検証に成功した場合にクエリを返します。
    - 検証に費用がかかる場合、`FeeRequired`エラーを発生させることがあります。

:::message
このコントラクトは、オフチェーンデータの取得、検証、および使用を自動化するための方法を提供します。
クロスチェーンデータや価格フィードなどの最新データを安全に取得することが重要なアプリケーションで役立ちます。
この機構により、スマートコントラクトは最新のデータに基づいて正確に動作することが保証されます。
:::

### ICrosschainContract

```ts
interface ICrosschainContract {
  function functionA(uint x) external returns (uint y);
  function functionB(uint x) external returns (uint y);
}

contract CrosschainAdder {
  IERC7412 oracleContract = 0x0000;

  function add(uint chainIdA, address contractAddressA, uint chainIdB, address contractAddressB) external returns (uint sum){
    sum = abi.decode(oracleContract.retrieveCrossChainData(chainIdA, contractAddressA, abi.encodeWithSelector(ICrosschainContract.functionA.selector,1)), (uint)) + abi.decode(oracleContract.retrieveCrossChainData(chainIdB, contractAddressB, abi.encodeWithSelector(ICrosschainContract.functionB.selector,2)),(uint));
  }
}
```

このコード例では、クロスチェーンデータを利用するトップレベルのプロトコルスマートコントラクトの実装方法を説明しています。
具体的には、異なるブロックチェーン上に存在する2つのコントラクトからデータを取得し、それらのデータを加算する`CrosschainAdder`というコントラクトを示しています。

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

`functionA`と`functionB`という2つの関数を持つインターフェースです。
これらは外部から呼び出すことができ、`uint`型の引数を取り、`uint`型の値を返します。

#### CrosschainAdderコントラクト

`IERC7412`オラクルコントラクトのインスタンスを参照します。
このオラクルコントラクトは、オフチェーンデータの取得と検証を行う機能を提供します。

#### add関数

`add`関数は、2つのチェーンID（`chainIdA`と`chainIdB`）、およびそれぞれのチェーン上のコントラクトアドレス（`contractAddressA`と`contractAddressB`）を引数として受け取ります。
`oracleContract.retrieveCrossChainData`関数を呼び出し、指定されたチェーンとコントラクトアドレスからデータを取得します。
この時、`abi.encodeWithSelector`を用いて、対象の関数セレクタと引数をエンコードします。
取得したデータ（`functionA`と`functionB`からの戻り値）をデコードし、加算します。
この合計値が`add`関数の戻り値となります。

#### 開発者の観点

`CrosschainAdder`関数の開発者は、オフチェーンデータを検証し取得する標準の実装について心配する必要はありません。
`oracleContract`を通じてデータを取得するプロセスは、通常のオンチェーンデータの取得と同様に簡単に行えます。

#### 利用シナリオ

このようなクロスチェーン関数は、オンチェーンでの**O(n)**以上のループを回避するためにも利用できます。
例えば、`chainIdA`と`chainIdB`が同じチェーンを参照し、`functionA`と`functionB`が計算量の多いループを含む`view`関数である場合、計算をオフチェーンで効率的に実行し、結果だけをオンチェーンで取り扱うことができます。

このコード例は、オラクルを介してクロスチェーンデータを取得し、それを利用するスマートコントラクトの構築方法を示しており、ブロックチェーン間でのデータの効率的な取引と処理を実現する方法を提供しています。

## セキュリティ

オフチェーンデータを扱うための提案された標準（マルチコールを使用する方法を含む）に関連する潜在的なリスクと責任について説明しています。

### マルチコールに関連するリスク

#### トランザクションデータの不透明性
マルチコールの依存は、特により複雑なトランザクションのデコード機能を持たないウォレットアプリケーションにおいて、トランザクションデータを不透明にする可能性があります。
これは、トランザクションが何をしているのかをユーザーが理解しにくくすることがあります。

#### ウォレット開発者の挑戦
マルチコールはこの標準に限らずプロトコル開発でますます一般的になってきており、ウォレットアプリケーション開発者はこの課題に対処しています。
トランザクションの透明性を高めるために、より洗練されたトランザクションデコード機能の開発が求められます。

### データの検証と責任

#### 検証コントラクトの責任
この標準では、オラクルネットワークから提供されるデータの有効性を確認する責任は、検証コントラクト（verifier contract）にあります。
つまり、データがスマートコントラクトに提供される前に、そのデータが正しいかどうかを確認するのは検証コントラクトの役割です。

#### 無効なデータの新たなリスクはない
この標準は、スマートコントラクトに無効なデータが提供される新たな機会を生み出すものではありません。
すでに存在するオラクルネットワークからのデータ提供のプロセスにおいて、正確さと信頼性を保証するための追加のメカニズムを提供します。

:::message
この提案された標準は、オフチェーンデータを扱う際に潜在的なリスクと責任を認識しつつ、スマートコントラクトの機能とスケーラビリティを向上させる方法を提供します。
ウォレットアプリケーションの進化とともに、ユーザーがトランザクションの詳細をよりよく理解できるようになることが期待されます。
また、データの正確性を保証するプロセスは、引き続き重要な役割を果たします。
:::

## 引用

Noah Litvin (@noahlitvin), db (@dbeal-eth), "ERC-7412: On-Demand Off-Chain Data Retrieval [DRAFT]," Ethereum Improvement Proposals, no. 7412, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7412.

## 最後に

今回は「複数のブロックチェーンやオラクルネットワーク間でマルチコールを利用して、安全かつ効率的にやりとりする仕組みを提案している**ERC7412**」についてまとめてきました！
いかがだったでしょうか？

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