# [ERC7936] 複数バージョンを選べるプロキシの仕組みを理解しよう！

> ERC7936は、スマートコントラクトのプロキシ（実体のロジックを別コントラクトに委ねる仕組み）に「バージョンを選んで呼び出せる」という考え方を取り入れた標準を提案しています。

- URL: https://cardene.dev/articles/81a8458d820f52efc388
- 公開日: 2025-11-08
- 更新日: 2025-11-08
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、複数のコントラクトの実装バージョンを切り替えて使えるプロキシを定義し、利用者が安心して好きなバージョンを選べるようにする仕組みを提案している**ERC7936**についてまとめていきます！

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

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

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ERC7936**は、スマートコントラクトの**プロキシ**（実体のロジックを別コントラクトに委ねる仕組み）に「**バージョンを選んで呼び出せる**」という考え方を取り入れた標準を提案しています。
一般的なプロキシでは、利用者は常に「**最新の実装**」だけを使うことになります。
しかし、仕様変更や機能追加のたびに最新バージョンへ強制的に切り替わると、既存の利用者や外部システムが動かなくなることがあります。

**ERC7936**では、プロキシ内部に「**バージョン番号と実装アドレスを結びつけた登録情報（レジストリ）**」を持たせます。
利用者は、その登録情報の中から使いたいバージョンを選び、そのバージョンに対応した実装へ直接アクセスできます。
これにより、古いバージョンを使い続けながら、新しいバージョンへの移行も段階的に進められる柔軟性が生まれます。

## 動機

スマートコントラクトはブロックチェーン上で長期間動き続けるため、問題の修正や新機能の追加が欠かせません。
しかし、多くのプロキシパターンでは、アップグレード後に「**すべての利用者が新しい実装に切り替わる**」という仕組みになっています。この方式には、いくつかの課題があります。

#### 互換性の問題

新しい実装で関数の定義やデータ構造が変わると、すでに連携しているアプリケーションやサービスが正しく動かなくなることがあります。
特に外部サービスが依存しているインターフェースが変わると、修正には大きな負担がかかります。

#### 新バージョンの段階的な採用ができない

従来のプロキシでは、アップグレード後に「**新しい実装を使うか使わないか**」の二択でした。
利用者が「**試しに新しいバージョンを使ってみる**」といった段階的な移行ができず、リスクを避けたい利用者にとっては扱いづらいものでした。

#### 悪意あるアップグレードのリスク

管理者（またはガバナンス機関）が悪意を持って新しい実装を登録した場合、利用者は最新実装を使わざるを得ないため、意図しない挙動や資産の危険にさらされる可能性があります。
**ERC7936**では、利用者が信頼しているバージョンを選んで使い続けられるため、こうしたリスクを抑えることができます。

#### 信頼の偏り

従来のプロキシでは「**管理者が常に安全なアップグレードを行う**」という前提を利用者が受け入れなければなりませんでした。
管理者の秘密鍵が漏洩したり、ガバナンスが乗っ取られたりすると、すべての利用者が影響を受けます。
**ERC7936**では、利用者自身がどの実装を信頼するか選択できるため、管理者への依存度が下がり、より安全な運用が可能になります。

## 仕様

### インターフェース

```js
interface IVersionedProxy {
    /// @notice Emitted when a new implementation version is registered
    /// @param version The version identifier
    /// @param implementation The address of the implementation contract
    event VersionRegistered(bytes32 version, address implementation);
        
    /// @notice Emitted when the default version is changed
    /// @param oldVersion The previous default version
    /// @param newVersion The new default version
    event DefaultVersionChanged(bytes32 oldVersion, bytes32 newVersion);
    
    /// @notice Registers a new implementation version
    /// @param version The version identifier (e.g., "1.0.0")
    /// @param implementation The address of the implementation contract
    function registerVersion(bytes32 version, address implementation) external;
    
    /// @notice Removes a version from the registry
    /// @param version The version identifier to remove
    function removeVersion(bytes32 version) external;
    
    /// @notice Sets the default version to use when no version is specified
    /// @param version The version identifier to set as default
    function setDefaultVersion(bytes32 version) external;
    
    /// @notice Gets the implementation address for a specific version
    /// @param version The version identifier
    /// @return The implementation address for the specified version
    function getImplementation(bytes32 version) external view returns (address);
    
    /// @notice Gets the current default version
    /// @return The current default version identifier
    function getDefaultVersion() external view returns (bytes32);
    
    /// @notice Gets all registered versions
    /// @return An array of all registered version identifiers
    function getVersions() external view returns (bytes32[] memory);
    
    /// @notice Executes a call to a specific implementation version
    /// @param version The version identifier of the implementation to call
    /// @param data The calldata to forward to the implementation
    /// @return The return data from the implementation call
    function executeAtVersion(bytes32 version, bytes calldata data) external payable returns (bytes memory);
}
```

