はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、オンチェーンに全てのデータを置かずに、コントラクトが外部のデータを安全に取得・検証できる仕組みを提案しているERC3668についてまとめていきます!

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIPについてまとめています。

概要

ERC3668は、スマートコントラクトが外部(オフチェーン)に保存されたデータを取得するための共通仕様です。 コントラクトはデータを直接返すのではなく、OffchainLookup というエラーを発生させます。 このエラーには、外部リクエスト先のURLや呼び出しデータ、コールバック関数などが含まれています。 クライアントはこの情報をもとに、指定されたURLへリクエストを送り、応答を受け取ります。 その後、コントラクトに定義されたコールバック関数を呼び出して応答データを渡します。 コントラクトは受け取ったデータを検証し、正しいかどうかを確認します。

この仕組みによって、クライアントは外部データを透過的に扱えるようになり、コントラクト開発者は自由に検証方法を設計できます。 多くの場合、オンチェーンにすべてのデータを保存するのと比べて、新たな信頼関係を増やすことなくより効率的にデータを利用できます。

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

動機

Ethereum上でのストレージやトランザクションコストの高さから、開発者はデータをオフチェーンに移すさまざまな工夫を行ってきました。 例えば、ハッシュ値やマークルツリーと呼ばれる階層的なハッシュ構造を用いる方法、またはL2(レイヤー2)ソリューションを使う方法です。 これらはいずれもオンチェーンに「検証に必要な最小限の情報」だけを残し、必要なときに外部データと照合する仕組みです。

しかし、これまではプロジェクトごとに独自の仕組みを作っており、共通の規格は存在しませんでした。 そのため、1つのアプリケーションでは成り立っても、利用者や他の開発者が異なる仕組みを使おうとすると互換性の問題が発生していました。

ERC3668を導入することで、クライアントはストレージ方法の違いを意識せずに外部データを扱えるようになります。 結果として、以下のような新しいユースケースが可能になります。

  • 外部に保存されたエアドロップ対象者リストを利用するコントラクトとのやり取り
  • L2に保存されたトークン情報をL1(Ethereum本体)のトークンのように表示すること
  • ENSドメインなどのデータをL2に委譲しても、クライアントが個別対応しなくて済むこと
  • コントラクトが必要に応じて外部データを要求し、呼び出し元がその仕組みを知らなくても処理を進められること

このように、標準化された「オフチェーンデータ取得の仕組み」を定義することで、Ethereum上のアプリケーションはより柔軟で効率的に設計できるようになります。

仕様

概要

ERC3668は、スマートコントラクトが外部(オフチェーン)の情報を利用できるようにするための標準的な流れです。 CCIP Read は大きく3つのステップで構成されています。

ステップ1:コントラクトへの問い合わせ

まず、通常のブロックチェーンコールと同じように、クライアントがコントラクトの関数を呼び出します。 しかし、コントラクトは直接答えを返さず、OffchainLookup というエラーを返します。 このエラーの中には、以下の情報が含まれています。

  • 外部のゲートウェイサービスにアクセスするためのURL
  • 呼び出しデータ(callData)
  • コールバック関数名(callbackFunction)
  • 後で必要になる追加情報(extraData)

この時点で「このデータはオフチェーンにあるので、指定のゲートウェイに問い合わせてください」という指示が返ってきます。

ステップ2:ゲートウェイへの問い合わせ

クライアントは、ステップ1で得た情報を使ってゲートウェイにHTTPリクエストを送ります。 リクエストにはコントラクトが返した callData が含まれます。 ゲートウェイはそのリクエストを処理し、クライアントに応答を返します。 クライアントにとって、この応答の中身は解釈不要で「そのまま使うデータ」という扱いです。

ステップ3:コントラクトへの再呼び出し

次に、クライアントは再びコントラクトを呼び出します。 その時には、ステップ2で受け取った「ゲートウェイの応答」と、ステップ1でコントラクトから渡された extraData を一緒に引数として与えます。 コントラクトはこれらを受け取り、内部でデータの検証を行います。 検証に成功すれば情報を返すか、必要であればトランザクションの処理を進めます。

