# Understanding Uniswap V2 Code (1/n) - Swap Mechanism > A deep dive into Uniswap V2's swap mechanism - from user click to AMM math ## TLDR - **Architecture**: Core (immutable protocol) + Periphery (upgradable user interface) - **Key insight**: Optimistic transfer pattern - **Math**: xy=k with 0.3% fee built into the constant product check - **Example**: Full trace of 100 token0 → 90 token1 swap with all state changes in concrete numbers ## Architecture Overview ``` ┌─────────────────────────────────────────────────────┐ │ v2-periphery (User Interaction Layer) │ │ ├── Router (SwapExactTokensForTokens, etc) │ │ ├── Library (Price oracles, multicall helpers) │ │ └── Interfaces (What users interact with) │ └─────────────────────────────────────────────────────┘ ↓ calls ┌─────────────────────────────────────────────────────┐ │ v2-core (The Protocol/AMM Layer) │ │ ├── Factory (Creates and tracks pairs) │ │ ├── Pairs (AMM logic, liquidity, swaps) │ │ └── ERC20 (LP token implementation) │ └─────────────────────────────────────────────────────┘ ``` ### Design Logic V2-Core - The Immutable Protocol - Once deployed, v2 contracts should be **immutable and unchangeable** - Because **billions of dollars depend on it working exactly the same way forever** - **One Pair Per Token Pair**: one pool = max liquidity concentration V2-Periphery - The Upgradable User Inteface Layer - Can be upgraded or replaced without touching the core protocol - Provide the public-facing functions users and apps interact with - Include the Router for swaps, and Libraries for helpers like price oracles and multicalls ## Uniswap V2 Swap Flow ### High-Level Flow `User -> Router -> Pair Contract -> Token Transfer -> Reserves Updated` ### Step 1: User Initiates Swap (Router - periphery) User calls `swapExactTokensForTokens` to swap an exact amount of token A for token B ```solidity // UniswapV2Router02.sol function swapExactTokensForTokens( uint amountIn, // amount of token user is selling uint amountOutMin, // min amount user is willing to accept (slippage protection) address[] calldata path, // token route: eg. [USDC, ETH, DAI] means USDC -> ETH -> DAI address to, // address receive the output token uint deadline // latest timestamp this tx can execute (MEV protection) ) external virtual override ensure(deadline) returns (uint[] memory amounts) { // calculate the amountOut for the multi-hop swap amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); // check the amountOut against the min amount accepted require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); // initiate the first swap TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); // handle the rest of the swap chain _swap(amounts, path, to); } ``` Router is the trusted entry point that - Validate inputs (deadline, min amounts) - Calculate optimal swap path - Handle ERC20 token transfer to the pair - Calls the pair contract's `swap()` function ### Step 2: `swapExactTokensForTokens` internally calls `getAmountsOut` and `getAmountOut` `getAmountsOut` calculate the output token amounts for a multi-hop swap path - Example: - Input: 1000 DAI, Path [DAI -> USDC -> ETH] - Output: [1000, 906, 8.28] ```solidity // UniswapV2Router02.sol function getAmountsOut( address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); amounts = new uint[](path.length); amounts[0] = amountIn; // loop through each pair in the path, and calculate the amountOut for (uint i; i < path.length - 1; i++) { (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); } } ``` `getAmountOut` calculate the output token amount for a single swap - Formule to calculate amountOut: (detailed derivation in below section) - `amountOut = (reserveOut x amountIn * 0.997) / (reserveIn + amountIn * 0.997)` ```solidity // UniswapV2Router02.sol function getAmountOut( uint amountIn, uint reserveIn, uint reserveOut ) internal pure returns (uint amountOut) { require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint amountInWithFee = amountIn.mul(997); // subtract 0.3% fee uint numerator = amountInWithFee.mul(reserveOut); uint denominator = reserveIn.mul(1000).add(amountInWithFee); amountOut = numerator / denominator; } ``` ### Step 3: `swapExactTokensForTokens` then internally calls `_swap` `_swap` executes the actual token swaps through multiple pairs in sequence ```solidity function _swap( uint[] memory amounts, // array of amountOut at each step address[] memory path, // array of token address address _to // final destination address ) internal virtual { // loop through each consecutive pair in the path for (uint i; i < path.length - 1; i++) { // extract current pair (address input, address output) = (path[i], path[i + 1]); // sort 2 tokens in canonical order and return token0 address (address token0,) = UniswapV2Library.sortTokens(input, output); // get pre-calculated amountOut for this swap uint amountOut = amounts[i + 1]; // check if amountOut is token0 or not, update the output with amountOut (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); // determine output recipient address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; // call pair contract's swap function IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) ); } } ``` ### Step 4: Pair Contract Receives Swap Call (Pair - core) `_swap()` calls `IUniswapV2Pair(...).swap()` in v2-core ```solidity // UniswapV2Pair.sol function swap( uint amount0Out, // amount of token0 to send out uint amount1Out, // amount of token1 to send out address to, // address to send the output token bytes calldata data // for flash swap ) external lock { // Step 1: Validation // At least one of the token amount is positive require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); // Get current reserves (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings // Verify requested outputs don't exceed reserves require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); // Step 2: Optimistically transfer output tokens uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors // Validate recipient: can't send to token addresses themselves address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); // Optimistically send output token first if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens // Step 3: Execute flash loan callback (if needed) if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); // Step 4: Check input tokens received 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'); // Step 5: Validate the constant product formula (k) { // scope for reserve{0,1}Adjusted, avoids stack too deep errors 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'); } // Step 6: Update reserves & Emit event _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); } ``` **Key insights about the swap mechanism** 🔄 **The Optimistic Transfer pattern** - Pair sends output tokens first (without checking if the user has already deposited enough amount) - Then checks if input tokens arrived - Why? This enables Flash Swaps - borrow funds now, use them, and repay later in the same transaction 💰 **The Constant Product Formula Check** - The 0.3% fee is built into the K check, not a separate step - `balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2)` - `balance0Adjusted = balance0 * 1000 - amount0In * 3` (0.3% fee deducted) - `balance1Adjusted = balance1 * 1000 - amount1In * 3` (0.3% fee deducted) - The math: `(reserve0 + amount0In - fee) * (reserve1 - amount0Out) >= reserve0 * reserve1` - This ensures `k` never decreases, guaranteeing the AMM formula holds ### How does the `xy = k` is used to calculate `amountOut` - Starting point: `reserveIn x reserveOut = k` - After swap: `(reserveIn + amountIn) * (reserveOut - amountOut) = k` - After apply the 0.3% fee: `(reserveIn + amountIn * 0.997) * (reserveOut - amountOut) >= reserveIn x reserveOut` - Enforce that `k` doesn't decrease to ensure the pool remains solvent and prevents mev exploitation - Solve for `amountOut` step by step based on `(reserveIn + amountIn * 0.997) * (reserveOut - amountOut) = reserveIn x reserveOut` - `reserveOut - amountOut = reserveIn x reserveOut / (reserveIn + amountIn * 0.997)` - `amountOut = reserveOut - reserveIn x reserveOut / (reserveIn + amountIn * 0.997)` - `amountOut = (reserveOut x (reserveIn + amountIn * 0.997) - reserveIn x reserveOut) / (reserveIn + amountIn * 0.997)` - `amountOut = (reserveOut x amountIn * 0.997) / (reserveIn + amountIn * 0.997)` ### Example Flow With Concrete Numbers ``` ========== Initial State - Pair contract reserves: - reserve0 (token0): 1,000 tokens - reserve1 (token1): 1,000 tokens - User balances - Has 100 token0 - Want to swap for token1 ========== Step 1: User transfers input to pair - User sends 100 token0 to pair address - token0.transfer(pair_address, 100); - State after transfer: - Pair's token0 balance: 1,000 + 100 = 1,100 - Pair's token1 balance: 1,000 (unchanged) - Pair's recorded reserves: {1,000, 1,000} (not updated yet) ========== Step 2: User calls swap() with calculated output - User calls calculate how much token1 they should get - amountOut = (amountIn x (1 - fee%)) x reserveOut / (reserveIn + amountIn x (1 - fee%)) - amountOut = (100 x 0.997) x 1000 / (1000 + 100 x 0.997) = 90 token1 - User calls - pair.swap(0, 90, user_address, ""); ========== Step 3: Pair checks - Output requirement - Check: amount0Out > 0 || amount1Out > 0 0 ❌ 90 ✅ ========== Step 4: Pair checks - Sufficient liquidity - Check: amount0Out < _reserve0 && amount1Out < _reserve1 0 < 1000 && 90 < 6000 ✅ ✅ ========== Step 5: Optimistic transfer - if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // 0 > 0? No - if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // 90 > 0? YES ✅ - Transfer happens - Pair sends 531 token1 to user address - State after transfer - Pair's token0 balance: 1,100 - Pair's token1 balance: 1,000 - 90 = 910 - User's token1 balance: 90 ========== Step 6: Pair checks input - Now pair checks if user actually paid by compare current balances vs old reserves - uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; - uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; - Calculate amount0In - balance0 = 1,100; _reserve0 = 1,000; amount0Out = 0 - balance0 > _reserve0 - amount0Out ✅ - amount0In = 1,100 - (1,000 - 0) = 100 - Calculate amount1In - balance1 = 910; _reserve1 = 1,000; amount1Out = 90 - balance1 > _reserve1 - amount1Out ❌ - amount1In = 0 - Check: amount0In > 0 || amount1In > 0 100 > 0 || 0 > 0 ✅ ❌ ========== Step 7: Validate k invariant - Calculate balance0Adjusted with 0.3% fee = balance0 x 1000 - amount0In x 3 = 1,100 x 1000 - 100 x 3 = 1,099,970 - Calculate balance1Adjusted with 0.3% fee = balance1 × 1000 - amount1In × 3 = 910 x 1,000 - 0 = 910,000 - Check k - Left side: balance0Adjusted x balance1Adjusted = 1,000,727,000,000 - Right side: reserve0 * reserve1 * 1,000,000 = 1,000,000,000,000 - Left >= Right ✅ ========== Step 8: Update reserves - New state - reserve0 = 1,100 - reserve1 = 910 ``` ## 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 1st, 2025* *Part of my #LearnInPublic Solidity series*