# [ERC7511] Minimal Proxy Contractのデプロイコストを下げる仕組みを理解しよう！

> PUSH0 という新しい命令がShanghaiアップグレードで追加されたことで、**ERC1167（Minimal Proxy Contract）**をより効率的に実装できるようになりました。

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

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、`PUSH0` を使い、**ERC1167**のミニマルプロキシをさらに軽量化する仕組みを提案している**ERC7511**についてまとめていきます！

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

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

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

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

## 概要

`PUSH0` という新しい命令がShanghaiアップグレードで追加されたことで、**ERC1167（Minimal Proxy Contract）**をより効率的に実装できるようになりました。
具体的には、デプロイ時のガス消費が約 `200` ガス減り、実行時のガスも約 `5` ガス減っています。
従来の仕組みと同じ動作を維持しながら、より軽くて読みやすいプロキシコードを作れるようになった点が大きなメリットです。

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

[\[ERC1167\] コントラクトを簡単にガス効率良く複製する仕組みを理解しよう！](/articles/85f77603ed33e3eaab07)

**Minimal Proxy Contract**は、既存のロジックを持つコントラクトを参照して必要最小限の機能だけを持つ「**軽量なコピー**」を作るための仕組みです。
この仕組みに `PUSH0` を活用することで、無駄な命令を減らして処理をシンプルにできます。

## 動機

#### コントラクトのバイトコードを 1 バイト削減する

従来の**ERC1167**では、アドレスやスタックの並び替えに使う `SWAP` 命令が含まれていました。
この命令は処理を行うためには必要ではあるものの、`PUSH0` を利用するとそもそも `SWAP` を使わなくても成立する構成を作ることができます。

`PUSH0` はスタックに「`0`」を積むだけの命令で、これをうまく利用するとスタック操作の組み立てがシンプルになるため、結果として `SWAP` が不要になります。
`SWAP` を1つ取り除くだけで、生成されるコントラクトのバイトコードが `1` バイト短くなります。

**Minimal Proxy Contract**は大量にデプロイされるケースが多いため、この `1` バイト削減には実用的な価値があります。

#### 実行時ガスを削減する

従来のプロキシ実装では、元のコントラクトを呼び出すために `DUP`（スタックの値を複製する命令）が2回必要でした。
`DUP` は `3` ガスかかりますが、`PUSH0` を使う構成に変えるとこの複製処理をより軽量にできます。

`PUSH0` のコストは `2` ガスのため、2回使用すると合計 `4` ガスです。
一方、`DUP` を2回使うと `6` ガスかかります。
これにより、1回の実行で `2` ガス軽くなり、その他の命令構成の見直しと合わせて約 `5` ガス削減できます。

ガス消費は長期運用や多数の呼び出しが発生する環境では積み重なるため、この削減は意味があります。

#### プロキシコードの読みやすさを改善する

`PUSH0` は「**ゼロをスタックに積む**」という非常に単純な命令です。
この特徴によって、スタック操作の流れをより自然に組み立てられるようになります。
**Minimal Proxy Contract**の構造を `PUSH0` を前提に作り直すと、不必要なスタック操作が減り、動作の意図をコードから理解しやすくなります。

従来の**ERC1167**コードは、最小化を優先した結果、必ずしも読みやすくありませんでした。
`PUSH0` を使った新しい構成では、同じ動きを保ちながら、より直感的で理解しやすいコードに整理できます。

## 仕様

#### 標準プロキシコントラクト（Standard Proxy Contract）

最適化された**Minimal Proxy Contract**のランタイムコードは次の `44` バイトです。

```
365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3
```

このうち、インデックス `9`〜`28` の `20` バイトが実装元コントラクト（implementation contract）のアドレスに置き換えられます。
これがプロキシの目的である「**別のコントラクトを参照して動作する仕組み**」を実現します。

#### ランタイムコードのディスアセンブル

以下の表では、バイトコードを命令単位に分解して命令の意味とスタックの状態を示しています。