もしコントラクトがさらに別のデータを必要とする場合には、再度 OffchainLookup を返すこともあり、その場合は再びステップ1に戻って同じ流れが繰り返されます。

全体の処理フロー図

以下はクライアント・コントラクト・ゲートウェイ間のやり取りを示す図です。

text
┌──────┐                                          ┌────────┐ ┌─────────────┐
│Client│                                          │Contract│ │Gateway @ url│
└──┬───┘                                          └───┬────┘ └──────┬──────┘
   │                                                  │             │
   │ somefunc(...)                                    │             │
   ├─────────────────────────────────────────────────►│             │
   │                                                  │             │
   │ revert OffchainLookup(sender, urls, callData,    │             │
   │                     callbackFunction, extraData) │             │
   │◄─────────────────────────────────────────────────┤             │
   │                                                  │             │
   │ HTTP request (sender, callData)                  │             │
   ├──────────────────────────────────────────────────┼────────────►│
   │                                                  │             │
   │ Response (result)                                │             │
   │◄─────────────────────────────────────────────────┼─────────────┤
   │                                                  │             │
   │ callbackFunction(result, extraData)              │             │
   ├─────────────────────────────────────────────────►│             │
   │                                                  │             │
   │ answer                                           │             │
   │◄─────────────────────────────────────────────────┤             │
   │                                                  │             │

CCIP Read 対応コントラクト

CCIP Readを利用するコントラクトは、外部データが必要な関数を呼び出されたときに特定のエラー OffchainLookup を返す必要があります。 この仕組みによって、コントラクトはクライアントに「必要なデータはオフチェーンにあるので、指定の方法で取得してください」と伝えることができます。 ここでは、そのエラー仕様とコールバックの仕組みを解説します。

OffchainLookup エラーの定義

コントラクトは以下のようなエラーを必ず実装し、外部データが必要な関数で利用します。

javascript
error OffchainLookup(
    address sender,
    string[] urls,
    bytes callData,
    bytes4 callbackFunction,
    bytes extraData
);
  • sender エラーを返したコントラクト自身のアドレス。 これによって、呼び出されたコントラクト自身が返したエラーなのか、内部の別コントラクトから伝わったものかを識別できます。

  • urls データ取得を行うゲートウェイのURLリスト。 空リスト [] にすることもでき、その場合クライアントがURLを指定する必要があります。 複数URLがある場合は優先度順に返すことが推奨されます。 URL内では {sender} と {data} のプレースホルダを使え、実際のリクエスト前にそれぞれコントラクトアドレスと callData に置き換えられます。

  • callData ゲートウェイに送るリクエストデータ。 通常はABIエンコードされていますが、仕様はコントラクトとゲートウェイの間で決められます。

  • callbackFunction ゲートウェイの応答を受け取った後に呼び出す、コントラクト内の関数を指定する4バイトの関数セレクタです。

  • extraData コールバックに必要な追加情報で、クライアントはこれをそのまま渡す必要があります。

コールバック関数の実装

コントラクトはゲートウェイからの応答を検証するためのコールバック関数を必ず実装します。 この関数は以下のような形式を持ちます。

javascript
function callback(bytes response, bytes extraData) 
    public view returns (同じ戻り値型);

ここで重要なのは、戻り値の型が元の関数と一致している必要がある点です。 これにより、クライアントにとっては最初に呼んだ関数からそのまま結果が返ってきたように見えます。

また、コールバック関数の中から新たに OffchainLookup をエラーを返し、再び外部データを要求することも可能です。 ただし、再帰的な呼び出しの回数にはクライアントごとに制限がある点に注意が必要です。

実装例

以下は balanceOf 関数を例にした実装です。 この関数では、アドレスの残高情報がオフチェーンに保存されており、その証明をゲートウェイから取得して検証する流れを取ります。

