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.
Recommended Fix
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 "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts-upgradeable/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.