SELFDESTRUCTはcontractを消す?EIP-6780後のcode・storage・再deployを検証
EIP-6780後のSELFDESTRUCTをcode、storage、balance、nonce、同一transaction例外、CREATE2再deployへ分け、ShanghaiとCancunで検証します。

SELFDESTRUCTを「contractを完全に削除する命令」と覚えているなら、その説明には実行chainのforkとtransaction境界が足りません。Ethereum mainnetでDencunが有効になった後、以前から存在するcontractがこのopcodeを実行しても、原則として全balanceをbeneficiaryへ送り、現在のcall frameを終了するだけです。code、storage、account自体は削除されません。
例外は、contractを作成したのと同じtransaction内でSELFDESTRUCTを実行した場合です。このときは旧挙動が保たれ、transactionの終わりにcodeやstorageを含むaccountが削除されます。
この記事は、deploy factoryやupgrade機構を実装する開発者、古いexploitやmetamorphic contractを調べる人、EVM version差をreviewする人向けです。chainとfork → creation transaction → selfdestruct transaction → code・storage・balance・nonce → CREATE2 preimage → 次transactionの順に確認し、「消えた」「残った」「再deployできる」を分けます。
検証にはSolidity 0.8.36、Foundry / Anvil 1.8.0、viem 2.56.0を使いました。同じShanghai互換bytecodeを、ShanghaiとCancunに固定した2つのlocal chainで実行しています。public RPC、mainnet fork、wallet、実資産、公開transactionは使っていません。
最初にchain、fork、transactionを固定する
EIP-6780はhard forkによるconsensus ruleの変更です。Ethereum mainnetではDencunの一部として、timestamp 1710338135、2024年3月13日13:55 UTCに有効になりました。
この日時を、すべてのEVM chainへそのまま当てはめてはいけません。L2、sidechain、private chain、local nodeでは、同じopcodeでもforkの有効化時期や設定が異なります。
| 最初に残す項目 | 何を確認するか | 省くと起きる混同 |
|---|---|---|
| chain | chain ID、network、client | Ethereum mainnetのfork時刻を別chainへ使う |
| block | number、hash、timestamp | pre-Cancunとpost-Cancunを混ぜる |
| runtime fork | Shanghai / Cancun以降 | compiler設定だけで実行挙動を決める |
| creation | contractを作ったtransaction | same-transaction例外を判定できない |
| destruction | SELFDESTRUCTを実行したtransaction |
同じblockを同じtransactionと誤認する |
| post-state | 次blockのcode、slot、balance、nonce | traceにopcodeがあるだけで削除と断定する |
Solidityの資料は、Cancun以降の挙動がnetwork-wideの変更であり、compile時の--evm-versionはlive chain上のSELFDESTRUCT semanticsを決めないと明記しています。compiler targetは生成bytecodeの互換性に関係しますが、既にdeploy済みのcontractへどのfork ruleを適用するかは実行側のEVMが決めます。
code・storage・balance・nonceは別々に読む
local検証では、CREATE2で作ったcontractを次の初期stateへ固定しました。
| field | selfdestruct前 |
|---|---|
| runtime code | 195 bytes |
| storage slot 0 | 77 |
| balance | 1 ETH |
| nonce | 1 |
別transactionで同じdestroy(beneficiary)を実行し、transaction確定後にeth_getCode、eth_getStorageAt、eth_getBalance、eth_getTransactionCountを読み直した結果です。
| 実行rule | code | slot 0 | balance | nonce | beneficiary |
|---|---|---|---|---|---|
| Shanghai | 0 bytes | 0 |
0 ETH |
0 |
+1 ETH |
| Cancun | 195 bytes | 77 |
0 ETH |
1 |
+1 ETH |
両方でSELFDESTRUCT opcodeは1回実行され、beneficiaryへ1 ETHが移りました。しかし、Cancunではcode hash、storage、nonceが実行前と同じです。balanceが0になったことは、contractが消えた証拠ではありません。
逆にShanghaiのlocal ruleでは、transaction終端後にcode、slot、account nonceが空になりました。それでも過去block、transaction、traceまで消えるわけではありません。stateからaccountが削除されることと、blockchain historyから情報が消去されることも分けます。
callの途中でaddress.code.lengthを読んだ値だけにも注意が必要です。旧ruleでも削除はtransaction終端で確定するため、同じtransaction内の観測をpost-stateの代わりにできません。receiptを待ち、確定blockを指定して4項目を読み直します。
同一transactionでcreateして壊した場合だけ例外になる
EIP-6780は、contractが作られたのと同じtransactionでSELFDESTRUCTした場合に旧挙動を残します。外部accountから直接作るcontract-creation transactionだけでなく、CREATEやCREATE2が始まった時点もcreationです。
検証では、constructorの中でSELFDESTRUCTする小さなcontractをCREATE2で作りました。ShanghaiとCancunのどちらでも、transaction確定後は次のstateです。
| field | 確定後 |
|---|---|
| runtime code | 0 bytes |
| storage slot 0 | 0 |
| balance | 0 ETH |
| nonce | 0 |
ただし、同じtransaction内で同じfactory、salt、init codeを使って2回目のCREATE2を実行すると、返り値はzero addressでした。EIP-1014が説明するように、SELFDESTRUCTは同じtransactionの途中でcodeやnonceを即時消去しないため、creation collisionを避けられません。
次のtransactionではaccountが空になっているため、1回目のCREATE2は同じ予定addressで再び成功しました。整理すると、同じtransactionの2回目は失敗し、次transactionの1回目は成功するという境界です。
「同じblockで別transaction」と「一つのtransaction内の別call」も同じではありません。調査記録にはblock numberだけでなくtransaction hashとcall traceを残します。
CREATE2はsaltだけで同じaddressを作るわけではない
CREATE2の予定addressは、次のpreimageから計算されます。
address = keccak256(
0xff ++ factory_address ++ salt ++ keccak256(init_code)
)[12:]
固定するのは、次の3項目です。
- CREATE2を実行するfactory address
- 32-byte salt
- constructor引数を含むinit codeのhash
local検証のpersistent targetでは、factoryが0x5FbD…0aa3、saltが0xa4a8…3336、init-code hashが0x4374…433dで、予定addressは0x9e48…9ab4でした。constructor引数を77から78へ変えるとinit code hashが変わるため、同じsaltでも別addressになります。
factory、salt、constructor、linked library、metadata、proxy initializerまで含む一般的な予測手順は、CREATE2 addressをSolidityとviemで独立計算する記事で扱っています。
Shanghai ruleでは、既存targetを別transactionでselfdestructした後、同じ3項目を使う次transactionのCREATE2が成功し、0x9e48…9ab4へcodeとslot 77が戻りました。Cancun ruleではcodeとnonceが残るためCREATE2がcollisionし、zero addressを返しました。
CREATE2のaddress計算そのものはEIP-6780で変わっていません。未deploy addressを先に計算して、後で一度だけdeployするcounterfactual用途も、SELFDESTRUCTによる再deployとは別です。ERC-6492のcounterfactual account検証では、factory、salt、init codeから予定signerを結び付ける例を確認できます。
metamorphic contractとproxy upgradeを混同しない
古典的なmetamorphic contractは、一定のinit codeから外部stateに応じたruntime codeを返し、既存contractをselfdestructしてから同じCREATE2 addressへ別codeを置く設計です。Cancun以降、以前のtransactionから存在するcontractは削除されないため、この再deploy cycleは成立しません。
すべてのCREATE2 factoryやupgradeが壊れた、という意味ではありません。
| pattern | 同じaddressを保つ方法 | code / storageの所在 | EIP-6780との関係 |
|---|---|---|---|
| persistent CREATE2 | 同じfactory・salt・init code | deploy先contract | 空addressへ一度deployする用途は継続 |
| classic metamorphic | delete後に同じaddressへ再deploy | 同じaddressのruntimeを入れ替える | 既存contractの削除が止まりcycleが失敗 |
| constructor-selfdestruct | creation transaction内で削除 | 確定後はempty | 次transactionで再利用できるがlive upgradeではない |
| ERC-1967 proxy | proxyがimplementation addressを読む | stateはproxy、実行codeはimplementation | selfdestruct再deployではなくslot更新で切り替える |
proxyを調べるときは、ERC-1967のimplementation・admin・beacon slotを読む方法のように、proxy addressのstateとimplementation addressのruntime codeを分けます。verified sourceを含む再構築はbytecode・compiler設定・proxy先を同じblockで照合する方法へ進めます。
もう一つの注意はdelegatecallです。Solidityの資料は、対象contract自身のsourceにSELFDESTRUCTがなくても、delegatecall先のcodeからopcodeを実行できると警告しています。delegatecallではcaller側のstorage、balance、address contextで実行されるため、implementationだけを検索して「proxyにはopcodeがない」と終えません。
Cancun ruleなら既存proxyのcodeとstorageを削除しませんが、balance送付とcall frame終了は起きます。deprecated warningが出ること、codeが残ること、影響がないことは同義ではありません。
historical挙動はlocal hardforkと実blockを分けて再現する
今回のShanghai結果は、旧semanticsを比較するdeterministic local testです。Ethereum mainnetの過去transactionをreplayした結果ではありません。
実際のincidentをhistorical blockで確認するなら、少なくとも次を一組で保存します。
- chain ID、block number、block hash、timestamp
- creation transactionとselfdestruct transaction
- clientと適用hardfork
- trace内の
CREATE/CREATE2/SELFDESTRUCTとcall depth - selfdestruct前後blockのcode、relevant storage slot、balance、nonce
- beneficiaryとbalance差分
- CREATE2ならfactory、salt、完全なinit code hash
opcode traceにSELFDESTRUCTが1回あるだけでは、どのpost-stateになったか分かりません。今回もShanghaiとCancunのtraceはどちらもopcodeを1回含み、差が現れたのはtransaction確定後のstateでした。
公開したShanghai / Cancun比較JSONには、同じaddressのbefore / after、CREATE2結果、opcode count、固定versionを分けてあります。値はlocal chainの再現結果であり、特定mainnet contractの安全性を示すものではありません。
incident調査とcode reviewのチェックリスト
- chain ID、block number / hash、runtime hardforkを固定した
- creation transactionとselfdestruct transactionが同じか確認した
- receipt後の
eth_getCodeを確定blockで読んだ - relevant storage slotをselfdestruct前後で比較した
- contract balanceとbeneficiaryの増分を別々に確認した
- account nonceを読み、CREATE2 collision条件を確認した
- factory address、salt、constructor引数込みinit-code hashを保存した
- traceでCREATE2の返り値とSELFDESTRUCTのcall contextを確認した
- delegatecall経路を含め、実際にopcodeへ到達できる権限を確認した
- metamorphic、proxy、counterfactual deploymentを別patternとして扱った
- compiler warning、compile target、runtime forkを同じversion情報へ潰していない
現行ruleでの結論は「SELFDESTRUCTが何もしない」ではありません。既存contractではcodeとstorageを残しながら、balanceを送り、実行を終了します。何が残ったかをfieldごとに読み、transactionとforkを固定して初めて、安全な停止設計やincidentの説明につながります。
この記事には特定contract、token、upgrade serviceの利用や投資を促す導線はありません。将来audit、verification、deployment toolingを紹介する場合も、広告表記と技術検証を分離します。
確認した一次情報
- EIP-6780 SELFDESTRUCT only in same transaction確認日: 2026/08/31
- EIP-6049 Deprecate SELFDESTRUCT確認日: 2026/08/31
- EIP-1014 Skinny CREATE2確認日: 2026/08/31
- EIP-7569 Dencun hardfork meta確認日: 2026/08/31
- Ethereum Foundation Dencun mainnet announcement確認日: 2026/08/31
- Solidity selfdestruct documentation確認日: 2026/08/31
- Solidity contract deactivation and delegatecall warning確認日: 2026/08/31
- Ethereum JSON-RPC API確認日: 2026/08/31
- Foundry EVM version configuration確認日: 2026/08/31
- Foundry v1.8.0確認日: 2026/08/31



