霍美予Mabel头像
关注

WTF-Solidity 第24讲:在 Solidity 合约中创建新合约(CREATE 操作码与极简 Uniswap 工厂模式)

WTF-Solidity 第24讲:在 Solidity 合约中创建新合约(CREATE 操作码与极简 Uniswap 工厂模式)

【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 【免费下载链接】WTF-Solidity 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

在以太坊上,不仅用户(外部账户 EOA)可以部署智能合约,合约自身同样可以创建新的合约——这正是去中心化交易所 Uniswap 借助工厂合约(PairFactory)批量创建币对合约(Pair)的底层能力。本讲基于 WTF-Solidity 教程第 24 讲,先讲透最基础的 create 方法语法与底层地址生成原理,再用一个可编译、可部署的极简 Uniswap 实例(完整源码见 24_Create/Create.sol)演示工厂模式的落地,最后给出在 Remix 上的完整验证步骤。学完本讲,你将掌握"合约创建合约"的核心写法,并能自行实现类似 Uniswap V2 的工厂-子合约架构。

为什么合约能创建合约

以太坊上的账户分为两类:

  1. 外部账户(EOA):由私钥控制,可以发起交易,也可以创建智能合约;
  2. 合约账户:由代码控制,同样拥有调用 create / create2 操作码的能力。

Uniswap V2 的核心合约正是利用了后者的能力:UniswapV2Factory(工厂合约)负责创建并管理所有 UniswapV2Pair(币对合约),后者则负责管理币对地址、流动性以及买卖逻辑。每上线一个代币交易对,工厂合约就在链上"生"出一个新的币对合约,这就是"合约创建合约"最典型的工业级应用。

Solidity 中创建新合约有两种方法:createcreate2。本讲聚焦 createcreate2 会在仓库 25_Create2/readme.md 中展开。

create 语法详解

create 的用法非常直接:用 new 关键字实例化一个合约,并传入新合约构造函数所需的参数:

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

其中各部分的含义如下:

语法元素说明
Contract要创建的合约名(需在编译单元内可见)
x新创建合约的对象引用,其地址可通过 address(x) 获取
{value: _value}可选。当新合约构造函数声明为 payable 时,可在创建的同时转入 _value 数量的 ETH
params新合约构造函数的入参

这条语句会在当前交易的上下文中触发一次新的合约部署,消耗一笔额外的 gas(由合约创建、存储读写和初始化代码执行产生)。

极简 Uniswap:用 create 实现工厂模式

参照 Uniswap V2 的核心架构,仓库 24_Create/Create.sol 用两个合约演示了完整的"工厂创建子合约"流程:

  • Pair:币对合约,负责管理币对地址;
  • PairFactory:工厂合约,负责创建新的币对并登记管理。

Pair 币对合约

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

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;
    }
}

对应源码见 24_Create/Create.sol#L4-L19Pair 合约本身很精简,只包含三个状态变量:

  • factory:部署它的工厂合约地址。构造函数中通过 msg.sender 直接捕获创建者——也就是 PairFactory 合约的地址;
  • token0 / token1:币对中的两种代币地址;
  • initialize:由工厂在部署完成后手动调用一次,用于设置代币地址。函数内用 require(msg.sender == factory, 'UniswapV2: FORBIDDEN') 做权限检查,确保只有当初部署它的工厂合约才能初始化,防止外部任意调用篡改币对。

提问:为什么 Uniswap 不在 constructor 中直接把 token0token1 地址设置好,而要额外提供一个 initialize 函数?

:因为 Uniswap 实际使用的是 create2 创建合约,生成的合约地址可以事先预测。而 create2 的地址计算依赖的是创建者地址、saltinitcode,与构造函数参数无关,因此需要把"设置币种参数"这一步骤拆到部署完成之后、由工厂显式调用的 initialize 中完成。详细原理参见仓库 25_Create2/readme.md 及其配套源码 25_Create2/Create2.sol

PairFactory 工厂合约

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();
        // 调用新合约的initialize方法
        pair.initialize(tokenA, tokenB);
        // 更新地址map
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

