Liquidity Pool Deep Dive — AMM Foundations to Production DeFi

Liquidity Pool Deep Dive — AMM Foundations to Production DeFi

This guide provides a thorough, expert-level understanding of Automated Market Maker (AMM) liquidity pools. Starting from first principles and progressing through advanced mechanics, it covers everything you need to confidently deploy, manage, and reason about liquidity pools in production EVM environments using Uniswap V2-compatible DEXs such as PancakeSwap and others supported by CCBus.


Table of Contents


1. What Is a Liquidity Pool?

A liquidity pool is a smart contract that holds reserves of two tokens and enables decentralized, trustless trading between them. Unlike traditional order books (which need buyers and sellers to match at the same time), a liquidity pool uses a mathematical formula to determine prices algorithmically — this is the Automated Market Maker (AMM) model.

Traditional Order Book vs AMM Liquidity Pool Traditional Order Book SELL 100 @ $1.05 SELL 80 @ $1.03 SELL 50 @ $1.01 ── spread ── BUY 40 @ $0.99 BUY 60 @ $0.97 BUY 200 @ $0.94 Needs counterparty to match vs AMM Liquidity Pool Pool Contract Reserve A: 10,000 TOKEN Reserve B: 100 WETH Price = Reserve B / Reserve A x · y = k (always) Always available, no counterparty needed

Key insight: The pool is always available to trade against. There is no matching engine, no waiting for a buyer or seller. The price is determined entirely by the ratio of reserves in the pool.


2. The AMM Formula — Constant Product Market Maker

2.1 The x·y = k Invariant

The Uniswap V2 AMM is a Constant Product Market Maker. It enforces one simple invariant:

1
x · y = k

Where:

  • x = reserve of Token A
  • y = reserve of Token B
  • k = a constant (never decreases, only grows with fees)

When a trade occurs, tokens flow in and out such that the product x · y remains constant (before fees). This creates the characteristic hyperbolic price curve:

x · y = k Constant Product Curve Reserve X Reserve Y 0 100 200 300 400 Current State (x=200, y=50) After Buy X (x=290, y=34.5) Δx in Δy out slope = spot price k = 10,000

The curve is a hyperbola. As you try to buy more of one token, the price increases non-linearly — this is price impact.

2.2 Price Impact and Slippage

When you execute a trade, the price you pay is the average across the curve segment traversed, not the spot price:

1
2
3
4
Spot price (marginal):   price = y / x
Execution price (avg): price = Δy / Δx (always worse than spot)

The larger the trade relative to pool size → greater price impact
Trade Size (% of Pool) Approximate Price Impact
0.01% ~0.01% (negligible)
0.1% ~0.1%
1% ~1.01%
5% ~5.26%
10% ~11.11%
20% ~25%
50% ~100% (doubles price)

2.3 The Spot Price Formula

1
2
3
4
5
6
Spot price of X in terms of Y:  P(X) = reserveY / reserveX

Example:
reserveX = 10,000 TOKEN
reserveY = 100 WETH
→ 1 TOKEN = 0.01 WETH = 100 TOKEN per WETH

Key point: The spot price is just the reserve ratio. Any trade that changes reserves also changes the price.


3. Uniswap V2 Architecture

3.1 The Three-Contract Model

Uniswap V2 (and all compatible DEXs like PancakeSwap V2) use a three-layer contract architecture:

Uniswap V2 Contract Architecture 👤 User / DApp msg.sender calls Router Contract addLiquidity / removeLiquidity swapExactTokensForTokens / ... getPair / createPair mint / burn / swap Factory Contract getPair(tokenA, tokenB) createPair → deploys Pair Pair Contract #1 TOKEN / WETH reserve0, reserve1 ERC-20 LP Token Pair Contract #2 TOKEN / USDT reserve0, reserve1 ERC-20 LP Token creates via CREATE2 Contract Responsibilities Router: Safe user-facing entry point, slippage + deadline protection Factory: Pair registry, one pair per token combo Pair: Holds reserves, mints/burns LP tokens, executes swaps

3.2 Factory Contract — Pool Registry

