はじめに

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

今回は、ストレージを使わずに設定値を保持し、さらに現在の実装コントラクトを安全にオンチェーンから取得できる最小構成プロキシの仕組みを提案しているERC7760についてまとめていきます!

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

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

概要

ERC7760は、アップグレード可能なコントラクトで広く使われているERC1967方式のプロキシについて、最小構成の標準バイトコードを定義する規格です。

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

対象となるプロキシのパターンは以下の3つです。

  • Transparent Proxy(トランスペアレント・プロキシ)
  • UUPS(Universal Upgradeable Proxy Standard)
  • Beacon Proxy(ビーコン・プロキシ)

これらはすべて「ロジックコントラクト(実装)」と「ステートを保持するプロキシ」を分離するアップグレード可能アーキテクチャですが、ERC7760ではそれぞれに対して「最小サイズの共通バイトコード実装」を定義しています。

さらに、ERC7760のプロキシは「イミュータブル引数(immutable arguments)」をサポートします。 これは、デプロイ時に固定され、その後変更できない引数のことで、ランタイムバイトコードの末尾に直接埋め込まれます。 この構造により、ストレージを使わずに定数データを保持でき、ガスコスト削減と高速な参照が可能になります。

加えて、オンチェーンで以下を取得できる派生バリアントも定義されています。

  • 現在使われている実装コントラクトのアドレス
  • プロキシに埋め込まれたイミュータブル引数

つまり、ERC7760は以下を同時に満たすプロキシ標準です。

項目 説明
最小バイトコード 余分な処理を省いた極小サイズのプロキシ実装
3方式対応 Transparent / UUPS / Beacon の3パターン
Immutable引数 バイトコード末尾に固定値を格納
実装取得可能 オンチェーンで実装アドレスを取得可能
検証容易性 ブロックエクスプローラでの自動検証が可能

動機

ERC7760が定義された背景は、「アップグレード可能プロキシの実装が多様すぎて、検証・解析・最適化が難しい」という問題です。 その解決のために、以下の利点を実現することが目的です。

ブロックエクスプローラでの自動検証

現在のプロキシは、実装ごとに微妙にバイトコードが異なることが多く、Etherscanなどで自動検証できないケースがあります。 ERC7760は最小構成の標準バイトコードを固定することで、同一ハッシュのプロキシが自動的に認識・検証されるようになります。

これにより以下が可能になります。

  • プロキシであることの即時判別
  • 実装アドレスの自動解析
  • フロントエンドや監査ツールの互換性向上

イミュータブル引数をオンチェーンで取得可能

イミュータブル引数はバイトコードの末尾に配置されます。 ERC7760では、そのオフセット位置が標準化されているため、コントラクト内部や外部ツールから安全に読み取れます。

これにより以下が成立します。

  • ストレージを使わずに設定値を保持
  • ガスコスト削減
  • 実装コントラクト側から codecopy などで直接参照可能
  • 引数の取得位置が標準化されることで、ツール側も解析可能

実装コントラクトのオンチェーン検証

通常のプロキシでは、実装アドレスの取得方法が方式ごとに異なります。

方式 実装の保存場所
Transparent ERC1967スロット
UUPS 同上(実装側がupgrade関数を持つ)
Beacon Beaconコントラクト経由

ERC7760では、これらの方式に対して「オンチェーンで実装を問い合わせるための共通インターフェース付き最小プロキシ」を定義しています。 これにより、ブロックエクスプローラやセキュリティツールが、方式に依存せずに実装を取得・検証できます。

最小構成によるガス削減

プロキシはすべての呼び出しが delegatecall を経由するため、バイトコードが小さいほど以下のコストが下がります。

  • デプロイ時のガス
  • 実行時のメモリアクセス
  • キャッシュ効率

ERC7760は以下を徹底しています。

  • upgrade処理を持たない純粋転送ロジック
  • 余分なチェックや抽象化の排除
  • 実装照会機能も最小命令列で実装

結果として、アップグレード可能でありながら、通常のコントラクトに近いガス効率を実現することが目的です。

仕様

一般仕様

