Opcode Summary

PropertyValue
Opcode0xFF
MnemonicSELFDESTRUCT
Gas5000 base + 25000 (if sending value to an empty account) + 2600 (cold address access, EIP-2929)
Stack Inputaddr (the address to receive all remaining ETH)
Stack Output(none)
BehaviorSends the entire ETH balance of the executing contract to addr, then halts execution. Pre-Dencun (before EIP-6780): destroys the contract — zeroes code, storage, nonce, and balance. The account ceases to exist at the end of the transaction. Post-Dencun (EIP-6780, March 2024): only destroys the contract if SELFDESTRUCT is executed in the same transaction that created the contract. Otherwise, SELFDESTRUCT only transfers the ETH balance to addr — code, storage, and nonce are preserved. In a DELEGATECALL context, SELFDESTRUCT destroys the calling (proxy) contract, not the implementation. The ETH transfer bypasses receive(), fallback(), and all code execution on the recipient — it is an unconditional balance credit.

Threat Surface

SELFDESTRUCT is the most dangerous opcode in Ethereum’s history. No other single instruction has caused more catastrophic, irrecoverable losses. It combines three uniquely destructive capabilities: permanent contract destruction, forced ETH transfers that bypass all recipient logic, and — when paired with CREATE2 — the ability to replace contract code at a fixed address (metamorphic contracts).

The threat surface divides into two eras:

Pre-Dencun (before March 2024): SELFDESTRUCT’s full destructive power was available to any contract that included the instruction. The attack surface was enormous:

  1. Irreversible contract destruction. A single SELFDESTRUCT call permanently deleted all code, storage, and state. If the destroyed contract was a shared library or proxy implementation, every dependent contract was bricked with no recovery path. The Parity wallet freeze ($150M, November 2017) demonstrated this at scale — one SELFDESTRUCT call on a shared library froze 587 wallets.

  2. Metamorphic contracts via SELFDESTRUCT + CREATE2. By deploying a contract with CREATE2 (deterministic address), self-destructing it, and redeploying different code at the same address, an attacker could replace a benign contract with a malicious one. This enabled the Tornado Cash governance attack (May 2023): a proposal contract was voted in as benign, self-destructed, then redeployed with malicious code that seized governance control.

  3. Forced ETH transfers. SELFDESTRUCT sends ETH to any address with no way for the recipient to reject, revert, or even detect the transfer during execution. This breaks contracts that rely on address(this).balance for accounting, invariant checks, or game logic.

  4. Proxy bricking via delegatecall. When SELFDESTRUCT executes inside a DELEGATECALL, it destroys the calling contract (the proxy), not the implementation. An attacker who gains control of the implementation can brick every proxy that delegates to it.

Post-Dencun (EIP-6780, March 2024): The most destructive capabilities are neutered on L1 — code and storage are no longer deleted unless the SELFDESTRUCT occurs in the same transaction as creation. However:

  1. Forced ETH transfer still works. SELFDESTRUCT still unconditionally sends all ETH to the target address, bypassing receive() and fallback(). Balance-dependent contracts remain vulnerable.

  2. Same-transaction destruction still works. Contracts that CREATE and SELFDESTRUCT within a single transaction retain the full pre-Dencun behavior, keeping factory/cleanup patterns functional but also preserving attack vectors for ephemeral contracts.

  3. L2 divergence. Not all L2 chains have adopted EIP-6780 on the same timeline or with the same semantics. Metamorphic contract attacks that are neutered on L1 may still work on certain L2s that run older EVM versions or have custom SELFDESTRUCT implementations.


Smart Contract Threats

T1: Forced ETH Transfer Bypassing fallback/receive (High)

