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