はじめに

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

今回は「ERC8152」についてまとめていきます。

ERC8152は、プロキシが使うロジックモジュールを、ランタイムバイトコードそのものから決まる同一アドレスで各チェーンへ再配置しやすくする提案です。 DiamondやUUPSのようなアップグレード可能な構成で、同じ処理をチェーンごとに別アドレスへ散らさず、検証しやすい形で再利用したい時の土台になります。

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

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

概要

ERC8152は、CALM(Content-Addressable Logic Modules) という形式で、プロキシから delegatecall されるロジックを標準化する提案です。

ポイントは、ロジックモジュールのアドレスを「そのランタイムバイトコードへの暗号学的なコミットメント」として扱う点です。 つまり、同じバイトコードならどのEVMチェーンでも同じアドレスへ配置しやすくし、チェーンごとに別アドレスへ散らばる問題を減らします。

以下の図は、CALMを使うと「Proxyはチェーンごとに別でも、共有ロジックは同じアドレスへ置きやすい」という全体像を示しています。

CALMのマルチチェーン全体像

CALMは単独の仕組みではなく、既存のProxy構成やストレージ分離の考え方と接続して使う前提です。 先に関連規格の位置づけを短く整理しておきます。

対象 役割
ERC2535 Diamondパターンとして複数のロジックをFacet単位で切り替える土台です。
ERC8153 FacetベースのDiamondを整理する関連提案です。
ERC8167 selectorごとにモジュールを割り当てやすいModular Dispatch Proxyです。
ERC1822 UUPSパターンとして実装コントラクトを差し替える仕組みです。

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

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

普通のライブラリやFacetは、同じ処理でもチェーンごと、あるいはデプロイごとに別アドレスへ散らばりがちです。 すると、監査済みロジックの再利用、クロスチェーンでの検証、同一実装の追跡が面倒になります。

ERC8152は、コンストラクタやimmutable、SELFDESTRUCT のような「アドレスや永続性を揺らす要素」を避け、ランタイムコード自体を識別子に近づけます。 要するに、「どこへ置かれたか」より「どんなコードか」を軸に共有しやすくする発想です。

動機

既存のプロキシ構成では同じロジックが散らばりやすい

DiamondやUUPSのような構成では、ユーザーが触る本体はProxyでも、実際の処理はFacetやImplementationに逃がすのが一般的です。 これはアップグレードや機能分割には向いていますが、同じロジックを複数チェーンへ持っていく時に、実装先アドレスまで揃うとは限りません。

そのため、あるチェーンで監査済みだった実装が、別チェーンでは「同じソースのはずだが別アドレスにある」状態になりやすくなります。 運用側は、チェーンごとに配置先を管理し、利用側は「そのアドレスのコードが本当に同じか」を毎回確認し直す必要があります。

以下の図は、従来のFacetやImplementationがチェーンごとに散る形と、CALMで共有ロジックの住所を揃えやすくする形を比べたものです。

従来構成とCALMの比較

左側ではチェーンごとに別アドレスの実装を追いかける必要があります。 右側ではランタイムバイトコードを軸に、共有ロジックを同じ住所で見つけやすくするのが狙いです。

CALMが解こうとしている課題

ERC8152が解こうとしている課題は大きく4つあります。

  • 同じロジックを同じアドレスで再利用したい ランタイムコードが同じなら、EVMチェーンをまたいでも同じアドレスへ置ける方が追跡しやすくなります。
  • クロスチェーン検証を簡単にしたい 「このアドレスのロジックはメインネットでもL2でも同じです」と言いやすくなります。
  • 監査結果を使い回しやすくしたい 監査対象が毎回別アドレスへ散らばるより、共通ロジックとして扱える方が整理しやすくなります。
  • ガスや実行効率でも共有のメリットを出したい 将来のアドレスwarming改善や、ノード側のコードキャッシュ効率も論点に含まれています。

なぜ「ランタイムバイトコード中心」なのか

ここでは、ソースコードやコントラクト名ではなく、最終的に実行されるランタイムバイトコードを中心に据えます。 理由は、実際にチェーン上で意味を持つのはそのバイトコードであり、コンストラクタやimmutableの差分が入ると、見た目は近くてもアドレスや中身がずれてしまうからです。

