Solidity 学习 -练气期(十二)

合约中创建新的合约

以太网区块链上,外部账户(Externally Owned Account,EOA)可以创建智能合约,智能合约同样可以创建新的合约。去中心化交易所Uniswap 就是利用工厂合约(PairFactory)创建了无数个币对合约(Pair)。

基于CREATE 操作码生成合约

在以太网虚拟机(EVM)中,在合约中创建新合约设计两个操作码:
CREATECREATE2
基于CREATE操作码生成合约,在Solidity中只需要用new操作符创建一个新合约,并传入合约的构造函数所需要的参数:
Conrtract x = new Contract{value: _value}(params)
Contract是要创建的合约名称,x 合约对象(合约地址),params 新合约的构造函数所需要的参数类型和值,逗号分隔。如果构造函数声明了payable关键字,可以创建时传入_value数量的ETH

例子:一个简化版的Uniswap

Uniswap V2 是一个通过工厂合约的方式创建新合约的框架。框架核心时两个合约:
(1)UniswapV2Pair:币对合约,用于管理币对地址、流动性、具体交易流程。
(2)UniswapV2Factory:工厂合约,用于创建新的币对,并管理币对地址。
利用CREATE实现一个简化版的Uniswap,包含负责管理币对地址的Pair合约和负责创建新的币对、管理币对地址的PairFactory

  1. Pair 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Pair{

    address public factory;// 工厂合约地址
    address public token0;//代币1
    address public token1;//代币2

    constructor() payable {
        factory = msg.sender;
    }

    function initialize(address _token0,address _token1) external {
        require(msg.sender == factory,'UniswapV2: FORBIDDON');
        token0 = _token0;
        token1 = _token1;
    }
}

为什么Uniswap V2 不再构造函数中直接更新token0和token1?答案是Uniswap V2 基于CREATE2操作码创建合约,限制了构造函数不能有参数。而CREATE操作码生成,允许合约构造函数有参数,讲token0和token1 更新。我们还是用initialize函数进行更新,目的是比较容易迁移到CREATE2的操作。

  1. PairFactory 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "./Pair.sol";

