16. Solidity:在合约中创建合约、合约自毁

16.1 在合约中创建合约

16.1.1 create

create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数:

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

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

例子:极简Uniswap
Uniswap V2核心合约中包括两个合约:

  1. UniswapV2Pair:币对合约,用于管理币对地址、流动性、买卖。
  2. UniswapV2Factory:工厂合约,用于创建新的币对合约,并管理币对地址。

本节通过create方法实现一个极简Uniswap:

  1. Pair合约管理币对地址。
  2. 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;
    }
}
  1. 合约部署时,转入10ETH,调用getBalance ()可以获取到合约余额为10ETH,同时钱包地址余额减少了10ETH;
  2. 调用合约的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)"
}

注意:

  1. 对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符onlyOwner进行函数声明。
  2. 当合约被销毁后与智能合约的交互会报错。
  3. 当合约中有selfdestruct功能时常常会带来安全问题和信任问题,合约中的Selfdestruct功能会为攻击者打开攻击向量(例如使用selfdestruct向一个合约频繁转入token进行攻击,这将大大节省了GAS的费用,虽然很少人这么做),此外,此功能还会降低用户对合约的信心。
  4. selfdestruct 已被认为是废弃的(EIP-6049),编译器将在 Solidity 和 Yul 中警告其使用,包括内联程序集。目前没有替代方案,但强烈不建议使用,因为它最终将改变语义,并且所有使用它的合约将在某些方面受到影响。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容