#### イベント

##### `VersionRegistered`

```javascript
event VersionRegistered(bytes32 version, address implementation);
```

新しい実装バージョンが登録された時に発行されるイベント。
新しいバージョンとして扱うための実装アドレスが紐づけられた時に発行されます。
登録が成功したことを確認するために使います。

**パラメータ**

* `version`
  * バージョンを識別するための値。
* `implementation`
  * 登録された実装コントラクトのアドレス。

##### `DefaultVersionChanged`

```javascript
event DefaultVersionChanged(bytes32 oldVersion, bytes32 newVersion);
```

デフォルトのバージョンが変更された時に発行されるイベント。
プロキシが「**明示されたバージョンがない時に使う実装**」を切り替えた際に発行されます。
利用者が何も指定しなかった場合のアクセス先が変わるため、変更の通知として重要です。

**パラメータ**

* `oldVersion`
  * 変更前のデフォルトバージョン。
* `newVersion`
  * 新しく設定されたデフォルトバージョン。

#### 関数

##### `registerVersion`

```javascript
function registerVersion(bytes32 version, address implementation) external;
```

新しいバージョンを登録する関数。
指定されたバージョン識別子と実装コントラクトのアドレスを関連付けます。
プロキシが新しいバージョンを認識できるようになります。

**引数**

* `version`
  * 登録するバージョン識別子。
* `implementation`
  * そのバージョンで利用される実装コントラクトのアドレス。

##### `removeVersion`

```javascript
function removeVersion(bytes32 version) external;
```

登録済みバージョンを削除する関数。
特定のバージョンをレジストリから取り除きます。
削除したバージョンは以降使用できなくなります。

**引数**

* `version`
  * 削除したいバージョン識別子。

##### `setDefaultVersion`

```javascript
function setDefaultVersion(bytes32 version) external;
```

デフォルトとして扱うバージョンを設定する関数。
明示的にバージョンを指定せずにコールされた場合に使用する実装を指定します。
この設定は、利用者が特定バージョンを指定しないケースで重要になります。

**引数**

* `version`
  * デフォルトに設定したいバージョン識別子。

##### `getImplementation`

```javascript
function getImplementation(bytes32 version) external view returns (address);
```

指定したバージョンの実装アドレスを取得する関数。
バージョン識別子に紐づけられた実装コントラクトのアドレスを返します。

**引数**

* `version`
  * 実装を知りたいバージョン識別子。

**戻り値**

* `address`
  * 指定されたバージョンに対応する実装コントラクトのアドレス。

##### `getDefaultVersion`

```javascript
function getDefaultVersion() external view returns (bytes32);
```

現在のデフォルトバージョンを取得する関数。
プロキシが明示的なバージョン指定なしに使用するバージョン識別子を返します。

**戻り値**

* `bytes32`
  * 現在設定されているデフォルトバージョン。

##### `getVersions`

```javascript
function getVersions() external view returns (bytes32[] memory);
```

登録されているバージョン一覧を取得する関数。
プロキシに登録されたすべてのバージョン識別子を配列として返します。
どのバージョンが利用できるかを確認する時に便利です。

**戻り値**

* `bytes32[]`
  * 登録済みバージョン識別子の一覧。

##### `executeAtVersion`

```javascript
function executeAtVersion(bytes32 version, bytes calldata data) external payable returns (bytes memory);
```

指定したバージョンの実装へ処理を委ねて実行する関数。
呼び出し時に指定されたバージョンの実装コントラクトに対して、`data` をそのまま転送して実行します。
戻り値も同様に返されるため、任意の機能をバージョンごとに利用できます。

**引数**

* `version`
  * 実行したい実装のバージョン識別子。
* `data`
  * 転送するコールデータ。

**戻り値**

* `bytes`
  * 実装コントラクトから返却された生データ。

### 動作要件

プロキシは、以下の要件を満たす必要があります。

まず、バージョン識別子と実装アドレスを紐づけるためのマッピングを保持します。
さらに、バージョンが指定されなかった時に使用するためのデフォルトバージョンを保持します。

`executeAtVersion` が呼び出された場合、指定されたバージョンが登録済みかどうかを検証します。
その後、対応する実装コントラクトへ処理を転送し、戻り値をそのまま返します。
バージョンの登録やデフォルトバージョンの変更が行われた場合は、対応するイベントを発行します。

また、バージョンの登録・削除・デフォルト設定といった管理操作については、適切なアクセス制御を設けることが推奨されています。
既存ツールとの互換性を意識する場合、**EIP1967**のストレージスロット構造を採用しても問題ありません。

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