| `pc` |  `op`  | 説明  | スタック    |
| -: | :--: | :--------------------------- | :----------------- |
| 00 |  36  | `CALLDATASIZE`（呼び出しデータのサイズを取得） | cds   |
| 01 |  5f  | `PUSH0`（0 を積む）      | 0 cds   |
| 02 |  5f  | `PUSH0`                        | 0 0 cds     |
| 03 |  37  | `CALLDATACOPY`（calldata をコピー）  | |
| 04 |  5f  | `PUSH0`                        | 0      |
| 05 |  5f  | `PUSH0`                        | 0 0                |
| 06 |  36  | `CALLDATASIZE`                 | cds 0 0            |
| 07 |  5f  | `PUSH0`                        | 0 cds 0 0          |
| 08 |  73… | `PUSH20` <実装元アドレス> | impl 0 cds 0 0     |
| 1d |  5a  | `GAS`                          | gas impl 0 cds 0 0 |
| 1e |  f4  | `DELEGATECALL`（実装元へ委譲呼び出し）     | suc    |
| 1f |  3d  | `RETURNDATASIZE`               | rds suc            |
| 20 |  5f  | `PUSH0`                        | 0 rds suc          |
| 21 |  5f  | `PUSH0`                        | 0 0 rds suc        |
| 22 |  3e  | `RETURNDATACOPY`               | suc                |
| 23 |  5f  | `PUSH0`                        | 0 suc              |
| 24 |  3d  | `RETURNDATASIZE`               | rds 0 suc          |
| 25 |  91  | `SWAP2`                        | suc 0 rds          |
| 26 | 602a | `PUSH1` 0x2a（JUMPDEST の位置を積む） | 0x2a suc 0 rds |
| 27 |  57  | `JUMPI`（成功/失敗で分岐）    | 0 rds |
| 29 |  fd  | `REVERT`（失敗時の終了）    |       |
| 2a |  5b  | `JUMPDEST`（成功時の復帰位置）   | 0 rds   |
| 2b |  f3  | `RETURN`（結果を返す）       |                    |

コード全体は、呼び出しデータのコピー → 実装元への `DELEGATECALL` → 戻り値のコピー → `RETURN`/`REVERT` の流れで構成されます。
`PUSH0` の採用により、従来の `DUP` などを使わずにスタック操作を簡潔に表現しています。

#### 最小クリエーションコード（Minimal Creation Code）

デプロイ時に使用されるコードは以下の `53` バイトです。

```
602c8060095f395ff3365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3
```

先頭の `9` バイトが `initcode` で、残りがランタイムコードとしてコントラクトに保存されます。

```
602c8060095f395ff3
```

`initcode` の役割は、ランタイムコードをメモリに配置し、そのまま `RETURN` するシンプルなものです。
最小限の処理だけでプロキシを展開できるよう設計されています。

#### デプロイ

以下のSolidityコントラクトは、`PUSH0` を利用した**Minimal Proxy Contract**をEthereum上にデプロイします。

```js
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

// Note: this contract requires `PUSH0`, which is available in solidity > 0.8.20 and EVM version > Shanghai
contract Clone0Factory {
    error FailedCreateClone();

    receive() external payable {}

    /**
     * @dev Deploys and returns the address of a clone0 (Minimal Proxy Contract with `PUSH0`) that mimics the behaviour of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     */
    function clone0(address impl) public payable returns (address addr) {
        // first 18 bytes of the creation code 
        bytes memory data1 = hex"602c8060095f395ff3365f5f375f5f365f73";
        // last 15 bytes of the creation code
        bytes memory data2 = hex"5af43d5f5f3e5f3d91602a57fd5bf3";
        // complete the creation code of Clone0
        bytes memory _code = abi.encodePacked(data1, impl, data2);

        // deploy with create op
        assembly {
            // create(v, p, n)
            addr := create(callvalue(), add(_code, 0x20), mload(_code))
        }

        if (addr == address(0)) {
            revert FailedCreateClone();
        }
    }
}
```

このコントラクトは、creation codeの前半（`18` バイト）、後半（`15` バイト）、そして実装元アドレスを結合して、完全なcreation codeを生成します。
そのコードを `create` 命令でデプロイし、新しい**Minimal Proxy Contract**のアドレスを返します。

`PUSH0` を前提にしているため、Solidity 0.8.20以降かつEVMがShanghai以降である必要があります。

## 補足

最適化された**Minimal Proxy Contract**は、プロキシに必要な最小限の処理だけを組み込みつつ、`PUSH0` を活用することでコード量とガス消費を削減しています。
ここでは、プロキシを構成する基本要素と、それをどのように効率的に組み立てているのかを手順ごとに説明します。

プロキシの主な流れは以下の4つです。

1. `CALLDATACOPY` で `calldata` をコピーする
2. `DELEGATECALL` で実装元コントラクトへ処理を委譲する
3. `RETURNDATACOPY` で返されたデータをコピーする
4. 成功か失敗かに応じて `RETURN` か `REVERT` を実行する

以下では、それぞれの手順をバイトコードとスタック操作を交えて解説します。

#### 手順 1：`Calldata` のコピー

最初に、呼び出し時に渡された `calldata` をメモリにコピーします。
`CALLDATACOPY` には3つの引数が必要で、順に「**コピー先のメモリ位置**」、「**コピー元の開始位置**」、「**コピーサイズ（calldata size）**」です。
ここでは `[0, 0, cds]` を積む必要があります。