すべてのプロキシは、ランタイムバイトコードの末尾に「任意の追加データ(イミュータブル引数)」を付加してもよいと定義されています。 このデータはストレージではなくコード領域に埋め込まれ、ガス効率が良く、オフチェーン・オンチェーン双方から取得可能です。

また、初期化時にERC1967のイベント(ImplementationUpgraded など)を emit するかどうかは任意です。 そのため、インデクサやブロックエクスプローラは「初期化時に必ずイベントが出る」と仮定してはいけません。

I-variantにおける実装アドレスのオンチェーン取得

I-variant(Implementation Query Variant)は、プロキシ自身のバイトコード内に「実装アドレスを返すロジック」を直接持っています。

特徴は以下です。

  1. calldata が1バイト以上で呼ばれた場合。
  2. delegatecall せずに即座に実装アドレスを返します。
  3. 戻り値は32バイトで左側がゼロ埋めされたアドレスです。

つまり、通常の関数呼び出しをせずとも、単なる低レベル呼び出しで「現在の実装」を安全に取得できます。

さらに、返ってきた実装アドレスを信頼するには、以下の検証手順が必須です。

  1. EXTCODECOPY でイミュータブル引数部分を除いたバイトコードを取得します。
  2. バイトコード中に埋め込まれたファクトリアドレスをゼロでマスクします。
  3. その結果のハッシュが、規格で定義された正しいバイトコードハッシュと一致するかを確認します。
  4. 一致しない場合、そのプロキシが返す実装アドレスは信頼してはいけません。

この仕組みにより、「偽のI-variant」が偽の実装を返す攻撃を防止できます。

最小ERC1967 Transparentプロキシ

ERC7760のTransparent Proxyは、アップグレード権限を持つ「ファクトリコントラクト」を前提に設計されています。

このファクトリは以下の特徴を持ちます。

  • プロキシのデプロイ元である。
  • プロキシの実装スロットを書き換える唯一の権限を持つ。
  • アドレスはバイトコードにイミュータブルとして埋め込まれる。

ランタイムコードには「任意のストレージスロットに任意の値を書き込める命令列」が含まれるため、初期化時に実装アドレスをストレージに書き込まなくてもよい場合があります。

また、アップグレード時にもERC1967のイベントは発行しません。 そのため、インデクサはイベントに依存した実装追跡をしてはいけません。

アップグレード時の呼び出し形式は以下です。

js
abi.encodePacked(
    uint256(uint160(implementation)), // 新しい実装アドレス
    bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc), // ERC-1967実装スロット
    "" // 実装にdelegatecallされる初期化データ
)

Transparentプロキシ

20バイトFactory版
text
3d3d3373________________________________________14605757363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d356020355560408036111560525736038060403d373d3d355af43d6000803e6052573d6000fd
14バイトFactory版
text
3d3d336d____________________________14605157363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e604c573d6000fd5b3d6000f35b3d3560203555604080361115604c5736038060403d373d3d355af43d6000803e604c573d6000fd

アドレス長を短縮することで、さらにバイトコードを削減しています。

Transparentプロキシ(I-variant)

実装アドレス照会ロジックが組み込まれた版です。

20バイトFactory版
text
3658146083573d3d3373________________________________________14605D57363d3d37363D7f360894a13ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6058573d6000fd5b3d6000f35b3d35602035556040360380156058578060403d373d3d355af43d6000803e6058573d6000fd5b602060293d393d51543d52593df3
14バイトFactory版
text
365814607d573d3d336d____________________________14605757363d3D37363d7F360894A13Ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d35602035556040360380156052578060403d373d3d355af43d6000803e6052573d6000fd5b602060233d393d51543d52593df3

最小ERC1967 UUPSプロキシ

UUPS型はプロキシ側にアップグレードロジックを持たないため、初期化時に必ず以下のスロットへ実装を格納します。

text
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
Basic Variant
text
363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3
I-variant
text
365814604357363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e603e573d6000fd5b3d6000f35b6020600f3d393d51543d52593df3

最小ERC1967 Beaconプロキシ

Beacon方式もプロキシ自体にアップグレード機能を持たないため、初期化時に必ず実装スロットへBeaconアドレスを格納します。

Basic Variant
text
363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3
I-variant
text
363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa361460525736515af43d600060013e6052573d6001fd5b3d6001f3

