← Back to list

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

Uniswap v2 Deep Dive: Liquidity Provision and the Art of the Perfect Choco Latte

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

6. Spoon-Fed Uniswap V2: Shares, addLiquidity/mint— Code level walkthrough

Shares: Who Owns What?

Imagine you’re starting a business with two friends. You need initial capital to get the doors open.

  • A puts in $500.
  • B puts in $300.
  • C puts in $200.

What are the shares? You don’t need a PhD in math to figure this out. It’s 50%, 30%, and 20%. It’s intuitive, common sense, and exactly how Uniswap pools work.

Take an ETH-USDT pool. Currently, it holds 10 ETH and 30,000 USDT. Various people deposited these tokens to earn a slice of the pie (trading fees). We know Uniswap v2 charges a 0.3% fee on Every swap. How do we split that fee?

Simple: By your share. If I provide 20% of the total liquidity, I take 20% of the fees. It’s only fair. But before we look at the code, let’s get the geometry straight.

The “Choco Latte” Rule: Adding and Removing Liquidity

In a Uniswap pool, swaps happen along the curve defined by $x \cdot y = k$. When you swap, you move along the curve, changing the price.

But what happens when you add or remove liquidity? Think of it like making a Choco Latte. Let’s say the perfect ratio is 1 part Chocolate to 3 parts Milk. If you want to make a larger batch while keeping the taste exactly the same, how do you add ingredients?

You must add them in that same 1:3 ratio. If you dump in too much chocolate, the “taste” (price) changes.

In Uniswap, we must provide liquidity according to the current pool ratio to avoid moving the price.

If adding liquidity moved the price, it would be chaos — people would be moving markets just by depositing funds!

  • Swap: The curve stays the same; the point moves along it (Price changes).
  • Liquidity Add/Remove: The curve itself shifts (gets bigger or smaller), but the ratio stays the same (Price stays constant).

Let’s say you have a pool with X: 20 / Y: 5. Naturally, $k = 100$ (we also express this as $L²$, it’s the same thing).

If someone adds funds while maintaining the exact ratio, the curve expands — visualize it growing outward like a red line. Conversely, if someone removes liquidity? To keep the price identical, they withdraw in the same ratio, and the curve shrinks back toward the origin.

Feeling like you almost get it, but it’s still a bit “hazy”? That’s a great start. Let’s summarize the concept:

  • Swap: The curve stays fixed; only the point on the curve moves (Price changes).
  • Liquidity Provision/Removal: The curve itself grows or shrinks while maintaining the same ratio (Price stays constant, only pool size changes).

Got it? Now that the theory is out of the way, let’s see how this is implemented in code.

Liquidity Provision/Removal — Code Level Analysis

Let’s look at the rough structure first:

  1. User → Calls addLiquidity or removeLiquidity on the Router.
  2. Router → Calls mint or burn on the Pair contract.

To avoid confusion, let’s focus purely on Provision (Add) first.

1. Liquidity Provision

1–1. addLiquidity (v2-periphery/UniswapV2Router02.sol)

You’re likely used to looking at the interface “shell” before diving into the body, so let’s look at them together.

Solidity

function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);
  • Which pool? (tokenA, B)
  • How much do I ideally want to put in? (amountDesired)
  • What is the bare minimum I’ll accept? (amountMin)
  • Who owns the LP tokens (the shares)? (to)
  • The “Hurry up” timer: (deadline)
  • Returns: The actual amount of tokens used and the number of LP tokens received.

Nothing too difficult. Now, the function body:

Solidity

function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
    (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
    liquidity = IUniswapV2Pair(pair).mint(to);
}

T1hey say good code is intuitive to read, and this is 2a prime example:

  • Calculate how much token to actually put in.
  • Find the pool address.
  • Send Token A from User → Pool.
  • Send Token B from User → Pool.
  • Call mint on the Pair.

Just like with the swap function, you’ll notice a "double-check" pattern: I calculate it myself before calling, and the actual logic verifies it once more at the end.

1) _addLiquidity

First, the Router calculates the amounts internally. Why? Because there’s a delay between when a user sees the screen and when the transaction actually executes. The state might change. So the user provides a “Best case” (desired) and a "Worst case" (min).

Solidity