contract PairFactory {
    mapping(address => mapping(address => address)) public  getPair;// 通过两个代币地址查询Pair地址
    address[] public allPairs; // 保存所有Pair地址

    function createPair(address tokenA, address tokenB) external returns(address pairAddr) {
        Pair pair = new Pair();
        pair.initialize(tokenA, tokenB);
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

部署好合约,看看创建币对的地址
(1)部署好PairFactory 合约后,使用两个部署好的合约地址,作为参数调用createPair函数。
每次生成的地址不一样
(2)将上衣步骤生成的地址填入“AtAddress”,找到创建Pair合约并部署,查看其中的变量
(3)为使用new的操作语句打上断电,让人年后打开调试器模式,调用createPair函数,在编译生成的EVM操作码,不难找到用于创建合约的CREATE操作码

CREATE2 操作码

CREATE2 时以太坊虚拟机EVM引入的一个新的操作码,是我们在部署以太网之前就能与测试合约的地址。Uniswap v2框架创建Pair合约就是基于CREATE2,取代了基于CREATE的操作码的方法。

  1. CREATE 操作码如何计算新合约地址
    智能合约可以由其他合约和普通账户利用CREATE操作码创建。
    在这两种情况下,新合约的地址都以相同的方式计算:
    获取创建者的地址_CreateorAddress(通常是钱包地址或者合约地址)和发送交易的总数nonce(对于合约账户,nonce是创建的合约总数,每创建一个合约nonce 加1)
    然后计算hash值,得到新的地址_Address.

_Address = hash(_CreatorAdress, nonce)

CREATE 操作码的问题是:创建者的地址_CreatorAddress不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址难以预测。

  1. CREATE2 操作码如何计算新合约地址
    CREATE2的目的是让合约地址独立于未来的事件。不管未来区块链上发生了什么,用户都可以把合约部署在事先计算好的地址上。用CREATE2创建的合约地址由4个参数决定:
    (1)0xFF:一个常数,目的是避免和CREATE创建的地址冲突
    (2)_CreatorAddress:调用CREATOR2的当前合约(创建合约)的地址。
    (3)salt:一个创建者指定的uint256类型的值,主要目的是用来影响新创建的合约地址
    (4)initcode:新合约的初始代码。
    CREATE2操作码汇总以上参数并计算哈希值,获得新合约的地址:

_Address = hash(0FF, _CreatorAddress, salt, initcode)

CREATE2确保了,如果创建者提供指定的salt和合约的initcode,通过CREATE2操作码将获得一个固定的地址_Address,不会因为后续区块链的变化而变化。

简化版的Uniswap2 ,升级上面的代码

  1. Pair合约,和上面一样
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Pair{

    address public factory;// 工厂合约地址
    address public token0;//代币1
    address public token1;//代币2

    constructor() payable {
        factory = msg.sender;
    }

    function initialize(address _token0,address _token1) external {
        require(msg.sender == factory,'UniswapV2: FORBIDDON');
        token0 = _token0;
        token1 = _token1;
    }
}
  1. PairFactory2 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "./Pair.sol";

contract PairFactory2{
    mapping(address => mapping(address => address)) public  getPair;// 通过两个代币地址查询Pair地址
    address[] public allPairs; // 保存所有Pair地址
    
    function createPair2(address tokenA, address tokenB) external returns(address pairAddr) {
        require(tokenA != tokenB, 'IDENTICAL_ADDRESS');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        // CREATE2 创建新合约
        Pair pair = new Pair{salt: salt}();
        // 调用新合约的initialize方法
        pair.initialize(tokenA, tokenB);
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

PairFactory2 合约有两个状态变量:getPair 是两个代币地址到币对地址的映射,方便根据代币找到币对地址;allPairs是币对地址的数组,存储了所有币对地址

PairFactory2 合约只有一个createPair2函数,基于CREATE2 操作码根据输入的两代币地址tokenA和tokenB 来创建新的Pair合约。其中创建合约的核心代码:
Pair pair = new Pair{salt:salt}();
与基于CREATE 操作码创建的方式相比,我们需要提供salt作为参数。在PairFactory2 合约中,我们利用tokenA和tokenB的值计算hash值,充当salt:
bytes32 salt = keccak256(abi.encodePacked(token0, token1));

  1. 实现计算Pair合约的地址

编写一个calculateAddr 函数来事先计算通过tokenA和tokenB生成的Pair的地址。通过它,可以验证我们计算的地址和实际地址是否相同。

function caculateAddr(address tokenA, address tokenB) public view 
        returns(address predictedAddress){
            require(tokenA != tokenB, 'IDENTICAL_ADDRESS');
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));  
            // 计算合约地址的方法 hash()
            predictedAddress = address(uint160(uint(
                keccak256(abi.encodePacked(
                    bytes1(0xff), address(this), salt,
                    keccak256(type(Pair).creationCode))
                ))
            ));          
        }

注意⚠️:如果要部署的合约构造函数中存在参数
Pair pair = new Pair{salt:salt}(address(this));
那么计算合约地址predictedAddress时,就应当把代入构造函数时的参数和Pair合约的bytecode一起打包,也就是改成

// 计算合约地址的方法 hash()
            predictedAddress = address(uint160(uint(
                keccak256(abi.encodePacked(
                    bytes1(0xff), address(this), salt,
                    keccak256(type(Pair).creationCode)),
                    abi.encode(address(this)) // ⚠️这里
                ))
            ));    

测试,验证下地址
tokenA: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
tokenB:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

先预测一下地址caculatedAddr:
address: predictedAddress 0xC74cB7d5B0803050533F19A040AA452732ff7E60


image.png

getPair:address: 0xC74cB7d5B0803050533F19A040AA452732ff7E60

验证结果一致。

CREATE2操作码实际场景

CREATE2 操作码出现,许多场景逐步替代CREATE操作码
(1)交易所为新用户创建并预留的钱包合约地址
(2)在UniswapV2,基于CREATE2操作码的工厂合约完成,这样做的好处是:
它可以得到一个确定的交易对地址,通过Router函数就可以由币对地址(tokenA,tokenB)计算出交易对地址,不再需要通过调用工厂合约函数Factory.getPari(tokenA,tokenB)获得交易对地址,省去了跨合约调用。

CREATE2让我们在部署合约前确定他的合约地址,这也是一些基于以太坊Layer2方案的合约项目的基础

删除合约

selfdestruct命令

用来删除智能合约,并将该合约中剩余的ETH转到指定地址。selfdestruct为了应对合约出错的极端情况而设计的。最早命名suicide,但是这个词过于敏感,后来改为selfdestruct

selfdestruct命令的格式:
selfdestruct(_addr);
_addr是目标合约或者钱包地址,用来接收合约中剩余的ETH。

例子 DeleteConstract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract DeleteContract {
    uint public value = 10;
    constructor() payable {}
    receive() external payable { }

    function getBalance() external view returns(uint balance){
        balance =address(this).balance;
    }

    function deleteConstract() external{
        selfdestruct(payable (msg.sender));
    }
}

测试:
(1)部署合约DeleteConstract之前设置转入金额1ETH,因为合约具有payagble 的构造函数
(2)部署后调用合约getBalance()函数返回余额是1ETH
(3)查看原来钱包地址中的余额,减少的金额略多于1ETH,因为转入合约时候只出了gas,从钱包扣除
(4)调用deleteContract()函数后,合约竟被删除,此时试图调用合约函数将会报错
(5)查看钱包地址中的余额,删除合约时,钱包回收了剩余的1ETH,但支出了额外的gas

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容