補足

PUSH0命令を使用しない理由

ERC7760のプロキシは、EIP3855で導入された PUSH0 オペコードをあえて使用していません。 理由は、より広いEVM互換性を維持するためです。

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

PUSH0 は比較的新しい命令であり、古いチェーンや一部の互換EVMでは未対応の場合があります。 そのため、現時点で最大限の互換性を確保する目的で、すべてのチェーンで確実に動作する既存命令のみを使っています。

将来的に PUSH0 を用いたさらに最適化されたバイトコードを定義する場合は、別のERCとして切り出す方針です。

最適化の優先順位

ERC7760の最適化方針は以下です。

  • 実行時ガスコストの最小化
  • バイトコードサイズの最小化

まずランタイムでの delegatecall や分岐コストを極限まで削減し、その上で可能な限りコードサイズも削るという順序です。 これは、プロキシは毎回の呼び出しで必ず実行されるため、デプロイコストよりも実行コストの削減が長期的に重要だからです。

最小構成であることの意味

これらのプロキシはすべて、手書きのEVMバイトコードで設計されています。 現時点で理論上かなり最小に近い構成ですが、将来さらに短縮できる可能性はあります。

ただし、すでに実運用で使われたバイトコードについては、わずかなガス削減よりも以下の価値が優先されます。

  • バイトコードハッシュの固定による自動検証
  • ブロックエクスプローラでの即時識別
  • 既存ツールとの互換性

過去の例として、ERC1167のMinimal Proxyも当時の理論最小ではなく、その後「0age minimal proxy」の方が小さく高速であることが分かりました。 それでもERC1167が標準として残っているのは、エコシステム全体での互換性と検証容易性が圧倒的に重要だからです。 ERC7760も同じ思想で、安定したバイトコードレイアウトを重視しています。

Transparentアップグレードプロキシにおける設計理由

ファクトリアドレスの埋め込み

Transparent Proxyでは、アップグレード権限を持つファクトリのアドレスがバイトコードに直接埋め込まれます。 これにより、毎回の呼び出しで SLOAD によって管理者アドレスを読む必要がなくなり、ガスが節約されます。

また、ファクトリ側に以下のようなロジックを実装できます。

  • マルチシグによる承認
  • 管理者のローテーション
  • タイムロック制御

そのため、アドレスを固定しても柔軟性は失われません。

任意スロット書き換え型のアップグレード命令

アップグレード命令は以下のように設計されています。

  • 任意の32バイト値
  • 任意の32バイトスロット

を指定して書き込めます。 これは以下の理由によります。

  • バイトコードを短くできる。
  • 将来の拡張スロットにも対応できる。
  • セキュリティは実装側で制御されるため、制限する必要がない。

実装コントラクトはもともと delegatecall により全ストレージへ書き込み可能であるため、ここで柔軟性を削っても安全性は向上しません。

14バイトファクトリアドレスのサブバリアント

ファクトリを先頭ゼロ付きの「バニティアドレス」でデプロイすると、プロキシのバイトコードをさらに短くできます。 6バイト分がゼロであれば、アドレス長は14バイトで済みます。

14バイトが選ばれた理由は以下のバランスです。

観点 内容
マイニング難易度 先頭ゼロが多すぎると生成が困難
バイトコード削減 14バイトで十分に短縮効果が出る

そのため、現実的な探索コストと最適化効果のバランス点として14バイト版が定義されています。

I-variantの設計意図

I-variantは、実装アドレスをオンチェーンで安全に取得できるプロキシです。

バイトコードの先頭が標準と一致していれば、以下が保証されます。

  • 実装アドレスはコードに焼き込まれている。
  • 外部から改ざんされていない。
  • 返却値は偽装できない。
1バイト calldata を使う理由

実装照会は「1バイトの calldata」で呼び出します。 これは以下の理由によります。

種類 calldata 長
ETH送金 0バイト
Solidity関数呼び出し 4バイト以上
実装照会 1バイト

これにより、通常の関数呼び出しと衝突せず、かつ最小コストで分岐できます。

イベントを出力しない理由

