Metamorphic Contract
Detects contracts that can be destroyed and redeployed at the same address with different code using CREATE2 and SELFDESTRUCT.
Metamorphic Contract
Overview
The metamorphic contract detector identifies contracts that combine CREATE2 and SELFDESTRUCT opcodes, enabling a code-replacement attack. CREATE2 deploys contracts at deterministic addresses based on the deployer, salt, and init code hash. If the deployed contract can SELFDESTRUCT, the deployer can redeploy different bytecode at the same address. Other contracts holding references to that address now interact with entirely different logic.
The Tornado Cash governance attack (2023) exploited this pattern: a malicious proposal contract was deployed, destroyed, and redeployed with different code at the same address to hijack governance.
Why This Is an Issue
Smart contracts are generally assumed to be immutable once deployed. Other protocols, multisigs, and users trust that a contract at a given address will always run the same code. Metamorphic contracts break this assumption. An attacker can pass an audit with benign code, get the contract address whitelisted by other protocols, then swap the code for a malicious version.
Post-Cancun (EIP-6780), SELFDESTRUCT only works within the same transaction as contract creation, significantly limiting this attack vector for modern contracts. However, pre-Cancun contracts remain vulnerable.
How to Resolve
// Before: Vulnerable -- CREATE2 + SELFDESTRUCT in same contract
contract Factory {
function deploy(bytes32 salt, bytes memory code) external returns (address) {
address addr;
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
}
return addr;
}
function destroy(address target) external {
Destroyable(target).destroy(); // Enables redeploy
}
}
// After: Fixed -- remove SELFDESTRUCT capability
contract SafeFactory {
function deploy(bytes32 salt, bytes memory code) external returns (address) {
address addr;
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
}
return addr;
}
// No destroy function -- deployed contracts are immutable
}
Examples
Vulnerable
contract Metamorphic {
address public owner;
constructor() { owner = msg.sender; }
function kill() external {
require(msg.sender == owner);
selfdestruct(payable(owner)); // Allows redeployment
}
}
Fixed
contract Immutable {
address public owner;
constructor() { owner = msg.sender; }
// No selfdestruct -- contract is permanent
// Use a proxy pattern if upgradeability is needed
}
Sample Sigvex Output
[HIGH] metamorphic-contract
Metamorphic pattern: CREATE2 + SELFDESTRUCT in deployAndDestroy
Location: deployAndDestroy @ block 0, instruction 4
Confidence: 0.70
Function 'deployAndDestroy' contains both CREATE2 and SELFDESTRUCT
opcodes. This enables code replacement at deterministic addresses.
Detection Methodology
- Opcode scanning: Identifies
CREATE2andSELFDESTRUCTinstructions across all functions in the contract. - Same-function detection: Reports highest confidence when both opcodes appear in the same function.
- Cross-function detection: Reports lower confidence when
CREATE2is in one function andSELFDESTRUCTin another. - Context-aware suppression: Reduces confidence for known proxy patterns, access-controlled factories, and audited library code. Contracts compiled with Solidity 0.8.18+ (EIP-6049 deprecation) are downgraded to low severity.
Limitations
False positives: Legitimate factory contracts using CREATE2 for deterministic deployment alongside upgrade mechanisms may be flagged. Audited libraries (OpenZeppelin, Solmate, Solady) with factory patterns are suppressed. False negatives: Metamorphic patterns split across multiple contracts (factory in one contract, selfdestruct in another) require cross-contract analysis.
Related Detectors
- CREATE2 Collision — detects CREATE2 salt predictability
- Selfdestruct — detects selfdestruct usage