# [ERC8167] selectorごとにdelegatecall先を切り替えるModular Dispatch Proxyの仕組みを理解しよう

> ERC8167は、selector -> delegate の対応表を持つプロキシを標準化します。

- URL: https://cardene.dev/articles/dddba8abb85200b4b49e
- 公開日: 2026-05-21
- 更新日: 2026-05-21
- トピック: Blockchain
- タグ: solidity, smartcontract, ブロックチェーン, erc

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

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

ERC8167は、1つのプロキシが受け取った `calldata` の先頭4バイト、つまり関数 `selector` ごとに `delegatecall` 先を切り替える **Modular Dispatch Proxy** を定義する提案です。
従来の「**1つのプロキシが1つの実装コントラクトへ丸ごと転送する**」構成ではなく、機能ごとにロジックを分割しつつ、ツールがそのABIと変更履歴を追える最小インターフェースまでそろえることを狙っています。

https://eips.ethereum.org/EIPS/eip-8167

## 概要

ERC8167は、`selector -> delegate` の対応表を持つプロキシを標準化します。
呼び出し時は `msg.sig` に対応するdelegateへ `delegatecall` し、調査時は `selectors()` と `implementation(bytes4)` で「**どの関数が、どのロジックへつながっているか**」を外から読めるようにします。

以下の図は、ERC8167 の実行時の全体像です。

![ERC8167 の全体像](/images/qiita-7d419d60-615d-43fb-b873-a9e9d1c7b819.webp)

実行時の中心は、`selector` を見てdelegateを選び、元の `calldata` をそのまま `delegatecall` で渡す処理です。
調査時の中心は、`implementation(bytes4)` と `selectors()` でプロキシが持つ関数集合を外から復元できる点です。
つまりERC8167は、機能分割と段階的なアップグレードをしやすくしつつ、ツールがABIと変更履歴を追えるようにする最小標準です。

## 動機

#### 1つの `implementation` に寄せるプロキシの限界

一般的なUpgradeableプロキシは、受け取った呼び出しを単一の `implementation` へ転送します。
この構成は分かりやすい一方で、機能が増えるほど実装全体が大きくなり、変更も「**全部まとめて差し替える**」形になりやすいです。

その結果、以下の問題が出ます。

- コードサイズ制限に近づきやすい
- 小さな修正でも `implementation` 全体の差し替えになる
- 追加拡張のたびに既存部分へ影響が広がりやすい
- 監査や差分確認の単位が大きくなる

ERC8167は、この問題を「**機能ごとにdelegateを分ける**」ことで和らげます。

#### 共有ロジックと拡張を両立したい

トークンの残高管理や転送処理は、複数コントラクトで共有しやすいロジックです。
一方、名称や発行ポリシーは個別差分が大きく、別moduleへ分ける価値があります。
単一の `implementation` では、その違いがあるだけで別々の実装を持ちやすくなります。
一方、selector単位でdelegateを差し替えられるなら、共通部分だけを同じmoduleに寄せられます。

また、新しい拡張規格が出た時も、必要な関数群だけを追加moduleとして導入しやすくなります。
具体例として、スマートアカウントや拡張トークンのようなユースケースが想定されています。

:::message
**EIP7702**については以下の記事を参考にしてください。
:::

[\[EIP7702\] EOAをコントラクトアカウントに変換する仕組みを理解しよう！](/articles/13a828c97786aab953e4)

#### 段階的なアップグレードとhardening

モノリシックなプロキシでは、アップグレードのたびに大きな変更セットを一度に載せがちです。
ERC8167はselector単位でdelegateを追加、更新、削除できる前提なので、変更の粒度を細かくしやすいです。

さらに、アップグレード用のメソッドを最終的に取り外せば、immutableに近い状態へ寄せるhardeningもできます。
つまり「**最初はUpgradeableに運用し、十分に固まったら変更経路を閉じる**」という運用を取りやすくします。

以下の図は、Upgradeableな段階からhardening後の段階までの見取り図です。