function _addLiquidity( ... ) internal virtual returns (uint amountA, uint amountB) {
    // create the pair if it doesn't exist yet
    if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
        IUniswapV2Factory(factory).createPair(tokenA, tokenB);
    }

    // read currentReserve variables
    (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);

    if (reserveA == 0 && reserveB == 0) {
        // If the pool is empty...
        (amountA, amountB) = (amountADesired, amountBDesired);
    } else {
        uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
        if (amountBOptimal <= amountBDesired) {
            require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
            (amountA, amountB) = (amountADesired, amountBOptimal);
        } else {
            uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
            assert(amountAOptimal <= amountADesired);
            require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
            (amountA, amountB) = (amountAOptimal, amountBDesired);
        }
    }
}

Everything we studied3 before is paying off — the first half should look familiar.

  • Find the pair (create it if missing).
  • Read the current reserves (using getReserves, not balanceOf).

Now, look at the if statement. If liquidity is 0, the pool is empty. Does a price exist? Is there a set ratio? No. You are the King. You just put in whatever amount you want. That’s why:

(amountA, amountB) = (amountADesired, amountBDesired);

But what if a price already exists? You must calculate the current ratio and put in exactly enough. What does UniswapV2Library.quote do? It calculates how much you need to maintain that ratio.

Solidity

function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
    // ... requires ...
    amountB = amountA.mul(reserveB) / reserveA;
}

It’s the formula for: “I want to put in this much A, so how much B do I need to keep the ratio?”

In short, the logic:

  • Calculate the optimal B needed for your desired A (amountBOptimal).
  • Compare it with the B you actually brought (amountBDesired).
  • B is sufficient: Use all of A, and only the necessary “optimal” amount of B.
  • B is insufficient: You can’t use all of A. Recalculate based on using all of your B instead.

2, 3, 4) Find the address and send Token A/B to the Pair.

5) Call mint on the Pair.

Now the stage shifts from the Router to the Pair contract.

1–2. mint (v2-core/UniswapV2Pair.sol)

Solidity

function mint(address to) external returns (uint liquidity);

Q: Wait, why doesn’t this function show how much I’m sending?

A: Because we already sent the tokens before calling this! Just like in a swap, it figures it out by the difference between reserve and balanceOf.

Solidity

function mint(address to) external lock returns (uint liquidity) {
    // 1. Figure out how much token0 / token1 came in
    (uint112 _reserve0, uint112 _reserve1,) = getReserves();
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(token1).balanceOf(address(this));
    uint amount0 = balance0.sub(_reserve0);
    uint amount1 = balance1.sub(_reserve1);
    // 2. Optionally mint protocol fee
        bool feeOn = _mintFee(_reserve0, _reserve1);
        uint _totalSupply = totalSupply; 
        // 3. Compute how many LP tokens to mint
        if (_totalSupply == 0) {
            liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
           _mint(address(0), MINIMUM_LIQUIDITY); 
        } else {
            liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
        }

        // 4. Mint LP tokens
        _mint(to, liquidity);
        // 5. Update reserves
        _update(balance0, balance1, _reserve0, _reserve1);
        // ...
    }
  • Checking the deposit: As expected, it checks balanceOf - reserve. Easy.

_mintFee: This function takes a tiny slice of the accumulated fees for the protocol (if feeTo is set).

Wait… “Protocol slice”? Let’s clarify. Usually, the 0.3% fee goes entirely to the LPs. However, if feeTo is turned on, it becomes LP = 0.25% / Protocol = 0.05%. Instead of splitting it every swap, they let fees accumulate in the pool and then mint new LP tokens to the protocol to give it its “share.” This is what _mintFee does.

Calculating LP tokens: If I’m the first provider, how many LP tokens do I get? My share is 100%. But how do we express that in absolute numbers? We have to set the “standard” for everyone who follows.

Uniswap’s rule: “The total supply of LP tokens is proportional to $\sqrt{x \cdot y}$.”

If I deposit 10 and 40 ($k = 400$), then $\sqrt{400} = 20$. We mint 20 LP tokens. That’s the rule.

If you aren’t the first, you just get your fair share. “How many % did this guy add compared to the existing pool?” liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);

Q1: Why calculate based on tokens instead of $k$?

A1: Because we’d need another variable for $k$. We can just use the existing reserve variables.

Q2: Why take the min (minimum) of the two?

A2: In theory, both tokens should result in the same ratio. But in reality, due to various issues, it might be slightly off. So we are conservative and give the user the smaller amount.

  • **_mint:** The Pair contract is already an ERC20, so it just updates the balances internally.
  • **_update:** Tokens came in, so we update the state variables.

Summary

Today we looked into shares (LP tokens) and liquidity provision. It was a long read, but the core takeaway is that it’s not far from common sense.

The math behind LP minting formulas and token amount calculations might take some getting used to, but you’ve got the concept down. Next time, we’ll analyze the opposite: Liquidity Removal. See you then!


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