はじめに

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

今回は、人間が読める意味のある文字列のkeccak256ハッシュを使ってコントラクトストレージ内の構造体の配置場所を定義する仕組みを提案しているERC8042についてまとめていきます!

以下にまとめられているものを解説しながらまとめていきます。

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

概要

ERC8042とは、ERC2535 Diamondsで初めて導入され広く採用されてきたDiamond Storageパターンを正式に標準化する規格です。

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

Diamond Storageは、コントラクトストレージ内で構造体の配置場所を決定する時に、人間が読める意味のある文字列のkeccak256ハッシュを使うという手法です。 もともとはプロキシコントラクト向けに考案されましたが、あらゆるスマートコントラクトでストレージの整理やデータアクセスの管理に活用できます。

ERC7201が汎用的なストレージ名前空間の仕組みを標準化した一方で、ERC8042はそれよりもシンプルで、既にプロダクション環境にデプロイされている多くのプロジェクトとの後方互換性を保つDiamond Storageの慣習を標準化するものです。

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

動機

この提案が生まれた背景には以下のような課題がありました。

Diamond Storageの形式化の必要性

2020年3月、Solidityコンパイラに構造体を任意のストレージ位置に割り当てる機能が追加されました。 この機能を活用して、ストレージの別々の領域を使い分ける新しいパターンが生まれました。 このパターンはERC2535 Diamondsによって初めて広く使われるようになり、Diamond Storageと呼ばれるようになりました。 しかし、Diamond Storage自体は正式な標準として文書化されていませんでした。 標準化されていない状態では、その仕組みや使い方の正確な仕様が曖昧なままであり、ツールやフレームワークがDiamond Storageを統一的に扱うことが困難でした。

ERC7201との計算式の違い

2023年6月にERC7201が導入され、ストレージ名前空間のパターンが標準化されました。 しかし、ERC7201が提案したストレージ位置の計算式は、Diamond Storageが既に確立して広く使っていた計算式とは異なるものでした。 Diamond Storageではkeccak256("文字列識別子")という単純な計算でストレージ位置を決定するのに対し、ERC7201はより複雑な計算式を採用しています。 この違いにより、既存のDiamond Storage実装とERC7201の間に互換性の問題が生じていました。

この提案による解決策

ERC8042はDiamond Storageを正式に標準化することで、新規プロジェクトでの採用と過去のDiamond Storage実装の検証の両方に対応します。 開発者はERC7201とERC8042のどちらを使うかを選択できます。 Diamond Storageのシンプルさ、ASCII文字に限定された識別子、直接的なハッシュベースのストレージ位置計算を好む開発者にとって、ERC8042は軽量な代替手段となります。 また、ツールがスマートコントラクトストレージ内のデータを検出しアクセスする時にも、この標準を活用できます。

仕様

Diamond Storageは、コントラクトストレージ内で構造体がどこに配置されるかを定義します。 ここでは、識別子の定義、ストレージ位置の計算方法、推奨事項、NatSpecタグについて解説します。

以下の図は、Diamond Storageの全体的な仕組みを示しています。

flowchart TD
    A["文字列識別子"] --> B["keccak256でハッシュ化"]
    B --> C["ストレージ位置 (bytes32)"]
    C --> D["構造体をこの位置に配置"]
    D --> E["getStorage()でポインタ取得"]
    E --> F["各関数からアクセス"]

Diamond Storage識別子

Diamond Storage識別子は、印刷可能なASCII文字のみを含む文字列として定義されます。 具体的には、文字コード0x20から0x7Eの範囲に含まれる文字のみが使用可能です。 この範囲には、アルファベット、数字、記号、スペースが含まれます。

ストレージ位置の計算

Diamond Storageにおける構造体のストレージ位置は、Diamond Storage識別子のkeccak256ハッシュによって決定されます。 Solidityでの記述は以下のようになります。

javascript
bytes32 constant STORAGE_POSITION = keccak256("myproject.erc721.registry");

このコードでは、文字列"myproject.erc721.registry"をkeccak256でハッシュ化し、その結果を構造体の格納位置として使用しています。 計算が非常にシンプルであることがDiamond Storageの特徴です。 ERC7201のように中間変換や追加の演算を必要とせず、識別子を直接ハッシュ化するだけでストレージ位置が確定します。

flowchart LR
    A["文字列識別子"] -->|keccak256| B["ストレージ位置 (bytes32)"]
    B -->|"assembly: s.slot := position"| C["構造体へのポインタ"]

推奨事項

ERC8042では、安全かつ効果的にDiamond Storageを使用するために以下のような推奨事項が示されています。

Solidityの文字列リテラルを使用する

