はじめに
初めまして。 『DApps開発入門』という本や色々記事を書いているかるでねです。
以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
今回は「ERC8167」についてまとめていきます。
ERC8167は、1つのプロキシが受け取った calldata の先頭4バイト、つまり関数 selector ごとに delegatecall 先を切り替える Modular Dispatch Proxy を定義する提案です。
従来の「1つのプロキシが1つの実装コントラクトへ丸ごと転送する」構成ではなく、機能ごとにロジックを分割しつつ、ツールがそのABIと変更履歴を追える最小インターフェースまでそろえることを狙っています。
概要
ERC8167は、selector -> delegate の対応表を持つプロキシを標準化します。
呼び出し時は msg.sig に対応するdelegateへ delegatecall し、調査時は selectors() と implementation(bytes4) で「どの関数が、どのロジックへつながっているか」を外から読めるようにします。
以下の図は、ERC8167 の実行時の全体像です。

実行時の中心は、selector を見てdelegateを選び、元の calldata をそのまま delegatecall で渡す処理です。
調査時の中心は、implementation(bytes4) と selectors() でプロキシが持つ関数集合を外から復元できる点です。
つまりERC8167は、機能分割と段階的なアップグレードをしやすくしつつ、ツールがABIと変更履歴を追えるようにする最小標準です。
動機
1つの implementation に寄せるプロキシの限界
一般的なUpgradeableプロキシは、受け取った呼び出しを単一の implementation へ転送します。
この構成は分かりやすい一方で、機能が増えるほど実装全体が大きくなり、変更も「全部まとめて差し替える」形になりやすいです。
その結果、以下の問題が出ます。
- コードサイズ制限に近づきやすい
- 小さな修正でも
implementation全体の差し替えになる - 追加拡張のたびに既存部分へ影響が広がりやすい
- 監査や差分確認の単位が大きくなる
ERC8167は、この問題を「機能ごとにdelegateを分ける」ことで和らげます。
共有ロジックと拡張を両立したい
トークンの残高管理や転送処理は、複数コントラクトで共有しやすいロジックです。
一方、名称や発行ポリシーは個別差分が大きく、別moduleへ分ける価値があります。
単一の implementation では、その違いがあるだけで別々の実装を持ちやすくなります。
一方、selector単位でdelegateを差し替えられるなら、共通部分だけを同じmoduleに寄せられます。
また、新しい拡張規格が出た時も、必要な関数群だけを追加moduleとして導入しやすくなります。 具体例として、スマートアカウントや拡張トークンのようなユースケースが想定されています。
EIP7702については以下の記事を参考にしてください。
段階的なアップグレードとhardening
モノリシックなプロキシでは、アップグレードのたびに大きな変更セットを一度に載せがちです。 ERC8167はselector単位でdelegateを追加、更新、削除できる前提なので、変更の粒度を細かくしやすいです。
さらに、アップグレード用のメソッドを最終的に取り外せば、immutableに近い状態へ寄せるhardeningもできます。 つまり「最初はUpgradeableに運用し、十分に固まったら変更経路を閉じる」という運用を取りやすくします。
以下の図は、Upgradeableな段階からhardening後の段階までの見取り図です。

仕様
実行ルール
Modular Dispatch Proxyは、受け取った calldata の先頭4バイトに対応するdelegateへ、元の calldata をそのまま delegatecall しなければなりません。
つまり、プロキシ側で引数を組み替えたり、独自ABIに変換したりすることは想定していません。
この時に使うselectorは、Solidityの4-byte ABIそのものです。 ERC8167は新しいdispatch方式を定義せず、既存ABIのselectorをそのままルーティングキーとして再利用します。
該当する selector が見つからない場合の扱いについては、対応するdelegateがない時にrevertし、FunctionNotFound(bytes4 selector) を返すことを推奨しています。
ただし、これは REQUIRED ではなく RECOMMENDED です。
IERC8167
interface定義は以下です。
interface IERC8167 {
event SelectorDelegated(bytes4 indexed selector, address indexed delegate);
function implementation(bytes4 selector) external view returns (address);
function selectors() external view returns (bytes4[] memory);
error FunctionNotFound(bytes4 selector);
}各要素の役割は以下です。
| 要素 | 種別 | 必須性 | 役割 |
|---|---|---|---|
SelectorDelegated |
イベント | 推奨 | selectorへの割り当て変更を履歴として残す |
implementation(bytes4) |
関数 | 必須 | 特定selectorがどのdelegateを向くか返す |
selectors() |
関数 | 必須 | 実装済みselector一覧を返し、プロキシのABI発見を助ける |
FunctionNotFound |
エラー | 推奨 | selectorが未登録の時の失敗理由を明示する |
IERC8167 関数は、プロキシ本体ではなくdelegate側に持たせることも推奨されています。
この構成では、プロキシ本体にはselector判定とdelegatecallを置き、管理用や参照用の処理はdelegate側へ置けます。
implementation(bytes4 selector)
function implementation(bytes4 selector) external view returns (address);この関数は、あるselectorに対応するdelegateのアドレスを返します。
未登録なら address(0) を返します。
この関数があれば、ストレージ配置を知らなくても、外部ツールが「このselectorはどこへ飛ぶのか」を共通の方法で読めます。
eth_getStorageAt でも調べられる場合はありますが、共通 interface があればストレージレイアウトの違いを吸収しやすくなります。
selectors()
function selectors() external view returns (bytes4[] memory);この関数は、現在implementationを持つselector一覧を返します。 ここがERC8167のツール向けの価値の中核です。
selectorだけでは関数名や引数型は分かりません。 しかし、以下の順序で辿ればプロキシのABIを再構成できます。
selectors()でselector一覧を取得する まずプロキシが公開している関数候補を集める。- 各selectorに対して
implementation(selector)を呼ぶ どのdelegateがそのselectorを担当しているか対応付ける。 - delegate側の検証済みABIを取得する ソース検証済みならABIを取れますし、未検証なら利用者が補う余地があります。
- selectorとdelegate ABIを突き合わせてプロキシ全体の関数集合を組み立てる 同じdelegateを複数selectorが共有していても、この段階でまとめられる。
以下の図は、その発見フローをそのまま表したものです。

selector一覧は SelectorDelegated イベントをログから集めることもできますが、ログ検索に頼らず取得できる入口として selectors() を置く意義があります。
特にindexerなしの環境では、この関数があるだけでツール実装がかなり単純になります。
constructor の前提
dispatch proxyのconstructorは、少なくとも1つのdelegateを設定しているべきです。 空のプロキシをdeployして後から全部登録する方式を完全に禁止しているわけではありませんが、最低限の自己起動性を推奨しています。
設計根拠
なぜ bytes4 selector なのか
bytes4 selector を使う理由は、Solidityの4-byte selectorが広く使われているABIの入口だからです。
新しい識別単位を持ち込まず、既存ABIとそのまま揃えることで、既存ツールとの接続を保ちやすくします。
なぜアップグレード手順まで規格化しないのか
ERC8167はdispatch proxy自体の最小標準に絞っています。 そのため、アップグレード関数やバージョン管理の設計までは規定していません。
これには利点があります。 アクセス制御、batch upgrade、governance、timelockなどは用途差が大きく、そこまで固定すると最小標準ではなくなってしまうからです。 ERC8167は「selectorごとのdispatch」と「それを調べる参照関数」だけを共通化し、運用面は別規格や各実装に委ねています。
storage layoutも固定しない
ERC8167はstorage layoutも規定しません。 namespaceベースにするか、共通base storageを継承するかは実装戦略ごとの差が大きいからです。
ただし、storage collisionは避ける必要があります。 そのため、layoutを自由にする代わりに、衝突回避の責任は実装側へ明確に残しています。
ERC7201については以下の記事を参考にしてください。
互換性
ERC8167は、既存のmodular proxy標準である ERC2535 を改善する立場を取っています。 主な改善点は以下の3つです。
- diamondという専用用語を外し、より一般的な説明に寄せる
- introspection関数を減らし、より単純な
interfaceにする - upgrade eventを単純化する
以下の図は、その差分を一枚にまとめたものです。

ERC2535については以下の記事を参考にしてください。
ERC8167はERC2535と同じ「関数selectorごとにlogic moduleへ振り分ける」という方向性を持ちつつ、標準面ではもっと小さく切っています。 特にツールに必要な最小面だけを残したい、という設計意図が見えます。
既存のmonolithic proxyからの移行手順は以下です。
- selectorごとのdelegate mappingを埋められる
implementationへアップグレードする - 既存ABIの各関数とintrospection関数についてmappingを埋める
- そのmappingを使うdispatch proxyへ
implementationを切り替える
既存のmodular proxyなら、selectors() と implementation(bytes4) を追加し、必要ならinstalled functionごとに SelectorDelegated イベントを発行することで寄せられます。
参考実装
参考実装は3ファイルで構成されています。
| ファイル | 役割 |
|---|---|
IERC8167.sol |
規格 interface の定義 |
ProxyStorageBase.sol |
selector情報を置くnamespaceベースstorage |
Proxy.sol |
Setup、FullAdmin、ProxyStorageView、Proxy の実装 |
以下の図は、参考実装の責務分担をまとめたものです。

ProxyStorageBase.sol
参考実装では、selectorごとの情報を以下の struct で持ちます。
struct SelectorInfo {
address delegate;
uint96 index;
}delegate は転送先です。
index は selectors 配列中の位置で、削除時の swap-and-pop を O(1) で行うために使います。
Setup と FullAdmin
Setup は初期installだけを持つmoduleです。
FullAdmin はそこへ upgrade と uninstall を足します。
各関数の役割は以下です。
| 関数 | 役割 | 主なチェック |
|---|---|---|
install(bytes4 selector, address delegate) |
新しいselectorを登録 | delegate != address(0)、コードあり、未登録 |
upgrade(bytes4 selector, address delegate) |
既存selectorのdelegateを差し替え | delegate != address(0)、コードあり、既登録 |
uninstall(bytes4 selector) |
selectorの割り当てを削除 | 既登録であること |
これらはすべて成功時に SelectorDelegated を発行します。
削除時は delegate = address(0) を使って「removeされた」ことを表します。
ProxyStorageView
selectors() と implementation(bytes4) は参考実装では ProxyStorageView delegateにまとまっています。
IERC8167 の関数はプロキシ本体ではなくdelegate側へ置くことが推奨されており、この構成はその意図に沿っています。
プロキシ本体にはselector判定とdelegatecallを実装し、調査用 interface は ProxyStorageView delegate側へ置けます。
Proxy constructor の bootstrap
参考実装で面白いのはconstructorです。
deploy時に Setup delegateを新しく作り、その install 関数自身を最初のselectorとして登録します。
constructor() {
address installDelegate = address(new Setup(msg.sender));
(bool success,) = installDelegate.delegatecall(
abi.encodeWithSelector(
Setup.install.selector,
Setup.install.selector,
installDelegate
)
);
require(success);
}これによりプロキシは、deploy直後から少なくとも install だけは呼べる状態で立ち上がります。
提案の「constructor SHOULD configure at least one delegate」を、かなり直接的に実装した形です。
以下の図は、その bootstrap を順に追ったものです。

セキュリティ
access control
ERC8167はアップグレード関数そのものを規定しませんが、アップグレード機能を持つならaccess controlが必要です。
selectorごとに implementation を差し替えられる以上、管理経路が破られるとプロキシの挙動を丸ごと乗っ取られます。
そのため、少なくとも以下は設計時に決める必要があります。
- 誰がinstall、upgrade、uninstallを呼べるか
- 変更を即時反映するか、timelockを置くか
- batch変更時にどこまで一括失敗させるか
- hardening後に本当にadmin経路が残っていないか
self-destruct できる delegate を入れない
delegateは self-destruct してはなりません。
もしdelegateが消えると、そのmoduleに依存するプロキシは呼び出し不能になります。
プロキシ側のstateが無事でも、呼び出し先コードが消えれば実質的に壊れます。
storage collision
delegatecall ベースのmodular architectureで最も典型的な事故はstorage collisionです。
別々のmoduleが同じslotを異なる意味で使うと、アップグレードや拡張のたびにstateを壊します。
以下の図は、安全な2パターンと危険なパターンをまとめたものです。

衝突回避の方法は大きく2つあります。
- 全delegateが共通superclassのstorage layoutを共有する 追加storageを各subclassで勝手に宣言しない前提で運用する。
- storage namespaceを使う 共通baseに寄せにくいshared library型のmoduleではこちらが向きます。
後者の例として、ERC7201とERC8042が挙げられています。 selector単位でmoduleを差し替える設計は便利ですが、storageまで自由に混ぜると事故の影響範囲が広くなるので、ここは規格本文の中でもかなり重要な注意点です。
最後に
今回は「ERC8167」についてまとめてきました。
ERC8167は、selectorごとに delegatecall 先を切り替えるmodular proxyを最小 interface で標準化し、実行時のdispatchだけでなくツール向けのABI発見経路までそろえる提案です。
単一の implementation に全部を詰め込むのではなく、機能ごとのmodule分割、段階的アップグレード、最終的なhardeningを取りやすくする点が、この提案の価値です。
一方で、access control、storage collision、delegateの寿命管理は規格外のまま残るので、実装側の責任はむしろ明確に重くなります。
他でも色々記事を書いているのでぜひよろしければ読んでいってください!