アップグレード時や初期化時にERC1967のイベントを発行しない理由は以下です。

  • バイトコードを短くできる。
  • デプロイガスが減る。
  • 実行時コストも下がる。

現在のブロックエクスプローラやインデクサは、イベントに頼らずともERC1967スロットを直接読み取ることで実装を追跡できます。 そのため、イベントは機能的に必須ではありません。

Immutable引数をcalldataに付与しない理由

一部の標準では、追加データをcalldata末尾に連結する方式があります。 ERC7760はこれを採用しません。

理由は以下です。

  • 既存ERCとの互換性問題を避けるため。
  • セキュリティ上、calldata 改変は予期せぬ副作用を生むため。

代わりに、以下の方法で取得します。

text
EXTCODECOPY

により、バイトコード末尾から直接読み出します。

初期化コードを固定しない理由

ERC7760は初期化コードを規定しません。 必要なのは以下のみです。

  • UUPSおよびBeaconの場合、ERC1967の実装スロットを正しく初期化すること。

それ以外のロジックは、プロジェクトごとに自由に設計できます。

スコープ外とされた要素

以下はあえて標準化の対象外とされています。

対象 理由
プロキシファクトリ 認可方式やデプロイ戦略が多様
I-variant検証ロジック チェーンや用途ごとに異なる
Beaconコントラクト アップグレード戦略がプロジェクト依存

これらは標準化しない代わりに、「正しく実装しなければ重大な脆弱性になる」領域でもあります。 そのため、ERC7760はプロキシの最小核のみを定義し、周辺ロジックは設計者の責任で慎重に実装する前提になっています。

参考実装

Minimal ERC-1967 transparent upgradeable proxy implementation

Minimal ERC-1967 transparent upgradeable proxy implementation (basic variant)

ERC1967MinimalTransparentUpgradeableProxyLib.sol
pragma solidity ^0.8.0;

library ERC1967MinimalTransparentUpgradeableProxyLib {
    function initCodeFor20ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes13(0x607f3d8160093d39f33d3d3373),
            address(this),
            bytes32(0x14605757363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc),
            bytes32(0x3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b),
            bytes32(0x3d356020355560408036111560525736038060403d373d3d355af43d6000803e),
            bytes7(0x6052573d6000fd)
        );
    }

    function initCodeFor14ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes13(0x60793d8160093d39f33d3d336d),
            uint112(uint160(address(this))),
            bytes32(0x14605157363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc),
            bytes32(0x3735a920a3ca505d382bbc545af43d6000803e604c573d6000fd5b3d6000f35b),
            bytes32(0x3d3560203555604080361115604c5736038060403d373d3d355af43d6000803e),
            bytes7(0x604c573d6000fd)
        );
    }

    function initCode() internal view returns (bytes memory) {
        if (uint160(address(this)) >> 112 != 0) {
            return initCodeFor20ByteFactoryAddress();
        } else {
            return initCodeFor14ByteFactoryAddress();
        }
    }

    function deploy(address implementation, bytes memory initializationData)
        internal
        returns (address instance)
    {
        bytes memory m = initCode();
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed.");
        upgrade(instance, implementation, initializationData);
    }

    function upgrade(address instance, address implementation, bytes memory upgradeData) internal {
        (bool success,) = instance.call(
            abi.encodePacked(
                // The new implementation address, converted to a 32-byte word.
                uint256(uint160(implementation)),
                // ERC-1967 implementation slot.
                bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc),
                // Optional calldata to be forwarded to the implementation
                // via delegatecall after setting the implementation slot.
                upgradeData
            )
        );
        require(success, "Upgrade failed.");
    }
}

Minimal ERC-1967 transparent upgradeable proxy implementation (I-variant)

ERC1967IMinimalTransparentUpgradeableProxyLib.sol
pragma solidity ^0.8.0;