The Factory contract is the registry for all pools on a DEX. It has two critical jobs:

  1. getPair(tokenA, tokenB) — Returns the pair contract address, or address(0) if not yet created
  2. createPair(tokenA, tokenB) — Deploys a new pair contract using CREATE2; reverts if already exists

The factory normalizes token ordering (token0 < token1 by address) so getPair(A, B) and getPair(B, A) always return the same pool.

3.3 Pair Contract — The Pool

Each pair contract is an ERC-20 LP token contract that also holds the pool’s token reserves. It exposes:

Function Description
mint(to) Called after tokens are transferred in; mints LP tokens
burn(to) Burns LP tokens, sends proportional reserves out
swap(amount0Out, amount1Out, to, data) Executes a swap after tokens are sent in
getReserves() Returns (reserve0, reserve1, blockTimestampLast)
token0(), token1() The two token addresses
totalSupply() Total LP token supply across all holders
balanceOf(address) LP tokens held by a specific address

3.4 Router Contract — User Interface

The Router is the safe user-facing entry point. Users should always interact with the Router, not the Pair directly. The Router:

  • Validates slippage tolerances (amountMin parameters)
  • Enforces transaction deadlines
  • Handles ETH ↔ WETH wrapping transparently
  • Performs multi-hop routing across multiple pools
  • Handles the createPair flow if pool doesn’t exist

4. Pool Uniqueness and Deterministic Addresses

4.1 What Makes a Pool Unique

A pool is uniquely identified by exactly three factors. Wallet address is NOT one of them.

Pool Identity: Three Required Factors ① Token Pair TOKEN_A + TOKEN_B Order doesn't matter: getPair(A,B) == getPair(B,A) ✓ Normalized by address sort + ② DEX Factory PancakeSwap Factory vs ApeSwap Factory vs BiSwap Factory → Different factory = different pool + ③ Blockchain Ethereum (ChainID: 1) BSC (ChainID: 56) Polygon (ChainID: 137) → Different chain = different pool = ONE deterministic pool address ❌ NOT a uniqueness factor: Wallet Address Alice (0xAaaa...1111) and Bob (0xBbbb...2222) both share the EXACT same TOKEN/WETH pool on the same factory + chain. Your wallet does NOT create a separate pool. You are always adding to the shared pool.

4.2 CREATE2 Deterministic Deployment

Pool addresses are pre-computable — they don’t require a transaction to look up. The factory deploys pair contracts using the CREATE2 opcode with a salt derived from the token pair:

1
2
3
4
salt     = keccak256(abi.encodePacked(token0, token1))
initHash = keccak256(pairBytecode)

pairAddress = keccak256(0xff ++ factoryAddress ++ salt ++ initHash)[12:]

This means:

  • The same token pair on the same factory always produces the same address
  • Pair addresses can be computed off-chain before any transaction
  • Two concurrent createPair calls for the same pair: the second will revert because the CREATE2 target address is already occupied

4.3 One Pool Per Pair Per Factory

1
2
3
4
5
6
TOKEN/WBNB on PancakeSwap V2 = Pool 0xAAA... (ONE pool, shared by everyone)
TOKEN/WBNB on ApeSwap = Pool 0xBBB... (different DEX = different pool)
TOKEN/WETH on Ethereum = Pool 0xCCC... (different chain = different pool)

getPair(TOKEN, WBNB) → 0xAAA...
getPair(WBNB, TOKEN) → 0xAAA... ← same! order normalized

5. LP Tokens — Proof of Ownership

5.1 LP Token Mechanics

When you add liquidity, the pair contract mints LP tokens to you. LP tokens are standard ERC-20 tokens representing your proportional claim on the pool’s reserves.

LP Token Minting Flow User Wallet 1,000 TOKEN 10 WETH transfer tokens in Pair Contract Before: reserve0: 9,000 TOKEN reserve1: 90 WETH totalSupply: 900 LP After: reserve0: 10,000 TOKEN reserve1: 100 WETH totalSupply: 1,000 LP mint 100 LP User gets 100 LP Tokens (10% ownership) LP Token Minting Formula First mint: LP = sqrt(amountA × amountB) − MINIMUM_LIQUIDITY Subsequent: LP = min( amountA/reserveA, amountB/reserveB ) × totalSupply