javascript
function balanceOf(address addr) public view returns(uint balance) {
    revert OffchainLookup(
        address(this),
        [url],
        abi.encodeWithSelector(Gateway.getSignedBalance.selector, addr),
        ContractName.balanceOfWithProof.selector,
        abi.encode(addr)
    );
}

この場合、addr はゲートウェイにも必要であり、同時にコントラクト側で検証にも使うため、callData と extraData の両方に含めています。 もしゲートウェイからの応答に依存して addr を取得しようとすると、悪意のあるゲートウェイが異なるデータを返す危険があるためです。

CCIP Read における再帰的呼び出しの扱い

CCIP Readに対応したコントラクトが、別のコントラクトを呼び出すとき、その相手がCCIP Readを実装している可能性がある場合には注意が必要です。 特に「OffchainLookup エラー」がネストされた呼び出しでどのように伝播するかを正しく処理しないと、クライアント側が誤って処理を完了したと認識してしまう危険があります。

再帰呼び出し時の基本ルール

CCIP対応コントラクトが他のコントラクトを呼び出す場合、以下の点を守る必要があります。

  • 呼び出し先から OffchainLookup が返された場合、その sender が呼び出し先のアドレスと一致するかを確認する必要があります。 もし異なる場合には、InvalidOperation のような別のエラーで終了させる必要があります。
  • コントラクトは、呼び出し先からの OffchainLookup をそのままバブルアップ(伝播)させてはいけません。 なぜなら、伝播したエラーが一見正しいように見えてしまい、クライアントが誤って処理を受け入れる可能性があるためです。
  • 実装の選択肢として、ネストされたCCIP呼び出しを完全に禁止し、すべての OffchainLookup を別のエラーに置き換えることも可能です。 その場合、実装は簡単になりますが再帰的な CCIP 呼び出しはできなくなります。

「バブルアップ(bubble up)」とは、プログラムやシステムで発生したエラーや例外が、呼び出し元に自動的に伝わっていく挙動のことを指します。

SolidityやCCIP Readの文脈では、あるコントラクトが内部で別のコントラクトを呼び出し、その呼び出し先でエラー(例えば OffchainLookup)が返された場合に、そのエラーを特別に処理せずにそのまま呼び出し元に伝播させることを「バブルアップ」と言います。

不具合の例

もしネストされた呼び出しで OffchainLookup をそのままバブルアップさせると、以下のような誤動作が起こる可能性があります。

  1. コントラクトAがコントラクトBを呼び出す。
  2. Bが再度Aを呼び出す。
  3. ネストされた呼び出しの中でAが OffchainLookup を返す。
  4. BはCCIPを理解しないため、そのままエラーを伝播する。
  5. Aもエラーを伝播する。
  6. クライアントから見ると「Aが直接 OffchainLookup を返した」ように見えるが、実際にはネストした呼び出しの途中で生じたものであり、正しく処理できない。

実装例

以下は、ネストされたCCIP Read呼び出しを正しく処理する一例です。 ここではSolidityの try/catch を使っています(現時点ではカスタムエラーのキャッチは正式にサポートされていませんが、概念を示すものです)。

javascript
contract NestedLookup {
    error InvalidOperation();
    error OffchainLookup(
        address sender,
        string[] urls,
        bytes callData,
        bytes4 callbackFunction,
        bytes extraData
    );

    function a(bytes calldata data) external view returns(bytes memory) {
        try target.b(data) returns (bytes memory ret) {
            return ret;
        } catch OffchainLookup(
            address sender,
            string[] urls,
            bytes callData,
            bytes4 callbackFunction,
            bytes extraData
        ) {
            if(sender != address(target)) {
                revert InvalidOperation();
            }
            revert OffchainLookup(
                address(this),
                urls,
                callData,
                NestedLookup.aCallback.selector,
                abi.encode(address(target), callbackFunction, extraData)
            );
        }
    }

    function aCallback(bytes calldata response, bytes calldata extraData)
        external view returns(bytes memory)
    {
        (address inner, bytes4 innerCallbackFunction, bytes memory innerExtraData) =
            abi.decode(extraData, (address, bytes4, bytes));

        return abi.decode(
            inner.call(
                abi.encodeWithSelector(innerCallbackFunction, response, innerExtraData)
            ),
            (bytes)
        );
    }
}
  • try/catch を使って呼び出し先からの OffchainLookup を補足しています。
  • sender が呼び出し先(target)であることを確認し、不正な場合は InvalidOperation を返しています。
  • もし正しい場合は、自分自身の OffchainLookup に変換し直して返しています。 ここで extraData に「呼び出し先アドレス」、「元のコールバック関数」、「追加データ」をまとめて保持します。
  • aCallback 関数で、保持した extraData を使って元のコールバックを正しく実行できるようにしています。

Gatewayインターフェース

CCIP Readの仕組みでは、コントラクトが OffchainLookup を返す時に指定するURLを通じて外部ゲートウェイからデータを取得します。 このゲートウェイとの通信にはルールが定められており、クライアントとゲートウェイのやり取りが標準化されています。

URL テンプレートとリクエスト形式

コントラクトが返すURLには、{sender} と {data} というプレースホルダが含まれる場合があります。

  • {sender} はコントラクトアドレス(小文字の16進数、0x付き)に置き換えられます。
  • {data} は callData(16進数で表現されたバイト列、0x付き)に置き換えられます。

この置き換えにより最終的なリクエストURLが構成されます。

例)

text
urls = ["https://example.com/gateway/{sender}/{data}.json"]
sender = "0xaabbccddeeaabbccddeeaabbccddeeaabbccddee"
callData = "0x00112233"

生成されるリクエストURLは以下です。

text
https://example.com/gateway/0xaabbccddeeaabbccddeeaabbccddeeaabbccddee/0x00112233.json

GET と POST の使い分け

  • URLに {data} が含まれる場合、クライアントはGET リクエストを送ります。
  • URLに {data} が含まれない場合、クライアントはPOST リクエストを送ります。

POST の場合、リクエストは必ず application/json 形式で送られ、以下のようなJSONボディを持ちます。

json
{
  "data": "0x-prefixed hex string containing the callData from the contract",
  "sender": "0x-prefixed hex string containing the sender parameter from the contract"
}

ゲートウェイからのレスポンス仕様

ゲートウェイが正常に応答する場合、レスポンスは application/json 形式で、以下のような構造を持ちます。

json
{
  "data": "0x-prefixed hex string containing the result data."
}

失敗した場合は適切なHTTPステータスコードを返し、application/json の場合は以下のようなエラーメッセージを含みます。

json
{
  "message": "A human-readable error message."
}

例えば、サポートされていないアドレスであれば 404、無効な callData であれば 400、サーバ内部の問題であれば 500 といった形です。

実際の例

GET リクエストの例
bash
# リクエスト
curl -D - https://example.com/gateway/0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8/0xd5fa2b00.json

# 正常な応答
HTTP/2 200
content-type: application/json; charset=UTF-8

{"data": "0xdeadbeefdecafbad"}

# エラー応答
HTTP/2 404
content-type: application/json; charset=UTF-8

{"message": "Gateway address not supported."}
POST リクエストの例
bash
# リクエスト
curl -D - -X POST -H "Content-Type: application/json" \
--data '{"data":"0xd5fa2b00","sender":"0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8"}' \
https://example.com/gateway/0x226159d592E2b063810a10Ebf6dcbADA94Ed68b8.json

# 正常な応答
HTTP/2 200
content-type: application/json; charset=UTF-8

{"data": "0xdeadbeefdecafbad"}

# エラー応答
HTTP/2 404
content-type: application/json; charset=UTF-8

{"message": "Gateway address not supported."}

Client Lookup Protocol

CCIP Readをサポートするクライアントは、通常のスマートコントラクト呼び出しに加えて「オフチェーンデータを参照する流れ」を正しく処理しなければなりません。 この仕様では、クライアントがコントラクトを呼び出す際に従うべき手順が定められています。

基本の流れ