[\[ERC1967\] コントラクトのアップグレードの仕組みについて理解しよう！](/articles/8a46bae9f23f2315ba4e)

#### フォールバック関数（Fallback Function）

プロキシにはフォールバック関数の実装が推奨されています。
これは、呼び出す関数が特定されない場合にデフォルトバージョンの実装へ処理を転送する仕組みです。
この動作は従来型のプロキシと同じであり、既存の利用者との互換性を保つために重要です。

## 補足

#### バージョン識別子に `bytes32` を採用する理由

**ERC7936**では、バージョン識別子に文字列ではなく `bytes32` を使います。
これは以下の理由からです。

まず、`bytes32` であればバージョンの表現方法を固定せず、開発者が任意の方式で運用できます。
一般的な「`1.0.0`」といったセマンティックバージョン（Semantic Versioning）をそのまま文字列のまま変換して使うこともできますし、数値ベースのバージョン管理にも適しています。

また、文字列と比べてストレージ使用量が少なく、比較処理が単純なためガスコストを減らすことができます。
プロキシはバージョン情報を頻繁に参照するため、コスト削減は重要です。

さらに、GitのコミットIDのような `SHA-1` や `SHA-256` 形式の識別子を格納するケースにも対応できます。
開発の現場では「**特定のコミットをそのままバージョン**」とする運用が行われることもあり、`bytes32` はその柔軟性を自然に支えます。

#### 明示的なバージョン選択

**ERC7936**では、コールデータの中にバージョンを埋め込む設計ではなく、`executeAtVersion` の引数として明示的にバージョンを渡す方式を取ります。
これは、バージョンの切り替えと実際の関数呼び出しをはっきり分けるためです。

関数シグネチャを変更せずに、既存コントラクトの構造をそのまま保てるメリットがあります。
関数本来の役割とは別にバージョンを判定するロジックを埋め込む必要もありません。

この方式にすることで、「**どのバージョンが使われたか**」が明確に記録・検証できるようになります。
透明性が高まり、特に重要な操作を行う時の監査性が向上します。

#### レジストリ方式を採用した理由

バージョン管理の方法には複数の案が考えられますが、**ERC7936**では「**レジストリ方式**」を選択しています。

まず、バージョンごとに複数のプロキシを用意する方法は、デプロイ数が増えてしまうため管理負担が大きくなります。
また、利用者がどのアドレスを使うべきかを都度把握する必要があり、混乱を招きやすい設計です。

次に、「**最新バージョンだけをストレージに保持する方式**」では、同時に複数バージョンを使うことができません。
ある利用者が古いバージョンを使いたい一方で、別の利用者は新しいバージョンを試したい、という状況への対応ができなくなります。

レジストリ方式であれば、複数のバージョンを同時に参照でき、利用者の事情に合わせて最適なバージョンを選択できる柔軟性があります。

#### デフォルトバージョンの仕組み

デフォルトバージョンは、従来のプロキシと同じ挙動を維持するために用意されています。

利用者がバージョンを指定しない場合でも、プロキシはどの実装へ処理を転送すべきかを把握する必要があります。
従来は常に「**最新の実装**」として扱われていた部分を、**ERC7936**では「**デフォルト**」として明確に定義することで、従来のプロキシの使い勝手をそのまま残しています。

この仕組みにより、単に「**普通にプロキシとして使いたいだけ**」という利用者の需要に応える一方で、必要な人にはバージョン指定による柔軟な操作を提供できます。

## 互換性

**ERC7936**は、既存の仕組みやEthereumの標準に対して互換性を損なうものではありません。
プロキシの振る舞いは、フォールバック関数によって引き続きデフォルトのバージョンに処理を転送します。

そのため、従来のプロキシを前提に作られたコントラクトや外部サービスは、特別な修正を加えずにそのまま利用できます。
バージョン指定を使いたい利用者だけが `executeAtVersion` を利用する形になるため、既存のプロジェクト環境に影響が出ません。

この設計によって、アップグレード可能コントラクトの自由度を大きく広げつつ、既存の仕組みを壊さないというバランスが保たれています。

## 引用

Raphina Liu (@Stamp9), Monica Jin (@mokita-j), Martin Monperrus (@monperrus), "ERC-7936: Versioned Proxy Contract Interface [DRAFT]," Ethereum Improvement Proposals, no. 7936, April 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7936.













## 最後に

今回は「複数のコントラクトの実装バージョンを切り替えて使えるプロキシを定義し、利用者が安心して好きなバージョンを選べるようにする仕組みを提案している**ERC7936**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