5.2 Initial Liquidity and Minimum Lock

On the first mint, Uniswap V2 burns a small amount of LP tokens (MINIMUM_LIQUIDITY = 1000 wei) by sending them to address(0). This permanently locks a tiny amount of liquidity in the pool and prevents the total supply from ever reaching zero, protecting against division-by-zero and price manipulation attacks.

1
2
3
First mint:
LP minted to provider = sqrt(amount0 * amount1) - 1000
LP burned to address(0) = 1000 ← locked forever

5.3 Proportional Ownership

Your LP tokens represent a proportional claim on the pool’s reserves:

1
2
3
4
Your share %  = yourLP / totalLP × 100

Your TOKEN A = reserve0 × (yourLP / totalLP)
Your TOKEN B = reserve1 × (yourLP / totalLP)

If the pool grows (from fees or others adding liquidity), the absolute value of your position grows even if your percentage changes.


6. Adding Liquidity

6.1 First Liquidity — Setting the Price

The first liquidity provider sets the pool’s initial price by choosing the ratio of tokens they deposit. This is a critical responsibility:

If you deposit 10,000 TOKEN and 1 WETH, you’ve set the price at 10,000 TOKEN per WETH.
If the market price is 500 TOKEN per WETH, arbitrageurs will immediately buy WETH from your pool, causing you impermanent loss.

Best practice: Deposit at the current market price ratio.

6.2 Subsequent Liquidity — Matching the Ratio

All liquidity added after pool creation must match the current reserve ratio (within the slippage tolerance you specify). The router calculates the optimal amounts:

1
2
3
4
Desired: add amountADesired of TokenA
→ Required TokenB = amountADesired × reserveB / reserveA
→ If you provide more TokenB than required, only reserveB/reserveA proportion is used
→ Excess is returned to your wallet

This is why you always specify both amountADesired AND amountBDesired plus amountAMin / amountBMin — the router will use whichever side is the binding constraint.

6.3 Multi-User Pool Lifecycle

Pool Lifecycle: Three LPs Joining Over Time ① Alice Creates Pool Deposits: 1,000 TOKEN + 10 WETH Sets price: 100 TOKEN/WETH TOKEN: 1,000 WETH: 10 LP supply: 100 (Alice: 100%) ② Bob Adds Liquidity Deposits: 500 TOKEN + 5 WETH Matches existing ratio TOKEN: 1,500 WETH: 15 LP: 150 (Alice 67%, Bob 33%) ③ Carol Adds Liquidity Deposits: 1,500 TOKEN + 15 WETH Matches existing ratio TOKEN: 3,000 WETH: 30 LP: 300 (A:33% B:17% C:50%) ALL THREE USE THE SAME POOL CONTRACT Final Shared Pool State Ownership Alice: 100 LP (33.3%) Bob: 50 LP (16.7%) Carol: 150 LP (50%) Total: 300 LP TOKEN reserve: 3,000 WETH reserve: 30 Price: 100 TOKEN/WETH k = 3,000 × 30 = 90,000 Fee rate: 0.3% Each user's tokens are intermingled in the pool. Only LP tokens track individual ownership.

7. Swapping — How Trades Work

7.1 The Swap Mechanics

A swap in Uniswap V2 follows this sequence:

Token Swap Flow ① User Calls router swapExactTokens ForTokens() ② Router Check amountOutMin Verify deadline Transfer tokenIn → pair ③ Pair Contract Detect tokenIn received Compute amountOut Update reserves ④ User Receives tokenOut transferred to user address ≥ amountOutMin ✓ Pair Contract Computation (with 0.3% fee) Given: amountIn = 10 WETH, reserveIn = 100 WETH, reserveOut = 10,000 TOKEN Step 1: amountInWithFee = amountIn × 997 → 10 × 997 = 9,970 Step 2: numerator = amountInWithFee × reserveOut → 9,970 × 10,000 = 99,700,000 Step 3: denominator = reserveIn × 1000 + amountInWithFee → 100,000 + 9,970 = 109,970 Result: amountOut = 99,700,000 / 109,970 ≈ 906.6 TOKEN (vs 1,000 TOKEN at spot — 9.3% price impact)

