【Solidity】创建合约

assembly create

create 方法通常在内联汇编中使用,用于动态创建、部署新合约,并获取新合约地址。

create 方法的语法:create(v, p, n)

  • v:要发送的以太数量(以 wei 为单位)
  • p:指向 Creation ByteCode 的内存指针
  • n:Creation ByteCode 的长度

注意事项:

  1. Solidity 中一般用 msg.value 获取以太数量,但在内联汇编中要用 callvalue()

  2. 在 Solidity 中,动态数组和字符串的前 32 字节用于存储长度信息。因此,实际的 Creation ByteCode 数据从第 33 字节开始。所以:

    1. 传入参数 p 时要跳过 32 byte,在汇编中可以通过 add(_creationCode, 0x20) 实现
    2. Creation ByteCode 的长度在汇编中可以用 mload(_creationCode) 获取
contract Demo {
    address public owner = msg.sender;
}

contract Helper {
    function getByteCode() public pure returns (bytes memory) {
    	// 获取 Demo 合约的 Creation ByteCode 并返回
        return type(Demo).creationCode;
    }
}

contract Proxy {
    function deployDemo(
        bytes memory _creationCode // 合约的 Creation ByteCode
    ) public payable returns (address addr) {
        assembly {
            // 创建、部署合约, 获取合约地址
            addr := create(callvalue(), add(_creationCode, 0x20), mload(_creationCode))
        }
        // 检查合约地址的有效性
        require(addr != address(0), "Failed to deploy contract");
    }
}
  1. 部署 Helper 合约、Proxy 合约

  2. 执行 Helper 合约的 getByteCode 方法,得到 Demo 合约的 Creation ByteCode

  3. 将 Demo 合约的 Creation ByteCode 作为参数传入 Proxy 合约的 deployDemo 方法并执行,创建、部署 Demo 合约并得到其地址

  4. 通过控制台获取到 Demo 合约地址;通过地址添加 Demo 合约到 Remix

  5. 检查 Demo 合约的 owner 是否为 Proxy 合约的地址


部署合约并传入参数

contract Demo {
    address public owner = msg.sender;
    uint public count;

    constructor(uint _count) {
        count = _count;
    }
}

contract Helper {
    function getByteCode(uint _count) public pure returns (bytes memory) {
        return abi.encodePacked(type(Demo).creationCode, abi.encode(_count));
        // 用 encode 编码传入的参数; 用 encodePacked 包装 Demo 合约的 Creation ByteCode 和编码后的参数
    }
}

contract Proxy {
    function deployDemo(
        bytes memory _creationCode
    ) public returns (address addr) {
        assembly {
            addr := create(callvalue(), add(_creationCode, 0x20), mload(_creationCode))
        }
        require(addr != address(0), "Failed to deploy contract");
    }
}
  1. 部署 Helper 合约、Proxy 合约

  2. 传入参数 _count 到 Helper 合约的 getByteCode 方法并执行,得到包装好的 Demo 合约的 Creation ByteCode 和编码后的参数

  3. 将步骤 2 得到的数据作为参数传入 Proxy 合约的 deployDemo 方法并执行,创建、部署 Demo 合约并得到其地址

  4. 通过控制台获取到 Demo 合约地址;通过地址添加 Demo 合约到 Remix

  5. 检查 Demo 合约的 owner 是否为 Proxy 合约的地址、count 是否为步骤 2 中传入的参数


部署合约并传输以太

contract Demo {
    address public owner = msg.sender;
    uint public value = msg.value;

    constructor() payable {} // 声明 payable 构造方法, 以支持在部署合约时传输以太
}

contract Helper {
    function getByteCode() public pure returns (bytes memory) {
        return type(Demo).creationCode;
    }
}

contract Proxy {
    function deployDemo(
        bytes memory _creationCode
    ) public payable returns (address addr) {
        // 使用 payable 修饰方法, 以支持在 public / external 方法中使用 callvalue() 获取以太数量
        assembly {
            addr := create(callvalue(), add(_creationCode, 0x20), mload(_creationCode))
        }
        require(addr != address(0), "Failed to deploy contract");
    }
}
  1. 部署 Helper 合约、Proxy 合约

  2. 执行 Helper 合约的 getByteCode 方法,得到 Demo 合约的 Creation ByteCode

  3. 设置以太数量、将 Demo 合约的 Creation ByteCode 作为参数传入 Proxy 合约的 deployDemo 方法并执行,创建、部署 Demo 合约并得到其地址

  4. 通过控制台获取到 Demo 合约地址;通过地址添加 Demo 合约到 Remix

  5. 检查 Demo 合约的 owner 是否为 Proxy 合约的地址、value 是否为步骤 3 中传输的以太数量



CREATE

在以太坊中,外部账户(EOA)和智能合约都可以创建新的智能合约。例如,Uniswap 使用工厂合约(PairFactory)创建多个币对合约(Pair)。

CREATE 的使用方法简单,只需用 new 关键字实例化一个合约,并传入构造函数所需的参数。如果构造函数是 payable 的,还可以在创建时发送以太币。

Contract a = new Contract{value: 100}(arg1, arg2);
// `value: 100` 表示 [当前合约] 向 a 合约发送的以太币下限为 100 wei

demo - 极简 Uniswap

Uniswap V2 的核心合约包含两个部分:

  1. UniswapV2Pair: 币对合约,管理币对地址、流动性和交易

  2. UniswapV2Factory: 工厂合约,创建新币对并管理币对地址

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

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

    // called once by the factory at time of deployment
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, "UniswapV2: FORBIDDEN"); // sufficient check
        token0 = _token0;
        token1 = _token1;
    }
}

