0xAA

Posted on May 15, 2022Read on Mirror.xyz

Solidity极简入门: 25. Create2

我最近在重新学solidity,巩固一下细节,也写一个“Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。

欢迎关注我的推特:@0xAA_Science

WTF技术社群discord,内有加微信群方法:链接

所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): github.com/AmazingAng/WTFSolidity


CREATE2

CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。Uniswap创建Pair合约用的就是CREATE2。这一讲,我将介绍CREATE2的用法

CREATE如何计算地址

智能合约可以由其他合约和普通账户利用CREATE操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算:创建者的地址和nonce(该地址发送交易的总数)的哈希。

新地址 = hash(创建者地址, nonce)

创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。

CREATE2如何计算地址

CREATE2的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。用CREATE2创建的合约地址由4个部分决定:

  • 0xFF:一个常数,避免和CREATE冲突
  • 创建者地址
  • salt(盐):一个创建者给定的数值
  • 待部署合约的字节码(bytecode
新地址 = hash("0xFF",创建者地址, salt, bytecode)

CREATE2 确保,如果创建者使用 CREATE2 和提供的 salt 部署给定的合约bytecode,它将存储在 新地址 中。

如何使用CREATE2

CREATE2的用法和之前讲的Create类似,同样是new一个合约,并传入新合约构造函数所需的参数,只不过要多传一个salt参数:

Contract x = new Contract{salt: _salt, value: _value}(params)

其中Contract是要创建的合约名,x是合约对象(地址),_salt是指定的盐;如果构造函数是payable,可以创建时转入_value数量的ETHparams是新合约构造函数的参数。

极简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个状态变量:factorytoken0token1

构造函数constructor在部署时将factory赋值为工厂合约地址。initialize函数会在Pair合约创建的时候被工厂合约调用一次,将token0token1更新为币对中两种代币的地址。

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) {
            // 计算用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根据输入的两个代币地址tokenAtokenB来创建新的Pair合约。其中

    Pair pair = new Pair{salt: salt}(); 

就是利用CREATE2创建合约的代码,非常简单,而salttoken1token2hash

            bytes32 salt = keccak256(abi.encodePacked(token0, token1));

事先计算Pair地址

        // 提前计算pair合约地址
        function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){
            // 计算用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函数来事先计算tokenAtokenB将会生成的Pair地址。通过它,我们可以验证我们事先计算的地址和实际地址是否相同。

大家可以部署好PairFactory2合约,然后用下面两个地址作为参数调用createPair2,看看创建的币对地址是什么,是否与事先计算的地址一样:

WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址:
0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c

总结

这一讲,我们介绍了CREATE2操作码的原理,使用方法,并用它完成了极简版的Uniswap并提前计算币对合约地址。CREATE2让我们可以在部署合约前确定它的合约地址,这也反事实系统和很多layer2的基础。