7.2 Fee Collection (0.3%)

Uniswap V2 charges 0.3% on every swap. This fee is not sent anywhere — it is left in the pool’s reserves, increasing the k value slightly after each trade.

1
2
Every swap:  new_k = (reserveIn + amountIn × 0.997) × (reserveOut − amountOut)
new_k > original_k ← pool grows with each trade

LP token holders benefit because the reserves backing each LP token increase over time. There is no separate "claim fees" transaction — fees are automatically compounded into the reserve ratio.

7.3 Minimum Output and Deadline

The Router enforces two critical protections:

Parameter Purpose
amountOutMin Minimum tokens you’ll accept (slippage protection)
deadline Unix timestamp after which tx will revert (prevents stuck txs from executing at bad prices)
1
2
3
// Typical slippage: 0.5%
const amountOutMin = expectedOut * 0.995n; // accept up to 0.5% slippage
const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes

8. Removing Liquidity

Removing liquidity burns your LP tokens and returns your proportional share of the current reserves:

Remove Liquidity Flow Pool Before TOKEN: 10,000 WETH: 100 Total LP: 1,000 Your LP: 100 (10%) Your share: 1,000 T + 10 W burn 100 LP Pair.burn() 1. Burns 100 LP tokens 2. Calc proportional share 3. Transfer tokens to you 4. Update reserves You receive ~1,000 TOKEN ~10 WETH (+ accumulated fees) amounts may differ from original deposit Pool After: TOKEN: 9,000 | WETH: 90 | Total LP: 900 Pool continues to exist. Other LPs now own higher %. Pool is NOT deleted. Note: amounts received reflect current reserves, not original deposit amounts (impermanent loss / gain).

Important: The tokens you receive when withdrawing are based on the current reserves, not what you deposited. If significant trading occurred, the ratio of tokens you get back will differ from what you put in — this is the essence of impermanent loss.


9. Impermanent Loss — The Hidden Risk

9.1 What Is Impermanent Loss?

Impermanent loss (IL) occurs because the AMM formula automatically rebalances the pool as prices change. When the price of one token rises relative to the other, the AMM sells the appreciating token and buys the depreciating one. This means LPs hold less of the token that went up, and more of the one that went down — compared to simply holding.

The loss is "impermanent" because it disappears if prices return to the original ratio. But if prices do not revert, the loss becomes realized upon withdrawal.

9.2 The Impermanent Loss Formula

Let r = price change ratio (final price / initial price):

1
IL(r) = 2√r / (1 + r) − 1
Impermanent Loss vs Price Change 0% -1.2% -3.8% -5.7% -11.1% -20% 1x 1.25x 1.5x 2x 3x 4x 5x 10x Price Change Ratio (r) IL % HODL (0% loss) 2x: -5.7% IL 4x: -20% IL 10x: -42.5% IL Same curve applies for price decrease (r = 1/original) e.g., price drops to 0.5x → same as 2x increase → -5.7% IL
Price Change Impermanent Loss
±0% (no change) 0.00%
±25% −0.6%
±50% −2.0%
±2x (doubles or halves) −5.7%
±3x −13.4%
±4x −20.0%
±5x −25.5%
±10x −42.5%

9.3 Impermanent Loss vs Fee Income

IL is only one side of the equation. LPs earn 0.3% fee on every swap. Whether LP-ing is profitable depends on which is larger:

1
2
3
4
5
6
7
8
9
Net LP return = Fee income − Impermanent loss

Profitable when:
High trading volume (many swaps) → high fee income
Low price volatility → low IL

Risky when:
Low volume → low fees
High volatility → large IL

Stable pairs (e.g., USDC/USDT) have very low IL but also lower fee income. Volatile pairs have higher potential fees but also higher IL risk.


10. Fee Accumulation and LP Returns

Fees accumulate inside the pool reserves. When you remove liquidity, you receive your proportional share of the grown reserves:

