Diamond Proxyの読み方:EIP-2535 facet・selector・storage・upgradeを検証する
EIP-2535 Diamondのselector routing、facet、loupe、DiamondCut、initializer、storage pattern、upgrade authorityをローカル環境で検証します。

EIP-2535 Diamondは、1つのcontract addressへ複数の実装contractを組み合わせる仕組みです。機能ごとの実装をfacetと呼び、diamondのfallbackがcalldata先頭4 bytesのfunction selectorを見て、対応するfacetへdelegatecallします。
これは「implementation slotを1つ読む」proxyとは構造が違います。ERC-1967 Proxyをraw slotから読む方法ではimplementation / beacon / adminを分けますが、Diamondはselectorごとに実行先を分けます。同じdiamond addressでも、transferはFacet A、mintはFacet B、loupeはFacet Cというように実行codeが分かれます。stateはfacet側ではなくdiamond側に残るため、routing、facet code、storage layout、upgrade authorityを一緒に読まなければ現在の動作を説明できません。
この記事はDiamondを実装・reviewする開発者と、既存diamondをread-onlyで調べる人向けです。完了条件を chain → diamond → selector → facet → source and runtime → loupe → DiamondCut logs → initializer → storage roots → authority の順に固定します。
検証にはSolidity 0.8.36、Foundry / Anvil 1.8.0、viem 2.56.0を使いました。EVM versionはCancun、optimizerは200 runs、local chain IDは31348です。public RPC、mainnet fork、browser wallet、user key、public transaction、実資産は使っていません。
Diamondはselectorごとにfacetを選ぶ
通常のsingle-implementation proxyは、fallbackから1つのimplementationへほぼすべてのcallを転送します。Diamondでは、diamondがbytes4 selector => address facetに相当するrouting tableを持ちます。
calldata
└─ first 4 bytes = function selector
└─ diamond routing table
└─ selected facet code
└─ delegatecall with diamond address and storage
delegatecallではfacetのcodeを使いますが、address(this)、storage、balanceはdiamondのcontextです。msg.senderとmsg.valueも元のcallから引き継がれます。facet contract自身のstorageへ書く通常callとは分けて考えます。
今回のローカル再現では、V1 facetのsetValue(17)をdiamond addressへ送ると、diamondのslot 0が17、slot 1がversion 1になりました。呼び出し元もdiamondのslot 3へ残り、V1 facet contract自身のslot 0はzeroのままです。codeの場所とstateの所有者が違うことをraw slotで確認できます。
function selectorをsignatureから再計算する
function selectorはcanonical function signatureをKeccak-256へ通した先頭4 bytesです。戻り値の型はsignatureへ含めません。
bytes4(keccak256("setValue(uint256)")) = 0x55241077
配列、tuple、uintとuint256のcanonical表記を誤ると別の値になります。calldataの手読みとcanonical typeの基本はABI・function selector・calldata・event logsを読む方法で確認できます。
4 bytesは約43億通りなので、異なるsignatureが同じselectorになるcollisionは起こり得ます。selectorだけを検索して関数名を確定せず、対象facetのABI、verified source、runtime bytecode、compiler条件まで照合します。
ローカル再現の最終routingから一部を抜き出すと次の通りです。
| signature | selector | final facet |
|---|---|---|
diamondCut((address,uint8,bytes4[])[],address,bytes) |
0x1f931c1c |
DiamondCut facet |
facets() |
0x7a0ed627 |
Loupe facet |
facetAddress(bytes4) |
0xcdffacc6 |
Loupe facet |
setValue(uint256) |
0x55241077 |
Counter V2 facet |
value() |
0x3fa4f245 |
Counter V1 facet |
owner() |
0x8da5cb5b |
Ownership facet |
note() |
0x26d111f5 |
zero address(remove後) |
setValueだけをV2へreplaceしたため、同じCounter機能でもvalue()など7 selectorsはV1に残ります。「facet単位でupgradeされたはず」と推測せず、selectorごとの実際の行き先を一覧にします。
Loupeの4関数で現在のroutingを読む
EIP-2535のloupeは、現在のfacetとselectorを調べるread-only interfaceです。
| function | 返すもの | 主な用途 |
|---|---|---|
facets() |
facet addressとselectorsの全group | current inventoryの一括取得 |
facetFunctionSelectors(address) |
指定facetのselectors | 1 facetの担当範囲 |
facetAddresses() |
current facet address一覧 | code確認targetの列挙 |
facetAddress(bytes4) |
selectorのcurrent facet | 既知selectorの直接確認 |
IDiamondLoupeのinterface IDは4関数selectorのXORで0x48e2b093です。ローカル再現ではERC-165のsupportsInterfaceがIERC165 0x01ffc9a7、IDiamondCut 0x1f931c1c、IDiamondLoupe 0x48e2b093へtrue、0xffffffffへfalseを返しました。
ただしloupeは「今」を返します。過去のfacet、変更順、initializer、変更authorityはloupeだけでは復元できません。また、大きなdiamondで全件配列を返す関数はgasやRPC responseの上限へ達する可能性があります。既知selectorならfacetAddress(bytes4)を優先し、一括取得は分割可能な独自indexや複数RPCも検討します。
facet addressのcodeとsourceを個別に照合する
loupeからaddressを得たら、各addressのruntime code有無とhashを同じblockで読みます。diamond addressのsourceだけをverifyしても、delegatecall先のfacet sourceまでは証明しません。
固定したローカル再現のcode hashは次のように分かれました。
| facet | runtime bytes | runtime code hash |
|---|---|---|
| DiamondCut | 3249 | 0x9ae07cb18db7121d650f59f685c3c4f4a50d00b577ab5f24acc6ae6e9f4a1fbf |
| Loupe | 2065 | 0x288e55868ce4c3d982a9152e8307e597298fa20b17406a7dbce50ff9b91830f4 |
| Counter V1 | 590 | 0x3f59288ec1ad70dbbfec5a6ec3d3b917fabf97ceedb97fff7d203df941f0a689 |
| Counter V2 | 234 | 0x7b273f7f5323d07ab71d4c54be522b611520f1a7fb9240d3f6aded0d2365a00a |
| Ownership | 389 | 0xa234efe7ddad9e7b48e486e35bd785622305a8c43bebf0f3bd6e944e30b550fb |
本番調査ではchain、diamond、fixed block number / hashを先に固定し、diamond fallback、loupe、すべてのcurrent facets、cut / authority facet、initializer targetを別targetとして再構築します。verified sourceをcompiler・constructor・proxyまで確認する手順と同じく、verified badgeを監査や安全保証に置き換えません。
diamondCutのadd・replace・removeをeventとpost-stateで結ぶ
diamondCutは複数のFacetCutと任意のinitializer callを1 transactionで適用します。actionの値は次の通りです。
| action | enum value | facet address | selectorの条件と結果 |
|---|---|---|---|
| Add | 0 | codeがあるfacet | 未登録selectorを新規routingへ追加 |
| Replace | 1 | codeがある別facet | 登録済みselectorのroutingを変更 |
| Remove | 2 | zero address | 登録済みselectorのroutingを削除 |
ローカル再現ではnote()をExtra facetへaddし、setValue(uint256)だけをCounter V2へreplaceし、最後にnote()をremoveしました。それぞれdiamond addressからDiamondCut eventが発行され、actionは0 → 1 → 2でした。replace後にsetValue(21)を呼ぶとV2の処理でvalueは42、versionは2になり、V1から引き継いだdiamond storageを読めました。
重複selectorのAdd、存在しないselectorのReplace / Remove、codeがないfacet、同じfacetへのReplaceは拒否されるべき条件です。ローカル再現では重複Addとcodeなしfacetを拒否し、Remove後のnote()はroutingなしとしてrevertしました。
DiamondCut eventを見つけただけで変更完了を断定しません。receipt status、emitter、ordered log、cut内容、同じblockのloupe、各facet code hashを対応させます。過去履歴ではdeployment transactionからcurrent fixed headまでeventを集め、初期selectorもconstructor dataとdeployment logsから確認します。
initializerはcutと同じtransactionの任意delegatecall
diamondCutは_init addressと_calldataを受け取り、selector変更後の初期化やmigrationを同じtransactionで実行できます。initializerもdiamond contextのdelegatecallなので、diamond storageを書き換えられます。
ローカル再現の初期cutではvalue 41、version 1を設定しました。別のcutでvalue 77、version 3へ更新し、selector追加とstate変更が同時に反映されることも確認しました。
重要なのは、initializerが単なる補助関数ではなく任意code実行点だということです。次を固定します。
_initaddressのruntime code hashとsource_calldataのselector、引数、復号結果- 書き換えるstorage rootとfield
- initializerの再実行防止とversion
msg.senderを使う場合の意味- external call、approval、asset移動の有無
- revertした場合にcut全体が巻き戻ること
revertするinitializerとnote()のAddを組み合わせたローカル再現では、シミュレーションが拒否され、post-stateのfacetAddress(0x26d111f5)はzeroのままでした。原子性は確認できても、成功するinitializerの内容が安全だとは証明しません。
Diamond Storage・AppStorage・ERC-7201を混同しない
EIP-2535はfacet間でstateを共有する必要性を説明しますが、application stateを1種類のlayoutへ固定しません。代表的な設計を分けます。
| pattern | rootの考え方 | 利点 | 注意点 |
|---|---|---|---|
| Diamond Storage | module固有文字列などのhash slot | facet / moduleごとに離れたstructを置ける | 定数衝突、ID typo、struct内互換性 |
| AppStorage | slot 0などに1つの大きなstruct |
application stateをまとめて型付きで参照 | 全facetがfield順・型を厳密に共有する必要 |
| ERC-7201 formula | namespace IDから256-slot aligned rootを計算 | namespaceの宣言とtoolingを標準化しやすい | EIP-2535とは別規格で、自動採用されない |
ローカル再現ではAppStorage rootがzero、routing用Diamond Storage rootが0xc8fcad8db84d3cc18b4c41d551ea0ee66dd599cde068d998e57d5e09332c131c、ERC-7201式の例が0x3286117daff9f85013498e4492687a45855d62df0707c718045a4347f7b1d900で、互いに異なりました。
AppStorageのfield順をvalue, versionからversion, valueへ取り違えたfacetは、同じraw slot 0 = 41をversion、slot 1 = 1をvalueとして読みました。facetを差し替えてもstorage bytesは自動移行されません。
namespace rootの式、annotation、同じnamespace内の末尾追加・並べ替え・型変更はERC-7201 Namespaced Storageの検証手順で詳しく比較しています。Diamondでは全facetとlibraryを横断してroot、struct、slot、offset、typeをinventory化します。
upgrade authorityはEIP-2535だけでは決まらない
EIP-2535はdiamondCutの仕組みを定めますが、誰が実行できるかを1つの方式へ固定しません。owner、AccessControl、multisig、timelock、governance、immutable化など、実装ごとに異なります。
ローカル再現はownerだけを許可し、無関係なlocal accountのcutを拒否しました。本番ではowner()が読めるだけで終えず、次のauthority chainを追います。
diamondCutselectorのcurrent facetをloupeで得る- cut facetのruntime sourceとアクセス制御を読む
- owner / role / adminのcurrent valueをfixed blockで読む
- addressがmultisigやtimelockなら、そのthreshold、members、delay、proposer / executorを追う
- ownership、role、governance parameterの変更eventを履歴化する
- upgrade functionを削除・置換できる別経路がないか全selectorsとsourceを検索する
EIP-2535ではupgradeability自体は任意です。標準のcut入口がない、または削除されたdiamondもあり得ますが、「upgrade不能」は関数名が見えないことだけでは証明できません。fallback直書き、別selector、外部storage contract、delegatecall先の管理機能を含めて調べます。
固定検証で確認した境界
公開した固定検証JSONには、17 signaturesのselector table、initial / final loupe、4件のDiamondCut event、7 contractのruntime code hash、raw slots、3種類のstorage root、authority拒否を保存しています。
Foundryの12 testsとviem / Anvil lifecycleは、次を固定しました。
- selectorからfacetへのfallback routing
- diamond storageとfacet自身のstorageの分離
- ERC-165と3 interface IDs
- loupe 4関数のcurrent inventory
- Add / Replace / Removeとevent
- duplicate selector、codeなしfacet、unauthorized cutのreject
- replace後も残るdiamond-owned state
- remove後のfallback revert
- initializer成功時のstate変更と失敗時の全体rollback
- AppStorage、Diamond Storage、ERC-7201例のroot分離
- 誤ったAppStorage field順によるraw slotの再解釈
これはローカル環境の再現結果です。production diamond、facet、initializer、storage layout、governance、監査、安全性には一般化しません。生成画像も概念説明であり、transaction、event、code、storage、authorityの証拠には使っていません。
Diamond調査チェックリスト
Identityとrouting
- chain ID、diamond address、fixed block number / hashを固定した
- diamond runtime code hashとfallback sourceを確認した
- calldata selectorをcanonical signatureから再計算した
-
facetAddress(selector)と全loupe inventoryを保存した - selector collisionや同名関数をABIだけで断定していない
- 各current facetのcode有無、runtime hash、verified source、compiler条件を照合した
Cutとhistory
- deploymentからfixed headまで
DiamondCutlogsをemitter・log index順に集めた - Add
0、Replace1、Remove2とfacet addressの条件を検査した - event直後とcurrent blockのloupe stateを比較した
- initializer address、calldata、source、storage write、external callを復号した
- initializer revert時にcut全体がrollbackする実装か確認した
- 標準外routing mutationやupgrade入口をsourceと全selectorsから探した
Storageとauthority
- 全facet / libraryのstorage root、struct、slot、offset、typeを一覧化した
- AppStorageのfield順と型をfacet間で比較した
- Diamond Storage / ERC-7201のID、root、重複、typoを検査した
- upgrade前後の重要getterとraw slotsを保存した
- cutのowner / role / multisig / timelock / governanceを末端まで追った
- source verification、audit、loupe対応を安全保証と同一視していない
まとめ
Diamondを読む起点はdiamond addressだけではなく、calldata先頭4 bytesのselectorです。loupeでcurrent facetを取得し、facetごとのruntime sourceとcode hash、DiamondCut履歴、initializer、共有storage layout、upgrade authorityへつなげます。
最終確認は chain → diamond → selector → facet → source and runtime → loupe → DiamondCut logs → initializer → storage roots → authority です。routingの現在値、変更履歴、stateの意味、変更できる主体を同じfixed blockの記録へ揃えてから、動作とriskを判断してください。
確認した一次情報
- EIP-2535 Diamonds, Multi-Facet Proxy確認日: 2026/08/31
- Solidity Contract ABI Specification確認日: 2026/08/31
- Solidity Introduction to Smart Contracts確認日: 2026/08/31
- Solidity Layout of State Variables in Storage確認日: 2026/08/31
- ERC-165 Standard Interface Detection確認日: 2026/08/31
- Nick Mudge diamond-3 reference implementation確認日: 2026/08/31



