Appearance
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 地址计算:
CREATE:address = keccak256(sender, nonce)[12:]— 依赖发送者 nonceCREATE2:address = 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 + salt | new Contract{salt: salt}(args) → CREATE2 确定性部署 |
| Canonical Ordering | token0 < token1 消除代币顺序歧义 |
| CREATE2 地址公式 | address = keccak256(0xff, factory, salt, initCodeHash)[12:] |
| 预计算地址 | 在部署前就可以知道合约地址 |
| 防重复 | getPair[token0][token1] 查表 + CREATE2 天然防碰撞 |
| Uniswap V2 | 本关的直接灵感来源 (Pair + Factory 架构) |