Minimal Proxyとは:EIP-1167 cloneのbytecode・initializer・factoryを検証する
EIP-1167 minimal proxyの45-byte runtime、埋め込みimplementation、delegatecall、initializer、CREATE2 factory、immutable args、調査手順をlocalで検証します。

Minimal Proxyは、同じ機能のcontractを何度もfull deployせず、短いproxyを複数作って1つのimplementation codeを共有する方法です。ERC-1167(EIP-1167)は、implementation addressを埋め込んだ標準runtime bytecodeを定めています。
短いcodeだから自動的に安全になるわけではありません。cloneのstorageは空から始まり、implementationのconstructorはclone側で動きません。initializeを別transactionへ残すと、第三者が先にownerを設定できる設計があります。
この記事はaccount、vault、poolなどをclone factoryで量産する開発者と、ExplorerやRPCからcloneを調べる人向けです。chain → clone runtime → implementation address / code hash → delegatecall context → storage layout → initializer state → factory / salt → extension → authorityの順に確認できる状態を目指します。
検証にはSolidity 0.8.36、Foundry / Anvil 1.8.0、OpenZeppelin Contracts 5.6.1、viem 2.56.0を使いました。EVM versionはCancun、chain IDは31346です。隔離したlocal chainだけで実行し、public RPC、mainnet fork、browser wallet、実資産は使っていません。
Full deploymentとminimal cloneの違い
full deploymentは、各instanceへ機能を含むruntime codeを置き、constructorでそのinstanceのstorageを初期化します。minimal cloneは45-byteの小さなruntimeだけを置き、呼び出しを固定implementationへDELEGATECALLします。
固定したlocal結果では、比較用full contractのruntimeは366 bytes、cloneのruntimeは45 bytes、共有implementationは1,079 bytesでした。code sizeは小さくなりますが、cloneごとのstorage、初期化transaction、implementationの検証は別に必要です。
| 項目 | Full deployment | 標準minimal clone |
|---|---|---|
| instanceのruntime | 機能を含むcode | 45-byte redirect code |
| 実行するcode | instance自身 | 固定implementation |
| stateの保存先 | instance | clone |
| constructor | instance作成時に実行 | implementation constructorはcloneで再実行されない |
| 初期値 | constructorで設定できる | initializer等を別途設計する |
| upgrade | contract設計次第 | 標準runtime自体にadmin / upgrade機能はない |
deployment gasの差だけで選ぶと、initializer、implementation dependency、運用上の調査コストを落とします。最初に「codeを共有すること」と「stateを共有すること」を分けてください。clone同士が共有するのはimplementation codeで、通常のstorageはcloneごとに別です。
EIP-1167 runtimeは45 bytes
EIP-1167が示す標準runtimeは次の45 bytesです。<implementation>には20-byte addressが入ります。
363d3d373d3d3d363d73
<20-byte implementation>
5af43d82803e903d91602b57fd5bf3
0始まりのbyte indexでは、implementation addressは10から29までです。opcodeとしてはoffset 9のPUSH20が続く20 bytesをstackへ積み、offset 31のDELEGATECALLがその宛先を使います。revert時はreturndataをcloneのcallerへ戻し、成功時も同様にreturn dataを返します。
localでdeployしたcloneのruntimeは次の値でした。
0x363d3d373d3d3d363d73
e7f1725e7734ce288f8367e1bb143e90bb3f0512
5af43d82803e903d91602b57fd5bf3
抽出した0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512は、実際にdeployしたimplementation addressと一致しました。
TypeScriptでは、長さとprefix / suffixを確認してからaddress部分を切り出せます。
const prefix = '363d3d373d3d3d363d73'
const suffix = '5af43d82803e903d91602b57fd5bf3'
function extractImplementation(runtime: `0x${string}`) {
if ((runtime.length - 2) / 2 !== 45) throw new Error('not 45-byte runtime')
if (!runtime.slice(2).startsWith(prefix)) throw new Error('prefix mismatch')
if (!runtime.slice(2).endsWith(suffix)) throw new Error('suffix mismatch')
return `0x${runtime.slice(2 + 10 * 2, 2 + 30 * 2)}`
}
delegatecallはimplementation codeをcloneのcontextで動かす
Solidityの説明どおり、delegatecallではtargetのcodeをcaller contractのcontextで実行します。ここでcaller contractはcloneです。
| 実行要素 | delegatecall時に使われるもの |
|---|---|
| code | implementation addressのcode |
| storage | cloneのstorage |
address(this) |
clone address |
| balance | clone balance |
msg.sender |
cloneを呼んだ元callerのまま |
msg.value |
cloneへ渡したvalueのまま |
local例では、ownerがcloneのexecute(5)へ123 weiを付けて呼びました。cloneのvalueは77から82へ変わり、address(this)はclone address、lastCallerはowner、clone balanceは123 weiになりました。一方、implementation側のvalueとbalanceは0のままです。
この性質により、implementationとcloneのstorage layoutが一致しないと、別のslotをownerやvalueとして読み書きします。変数名が同じかではなく、slot番号、型、packing、継承順を照合します。
implementationのconstructorはclone storageを初期化しない
constructorは、そのcontractを作るcreation codeの実行中に1回だけ動きます。implementationをdeployしたときのconstructorはimplementation storageを変更しますが、後から作るclone storageにはコピーされません。
local例では、implementationのconstructorが記録したdeployer addressを確認できました。同じgetterを未初期化cloneで読むとzero addressで、ownerもversionもzeroでした。
contract CloneLogic {
address public owner;
uint64 public initializedVersion;
address public constructorSender;
constructor() {
constructorSender = msg.sender; // implementation側だけ
}
function initialize(address initialOwner) external {
require(initializedVersion == 0, "already initialized");
require(initialOwner != address(0), "zero owner");
initializedVersion = 1;
owner = initialOwner; // delegatecallならclone storageへ書く
}
}
Solidityのimmutableも注意が必要です。immutable値はruntime codeへ埋め込まれるため、cloneごとにstorageへ置かれる値ではありません。implementation constructorで決まったimmutableは、そのimplementationを使うcloneから共通に見えます。
initializerはdeployと同じtransactionで完了させる
安全側のfactoryは、clone作成とinitializerをatomicに実行し、どちらかが失敗すればtransaction全体をrevertします。
function deployCloneAndInitialize(
address implementation,
address owner,
uint256 value
) external returns (address instance) {
require(implementation.code.length > 0, "implementation has no code");
instance = Clones.clone(implementation);
CloneLogic(instance).initialize(owner, value);
}
clone()だけを先に実行し、後のtransactionでinitialize()する形には未初期化の時間があります。permissionlessなinitializerなら、mempoolで見た第三者や、単に先に呼んだ第三者がownerになれます。
固定例では、uninitialized cloneへattackerがinitialize(attacker, 1)を先に実行し、ownerになりました。その後の予定ownerによるinitialize(owner, 77)はalready initializedでrevertしました。
| state / failure | 観測 | 対応 |
|---|---|---|
| atomic initialize | deployと設定が同じtransactionで完了 | factoryで一括実行し、失敗時は全体revert |
| uninitialized | owner / versionがzero | 利用開始・送金・権限付与を止める |
| first-caller claim | 想定外addressがowner | incidentとしてfactory transactionとcallerを追う |
| initializer replay | 2回目も通る | version guardを実装する |
| unauthorized reinitialize | owner以外がversionを進められる | authorityとversionを両方検証する |
| implementationにcodeなし | initializeが成功したように見えるがstateが変わらない | clone作成前後にcode length / hashを固定する |
initializeだけでなく、reinitializeV2のような移行関数も対象です。local例ではversion 1 → 2をownerだけに許可し、owner以外、同versionの再実行、初期化の再実行をそれぞれrevertさせました。
codeのないimplementationでは成功に見える場合がある
OpenZeppelin Contracts 5.6.1のClonesは、cloneとcloneDeterministicがimplementationにcodeがあるか確認しないと警告しています。codeのないaddressへのDELEGATECALLは、呼び出し自体がsuccessになっても実行するcodeがなく、stateを変更しません。
local例では、code lengthが0の0x…bEEFをimplementationとして45-byte cloneを作りました。initializerのlow-level callはsuccess = true、return dataは0xでしたが、owner slotはzeroのままでした。owner()を読むlow-level callもsuccessで空dataを返し、ABIで復号できる32-byte値は得られません。
factoryでは少なくとも次を別々に確認します。
- deploy前のimplementation
code.length > 0 - 想定したimplementation runtime code hashとの一致
- clone runtimeから抽出したimplementation addressとの一致
- initializer callのsuccessだけでなく、owner、version、domain等のpost-state
- eventだけでなくtransaction receiptとpost-state
「transactionがsuccess」「factory eventが出た」「initializer callがrevertしなかった」のどれも、初期化完了を単独では証明しません。
deterministic cloneはimplementation・salt・factoryを固定する
cloneDeterministicはCREATE2でcloneを作ります。標準cloneのinit codeにはimplementation addressが入り、CREATE2 addressはfactory、salt、init-code hashから決まります。
clone address = keccak256(
0xff ++ factory ++ salt ++ keccak256(clone_init_code)
)[12:]
local例では、factory 0x5FbD…aa3、implementation 0xe7f1…0512、固定saltから予測した0x5f19…0e40とdeploy後addressが一致しました。同じ組み合わせをもう一度deployするとcollisionでrevertしました。
この一致はlibraryの予測関数だけで確認していません。45-byte runtimeへ10-byteのcreation prefixを付けたclone init codeは55 bytes、init-code hashは0xcb1c…e7c8でした。前掲のEIP-1014式へfactory、32-byte salt、このhashを入れた手計算結果も0x5f19…0e40です。
標準cloneでは通常、initializer calldataはclone init codeに含まれません。そのため、同じimplementation、factory、saltならinitializerのownerやvalueを変えても予定addressは変わらない設計があります。だからこそ、deploy直後に同じtransactionでinitializeする必要があります。
salt、init code、factoryを含むCREATE2の計算とcollisionは、CREATE2 addressを予測する手順で詳しく分けています。
immutable args付きcloneは標準45-byte runtimeと分ける
OpenZeppelin Contracts 5.6.1には、cloneWithImmutableArgsとcloneDeterministicWithImmutableArgsがあります。argsはclone codeの末尾へ付加され、fetchCloneArgsで取得できます。
local例では、標準cloneは45 bytes、64-byteのABI encoded argsを付けたcloneは109 bytesでした。後者の先頭には標準redirect部分がありますが、contract code全体はEIP-1167が示すexact 45-byte runtimeではありません。
| 判定 | 標準clone | immutable-args拡張 |
|---|---|---|
| code length | 45 bytes | 45 bytes + args length |
| args保存 | なし | code末尾 |
| 取得方法 | 該当なし | library固有の取得処理 |
| Explorer判定 | exact bytecode match | extensionとして別判定 |
| CREATE2入力 | 標準clone init code | implementation・argsを含む拡張init code |
custom minimal proxyはprefix、suffix、revert処理、calldata加工、immutable argsの読み方が異なる場合があります。45 bytesでないcodeを、addressらしい20 bytesが見つかったという理由だけで標準cloneと断定しません。
standard clone自体はupgradeableではない
EIP-1167の標準runtimeは、20-byte implementation addressを固定で埋め込みます。admin slot、upgrade function、implementationを書き換えるstorage slotはありません。そのclone単体については、通常のERC-1967 proxyのようにimplementationを変更できません。
local cloneでERC-1967 implementation slotとadmin slotをraw storageから読むと、どちらもzeroでした。調査時は次の違いを先に固定します。
| pattern | implementationの場所 | upgrade経路 | 主なauthority確認 |
|---|---|---|---|
| EIP-1167 standard clone | runtime byte index 10–29 | 標準cloneにはなし | factory、initializer、埋め込みimplementation |
| UUPS proxy | ERC-1967 implementation slot | implementation側のupgrade関数 | upgrade authorizationを実装するrole / owner |
| Transparent proxy | ERC-1967 implementation / admin slot | adminまたはProxyAdmin経由 | admin / ProxyAdmin ownerとadmin fallback制限 |
UUPSとTransparentをcode lengthだけで区別せず、ERC-1967 slot、verified implementation、upgrade function、admin / authorityを合わせて確認します。
ただし、次の依存は残ります。
- 埋め込まれたimplementation codeが正しいか
- cloneとimplementationのstorage layoutが一致するか
- implementation codeが外部contract、oracle、registryへ依存するか
- clone storageに別proxy用slotを持ち、implementation側のcodeがそれを読む設計か
- chain / forkによってcode lifecycleの前提が違わないか
SELFDESTRUCTについて「implementationがいつでも消えてcloneが壊れる」と現在のEthereumへ一律に書くのは不正確です。Cancun以降、既存contractのSELFDESTRUCTは通常codeを削除せず、作成と同じtransactionで実行した場合だけ従来の削除動作を保ちます。fork境界はSELFDESTRUCTとEIP-6780の検証で分けています。
一方、別chain、古いfork、作成transaction内の特殊なlifecycle、最初からcodeのないaddressは別です。調査時はchainとblockを固定し、その時点のeth_getCodeとcode hashを記録します。
ExplorerとRPCでcloneを調べる順番
Explorerの「Proxy」表示だけを結論にせず、raw codeとtransactionを順に確認します。
| 順番 | 確認するもの | 判断できること |
|---|---|---|
| 1 | chain ID、block、clone address | 別network / 別時点の混同を防ぐ |
| 2 | clone eth_getCode、length、code hash |
exact 45-byteか、extension / customか |
| 3 | prefix、bytes 10–29、suffix | 標準runtimeとembedded implementation |
| 4 | implementation code / code hash | codeの有無と想定artifactとの一致 |
| 5 | clone creation transaction、factory | deployer、CREATE / CREATE2、atomic call |
| 6 | initializer calldata、receipt、trace | 誰が何を設定したか |
| 7 | owner、version、domain、重要slot | post-stateが完了したか |
| 8 | storage layoutとverified source | delegatecall先とclone slotの整合 |
| 9 | salt derivation、prediction | deterministic addressの再計算 |
| 10 | authority / external dependency | 後から挙動へ影響できる主体 |
verified sourceがある場合も、表示sourceとon-chain runtimeの一致条件を確認します。compiler、optimizer、metadata、linked libraryを固定してbytecodeを再構築する方法は、verified sourceとruntime bytecodeを照合する記事を参照してください。
ERC-1967、Transparent、UUPS、Beaconとminimal cloneを同じ「proxy」という言葉だけでまとめると調査を誤ります。implementation slot、admin、upgrade authorityの読み分けは、ERC-1967 proxyの調査手順で整理しています。
cloneやproxyが読むstorageをmoduleごとに分ける設計では、namespaceを採用しただけでfield互換性が保証されるわけではありません。ERC-7201 Namespaced Storageのroot・並べ替え・型変更を検証する手順で、通常layoutとnamespace layoutのcollision境界を比較できます。
Factory reviewで残すチェックリスト
deploy前、transaction内、deploy後を分けます。
Deploy前
- chain ID、factory address、factory code hashを固定したか
- implementation address、code length、runtime code hashを固定したか
- cloneとimplementationのstorage layoutが一致するか
- initializerにversion guardとauthorityの検査があるか
- saltの導出規則とcaller namespaceを説明できるか
- standard、immutable args、custom proxyのどれか明示したか
同じtransaction内
- clone deployとinitializeをatomicにしたか
- initializer failureで全体がrevertするか
- initial owner / role / domain / assetがcaller任せになっていないか
- initializer returnだけでなく、期待stateを検査できるか
- CREATE2 collision時のfailureを処理しているか
Deploy後
- clone codeが期待するlength、prefix、implementation、suffixか
- implementation code hashがdeploy前と一致するか
- owner、initializer version、role、重要parameterが期待値か
- event、receipt、trace、post-stateが同じinstanceを指すか
- factoryとimplementationへ影響できるauthorityを記録したか
まとめ
Minimal Proxyの確認順は、chain → clone runtime → implementation address / code hash → delegatecall context → storage layout → initializer state → factory / salt → extension → authorityです。
標準EIP-1167 runtimeは45 bytesで、implementation addressはbyte index 10–29へ埋め込まれます。実行codeはimplementationから借りますが、storage、address(this)、balance、msg.sender、msg.valueはclone側です。
constructorはclone storageを初期化しません。factoryはimplementation codeを検査し、clone作成とinitializeを同じtransactionで完了させ、ownerやversionのpost-stateまで確認します。45 bytesより長いimmutable-args / custom extensionは標準runtimeと分けて判定してください。
確認した一次情報
- ERC-1167 Minimal Proxy Contract確認日: 2026/08/31
- OpenZeppelin Contracts Clones API確認日: 2026/08/31
- OpenZeppelin Contracts 5.6.1 Clones.sol確認日: 2026/08/31
- Solidity delegatecall and libraries確認日: 2026/08/31
- EIP-1014 Skinny CREATE2確認日: 2026/08/31
- EIP-6780 SELFDESTRUCT only in same transaction確認日: 2026/08/31



