create2原理讲解

说明

2019年2月底,操作码create2被添加到以太坊虚拟机。这段操作码引入了第二种计算新智能合约地址的方法(以前只有CREATE可用)。使用CREATE2当然比最初的CREATE更复杂。不再仅仅写new Token()就行了,通过写汇编代码或者通过指定salt使用 create2。

CREATE2 与 CREATE

在介绍create2之前,先来了解create(通过new的方式)创建合约的方法。通过new方法创建的合约地址由部署者的地址与nonce共同做keccak哈希生成的。如下所示:

1
contractAddress = keccak256(rlp.encode(deployingAddress, nonce))[12:]

对于那些需要在多个链部署的项目来说,如果保证部署者的地址相同,nonce相同,则可以部署出相同地址的合约。

CREATE2指令是以太坊EIP-1014引入的一种能预先计算合约地址的新的指令,它的算法如下:

1
contractAddress = keccak256(0xff + deployingAddress + salt + keccak256(bytecode))[12:]

以下展示几种使用合约创建合约的例子:

1
2
3
4
5
6
7
8
9
10
11
contract Car {
address public owner;
string public model;
address public carAddr;

constructor(address _owner, string memory _model) payable {
owner = _owner;
model = _model;
carAddr = address(this);
}
}

使用new的方式创建合约:

1
2
3
function create(address _owner, string memory _model) public {
Car car = new Car(_owner, _model);
}

使用create2的方式创建合约:

1
2
3
4
5
6
7
function create2(
address _owner,
string memory _model,
bytes32 _salt
) public payable {
Car car = (new Car){salt: _salt}(_owner, _model);
}

这个是通过salt的方式创建合约,salt是一个随机数.

使用create2的方式创建合约:

1
2
3
4
5
6
7
function create2ByASM(bytes32 salt, bytes memory CarContractBytecode) public {
address addr;
assembly {
addr := create2(0, add(CarContractBytecode, 0x20), mload(CarContractBytecode), salt)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
}

这个是通过汇编的方式创建合约,salt是一个随机数.

只要保证deployingAddress和一个指定的salt不变,相同的合约代码(bytecode)部署后得到的合约地址就一定是相同的。如果deployingAddress是一个EOA地址就很容易实现。如果deployingAddress是一个合约地址,就需要保证这个合约地址在所有的链上都是一样的。

如何保证deployingAddress合约地址是一样的呢?很简单,我们使用一个新账号,在每个链上都是用create的方式部署这个合约。因为这个新账号的nonce是1,所以在每个链上部署合约的地址都是一样的了。

create2的优势,就是可以预先计算合约地址,不需要等待合约部署完成,就可以知道合约地址。这样就可以在合约中调用其他合约的方法了。后面在实际应用中会有体现。

Create2的原理

根据 eip-1014 的介绍,create2需要接受4个参数:

  1. endowment,创建合约时向合约中打的ETH的数量
  2. memory_start,代码的起始位置,一般固定为 add(bytecode, 0x20)
  3. memory_length,代码长度,一般固定为 mload(bytecode)
  4. salt,随机值

如果第一个参数大于0,那创建合约的构造函数必须是payable,能够接受发送过来的eth。

第三方库OpenZeppelin中也封装好了create2方法用于部署合约,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Requirements:
*
* - `bytecode` must not be empty.
* - `salt` must have not been used for `bytecode` already.
* - the factory must have a balance of at least `amount`.
* - if `amount` is non-zero, `bytecode` must have a `payable` constructor.
*/
function deploy(
uint256 amount,
bytes32 salt,
bytes memory bytecode
) internal returns (address addr) {
require(address(this).balance >= amount, "Create2: insufficient balance");
require(bytecode.length != 0, "Create2: bytecode length is zero");
/// @solidity memory-safe-assembly
assembly {
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
}
require(addr != address(0), "Create2: Failed on deploy");
}

按照官方注解,实际使用时,需要注意以下问题:

  1. bytecode的长度不能为空
  2. 相同的bytecode不能有同样的salt,这样的话就会计算出相同的地址
  3. 如果amount不为0,则要求部署合约中必须有大于amount数量的eth,保证可以成功创建出合约
  4. 如果amount不为0,则要求待部署合约有payable的构造方法

Contract that Creates other Contracts中就展现了各种通过合约创建合约的方法。包括:

  • create,传统的create方法
  • createAndSendEther,传统的create方法并同时发送ETH到创建的目标合约
  • create2,使用 (new Car){salt: _salt}的方式创建合约
  • create2AndSendEther,使用{value: msg.value, salt: _salt}创建合约并同时发送ETH到创建的目标合约

除了在部署上和传统的create的方式不同之外,通过create2方式创建的合约可以在合约未创建出来时就可以获知合约的地址。具体的计算原理可以参考OpenZeppelin中的computeAddress方法。

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
26
27
function computeAddress(
bytes32 salt,
bytes32 bytecodeHash,
address deployer
) internal pure returns (address addr) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40) // Get free memory pointer

