---
# System prepended metadata

title: Understanding Uniswap V2 Code (2/n) - Liquidity Management

---

# Understanding Uniswap V2 Code (2/n) - Liquidity Management
> Deep dive into how Uniswap V2 handles adding and removing liquidity - from ratio matching to LP token math

## TLDR
- **Add Liquidity**: Match existing ratio → Transfer tokens → Mint LP tokens proportionally
- **Remove Liquidity**: Burn LP tokens → Receive pro-rata share of pool
- **Key Math**: First LP uses sqrt(x * y), subsequent LPs use min(amount * supply/reserve)
- **Critical Detail**: MINIMUM_LIQUIDITY (1000 LP token) locked forever on first deposit

## Uniswap V2 Add Liquidity
### High-level Flow
`User deposits 2 tokens -> Receive LP tokens`

### Step 1: `addLiquidity()` - Router entry point
- Call `_addLiquidity()` to calculate actual amounts (handle ratio matching)
- Transfer both tokens to the pair contract
- Call the pair's `mint()` to create LP tokens
- Return
    - `uint amountA`: the actual amount of tokenA that was deposited into the liquidity pool
    - `uint amountB`: the actual amount of tokenB that was deposited into the liquidity pool
    - `uint liquidity`: the amount of LP tokens minted and sent to the `to` address

```solidity
// UniswapV2Rounter02.sol
function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,    // amount of tokenA user want to deposit
        uint amountBDesired,    // amount of tokenB user want to deposit
        uint amountAMin,        // min A to accept (slippage)
        uint amountBMin,        // min B to accept (slippage)
        address to,             // address to receive LP tokens
        uint deadline           // transaction deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) 
{
    // calculate the optimal amount of tokenA, tokenB to be deposited
    (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
    
    // lookup the pair contract address for tokenA-tokenB
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    
    // transfer the calculated amount of tokenA and B to the pair contract
    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);

    // mint LP tokens
    liquidity = IUniswapV2Pair(pair).mint(to);
}
```

### Step 2: `_addLiquidity()` calculate optimal amounts
- Return
    - `uint amountA`: the actual amount of tokenA that was deposited into the liquidity pool
    - `uint amountB`: the actual amount of tokenB that was deposited into the liquidity pool

```solidity
function _addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,    // amount of tokenA user want to deposit
        uint amountBDesired,    // amount of tokenB user want to deposit
        uint amountAMin,        // min A to accept (slippage)
        uint amountBMin,        // min B to accept (slippage)
) 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);
    }

    // get token reserves for tokenA and tokenB
    (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);

    // case 1: empty pool, no ratio constraint, deposit what user wants
    if (reserveA == 0 && reserveB == 0) {
        (amountA, amountB) = (amountADesired, amountBDesired);
    }
    // case 2: pool already exist, match reserve ratio
    else {
        // calculate optimal amount of B give amount A with simple proportion
        // amountBOptimal = amountA x reserveB / reserveA
        uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
        // optimal amountB is within user acceptable range
        if (amountBOptimal <= amountBDesired) {
            require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
            (amountA, amountB) = (amountADesired, amountBOptimal);
        }
        // optimal amountB is too much for user acceptable range, adjust amountA instead
        else {
            // amountAOptimal = amountB x reserveA / reserveB
            uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
            // amountAOptimal needs to lower than user acceptable range
            assert(amountAOptimal <= amountADesired);
            require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
            (amountA, amountB) = (amountAOptimal, amountBDesired);
        }
    }
}
```

### Step 3: `mint()` creates LP tokens
- Return:
    - `uint liquidity`: the amount of LP tokens minted and sent to the `to` address