まずクライアントは通常通り、対象のコントラクトアドレスと callData(呼び出しのためのデータ)を使ってコントラクトを呼び出します。 もし関数が通常通り成功して結果を返した場合、そのままユーザーに返して処理は終了します。 逆に、通常のエラーが返された場合もそのまま返します。

特殊なのは OffchainLookup エラーが返ってきた場合です。 この場合、コントラクトが「必要な情報はオフチェーンにあるので、こちらを参照してください」と指示を出していることを意味します。 クライアントはこのエラーから sender、urls、callData、callbackFunction、extraData を取り出して処理を続けます。

URL の生成と外部リクエスト

OffchainLookup で得られたURLテンプレートに、sender と callData を埋め込んでリクエスト先を決定します。 複数のURLが指定されている場合、基本的にはリストの先頭から優先して試みます。

クライアントはこのURLに対してHTTPリクエストを行います。 結果が 400 番台のエラーであれば処理を停止してユーザーにエラーを返します。 500 番台のエラーであれば別のURLに切り替えて再試行し、候補がなくなれば停止します。 200 番台で成功した場合、そのデータを取得します。

コールバックの実行

ゲートウェイからの応答が得られたら、クライアントはそれをもとに再度コントラクトを呼び出します。 具体的には、callbackFunction を指定して、ゲートウェイの応答データと extraData を引数として与えます。 コントラクトはこのデータを検証し、正しい結果を返すか、必要に応じて再び OffchainLookup エラーを返す場合もあります。 その場合、クライアントは再び同じ流れを繰り返します。

ただし無限に繰り返されないよう、クライアントは「最大試行回数」を設ける必要があります。 この仕様では少なくとも4回は許容すべきとされています。

擬似コード

仕様ではクライアントの動作を擬似コードで説明しています。

javascript
async function httpcall(urls, to, callData) {
    const args = {sender: to.toLowerCase(), data: callData.toLowerCase()};
    for(const url of urls) {
        const queryUrl = url.replace(/\{([^}]*)\}/g, (match, p1) => args[p1]);
        // GET または POST リクエスト
        const response = await fetch(queryUrl, url.includes('{data}') ? undefined : args);
        const result = await response.text();
        if(result.statusCode >= 400 && result.statusCode <= 499) {
            throw new Error(data.error.message);
        }
        if(result.statusCode >= 200 && result.statusCode <= 299) {
            return result;
        }
    }
}

async function durin_call(provider, to, data) {
    for(let i = 0; i < 4; i++) {
        try {
            return await provider.call(to, data);
        } catch(error) {
            if(error.code !== "CALL_EXCEPTION") {
                throw(error);
            }
            const {sender, urls, callData, callbackFunction, extraData} = error.data;
            if(sender !== to) {
                throw new Error("Cannot handle OffchainLookup raised inside nested call");
            }
            const result = httpcall(urls, to, callData);
            data = abi.encodeWithSelector(callbackFunction, result, extraData);
        }
    }
    throw new Error("Too many CCIP read redirects");
}

トランザクションにおける CCIP Read の利用

CCIP Readはもともと読み取り専用のコントラクト呼び出し(例: eth_call)で利用される仕様ですが、この仕組みはトランザクション送信(例: eth_sendTransaction や eth_sendRawTransaction)にも応用することができます。 これにより、オンチェーンとオフチェーンを組み合わせた高度なユースケースを実現できます。

トランザクション利用の流れ

通常、クライアントはトランザクションを送信する前に eth_estimateGas や eth_call を使って事前に「このトランザクションは正しく動作するか」をシミュレーションします。 この過程で OffchainLookup が返ってきた場合、クライアントは「Client Lookup Protocol」で説明されている手順を使って外部データを取得します。

違いは、最後のステップで「コントラクトへの再呼び出し」を通常の呼び出しではなく、実際のトランザクション送信として置き換える点です。 つまり、クライアントはゲートウェイから得たデータを使い、トランザクションを生成して送信します。

利用例

