Preview mode. Module 1 is free to read. Sign in to unlock the full course.
DeFi Deep DivesModule 1: DEX ARCHITECTUREUniswap V2: The Canonical AMM
Lesson 1.1·14 min

Uniswap V2: The Canonical AMM

OVERVIEW

Uniswap V2 (launched May 2020) is the most influential DeFi protocol
ever deployed. Its design — 700 lines of Solidity, no upgrades, no
admin keys — set the template for permissionless exchange. Every
AMM that followed either copied it or diverged from it deliberately.
Understanding V2 at the bytecode level is foundational.

THE COMPLETE V2 ARCHITECTURE

Three contracts, three purposes:

  UniswapV2Factory:
    Creates and tracks Pair contracts. Stores protocol fee address.
    One deployment on mainnet. Immutable.
    Key function: createPair(address tokenA, address tokenB)
    Key storage: getPair[token0][token1] → pair address

  UniswapV2Pair:
    The core AMM. One pair per token combination.
    Holds reserves. Mints/burns LP tokens. Executes swaps.
    Implements the x*y=k invariant.
    Immutable. No owner. No admin. No upgrades.

  UniswapV2Router02:
    User-facing interface. Handles multi-hop routing.
    Manages slippage protection (amountOutMin/amountInMax).
    Converts between ETH and WETH automatically.
    NOT the same as the pair — the pair does the actual swap.

THE SWAP FUNCTION — LINE BY LINE

UniswapV2Pair.swap() is the most important function in DeFi:

  function swap(
      uint amount0Out,
      uint amount1Out,
      address to,
      bytes calldata data
  ) external lock {
      require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
      (uint112 _reserve0, uint112 _reserve1,) = getReserves();
      require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

      uint balance0;
      uint balance1;
      {
          address _token0 = token0;
          address _token1 = token1;
          require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');

          // OPTIMISTIC TRANSFER: sends output tokens BEFORE verifying input!
          if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
          if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);

          // Flash swap callback (if data provided):
          if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);

          balance0 = IERC20(_token0).balanceOf(address(this));
          balance1 = IERC20(_token1).balanceOf(address(this));
      }

      uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
      uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
      require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');

      // VERIFY K INVARIANT (with fee adjustment):
      uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
      uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
      require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2),
          'UniswapV2: K');

      _update(balance0, balance1, _reserve0, _reserve1);
      emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
  }

Critical design decisions to understand:

  OPTIMISTIC TRANSFER:
    Tokens are sent OUT before verifying tokens came IN.
    The pair checks the balance AFTER the transfer.
    This enables flash swaps: receive tokens, use them, repay.
    The K check at the end is the atomic guarantee.

  K VERIFICATION WITH FEE:
    The fee is 0.3% (3/1000). Instead of deducting fee from amountIn,
    the K check scales balances by 1000 and subtracts 3x the input.
    balance0Adjusted = balance0 * 1000 - amountIn * 3
    Requirement: balance0Adjusted * balance1Adjusted >= reserve0 * reserve1 * 1000^2
    This verifies k is maintained AFTER the fee deduction.

  REENTRANCY LOCK:
    The 'lock' modifier uses a uint private unlocked = 1; pattern.
    Prevents any reentrant call to swap() during execution.

THE PRICE ORACLE MECHANISM

V2 pairs accumulate a TWAP by storing cumulative prices:

  price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
  price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;

  UQ112x112 is a fixed-point representation (112 binary decimal places).
  The cumulative sums tick up every block by (price * elapsed_time).

  To get TWAP over a window:
  TWAP = (priceCumulative_end - priceCumulative_start) / timeElapsed

  Manipulation cost scales with window length × liquidity depth.
  Very small pools have cheap-to-manipulate TWAPs.
  Still used for low-risk applications (governance snapshots, less precise risk).

LP TOKEN MATH — FIRST DEPOSIT

On first deposit (totalSupply == 0):
    shares = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
    MINIMUM_LIQUIDITY (1000) is burned to address(0) permanently.

  Why MINIMUM_LIQUIDITY?
    Without it: an attacker deposits 1 wei → gets 1 share.
    Then donates 1e18 tokens directly to pair.
    Share price becomes 1e18 tokens per share.
    Subsequent LP deposits 2e18 tokens → gets 1 share (rounds to 1).
    Attacker owns 50% of pool via 1 share. Victim owns 50% via 1 share.
    Attack requires only 1e18 tokens of "donation" capital.

    With 1000 locked shares: the share price never reaches 0 again.
    The minimum capital required for the attack scales with MINIMUM_LIQUIDITY.

  On subsequent deposits:
    shares = min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1)
    min() ensures LP cannot claim more shares than their proportional contribution.
    Excess tokens beyond the ratio are NOT added (handled by router's optimal amounts).

FLASH SWAPS — V2'S HIDDEN FEATURE

V2's swap function accepts a data parameter. If non-empty, it calls
uniswapV2Call() on the recipient before verifying the K invariant.
This creates a flash swap: receive tokens, do anything, repay.

  interface IUniswapV2Callee {
      function uniswapV2Call(
          address sender,
          uint amount0,
          uint amount1,
          bytes calldata data
      ) external;
  }

  contract FlashSwapExample is IUniswapV2Callee {
      function initiateFlashSwap(address pair, uint wethAmount) external {
          // Request WETH, repay with USDC (cross-asset flash swap):
          bytes memory data = abi.encode(wethAmount);
          IUniswapV2Pair(pair).swap(wethAmount, 0, address(this), data);
      }

      function uniswapV2Call(address, uint amount0, uint, bytes calldata data) external {
          uint wethBorrowed = abi.decode(data, (uint));
          // Do anything with wethBorrowed WETH here...
          // Arbitrage, liquidation, whatever profitable action.

          // Repay: must return wethBorrowed * 1000/997 (+ fee) in token0
          // OR equivalent value in token1
          uint repayAmount = wethBorrowed * 1000 / 997 + 1;
          IERC20(WETH).transfer(msg.sender, repayAmount);
      }
  }

V2 PROTOCOL FEE

V2 has an optional protocol fee (fee switch). When enabled:
  → 1/6 of the 0.3% swap fee goes to the protocol (feeTo address).
  → This is implemented as minting LP tokens to feeTo, not a direct cut.
  → The mint happens on the NEXT liquidity event (deposit or withdrawal).
  → By default: disabled (feeTo = address(0)).
  → On mainnet: has always been disabled (Uniswap Labs turned it on briefly
    for governance experimentation but the DAO never voted to permanently enable).

WHY V2 IS STILL RELEVANT

Despite V3's capital efficiency superiority, V2 still processes billions
in weekly volume because:
  → Simpler integration (fungible LP tokens, simpler math).
  → More forks than any other AMM (PancakeSwap, SushiSwap, TraderJoe V1, etc.)
  → Fungible LP tokens composable with yield farms.
  → Better for long-tail assets where concentrated ranges are impractical.

KEY TAKEAWAY

V2's core innovation is the optimistic transfer + K-check pattern that
enables flash swaps. The fee is collected by adjusting the K invariant
check. MINIMUM_LIQUIDITY prevents share price manipulation. The TWAP oracle
accumulates prices over blocks for manipulation-resistant off-chain reads.
V2 remains the most-forked DeFi primitive in existence.

Module 1: DEX ARCHITECTURE

From Constant Product to Concentrated Liquidity

0/4 lessons0%

Lessons

Major events are on Discord

Join for live sessions, announcements, and event rooms while you learn.

Join Discord →