```solidity
// UniswapV2Pair.sol
function mint(address to) external lock returns (uint liquidity)
{
    // get pair's old reserve
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings

    // get current balance (after user's deposits)
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(token1).balanceOf(address(this));

    // calculate actual deposit amount of both tokens
    uint amount0 = balance0.sub(_reserve0);
    uint amount1 = balance1.sub(_reserve1);

    // check if a protocol fee is charged on the liquidity being minted
    bool feeOn = _mintFee(_reserve0, _reserve1);

    // totalSupply tracks the total amount of LP tokens that have been minted
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    
    // case 1: first mint
    if (_totalSupply == 0) {
        // sqrt(amount0 x amount1) - min_liquidity
        // min_liquidity is defined as 1,000
        liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
        _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
    } 
    // case 2: subsequent mint, proportional to existing supply
    // min of: amount0 * totalSupply / reserve0, or amount1 * totalSupply / reserve1
    else {
        liquidity = Math.min(
            amount0.mul(_totalSupply) / _reserve0, 
            amount1.mul(_totalSupply) / _reserve1);
    }
    require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
    // only mint to the receiver address if liquidity > 0
    _mint(to, liquidity);

    // update the balance and reserve of token pair
    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Mint(msg.sender, amount0, amount1);
}
```

## Uniswap V2 Remove Liquidity
### High-level Flow
`User burns LP tokens -> Receive share of 2 tokens back`
### Step 1: `removeLiquidity()` - Router entry point
- Transfer LP tokens from user to the pair
- Call pair's `burn()` function
- Validate min amounts user accept to receive
- Return
    - `uint amountA`: the acutal amountA user receive
    - `uint amountB`: the acutal amountB user receive

```solidity
// UniswapV2Router02.sol
function removeLiquidity(
    address tokenA,
    address tokenB,
    uint liquidity,     // amount of LP token to burn
    uint amountAMin,    // min tokenA to accept
    uint amountBMin,    // min tokenB to accept
    address to,         // address to receive tokens
    uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
    // lookup the pair address of tokenA-tokenB
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    
    // transfer the liquidity from user to pair address
    IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
    
    // burn the LP token 
    (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);

    // order the token amounts to match the sorted token order in the pair
    (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
    (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);

    // check amount is larger than user's min acceptance amount
    require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
    require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
```

### Step 2: `burn()` burns LP tokens & returns token

```solidity
// UniswapV2Pair.sol
function burn(address to) external lock returns (uint amount0, uint amount1) 
{
    // get old reserves of the pair
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    address _token0 = token0;                                // gas savings
    address _token1 = token1;                                // gas savings
    
    // get current token balances
    uint balance0 = IERC20(_token0).balanceOf(address(this));
    uint balance1 = IERC20(_token1).balanceOf(address(this));
    uint liquidity = balanceOf[address(this)];

    bool feeOn = _mintFee(_reserve0, _reserve1);

    // track total supply of LP token
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    
    // calculate token amounts to return (pro-rata)
    // amount0 = LP token amount / LP totalSupply * balance0
    // amount1 - LP token amount / LP totalSupply * balance1
    amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
    amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
    require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

    // burn LP tokens
    _burn(address(this), liquidity);

    // transfer token0, token1 to receiver address
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);

    // get the updated balance of tokens
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));

    // update pair's balance and reserve & emit event
    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Burn(msg.sender, amount0, amount1, to);
}
```
## Key Takeways
1. LP tokens = Ownership
    - Each LP token represents a proportional share of the pool
    - When burn LP tokens, user get back the proportional share of the tokens in the pool
2. Ratio matching
    - Adding liquidity requires maintaining the reserve ratio
    - If ratio doesn't match your deposit ratio, one token gets partially refunded
3. Profit from fees
    - LPs earn 0.3% of every swap fee
    - Overtime, user share of the pool grows
    - When users remove liquidity, they get back more tokens than they deposited
4. First LP advantage & disadvantage
    - Advantage: can set the initial token pair ratio (1:1, 1:2, etc.)
    - Disadvantage: MINMUM_LIQUIDITY is locked forever


## Reference
- UniswapV2 doc: https://docs.uniswap.org/contracts/v2/overview
- UniswapV2 core repo: https://github.com/Uniswap/v2-core
- UniswapV2 periphery repo: https://github.com/Uniswap/v2-periphery

## Discussion
Found an error? Have questions? 
- Twitter: [@chloe_zhuX](https://x.com/Chloe_zhuX)
- Telegram: [@Chloe_zhu](https://t.me/chloe_zhu)
- GitHub: [@Chloezhu010](https://github.com/Chloezhu010)

---

*Last updated: Nov 2nd, 2025*
*Part of my #LearnInPublic Solidity series*
