0%

创建管理合约

在 Solidity 中,一个合约可以通过多种方式在另一个合约中创建和管理另一个合约。

合约内部部署(创建一个新合约

Solidity 允许你在一个合约内部部署另一个合约。这通常是通过在合约中创建一个新的合约实例来实现的。你可以在构造函数或其他函数中动态部署一个新合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract NewContract {
address public owner;

constructor(address _owner) {
owner = _owner;
}

function getOwner() public view returns (address) {
return owner;
}
}

contract CreatorContract {
address public lastCreatedContract;

function createNewContract() public {
NewContract newContract = new NewContract(msg.sender);
lastCreatedContract = address(newContract);
}
}

解释:

  • CreatorContract 中有一个 createNewContract 函数,它使用 new 关键字创建了一个 NewContract 实例。NewContract 合约的构造函数接收 msg.sender 作为参数,意味着新合约的 owner 将是调用 createNewContract 函数的账户。
  • 创建的新合约地址会存储在 lastCreatedContract 中。

合约工厂模式(Factory Pattern)

另一种常见的做法是使用工厂模式,即一个合约作为工厂来部署多个合约实例。工厂合约允许你根据需要创建新合约,并管理这些合约实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Item {
uint public id;
address public creator;

constructor(uint _id, address _creator) {
id = _id;
creator = _creator;
}
}

contract ItemFactory {
Item[] public items;

function createItem(uint _id) public {
Item newItem = new Item(_id, msg.sender);
items.push(newItem);
}

function getItems() public view returns (Item[] memory) {
return items;
}
}

解释:

  • ItemFactory 是一个工厂合约,它可以创建多个 Item 合约的实例,每个 Item 合约的创建者是调用工厂合约的账户。
  • createItem 函数部署了一个新的 Item 合约,并将它存储在 items 数组中。

调用外部合约(外部合约部署后实例化)

有时,合约不需要在自己内部部署其他合约,而是可以通过调用已经部署的外部合约的地址来与其交互。这不是“创建”合约,但却是一种与其他合约交互的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IExternalContract {
function getOwner() external view returns (address);
}

contract ContractInteraction {
IExternalContract public externalContract;

constructor(address _externalContractAddress) {
externalContract = IExternalContract(_externalContractAddress);
}

function getExternalOwner() public view returns (address) {
return externalContract.getOwner();
}
}

解释:

  • ContractInteraction 合约通过接口与外部合约交互,调用了 IExternalContract 中的 getOwner 函数。
  • 外部合约已经部署并被传入构造函数,ContractInteraction 合约与它交互。

使用 create2 来部署合约(指定地址部署)

create2 是一种更高级的部署方法,它允许你通过指定合约地址来部署合约。使用 create2 可以确保合约在指定的地址上部署,前提是你知道合约的字节码和盐值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FactoryWithCreate2 {
event ContractCreated(address contractAddress);

function createNewContract(bytes32 salt) public {
address newContractAddress;
bytes memory bytecode = type(NewContract).creationCode;

assembly {
newContractAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}

emit ContractCreated(newContractAddress);
}
}

contract NewContract {
address public creator;

constructor() {
creator = msg.sender;
}
}

解释:

  • createNewContract 函数使用 create2 来部署一个新的 NewContract 合约。
  • create2 允许你在部署时指定一个 salt,确保你可以预测到部署合约的地址,前提是知道合约的字节码。

扩展信息

在 Solidity 中,type(NewContract).creationCode 是一种用于获取合约字节码的方法,它表示合约的创建代码。合约的字节码(bytecode)是它部署到区块链上的机器代码,其中包含了合约的逻辑和部署时所需的初始化数据。

合约字节码的组成部分

合约字节码可以分为以下几个主要部分:

  1. 合约构造函数的初始化字节码
  2. 部署过程中的合约代码
  3. 合约的状态变量初始值

合约构造函数的初始化字节码

  • 构造函数的字节码:每个合约在部署时都会执行其构造函数。creationCode 中包含了构造函数的代码部分,它负责初始化合约的状态,通常包括合约的状态变量赋值、事件初始化等操作。
  • 初始化参数:如果构造函数带有参数(例如合约需要初始化的状态变量),这些参数也会包含在 creationCode 中。部署合约时需要传递这些参数。

creationCode:合约的“创建代码”,是用于部署合约的字节码,包含了构造函数的执行内容以及初始化步骤。在合约部署时,链上会执行这段创建代码,初始化合约,并且部署合约的 runtimeCode

type(NewContract).creationCode 提供了一个方法来获取合约的 创建代码(即部署合约时使用的字节码),这段代码由合约的构造函数(constructor)以及合约的初始化部分组成,执行时会将合约的运行时代码存储在区块链上。

creationCode 是合约创建时所需的字节码,包括构造函数的初始化部分、合约的部署逻辑等。

部署过程中的合约代码

  • 字节码本体:合约部署的代码(通常是 runtime code)会在合约创建时存储在链上。这个字节码本体包括了所有的业务逻辑和函数实现。它包含了所有合约的行为和功能,例如状态修改、函数调用等。

runtimeCode:合约部署后剩余的代码,包含了合约的所有业务逻辑。合约一旦部署完成,它的 runtimeCode 就是唯一存在于区块链上的代码,而 creationCode 则只在合约创建时有作用。

合约的状态变量初始值

  • 初始化存储:合约在部署时,通常会有一些状态变量。它们的初始值会作为部署合约时的一部分被编码到创建代码中(如果有默认值)。这些值将在合约部署后存储到合约的状态中。

create 和 create2

在 Solidity 中,create 和 create2 是用于部署合约的两种不同方法。二者的主要区别在于如何计算合约地址及其对地址预测的能力。理解这两者的原理及其差异对于优化合约部署和管理合约地址是非常重要的。

create 的原理

create 是最常见的合约部署方式,它通过在 Ethereum 网络上发送一笔交易来部署一个新的合约。

  • 部署过程:合约的字节码通过交易提交到区块链网络,然后在区块链中分配一个新的地址。
  • 地址计算:create 部署的合约的地址由 创建者的地址(msg.sender) 和 创建交易的nonce 决定。

合约地址计算公式:

1
address = keccak256(rlp_encode(sender, nonce))[12:]
  • sender:部署合约的账户地址(msg.sender)。
  • nonce:msg.sender 的交易计数,即发送交易的次数。
1
address create(uint256 value, bytes memory bytecode, uint256 bytecodeSize)
  1. value(uint256):该参数表示向新合约发送的以太币数量。在合约部署时,你可以指定一个 value,这部分以太币将转移到新部署的合约中。若不想发送以太币,value 为 0。
    value 是一个 uint256 类型,表示你要发送到新合约的以太币金额。
  2. bytecode(bytes memory):即合约的字节码,它是合约代码的二进制形式。这是通过 type(Contract).creationCode 或其他类似的方法获取的合约字节码。
    这段字节码包含了合约的构造函数以及合约的逻辑代码。可以通过 type(MyContract).creationCode 获取。add(bytecode, 0x20) 字节码的存储位置,跳过前 32 字节,指向字节码的实际数据。
  3. bytecodeSize(uint256) : 它表示合约字节码的长度(通常是 32 字节)。

使用create

create 是标准的合约部署方式,适用于常规场景。通常情况下,您无需关心合约的地址,只需让合约被部署到区块链即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.8.0;

contract NewContract {
uint256 public value;

constructor(uint256 _value) {
value = _value;
}
}

contract Factory {
address public deployedContract;

function deploy(uint256 _value, bytes32 _salt) external {
bytes memory bytecode = type(NewContract).creationCode; // 获取合约字节码
address contractAddress;

assembly {
contractAddress := create(0, add(bytecode, 0x20), mload(bytecode))
}

deployedContract = contractAddress; // 记录新合约地址
}
}

在这个例子中,Factory 合约会通过 create 部署一个新的 NewContract 合约。NewContract 的地址由 msg.sender 和 nonce 决定,因此无法在部署前预测。

create2 的原理

create2 是 Solidity 0.5.0 引入的一种新的合约部署方式,它为部署合约提供了额外的控制能力,特别是在可预测的合约地址生成方面。与 create 不同,create2 的合约地址不仅由部署者的地址和交易 nonce 决定,还由以下额外的内容决定:

  • 部署者的地址 (msg.sender)
  • salt:一个额外的任意字节值
  • 合约字节码的哈希:即部署的合约的字节码。

合约地址计算公式:

1
address = keccak256(0xff ++ sender ++ salt ++ keccak256(bytecode))[12:]
  • 0xff 是一个常量,用于确保合约地址计算的唯一性。
  • sender 是合约创建者的地址。
  • salt 是一个提供给 create2 的自定义值,可以是任意字节。
  • bytecode 是合约的字节码。

create2 使得合约的地址可以在合约部署之前预先计算出来,因此我们可以在部署合约之前预测合约的地址。

1
address create2(uint256 value, bytes memory bytecode, uint256 bytecodeSize, bytes32 salt)
  1. value(uint256):该参数表示向新合约发送的以太币数量。在合约部署时,你可以指定一个 value,这部分以太币将转移到新部署的合约中。若不想发送以太币,value 为 0。
    value 是一个 uint256 类型,表示你要发送到新合约的以太币金额。
  2. bytecode(bytes memory):即合约的字节码,它是合约代码的二进制形式。这是通过 type(Contract).creationCode 或其他类似的方法获取的合约字节码。
    这段字节码包含了合约的构造函数以及合约的逻辑代码。可以通过 type(MyContract).creationCode 获取。add(bytecode, 0x20) 字节码的存储位置,跳过前 32 字节,指向字节码的实际数据。
  3. bytecodeSize(uint256) : 它表示合约字节码的长度(通常是 32 字节)。
  4. salt(bytes32):salt 是一个自定义的 bytes32 类型值,它为合约的创建地址提供一个“随机数”。salt 用于确保合约地址计算的唯一性。通过修改 salt 的值,可以控制生成的合约地址,从而为每个合约生成一个独特的地址。
    salt 是唯一标识合约部署的一部分。你可以通过传入不同的 salt 值来生成不同的合约地址,即使合约字节码完全相同。

使用create2

create2 则允许你指定一个 salt 值,进而可以提前计算出部署后的合约地址。这对于某些应用非常有用,例如需要在合约创建之前就知道合约的地址,或者在某些情境下需要复用相同地址的合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pragma solidity ^0.8.0;

contract NewContract {
uint256 public value;

constructor(uint256 _value) {
value = _value;
}
}

contract Factory {
address public deployedContract;

function deploy(uint256 _value, bytes32 _salt) external {
bytes memory bytecode = type(NewContract).creationCode; // 获取合约字节码
bytes32 salt = _salt; // 使用用户传入的 salt 值
address contractAddress;

assembly {
contractAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}

deployedContract = contractAddress; // 记录新合约地址
}
}

在这个例子中,Factory 合约使用 create2 部署一个 NewContract 合约。通过传入 salt 值,create2 会计算出一个特定的合约地址。这样,你可以在合约部署之前预测合约的地址。合约地址的计算基于 bytecode 和 salt 的哈希,因此只要 salt 和 bytecode 不变,生成的地址也将是固定的。

create 和 create2 的主要区别

特性 create create2
地址生成 由 msg.sender 地址和交易 nonce 决定 由 msg.sender 地址、salt 和合约字节码的哈希决定
预测地址 无法预测合约地址(取决于交易 nonce 和区块时间) 可以在合约部署前预测合约地址
灵活性 地址不可控制,无法提前决定 可以通过不同的 salt 值控制地址并预先计算
安全性 地址依赖于交易 nonce,可能会有一定的不确定性 地址可由部署者提前确定,减少了竞争条件
适用场景 普通合约部署 需要提前知道合约地址的场景
使用复杂度 简单 需要手动计算字节码和 salt

参考&鸣谢