SELFDESTRUCT sends ETH to any address without triggering the recipient’s code. This is an unconditional balance credit at the protocol level — no receive(), no fallback(), no msg.data, no gas execution on the target. This remains fully functional post-EIP-6780.

  • Balance invariant corruption. Contracts that use address(this).balance for accounting (e.g., “the pot equals the contract balance”) can be corrupted by force-sending ETH. If a game contract checks require(address(this).balance == expectedAmount), an attacker can force-send ETH to make the condition permanently unsatisfiable, locking funds or bricking logic.

  • Denial of service on balance-gated logic. Contracts with conditions like require(address(this).balance >= threshold) to trigger payouts can be griefed by force-sending ETH below the threshold (making the check pass prematurely) or above it (if the logic requires exact equality).

  • Bypassing “no ETH accepted” patterns. Contracts without receive() or fallback() functions, or those that revert on ETH receipt, assume they cannot hold ETH. SELFDESTRUCT breaks this assumption. A contract that asserts address(this).balance == 0 in its invariants will fail after a force-send.

Why it matters: This is the one SELFDESTRUCT threat vector that EIP-6780 did not fix. Any contract relying on address(this).balance for logic remains vulnerable on all chains.

T2: Proxy Bricking via Implementation Destruction (Critical)

When a proxy uses DELEGATECALL to execute implementation code containing SELFDESTRUCT, the proxy is destroyed — not the implementation. An attacker who takes ownership of an uninitialized implementation contract can trigger SELFDESTRUCT through the upgrade mechanism, permanently bricking all proxies.

  • Parity wallet pattern. Parity’s multi-sig wallets delegated all calls to a shared WalletLibrary. An attacker called the unprotected initWallet() directly on the library contract, became its owner, and called kill() (SELFDESTRUCT). Because SELFDESTRUCT on the library contract destroyed the library itself, all 587 wallets that delegated to it became permanently non-functional, freezing $150M+ in ETH.

  • UUPS proxy vulnerability. In September 2021, iosiro disclosed that uninitialized OpenZeppelin UUPS implementation contracts could be initialized by anyone, who could then call upgradeToAndCall() with a malicious contract containing SELFDESTRUCT. The delegatecall executes SELFDESTRUCT in the implementation’s context, destroying the implementation. All proxies pointing to that implementation are bricked. Over 44M), Rivermen NFT (~$6.95M), and others.

  • Post-EIP-6780 mitigation. On L1 after Dencun, SELFDESTRUCT no longer deletes code/storage for contracts created in prior transactions, significantly reducing this threat. However, the attack remains viable for contracts created and destroyed in the same transaction, and potentially on L2s that haven’t adopted EIP-6780.

Why it matters: Before EIP-6780, this was the single highest-impact smart contract vulnerability class. The Parity freeze alone caused $150M in permanent, irrecoverable losses — more than most protocol hacks.

T3: Metamorphic Contracts via SELFDESTRUCT + CREATE2 (Critical, pre-EIP-6780)

CREATE2 computes deployment addresses deterministically from keccak256(0xFF ++ deployer ++ salt ++ keccak256(init_code)). If the deployed contract self-destructs, the address becomes empty, and the same CREATE2 call (same deployer, salt, and init_code) can deploy a completely different runtime bytecode at the same address — because only the init code hash must match, not the runtime code.

  • Tornado Cash governance attack (May 2023). The attacker deployed a factory contract that used CREATE2 to deploy a “child” deployer, which then used CREATE to deploy a benign governance proposal. The proposal passed a governance vote. The attacker then self-destructed both the child deployer and the proposal contract, redeployed the child at the same address via CREATE2 (same salt, same init code), and since the child’s nonce reset to 0, the CREATE redeployed a malicious proposal at the original proposal’s address. The governance contract still trusted that address, granting the attacker full control. They drained ~$1M+ in TORN tokens.

  • Mechanism. The three-step attack: (1) Deploy benign code at a deterministic address, pass an audit or governance vote. (2) SELFDESTRUCT the contract. (3) Redeploy different code at the same address. Any system that trusted the original address now trusts the malicious replacement.

  • Post-EIP-6780 status. Neutered on L1 — SELFDESTRUCT no longer clears code/storage unless same-tx-as-creation. The address cannot be reused because the account still has code. However, this attack may still work on L2s that haven’t fully adopted EIP-6780, and the same-transaction variant (CREATE2 + SELFDESTRUCT + CREATE2 in a single tx) remains theoretically possible.