識別子の作成にはSolidityの文字列リテラルを使うことが推奨されています。 文字列リテラルとは、ダブルクォートで囲んだASCII文字列のことで、例えば"this is a string literal"のような記述です。 Solidityコンパイラは文字列リテラルに対して印刷可能なASCII文字(0x20から0x7E)のみが含まれていることを強制的にチェックします。 そのため、文字列リテラルを使うことで仕様に準拠した識別子を安全に作成できます。

なお、16進エスケープシーケンス(\xNN)やUnicodeエスケープシーケンス(\uNNNN)はDiamond Storage識別子に使用してはいけません。 また、コンパイル時定数の文字列リテラルを使用することが推奨されています。

一意で人間が読める意味のある文字列を使用する

Diamond Storage識別子は、一意で人間が読め、かつ意味のある文字列にすることが推奨されています。 「人間が読める」とは、人間が通常読んで理解できる文字列のことです。 「意味のある」とは、ストレージ空間を適切に名前付けしたり説明したりする文字列、またはそのためのパターンを使用する文字列のことです。

例えば"myproject.erc721.registry"は、プロジェクト名・規格名・用途を階層的に組み合わせた意味のある識別子です。 一方で"car.fish.piano.run"のようなランダムな単語の組み合わせは、何も名前付けしたり説明したりしていないため、意味のある文字列とは言えません。

Unicodeリテラルを使用しない

Unicodeリテラル(例えばunicode"Hello 😃")はDiamond Storage識別子の作成に使用してはいけません。 Unicodeリテラルには不可視文字やASCII以外の文字が含まれる可能性があり、この標準の仕様に違反するためです。

スペース文字を使用しない

スペース文字(0x20)をDiamond Storage識別子に含めることは推奨されていません。 スペースがあると、次に説明するNatSpecタグなどのツールで問題が生じる可能性があるためです。

NatSpecタグ

ERC7201は、構造体のストレージ位置を示すためのNatSpecタグ@custom:storage-location <FORMULA_ID>:<NAMESPACE_ID>を定義しています。 <FORMULA_ID>はストレージ位置の計算に使用する計算式を識別するもので、ERC8042ではerc8042という計算式IDを定義しています。

ERC8042の計算式は以下のように定義されます。

text
erc8042(id: string literal) = keccak256(id)

Solidityでは、この計算式はkeccak256(id)という式に対応します。 NatSpecタグとしては@custom:storage-location erc8042:<NAMESPACE_ID>という形式で記述します。

以下はNatSpecタグの使用例です。

javascript
/// @custom:storage-location erc8042:myproject.erc721.registry
struct ERC721RegistryStorage {
    address[] erc721Contracts;
    uint256 registryLimit;
}

このNatSpecタグは、ERC721RegistryStorage構造体がkeccak256("myproject.erc721.registry")の位置に格納されることを示しています。 ツールやフレームワークはこのタグを読み取ることで、構造体のストレージ位置を自動的に特定できます。

補足

ERC8042とERC7201の比較

ERC8042とERC7201はどちらもスマートコントラクトのストレージ位置を管理するための仕組みですが、設計思想と計算式が異なります。

以下の図は、両者の計算フローの違いを示しています。

ERC8042とERC7201の計算フロー比較

ERC7201の特徴

ERC7201では名前空間IDに対して計算式を適用してストレージ位置を生成します。 名前空間IDは空白文字を含まない文字列ですが、それ以外に制約はありません。 つまりmycompany.projectA.erc721のような意味のある文字列でも、ランダムなバイト列や文字列でも使用可能です。

ERC7201のストレージ位置計算式はSolidityで以下のように記述されます。

javascript
keccak256(abi.encode(uint256(keccak256(bytes(namespace_id))) - 1)) & ~bytes32(uint256(0xff))

この計算式は2つの部分から構成されています。 前半のkeccak256(abi.encode(uint256(keccak256(bytes(namespace_id))) - 1))は、2回目のkeccak256への入力バイトがSolidityの型(マッピングや動的配列など)がkeccak256で使用する入力バイトと一致しないことを保証します。 これにより任意のバイト列を名前空間IDとして安全に使用できます。 後半の& ~bytes32(uint256(0xff))は、最終的なストレージ位置が256の倍数になることを保証し、将来的なガス最適化の可能性を提供します。

ERC8042の特徴

ERC8042では識別子を印刷可能なASCII文字のみに制限し、人間が読める意味のある文字列を使うことが推奨されています。

ERC8042のストレージ位置計算式はSolidityで以下のように記述されます。

javascript
keccak256(string_literal)

ERC7201と比較すると非常にシンプルです。 中間変換も追加演算も不要で、文字列を直接ハッシュ化するだけです。

比較表

以下のテーブルにERC8042とERC7201の主な違いをまとめます。

項目 ERC8042 ERC7201
識別子の制約 印刷可能ASCII文字のみ 空白文字以外のあらゆるバイト列
推奨される識別子 人間が読める意味のある文字列 制約なし
計算式 keccak256(id) keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff))
複雑さ シンプル より複雑
256の倍数保証 なし あり
既存の採用実績 2020年3月から使用 2023年6月に導入

