はじめに

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

今回は、コントラクト本体とは独立して実行用スクリプトのURIをオンチェーンで登録・取得できる仕組みを提案しているERC7738についてまとめていきます!

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

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

概要

ERC7738は、任意のスマートコントラクトアドレスに対して、対応する実行可能スクリプトのURI(インターネット上の取得先アドレス)を登録・取得できるレジストリコントラクトを定義しています。

このレジストリは以下の特徴を持ちます。

まず、1つの「グローバルなレジストリコントラクト」として設計されます。 これはマルチチェーン環境を前提としており、Ethereum、L2、サイドチェーンなど複数のチェーンにおいて、まったく同じアドレスに同じコードのコントラクトがデプロイされます。 これを「マルチチェーン・シングルトン(全チェーンで同一アドレスに存在する単一実体)」と呼びます。

次に、レジストリは「コントラクトアドレス → スクリプトのURI一覧」という対応表を管理します。 ウォレット、NFT Viewer、ミニDAppなどのクライアントは、対象トークンやコントラクトのアドレスをこのレジストリに問い合わせることで、関連するスクリプトの取得先を知ることができます。

スクリプトの登録は、特定の管理者に限定されません。 レジストリにはパーミッションレス(誰でも実行可能)な setter 関数が用意されており、任意のアドレスに対してスクリプトのURIを登録できます。 これにより、後からUI拡張やツール連携を追加したい場合でも、元のコントラクトを改修せずに機能拡張が可能になります。

まとめると、ERC7738は以下を実現します。

項目 内容
管理方式 スクリプト参照先を共通レジストリで一元管理します。
対象 任意のスマートコントラクトアドレスに紐づけ可能です。
取得方法 ウォレットやDAppがURIを取得し、スクリプトをダウンロードして実行します。
設置形態 複数チェーンに同一アドレスで配置されるシングルトン構成です。
登録権限 誰でも登録できるパーミッションレス設計です。

この仕組みによって、トークンやNFTの「振る舞い」や「表示ロジック」を、オンチェーンのコントラクト本体とは独立して柔軟に進化させられるようになります。

動機

ERC7738が必要とされる背景には、既存の仕組みであるERC5169の制約があります。 ERC5169は、スマートコントラクトに対応する「クライアント用スクリプト(ウォレットやUIで実行されるJavaScriptなどのプログラム)」の場所を取得するための標準です。 これにより、トークンやNFTに専用の表示ロジックや操作ロジックを紐づけることができます。

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

しかし ERC5169には重要な制限があります。

1つ目は、コントラクト自身がERC5169のインターフェース(外部から呼び出せる関数の定義)を実装する必要があります。 つまり、コントラクトをデプロイ(ブロックチェーン上に配置)した時点で、その仕様を組み込んでいない場合、後から簡単に対応させることができません。 アップグレード可能な設計にしていない限り、将来的にスクリプト連携を追加したくなっても不可能になります。

2つ目は、スクリプトの管理主体が各コントラクトに分散してしまう点です。 それぞれのコントラクトが独自にスクリプトの参照先を持つため、ウォレットやViewer側は、チェーンごと・コントラクトごとに個別対応をする必要があります。