この方法は「オフチェーンの証明データを使ってオンチェーンで権利を主張する」といった用途に適しています。 例えば、あるユーザーが特定の条件を満たしていることを示す証明書がオフチェーンに保存されている場合、その証明をゲートウェイから取得し、それをトランザクションに組み込んでオンチェーンのスマートコントラクトに提出することができます。

用語

  • Client ブラウザ上の JavaScript やバックエンドサービスなど、ブロックチェーンに問い合わせを行う主体のこと。 CCIP Readを理解して外部データを取得する機能を持ちます。

  • Contract Ethereumなどのブロックチェーン上に存在するスマートコントラクト。

  • Gateway 特定のアプリケーションに対応したCCIP Readのリクエストに応答するサービスで、通常はHTTPSを介して動作します。

補足

Revertを使って呼び出し情報を伝える理由

オフチェーンデータ参照を実現するためには、クライアントが「この関数は外部データを参照する必要がある」と知る必要があります。 その方法としては、ABI(関数定義情報)に特別な指定を加えるという案もあります。 しかしその場合、既存の関数に対応させるのが難しくなり、同じ関数名で異なる戻り値型を持つ仕様が生まれてしまい、既存クライアントにとってはデコードエラーを引き起こす可能性があります。 また、既存関数を複製して新しい名前(例: balanceOf → offchainBalanceOf)で実装し直す必要があり、非効率的です。

そこで採用されたのが「revert(エラー)」を利用する方法です。 関数は通常通り定義しておき、必要な場合に OffchainLookup という特別なエラーを返すことで「外部データが必要だ」というシグナルをクライアントに送ります。 これにより、既存の関数をそのままオフチェーン対応に拡張でき、互換性を保ちながら新しい仕組みを取り込めます。

コントラクトアドレスをゲートウェイに渡す理由

ゲートウェイは特定のコントラクトだけでなく、同じインターフェースを持つ複数のコントラクトに対応できる方が望ましいです。 そのため、sender としてコントラクトのアドレスをゲートウェイに渡します。 これにより、ゲートウェイは必要に応じて元のコントラクトを参照し、追加情報を取得できるようになります。 結果として、1つの汎用ゲートウェイを複数のコントラクトで共有でき、開発者の負担が軽減されます。

extraData 引数が存在する理由

コントラクトは呼び出しのたびに状態をリセットするため、最初の呼び出し時の文脈(コンテキスト)を保持できません。 extraData はこの問題を解決するために設けられています。 最初の呼び出し時に追加情報を埋め込み、後続のコールバックでそのまま利用できるようにすることで、クエリが正しく対応しているかを確認できます。 これにより「ゲートウェイが別のリクエストに対する応答を返す」といった不正な動作を防ぎ、セキュリティを確保します。

GET と POST を使い分ける理由

ゲートウェイとの通信方法として GET と POST の両方が用意されています。

  • GET の利点 クエリデータをURLに含めるためシンプルで、静的ファイルサーバ(HTTPサーバや IPFS 上のファイル)でも対応できます。 このため、小規模でシンプルなユースケースでは GET が有効です。

  • 制約 URL は最大で約2KBまでしか扱えないため、大きなデータをやり取りする場合には不向きです。

そのため、より複雑なデータや大きなペイロードが必要な場合には POST を使用します。 POST ではJSONボディにデータを含めて送信できるため、柔軟に拡張できます。 こうした使い分けにより、シンプルさと柔軟性の両立が可能になっています。

互換性

CCIP Readの仕様は、既存のコントラクトやクライアントとの互換性を考慮して設計されています。 そのため、この仕組みを使いたいコントラクトと、従来通りの挙動を望むコントラクトの両方が同時に共存できます。

既存コントラクトへの影響

ERC3668を利用しないコントラクトは、何の影響も受けません。 従来通りの関数呼び出しやエラー処理が行われ、CCIP Readを理解しないクライアントであっても問題なく動作します。 つまり、仕様を採用するかどうかはコントラクト開発者の選択に委ねられています。

クライアント側の対応

