Skip to content
On this page

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 new operator completes a single deployment)
  • Deterministic addresses: Through the salt mechanism, 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 allPairs array), 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: CREATE2 will 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 nonce
  • CREATE2: 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 PointDescription
Factory PatternOne contract creates and manages multiple standardized child contracts
new + saltnew Contract{salt: salt}(args) -> CREATE2 deterministic deployment
Canonical Orderingtoken0 < token1 eliminates token order ambiguity
CREATE2 Address Formulaaddress = keccak256(0xff, factory, salt, initCodeHash)[12:]
Precomputed AddressesThe contract address can be known before deployment
Duplicate PreventiongetPair[token0][token1] lookup + CREATE2 natural collision resistance
Uniswap V2The direct inspiration for this challenge (Pair + Factory architecture)

Built with AiAda