Author: Xiao Bai

Editor: Liz

Background overview

In the Ethereum ecosystem, the deterministic generation mechanism of contract addresses provides convenience for developers, but it also introduces new attack surfaces. In this issue, we will analyze the attack techniques and defense strategies for deploying different contracts to the same address using the CREATE and CREATE2 opcodes at different times. Previous articles on smart contract security auditing can be found in the collection.

Prerequisite knowledge

First, let's understand the two rules for generating Ethereum addresses:

1. CREATE

CREATE is the native opcode for dynamically deploying smart contracts in the Ethereum Virtual Machine (EVM). Since the Ethereum genesis block, all contract deployments rely on this mechanism. Its core feature is that the address generation depends on the deployer's account nonce, making the address non-deterministic (impossible to predict accurately before deployment).

The contract address generated by CREATE is determined by the deployer's address and the nonce of the address:

2. CREATE2

CREATE2 is a new contract creation opcode introduced in the Ethereum Constantinople hard fork (February 2019). Unlike traditional CREATE, before deploying the contract, off-chain participants can pre-calculate the contract address, enabling off-chain interactions (such as state channels) and complex contract architectures.

The contract address generated by CREATE2 is determined by the following four parameters:

By now, I believe everyone has understood the two ways to generate contract addresses in Ethereum. Attentive readers may wonder, what if the computed contract address already exists? This is not a concern. In Ethereum, whether generated by CREATE or CREATE2, as long as the address already exists on the chain (whether an externally owned account EOA or a contract account), the EVM will reject the request to create the contract. The following are two scenarios for address conflicts:

1. The target address is an externally owned account (EOA)

  • Rule: If the target address is an existing EOA (for example, a user's wallet address), the EVM will reject the contract deployment request.

  • Result: Transaction fails, gas is consumed, the contract is not created, and no data at that address is overwritten.

2. The target address is a contract account

  • Rule: If the target address is an already deployed contract account, the EVM will also reject the deployment request.

  • Result: Transaction fails, gas is consumed, and the code and stored data of the original contract remain unchanged.

Of course, there are exceptions. If the target address is a contract and has been self-destructed, then it can redeploy a new contract at that address.

So far, we have introduced the various characteristics of the CREATE and CREATE2 opcodes. Next, let's see how to use these characteristics to perform a combination attack and complete a contract attack.

Vulnerability example

Vulnerability analysis

This DAO contract implements a basic governance mechanism: the Owner reviews proposals through the approve function and records the proposal contract address in the Proposals array. Any user can then execute the reviewed proposal through the execute function. Although the permission control seems tight (only the Owner can review proposals) combined with execution checks (the proposal must be reviewed and not executed), there is actually a hidden logical flaw: the reviewed proposal address may point to completely different contract code during execution. The attacker can carry out the attack in the following three steps:

1. Deploy a normal contract and obtain authorization

The attacker first deploys a contract A containing a harmless executeProposal() function, and adds the address to the proposal list through the Owner's review.

2. Self-destruct the original contract and occupy the address

Contract A executes the self-destruct operation (selfdestruct) to clear the code, and then the attacker uses the CREATE2 opcode to deploy the malicious contract B (containing dangerous logic for executeProposal()) at the same address.

3. Trigger execution, hijack control

When the user calls execute, the contract will execute the code of the newly deployed malicious contract B through delegatecall. Since delegatecall retains the current contract's context, the attacker can use this operation to tamper with the state of the DAO contract (such as modifying the Owner) or transfer assets.

Next, let's look at the specific attack process in conjunction with the attack contract.

Attack contract

The attack process is as follows:

1. Alice deploys the DAO contract.

2. Evil deploys the DeployerDeployer contract, with address DD.

3. Evil calls DD.deploy(), using CREATE2 to deploy the Deployer to address D (fixed salt).

4. Call D.deployProposal() to create Proposal contract address P, at this time D's nonce is 0.

5. Alice approves address P.

6. The attacker calls D.kill() to destroy D, at this time the account of address D is cleared, including the nonce being reset to 0.

7. The attacker calls DD.deploy() again to redeploy the Deployer to address D (with the same CREATE2 parameters).

8. Call D.deployAttack(), because at this time D's nonce is reset to 0 and D's address remains unchanged, the created Attack contract address is the same as the previous Proposal contract address P.

9. At this point, there is a proposal in the DAO's proposals array pointing to address P, but now address P has been redeployed as the Attack contract. Therefore, when execute() is called, it will execute Attack's executeProposal(), ultimately modifying the DAO's Owner to msg.sender, which is the Attack contract.

The attack principle can be summarized as the attacker utilizing CREATE2 to redeploy a contract to the same address. First, deploy a legitimate Proposal contract and have the DAO approve it. Then, destroy the Deployer contract, redeploy the Deployer to the same address, reset the nonce of that address, and finally successfully deploy the malicious contract to the same address as the previous Proposal. Since the proposal address stored in the DAO now points to the malicious contract, when executing the proposal, the malicious code executes in the DAO's context, successfully modifying the Owner to the attacker.

Fix suggestion

As a developer:

  • When approving a proposal, not only should the address be recorded, but also the code hash of that address, and verify the code hash for consistency during execution.

  • Avoid using delegatecall to call external contracts unless there are sufficient security measures.

  • Consider the possibility of redeploying to the same address after the contract self-destructs. When making external calls, check whether the external contract is trustworthy. If calling an unfamiliar external contract, it is recommended to check whether there is a self-destruct prevention mechanism in the contract.

As an auditor:

  • Identify whether there is a selfdestruct feature in the externally called contract. If it exists, be cautious of the attack risk of malicious code being deployed to the same address after the external contract self-destructs.

  • Check whether the contract deployment method used CREATE2, and ensure that the salt value used to generate the contract address is sufficiently random to prevent the address from being pre-emptively predicted and occupied by an attacker.

  • Check whether the DAO verifies the code consistency of the target address when executing proposals, such as comparing the current code hash with the hash at the time of approval.

  • Carefully analyze the usage scenarios of delegatecall, confirm whether the target address is trustworthy, and whether there is a risk of arbitrary address calling.

  • Review the lifecycle management of proposals, such as whether there is a mechanism to prevent proposals from being modified or replaced, for example, locking the status of the target address after approval.