逆に言えば、同じ処理を同じアドレスで共有したいなら、デプロイ時に差し込まれる要素を減らす必要があります。 ここから先の仕様は、そのための制約を定義していると見ると理解しやすいです。

仕様

CALMが前提にしている実行モデル

CALMは、単独で状態を持つ通常コントラクトというより、Proxyのストレージ文脈で動くロジックモジュールとして使う前提です。 つまり、CALM自体のアドレスに状態をためるのではなく、delegatecall で呼び出された先としてProxyの状態を書き換えます。

CALMが自分自身のストレージを持っていても、それを完全に防ぐチェックまでは要求されていません。 ただし、実際に使う時は、Proxy側の状態だけを安全に触るよう設計することが期待されています。

ストレージ分離

ストレージ衝突を避けるため、CALMは決定的なストレージオフセットを使うべきだとされています。 提案の中心は、名前空間や決まったスロットを使って、どの領域を触るかを明確にすることです。

この図では、CALMが自分の状態を持つのではなく、Proxy側のどの保存領域を触るかを揃える必要があることを示しています。

CALMとProxyストレージの関係

ここで参照されているのが ERC7201 と ERC8042 です。 前者は名前空間ベースのストレージ管理、後者はDiamond Storageを標準化する提案です。

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

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

原則としては、こうした名前空間付きの分離を使う方が安全です。 一方で、すでに稼働中の古いProxyとの後方互換性のために、ゼロ始まりのレガシーなストレージ配置を使う余地も残されています。

ただしその場合でも、どのスロットへ影響するのかを明示しなければ、モジュールを差し替えた時に状態が壊れやすくなります。 ERC8152はこの点をかなり重く見ていて、後述するImpact Manifestでもストレージ影響の開示を促しています。

デプロイ制約

CALMとして準拠するには、デプロイ時にいくつかの強い制約があります。

コンストラクタを使わない

CALMのinitcodeは、ランタイムバイトコードを配置する以外のロジックを実行してはいけません。 なぜなら、チェーンをまたいで同じアドレスへ再配置する時に、コンストラクタで何かを実行すると、その副作用を再現できないからです。

要するに、デプロイ時に状態初期化や条件分岐を済ませる発想を捨てて、完成済みのランタイムコードをそのまま置く形へ寄せます。

immutableを使わない

immutableは便利ですが、実体としてはデプロイ時にランタイムコードへ埋め込まれます。 そのため、値が変わるとバイトコードハッシュも変わり、アドレスの決定性が崩れます。

ERC8152では、ランタイムコードの同一性こそが中心なので、immutableのような埋め込み差分は相性が悪いです。

同じアドレスへ再配置できること

CALMのアドレスは公開された定数とランタイムバイトコードから決まるべきだとされています。 本文では概念的に次のような形で示されています。

txt
address = function(<publicly known constants>, runtimeBytecode)

この図では、constructorやimmutableを避けて、固定の前提とランタイムバイトコードから同じアドレスへ再配置する流れをまとめています。

CALMの再配置条件

ここでいう公開定数の例として、固定salt、固定のmicro constructor bytecode、どのチェーンでも同じアドレスへ置けるdeployerコントラクトが挙げられています。 重要なのは式の細部より、「誰でも同じ前提を使って再配置できること」です。

この性質があると、あるチェーンで見つかった有用なロジックを、別チェーンでも許可なく同じアドレスへ再現できます。 つまり、特定の運営者に再配置を頼まなくても、同じ条件を満たせば別の人が同じロジックを同じ住所へ置き直せます。 そのため、監査済みロジックの再利用や、チェーンをまたいだ追跡がやりやすくなります。

ランタイム制約

SELFDESTRUCT を含めない

CALMは SELFDESTRUCT を含んではいけません。 さらに、そのような命令を含む別コントラクトへ delegatecall することも禁じています。

理由は単純で、Proxyが依存するロジックが消える可能性を残すと、共有ロジックとして長期利用しにくくなるからです。 EIP6049 や EIP6780 にも触れつつ、チェーンごとの差異があっても永続的に参照できる形を優先しています。

自分自身のストレージへ直接触れない

CALMは「状態を持たないが、Proxyの状態を操作する」モジュールとして振る舞います。 このため、自分のストレージを直接読む前提にすると、Proxy経由で使った時の意味がぶれます。

この制約は、Facetやライブラリ風の使い方をさらに厳密にしたものだと考えると分かりやすいです。

