← Back to list

5. Spoon-Fed Uniswap V2: createPair — Code level walkthrough

createPair

Dreamboys · 2025-12-22 04:34 · 0 claps · 3.8 min read
#defi #uniswap-v2 #dreamboy #blockchain
Open on Medium ↗
Wiki topics: MAC · Macroeconomics CRY · Crypto & Web3

5. Spoon-Fed Uniswap V2: createPair — Code level walkthrough

createPair

Alright, quick recap.

A Pair contract is deployed by the Factory. And like we briefly tasted during the swap deep-dive: the pair address isn’t some “random gift from the blockchain gods.”

Uniswap V2 makes the Pair address deterministic using the two token addresses.

Meaning:

If you know tokenA and tokenB, you can predict the Pair contract address without asking anyone.

Let’s open up the hood and see exactly how.

0) Warm-up: the interface

v2-core/IUniswapV2Factory.sol

function createPair(address tokenA, address tokenB) external returns 
(address pair);

The vibe is obvious:

“Here are two token addresses. Make me a pool.”

Factory:

“Got you. Here’s the pair address.”

…Except the factory is also doing something sneaky and elegant: it guarantees the address ahead of time.

1) The real code: UniswapV2Factory.createPair

v2-core/UniswapV2Factory.sol

function createPair(address tokenA, address tokenB) external returns (address pair) {
    // 1.
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
    // 2.
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    // 3.
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair;
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}

Let’s slice it into 3 phases.

Phase 1 — Sanity checks (a.k.a. “don’t be weird”)

  • Tokens must be different tokenA != tokenB
  • Sort them so ordering is consistent token0 < token1 always
  • No zero address
  • Pair must not already exist

That sorting part is not “style.” It’s a critical invariant.

If you don’t sort:

  • WETH/USDT and USDT/WETH would be treated as different pairs
  • which would be… chaos with extra steps.

Also: yes, they store the pair address in a mapping, which feels expensive — but it makes lookup cheap and standard.

Phase 2 — Deploy the Pair with CREATE2

Here’s the core idea:

CREATE vs CREATE2

  • CREATE: address depends on deployer + nonce (good luck guessing a factory’s nonce at some historical moment)
  • CREATE2: address depends on (deployer address, salt, init code hash)

So Uniswap uses CREATE2 so that:

knowing token0 and token1 is enough to compute the pair address.

Step 2.1 — grab init code (creation code)

bytes memory bytecode = type(UniswapV2Pair).creationCode;

That returns the init code used at deployment time.

Important mental model:

  • creation code / init code runs only once, during deployment
  • it returns the runtime code
  • runtime code is what ends up stored on-chain as “the contract”

So CREATE2 executes init code, then stores whatever it returns.

Step 2.2 — compute the salt

bytes32 salt = keccak256(abi.encodePacked(token0, token1));

Salt rule is simple: pack the two sorted token addresses and hash them.

So again:

token addresses → salt → predictable address

Step 2.3 — raw create2 call (assembly)

assembly {
    pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}

create2(value, ptr, size, salt):

  • value: ETH to send on deployment (0)
  • ptr: memory pointer to init code
  • size: init code length
  • salt: our deterministic ingredient

Now the “ptr/size” part looks scary because Solidity memory is a bit… “why are you like this?”

Why add(bytecode, 32)?

Dynamic bytes in memory is stored like:

  • first 32 bytes: length
  • then: actual bytes data

So:

  • bytecode points to the length slot
  • add(bytecode, 32) points to the real code bytes

Why mload(bytecode)?

That loads the first 32 bytes = the length.

So the assembly is basically:

“Take this init code from memory, run it, and deploy the returned runtime code — but pick the address deterministically using salt.”

That’s it.

All the mysticism is just memory layout.

Phase 3 — Initialize the Pair

After deploying, Factory calls:

IUniswapV2Pair(pair).initialize(token0, token1);

Inside the Pair:

function initialize(address _token0, address _token1) external {
    require(msg.sender == factory, 'UniswapV2: FORBIDDEN');
    token0 = _token0;
    token1 = _token1;
}

It just stores which tokens this pair is for.

“Why not constructor?”

Because Uniswap wants the init code to be identical for all pairs.

If token addresses were constructor args:

  • each pair’s init code would differ (args get baked into creation)
  • which means init code hash differs
  • which changes the CREATE2 address formula inputs
  • and your “predictable address from token addresses” property becomes messier / less clean.

So they keep init code constant and pass token addresses via an initialize() call instead.

Clean, deterministic, reusable.

Phase 4 — Save and emit (done)

getPair[token0][token1] = pair;
getPair[token1][token0] = pair;
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);

They store it both ways for convenience, push into array, and emit event.

The real takeaway

Uniswap V2 Pair addresses are deterministic because:

  • tokens are sorted
  • salt is hash(token0, token1)
  • init code is constant
  • deployer (factory) is fixed
  • deployed via CREATE2

So:

tokenA + tokenB → (sort) → salt → predictable CREATE2 address

And yeah: this “predict the pool address before it exists” trick is one of those patterns you’ll keep seeing all the way through v4 (just with different flavors).

If you want, next I can write the “compute pair address off-chain” section in the same vibe (the famous keccak256(0xff ++ factory ++ salt ++ init_code_hash) formula) — that’s the final piece that makes the determinism feel real.


메타데이터
post_id
6bf0a09ef5c6
slug
5-spoon-fed-uniswap-v2-createpair-code-level-walkthrough-6bf0a09ef5c6
url
https://medium.com/@dreamboys0107/5-spoon-fed-uniswap-v2-createpair-code-level-walkthrough-6bf0a09ef5c6
canonical_url
https://medium.com/@dreamboys0107/5-spoon-fed-uniswap-v2-createpair-code-level-walkthrough-6bf0a09ef5c6
author_url
https://medium.com/@dreamboys0107
status
ok
fetched_at
2026-08-20 18:19:56