16.1 在合约中创建合约
16.1.1 create
create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数:
Contract x = new Contract{value: _value}(params)
其中Contract是要创建的合约名,x是合约对象(地址),如果构造函数是payable,可以创建时转入_value数量的ETH,params是新合约构造函数的参数。
例子:极简Uniswap
Uniswap V2核心合约中包括两个合约:
- UniswapV2Pair:币对合约,用于管理币对地址、流动性、买卖。
- UniswapV2Factory:工厂合约,用于创建新的币对合约,并管理币对地址。
本节通过create方法实现一个极简Uniswap:
- Pair合约管理币对地址。
- PairFactory工厂合约创建新的币对合约,并管理币对地址。
Pair合约:
contract Pair {
address public factory; // 工厂合约地址
address public token0; // token0合约地址
address public token1; // token1合约地址
// 初始化factory
constructor() payable {
factory = msg.sender;
}
// 初始化token0、token1
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, "Fobidden");
token0 = _token0;
token1 = _token1;
}
}
PairFactory合约:
contract PairFactory {
mapping (address => mapping (address => address)) public getPair; // 通过两个代币地址获取币对合约地址
mapping (address => mapping (address => bool)) public pairExist; // 通过两个代币地址判断币对合约是否存在
address[] public allPairs; // 保存所有币对合约地址
function createPair(address _token0, address _token1) external returns (address pairAddr) {
// 检查
require(pairExist[_token0][_token1] == false && pairExist[_token1][_token0] == false, "pair exist!");
// 创建币对合约
Pair pair = new Pair();
// 初始化token0、token1
pair.initialize(_token0, _token1);
pairAddr = address(pair);
// 更新map
getPair[_token0][_token1] = pairAddr;
getPair[_token1][_token0] = pairAddr;
pairExist[_token0][_token1] = true;
pairExist[_token1][_token0] = true;
// 更新数组
allPairs.push(pairAddr);
}
}
16.1.2 create2
CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。Uniswap创建Pair合约用的就是CREATE2而不是CREATE。
CREATE如何计算地址:
智能合约可以由其他合约和普通账户利用CREATE操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算:创建者的地址(通常为部署的钱包地址或者合约地址)和nonce(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1))的哈希。
新地址 = hash(创建者地址, nonce)
创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。
CREATE2如何计算地址:
CREATE2的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。用CREATE2创建的合约地址由4个部分决定:
- 0xFF:一个常数,避免和CREATE冲突
- 创建者地址
- salt(盐):一个创建者给定的数值
- 待部署合约的字节码(bytecode)
新地址 = hash("0xFF",创建者地址, salt, bytecode)
如果创建者使用 CREATE2 和提供的 salt 部署给定的合约bytecode,它将存储在新地址中,而新地址是可以提前计算的。
create2方法:Pair合约:
contract Pair {
address public factory;
address public token0;
address public token1;
constructor(address _token0, address _token1) payable {
factory = msg.sender;
token0 = _token0;
token1 = _token1;
}
}
create2方法:PairFactory合约:
contract PairFactory {
mapping (address => mapping (address => address)) public getPair;
address[] public allPairs;
function create2Pair(address _token0, address _token1) external returns (address pairAddr) {
// 要求两个代币不相同
require(_token0 != _token1, "IDENTICAL_ADDRESSES");
// token地址排序
(address tokenA, address tokenB) = _token0 < _token1 ? (_token0, _token1) : (_token1, _token0);
// 根据两个代币地址计算盐
bytes32 _salt = keccak256(abi.encodePacked(tokenA, tokenB));
// 创建币对合约
Pair pair = new Pair{salt:_salt}(tokenA, tokenB);
pairAddr = address(pair);
// 更新map
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
// 更新数组
allPairs.push(pairAddr);
}
function calculateAddr(address _token0, address _token1) external view returns (address) {
// 要求两个代币不相同
require(_token0 != _token1, "IDENTICAL_ADDRESSES");
// token地址排序
(address tokenA, address tokenB) = _token0 < _token1 ? (_token0, _token1) : (_token1, _token0);
// 根据两个代币地址计算盐
bytes32 _salt = keccak256(abi.encodePacked(tokenA, tokenB));
address pridictAddr = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
keccak256(abi.encodePacked(
type(Pair).creationCode,
abi.encode(tokenA,tokenB)
))
)))));
return pridictAddr;
}
}
需要注意的是,合约字节码需要包括参数tokenA和tokenB,并且参数需要使用abi.encode
进行编码:
abi.encode(tokenA,tokenB)
create2的优点:
- 可以在链下计算出已经创建的交易池的地址
- 其他合约不必通过接口来查询交易池的地址,可以节省 gas
- 合约地址不会因为reorg (区块重组、分叉) 而改变
- 如果一个合约自毁了,那么新合约未来可以再次部署到这个地址上
- 在未部署前可以提前获取合约地址
16.2 合约自毁
selfdestruct命令可以用来删除智能合约,并将该合约剩余ETH转到指定地址。selfdestruct是为了应对合约出错的极端情况而设计的。它最早被命名为suicide(自杀),但是这个词太敏感。为了保护抑郁的程序员,改名为selfdestruct。
selfdestruct使用起来非常简单:
selfdestruct(_addr);
其中_addr是接收合约中剩余ETH的地址。
示例:
contract Kill {
constructor() payable {}
function kill() external {
selfdestruct(payable(msg.sender));
}
function getBalance() external view returns (uint) {
return address(this).balance;
}
}
- 合约部署时,转入10ETH,调用
getBalance ()
可以获取到合约余额为10ETH,同时钱包地址余额减少了10ETH; - 调用合约的
kill()
方法,钱包余额增加了10ETH,同时再次调用getBalance ()
函数会报错:
{
"error": "Failed to decode output: Error: hex data is odd-length (argument=\"value\", value=\"0x0\", code=INVALID_ARGUMENT, version=bytes/5.7.0)"
}
注意:
- 对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符onlyOwner进行函数声明。
- 当合约被销毁后与智能合约的交互会报错。
- 当合约中有selfdestruct功能时常常会带来安全问题和信任问题,合约中的Selfdestruct功能会为攻击者打开攻击向量(例如使用selfdestruct向一个合约频繁转入token进行攻击,这将大大节省了GAS的费用,虽然很少人这么做),此外,此功能还会降低用户对合约的信心。
- selfdestruct 已被认为是废弃的(EIP-6049),编译器将在 Solidity 和 Yul 中警告其使用,包括内联程序集。目前没有替代方案,但强烈不建议使用,因为它最终将改变语义,并且所有使用它的合约将在某些方面受到影响。