Why it matters: Metamorphic contracts undermine the fundamental trust assumption that deployed code is immutable. Any address-based trust (governance proposals, token whitelists, approved contracts) is exploitable if the contract at that address can morph.

T4: Breaking Code-Dependent Logic via Contract Destruction (Medium, pre-EIP-6780)

When a contract is destroyed by SELFDESTRUCT, EXTCODESIZE returns 0, EXTCODECOPY returns empty, and EXTCODEHASH returns keccak256("") (the empty hash). Contracts that use these opcodes to determine whether an address is a contract or to verify code integrity break silently.

  • isContract() checks become unreliable. A common pattern is require(addr.code.length > 0) to verify that an address is a contract. After SELFDESTRUCT, this returns false, potentially causing downstream contracts to treat the destroyed address as an EOA or refuse to interact with it.

  • Code hash verification fails. Protocols that store EXTCODEHASH(addr) during registration and verify it later will see a mismatch after the target self-destructs.

  • Post-EIP-6780 mitigation. On L1, SELFDESTRUCT no longer clears code for existing contracts, so EXTCODESIZE/EXTCODEHASH remain consistent. This threat is largely historical on L1 but may persist on pre-6780 L2s.

Why it matters: Any protocol that caches or checks code properties of external addresses was silently vulnerable to state corruption via SELFDESTRUCT.

T5: Contract Resurrection Post-Destruction (Medium, pre-EIP-6780)

Pre-EIP-6780, a self-destructed contract’s address became empty. A new contract could be deployed at the same address using CREATE2 (or CREATE if the deploying factory’s nonce was carefully managed). The new contract had fresh storage but occupied the same address, “resurrecting” it with potentially different code.

  • Nonce reset. After SELFDESTRUCT, the account’s nonce resets to 0. If the same deployer uses CREATE, the address computation starts fresh, enabling the metamorphic pattern.

  • Storage wipe. The resurrected contract has empty storage — all previous state (balances, approvals, ownership) is gone. Protocols holding references to the original contract may interact with the resurrected version assuming continuity.

  • Post-EIP-6780 status. On L1, accounts are no longer deleted (except same-tx), so the nonce is not reset and the address cannot be reused. This threat is effectively eliminated on L1.

Why it matters: Before EIP-6780, contract addresses were not permanently bound to specific code, breaking a fundamental assumption of address-based trust.


Protocol-Level Threats

P1: EIP-6780 — Behavioral Fork in SELFDESTRUCT Semantics (Medium)

EIP-6780 (Dencun, March 2024) fundamentally changed SELFDESTRUCT from “always destroy” to “only destroy if same-tx-as-creation, otherwise just send ETH.” This creates a semantic fork:

  • Legacy contracts. Contracts deployed before Dencun that rely on SELFDESTRUCT for legitimate cleanup (e.g., factory patterns that deploy and destroy ephemeral contracts across transactions) no longer function as designed. The SELFDESTRUCT executes and sends ETH, but the contract’s code and storage persist.

  • Auditing assumptions. Pre-Dencun audits that flagged SELFDESTRUCT risks (metamorphic contracts, proxy bricking) may be incorrectly assumed to be “fixed” post-6780. The forced ETH transfer vector is still live, and same-transaction attacks still work.

  • Verkle tree preparation. EIP-6780 was motivated by the transition to Verkle trees, where account data is spread across many tree nodes. Full account deletion would require touching all those nodes, making it impractical. The EIP is a stepping stone toward eventual full deprecation of SELFDESTRUCT.

P2: State Trie Cleanup and Gas Accounting (Low)

Pre-EIP-6780, SELFDESTRUCT was the only mechanism to remove accounts from the state trie. Post-6780, accounts persist even after SELFDESTRUCT (except same-tx). This affects state growth:

  • No more state cleanup. The gas cost of SELFDESTRUCT (5000+) was originally justified partly by the state reduction benefit. Now the gas is paid but no state is removed, making SELFDESTRUCT purely a cost with no state benefit (except same-tx).

  • EIP-3529 (London, August 2021). Previously, SELFDESTRUCT provided a 24,000 gas refund, incentivizing patterns like GasToken that bloated state during low-fee periods and cleared it for refunds during high-fee periods. EIP-3529 removed the SELFDESTRUCT refund entirely, eliminating this economic attack vector but also removing any incentive for state cleanup.