Capability and Impact Manifest

ERC8152の途中には、少し変わった節があります。 それが Capability and Impact Manifest です。

これは、CALMのヘッダやNatSpecに「このモジュールはどのストレージへ触れるか」、「どんな外部依存があるか」、「どのEVM機能が必要か」を書く考え方です。 提案本文では、将来は専用の別規格で正式なスキーマを定める想定だと説明されています。

以下の図は、Manifestが便利でも自己申告にすぎず、統合側は静的解析や検証ツールで裏取りする必要があることを示しています。

Manifestと静的解析の関係

マニフェストに含めたい情報は主に以下です。

項目 何を書くか
Storage Impact どの名前空間、どのスロット、どの保存領域へ触れるか
External Dependencies 外部コントラクトや DELEGATECALL、決定的Factoryへの依存
EVM Compatibility 必要なopcode拡張、必要なprecompile、必要なハードフォーク

ただし、ここで重要なのは、マニフェスト自体は信用しすぎてはいけないという点です。 提案も、これはあくまで開発者の自己申告であり、最終的には静的解析や検証ツールで裏取りすべきだとしています。

ディスパッチモデル

ERC8152は、CALMの実行モデルを2種類に分けています。

以下の図は、複数機能を1つのモジュールへまとめる形と、1つのselectorに1つのロジックを割り当てる形の違いを並べたものです。

CALMのディスパッチモデル比較

モデル 説明 向いている用途
Multi-Method Solidityの通常コントラクトのように、内部dispatcherが msg.sig で処理を振り分けます。 関連する複数機能をまとめたFacet
Atomic-Logic 関数selectorごとの内部振り分けを持たず、fallback() に1つのロジックだけを持たせます。 単機能で極限まで小さくしたい処理

特に面白いのは Atomic-Logic 側です。 Diamondでは通常、ProxyがFacetへ delegatecall したあと、Facet側でもさらにselectorを見て分岐します。 しかし1つの処理だけを持つFacetなら、この2段目の分岐は無駄になります。

そこで、selectorごとに直接「1処理だけを持つCALM」を割り当てれば、到着後すぐロジックを実行できます。 これはSingle Function Facetに近い考え方です。

補足

なぜ共有ロジックがガス面でも意味を持つのか

CALMは、単なる管理しやすさだけでなく、実行効率の面からも意味があります。

1つ目は、将来的にEIP7863のようなアドレスwarming改善が入った場合です。 同じブロック内で先に呼ばれたCALMアドレスが温まれば、後続トランザクションも恩恵を受けやすくなります。

2つ目は、今のクライアント実装でも、よく使われるバイトコードはノード内キャッシュへ載りやすい点です。 同じ処理がチェーン上でバラバラのアドレスに散っているより、共通アドレスへ集約されている方がキャッシュ効率はよくなります。

もちろん、これだけで即座に大幅なガス削減が約束されるわけではありません。 ただし、ネットワーク全体で共有される「標準的な実装住所」が育つと、長期的には運用コストも実行コストも下げやすいというのが提案の見立てです。

メタデータを残すか外すか

ERC8152の中で特に悩ましいのが、CBORメタデータの扱いです。

Solidity系のコンパイラは、通常はランタイムバイトコード末尾へメタデータを付けます。 これにはソースやABI、コンパイラ設定に関わる情報が入るため、コメント1つ変わるだけでも最終バイトコードが変わり得ます。

CALMの立場から見ると、これは利点でもあり欠点でもあります。

以下の図は、metadataを外す時と残す時の差と、verificationとvalidationの役割差をまとめたものです。

metadataとverification/validationの比較

  • メタデータを外す 純粋にロジックだけでアドレスを決めやすくなります。
  • メタデータを残す コメントや監査前提、コンパイラ設定までアドレスに結びつけやすくなります。

提案は、どちらか一方だけを正解としていません。 単純なロジックならメタデータなしで共有性を優先し、セキュリティ上重要な高機能ロジックならメタデータ込みで完全性を優先する、という整理です。

検証と妥当性確認は別物

Verification と Validation は別物として整理されています。

  • Verification そのソースコードからこのバイナリが本当に生成されるかを確かめる作業です。
  • Validation そのアドレスが本当に対象バイトコードへの直接コミットメントになっているかを確かめる作業です。

前者は人間が読むための信頼、後者は機械的に確認できる信頼に近いです。 ERC8152は、両方必要だが同じものではない、と整理しています。

互換性

ERC8152 は既存の代表的なProxy構成と互換性があります。 本文で挙げられているのは ERC2535、ERC8153、ERC8167、ERC1822、ERC1167 です。

ただし、互換性があるというのは「そのまま安全に混ぜてよい」という意味ではありません。 実際には、どのストレージ分離方式を採るか、どのselectorをどのCALMへ向けるか、どのチェーン機能を前提にするかが揃っていないと不整合が起きます。

特に、ERC7201 と ERC8042 を混在させる時は注意が必要です。 論理的には同じ状態を触るつもりでも、参照スロットの設計が違えば別の場所を読んでしまうからです。

参考実装

提案のReference Implementationでは、decimals() を返すだけの最小ロジックが例として示されています。 狙いは、「1つの関数しか持たないAtomic-Logic CALMなら、ここまで小さくできる」ということを見せる点にあります。

最初に示されているのは、fallback() だけを使うSolidity実装です。

js
// SPDX-License-Identifier: MIT
pragma solidity 0.8.33;

contract Decimals_Const18_Optimized {
    fallback() external payable {
        assembly {
            mstore(0x00, 0x12)
            return(0x00, 0x20)
        }
    }
}

このコードは decimals() 相当の値として 18 を返すだけです。 通常の関数定義を置かず、fallback() から直接返すことで、selector分岐のぶんだけ余計な処理を減らしています。

コンパイラ設定例としては、次のようにCBORメタデータを外し、最適化を強くかける形が載っています。

toml
[profile.default]
via_ir = true
optimizer_runs = 20_000_000
bytecode_hash = "none"
cbor_metadata = false
evm_version = "shanghai"

この結果として得られるランタイムバイトコードが 0x60125f5260205ff3 になる例も示されています。 さらに、HashCarve Factoryを使うと、このロジックを各チェーンで同一アドレスへ再現できると説明しています。

ここで面白いのは、同じ意味の処理でも、Vyperで書けば別バイトコードになるため別アドレスになる点です。 つまり、ERC8152が保証したいのは「同じ機能」ではなく「同じランタイムバイトコード」です。

セキュリティ

ストレージ衝突

CALMは自分の状態を持たない代わりに、Proxyの状態を直接触ります。 そのため、最も分かりやすい事故はストレージ衝突です。

異なるCALMが同じスロットを別の意味で使ったり、ERC7201 と ERC8042 の設計を混ぜたまま同じ論理状態を読んだりすると、値が壊れます。 この問題は、CALM単体のコードレビューだけでは見抜きにくく、Proxy全体の配線まで含めて確認する必要があります。

マニフェストの過信

Impact Manifestは便利ですが、自己申告です。 「このモジュールはストレージへ触りません」と書いてあっても、実バイトコードが別のことをしていれば意味がありません。

そのため、統合側はマニフェストを読むだけで済ませず、静的解析やシンボリック実行で次を確認する必要があります。

  1. そのチェーンで未対応のopcodeに依存していないか。
  2. 宣言していないストレージや外部依存へ触れていないか。
  3. SELFDESTRUCT や危険な delegatecall 経路を含んでいないか。

メタデータ改ざんへの考え方

ソース検証が通って見えても、コメントや変数名で誤解を誘う余地はあります。 提案はこの点も意識していて、特に高リスクのロジックでは、CBORメタデータを残して監査前提までアドレスへ結びつける選択肢を示しています。

つまり、共有しやすさだけを追って常にメタデータなしへ寄せればよいわけではありません。 どの程度まで人間向けの検証情報をアドレスへ結びつけたいかも、設計判断に入ります。

最後に

今回は「ERC8152」についてまとめてきました。

ERC8152は、Proxy向けロジックを単なる実装コントラクトではなく、ランタイムバイトコードで識別できる共有モジュールとして扱おうとする提案です。 コンストラクタやimmutableを避け、ストレージ影響やEVM依存を明示し、同じロジックを同じアドレスへ再配置しやすくすることで、クロスチェーン検証と再利用のしやすさを上げようとしています。 一方で、ストレージ衝突、マニフェストの過信、メタデータの扱いなど、実運用では慎重に決めるべき論点も多いです。

他でも色々記事を書いているのでぜひよろしければ読んでいってください!