对应源码见 24_Create/Create.sol#L21-L36。工厂合约维护两个关键状态变量:

  • getPairtokenA -> tokenB -> pairAddr 的双层 mapping,支持双向查询(创建时同时写入 getPair[tokenA][tokenB]getPair[tokenB][tokenA]),方便根据任意一对代币快速定位币对地址;
  • allPairs:存储所有币对地址的数组,用于遍历全部币对。

核心逻辑集中在唯一的 createPair 函数中,它完成四件事:

  1. 创建新合约Pair pair = new Pair();——这就是"合约创建合约"的核心一行;
  2. 初始化币对:调用新 Pairinitialize(tokenA, tokenB),写入两种代币的地址;
  3. 登记地址:将 pairAddr 追加进 allPairs 数组;
  4. 建立索引:在 getPair 映射中双向登记,供后续按代币查币对。

create 地址生成的底层原理

create 创建的合约地址并不是随机的,而是由创建者信息决定的。依据仓库 25_Create2/readme.md 中的说明,通过 create 创建的新合约地址计算方式为:

新地址 = hash(创建者地址, nonce)

其中 nonce 对于外部账户是该地址发送交易的总数,对于合约账户则是其创建合约的总数(每创建一个合约 nonce 加一)。由于创建者地址固定不变、但 nonce 会随时间改变,因此用 create 创建的合约地址无法在部署前稳定预测——这正是 createcreate2 的核心差异,也是 Uniswap 选择 create2 的原因:它能把币对地址变成 (tokenA, tokenB) 的确定性函数,使 Router 无需跨合约调用 getPair 即可算出地址。关于 create20xFF + 创建者地址 + salt + initcode 四要素地址公式,可继续阅读 25_Create2/readme.md

在 Remix 上验证极简 Uniswap

动手验证前,先用下面两个地址作为 createPair 的入参(教程实测使用的示例地址):

WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c

第 1 步:调用 createPair 获取新币对地址

部署 PairFactory 合约后,在 Remix 的 DEPLOY & RUN TRANSACTIONS 面板中调用 createPair,分别传入 WBNBPEOPLE 的地址,返回的 decoded output 即为新创建的 Pair 合约地址(教程实测得到 0xD3e2008b4Da2cD6DEAF73471590fF30C86778A48):

Remix 调用 createPair 创建币对并返回 Pair 地址

第 2 步:用 At Address 加载 Pair 合约

将 Contract 下拉框切换为 Pair,在 At Address 输入框中粘贴上一步得到的 Pair 合约地址,点击按钮即可为已部署合约创建前端调用接口:

Remix 中通过 At Address 加载新创建的 Pair 合约

第 3 步:查看 Pair 合约的变量

在加载出的 Pair 合约实例上,可以依次查看 factory(应为 PairFactory 地址)、token0token1(应分别为传入的两种代币地址),从而确认工厂合约已正确创建并初始化了币对:

查看 Pair 合约的 factory、token0 与 token1 状态变量

第 4 步:Debug 查看 create 操作码

对刚才的 createPair 交易打开 DEBUGGER,在 EVM trace 中可以看到 0x02 CREATE 操作码——这正是新 Pair 合约通过 create 指令完成部署的直接证据:

通过 Debugger 在 EVM trace 中查看 CREATE 操作码

总结

本讲以极简 Uniswap 为例,完整演示了如何在合约中通过 create 创建新合约:Pair 币对合约负责记录工厂地址与两种代币地址,PairFactory 工厂合约则通过 new Pair() 批量部署币对,并用 mapping 与数组对币对地址做双向索引与管理。核心语法 Contract x = new Contract{value: _value}(params) 简单直接,但底层地址由 hash(创建者地址, nonce) 决定、无法事先预测,这也为下一讲 create2(可在部署前确定合约地址)埋下了伏笔——Uniswap 真实使用的正是 create2,读者可继续阅读 25_Create2/readme.md 深入了解。

【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 【免费下载链接】WTF-Solidity 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/gitblog_00342/article/details/157377254

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--