Create2库本质就是对EVM opcode CREATE2进行的一个封装,可以让开发者在非内联汇编环境下直接使用该opcode。 CREATE2是一种可提前计算合约部署地址的合约部署opcode。而传统的合约部署是通过opcode CREATE完成的。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/utils/Create2.sol
Create2库本质就是对EVM opcode CREATE2
进行的一个封装,可以让开发者在非内联汇编环境下直接使用该opcode。
CREATE2
是一种可提前计算合约部署地址的合约部署opcode。而传统的合约部署是通过opcode CREATE
完成的,部署的合约地址由deployer地址与当时交易的nonce值共同决定。
封装Create2 library成为一个可调用合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/utils/Create2.sol";
contract MockCreate2 {
function deploy(
uint256 amount,
bytes32 salt,
bytes memory bytecode
) external returns (address) {
return Create2.deploy(amount, salt, bytecode);
}
function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address) {
return Create2.computeAddress(salt, bytecodeHash);
}
function computeAddress(
bytes32 salt,
bytes32 bytecodeHash,
address deployer
) external pure returns (address addr){
return Create2.computeAddress(salt, bytecodeHash, deployer);
}
receive() external payable {}
}
全部foundry测试合约:
底层调用CREATE2
来部署合约。传入参数分别为:
type(合约名).creationCode
来获得。 function deploy(
uint256 amount,
bytes32 salt,
bytes memory bytecode
) internal returns (address addr) {
// 如果待部署合约的constructor函数执行时需要传入eth,事先要求本Create2部署合约中的eth余额大于等于部署时传入的eth数量
require(address(this).balance >= amount, "Create2: insufficient balance");
// 要求部署合约传入的bytecode长度不为0
require(bytecode.length != 0, "Create2: bytecode length is zero");
/// 内联汇编中调用create2
assembly {
// create2的参数解释:create2(v, p, n, s)
// v:待部署合约执行constructor函数时被deployer传入的eth值(单位为wei)
// p:内存中指向待部署合约bytecode内容的指针
// n: 部署合约bytecode的字节长度
// s: salt值。注:s是一个256位的大端字节序的值
// 如果执行错误,create2返回0。例如:部署的新合约地址与已经存在的合约地址产生冲突
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
}
// 如果利用create2部署成功,则得到的addr不为0
require(addr != address(0), "Create2: Failed on deploy");
}
foundry代码验证
contract Create2Test is Test {
MockCreate2 mc = new MockCreate2();
function test_Deploy() external {
string memory name = "Michael.W";
uint age = 18;
// 1. deploy the contract with a non-payable constructor
bytes32 salt = keccak256("deploy the contract with a non-payable constructor");
// constructor params
bytes memory encodedConstructorParams = abi.encode(name, age);
// bytecode = creation code + constructor params
bytes memory bytecode = abi.encodePacked(type(ContractWithConstructor).creationCode, encodedConstructorParams);
// deploy
address newContractAddress = mc.deploy(0, salt, bytecode);
// check constructor params
assertEq(name, ContractWithConstructor(newContractAddress)._name());
assertEq(age, ContractWithConstructor(newContractAddress)._age());
// 2. deploy the contract with a payable constructor
vm.deal(address(mc), 3 gwei);
salt = keccak256("deploy the contract with a payable constructor");
bytecode = abi.encodePacked(type(ContractWithPayableConstructor).creationCode, encodedConstructorParams);
newContractAddress = mc.deploy(1 gwei, salt, bytecode);
assertEq(name, ContractWithConstructor(newContractAddress)._name());
assertEq(age, ContractWithConstructor(newContractAddress)._age());
// check eth balances
assertEq(3 gwei - 1 gwei, address(mc).balance);
assertEq(1 gwei, address(newContractAddress).balance);
// revert
// 1. revert when creates contract on the same address
vm.expectRevert("Create2: Failed on deploy");
mc.deploy(1 gwei, salt, bytecode);
// 2. revert with empty bytecode
vm.expectRevert("Create2: bytecode length is zero");
mc.deploy(0, salt, "");
// 3. revert with insufficient balance
vm.deal(address(mc), 1 gwei);
vm.expectRevert("Create2: insufficient balance");
mc.deploy(1 gwei + 1, salt, bytecode);
}
}
contract ContractWithConstructor{
string public _name;
uint public _age;
constructor(string memory name, uint age){
_name = name;
_age = age;
}
}
contract ContractWithPayableConstructor{
string public _name;
uint public _age;
constructor(string memory name, uint age) payable {
_name = name;
_age = age;
}
}
以上两个computeAddress方法都是在部署合约前计算部署后的合约地址的工具方法。唯一不同的是:
computeAddress(bytes32 salt, bytes32 bytecodeHash)
是利用本合约作为deployer来部署合约,而computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer)
是输入deployer地址从而计算出合约部署后的地址。
注:计算合约地址需要传入bytecodeHash,而非bytecode。bytecodeHash的计算方式为keccak256(合约bytecode + constructor参数)
// 利用部署合约bytecode的hash值与salt值,计算其利用本合约作为deployer部署该bytecode的合约地址
function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
// 调用computeAddress(bytes32, bytes32, address)方法
return computeAddress(salt, bytecodeHash, address(this));
}
// 利用部署合约bytecode的hash值/salt值和deployer地址,计算该deployer部署该bytecode的合约地址
function computeAddress(
bytes32 salt,
bytes32 bytecodeHash,
address deployer
) internal pure returns (address addr) {
/// @solidity memory-safe-assembly
assembly {
// 获取空闲内存指针ptr
let ptr := mload(0x40)
// ptr的第3个字开始(即ptr向后移动2个32字节的位置)存储bytecodeHash
mstore(add(ptr, 0x40), bytecodeHash)
// ptr的第2个字存储salt
mstore(add(ptr, 0x20), salt)
// ptr的第1个字存储deployer地址。由于1个字是32字节,deployer地址是20字节,那么第1个字的前32-20=12个字节还都是0,即ptr开始的前12个字节目前为0,还没有使用到
mstore(ptr, deployer)
// start指针指向ptr向右偏移11个字节
let start := add(ptr, 0x0b)
// 向ptr开始第11个字节里写入内容0xff
mstore8(start, 0xff)
// start
// 从ptr 开始的memoy地址: 0 ... 0xb | 0xc - 0x1f | 0x20 - 0x3f | 0x40 - 0x5f |
// 内存中内容: ... 0xff | deployer | salt | bytecodeHash |
// 对start开始后85个字节内容取keccak256,该值就是利用CREATE2部署后的合约地址
addr := keccak256(start, 85)
// 85字节内容为:0xff + deployer + salt + bytecodeHash
// 字节数: 1 + 20 + 32 + 32
}
}
foundry代码验证
contract Create2Test is Test {
MockCreate2 mc = new MockCreate2();
function test_ComputeAddress() external {
string memory name = "Michael.W";
uint age = 18;
// 1. deploy the contract with a non-payable constructor
bytes32 salt = keccak256("deploy the contract with a non-payable constructor");
bytes memory encodedConstructorParams = abi.encode(name, age);
bytes memory bytecode = abi.encodePacked(type(ContractWithConstructor).creationCode, encodedConstructorParams);
bytes32 bytecodeHash = keccak256(bytecode);
assertEq(mc.deploy(0, salt, bytecode), mc.computeAddress(salt, bytecodeHash));
// 2. deploy the contract with a payable constructor
vm.deal(address(mc), 3 gwei);
salt = keccak256("deploy the contract with a payable constructor");
bytecode = abi.encodePacked(type(ContractWithPayableConstructor).creationCode, encodedConstructorParams);
bytecodeHash = keccak256(bytecode);
assertEq(mc.deploy(2 gwei, salt, bytecode), mc.computeAddress(salt, bytecodeHash));
assertEq(address(mc).balance, 1 gwei);
}
function test_ComputeAddress_WithDeployer() external {
MockCreate2 mcOther = new MockCreate2();
string memory name = "Michael.W";
uint age = 18;
// 1. deploy the contract with a non-payable constructor
bytes32 salt = keccak256("deploy the contract with a non-payable constructor");
bytes memory encodedConstructorParams = abi.encode(name, age);
bytes memory bytecode = abi.encodePacked(type(ContractWithConstructor).creationCode, encodedConstructorParams);
bytes32 bytecodeHash = keccak256(bytecode);
assertEq(mcOther.deploy(0, salt, bytecode), mc.computeAddress(salt, bytecodeHash, address(mcOther)));
// 2. deploy the contract with a payable constructor
vm.deal(address(mcOther), 3 gwei);
salt = keccak256("deploy the contract with a payable constructor");
bytecode = abi.encodePacked(type(ContractWithPayableConstructor).creationCode, encodedConstructorParams);
bytecodeHash = keccak256(bytecode);
assertEq(mcOther.deploy(2 gwei, salt, bytecode), mc.computeAddress(salt, bytecodeHash, address(mcOther)));
assertEq(address(mcOther).balance, 1 gwei);
}
}
ps:\ 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!