WTF Solidity极简入门: 25. CREATE2
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
CREATE2
CREATE2
操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。Uniswap
创建Pair
合约用的就是CREATE2
而不是CREATE
。这一讲,我将介绍CREATE2
的用法
CREATE如何计算地址
智能合约可以由其他合约和普通账户利用CREATE
操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算:创建者的地址(通常为部署的钱包地址或者合约地址)和nonce
(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1)的哈希。
新地址 = hash(创建者地址, nonce)
创建者地址不会变,但nonce
可能会随时间而改变,因此用CREATE
创建的合约地址不好预测。
CREATE2如何计算地址
CREATE2
的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。用CREATE2
创建的合约地址由4个部分决定:
0xFF
:一个常数,避免和CREATE
冲突CreatorAddress
: 调用 CREATE2 的当前合约(创建合约)地址。salt
(盐):一个创建者指定的bytes32
类型的值,它的主要目的是用来影响新创建的合约的地址。initcode
: 新合约的初始字节码(合约的Creation Code和构造函数的参数)。
新地址 = hash("0xFF",创建者地址, salt, initcode)
CREATE2
确保,如果创建者使用 CREATE2
和提供的 salt
部署给定的合约initcode
,它将存储在 新地址
中。
如何使用CREATE2
CREATE2
的用法和之前讲的CREATE
类似,同样是new
一个合约,并传入新合约构造函数所需的参数,只不过要多传一个salt
参数:
Contract x = new Contract{salt: _salt, value: _value}(params)
其中Contract
是要创建的合约名,x
是合约对象(地址),_salt
是指定的盐;如果构造函数是payable
,可以创建时转入_value
数量的ETH
,params
是新合约构造函数的参数。
极简Uniswap2
跟上一讲类似,我们用CREATE2
来实现极简Uniswap
。
Pair
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
合约很简单,包含3个状态变量:factory
,token0
和token1
。
构造函数constructor
在部署时将factory
赋值为工厂合约地址。initialize
函数会在Pair
合约创建的时候被工厂合约调用一次,将token0
和token1
更新为币对中两种代币的地址。
PairFactory2
contract PairFactory2{
mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
address[] public allPairs; // 保存所有Pair地址
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;
}
}
工厂合约(PairFactory2
)有两个状态变量getPair
是两个代币地址到币对地址的map
,方便根据代币找到币对地址;allPairs
是币对地址的数组,存储了所有币对地址。
PairFactory2
合约只有一个createPair2
函数,使用CREATE2
根据输入的两个代币地址tokenA
和tokenB
来创建新的Pair
合约。其中
Pair pair = new Pair{salt: salt}();
就是利用CREATE2
创建合约的代码,非常简单,而salt
为token1
和token2
的hash
:
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
事先计算Pair
地址
// 提前计算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));
// 计算合约地址方法 hash()
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(type(Pair).creationCode)
)))));
}
我们写了一个calculateAddr
函数来事先计算tokenA
和tokenB
将会生成的Pair
地址。通过它,我们可以验证我们事先计算的地址和实际地址是否相同。
大家可以部署好PairFactory2
合约,然后用下面两个地址作为参数调用createPair2
,看看创建的币对地址是什么,是否与事先计算的地址一样:
WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
如果部署合约构造函数中存在参数
例如当create2合约时:
Pair pair = new Pair{salt: salt}(address(this));
计算时,需要将参数和initcode一起进行打包:
keccak256(type(Pair).creationCode)=> keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
)))));
在remix上验证
首先用
WBNB
和PEOPLE
的地址哈希作为salt
来计算出Pair
合约的地址调用
PairFactory2.createPair2
传入参数为WBNB
和PEOPLE
的地址,获取出创建的pair
合约地址对比合约地址
create2的实际应用场景
交易所为新用户预留创建钱包合约地址。
由
CREATE2
驱动的factory
合约,在Uniswap V2
中交易对的创建是在Factory
中调用CREATE2
完成。这样做的好处是: 它可以得到一个确定的pair
地址, 使得Router
中就可以通过(tokenA, tokenB)
计算出pair
地址, 不再需要执行一次Factory.getPair(tokenA, tokenB)
的跨合约调用。
总结
这一讲,我们介绍了CREATE2
操作码的原理,使用方法,并用它完成了极简版的Uniswap
并提前计算币对合约地址。CREATE2
让我们可以在部署合约前确定它的合约地址,这也是一些layer2
项目的基础。