// | | ↓ ptr ... ↓ ptr + 0x0B (start) ... ↓ ptr + 0x20 ... ↓ ptr + 0x40 ... |
// |-------------------|---------------------------------------------------------------------------|
// | bytecodeHash | CCCCCCCCCCCCC...CC |
// | salt | BBBBBBBBBBBBB...BB |
// | deployer | 000000...0000AAAAAAAAAAAAAAAAAAA...AA |
// | 0xFF | FF |
// |-------------------|---------------------------------------------------------------------------|
// | memory | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
// | keccak(start, 85) | ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |

mstore(add(ptr, 0x40), bytecodeHash)
mstore(add(ptr, 0x20), salt)
mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
mstore8(start, 0xff)
addr := keccak256(start, 85)
}
}

实战部署

我们使用OpenZeppelin中的部署方法来实际部署一个合约。以下就是一个简单的部署合约以及计算合约的地址信息。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pragma solidity 0.8.13;

import "@openzeppelin/contracts/utils/Create2.sol";

contract Factory {

// 计算合约地址的方法
function getAddress() public view returns (address) {
return
Create2.computeAddress(
keccak256("Here is salt"),
keccak256(
abi.encodePacked(type(Car).creationCode, abi.encode("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","Model 3"))
)
);
}

// 部署合约
function deploy() public returns (address) {
address addr = Create2.deploy(
0,
keccak256("Here is salt"),
abi.encodePacked(type(Car).creationCode, abi.encode("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","Model 3"))
);
return addr;
}
}

contract Car {
address public owner;
string public model;
address public carAddr;

constructor(address _owner, string memory _model) payable {
owner = _owner;
model = _model;
carAddr = address(this);
}
}

通过foundry部署项目之后,编写TestCreate2.t.sol的如下代码进行测试:

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

import "forge-std/Test.sol";
import "../src/Factory.sol";

contract TestCreate2 is Test {
Factory public factory;
function setUp() public {
factory = new Factory();
}


function testCreate2Addr() public {
address computedAddress = factory.getAddress();
console2.logAddress(computedAddress);

address deployeddAddress = factory.deploy();
console2.logAddress(deployeddAddress);

}

}

最终得到输出结果是:

1
2
3
4
[PASS] testCreate2Addr() (gas: 218322)
Logs:
0x2265d86fbe4b7f324d8b8e2d6b5b3ddc9acfb9a6
0x2265d86fbe4b7f324d8b8e2d6b5b3ddc9acfb9a6

通过这样测试也验证我们之前的结论.我们如果通过create2这种方式创建合约,在事先得知合约创建合约的相关参数(盐,待部署合约的bytecode,构造函数的参数),那么我们就可以在合约未创建出来之前就可以获知合约的地址.这种特性,更加有利于我们在合约中进行一些预先的操作,比如在合约中进行一些预先的授权操作,这样就可以在合约创建出来之后,就可以直接使用这些预先授权的操作.