ERC7201は任意のバイト列を安全に使いたい場合や、機械生成のランダムな識別子を使いたい場合に適しています。 ERC8042は人間が読みやすい識別子を好み、シンプルな計算式を求める場合に適しています。 プロジェクトの要件に応じて、どちらの標準を採用するかを選択できます。

Diamond Storageの歴史的経緯

ERC2535 Diamondsは2020年3月にスマートコントラクトの個別ストレージ領域というパターンを初めて導入しました。 ERC7201は2023年6月にこの一般的なパターンを標準化しましたが、より緩い識別子制約とより複雑な計算式を採用しました。 ERC8042は、元々のシンプルなDiamond Storageの手法を新規プロジェクトのために標準化すると同時に、過去5年間にわたって使用されてきた既存のDiamond Storage実装との互換性を正式に認めるものです。

もともとプロキシコントラクト向けに作られたDiamond Storageですが、あらゆるスマートコントラクト内でストレージの整理やデータアクセスの管理に使用できます。

参考実装

以下は、Diamond Storageを使用するコントラクトの実装例です。 ERC721コントラクトのアドレスを登録・管理するレジストリコントラクトとして設計されています。

以下の図は、参考実装のコントラクト構造を示しています。

参考実装のコントラクト構造

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

javascript
/**
 * @title ERC721Registry
 * @notice A registry contract for tracking ERC721 contracts.
 *         Allows adding ERC721 contract addresses up to a configurable limit.
 * @dev EIP-8042 Diamond Storage is used to organize and manage access to storage.
 */
contract ERC721Registry {

  /// @notice Reverts when the registry reaches its configured capacity.
  error ERC721RegistryFull();

  /// @notice Struct storage position defined by keccak256 hash
  ///         of diamond storage identifier.
  bytes32 constant STORAGE_POSITION = keccak256("myproject.erc721.registry");

  /// @custom:storage-location erc8042:myproject.erc721.registry
  struct ERC721RegistryStorage {
    address[] erc721Contracts;
    uint256 registryLimit;
  }

  /// @notice Returns a pointer to the ERC721RegistryStorage struct in storage.
  function getStorage() internal pure  returns (ERC721RegistryStorage storage s) {
    bytes32 position = STORAGE_POSITION;
    assembly {
      s.slot := position
    }
  }

  /// @notice Sets the maximum number of ERC721 contracts the registry can hold.
  function setRegistryLimit(uint256 _newLimit) internal {
    getStorage().registryLimit = _newLimit;
  }

  /// @notice Adds an ERC721 contract address to the registry.
  function addERC721Contract(address _erc721Contract) internal {
    ERC721RegistryStorage storage s = getStorage();

    if(s.erc721Contracts.length == s.registryLimit) {
      revert ERC721RegistryFull();
    }

    s.erc721Contracts.push(_erc721Contract);
  }

  /// @notice Returns a list of all registered ERC721 contract addresses.
  function getERC721Registry() internal view returns (address[] memory) {
    return getStorage().erc721Contracts;
  }
}

このコントラクトの設計のポイントを解説します。

ストレージ位置の定義

まずSTORAGE_POSITION定数で、keccak256("myproject.erc721.registry")の結果をストレージ位置として定義しています。 "myproject.erc721.registry"という識別子は、プロジェクト名・トークン規格名・用途を階層的に組み合わせたもので、一意性と可読性を両立しています。

ストレージ構造体

ERC721RegistryStorage構造体は、Diamond Storageパターンでストレージに配置されるデータ構造です。 NatSpecタグ@custom:storage-location erc8042:myproject.erc721.registryによって、この構造体のストレージ位置がツールから自動的に特定可能です。

フィールド 型 説明
erc721Contracts address[] 登録されたERC721コントラクトアドレスの動的配列。
registryLimit uint256 レジストリに登録できるコントラクト数の上限。
getStorage関数
javascript
function getStorage() internal pure  returns (ERC721RegistryStorage storage s) {
    bytes32 position = STORAGE_POSITION;
    assembly {
      s.slot := position
    }
}

インラインアセンブリを使って、STORAGE_POSITIONで定義されたスロットにあるERC721RegistryStorage構造体へのポインタを返す関数です。 pure修飾子が付いているのは、この関数自体はストレージの読み書きを行わず、単にポインタを計算して返すだけだからです。 他の関数はこのgetStorage()を通じてストレージにアクセスすることで、Diamond Storageパターンに従った統一的なデータアクセスを実現しています。

setRegistryLimit関数

getStorage().registryLimit = _newLimit;の1行で、ストレージポインタを取得してレジストリの上限値を設定しています。 Diamond Storageパターンでは、ストレージへのアクセスが常にgetStorage()関数を介して行われるため、構造体の位置管理が1か所に集約されます。

addERC721Contract関数

ERC721コントラクトのアドレスをレジストリに追加する関数です。 まずgetStorage()でストレージポインタを取得し、登録数が上限に達していないかを確認します。 上限に達している場合はERC721RegistryFullカスタムエラーでリバートし、そうでなければアドレスを配列に追加します。

getERC721Registry関数

登録されたすべてのERC721コントラクトアドレスの配列を返す関数です。 getStorage().erc721Contractsにアクセスして、ストレージから配列データをメモリにコピーして返します。

互換性

Diamond Storageはこの標準で記述されている形式で5年間使用されてきました。 ERC8042は仕様に準拠するすべての過去のDiamond Storage実装を認識し、標準化するものです。 つまり、既存のDiamond Storageを使用しているプロジェクトは、コードを変更することなくERC8042に準拠していることになります。

ERC8042はERC7201が定義したNatSpecタグの形式に準拠しており、erc8042という計算式IDを使用します。 これにより、ERC7201対応のツールがERC8042のストレージ位置も検出できます。

セキュリティ

識別子の一意性

2つの独立したコントラクトやライブラリが同じ人間が読める文字列を使用すると、同じストレージスロットにマッピングされてしまいます。 開発者は、コントラクトシステム内でDiamond Storage識別子が一意であることを確認し、意図しないデータの重複や破損を防ぐ必要があります。 一般的な対策として、識別子にプロジェクト名、組織名、または規格名をプレフィックスとして付ける方法があります(例えば"myproject.erc721.registry"のように)。

ASCII入力制限によるストレージ衝突の防止

Diamond Storage識別子が印刷可能なASCII文字のみに制限されている理由は、ストレージ衝突を防ぐためです。

Solidityのストレージスロットのエンコーディングは、abi.encode(p)で生成される際、パディングルールによりヌルバイト(0x00)などの非印刷バイトを含みます。 もし識別子にそのようなバイトを含めることが許可されていた場合、悪意のある開発者が"config\x00\x00..."のような識別子を作成することが可能になります。 そのような識別子のバイト列は、マッピングや動的配列、文字列、bytes型がストレージ位置の計算に使うkeccak256の入力と衝突する可能性があります。 衝突が発生すると、データの上書きやコントラクト状態の破損、セキュリティの脆弱性につながりかねません。

識別子を印刷可能なASCII文字に制限することで、この種の衝突リスクが取り除かれます。 数学的には衝突が絶対に不可能だとは言い切れません。理論上はストレージレイアウト位置pが十分に大きい場合、偶然すべて印刷可能なASCIIバイトを含む可能性がありますが、実際にそれが起こる確率は極めて小さく、非現実的です。 人間が読める意味のある文字列を識別子に使用することで、衝突の実質的な可能性はほぼゼロとなり、コントラクトストレージの整合性と安全性が強力に保護されます。

文字列リテラルの安全性

Solidityの文字列リテラルを使用してDiamond Storage識別子を作成することが推奨されている理由は、Solidityコンパイラが印刷可能なASCII文字のみが使用されていることを強制するからです。 ただし、文字列リテラル内であってもUnicodeエスケープシーケンス(\uNNNN)や16進エスケープシーケンス(\xNN)は使用してはいけません。 これらのエスケープシーケンスを使うと、非印刷バイトを識別子に混入させることが可能になるためです。

Unicodeリテラルの危険性

Unicodeリテラル(例えばunicode"Hello 😃")をDiamond Storage識別子の作成に使用してはいけません。 Unicodeリテラルには非印刷バイトや文字が含まれる可能性があるだけでなく、見た目が同じだが実際には異なる文字を使って識別子を偽装することも可能です。 例えば、キリル文字のа(\u0430)はASCIIのa(\u0061)と見た目が同一です。 また、Unicodeにはテキストの表示方向を変更する制御文字も存在します。 このような文字が識別子に含まれると、開発者が意図しないストレージ衝突や、コードレビュー時に見落とされるセキュリティリスクにつながる可能性があります。

keccak256のハッシュ衝突耐性

Diamond Storage構造体の位置はkeccak256ハッシュ関数の出力によって決定されます。 コントラクトストレージの256ビットのアドレス空間は非常に広大であるため、Diamond Storage構造体が既存のストレージデータと偶然同じアドレスに配置される可能性は統計的にほぼありません。 Solidityのマッピング、動的配列、文字列、bytes型もストレージ位置の決定にkeccak256を使用しており、同じハッシュ関数に基づく衝突耐性の恩恵を受けています。

引用

最後に

今回は「ERC8042による人間が読める文字列でストレージ位置を定義するDiamond Storageの標準化」についてまとめてきました! いかがだったでしょうか?

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

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