Pair 的 initialize 函数会由工厂合约在部署完成后手动调用以初始化代币地址。

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

    function createPair(
        address tokenA,
        address tokenB
    ) external returns (address pairAddr) {
        // 创建新合约, msg.sender 为工厂合约地址
        Pair pair = new Pair();
        // 调用新合约的 initialize 方法, msg.sender 为工厂合约地址
        pair.initialize(tokenA, tokenB);
        // 更新状态变量
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

PairFactory 的状态变量 getPair 是两个代币地址到币对地址的 map,方便根据代币找到币对地址、allPairs 是币对地址的数组,存储了所有代币地址。PairFactory 的 createPair 函数会根据输入的两个代币地址 tokenA 和 tokenB 来创建新的 Pair 合约。

TIPS:1 个工厂合约 PairFactory 创建 Pair 合约的最大数量一般由 PairFactory 合约逻辑决定 ~



CREATE2

CREATE2 是 EVM 中的一个操作码,用于创建智能合约。与 CREATE 不同,CREATE2 允许开发者在部署合约之前预测其地址。

CREATE 计算合约地址的方法address = hash(sender, nonce)

  • sender 是创建合约的地址 (合约地址/钱包地址)

  • noncesender 的交易数

因为 nonce 是递增的,所以 CREATE 操作码创建的合约地址难以预测。

CREATE2 计算合约地址的方法address = hash(0xff + sender + salt + keccak256(bytecode))

  • 0xff 是一个常量,表示成 10 进制就是 255,用于区分 CREATE2 和 CREATE

  • sender 是创建合约的地址 (合约地址/钱包地址)

  • salt 是一个 bytes32 的随机值,由开发者提供,用于影响新创建的合约的地址

  • bytecode 是要部署的合约的创建字节码。

CREATE2 确保,如果创建者使用 CREATE2 和提供的 salt 部署给定的合约,它将存储在新地址 address 中。

CREATE2 的使用方法与 CREATE 类似, 只是多了一个 salt 参数:

Contract a = new Contract{value: 100, salt: 123}(arg1, arg2);

demo1 - 极简 Uniswap

contract Pair {
    // ... 与上面的 Pair 合约相同
}
contract PairFactory2 {
    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

    function createPair2(
        address tokenA,
        address tokenB
    ) external returns (address pairAddr) {
        require(tokenA != tokenB, "IDENTICAL_ADDRESSES"); // 避免 tokenA 和 tokenB 相同产生的冲突
        // 用 tokenA 和 tokenB 地址计算 salt
        (address token0, address token1) = tokenA < tokenB
            ? (tokenA, tokenB)
            : (tokenB, tokenA); // 将 tokenA 和 tokenB 按大小排序
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        // 用 CREATE2 部署新合约
        Pair pair = new Pair{salt: salt}();
        // 调用新合约的 initialize 方法
        pair.initialize(tokenA, tokenB);
        // 更新地址 map
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

可以事先计算 Pair 地址:

function calculateAddr(
    address tokenA,
    address tokenB
) public view returns (address predictedAddress) {
    require(tokenA != tokenB, "IDENTICAL_ADDRESSES"); // 避免 tokenA 和 tokenB 相同产生的冲突
    // 计算用 tokenA 和 tokenB 地址计算 salt
    (address token0, address token1) = tokenA < tokenB
        ? (tokenA, tokenB)
        : (tokenB, tokenA); // 将 tokenA 和 tokenB 按大小排序
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    // 计算合约地址方法
    predictedAddress = address(
        uint160(
            uint(
                keccak256(
                    abi.encodePacked(
                        bytes1(0xff),
                        address(this),
                        salt,
                        keccak256(type(Pair).creationCode)
                    )
                )
            )
        )
    );
}

demo2 - 使用 CREATE2 创建需要参数的合约

contract DeployWithCreate2 {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }
}

contract Create2Factory {
    event Deploy(address addr);

    // 使用 CREATE2 操作码创建、部署 DeployWithCreate2 合约
    function deploy(uint _salt) external {
        DeployWithCreate2 _contract = new DeployWithCreate2{
            salt: bytes32(_salt)
        }(msg.sender);
        emit Deploy(address(_contract));
    }

    function getByteCode(address _owner) public pure returns (bytes memory) {
        // 获取 DeployWithCreate2 合约的创建字节码
        bytes memory bytecode = type(DeployWithCreate2).creationCode;
        // 用 encode 编码传入的参数; 用 encodePacked 包装 DeployWithCreate2 合约的 Creation ByteCode 和编码后的参数
        return abi.encodePacked(bytecode, abi.encode(_owner));
    }

    // 计算合约地址
    function getAddress(
        bytes memory bytecode,
        uint _salt
    ) public view returns (address) {
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff),
                address(this),
                _salt,
                keccak256(bytecode)
            )
        );
        // 将 hash 转为 uint 再转为 uint160, 表示取最后 20 个字节, 因为以太坊地址的长度为 20 字节
        return address(uint160(uint(hash)));
    }
}
  1. 部署 Create2Factory 合约

  2. 将编辑器的地址作为参数传入 getByteCode 方法并调用,获取包装好的 DeployWithCreate2 合约的 Creation ByteCode 和编码后的参数

  3. 将步骤 2 获取到的数据和随机值作为参数传入 getAddress 方法并调用,计算合约地址;随机值这里以 123 为例

  4. 调用 deploy 方法,传入随机值 123,部署 DeployWithCreate2 合约

  5. 查看 DeployWithCreate2 合约地址,与计算的合约地址比对,两者应该相等



猜你喜欢

转载自blog.csdn.net/Superman_H/article/details/141402904