Uniswap分析

Uniswap中创建pair合约就是采用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
26
27
28
29
30
function createPair(address tokenA, address tokenB) external returns (address pair) {
//确认tokenA不等于tokenB
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
//将tokenA和tokenB进行大小排序,确保tokenA小于tokenB
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
//确认token0不等于0地址
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
//确认配对映射中不存在token0=>token1
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
//给bytecode变量赋值"UniswapV2Pair"合约的创建字节码
bytes memory bytecode = type(UniswapV2Pair).creationCode;
//将token0和token1打包后创建哈希
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
//内联汇编
//solium-disable-next-line
assembly {
//通过create2方法布署合约,并且加盐,返回地址到pair变量
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
//调用pair地址的合约中的"initialize"方法,传入变量token0,token1
IUniswapV2Pair(pair).initialize(token0, token1);
//配对映射中设置token0=>token1=pair
getPair[token0][token1] = pair;
//配对映射中设置token1=>token0=pair
getPair[token1][token0] = pair; // populate mapping in the reverse direction
//配对数组中推入pair地址
allPairs.push(pair);
//触发配对成功事件
emit PairCreated(token0, token1, pair, allPairs.length);
}

create2相关的代码是:

  1. bytes memory bytecode = type(UniswapV2Pair).creationCode; 获取UniswapV2Pair合约的创建字节码
  2. bytes32 salt = keccak256(abi.encodePacked(token0, token1)); 将token0和token1打包后创建哈希,作为后面create2的盐
  3. pair := create2(0, add(bytecode, 32), mload(bytecode), salt) 通过create2方法布署合约,并且加盐,返回地址到pair变量

按照上述的创建合约pair合约代码的例子,如果我们知道了token0token1的地址,那么我们就可以计算出这两个token的pair地址. 所以在Uniswap就运用了这种思想.如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function pairFor(
address factory,
address tokenA,
address tokenB
) internal pure returns (address pair) {
//排序token地址
(address token0, address token1) = sortTokens(tokenA, tokenB);
//根据排序的token地址计算create2的pair地址
pair = address(
uint256(
keccak256(
abi.encodePacked(
hex"ff",
factory,
keccak256(abi.encodePacked(token0, token1)),
// pair合约bytecode的keccak256
hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
)
)
)
);
}

按照``
我们可以简化上面的代码,就可以计算出任意的pair地址了,下面就是计算一对Token对应pair地址.

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

contract TestContract {
function testComputePair() public view returns (address) {
address token0 = 0x62f5C63FdB767aD1e891A872f39E3852c33132Ad; // GOLD
address token1 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH
address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; //
return address(
uint256(
keccak256(
abi.encodePacked(
hex"ff",
factory,
keccak256(abi.encodePacked(token0, token1)),
// pair合约bytecode的keccak256
hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
)
)
)
);
}
}

最终计算的地址是0xbAe08760DB8c07a5b28d473616E4E35Fd60D0842.在etherscan上查看,和预期的一直.说明我们的计算是正确的.

总结

create2这种可以在不创建合约的情况下就可以预先得到合约的地址的特性在Uniswap V2中运用得淋漓尽致。除此之外,create2还有更多其他的用法。包括通过CREATE2获得合约地址:解决交易所充值账号问题selfdestruct详解

其他的相关用法得到有机会再来和大家分享。

参考

  1. https://mirror.xyz/xyyme.eth/czipkrvqRwxHUjQey7zeEScfWDEVUYNajMAEo5e7Myw
  2. https://www.youtube.com/watch?v=ybEEhpDoY2s
  3. https://learnblockchain.cn/article/4249
  4. https://www.liaoxuefeng.com/article/1430588932227106
  5. https://eips.ethereum.org/EIPS/eip-1014
文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/09/24/create2-intro/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow