16.1 在合約中創(chuàng)建合約
16.1.1 create
create的用法很簡單,就是new一個合約,并傳入新合約構(gòu)造函數(shù)所需的參數(shù):
Contract x = new Contract{value: _value}(params)
其中Contract是要創(chuàng)建的合約名,x是合約對象(地址),如果構(gòu)造函數(shù)是payable,可以創(chuàng)建時轉(zhuǎn)入_value數(shù)量的ETH,params是新合約構(gòu)造函數(shù)的參數(shù)。
例子:極簡Uniswap
Uniswap V2核心合約中包括兩個合約:
- UniswapV2Pair:幣對合約,用于管理幣對地址、流動性、買賣。
- UniswapV2Factory:工廠合約,用于創(chuàng)建新的幣對合約,并管理幣對地址。
本節(jié)通過create方法實現(xiàn)一個極簡Uniswap:
- Pair合約管理幣對地址。
- PairFactory工廠合約創(chuàng)建新的幣對合約,并管理幣對地址。
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!");
// 創(chuàng)建幣對合約
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;
// 更新數(shù)組
allPairs.push(pairAddr);
}
}
16.1.2 create2
CREATE2 操作碼使我們在智能合約部署在以太坊網(wǎng)絡(luò)之前就能預(yù)測合約的地址。Uniswap創(chuàng)建Pair合約用的就是CREATE2而不是CREATE。
CREATE如何計算地址:
智能合約可以由其他合約和普通賬戶利用CREATE操作碼創(chuàng)建。 在這兩種情況下,新合約的地址都以相同的方式計算:創(chuàng)建者的地址(通常為部署的錢包地址或者合約地址)和nonce(該地址發(fā)送交易的總數(shù),對于合約賬戶是創(chuàng)建的合約總數(shù),每創(chuàng)建一個合約nonce+1))的哈希。
新地址 = hash(創(chuàng)建者地址, nonce)
創(chuàng)建者地址不會變,但nonce可能會隨時間而改變,因此用CREATE創(chuàng)建的合約地址不好預(yù)測。
CREATE2如何計算地址:
CREATE2的目的是為了讓合約地址獨立于未來的事件。不管未來區(qū)塊鏈上發(fā)生了什么,你都可以把合約部署在事先計算好的地址上。用CREATE2創(chuàng)建的合約地址由4個部分決定:
- 0xFF:一個常數(shù),避免和CREATE沖突
- 創(chuàng)建者地址
- salt(鹽):一個創(chuàng)建者給定的數(shù)值
- 待部署合約的字節(jié)碼(bytecode)
新地址 = hash("0xFF",創(chuàng)建者地址, salt, bytecode)
如果創(chuàng)建者使用 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);
// 根據(jù)兩個代幣地址計算鹽
bytes32 _salt = keccak256(abi.encodePacked(tokenA, tokenB));
// 創(chuàng)建幣對合約
Pair pair = new Pair{salt:_salt}(tokenA, tokenB);
pairAddr = address(pair);
// 更新map
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
// 更新數(shù)組
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);
// 根據(jù)兩個代幣地址計算鹽
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;
}
}
需要注意的是,合約字節(jié)碼需要包括參數(shù)tokenA和tokenB,并且參數(shù)需要使用abi.encode進(jìn)行編碼:
abi.encode(tokenA,tokenB)
create2的優(yōu)點:
- 可以在鏈下計算出已經(jīng)創(chuàng)建的交易池的地址
- 其他合約不必通過接口來查詢交易池的地址,可以節(jié)省 gas
- 合約地址不會因為reorg (區(qū)塊重組、分叉) 而改變
- 如果一個合約自毀了,那么新合約未來可以再次部署到這個地址上
- 在未部署前可以提前獲取合約地址
16.2 合約自毀
selfdestruct命令可以用來刪除智能合約,并將該合約剩余ETH轉(zhuǎn)到指定地址。selfdestruct是為了應(yīng)對合約出錯的極端情況而設(shè)計的。它最早被命名為suicide(自殺),但是這個詞太敏感。為了保護(hù)抑郁的程序員,改名為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;
}
}
- 合約部署時,轉(zhuǎn)入10ETH,調(diào)用
getBalance ()可以獲取到合約余額為10ETH,同時錢包地址余額減少了10ETH; - 調(diào)用合約的
kill()方法,錢包余額增加了10ETH,同時再次調(diào)用getBalance ()函數(shù)會報錯:
{
"error": "Failed to decode output: Error: hex data is odd-length (argument=\"value\", value=\"0x0\", code=INVALID_ARGUMENT, version=bytes/5.7.0)"
}
注意:
- 對外提供合約銷毀接口時,最好設(shè)置為只有合約所有者可以調(diào)用,可以使用函數(shù)修飾符onlyOwner進(jìn)行函數(shù)聲明。
- 當(dāng)合約被銷毀后與智能合約的交互會報錯。
- 當(dāng)合約中有selfdestruct功能時常常會帶來安全問題和信任問題,合約中的Selfdestruct功能會為攻擊者打開攻擊向量(例如使用selfdestruct向一個合約頻繁轉(zhuǎn)入token進(jìn)行攻擊,這將大大節(jié)省了GAS的費用,雖然很少人這么做),此外,此功能還會降低用戶對合約的信心。
- selfdestruct 已被認(rèn)為是廢棄的(EIP-6049),編譯器將在 Solidity 和 Yul 中警告其使用,包括內(nèi)聯(lián)程序集。目前沒有替代方案,但強烈不建議使用,因為它最終將改變語義,并且所有使用它的合約將在某些方面受到影響。