| pc |  op | 説明                            | スタック    |
| -: | :-: | :---------------------------- | :------ |
| 00 |  36 | `CALLDATASIZE`（calldata のサイズ取得） | cds     |
| 01 |  5f | `PUSH0`                         | 0 cds   |
| 02 |  5f | `PUSH0`                         | 0 0 cds |
| 03 |  37 | `CALLDATACOPY`                  |         |

`PUSH0` により0を積み、引数を揃える流れが従来より簡潔になっています。

#### 手順 2：Delegatecall の実行

`calldata` を実装元コントラクトに渡すため、`DELEGATECALL` を準備します。
`DELEGATECALL` には6つの引数（ガス量、対象アドレス、入力位置、入力サイズ、出力位置、出力サイズ）が必要ですが、このプロキシは入力・出力の位置を0とし、利用するガスは `GAS` 命令で取得、アドレスは `PUSH20` で積みます。

| pc |  op | 説明              | スタック               |
| -: | :-: | :-------------- | :----------------- |
| 04 |  5f | `PUSH0`           | 0                  |
| 05 |  5f | `PUSH0`           | 0 0                |
| 06 |  36 | `CALLDATASIZE`    | cds 0 0            |
| 07 |  5f | `PUSH0`           | 0 cds 0 0          |
| 08 | 73… | `PUSH20`（実装元アドレス） | impl 0 cds 0 0     |
| 1d |  5a | `GAS`（残りガス取得）     | gas impl 0 cds 0 0 |
| 1e |  f4 | `DELEGATECALL`    | suc                |

`DELEGATECALL` の結果は `suc`（成功:1 / 失敗:0）としてスタックに残ります。
この値を後の分岐に使います。

#### 手順 3：返り値のコピー

`DELEGATECALL` の返り値（returndata）は `RETURNDATACOPY` でメモリにコピーします。
`RETURNDATACOPY` の引数は [0, 0, rds] で、`rds` は `RETURNDATASIZE` 命令で取得します。

| pc |  op | 説明                     | スタック        |
| -: | :-: | :--------------------- | :---------- |
| 1f |  3d | `RETURNDATASIZE`（返り値サイズ） | rds suc     |
| 20 |  5f | `PUSH0`                  | 0 rds suc   |
| 21 |  5f | `PUSH0`                  | 0 0 rds suc |
| 22 |  3e | `RETURNDATACOPY`         | suc         |

ここでも `PUSH0` を使うことで、従来の `DUP` 指令を不要にし、スタック操作を最小限にしています。

#### 手順 4：`Return` または `Revert` の分岐

`DELEGATECALL` の成功または失敗に応じて結果を返すか、処理を巻き戻す必要があります。
しかし、EVMにはif/elseのような構文がないため、`JUMPI` と `JUMPDEST` を用いて条件分岐を構築します。

分岐のために `suc` をスタックの適切な位置に調整し、さらに `RETURN` と `REVERT` が必要とする引数 `[0, rds]` を共通で準備します。
`rds` は `DELEGATECALL` 後にしか取得できないため、`SWAP2` が必須になります。

| pc |  op  | 説明                   | スタック           |
| -: | :--: | :------------------- | :------------- |
| 23 |  5f  | `PUSH0`                | 0 suc          |
| 24 |  3d  | `RETURNDATASIZE`       | rds 0 suc      |
| 25 |  91  | `SWAP2`（スタック調整）        | suc 0 rds      |
| 26 | 602a | `PUSH1` 0x2a（成功時ジャンプ先） | 0x2a suc 0 rds |
| 27 |  57  | `JUMPI`（成功なら 0x2a へ）   | 0 rds          |
| 29 |  fd  | `REVERT`               |                |
| 2a |  5b  | `JUMPDEST`             | 0 rds          |
| 2b |  f3  | `RETURN`               |                |

この構造により、成功時は `RETURN`、失敗時は `REVERT` が実行されます。

#### 最終的なランタイムコード

手順を積み重ねて完成したランタイムコードが以下です。

```
365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3
```

全体で **44 バイト** となり、従来より 1 バイト短縮されています。

この最適化により、次のメリットがあります。

* デプロイ時に約 **200 gas 削減**
* 実行時に約 **5 gas 削減**
* PUSH0 によりスタック操作が簡潔になり、コードの読みやすさが向上

機能そのものは以前の**Minimal Proxy Contract**（**ERC1167**）と同じでありながら、サイズ・ガスコストの両面で効率化されています。

## 互換性