Fee Accumulation Over Time T0 Pool created k = 100,000 Swap #1 0.3% fee k grows Swap #2 0.3% fee k grows Swap #N fees compound k = 101,200 Withdraw Get more tokens than deposited! k increases with each swap (fee auto-compounded) Each LP token is backed by more reserves after fees. No separate claim needed. Fee APR ≈ (daily volume / pool TVL) × 0.3% × 365

Protocol fee: Uniswap V2 can optionally enable a protocol fee of 0.05% (1/6 of the 0.3%), reducing LP fee income to 0.25%. CCBus-compatible DEXs like PancakeSwap V2 have their own fee structures, typically at 0.25% to LPs with a portion to treasury.


11. Multiple Pools for the Same Pair

11.1 Cross-DEX Arbitrage

When the same token pair exists on multiple DEXs, price differences create arbitrage opportunities:

Cross-DEX Arbitrage: Price Equalization DEX Alpha (PancakeSwap) Pool: 0xAAA... TOKEN: 10,000 WBNB: 100 Price: 100 TOKEN/WBNB ← CHEAPER TOKEN HERE DEX Beta (ApeSwap) Pool: 0xBBB... (DIFFERENT!) TOKEN: 4,500 WBNB: 55 Price: 81.8 TOKEN/WBNB ← MORE EXPENSIVE TOKEN HERE Arbitrageur buy TOKEN @ DEX A sell TOKEN @ DEX B Arbitrage Effect: Prices Converge DEX Alpha TOKEN bought → TOKEN decreases WBNB added → WBNB increases Price rises ↑ DEX Beta TOKEN sold → TOKEN increases WBNB removed → WBNB decreases Price falls ↓ → Prices equalize ← Arbitrage continues until prices match (minus fees and gas) This is how cross-DEX price discovery happens. LPs in both pools earn fees from arbitrage trades.

11.2 Multi-DEX Liquidity Strategy

You can hold positions in the same token pair across multiple DEXs simultaneously. Each position is independent:

Pool DEX Your LP Your % Notes
TOKEN/WBNB PancakeSwap V2 100 LP 5.0% Higher volume
TOKEN/WBNB ApeSwap 50 LP 3.2% Lower TVL

Advantages: Capture fees from multiple DEXs, reduce single-DEX risk
Considerations: More capital required, more transactions to manage


12. Uniswap V2 vs V3 — Key Differences

CCBus targets Uniswap V2-compatible DEXs. Here’s how V3 differs:

V2: Full Range vs V3: Concentrated Liquidity Uniswap V2 / PancakeSwap V2 Uniform liquidity across entire price range 0 Liquidity spread across full range Current Price ✓ Simple — set and forget ✓ One pool per pair (no fee tiers) ✓ Lower gas, lower complexity ✗ Less capital efficient ✗ Most liquidity is idle at extremes → Supported by CCBus Uniswap V3 Concentrated liquidity in price ranges $0 $∞ Price LP1 LP2 LP3 ✓ Up to 4000x capital efficiency ✓ Multiple fee tiers (0.01–1%) ✗ More complex, higher gas ✗ Requires active management ✗ Multiple pools per pair (fee tiers) → Not currently supported by CCBus
Feature V2 V3
Liquidity type Full range Concentrated ranges
Pools per pair One per factory Multiple (per fee tier)
LP complexity Simple Requires range management
Capital efficiency Baseline Up to 4,000× higher
Fee tiers 0.3% fixed 0.01%, 0.05%, 0.3%, 1%
NFT positions No (ERC-20 LP) Yes (ERC-721)
Supported by CCBus Yes Roadmap

13. Common Misconceptions

❌ "I created the pool so it’s mine"
✅ You were the first LP. Others can add liquidity. You own X% of a shared pool.

❌ "My wallet address determines which pool I use"
✅ Pool address is derived from (token0, token1, factory). Wallet is irrelevant.

❌ "Removing my liquidity deletes the pool"
✅ Pool persists as long as any LP tokens exist. Other users are unaffected by your exit.

❌ "I’ll receive exactly what I deposited when I withdraw"
✅ You receive your proportional share of the current reserves, adjusted for trading fees and impermanent loss.

❌ "Pool address is random every time it’s created"
✅ CREATE2 makes pool addresses deterministic and pre-computable from (token0, token1, factory, initCodeHash).