P3: L1/L2 Divergence on SELFDESTRUCT Behavior (Medium)

L2 chains implement EVM semantics independently and may not track L1’s EIP adoption timeline:

  • Pre-6780 L2s. Any L2 running an EVM version prior to Dencun still supports full SELFDESTRUCT semantics — metamorphic contracts, proxy bricking, and full account deletion all work. Code that is “safe” on post-Dencun L1 may be exploitable on these L2s.

  • Custom SELFDESTRUCT implementations. Some L2s (e.g., zkSync Era, Scroll, or other zkEVM rollups) may implement SELFDESTRUCT with subtly different semantics due to their underlying proof systems. Developers deploying across chains must verify SELFDESTRUCT behavior per chain.

  • Deprecation timeline mismatch. L1 is on a path to fully remove SELFDESTRUCT. L2s may lag months or years behind, creating a long tail of chains where legacy SELFDESTRUCT attacks remain viable.

P4: Gas Refund Removal — EIP-3529 (Low)

EIP-3529 (London, August 2021) removed the 24,000 gas refund for SELFDESTRUCT. Before this:

  • GasToken exploitation. Contracts like GasToken deployed many small contracts during low-fee periods and self-destructed them during high-fee periods, collecting refunds. This was profitable arbitrage on gas prices but bloated the state trie with millions of tiny contracts.

  • Block size manipulation. The 50% gas refund cap (reduced to 20% by EIP-3529) meant that transactions with SELFDESTRUCT could effectively use up to 2x the nominal gas limit in state changes, destabilizing block size predictability.

  • Post-3529 status. No SELFDESTRUCT gas refund exists. The economic incentive to self-destruct is zero.


Edge Cases

Edge CaseBehaviorSecurity Implication
SELFDESTRUCT in same tx as creation (post-EIP-6780)Full destruction: code, storage, nonce, and balance are cleared; ETH sent to targetSame-transaction factory patterns still work. Attack vectors involving ephemeral contracts (CREATE + SELFDESTRUCT in one tx) remain viable.
SELFDESTRUCT to self (selfdestruct(address(this)))ETH is sent to self. Pre-6780: contract is still destroyed, and the self-sent ETH is burned (balance zeroed after send). Post-6780 (different tx): ETH stays in the contract (send to self is a no-op on balance), execution halts.Pre-6780: led to permanent ETH burn. A consensus bug in geth (v1.9.4-1.9.20, GHSA-xw37-57qp-9mm4) caused incorrect behavior when value was sent to a self-destructed address in the same transaction.
SELFDESTRUCT in DELEGATECALLDestroys the calling (proxy) contract, not the implementationThe most exploited edge case in Ethereum history. Parity freeze (50M+ at risk) both stem from this behavior.
SELFDESTRUCT sending to non-existent addressCreates the target account and credits it with the ETH balance. Costs an additional 25,000 gas (account creation).An attacker can create arbitrary accounts on-chain by self-destructing to any address. Post-6780, the sending contract retains its code/storage (unless same-tx).
Multiple SELFDESTRUCTs in same txPre-6780: first SELFDESTRUCT destroys the contract; subsequent calls in the same tx to the same address see balance 0 (ETH already sent). Post-6780 (different tx): each SELFDESTRUCT call sends the current balance to the target; code/storage persist.If a contract receives ETH between SELFDESTRUCT calls (e.g., via another SELFDESTRUCT targeting it), each call sends whatever balance exists at that point.
SELFDESTRUCT with zero balanceExecutes normally, sending 0 ETH. Pre-6780: contract is still destroyed. Post-6780 (same-tx): contract is still destroyed. Post-6780 (different tx): no-op on balance, but execution still halts.Can be used to halt execution flow without any ETH transfer.
SELFDESTRUCT in STATICCALLDisallowed. STATICCALL prohibits state-modifying opcodes; SELFDESTRUCT causes the call to revert.Prevents view functions from triggering destruction.
SELFDESTRUCT beneficiary is the contract itself + receives ETH later in tx (pre-6780)Contract is destroyed, self-sent ETH is lost, but ETH sent to the address later in the same tx is credited to the “empty” address.Created edge cases in transaction ordering where ETH could be sent to a destroyed address and become permanently locked.

Real-World Exploits

Exploit 1: Parity Wallet Library Self-Destruct — $150M Frozen Permanently (November 2017)

Root cause: An unprotected initWallet() function on a shared library contract allowed anyone to take ownership and call kill() (SELFDESTRUCT), destroying the library that 587 multi-sig wallets depended on via DELEGATECALL.

Details: On November 6, 2017, a user known as “devops199” called initWallet() on the Parity WalletLibrary contract. This function was intended to be called only during proxy initialization, but it was left public and had no guard against re-initialization (the owner field was address(0) because the library itself was never initialized — only proxies were). The user became the library’s owner, then called kill(), which executed selfdestruct(owner).

Because all ~587 multi-sig wallets deployed after July 20, 2017 used DELEGATECALL to this single library for all functionality, the library’s destruction meant every DELEGATECALL from these wallets returned failure. The wallets could still receive ETH (direct transfers don’t require code execution on the recipient), but could never send ETH, transfer tokens, or execute any function. 513,774.16 ETH plus various tokens were frozen permanently.

The self-destructing transaction: 0x47f7cff7a5e671884629c93b368cb18f58a993f4b19c2a53a8662e3f1482f690.

SELFDESTRUCT’s role: SELFDESTRUCT was the kill switch. Once the attacker had ownership of the library, the single selfdestruct(owner) call was irreversible and immediate — no timelock, no multi-sig, no recovery. The library’s code was permanently deleted from the state trie.

Impact: ~30M stolen via the same initWallet() re-initialization bug, but without SELFDESTRUCT.

References:


Exploit 2: Tornado Cash Governance Attack — Metamorphic Contract via SELFDESTRUCT + CREATE2 (~$1M+, May 2023)

Root cause: SELFDESTRUCT + CREATE2 enabled contract code replacement at a fixed address, allowing a governance proposal to morph from benign to malicious after passing a vote.

Details: The attacker used a three-stage metamorphic contract attack:

  1. Stage 1 — Deploy benign proposal. The attacker deployed a factory contract that used CREATE2 to deploy a “child” deployer at a deterministic address. The child used CREATE to deploy a DAOProposal contract (a benign governance proposal). This proposal was submitted to Tornado Cash governance and passed the community vote.

  2. Stage 2 — Destroy and redeploy. After the proposal was approved, the attacker called SELFDESTRUCT on both the child deployer and the DAOProposal, clearing both addresses. They then redeployed the child at the same address using CREATE2 (same deployer, same salt, same init code). Because the child’s nonce reset to 0 after destruction, the subsequent CREATE from the child deployed new code at the exact same address as the original DAOProposal.

  3. Stage 3 — Execute malicious code. The governance contract still trusted the original proposal address. When executeProposal() was called, it executed the malicious replacement code, which granted the attacker 10,000 TORN tokens (enough for governance majority). The attacker used this to drain funds from governance vaults.

SELFDESTRUCT’s role: SELFDESTRUCT was the critical enabler — it cleared the contract code and reset the nonce, allowing CREATE2 to redeploy at the same address. Without SELFDESTRUCT, deployed code is immutable and address reuse is impossible.

Impact: ~$1M+ in TORN tokens stolen. Full governance takeover of Tornado Cash. Demonstrated that any on-chain governance system trusting contract addresses (rather than code hashes) was vulnerable to metamorphic attacks.

Post-EIP-6780 status: This exact attack is no longer possible on L1 because SELFDESTRUCT no longer clears code/storage for contracts created in prior transactions.

References:


Exploit 3: OpenZeppelin UUPS Proxy Vulnerability — $50M+ at Risk (September 2021)

Root cause: Uninitialized UUPS implementation contracts could be initialized by anyone, who could then trigger SELFDESTRUCT via the upgrade mechanism’s delegatecall, permanently bricking all proxies.

Details: OpenZeppelin’s UUPSUpgradeable pattern (versions 4.0 through 4.3.1) placed the upgrade logic in the implementation contract rather than the proxy. The upgrade function upgradeToAndCall(address, bytes) used delegatecall to execute arbitrary code in the context of the calling contract.

If the implementation contract was not initialized (a common oversight), an attacker could:

  1. Call initialize() on the implementation contract directly, becoming its owner.
  2. Call upgradeToAndCall(malicious_contract, "") where malicious_contract contained selfdestruct(attacker).
  3. The delegatecall executed selfdestruct in the implementation’s context, destroying the implementation contract.
  4. All proxies delegating to the destroyed implementation became permanently non-functional.

Security researcher Ashiq Amien of iosiro disclosed this vulnerability privately. OpenZeppelin released v4.3.2 as a hotfix (CVE-2021-41264).

SELFDESTRUCT’s role: SELFDESTRUCT was the terminal payload. The attack chain (initialize upgrade delegatecall selfdestruct) required SELFDESTRUCT as the irreversible final step. Without it, the attacker could only upgrade the implementation, which would be detectable and potentially reversible.

Impact: Over 44M), Rivermen NFT (~$6.95M), and additional projects. No funds were lost due to responsible disclosure.

References:


Exploit 4: Forced ETH Transfer Balance Manipulation (Recurring, 2017-Present)

Root cause: Contracts that use address(this).balance for game logic, invariant checks, or payout conditions can be broken by SELFDESTRUCT force-sending ETH.

Details: Multiple contracts have been exploited or griefed via forced ETH transfers. The canonical example is the “EtherGame” pattern (documented by Solidity by Example and OpenZeppelin): a contract where players deposit exactly 1 ETH, and the 7th depositor wins the pot. The contract checks require(address(this).balance == 7 ether) to trigger the payout. An attacker deploys a contract with some ETH and calls selfdestruct(etherGameAddress), pushing the balance past 7 ETH without going through the deposit function. The == 7 ether check can never be satisfied, permanently locking all funds.

This pattern extends to any contract using address(this).balance for:

  • Exact balance checks (== amount)
  • Balance-based access control (require(address(this).balance >= threshold))
  • Proportional calculations (share = deposit / address(this).balance)

SELFDESTRUCT’s role: SELFDESTRUCT is the only mechanism (besides coinbase block rewards pre-merge) that can credit ETH to an address without triggering any code on the recipient. Post-EIP-6780, this vector is fully preserved.

Impact: Individually small amounts, but the pattern is pervasive. The OWASP Smart Contract Security guidelines (SCWE-050) list unprotected SELFDESTRUCT and forced ETH injection as a standard vulnerability class. The Ethernaut CTF “Force” challenge teaches this attack to every Solidity developer.

References:


Attack Scenarios

Scenario A: Forced ETH Transfer Breaking Balance Logic (Post-EIP-6780 viable)

contract VulnerableGame {
    uint256 public constant TARGET = 7 ether;
    mapping(address => uint256) public deposits;
 
    function deposit() external payable {
        require(msg.value == 1 ether, "must send 1 ETH");
        require(address(this).balance <= TARGET, "game full");
        deposits[msg.sender] += 1 ether;
    }
 
    function claimWinner() external {
        // VULNERABLE: address(this).balance can be manipulated
        // by SELFDESTRUCT force-sending ETH
        require(address(this).balance == TARGET, "not yet");
        require(deposits[msg.sender] > 0, "not a player");
        payable(msg.sender).transfer(address(this).balance);
    }
}
 
contract Attacker {
    function attack(address game) external payable {
        // Force-send ETH to game contract, pushing balance past TARGET.
        // claimWinner() can never be called because balance != 7 ether.
        // All deposited funds are permanently locked.
        selfdestruct(payable(game));
    }
}

Scenario B: Proxy Bricking via UUPS + SELFDESTRUCT (Pre-EIP-6780)

contract MaliciousImplementation {
    function destroy() external {
        selfdestruct(payable(msg.sender));
    }
}
 
contract UUPSImplementation is UUPSUpgradeable, OwnableUpgradeable {
    function initialize() public initializer {
        __Ownable_init();
    }
 
    function _authorizeUpgrade(address) internal override onlyOwner {}
}
 
// Attack flow:
// 1. Implementation was deployed but never initialized
// 2. Attacker calls: implementation.initialize() -- becomes owner
// 3. Attacker calls: implementation.upgradeToAndCall(
//        address(maliciousImpl),
//        abi.encodeWithSelector(MaliciousImplementation.destroy.selector)
//    )
// 4. upgradeToAndCall does delegatecall to maliciousImpl.destroy()
// 5. selfdestruct executes in implementation's context, destroying it
// 6. All proxies pointing to this implementation are permanently bricked

Scenario C: Metamorphic Contract via SELFDESTRUCT + CREATE2 (Pre-EIP-6780)

contract MetamorphicFactory {
    address public childAddress;
 
    function deployChild(bytes32 salt, bytes memory runtimeCode) external {
        // CREATE2 with a deployer that returns arbitrary runtime code
        bytes memory initCode = abi.encodePacked(
            hex"63",
            uint32(runtimeCode.length),
            hex"80600e6000396000f3",
            runtimeCode
        );
 
        address child;
        assembly {
            child := create2(0, add(initCode, 0x20), mload(initCode), salt)
        }
        require(child != address(0), "deploy failed");
        childAddress = child;
    }
 
    function destroyChild() external {
        // Pre-EIP-6780: clears code, storage, nonce at childAddress
        IDestructible(childAddress).destroy();
    }
 
    // Attack:
    // 1. deployChild(salt, benignCode) -- deploys benign contract, gets approved
    // 2. destroyChild() -- SELFDESTRUCT clears the address
    // 3. deployChild(salt, maliciousCode) -- same salt, different runtime code
    //    deploys at THE SAME ADDRESS because CREATE2 only hashes init_code
    // 4. Any system trusting childAddress now executes malicious code
}

Scenario D: SELFDESTRUCT in DELEGATECALL Destroying the Proxy

contract ImplementationV1 {
    address public owner;
 
    function initialize(address _owner) external {
        require(owner == address(0));
        owner = _owner;
    }
 
    function kill() external {
        require(msg.sender == owner);
        selfdestruct(payable(owner));
    }
}
 
