Appearance
L2-21: Contract Factory
1. Problem
How do you implement a contract Factory that can create new contract instances on demand (such as Uniswap's pair contracts) and maintain deterministic tracking of the created addresses?
Specifically: implement a PairFactory contract that uses the new operator + salt to create ERC20 token pair contracts (Pair). It must support deterministic address precomputation, prevent duplicate creation of the same pair, and track all created instances.
2. Why
The Contract Factory is a core pattern in DeFi infrastructure. Protocols like Uniswap, Aave, and Compound all use the factory pattern to manage large numbers of homogeneous contract instances (trading pairs, lending pools, etc.).
Why is the factory pattern needed?
- Gas efficiency: Avoid manually deploying hundreds of structurally identical contracts (the
newoperator completes a single deployment) - Deterministic addresses: Through the
saltmechanism, identical parameters always produce the same address -- users can compute and trust the address before deployment - Registry tracking: The factory maintains an index of all created contracts (the
allPairsarray), making frontend queries easy - Standardization: All contracts created through the factory share the same verified bytecode
3. Solution
Architecture Design
PairFactory
+-- createPair(tokenA, tokenB)
| +-- canonical ordering (smaller address first)
| +-- duplicate prevention: getPair[token0][token1] == 0
| +-- salt = keccak256(token0, token1)
| +-- new Pair{salt: salt}(token0, token1) <- CREATE2 deployment
| +-- getPair[token0][token1] = pair
| +-- allPairs.push(pair)
|
+-- computePairAddress(tokenA, tokenB) -> address
| +-- Manually implement the CREATE2 address formula for precomputation
|
+-- getPair[token0][token1] -> address
+-- allPairs[] + allPairsLength()
Pair (child contract)
+-- 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);
This pattern comes from Uniswap V2. Whether you pass (USDC, WETH) or (WETH, USDC), the same trading pair is created. You don't need to worry about the original order when looking up getPair[token0][token1].
CREATE2 Address Precomputation
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 formula: address = keccak256(0xff || deployer || salt || initCodeHash)[12:]
4. Pitfalls Encountered
4.1 Difference between new and new{salt}
new Pair(token0, token1) -> uses the CREATE opcode; address is determined by (deployer, nonce). new Pair{salt: salt}(token0, token1) -> uses the CREATE2 opcode; address is determined by (deployer, salt, initCode).
Without salt, each deployment produces a different address; with salt, identical parameters -> identical address -> natural duplicate deployment prevention.
4.2 Canonical Ordering Must Be Synchronized in Both Places
The factory's createPair does sorting but the Pair constructor does not -> internal ordering in Pair(token0, token1) is inconsistent. The Pair constructor does sorting but the factory does not -> the same token pair could create two different Pairs. Canonical ordering must be applied in both places.
4.3 init Code Calculation in computePairAddress
solidity
// Correct: includes constructor arguments
bytes memory bytecode = abi.encodePacked(type(Pair).creationCode, abi.encode(token0, token1));
// Incorrect: no arguments; computed result does not match the actual deployment address
bytes memory bytecode = type(Pair).creationCode;
4.4 Dual Protection Against Duplicate Deployment
- Application layer:
getPair[token0][token1] != address(0)check - EVM layer:
CREATE2will revert if the target address already contains code
Two layers of protection ensure a trading pair can never be created twice.
5. Why the Pitfalls Occur
5.1
EVM address calculation:
CREATE:address = keccak256(sender, nonce)[12:]-- depends on the sender's nonceCREATE2:address = keccak256(0xff, sender, salt, initCodeHash)[12:]-- fully deterministic
5.2
If canonical ordering is applied in only one place:
- Factory does it, Pair does not:
createPair(A, B)-> Pair(token0=B, token1=A) -> but users might expect token0=A - Factory does not, Pair does:
createPair(A, B)-> Pair(A, B);createPair(B, A)-> Pair(A, B) but with different salt -> may create two contracts for the same token pair due to non-colliding addresses
6. How to Resolve the Pitfalls
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. Technical Highlights
| Key Point | Description |
|---|---|
| Factory Pattern | One contract creates and manages multiple standardized child contracts |
new + salt | new Contract{salt: salt}(args) -> CREATE2 deterministic deployment |
| Canonical Ordering | token0 < token1 eliminates token order ambiguity |
| CREATE2 Address Formula | address = keccak256(0xff, factory, salt, initCodeHash)[12:] |
| Precomputed Addresses | The contract address can be known before deployment |
| Duplicate Prevention | getPair[token0][token1] lookup + CREATE2 natural collision resistance |
| Uniswap V2 | The direct inspiration for this challenge (Pair + Factory architecture) |