❌ "Fees are collected separately"
✅ Fees accumulate directly in pool reserves. They are automatically realized when you remove liquidity.


14. Best Practices

For Liquidity Providers

  1. Check if pool exists firstfactory.getPair(tokenA, tokenB) before any action
  2. Verify pool address — Cross-check with factory, never trust user-provided addresses
  3. Use market-rate ratios — When creating a new pool, match external price to avoid instant arbitrage loss
  4. Set appropriate slippage — 0.5–1% for stable pairs, 1–3% for volatile pairs
  5. Set deadline — Always use a deadline (20–30 minutes) to prevent stale tx execution
  6. Understand IL before large positions — Model your IL exposure for the pair’s historical volatility
  7. Monitor your LP % — Your percentage changes as others add/remove liquidity

For Developers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ Always check pool existence before operations
const pairAddress = await publicClient.readContract({
address: factoryAddress,
abi: FACTORY_ABI,
functionName: 'getPair',
args: [tokenA, tokenB]
});

const poolExists = pairAddress !== '0x0000000000000000000000000000000000000000';

if (!poolExists) {
// Inform user: they will SET the initial price
// Warn: check market price carefully
}
1
2
3
4
5
// ✅ Always verify the pool address from factory
const expectedPair = await factory.getPair(tokenA, tokenB);
if (userInput.toLowerCase() !== expectedPair.toLowerCase()) {
throw new Error('Pool address mismatch — possible scam');
}
1
2
3
// ✅ Always include slippage and deadline
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); // 20 min
const amountOutMin = (expectedOutput * 995n) / 1000n; // 0.5% slippage
1
2
3
4
// ✅ Calculate ownership % correctly
const lpBalance = await pair.balanceOf(userAddress);
const totalSupply = await pair.totalSupply();
const ownershipPct = (Number(lpBalance) * 100) / Number(totalSupply);

15. FAQ

Q: Can I create a private liquidity pool?

Standard Uniswap V2-style DEXs have fully public pools — anyone can add liquidity, remove liquidity, or trade. A private/permissioned pool requires a custom factory contract. CCBus works with public DEX infrastructure.

Q: What happens when two transactions simultaneously try to create the same pool?

CREATE2 ensures the address is deterministic. The first transaction deploys the contract; the second will revert because a contract already exists at that address. Standard routers handle this by calling getPair first and falling back to createPair only when needed.

Q: Does my LP token percentage change over time?

Yes:

  • Others add liquidity → your % decreases (dilution)
  • Others remove liquidity → your % increases
  • You add → your % increases
  • You remove → your % decreases
  • Swaps happen → your % stays the same (but absolute reserves backing each LP token grow due to fees)

Q: Can I transfer my LP tokens?

Yes. LP tokens are standard ERC-20 tokens and can be transferred, sold, or staked in yield farming contracts. Whoever holds LP tokens can redeem them for pool reserves.

Q: What’s the minimum liquidity locked in a pool?

MINIMUM_LIQUIDITY = 1000 (in the smallest unit of LP tokens) is minted to address(0) on the first mint. This cannot be redeemed. It prevents the pool from being completely drained and protects against certain price manipulation attacks.

Q: How do I calculate the current price from reserves?

1
2
3
4
5
6
const [reserve0, reserve1] = await pair.getReserves();
// token0 < token1 by address sort
// price of token0 in terms of token1:
const priceToken0 = Number(reserve1) / Number(reserve0);
// price of token1 in terms of token0:
const priceToken1 = Number(reserve0) / Number(reserve1);

Q: Why does addLiquidity sometimes return fewer tokens than I sent?

The router enforces ratio matching. If your provided amounts don’t perfectly match the current reserve ratio, one side is used in full and the excess of the other side is returned to your wallet. The amountAMin / amountBMin parameters protect you from excessive slippage in the refunded amounts.

Q: Can I use ETH (native BNB) directly in liquidity operations?

Yes, via addLiquidityETH / removeLiquidityETH on the Router. The Router automatically wraps your native token (BNB → WBNB, ETH → WETH) before interacting with the pool, which always holds the wrapped version.


See Also