Uniswap V2 第 3 章:创建交易对(Create Pool)
这一章讲 Factory:它如何为任意两种 ERC20 创建一个 Pair,为什么用 CREATE2 部署(让地址可以提前算出来),以及”每对代币只有一个池子”是怎么保证的。
目录
- 1. Factory 的职责
- 2. createPair 的流程
- 3. 为什么先排序 token0 / token1
- 4. CREATE2:可预测的池子地址
- 5. 案例:提前算出 Pair 地址
- 6. 每对代币唯一池子的保证
- 7. 本章小结
- 8. 动手练习
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. 本章小结
- Factory 负责创建、登记、枚举所有 Pair;主网地址
0x5C69...aA6f。 createPair五步:排序 → 查重(PAIR_EXISTS)→ CREATE2 部署 → initialize → 双向登记。- 排序 token0<token1 让
(A,B)和(B,A)归一化为同一池子,避免流动性分裂。 - CREATE2 + salt=keccak256(token0,token1) 让池子地址可提前计算(
pairFor/INIT_CODE_HASH),省去查映射。 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);
}
思路:
- 查已有池:调
getPair(DAI, WETH),打印地址(应是非零的真实池子)。验证getPair(WETH, DAI)返回同一地址(印证排序归一化)。 - 建新池:找两个还没池子的代币(或部署两个 MockERC20),调
createPair,打印返回地址。 - 再调一次
getPair确认登记成功;allPairsLength()应 +1。 - 重复创建应失败:再次对同一对调用
createPair,断言 revert(“PAIR_EXISTS”)。 - (进阶)自己实现
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)、以及最小流动性锁定。