assembly create
create
方法通常在内联汇编中使用,用于动态创建、部署新合约,并获取新合约地址。
create
方法的语法:create(v, p, n)
v
:要发送的以太数量(以 wei 为单位)p
:指向 Creation ByteCode 的内存指针n
:Creation ByteCode 的长度
注意事项:
-
Solidity 中一般用
msg.value
获取以太数量,但在内联汇编中要用callvalue()
-
在 Solidity 中,动态数组和字符串的前 32 字节用于存储长度信息。因此,实际的 Creation ByteCode 数据从第 33 字节开始。所以:
- 传入参数
p
时要跳过 32 byte,在汇编中可以通过add(_creationCode, 0x20)
实现 - Creation ByteCode 的长度在汇编中可以用
mload(_creationCode)
获取
- 传入参数
contract Demo {
address public owner = msg.sender;
}
contract Helper {
function getByteCode() public pure returns (bytes memory) {
// 获取 Demo 合约的 Creation ByteCode 并返回
return type(Demo).creationCode;
}
}
contract Proxy {
function deployDemo(
bytes memory _creationCode // 合约的 Creation ByteCode
) public payable returns (address addr) {
assembly {
// 创建、部署合约, 获取合约地址
addr := create(callvalue(), add(_creationCode, 0x20), mload(_creationCode))
}
// 检查合约地址的有效性
require(addr != address(0), "Failed to deploy contract");
}
}
-
部署 Helper 合约、Proxy 合约
-
执行 Helper 合约的 getByteCode 方法,得到 Demo 合约的 Creation ByteCode
-
将 Demo 合约的 Creation ByteCode 作为参数传入 Proxy 合约的 deployDemo 方法并执行,创建、部署 Demo 合约并得到其地址
-
通过控制台获取到 Demo 合约地址;通过地址添加 Demo 合约到 Remix
-
检查 Demo 合约的 owner 是否为 Proxy 合约的地址
部署合约并传入参数:
contract Demo {
address public owner = msg.sender;
uint public count;
constructor(uint _count) {
count = _count;
}
}
contract Helper {
function getByteCode(uint _count) public pure returns (bytes memory) {
return abi.encodePacked(type(Demo).creationCode, abi.encode(_count));
// 用 encode 编码传入的参数; 用 encodePacked 包装 Demo 合约的 Creation ByteCode 和编码后的参数
}
}
contract Proxy {
function deployDemo(
bytes memory _creationCode
) public returns (address addr) {
assembly {
addr := create(callvalue(), add(_creationCode, 0x20), mload(_creationCode))
}
require(addr != address(0), "Failed to deploy contract");
}
}
-
部署 Helper 合约、Proxy 合约
-
传入参数 _count 到 Helper 合约的 getByteCode 方法并执行,得到包装好的 Demo 合约的 Creation ByteCode 和编码后的参数
-
将步骤 2 得到的数据作为参数传入 Proxy 合约的 deployDemo 方法并执行,创建、部署 Demo 合约并得到其地址
-
通过控制台获取到 Demo 合约地址;通过地址添加 Demo 合约到 Remix
-
检查 Demo 合约的 owner 是否为 Proxy 合约的地址、count 是否为步骤 2 中传入的参数
部署合约并传输以太:
contract Demo {
address public owner = msg.sender;
uint public value = msg.value;
constructor() payable {} // 声明 payable 构造方法, 以支持在部署合约时传输以太
}
contract Helper {
function getByteCode() public pure returns (bytes memory) {
return type(Demo).creationCode;
}
}
contract Proxy {
function deployDemo(
bytes memory _creationCode
) public payable returns (address addr) {
// 使用 payable 修饰方法, 以支持在 public / external 方法中使用 callvalue() 获取以太数量
assembly {
addr := create(callvalue(), add(_creationCode, 0x20), mload(_creationCode))
}
require(addr != address(0), "Failed to deploy contract");
}
}
-
部署 Helper 合约、Proxy 合约
-
执行 Helper 合约的 getByteCode 方法,得到 Demo 合约的 Creation ByteCode
-
设置以太数量、将 Demo 合约的 Creation ByteCode 作为参数传入 Proxy 合约的 deployDemo 方法并执行,创建、部署 Demo 合约并得到其地址
-
通过控制台获取到 Demo 合约地址;通过地址添加 Demo 合约到 Remix
-
检查 Demo 合约的 owner 是否为 Proxy 合约的地址、value 是否为步骤 3 中传输的以太数量
CREATE
在以太坊中,外部账户(EOA)和智能合约都可以创建新的智能合约。例如,Uniswap 使用工厂合约(PairFactory)创建多个币对合约(Pair)。
CREATE 的使用方法简单,只需用 new 关键字实例化一个合约,并传入构造函数所需的参数。如果构造函数是 payable 的,还可以在创建时发送以太币。
Contract a = new Contract{value: 100}(arg1, arg2);
// `value: 100` 表示 [当前合约] 向 a 合约发送的以太币下限为 100 wei
demo - 极简 Uniswap:
Uniswap V2 的核心合约包含两个部分:
-
UniswapV2Pair: 币对合约,管理币对地址、流动性和交易
-
UniswapV2Factory: 工厂合约,创建新币对并管理币对地址
contract Pair {
address public factory; // 工厂合约地址
address public token0; // 代币 1
address public token1; // 代币 2
constructor() payable {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, "UniswapV2: FORBIDDEN"); // sufficient check
token0 = _token0;
token1 = _token1;
}
}
Pair 的 initialize 函数会由工厂合约在部署完成后手动调用以初始化代币地址。
contract PairFactory {
mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查 Pair 地址
address[] public allPairs; // 保存所有 Pair 地址
function createPair(
address tokenA,
address tokenB
) external returns (address pairAddr) {
// 创建新合约, msg.sender 为工厂合约地址
Pair pair = new Pair();
// 调用新合约的 initialize 方法, msg.sender 为工厂合约地址
pair.initialize(tokenA, tokenB);
// 更新状态变量
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
PairFactory 的状态变量 getPair 是两个代币地址到币对地址的 map,方便根据代币找到币对地址、allPairs 是币对地址的数组,存储了所有代币地址。PairFactory 的 createPair 函数会根据输入的两个代币地址 tokenA 和 tokenB 来创建新的 Pair 合约。
TIPS:1 个工厂合约 PairFactory 创建 Pair 合约的最大数量一般由 PairFactory 合约逻辑决定 ~
CREATE2
CREATE2 是 EVM 中的一个操作码,用于创建智能合约。与 CREATE 不同,CREATE2 允许开发者在部署合约之前预测其地址。
CREATE 计算合约地址的方法:address = hash(sender, nonce)
-
sender
是创建合约的地址 (合约地址/钱包地址) -
nonce
是sender
的交易数
因为 nonce 是递增的,所以 CREATE 操作码创建的合约地址难以预测。
CREATE2 计算合约地址的方法:address = hash(0xff + sender + salt + keccak256(bytecode))
-
0xff
是一个常量,表示成 10 进制就是 255,用于区分 CREATE2 和 CREATE -
sender
是创建合约的地址 (合约地址/钱包地址) -
salt
是一个 bytes32 的随机值,由开发者提供,用于影响新创建的合约的地址 -
bytecode
是要部署的合约的创建字节码。
CREATE2 确保,如果创建者使用 CREATE2 和提供的 salt
部署给定的合约,它将存储在新地址 address
中。
CREATE2 的使用方法与 CREATE 类似, 只是多了一个 salt 参数:
Contract a = new Contract{value: 100, salt: 123}(arg1, arg2);
demo1 - 极简 Uniswap:
contract Pair {
// ... 与上面的 Pair 合约相同
}
contract PairFactory2 {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
function createPair2(
address tokenA,
address tokenB
) external returns (address pairAddr) {
require(tokenA != tokenB, "IDENTICAL_ADDRESSES"); // 避免 tokenA 和 tokenB 相同产生的冲突
// 用 tokenA 和 tokenB 地址计算 salt
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA); // 将 tokenA 和 tokenB 按大小排序
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 用 CREATE2 部署新合约
Pair pair = new Pair{salt: salt}();
// 调用新合约的 initialize 方法
pair.initialize(tokenA, tokenB);
// 更新地址 map
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
可以事先计算 Pair 地址:
function calculateAddr(
address tokenA,
address tokenB
) public view returns (address predictedAddress) {
require(tokenA != tokenB, "IDENTICAL_ADDRESSES"); // 避免 tokenA 和 tokenB 相同产生的冲突
// 计算用 tokenA 和 tokenB 地址计算 salt
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA); // 将 tokenA 和 tokenB 按大小排序
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 计算合约地址方法
predictedAddress = address(
uint160(
uint(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(type(Pair).creationCode)
)
)
)
)
);
}
demo2 - 使用 CREATE2 创建需要参数的合约:
contract DeployWithCreate2 {
address public owner;
constructor(address _owner) {
owner = _owner;
}
}
contract Create2Factory {
event Deploy(address addr);
// 使用 CREATE2 操作码创建、部署 DeployWithCreate2 合约
function deploy(uint _salt) external {
DeployWithCreate2 _contract = new DeployWithCreate2{
salt: bytes32(_salt)
}(msg.sender);
emit Deploy(address(_contract));
}
function getByteCode(address _owner) public pure returns (bytes memory) {
// 获取 DeployWithCreate2 合约的创建字节码
bytes memory bytecode = type(DeployWithCreate2).creationCode;
// 用 encode 编码传入的参数; 用 encodePacked 包装 DeployWithCreate2 合约的 Creation ByteCode 和编码后的参数
return abi.encodePacked(bytecode, abi.encode(_owner));
}
// 计算合约地址
function getAddress(
bytes memory bytecode,
uint _salt
) public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
keccak256(bytecode)
)
);
// 将 hash 转为 uint 再转为 uint160, 表示取最后 20 个字节, 因为以太坊地址的长度为 20 字节
return address(uint160(uint(hash)));
}
}
-
部署 Create2Factory 合约
-
将编辑器的地址作为参数传入 getByteCode 方法并调用,获取包装好的 DeployWithCreate2 合约的 Creation ByteCode 和编码后的参数
-
将步骤 2 获取到的数据和随机值作为参数传入 getAddress 方法并调用,计算合约地址;随机值这里以 123 为例
-
调用 deploy 方法,传入随机值 123,部署 DeployWithCreate2 合约
-
查看 DeployWithCreate2 合约地址,与计算的合约地址比对,两者应该相等