contract Proxy {
    address public implementation;
 
    constructor(address _impl) {
        implementation = _impl;
    }
 
    fallback() external payable {
        address impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}
 
// If the implementation is shared and unprotected:
// 1. Attacker calls implementation.initialize(attacker) DIRECTLY
//    (not through proxy -- so it initializes the implementation itself)
// 2. Attacker calls implementation.kill() DIRECTLY
//    selfdestruct destroys the implementation contract
// 3. All proxies delegating to this implementation now delegate to
//    an empty address -- all calls fail, all funds are locked

Mitigations

ThreatMitigationImplementation
T1: Forced ETH transferNever use address(this).balance for logicTrack deposits with an internal uint256 totalDeposited variable; ignore ETH sent outside the deposit function
T1: Balance equality checksUse >= or <= instead of == for balance checksReplace require(address(this).balance == target) with range checks or internal accounting
T2: Proxy bricking via implementation destructionInitialize implementation contracts immediatelyCall _disableInitializers() in the implementation constructor (OpenZeppelin >= 4.3.2); deploy and initialize atomically
T2: UUPS upgrade to selfdestructValidate upgrade targetsUse OpenZeppelin’s UUPSUpgradeable >= 4.3.2 which adds a check that the new implementation is a valid UUPS contract
T3: Metamorphic contractsVerify code hashes, not just addressesStore EXTCODEHASH(addr) at registration time and re-verify before execution; reject addresses whose code hash changed
T3: CREATE2 address reuseCheck EXTCODESIZE before trustingrequire(addr.code.length > 0) before interacting; on post-6780 L1 this is largely moot, but still needed for L2s
T4: Code-dependent logic breakingUse code hashes for identityUse EXTCODEHASH instead of EXTCODESIZE for contract verification; store expected hashes immutably
T5: Contract resurrectionVerify contract state on each interactionDon’t cache contract references across transactions without re-verifying code existence
General: Unauthorized SELFDESTRUCTAccess-control SELFDESTRUCT with multi-sig + timelockNever expose SELFDESTRUCT behind a single onlyOwner modifier; use multi-sig and time-delayed execution
General: L2 divergenceCheck SELFDESTRUCT semantics per chainVerify EIP-6780 adoption on target L2 before deployment; add chain-specific guards

Compiler/EIP-Based Protections

  • EIP-6780 (Dencun, March 2024): Neutered SELFDESTRUCT on L1 — code and storage are only deleted when SELFDESTRUCT executes in the same transaction as contract creation. Forced ETH transfer is preserved. This is the single most impactful security improvement for SELFDESTRUCT.
  • EIP-3529 (London, August 2021): Removed the 24,000 gas refund for SELFDESTRUCT, eliminating the GasToken economic incentive to deploy-and-destroy contracts for gas arbitrage.
  • Solidity deprecation: Solidity >= 0.8.18 emits a warning when selfdestruct is used. The Solidity team has indicated that future compiler versions may remove selfdestruct from the language entirely.
  • OpenZeppelin >= 4.3.2: _disableInitializers() in UUPSUpgradeable prevents the implementation initialization attack. All UUPS implementations should call this in their constructor.
  • OpenZeppelin Initializable: The initializer modifier with _disableInitializers() prevents re-initialization of both proxy and implementation contracts.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighHighForced ETH balance manipulation (recurring, Ethernaut, OWASP SCWE-050). Still viable post-EIP-6780.
T2Smart ContractCriticalLow (post-6780) / High (pre-6780)Parity wallet freeze (50M+ at risk)
T3Smart ContractCriticalLow (L1 post-6780) / Medium (L2)Tornado Cash governance attack (~$1M+), metamorphic contract research
T4Smart ContractMediumLow (post-6780)Code-dependent logic breakage (theoretical, mitigated by EIP-6780 on L1)
T5Smart ContractMediumLow (post-6780)Contract resurrection via CREATE2 (demonstrated in Tornado Cash attack)
P1ProtocolMediumN/AEIP-6780 behavioral fork; legacy contracts affected
P2ProtocolLowN/AState trie cleanup removal; EIP-3529 refund elimination
P3ProtocolMediumMediumL1/L2 divergence on SELFDESTRUCT semantics (ongoing)
P4ProtocolLowN/AGasToken exploitation eliminated by EIP-3529 (London, 2021)

OpcodeRelationship
CREATE (0xF0)Creates new contracts. Used with SELFDESTRUCT in the metamorphic contract pattern: CREATE deploys at a nonce-dependent address, SELFDESTRUCT resets the nonce (pre-6780), allowing redeployment at the same address.
CREATE2 (0xF5)Deterministic contract creation. The key enabler for metamorphic contracts: CREATE2’s address depends only on deployer + salt + init_code_hash, so after SELFDESTRUCT clears the address (pre-6780), identical CREATE2 parameters redeploy at the same address with different runtime code.
CALL (0xF1)Standard ETH transfer mechanism. Unlike SELFDESTRUCT, CALL triggers recipient code execution. SELFDESTRUCT’s forced transfer bypasses all CALL-based protections (receive, fallback, gas limits).
BALANCE (0x31)Returns an address’s ETH balance. Contracts using BALANCE (or address(this).balance) for logic are vulnerable to SELFDESTRUCT force-sending, which inflates the balance without triggering any code.
SELFBALANCE (0x47)Returns the executing contract’s own ETH balance (cheaper than BALANCE(ADDRESS())). Same vulnerability as BALANCE — the value includes force-sent ETH from SELFDESTRUCT.
DELEGATECALL (0xF4)Executes code in the caller’s context. When SELFDESTRUCT runs inside a DELEGATECALL, it destroys the calling contract (proxy), not the implementation. This is the mechanism behind the Parity freeze and UUPS proxy vulnerability.