Uniswap V2 第3章 创建交易对:Factory 与 CREATE2

讲解 Uniswap V2 Factory 如何用 createPair 创建交易对、为何用 CREATE2 让池子地址可预测,以及每对代币唯一池子的保证。

4 分钟阅读
Uniswap V2 第3章 创建交易对:Factory 与 CREATE2

Uniswap V2 第 3 章:创建交易对(Create Pool)

这一章讲 Factory:它如何为任意两种 ERC20 创建一个 Pair,为什么用 CREATE2 部署(让地址可以提前算出来),以及”每对代币只有一个池子”是怎么保证的。


目录


1. Factory 的职责

UniswapV2Factory 是一个”池子注册表 + 部署器”:

  • 创建createPair(tokenA, tokenB) 部署一个新的 UniswapV2Pair 合约。
  • 登记:把新池子记进映射 getPair[tokenA][tokenB],方便以后查询。
  • 枚举:维护一个 allPairs 数组,记录所有创建过的池子。

主网 Factory 地址:0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f


2. createPair 的流程

function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
    // ① 排序,保证 token0 < token1
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'ZERO_ADDRESS');
    // ② 保证唯一性:该对还没有池子
    require(getPair[token0][token1] == address(0), 'PAIR_EXISTS');
    // ③ 用 CREATE2 部署 Pair,salt = keccak256(token0, token1)
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) }
    // ④ 初始化 Pair(告诉它管理哪两种代币)
    IUniswapV2Pair(pair).initialize(token0, token1);
    // ⑤ 双向登记 + 入数组
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair;
    allPairs.push(pair);
}

五步:排序 → 查重 → CREATE2 部署 → initialize → 登记。


3. 为什么先排序 token0 / token1

createPair(A, B)createPair(B, A) 应该指向同一个池子。如果不排序,两种顺序会算出两个不同的池子,造成流动性分裂。

解决办法:始终按地址大小排序,令 token0 < token1。这样无论用户以什么顺序传入,内部都归一化成同一对 (token0, token1),salt 也一致,池子地址也就一致。


4. CREATE2:可预测的池子地址

普通的 CREATE 部署,合约地址取决于部署者地址和 nonce,事先无法确定。而 CREATE2 让地址只取决于:

地址 = f(部署者, salt, 合约创建字节码)

Uniswap 用 salt = keccak256(token0, token1)。因为这三项在部署前都已知,所以任何人都能在池子还没创建时就算出它将来的地址

好处:

  • Periphery / 集成合约可以直接计算 Pair 地址,不必每次去 Factory 查映射(省一次外部调用的 gas)。
  • 这就是 UniswapV2Library.pairFor(factory, tokenA, tokenB) 做的事。

5. 案例:提前算出 Pair 地址

pairFor 的计算(伪代码):

token0, token1 = sort(tokenA, tokenB)
salt = keccak256(token0, token1)
pair = address(keccak256(
    0xff,
    factory,                 // 部署者
    salt,
    INIT_CODE_HASH           // Pair 创建字节码的 keccak256(一个固定常量)
)[12:])                       // 取后 20 字节

INIT_CODE_HASH 是一个写死的常量(Pair 字节码的哈希)。只要知道 factory 和两种代币地址,就能纯链下/链上算出池子地址,无需任何外部调用。

注意:算出的地址在池子真正创建前只是”未来地址”,此时还没有代码。要先确认池子已存在(或先 createPair)才能调用它。


6. 每对代币唯一池子的保证

createPair 里的这行:

require(getPair[token0][token1] == address(0), 'PAIR_EXISTS');

保证同一对代币只能创建一次。这很重要:

  • 如果允许多个 DAI/WETH 池子,流动性会被分散,每个池子都更浅、滑点更大。
  • 唯一池子让所有流动性汇聚,深度最大、滑点最小。

(这正是 Uniswap V3 的一大不同——V3 同一对代币按”费率档位”可以有多个池子。这是后话。)


7. 本章小结

  1. Factory 负责创建、登记、枚举所有 Pair;主网地址 0x5C69...aA6f
  2. createPair 五步:排序 → 查重(PAIR_EXISTS)→ CREATE2 部署 → initialize → 双向登记。
  3. 排序 token0<token1(A,B)(B,A) 归一化为同一池子,避免流动性分裂。
  4. CREATE2 + salt=keccak256(token0,token1) 让池子地址可提前计算pairFor/INIT_CODE_HASH),省去查映射。
  5. require(PAIR_EXISTS) 保证每对代币唯一池子,集中流动性、降低滑点。

8. 动手练习

对应课程的 Factory 练习:用 Factory 查/建池子。

练习:getPair 与 createPair

主网分叉,Factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f

interface IUniswapV2Factory {
    function getPair(address a, address b) external view returns (address);
    function createPair(address a, address b) external returns (address);
    function allPairsLength() external view returns (uint256);
}

思路:

  1. 查已有池:调 getPair(DAI, WETH),打印地址(应是非零的真实池子)。验证 getPair(WETH, DAI) 返回同一地址(印证排序归一化)。
  2. 建新池:找两个还没池子的代币(或部署两个 MockERC20),调 createPair,打印返回地址。
  3. 再调一次 getPair 确认登记成功;allPairsLength() 应 +1。
  4. 重复创建应失败:再次对同一对调用 createPair,断言 revert(“PAIR_EXISTS”)。
  5. (进阶)自己实现 pairFor(用第 5 节公式 + INIT_CODE_HASH),把算出的地址和 getPair 返回值对比,验证 CREATE2 地址可预测。

运行

forge test --evm-version cancun --fork-url $FORK_URL \
  --match-path test/UniswapV2Factory.t.sol -vvv

下一章(第 4 章 Add Liquidity)讲:提供流动性怎么铸 LP token、首次铸造为什么用几何平均数 √(x·y)、以及最小流动性锁定。

💬 评论