solidity:create创建合约

643 阅读7分钟

问题:new 底层是使用的create吗?

new关键字在Solidity中的底层实现确实使用了create操作。当你在Solidity合约中使用new关键字来创建一个新的合约实例时,它背后的机制实际上是调用EVM的create指令。

这里是new关键字和create指令之间的关系简述:

  1. new关键字(高级操作)new是Solidity的一个高级语法,用于创建新的合约实例。当你使用new时,Solidity编译器会处理所有与创建新合约相关的细节,包括构造函数参数的传递、合约字节码的准备、以及调用create指令。
  2. create指令(低级操作)create是EVM的低级操作,用于在区块链上创建一个新的合约。它接受合约的字节码和可选的构造函数参数,并在区块链上部署这个新合约。

因此,当你在Solidity合约中使用new关键字时,Solidity编译器在编译你的合约时会生成相应的底层代码,这些底层代码包括调用EVM的create操作。这使得new关键字成为一个简洁且安全的方式来创建新合约,因为它隐藏了许多底层操作的复杂性和潜在的陷阱。相比之下,直接使用create指令(如通过内联汇编)需要你手动处理更多的细节,通常只在特定的高级用例中才会使用。

预备知识:EVM地址的生成规则

对于使用CREATE指令(即通过高级语言如Solidity的new操作隐式使用)部署的合约,地址的生成遵循以下准则:

  1. 基于发送者地址和nonce:合约地址是由部署合约的账户(发送者)的地址和该账户的nonce(一个计数器,表示账户发起的交易数量)共同决定的。
  2. 地址的计算方法:具体来说,地址是通过对发送者地址和nonce进行Keccak-256哈希运算,然后取这个哈希值的最后20字节来生成的。这种方法确保了每个新创建的合约都有一个唯一的地址。
  3. Nonce的递增:每次账户发起交易(包括部署合约),其nonce就会递增。这意味着相同的账户无法再次生成相同的合约地址,因为每次部署合约时它的nonce都会不同。
  4. 合约部署:区分EOA账户与合约内部 new 关键字内部部署合约,两者在取的nonce是不同的。合nonce是由该合约创建的其他合约数量决定的。本质还是选择创建合约者的地址,只不过EOA与合约地址的nonce规则时不同的。

这个规则适用于通过CREATE指令部署的合约

CREATE2指令有不同的规则,允许通过特定的输入生成可预测的合约地址。对于CREATE2,合约地址基于部署者地址、提供的“salt”值(任意32字节的数据),以及合约初始化代码的哈希值来生成。

部署时Nonce的选择

以太坊中有两种类型的账户:外部拥有账户(Externally Owned Accounts,EOAs)和合约账户(Contract Accounts)。它们的nonce计算方式有所不同:

  1. 外部拥有账户(EOAs) :对于个人用户的账户(即EOAs),nonce是该账户发出的交易数量。每次账户发送一个新交易时,其nonce都会增加。这是以太坊防止交易重放攻击的一种机制。
  2. 合约账户:对于合约账户,nonce是由该合约创建的其他合约数量决定的。每当一个合约创建另一个新合约时,其nonce会增加。这就是为什么在上下文中说“当前已创建的合约数量”:它是指由特定合约账户创建的合约数量。

在讨论CREATE操作和合约地址生成时,我们通常指的是合约账户的nonce。当一个合约使用CREATE操作创建另一个合约时,新合约的地址部分是由父合约的地址和父合约的nonce(即它到目前为止创建的合约数量)来确定的。这与EOAs账户的nonce,即其发送的交易数量是两个不同的概念。

官方文档

文档地址:[docs.soliditylang.org/en/v0.8.25/…]

其中描述了如何通过new关键字创建合约以及CREATE2用于创建合约的高级用法。

通过new创建合约

合约可以使用new关键字创建其他合约。被创建的合约的完整代码在创建合约的合约编译时必须已知,因此不可能存在递归创建依赖。

solidityCopy code
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract D {
    uint public x;
    constructor(uint a) payable {
        x = a;
    }
}

contract C {
    D d = new D(4); // 将作为C的构造函数的一部分执行

    function createD(uint arg) public {
        D newD = new D(arg);
        newD.x();
    }

    function createAndEndowD(uint arg, uint amount) public payable {
        // 创建时发送以太币
        D newD = new D{value: amount}(arg);
        newD.x();
    }
}

如示例所示,可以在创建D的实例时使用value选项发送Ether,但无法限制gas的使用量。如果创建失败(由于栈溢出、余额不足或其他问题),将抛出异常。

盐化(salt)合约创建 / create2

创建合约时,合约地址是根据创建合约的合约地址和随每次合约创建而增加的计数器计算的。(nonce)

如果你指定了salt选项(一个bytes32值),那么合约创建将使用不同的机制来计算新合约的地址:

它将从创建合约的合约地址、给定的salt值、创建的合约的(创建)字节码和构造函数参数计算地址。

特别是,不使用计数器(“nonce”)。这为创建合约提供了更多的灵活性:在创建新合约之前,你可以推导出新合约的地址。此外,即使创建合约在此期间创建了其他合约,你也可以依靠这个地址。

这里的主要用例是作为线下交互裁判的合约,只有在有争议时才需要创建。

solidityCopy code
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract D {
    uint public x;
    constructor(uint a) {
        x = a;
    }
}

contract C {
    function createDSalted(bytes32 salt, uint arg) public {
        // 这个复杂的表达式只是告诉你地址如何被预先计算。它仅用于说明。
        // 你实际上只需要``new D{salt: salt}(arg)``。
        address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(abi.encodePacked(
                type(D).creationCode,
                abi.encode(arg)
            ))
        )))));

        D d = new D{salt: salt}(arg);
        require(address(d) == predictedAddress);
    }
}

warning

关于盐化创建有一些特殊之处。在合约被销毁后,可以在同一地址重新创建合约。然而,即使创建字节码相同(否则地址会改变),新创建的合约可能有不同的部署字节码。这是因为构造函数可以查询外部状态(可能在两次创建之间发生了变化)并在存储之前将其合并到部署的字节码中。

汇编指令使用create、create2(chatgpt)(了解)

使用 create 指令

pragma solidity ^0.8.0;

contract Deployer {
    function deploy(bytes memory bytecode) public returns (address) {
        address newContract;
        assembly {
            newContract := create(0, add(bytecode, 0x20), mload(bytecode))
        }
        require(newContract != address(0), "Failed to deploy contract");
        return newContract;
    }
}

这个deploy函数接受一个合约的字节码,并使用create指令来部署它。create指令接受三个参数:要发送到新合约的ether数量(这里是0),字节码在内存中的起始位置,以及字节码的大小。

使用 create2 指令

pragma solidity ^0.8.0;

contract Deployer {
    function deploy(bytes memory bytecode, uint256 salt) public returns (address) {
        address newContract;
        assembly {
            newContract := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }
        require(newContract != address(0), "Failed to deploy contract");
        return newContract;
    }
}

create类似,create2也是在内联汇编中调用。它接受一个额外的salt参数,这允许合约地址的预计算和重复创建具有相同地址的合约。参数包括发送到新合约的ether数量(这里是0)、字节码在内存中的起始位置、字节码的大小,以及salt值。