The Tornado Cash governance exploit of May 20, 2023, introduced many in the cryptocurrency community to a class of attack that, while known in security research circles, had not been demonstrated at scale in a live governance context. The attacker’s use of CREATE2 deterministic deployment to replace a governance proposal contract at the same address represents a sophisticated technique that demands advanced defensive measures. This tutorial provides a technical deep dive into the CREATE2 address collision attack vector and walks through practical countermeasures that smart contract developers and DAO architects can implement. At the time of writing, Bitcoin trades at approximately $26,750 and Ethereum at $1,805.
The Objective
This tutorial aims to equip experienced smart contract developers with the knowledge and tools to detect, prevent, and mitigate CREATE2-based address collision attacks in governance systems. By the end of this walkthrough, you will understand the mathematical basis for deterministic address generation, how attackers exploit the interaction between CREATE and CREATE2 opcodes, and how to build governance systems that are resilient against this class of attack.
Prerequisites
This tutorial assumes familiarity with Solidity development, the Ethereum Virtual Machine’s contract creation opcodes, and basic DAO governance patterns. You should be comfortable reading Solidity code and understanding concepts like delegatecall, storage layouts, and contract deployment mechanisms. Familiarity with tools like Foundry, Hardhat, or Brownie for testing smart contracts is recommended but not required.
Step-by-Step Walkthrough
Step 1: Understanding Deterministic Address Generation
The Ethereum Virtual Machine supports two opcodes for creating new contracts: CREATE and CREATE2. The address generated by each is calculated differently. For CREATE, the new address is computed as the rightmost 160 bits of the Keccak-256 hash of the sender’s address and their nonce. For CREATE2, the address is computed as the rightmost 160 bits of the Keccak-256 hash of 0xFF, the sender’s address, a user-provided salt, and the contract’s creation bytecode.
The critical insight is that CREATE generates addresses based on the sender and nonce, regardless of the bytecode being deployed. This means if a contract at address X with nonce N deploys a child contract, the child’s address depends only on X and N — not on what code is being deployed. An attacker can exploit this by first deploying a benign contract using a factory, then destroying the factory, redeploying the factory at the same address using CREATE2, and deploying a malicious child contract that resolves to the same address as the original benign contract.
Step 2: Analyzing the Tornado Cash Attack
In the Tornado Cash attack, the attacker deployed a factory contract at address 0x7dc8 using CREATE2. This factory then deployed Proposal 20 at address 0xc503 using CREATE. The proposal appeared legitimate and the community voted for it. After the vote passed, the attacker called a hidden emergencyStop function that executed SELFDESTRUCT on both the proposal and its factory. The attacker then redeployed the factory at the same address 0x7dc8 using CREATE2 with the same sender, salt, and initial factory bytecode. When the redeployed factory used CREATE to deploy a new contract, the sender (0x7dc8) and nonce (1) were identical to the original deployment, so the child contract resolved to the same address 0xc503 — but this time with completely different, malicious code.
The governance contract then called execute on the proposal, which used delegatecall to execute the malicious proposal code in the governance contract’s context, granting the attacker control over the DAO.
Step 3: Implementing Bytecode Verification
The most direct defense against address collision attacks is to verify the bytecode at the proposal address before execution. Implement a governance contract that stores the expected bytecode hash of each proposal at the time it is submitted. Before executing any proposal, the contract verifies that the bytecode currently residing at the proposal address matches the stored hash.
mapping(uint256 => bytes32) public proposalCodeHash;
function submitProposal(address target) external {
bytes32 codeHash;
assembly { codeHash := extcodehash(target) }
proposals[proposalCount] = Proposal({
target: target,
codeHash: codeHash
});
}
function executeProposal(uint256 proposalId) external {
Proposal storage p = proposals[proposalId];
bytes32 currentHash;
assembly { currentHash := extcodehash(p.target) }
require(currentHash == p.codeHash, "Code hash mismatch");
// proceed with execution
}
This approach ensures that if an attacker replaces the contract at the proposal address, the bytecode hash will differ and execution will revert.
Step 4: Preventing Self-Destruct in Governance
Since the attack requires the ability to destroy and redeploy contracts, preventing self-destruct functionality in proposal contracts eliminates the attack vector entirely. Use a proxy pattern where governance proposals are deployed behind immutable proxies that cannot be self-destructed. Alternatively, scan submitted proposal bytecode for the SELFDESTRUCT opcode (0xFF at the EVM level) and reject any proposal that contains it.
Step 5: Implementing Execution Delay with Verification
Combine a time lock with automated bytecode verification. After a proposal passes the vote, enforce a minimum delay of 48-72 hours. During this delay period, an automated verification script checks that the proposal contract’s bytecode hash has not changed since the vote began. If any change is detected, the proposal is automatically cancelled and an alert is raised.
Troubleshooting
Issue: Bytecode hash check fails for legitimate proposals. Some governance patterns involve proposals that legitimately modify their own code, such as upgradeable contracts. In these cases, implement a whitelist of approved code transition patterns that the verification system will accept, rather than requiring an exact hash match at execution time.
Issue: Gas costs for bytecode verification are too high. The extcodehash operation is relatively cheap at 700 gas, but if your governance system processes many proposals, consider batch verification where all pending proposals are verified in a single transaction before the execution window opens.
Issue: CREATE2 is needed for legitimate purposes. Some protocols use CREATE2 for predictable address generation in factory patterns. This is fine — the vulnerability is not CREATE2 itself but the combination of self-destruct and redeployment. Focus on preventing the self-destruct step rather than banning CREATE2 entirely.
Mastering the Skill
To deepen your understanding of governance security, study the OpenZeppelin Governor framework, which implements many of the protective patterns discussed in this tutorial. Practice deploying and attacking a vulnerable governance contract in a local test environment using Foundry. Review the audit reports from major DeFi protocols — many publicly disclose governance-related findings that provide real-world learning material. The Ethereum security community maintains resources at ethereum.org/en/developers/docs/security that cover additional attack vectors beyond CREATE2 collisions. As governance systems continue to manage increasingly large treasuries, the developers who master these defensive techniques will be in high demand across the Web3 ecosystem.
Disclaimer: This article is for informational purposes only and does not constitute financial or investment advice. Always conduct your own research before making any financial decisions.
the CREATE2 collision trick is genuinely clever from an exploit perspective. deploying a benign contract, self-destructing it, then redeploying malicious code at the same address. brutal
clever is one word for it. the EVM allowing self-destruct + CREATE2 at the same address is the real bug here, not the attacker
the EVM allowing selfdestruct + CREATE2 at the same address is a design flaw not a feature. this was documented in 2017 and nobody fixed it
EIP-6780 in the dendrum upgrade basically killed selfdestruct. the attack vector described here is now patched at the protocol level. important writeup but mostly historical now
sol_audit_ EIP-6780 disabling selfdestruct was the nuclear option. broke half of ENS wrapper contracts and temporary contract patterns that relied on it. messy fix but necessary
opcode_archaeologist EIP-6780 killed selfdestruct but also broke every contract using it for temporary patterns. the fix was correct but the migration was chaos
Elsa B. calling selfdestruct + CREATE2 a bug is generous. EVM designers explicitly chose to allow it. the bug was nobody thinking through governance implications
tornado cash governance was the wake up call. deploy benign, self destruct, redeploy malicious at the same address. elegant and terrifying
been waiting for someone to write this up properly. the math behind deterministic address generation gets handwaved in most blog posts. glad to see the full breakdown
agreed. most coverage just says governance attack without explaining the CREATE vs CREATE2 opcode interaction. this actually explains the mechanism
Chen is right. most coverage just says governance attack and moves on. the actual CREATE vs CREATE2 interaction is worth understanding if you build on evm
heaprot_ the deterministic address math is actually simple. keccak256(sender, salt, bytecode). the trick was never the math, it was realizing governance contracts could be swapped
this attack influenced every DAO security audit since mid 2023. the pattern of deploy benign, selfdestruct, redeploy malicious is now a standard checklist item for auditors. valuable reference even post-EIP-6780
Sasha M. this attack becoming a standard audit checklist item is the real legacy. every DAO audit since 2023 checks for CREATE2 replacement vectors now. brutal way to learn
Devi S. standard audit checklist is good but most DAOs still skip formal verification of their governance contracts. audit != verified. the gap is where exploits live
memory_safe_ formal verification is the real gap. audits catch common patterns but CREATE2 replacement requires mathematical proofs that most DAOs wont pay for
EIP-6780 killing selfdestruct was the right call but it broke contracts using it for factory patterns. migrations were brutal. some DAOs still havent upgraded
Tornado Cash governance exploit using CREATE2 to swap the proposal contract at the same address was elegant in a terrifying way. the EVM lets you do this by design
create2_tracer_ the scary part is most DAO governance contracts do not verify init code hash before executing proposals. this attack vector is still open in 2026
Anneliese W. most governance contracts still dont verify init code hash because it requires storing the expected hash on chain which costs gas. laziness driven vulnerability
Reinhold B. storing the hash onchain costs like 20k gas extra per proposal. protocols are skipping it to save $3 in fees while holding $500M TVL. priorities
CREATE2 letting you deploy to the same address with different init code is not a bug, its the EVM working as designed. the real failure is governance contracts not checking init code hash
EIP-6780 broke selfdestruct but CREATE2 address collision is still wide open. fixing one footgun while leaving the other loaded