部署智能合约是开发中必不可少的一个环节,常规的方式是借助像Hardhat这样的工具,通过编写ts部署脚本来实现。但在实际业务中,经常会遇到通过在合约中部署子合约的情况。比如添加Token流动池。这类需求在设计在上需要通过工厂合约来创建部署子合约来实现,它看起来就像是从一个模具厂生产的模具一样,只是每个模具的编号(子合约地址)不同。子合约的业务逻辑不是本次介绍的重点,我们主要关注在合约中部署合约的两种方式:

一、create部署

先确认子合约的内容,一个构造函数和简单的函数:

contract Son {address token1;address token2;constructor(address _token1,address _token2) {token1 = _token1;token2 = _token2;}function addLiquidity() external pure returns(uint256){return 1;}}

create的部署方式是通过new关键字来实现,所部署合约的地址是通过哈希计算得来:

  1. 部署地址 —msg.sender
  2. 之前在该地址部署的交易数  — nonce

keccak256(rlp.encode(deployingAddress, nonce))

nonce每次获取的不一样,因此每次部署的合约地址不同。在solidity最新的版本中,只需通过内置的关键字new即可:

X x = new X()

//新版本的create部署方式//0x4D1435CA4761C96ea6156AFf63A9F0D8D00D10c1//0xFF8C88bA69cbfba47BfC1ff84aB2cfA6a64B8429function create(address token1,address token2) external returns(address){Son son = new Son(token1,token2);return address(son);}

remix运行结果:

合约地址:0x5C9eb5D6a6C2c1B3EFc52255C0b356f116f6f66D,我们用相同的参数token1、token2,再次部署的结果:

合约地址:0xb8f43EC36718ecCb339B75B727736ba14F174d77,可以看到与第一次的地址不同,create部署的好处是每次的部署地址不同,不用担心合约地址被占用导致部署失败的问题,缺点是无法通过计算得到合约地址,不适合一些需要提前知道合约地址的场景。

二、create2部署

合约地址计算的方式:

  1. 部署地址(msg.sender)
  2. 部署合约代码字节码的哈希(bytecode)
  3. 创建者提供的随机的salt(32 字节字符串).

keccak256(0xff ++ deployingAddr ++ salt ++ keccak256(bytecode))

相较于create,多了参数salt。

//新版本的create2部署方式function create2(address token1,address token2) external returns(address){bytes32 salt = keccak256(abi.encodePacked(token1, token2));//盐值,可以是一个数字、一个字符串等,一般是随机数Son son = new Son{salt: salt}(token1,token2);return address(son);}

salt的获取规则是计算token1和token2的hash,也可以根据实际的业务进行调整。部署代码:

X x = new X{salt: salt}()

token1和token2仍然使用create中的参数值,remix运行结果:

合约地址为:0x8D0Cd60156182DF2263a41960c250Bd921047Bc3,与create部署的合约地址不同,因为底层的计算方式不同。现在我们用相同的参数再次部署:

已经报错,因为相同的salt计算的合约地址是相同的,而合约地址必须未被使用过。

我们用旧版本的create2来试试看

//老版本的create2部署方式,通过solidity汇编语法实现function deployAssembly(address token1,address token2) external returns(address addr){bytes memory bytecode = getBytecode(token1, token2);bytes32 salt = keccak256(abi.encodePacked(token1, token2));//盐值,可以是一个数字、一个字符串等,一般是随机数assembly {//param1:发送给新合约的wei数(msg.value)//param2:存储位置(从32开始)和需要的长度(bytecode)//param3:存储在memory中//param4:随机值addr := create2(0,add(bytecode, 32),mload(bytecode),salt)if iszero(extcodesize(addr)) {revert(0, 0)}}}function getBytecode(address token1,address token2) internal pure returns(bytes memory bytecode){bytecode = type(Son).creationCode;//获取合约字节码,也可通过编译合约获取bytecode = abi.encodePacked(bytecode, abi.encode(token1, token2));}

需要提前通过creationCode()函数获取部署子合约的bytecode。部署结果:

可以看到结果仍然是失败,因为与上述计算的合约地址相同,只是实现方式不同。我们通过计算合约地址的函数来验证是否与部署成功的地址一致:

可以看到计算结果是:0x8D0Cd60156182DF2263a41960c250Bd921047Bc3,与create2函数返回的合约地址相同。

相较于create,create2提供了更多的灵活性,使得开发者可以预先计算合约地址,从而更好地管理合约地址的分配。同时,create2还可以用于实现一些更高级的功能,例如合约工厂、二级合约等。

需要注意的是,使用create2创建合约时,由于需要提供一个预计地址,因此需要确保该地址没有被使用过,否则可能会导致创建失败。此外,使用create2创建合约时,需要确保预计地址的计算规则是确定的,这样可以确保预计地址与实际地址的一致性。