![アップグレードと hardening](/images/qiita-59ba7c61-2c6a-48b1-8c93-3173cdd8d974.webp)

## 仕様

### 実行ルール

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`定義は以下です。

```js
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)`

```js
function implementation(bytes4 selector) external view returns (address);
```

この関数は、あるselectorに対応するdelegateのアドレスを返します。
未登録なら `address(0)` を返します。

この関数があれば、ストレージ配置を知らなくても、外部ツールが「**このselectorはどこへ飛ぶのか**」を共通の方法で読めます。

`eth_getStorageAt` でも調べられる場合はありますが、共通 `interface` があればストレージレイアウトの違いを吸収しやすくなります。

#### `selectors()`

```js
function selectors() external view returns (bytes4[] memory);
```

この関数は、現在implementationを持つselector一覧を返します。
ここがERC8167のツール向けの価値の中核です。

selectorだけでは関数名や引数型は分かりません。
しかし、以下の順序で辿ればプロキシのABIを再構成できます。

1. `selectors()` でselector一覧を取得する
    まずプロキシが公開している関数候補を集める。
2. 各selectorに対して `implementation(selector)` を呼ぶ
    どのdelegateがそのselectorを担当しているか対応付ける。
3. delegate側の検証済みABIを取得する
    ソース検証済みならABIを取れますし、未検証なら利用者が補う余地があります。
4. selectorとdelegate ABIを突き合わせてプロキシ全体の関数集合を組み立てる
    同じdelegateを複数selectorが共有していても、この段階でまとめられる。

以下の図は、その発見フローをそのまま表したものです。

![ABI をどう見つけるか](/images/qiita-a3e2b89e-e7db-44ac-9035-7f25740293fd.webp)

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を自由にする代わりに、衝突回避の責任は実装側へ明確に残しています。

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

[\[ERC7201\] 名前空間を用いた効率的なストレージ管理の仕組みを理解しよう！](/articles/64ef137f8bd096681713)

## 互換性

ERC8167は、既存のmodular proxy標準である **ERC2535** を改善する立場を取っています。
主な改善点は以下の3つです。

- diamondという専用用語を外し、より一般的な説明に寄せる
- introspection関数を減らし、より単純な `interface` にする
- upgrade eventを単純化する

以下の図は、その差分を一枚にまとめたものです。

![ERC2535 との違い](/images/qiita-6da36248-f01f-447f-b209-c74bced155d3.webp)

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

[\[ERC2535\] 💎 ダイヤモンドパターンの仕組みを理解しよう！](/articles/51fc6fc998421c4d931a)

ERC8167はERC2535と同じ「**関数selectorごとにlogic moduleへ振り分ける**」という方向性を持ちつつ、標準面ではもっと小さく切っています。
特にツールに必要な最小面だけを残したい、という設計意図が見えます。

既存のmonolithic proxyからの移行手順は以下です。

1. selectorごとのdelegate mappingを埋められる `implementation` へアップグレードする
2. 既存ABIの各関数とintrospection関数についてmappingを埋める
3. その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` の実装 |

以下の図は、参考実装の責務分担をまとめたものです。

![参考実装の役割分担](/images/qiita-c29f2fbc-a73c-42f3-b11e-f27b75d583b0.webp)

#### `ProxyStorageBase.sol`

参考実装では、selectorごとの情報を以下の `struct` で持ちます。

```js
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として登録します。

```js
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 を順に追ったものです。

![constructor の bootstrap](/images/qiita-b363c24e-d99d-4834-b658-b5a8ae3aebfa.webp)

## セキュリティ

#### 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パターンと危険なパターンをまとめたものです。

![ストレージ衝突をどう避けるか](/images/qiita-0135604e-5e70-4d01-b5c0-67b5494dd31e.webp)

衝突回避の方法は大きく2つあります。

1. 全delegateが共通superclassのstorage layoutを共有する
    追加storageを各subclassで勝手に宣言しない前提で運用する。
2. 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の寿命管理は規格外のまま残るので、実装側の責任はむしろ明確に重くなります。

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

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