library ERC1967IMinimalTransparentUpgradeableProxyLib {
    function initCodeFor20ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes19(0x60923d8160093d39f33658146083573d3d3373),
            address(this),
            bytes20(0x14605D57363d3d37363D7f360894a13ba1A32106),
            bytes32(0x67c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e60),
            bytes32(0x58573d6000fd5b3d6000f35b3d35602035556040360380156058578060403d37),
            bytes32(0x3d3d355af43d6000803e6058573d6000fd5b602060293d393d51543d52593df3)
        );
    }

    function initCodeFor14ByteFactoryAddress() internal view returns (bytes memory) {
        return abi.encodePacked(
            bytes19(0x608c3d8160093d39f3365814607d573d3d336d),
            uint112(uint160(address(this))),
            bytes20(0x14605757363d3D37363d7F360894A13Ba1A32106),
            bytes32(0x67c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e60),
            bytes32(0x52573d6000fd5b3d6000f35b3d35602035556040360380156052578060403d37),
            bytes32(0x3d3d355af43d6000803e6052573d6000fd5b602060233d393d51543d52593df3)
        );
    }

    function initCode() internal view returns (bytes memory) {
        if (uint160(address(this)) >> 112 != 0) {
            return initCodeFor20ByteFactoryAddress();
        } else {
            return initCodeFor14ByteFactoryAddress();
        }
    }

    function deploy(address implementation, bytes memory initializationData)
        internal
        returns (address instance)
    {
        bytes memory m = initCode();
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed.");
        upgrade(instance, implementation, initializationData);
    }

    function upgrade(address instance, address implementation, bytes memory upgradeData) internal {
        (bool success,) = instance.call(
            abi.encodePacked(
                // The new implementation address, converted to a 32-byte word.
                uint256(uint160(implementation)),
                // ERC-1967 implementation slot.
                bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc),
                // Optional calldata to be forwarded to the implementation
                // via delegatecall after setting the implementation slot.
                upgradeData
            )
        );
        require(success, "Upgrade failed.");
    }
}

Minimal ERC-1967 UUPS proxy implementation

Minimal ERC-1967 UUPS proxy implementation (basic variant)

ERC1967MinimalUUPSProxyLib.sol
pragma solidity ^0.8.0;

library ERC1967MinimalUUPSProxyLib {
    function initCode(address implementation, bytes memory args)
        internal
        pure
        returns (bytes memory)
    {
        uint256 n = 0x003d + args.length;
        require(n <= 0xffff, "Immutable args too long.");
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            implementation,
            bytes2(0x6009),
            bytes32(0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076),
            bytes32(0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3),
            args
        );
    }

    function deploy(address implementation, bytes memory args)
        internal
        returns (address instance)
    {
        bytes memory m = initCode(implementation, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed.");
    }
}

Minimal ERC-1967 UUPS proxy implementation (I-variant)

ERC1967IMinimalUUPSProxyLib.sol
pragma solidity ^0.8.0;

library ERC1967IMinimalUUPSProxyLib {
    function initCode(address implementation, bytes memory args)
        internal
        pure
        returns (bytes memory)
    {
        uint256 n = 0x0052 + args.length;
        require(n <= 0xffff, "Immutable args too long.");
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            implementation,
            bytes23(0x600f5155f3365814604357363d3d373d3d363d7f360894),
            bytes32(0xa13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af4),
            bytes32(0x3d6000803e603e573d6000fd5b3d6000f35b6020600f3d393d51543d52593df3),
            args
        );
    }

    function deploy(address implementation, bytes memory args)
        internal
        returns (address instance)
    {
        bytes memory m = initCode(implementation, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed.");
    }
}

Minimal ERC-1967 beacon proxy implementation

Minimal ERC-1967 beacon proxy implementation (basic variant)

ERC1967MinimalBeaconProxyLib.sol
pragma solidity ^0.8.0;

library ERC1967MinimalBeaconProxyLib {
    function initCode(address beacon, bytes memory args) internal pure returns (bytes memory) {
        uint256 n = 0x0052 + args.length;
        require(n <= 0xffff, "Immutable args too long.");
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            beacon,
            bytes23(0x60195155f3363d3d373d3d363d602036600436635c60da),
            bytes32(0x1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6c),
            bytes32(0xb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3),
            args
        );
    }

    function deploy(address beacon, bytes memory args) internal returns (address instance) {
        bytes memory m = initCode(beacon, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed.");
    }
}

Minimal ERC-1967 beacon proxy implementation (I-variant)

ERC1967IMinimalBeaconProxyLib.sol
pragma solidity ^0.8.0;