クライアントがCCIP Readに対応する場合、すべてのコントラクト呼び出しにおいて追加のオーバーヘッドを生むことなく処理できます。 既存の呼び出しフローに対して互換性を保ちながら拡張する仕組みになっているため、非対応コントラクトと併用しても問題は生じません。

非対応クライアントとの関係

ただし、コントラクトがCCIP Readに依存している場合は、クライアントもこの仕様を理解している必要があります。 もし非対応のクライアントが呼び出した場合、コントラクトは OffchainLookup のエラーを返しますが、クライアントがそれを正しく処理できないため、最終的にユーザーに単なるエラーとして伝わってしまいます。 そのため、実際に利用する時には「コントラクトとクライアントの両方がCCIP Readに対応していること」が前提条件となります。

セキュリティ

ゲートウェイ応答データの検証

コントラクトは、悪意あるゲートウェイが不正な応答を返すことを防ぐため、extraData に十分な情報を含め、応答の妥当性を検証できるようにする必要があります。 例えば、ユーザーのアドレスに基づいて情報を要求する場合、そのアドレスを extraData に入れておくことで、コールバック関数で「返ってきたデータが正しいアドレスに対応しているか」を検証できます。

また、応答データそのものの検証も不可欠です。 これはアプリケーションごとに異なりますが、例としては以下のような方法があります。

  • L2やその他の状態管理におけるMerkle 証明の検証
  • 応答データに対する信頼できる署名者の署名検証

こうした手続きを導入することで、ゲートウェイの改ざんや虚偽応答を防げます。

クライアントが返す extraData の検証

トランザクションにCCIP Readを使う場合、クライアントが extraData を介して不正なデータを送り込む可能性があります。 そのため、コントラクトは初回の呼び出し時だけでなく、コールバック関数内でも同じ検証を行う必要があります。

例えば「特定のアドレスだけが実行可能なトランザクション」であれば、初回呼び出しで認可チェックを行うだけでなく、コールバックでも再度チェックする必要があります。 これを怠ると、悪意あるクライアントが extraData を改ざんして、権限のないアドレスで処理を実行できてしまう恐れがあります。

HTTPリクエストとフィンガープリント攻撃

CCIP Readではクライアント(ユーザーのブラウザなど)がゲートウェイに対して直接HTTPリクエストを送るため、プライバシー上のリスクがあります。 具体的には、ゲートウェイ運営者がユーザーのIPアドレスとウォレットアドレスを関連付ける可能性があります。 これを「フィンガープリント攻撃」と呼びます。

影響度はアプリケーションごとに異なります。 例えばENSドメインを解決する場合は大きな問題にならないかもしれませんが、NFT の Transfer トランザクションのようにユーザー特定につながるケースでは深刻です。

推奨される対策

このリスクを軽減するため、以下のような対策が推奨されています。

  • クライアントライブラリは、CCIP Readの呼び出しをプロキシ経由に書き換える機能や拒否する機能を提供し、ドメイン単位やコントラクト単位で制御できるようにする。
  • デフォルトでは、トランザクションに対してCCIP Readを無効化し、ユーザーが明示的に有効化するようにする。
  • アプリケーション開発者は、不確実なスマートコントラクトコードを実行する可能性がある場合、コントラクト呼び出しに from アドレスを渡さないようにする。
  • ウォレット開発者は、ユーザー情報を取得する時にCCIP Readを利用する場合、必ずプロキシを通す、あるいはトランザクション向けのCCIP Readを無効にすること。

これらの対策を守ることで、CCIP Readの利便性を活かしつつ、ユーザーのプライバシーやセキュリティを保護することができます。

引用

Nick Johnson (@arachnid), "ERC-3668: CCIP Read—Secure offchain data retrieval," Ethereum Improvement Proposals, no. 3668, July 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3668.

最後に

今回は「オンチェーンに全てのデータを置かずに、コントラクトが外部のデータを安全に取得・検証できる仕組みを提案しているERC3668」についてまとめてきました! いかがだったでしょうか?

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

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