ERC7738はこの問題を解決するために、スクリプトの所在を「共通のレジストリ(登録簿)」で管理する仕組みを提供します。 これにより、

  • 既存コントラクトを変更せずにスクリプトを紐づけられる
  • チェーンをまたいでも同じ仕組みで参照できる
  • 信頼済みスクリプト(certified)と試験的スクリプト(`prototype)を区別して配布できる

という構成が可能になります。

ここでいう「prototypeスクリプト」とは実験的・試作段階のプログラムを指し、「certifiedスクリプト」とは監査や検証を経た正式利用向けのプログラムを意味します。

仕様

必須要件と推奨要件

ERC7738のレジストリコントラクトには以下の振る舞いが求められます。

まず、このコントラクトはIERC7738インターフェースを実装する必要があります。 これは、ウォレットやDAppが共通の方法でスクリプト情報を取得できるようにするためです。

次に、スクリプトが更新されたときには、必ず ScriptUpdate イベントを発行する必要があります。 これにより、ウォレットはキャッシュしているスクリプトを自動的に更新できます。

また、scriptURI の戻り値の並び順についても推奨事項があります。 対象コントラクトのERC173の owner() が登録したスクリプトを先頭に並べることが望ましいとされています。 多くのウォレットは「最初に返ってきたURI」を採用するため、正規の管理者が設定したスクリプトを優先的に扱えるようにする意図です。

さらに、登録されるスクリプトが大量になる場合に備え、ページング(分割取得)の仕組みを用意することも推奨されています。 これは、1回の呼び出しで大量のURIを返すとガスコストやレスポンスサイズが問題になるためです。

インターフェース定義

以下がERC7738で定義されるインターフェースです。

js
interface IERC7738 {
    /// @dev This event emits when the scriptURI is updated, 
    /// so wallets implementing this interface can update a cached script
    event ScriptUpdate(address indexed contractAddress, string[] newScriptURI);

    /// @notice Get the scriptURI for the contract
    /// @return The scriptURI
    function scriptURI(address contractAddress) external view returns (string[] memory);

    /// @notice Update the scriptURI 
    /// emits event ScriptUpdate(address indexed contractAddress, scriptURI memory newScriptURI);
    function setScriptURI(address contractAddress, string[] memory scriptURIList) external;
}
ScriptUpdate イベント

ScriptUpdate は、あるコントラクトアドレスに紐づくスクリプトのURIが更新されたときに発行されるイベントです。 contractAddress には対象となるコントラクトのアドレスが入り、newScriptURI には新しく設定されたスクリプトのURI配列が渡されます。

ウォレットやViewerはこのイベントを監視することで、キャッシュしているスクリプト情報が変更されたことを検知できます。 その結果、トークンの表示ロジックや操作用の実行コードを自動的に更新し、常に最新の挙動を反映したUIを提供できるようになります。

scriptURI 関数

scriptURI 関数は、指定したコントラクトアドレスに関連付けられているスクリプトのURI一覧を取得するための参照用関数です。 返り値は文字列配列で、複数のスクリプトの取得先を同時に返すことができます。

これにより、1つのトークンやNFTに対して、表示用、操作用、検証用など、役割の異なる複数の実行スクリプトを関連付けることが可能になります。 ウォレットやミニDAppは、この配列の中から適切なものを選択して実行します。

setScriptURI 関数

setScriptURI 関数は、指定したコントラクトアドレスに対して、新しいスクリプトURIの配列を登録・更新するための関数です。 この関数が呼び出されると、必ず ScriptUpdate イベントが発行され、スクリプト情報が変更されたことがオンチェーンで通知されます。

ERC7738はパーミッションレスな設計のため、特定の管理者に限らず誰でもこの関数を呼び出して登録できます。 ただし、ウォレット側では無条件にすべてを採用するのではなく、ERC173の owner() が登録したものを優先するなど、信頼性を考慮した並び順や選択ルールに基づいて、実際に使用するスクリプトを決定することが想定されています。

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

補足

ERC7738がこの方式を採用している理由は大きく2つあります。

1つ目は、ERC5169を実装していない既存コントラクトでも、後付けでスクリプトを関連付けられる点です。 コントラクト本体を変更せず、外部レジストリにURIを登録するだけで、ウォレットやDAppに専用UIやロジックを提供できます。

2つ目は、中央集権的なサーバーを不要にする点です。 従来であれば「このトークンのUIはこのURL」という情報を、特定の企業や運営組織が管理するデータベースに置く必要がありました。 この場合以下のリスクが発生します。

  • サーバーの改ざんリスク
  • サービス停止リスク
  • 管理組織が実質的なゲートキーパーになる問題

ERC7738では、スクリプトの参照先をブロックチェーン上のレジストリコントラクトに直接保存します。 これにより、特定組織に依存せず、検証可能で改ざん耐性のある形でスクリプトの発見と配布が可能になります。

結果として、トークンやNFTの「振る舞い」や「表示ロジック」を、オンチェーンの信頼性と分散性を保ったまま拡張できる設計になっています。

参考実装

DecentralisedRegistry.sol
import "@openzeppelin/contracts/access/Ownable.sol";

contract DecentralisedRegistry is IERC7738 {
    struct ScriptEntry {
        mapping(address => string[]) scriptURIs;
        address[] addrList;
    }

    mapping(address => ScriptEntry) private _scriptURIs;

    function setScriptURI(
        address contractAddress,
        string[] memory scriptURIList
    ) public {
        require (scriptURIList.length > 0, "> 0 entries required in scriptURIList");
        bool isOwnerOrExistingEntry = Ownable(contractAddress).owner() == msg.sender 
            || _scriptURIs[contractAddress].scriptURIs[msg.sender].length > 0;
        _scriptURIs[contractAddress].scriptURIs[msg.sender] = scriptURIList;
        if (!isOwnerOrExistingEntry) {
            _scriptURIs[contractAddress].addrList.push(msg.sender);
        }
        
        emit ScriptUpdate(contractAddress, msg.sender, scriptURIList);
    }

    // Return the list of scriptURI for this contract.
    // Order the return list so `Owner()` assigned scripts are first in the list
    function scriptURI(
        address contractAddress
    ) public view returns (string[] memory) {
        //build scriptURI return list, owner first
        address contractOwner = Ownable(contractAddress).owner();
        address[] memory addrList = _scriptURIs[contractAddress].addrList;
        uint256 i;

        //now calculate list length
        uint256 listLen = _scriptURIs[contractAddress].scriptURIs[contractOwner].length;
        for (i = 0; i < addrList.length; i++) {
            listLen += _scriptURIs[contractAddress].scriptURIs[addrList[i]].length;
        }

        string[] memory ownerScripts = new string[](listLen);

        // Add owner scripts
        uint256 scriptIndex = _addScriptURIs(contractOwner, contractAddress, ownerScripts, 0);

        // Add remainder scripts
        for (uint256 i = 0; i < addrList.length; i++) {
            scriptIndex = _addScriptURIs(addrList[i], contractAddress, ownerScripts, scriptIndex);
        }

        return ownerScripts;
    }

    function _addScriptURIs(
        address user,
        address contractAddress,
        string[] memory ownerScripts,
        uint256 scriptIndex
    ) internal view returns (uint256) {
        for (uint256 j = 0; j < _scriptURIs[contractAddress].scriptURIs[user].length; j++) {
            string memory thisScriptURI = _scriptURIs[contractAddress].scriptURIs[user][j];
            if (bytes(thisScriptURI).length > 0) {
                ownerScripts[scriptIndex++] = thisScriptURI;
            }
        }
        return scriptIndex;
    }
}

セキュリティ

スクリプトの認証方法

対象コントラクトがERC173(Ownable)を実装している場合

対象コントラクトがERC173の owner() を持っている場合、比較的わかりやすく認証できます。 レジストリからスクリプトを取得した後で、そのスクリプトに付いた署名が owner() のアドレスに対応しているかを検証します。 一致すれば「コントラクト所有者が認めたスクリプト」と判断できます。

TokenScriptを使う場合は、認証に使える手段がいくつか提示されています。 どれも「署名が正しいか」を確認する点は同じです。

手段 何をするか 誰がやるか
TokenScript SDK TokenScriptの署名検証ロジックで正当性を確認します。 ウォレット / DApp
TokenScriptオンライン検証サービス 外部サービスに検証を委ね、正当性を確認します。 ウォレット / DApp
XMLから署名を取り出して検証 XMLから署名を取り出し、スクリプトのハッシュを計算して署名者を復元し、owner() と一致するか見ます。 ウォレット / DApp
sequenceDiagram
  participant R as ERC7738レジストリ
  participant W as ウォレット/DApp
  participant C as 対象コントラクト(ERC173)
  participant S as スクリプト(例: TokenScript)

  W->>R: scriptURI(contractAddress)
  R-->>W: URI配列
  W->>S: URIからスクリプト取得
  W->>S: XMLから署名を抽出
  W->>S: スクリプト本体をkeccak256でハッシュ化
  W->>S: ecrecoverで署名者アドレスを復元
  W->>C: owner()
  C-->>W: オーナーアドレス
  W-->>W: 署名者 == owner() を確認
  • keccak256 Ethereumで標準的に使われるハッシュ関数です。データを固定長の値に変換します。
  • ecrecover 署名とメッセージ(ここではハッシュ)から、署名者のアドレスを復元する仕組みです。

対象コントラクトが Ownable(ERC173)を実装していない場合

対象コントラクトに owner() がない場合でも、認証のやり方は残されています。

デプロイ時の鍵(デプロイヤー)を推定して照合する

ウォレットやDAppが、第三者APIやブロックエクスプローラ(例:Etherscanのような取引履歴閲覧サービス)を使って「そのコントラクトをデプロイしたアドレス」を調べます。 そして、スクリプトの署名者がそのデプロイアドレスと一致するかを確認します。

ここでいう「デプロイ鍵」は、厳密には秘密鍵そのものではなく、デプロイに使われたアドレス(署名検証で突き合わせる公開側)を指しています。

ホスティング側が事前認証済みの鍵束(キーchain)を埋め込む

ウォレットやアプリが「信頼する署名者の公開鍵(アドレス)一覧」を内部に持ち、署名者がその一覧に含まれているかで判定します。 「embedded keychain」は、OSのキーチェーンではなく、アプリ内に組み込んだ信頼済み鍵のリストという意味です。

ガバナンストークンで「スクリプト評議会」を作り、鍵の正当性を承認する

ガバナンストークンを使い、スクリプトの登録要求や鍵の検証を承認する主体(script council)を設ける案です。 これは「owner() がないなら、コミュニティや組織的な意思決定で正当性を担保する」という発想です。

認証条件を満たせない場合の扱い

上記の認証ができない、または満たしていない場合の方針も書かれています。 ここはチェーン環境によって扱いが分かれます。

環境 基本方針 最終判断
メインネット(本番) ウォレットはスクリプト利用に慎重であるべきです。 アプリ提供者やユーザーの裁量です。
テストネット 動かしてよいとされます。 ウォレット提供者の裁量です。

メインネットでは実害(資産の損失や詐欺誘導)が起き得るため、認証できないスクリプトを安易に実行しない姿勢が求められています。 一方、テストネットは検証・開発用途なので、多少リスクを許容して動作確認を優先してよい、という整理です。

引用

Victor Zhang (@zhangzhongnan928), James Brown (@JamesSmartCell), "ERC-7738: Permissionless Script Registry [DRAFT]," Ethereum Improvement Proposals, no. 7738, July 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7738.

最後に

今回は「コントラクト本体とは独立して実行用スクリプトのURIをオンチェーンで登録・取得できる仕組みを提案しているERC7738」についてまとめてきました! いかがだったでしょうか?

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

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