library ERC1967IMinimalBeaconProxyLib {
    function initCode(address beacon, bytes memory args) internal pure returns (bytes memory) {
        uint256 n = 0x0057 + args.length;
        require(n <= 0xffff, "Immutable args too long.");
        return abi.encodePacked(
            bytes1(0x61),
            uint16(n),
            bytes7(0x3d8160233d3973),
            beacon,
            bytes28(0x60195155f3363d3d373d3d363d602036600436635c60da1b60e01b36),
            bytes32(0x527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b3513),
            bytes32(0x3d50545afa361460525736515af43d600060013e6052573d6001fd5b3d6001f3),
            args
        );
    }

    function deploy(address beacon, bytes memory args) internal returns (address instance) {
        bytes memory m = initCode(beacon, args);
        assembly {
            instance := create(0, add(m, 0x20), mload(m))
        }
        require(instance != address(0), "Deployment failed.");
    }
}

セキュリティ

Transparentアップグレードプロキシのファクトリに関する安全性

ERC7760のTransparentアップグレードプロキシでは、アップグレード権限はプロキシ自身ではなく、バイトコードに埋め込まれたファクトリコントラクトが保持します。 そのため、システム全体の安全性は「ファクトリのアクセス制御が正しく設計されているか」に強く依存します。

具体的には、以下が満たされている必要があります。

  • 実装コントラクトの変更(アップグレード)を実行できるアカウントが厳密に制限されていること。
  • 不正な外部アカウントが、ストレージスロットを書き換える命令を実行できないこと。
  • マルチシグ、タイムロック、ロール管理などの仕組みにより、単一鍵の漏洩で即座に支配されない構成になっていること。

Transparent Proxyは、アップグレード時に「任意のストレージスロットへ任意の値を書き込める」強力な命令を持ちます。 これは柔軟性と最小バイトコードを両立するための設計ですが、裏を返すと以下の意味を持ちます。

状態 結果
ファクトリが安全 正常に実装のみが更新される
ファクトリが侵害 全ストレージが自由に改ざん可能

したがって、ERC7760を用いる場合、最も重要なセキュリティ境界は「プロキシ」ではなく「ファクトリの認可設計」です。 ここが弱いと、どれほど安全な実装コントラクトであっても、意味を失います。

I-variantにおけるcalldata長衝突の問題

I-variantは、実装アドレスを取得するために「calldata の長さが1バイトである呼び出し」を特別な意味として予約しています。 この場合、通常の delegatecall は行われず、即座に実装アドレスが返されます。

この設計は以下の理由で選ばれています。

  • 0バイトはETH送金と衝突するため使えない。
  • 4バイト以上は通常の関数セレクタと衝突する。
  • 1バイトは最も安く、安全に予約できる境界である。

ただし、この仕様には互換性上の注意点があります。

もし実装コントラクト側が、

  • 1バイトの calldata を意味のある入力として解釈する。
  • 低レベルで msg.data.length == 1 を使った独自プロトコルを実装している。

といった特殊な設計をしている場合、I-variantのプロキシ経由ではその呼び出しが実装に届かず、常に「実装アドレス照会」として処理されてしまいます。

つまり以下の衝突が起こります。

呼び出し意図 プロキシの挙動
1バイト引数で関数呼び出し 実装に転送されず、実装アドレスが返る

この問題は一般的なSolidityコントラクトではほぼ発生しません。 なぜなら、通常の関数呼び出しは必ず4バイトの関数セレクタを含むため、calldata 長が1になることはありません。

しかし、以下のようなケースでは注意が必要です。

  • アセンブリ主体で設計された低レベルプロトコル。
  • calldataをそのままバイナリストリームとして解釈する特殊用途コントラクト。
  • レイヤー2やクロスチェーン通信で独自エンコードを行う実装。

ERC7760のI-variantを使う場合、実装側が「1バイト calldata を意味のある入力として使わない」ことを設計段階で保証する必要があります。

最後に

今回は「ストレージを使わずに設定値を保持し、さらに現在の実装コントラクトを安全にオンチェーンから取得できる最小構成プロキシの仕組みを提案しているERC7760」についてまとめてきました! いかがだったでしょうか?

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

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