Section 8 of 16

Build
+15 Lynx

The Factory (UniswapV2Factory)

What You Are Building

UniswapV2Factory is the contract that deploys and registers trading pairs. It is the entry point for creating new markets. Every pair on Uniswap V2 was deployed by this single factory contract.

The factory is simpler than the pair. It has one core function (createPair), two governance setters, and a registry. But it introduces one of the most important concepts in the protocol: deterministic deployment with CREATE2. Understanding why the factory is designed this way reveals how Uniswap V2 achieves gas efficiency, composability, and a clean separation between pair logic and deployment logic.

CREATE2: Deterministic Contract Addresses

When you deploy a contract with the standard new keyword (CREATE opcode), the resulting address depends on the deployer's address and their current nonce. The nonce increments with every transaction, so the address is unpredictable before deployment. If you want to find a pair's address, you have to query the factory's getPair mapping, which costs gas.

CREATE2 changes the equation entirely. The deployed address is computed as:

address = keccak256(0xff, deployer, salt, keccak256(creationCode))[12:]

Four inputs, all deterministic. 0xff is a fixed prefix that distinguishes CREATE2 from CREATE. deployer is the factory's address (known and constant). salt is derived from the two token addresses (known). creationCode is the pair contract's bytecode (constant across all deployments). Since all four inputs are known before deployment, anyone can compute the pair address off-chain using pure math. No blockchain query needed.

The Router exploits this directly. When a user calls swapExactTokensForTokens, the Router needs to find the pair contract for each hop in the path. Instead of calling factory.getPair(tokenA, tokenB) (which is a STATICCALL costing ~2600 gas), the Router computes the address locally using the CREATE2 formula. For a multi-hop swap touching 3 pairs, this saves roughly 8000 gas. At scale, this adds up to meaningful savings for every user.

The salt is keccak256(abi.encodePacked(token0, token1)), where token0 and token1 are sorted. This means the salt is the same regardless of which order the tokens were passed to createPair. Combined with the fact that the creation code never changes, every pair has exactly one possible address.

Token Sorting: Canonical Order Everywhere

Uniswap V2 enforces a strict rule: in every pair, the token with the numerically smaller address is token0, and the larger is token1. The factory performs this sort in createPair before deployment, and the convention propagates everywhere. Reserves are stored in this order. The oracle accumulates prices in this order. The Router expects this order.

Why does this matter? Without canonical ordering, createPair(A, B) and createPair(B, A) would produce different salts, different CREATE2 addresses, and therefore two separate pair contracts for the same token pair. That would fragment liquidity and break the entire protocol. By sorting first, both calls produce the same salt and the same address. The factory also checks getPair[token0][token1] == address(0) before deploying, which prevents duplicate pairs.

The getPair mapping is populated in both directions: getPair[A][B] and getPair[B][A] both point to the same pair address. This is purely a convenience for callers who do not want to sort tokens before looking up a pair.

The Initialize Pattern: Why Not Use the Constructor?

After deploying the pair with CREATE2, the factory calls pair.initialize(token0, token1) to set the token addresses. This raises an obvious question: why not pass token0 and token1 as constructor arguments?

The answer is rooted in how CREATE2 address computation works. The creationCode in the CREATE2 formula includes constructor arguments. If you pass different token addresses to the constructor, the creation code changes, and therefore the keccak256(creationCode) changes, and therefore the init code hash that the Router uses to compute pair addresses changes per pair.

Uniswap V2 wanted a single, constant init code hash that the Router could hardcode. This hash is the INIT_CODE_PAIR_HASH that you see referenced in the Router library. If constructor arguments were used, the Router would need to know the pair's creation code including those specific arguments to compute its address, which defeats the purpose.

By using a parameterless constructor and a separate initialize() call, the creation code is identical for every pair. One constant hash works everywhere. The initialize() function includes a check that it can only be called once (by requiring token0 is still address(0)), so it cannot be re-initialized after deployment.

Note: in Solidity 0.8.x, you can write new UniswapV2Pair{salt: salt}() instead of using inline assembly CREATE2. The compiler handles the opcode. The original Uniswap V2 was written in Solidity 0.5.16 and used assembly for CREATE2 because the new ... {salt: ...} syntax did not exist yet. Both approaches produce the same bytecode at the EVM level.

The createPair() Function in Detail

The function follows a strict sequence. Each step exists for a specific reason.

Validation. First, require that tokenA != tokenB. Identical tokens would produce a zero-length pair with no trading purpose. Then sort the tokens and require that token0 != address(0). Since token0 is the smaller address, if token0 is not zero, token1 cannot be zero either (it is larger). Finally, check that getPair[token0][token1] == address(0). If the pair already exists, deploying another one would waste gas and create an orphaned contract.

Deployment. Compute the salt from the sorted token addresses and deploy with new UniswapV2Pair{salt: salt}(). The returned address is the pair contract.

Initialization. Call initialize(token0, token1) on the new pair. This sets the pair's token addresses permanently. The pair's constructor does not set them because of the init code hash constraint explained above.

Registration. Store the pair address in getPair[token0][token1] and getPair[token1][token0]. Push the address to the allPairs array. The array exists so anyone can enumerate all pairs on-chain.

Event. Emit PairCreated(token0, token1, pair, allPairs.length). The length field lets indexers track the total number of pairs without reading storage.

Governance: The feeTo and feeToSetter Model

The factory controls two governance parameters through a simple two-role model.

feeTo is the address that receives protocol fees. When feeTo is address(0) (the default), protocol fees are disabled entirely. The pair's _mintFee() function reads this value from the factory on every mint, burn, and swap. Setting it to a non-zero address activates the 1/6th protocol fee.

feeToSetter is the address authorized to change feeTo and to transfer the feeToSetter role to a new address. This is set in the constructor and acts as the governance root.

setFeeTo(address) changes the fee recipient. Only feeToSetter can call it. There is no timelock, no multisig requirement, no delay. In production Uniswap V2, feeToSetter was initially the Uniswap team and was later transferred to governance.

setFeeToSetter(address) transfers the governance role. Again, only the current feeToSetter can call it. This is a single-step transfer with no confirmation from the new address, which means sending it to a wrong address permanently locks governance. A two-step pattern (propose + accept) would be safer, but Uniswap V2 chose simplicity.

Your Task

Write three functions:

  1. createPair(address tokenA, address tokenB): The complete pair deployment flow. Validate inputs (no identical tokens, no zero addresses, no duplicates). Sort tokens. Deploy with CREATE2 using new UniswapV2Pair{salt: keccak256(abi.encodePacked(token0, token1))}(). Call initialize. Register in both directions of the getPair mapping and push to allPairs. Emit PairCreated.

  2. setFeeTo(address _feeTo): Require that msg.sender == feeToSetter. Set feeTo to the new address.

  3. setFeeToSetter(address _feeToSetter): Require that msg.sender == feeToSetter. Set feeToSetter to the new address.

The starter code gives you the state variables, the constructor, and the PairCreated event. The allPairsLength() view function is also provided.

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

createPair rejects identical addresses
createPair sorts tokens
createPair rejects zero address
createPair checks pair does not exist
createPair uses CREATE2
createPair computes salt from sorted tokens
createPair calls initialize
createPair registers pair in both directions
createPair pushes to allPairs
createPair emits PairCreated
setFeeTo checks authorization
setFeeTo updates feeTo
setFeeToSetter updates feeToSetter