Skip to main content
Sigvex

Remediating Metamorphic Contract Risks

How to prevent code replacement attacks by removing SELFDESTRUCT from CREATE2-deployed contracts and using standard proxy patterns.

Remediating Metamorphic Contract Risks

Overview

Related Detector: Metamorphic Contract

A metamorphic contract combines CREATE2 (deterministic address deployment) with SELFDESTRUCT to enable code replacement at a fixed address. The fix is to remove SELFDESTRUCT from contracts deployed via CREATE2. If upgradeability is needed, use a standard proxy pattern where the proxy address is stable and the implementation is swapped through a controlled mechanism.

Remove SELFDESTRUCT from CREATE2-Deployed Contracts

// BEFORE: Metamorphic pattern
contract Deployable {
    address owner;
    constructor() { owner = msg.sender; }

    function destroy() external {
        require(msg.sender == owner);
        selfdestruct(payable(owner));  // Enables redeployment
    }
}

// AFTER: No SELFDESTRUCT
contract Deployable {
    address owner;
    constructor() { owner = msg.sender; }
    // Contract is permanent -- no selfdestruct
    // Use UUPS or Transparent proxy if upgradeability is needed
}

Alternative Mitigations

Use UUPS Proxy Instead of CREATE2 + SELFDESTRUCT

If upgradeability is a requirement, replace the metamorphic pattern with a standard proxy:

import "@audited/proxy/ERC1967/ERC1967Proxy.sol";
import "@audited/proxy-utils/proxy/utils/UUPSUpgradeable.sol";

contract VaultV1 is UUPSUpgradeable {
    function initialize() public initializer { }

    function _authorizeUpgrade(address newImpl) internal override onlyOwner { }
}

// Deploy once -- proxy address is stable, implementation is upgradeable
ERC1967Proxy proxy = new ERC1967Proxy(address(new VaultV1()), "");

Verify External Contract Immutability On-Chain

When your contract depends on an external address, verify that the code at that address cannot change:

function setDependency(address target) external onlyOwner {
    // Verify the target has code and is not self-destructible
    uint256 size;
    assembly { size := extcodesize(target) }
    require(size > 0, "No code at address");
    dependency = target;
}

Note: This check does not prevent metamorphic redeployment. For high-value dependencies, verify the deployer and deployment method off-chain.

Common Mistakes

Mistake: Assuming EIP-6780 Eliminates All Risk

// Post-Cancun, SELFDESTRUCT only works in the same tx as creation.
// BUT: a factory can CREATE2 + SELFDESTRUCT in the same transaction,
// then CREATE2 again in the next transaction with different code.

EIP-6780 mitigates but does not eliminate same-transaction metamorphic patterns. Remove SELFDESTRUCT entirely for full protection.

Mistake: Relying on Access Control Alone

function destroy() external onlyOwner {
    selfdestruct(payable(owner));
}

If the owner key is compromised, the attacker can destroy and redeploy. The SELFDESTRUCT capability itself is the root risk, not the access control around it.

References