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