Skip to content
On this page

L2-21: Contract Factory(合约工厂模式)

1. 问题

如何实现一个合约工厂(Factory),能够按需创建新的合约实例(如 Uniswap 的交易对合约),并保持对创建地址的确定性追踪?

具体来说:实现一个 PairFactory 合约,使用 new 操作符 + salt 来创建 ERC20 代币交易对合约 (Pair)。要求支持确定性地址预计算、避免重复创建同一交易对、追踪所有已创建实例。

2. 原因

合约工厂是 DeFi 基础设施的核心模式。Uniswap、Aave、Compound 等协议都使用工厂模式来管理大量同质化合约实例(交易对、借贷池等)。

为什么需要工厂模式?

  • Gas 效率:避免手动部署数百个结构相同的合约(new 操作符完成一次部署)
  • 确定性地址:通过 salt 机制,相同参数始终产生相同地址 — 用户可以在部署前就计算并信任地址
  • 注册追踪:工厂维护所有已创建合约的索引(allPairs 数组),方便前端查询
  • 标准化:所有通过工厂创建的合约共享相同的已验证字节码

3. 方案

架构设计

PairFactory
  ├── createPair(tokenA, tokenB)
  │     ├── canonical ordering(较小地址在前)
  │     ├── 防重复: getPair[token0][token1] == 0
  │     ├── salt = keccak256(token0, token1)
  │     ├── new Pair{salt: salt}(token0, token1)  ← CREATE2 部署
  │     ├── getPair[token0][token1] = pair
  │     └── allPairs.push(pair)

  ├── computePairAddress(tokenA, tokenB) → address
  │     └── 手工实现 CREATE2 地址公式预计算

  ├── getPair[token0][token1] → address
  └── allPairs[] + allPairsLength()
Pair (子合约)
  ├── token0: immutable address
  ├── token1: immutable address
  ├── factory: immutable address
  └── getReserves() → (0, 0)

Canonical Ordering

solidity
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

这个模式来自 Uniswap V2。无论传入 (USDC, WETH) 还是 (WETH, USDC),都创建同一个交易对。getPair[token0][token1] 查表时不需要关心原始顺序。

CREATE2 地址预计算

solidity
function computePairAddress(address tokenA, address tokenB) external view returns (address pair) {
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    bytes memory bytecode = abi.encodePacked(type(Pair).creationCode, abi.encode(token0, token1));
    bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode)));
    pair = address(uint160(uint256(hash)));
}

CREATE2 地址公式:address = keccak256(0xff || deployer || salt || initCodeHash)[12:]

4. 遭遇的陷阱

4.1 new vs new{salt} 的区别

new Pair(token0, token1) → 使用 CREATE 操作码,地址由 (deployer, nonce) 决定。 new Pair{salt: salt}(token0, token1) → 使用 CREATE2 操作码,地址由 (deployer, salt, initCode) 决定。

没有 salt 时每次部署产生不同地址;有 salt 时相同参数 → 相同地址 → 天然防止重复部署。

4.2 Canonical Ordering 必须双重同步

工厂 createPair 做了排序但 Pair 构造函数没做 → Pair(token0, token1) 内部顺序不一致。 Pair 构造函数做了排序但工厂没做 → 同一对代币可能创建两个不同 Pair。 必须在两处都做 canonical ordering。

4.3 computePairAddress 的 init code 计算

solidity
// ✅ 正确:包含构造函数参数
bytes memory bytecode = abi.encodePacked(type(Pair).creationCode, abi.encode(token0, token1));

// ❌ 错误:不含参数,计算结果与实际部署地址不匹配
bytes memory bytecode = type(Pair).creationCode;

4.4 重复部署的双重防护

  • 应用层:getPair[token0][token1] != address(0) 检查
  • EVM 层:CREATE2 如果目标地址已有代码会 revert

两层防护确保不会有一个交易对被创建两次。

5. 陷阱的原因

5.1

EVM 地址计算:

  • CREATEaddress = keccak256(sender, nonce)[12:] — 依赖发送者 nonce
  • CREATE2address = keccak256(0xff, sender, salt, initCodeHash)[12:] — 完全确定性

5.2

如果只有一处做了 canonical ordering:

  • 工厂做了,Pair 没做:createPair(A, B) → Pair(token0=B, token1=A) → 但用户可能期望 token0=A
  • 工厂没做,Pair 做了:createPair(A, B) → Pair(A, B);createPair(B, A) → Pair(A, B) 但 salt 不同 → 可能因地址不冲突而创建两个相同代币对的合约

6. 如何解决陷阱

solidity
function createPair(address tokenA, address tokenB) external returns (address pair) {
    // 1. Canonical ordering
    (address token0, address token1) = tokenA < tokenB
        ? (tokenA, tokenB) : (tokenB, tokenA);

    // 2. Duplicate check
    require(getPair[token0][token1] == address(0), "Pair exists");

    // 3. Deploy with deterministic salt
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    pair = address(new Pair{salt: salt}(token0, token1));

    // 4. Register
    getPair[token0][token1] = pair;
    allPairs.push(pair);
}

7. 技术要点

要点说明
工厂模式一个合约创建并管理多个标准化子合约
new + saltnew Contract{salt: salt}(args) → CREATE2 确定性部署
Canonical Orderingtoken0 < token1 消除代币顺序歧义
CREATE2 地址公式address = keccak256(0xff, factory, salt, initCodeHash)[12:]
预计算地址在部署前就可以知道合约地址
防重复getPair[token0][token1] 查表 + CREATE2 天然防碰撞
Uniswap V2本关的直接灵感来源 (Pair + Factory 架构)

Built with AiAda