# 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*