`PUSH0` を使った新しい**Minimal Proxy Contract は、`PUSH0` オペコードが前提になっています。
`PUSH0` はShanghaiアップグレードで追加された命令なので、Shanghai以降のEVM環境でしかデプロイできません。

つまり、以下のような制約があります。

* Shanghaiより前のチェーン状態では、このプロキシはデプロイできない
* EVMがShanghaiに対応していないL2 / サイドチェーンなどでも動作しない

一方で、動作そのものは従来の**Minimal Proxy Contract**（**ERC1167**）と同じです。
`PUSH0` を使うことでコードサイズとガスが削減されただけで、プロキシとしての振る舞い（`DELEGATECALL` で実装元コントラクトに処理を委譲する動き）は変わりません。

そのため、既存の設計・アーキテクチャはそのままに、環境さえ対応していれば、古い**Minimal Proxy Contract**を新しいものに置き換えることができます。

## セキュリティ

`PUSH0` 対応版のプロキシは、構造・振る舞いともに**ERC1167**と同一です。
したがって、セキュリティ上の注意点も**ERC1167**と共通になります。

ポイントごとに整理します。

| 観点             | 内容                           |
| -------------- | ---------------------------- |
| アップグレード不可 | 一度どの実装コントラクトに委譲するか決めると変更できない |
| 初期化の問題 | コンストラクタが使えず、初期化関数を別途呼ぶ必要がある  |
| ロジックコントラクトの安全性 | 実装側に脆弱性があると、すべてのプロキシに影響する    |
| 透明性の問題 | 表面的には空に見え、実際のロジックが見えづらい   |

#### アップグレード不可（Non-Upgradability）

**Minimal Proxy Contract**は、特定の実装コントラクトに対して `DELEGATECALL` するだけの非常にシンプルな作りになっています。
どのアドレスに対して `DELEGATECALL` するかは、デプロイ時のコード（`PUSH20` で埋め込まれている実装アドレス）で固定され、後から変更する仕組みはありません。

つまり、一度デプロイしたプロキシは、後から「**別の実装コントラクトに差し替える**」ことができません。
一般的なアップグレード可能プロキシ（Admin 付きの Transparent Proxy や UUPS など）とは異なり、**ERC1167**系のプロキシはアップグレード前提の設計ではない、という点を意識して使う必要があります。

#### 初期化の問題

**Minimal Proxy Contract**では、*ンストラクタが使えません。
プロキシ自体はほぼ何も状態を持たず、実際の状態は実装コントラクト側のストレージレイアウトに従って書き込まれます。

そのため、デプロイ後には「**初期化用の関数**」を実装コントラクト側に用意しておき、プロキシ経由で呼び出す必要があります。
この初期化処理を怠ると、例えばオーナーアドレスが未設定のまま残るなど、安全でない状態のまま本番で動いてしまう可能性があります。

要するに、**Minimal Proxy Contract**を使う場合は、
「**デプロイ → 初期化関数を呼ぶ**」というフローをきちんと設計に含める必要があります。

#### ロジックコントラクトの安全性

**Minimal Proxy Contract**は、自身にはロジックをほとんど持たず、実装コントラクト（ロジックコントラクト）にすべての処理を委譲します。

この構造の結果として、もし実装コントラクトに脆弱性があれば、その実装を参照しているすべてのプロキシに影響が及びます。
プロキシ自体を何個デプロイしても、中身のロジックが同じであれば、同じバグを共有している状態になります。

したがって、**Minimal Proxy Contract**を大量にデプロイするようなケースでは、実装コントラクトの監査やテストを十分に行うことが特に重要になります。

#### 透明性の問題

**Minimal Proxy Contract**はバイトコードが極端に短く、Etherscanのようなブロックエクスプローラ上で見ると、「**何もしていないコントラクト**」のように見えることがあります。
実際には `DELEGATECALL` で別のアドレスに処理を飛ばしていますが、それを直感的に追うのはユーザーにとって簡単ではありません。

その結果、ユーザーから見ると、以下が分かりにくくなり透明性が下がるという問題があります。

* どのコントラクトに処理が委譲されているのか
* 実際のロジックがどこにあるのか

運用上は、ドキュメントやUIで実装コントラクトのアドレスを明示したり、「**このアドレスはMinimal Proxyであり、実際のロジックは別アドレスにある**」と説明するなど、補助的な工夫が必要になります。

## 引用

0xAA (@AmazingAng), vectorized (@Vectorized), 0age (@0age), "ERC-7511: Minimal Proxy Contract with PUSH0 [DRAFT]," Ethereum Improvement Proposals, no. 7511, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7511.

## 最後に

今回は「`PUSH0` を使い、**ERC1167**のミニマルプロキシをさらに軽量化する仕組みを提案している**ERC7511**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

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