DeFi – Uniswap V2

Here are my notes for the Uniswap V2 topic study.

Overview

Liquidity

Token x = 200
Token y = 200
l = liquidity (how big the AMM is)

How to determine the amount of token that goes in/out on a trade, is ruled by this equation:

x·y = l^2

The amount of liquidity (l) is very important, as the bigger the curve, the better price you’ll get when you do a trade, between x & y.

Contracts Uniswap V2

Factory Contract

To create a pair contract (ie ETH/USDT).

Router Contract

Instead of calling directly the pair contracts (ETH/USD, DAI/ETH, DAI/MKR etc.), where you can accidentally make a mistake, you call the Router contract. It’s an intermediate router, to safely add liquidity to the pair contracts or swap in a pair contract.

Swaps can interact with a single pair contract or with multiple pair contracts.

You can also call the Router contract to create a new pair contract, which in turn will call the Factory contract.

Swap

Math

Alice swaps dx for dy. How much amount of dy will she get?

x0·y0 = l^2

(x0+dx)·(y0-dy) = l^2

x0·y0 = (x0+dx)·(y0-dy)

Getting dy (without including swap fees):

dy = (y0 · dx) / (x0 + dx)

Fees

Swap fee charged on token in: 0 <= f <= 1.
Swap fee = f·dx , in Uniswap V2 the fee is f = 0.003

dy with fee:

dy = (dx·(1-f)·y0) / (x0 + dx·(1-f))

Swap Contract Calls

Swap WETH -> DAI

  1. The user calls Router
    swapExactTokensForTokens: try to convert all the WETH to DAI
    swapExactEthForTokens: same but ETH is not an ERC20
  2. The user calls transferFrom (WETH from the user to the pair contract)
  3. The Router contract calls swap
  4. The Router contract calls transfer (DAI from the pair contract to the user)

Multi-Hop Swap WETH -> MKR

We have: DAI/WETH and DAI/MKR

  1. The user calls Router
    swapExactTokensForTokens: try to convert all the WETH to DAI
  2. The user calls transferFrom (WETH from the user to the pair contract DAI/WETH)
  3. The Router contract calls swap DAI/WETH
  4. The Router contract transfer (DAI to the pair contract, instead of the user)
  5. The Router contract calls swap DAI/MKR
  6. The Router contract calls transfer (MKR from the pair contract to the user)

Code Walkthrough: v2-periphery (router)

  • swapExactTokensForTokens: This function lets you swap a specific amount of one token for another token, by using all the input for max output.
    • Calculate the output: getAmountsOut figure out how many tokens you’ll receive
    • Check slippage: ensures the calculated output ≥ amountOutMin
    • Transfer your tokens to the first liquidity pool
    • Execute the swap, which hops through each pool in the path, ultimately sending the output tokens to
function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        // calculates swap outputs
            // amounts[0] = input
            // amounts[last] = output
            // amounts[rest] = intermediate outputs
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(
          amounts[amounts.length - 1] >= amountOutMin,
          'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
        );
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
}
  • _swap: This function executes the token swap(s) across liquidity pools. It handles both single-hop and multi-hop swaps.
    • The tokens you’re swapping must already be in the first pool before calling this function
    • It loops through each pair of tokens in the path:
      path = [TokenA, WETH, TokenB]
      Iteration 0: TokenA → WETH (send output to WETH/TokenB pool)
      Iteration 1: WETH → TokenB (send output to final recipient)
    • The to logic:
      • Not the last swap? → Send tokens directly to the next liquidity pool (chaining)
      • Last swap? → Send tokens to the final recipient (_to)
    • This chaining is gas efficient: tokens flow pool-to-pool without touching the router.
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
        // i    | path[i]   | path[i + 1]
        // 0    | path[0]   | path[1]
        // 1    | path[1]   | path[2]
        // n-2  | path[n-2] | path[n-1]
        for (uint i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
            address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
            IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
                amount0Out, amount1Out, to, new bytes(0)
            );
        }
}

Visual Example (AI):

Path: [DAI, WETH, USDC]

[DAI/WETH Pool] --WETH--> [WETH/USDC Pool] --USDC--> [You]
    Swap #1                    Swap #2
  (to=next pool)              (to=_to)
  • getAmountsOut: This function calculates how many tokens you’ll get at each step of a swap path, before the swap actually happens. It’s a read-only preview.
// performs chained getAmountOut calculations on any number of pairs
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;
        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);
        }
}

Visual Example (AI):

Input: amountIn=100, path=[DAI, WETH, USDC]

Output: amounts = [100, 0.05, 98]
                   │     │     └── Final USDC you'll receive
                   │     └── WETH from first swap
                   └── Your DAI input
  • getReserves refers to the amount of balance of tokens that are locked inside the contract.

I created a test to get amounts out here. Adding 1 WETH, we would get that amount of DAI, and then with that amount of DAI, we would get that amount of MKR:

  • getAmountsIn: The getAmountsIn function calculates how much input token you need to receive a specific output amount when swapping through multiple liquidity pools. It works backwards from the desired output

Example: you want to receive exactly 1000 USDC at the end. How much DAI do you need to start with?

  • Path: [DAI, WETH, USDC] (3 tokens = 2 hops).
  • Pool Reserves (hypothetical):
PoolReserve InReserve Out
DAI/WETH500,000 DAI250 WETH
WETH/USDC200 WETH400,000 USDC

We know the last amount of the array [DAI, WETH, USDC]
amounts = [?, ?, 1000] // We want 1000 USDC out

I created a test to get amounts out here.

  • swap

Spot Price

The spot price is the instantaneous exchange rate between two tokens at a specific point on the curve. For a constant product AMM where x·y = k:

  • Spot price of X in terms of Y = y / x (how much Y you get per unit of X)
  • Spot price of Y in terms of X = x / y (how much X you get per unit of Y)

Mathematically, it’s the absolute value of the slope of the tangent line at that point: |dy/dx| = y/x.

Example, at point (25, 9):

  • Spot price = 9/25 = 0.36
  • This means: 1 token X ≈ 0.36 token Y at that instant

The spot price is theoretical, it’s what you’d get for an infinitely small trade. Real trades move along the curve, so larger swaps get worse rates (slippage). The tangent line visualizes this: actual trades follow the curve, not the straight line.

Execution Price

The execution price is the actual rate you get when making a real trade, it’s the total output divided by the total input:

Execution price = |Δy| / Δx
  • Spot price = rate for an infinitely small trade (tangent slope)
  • Execution price = rate for your actual trade size (secant slope)
  • You add Δx = 15 tokens X
  • You receive |Δy| ≈ 3.4 tokens Y
  • Execution price = 3.4 / 15 ≈ 0.22

But the spot price was 0.36 — you got ~40% less than the “advertised” rate!

As you move along the curve, each additional unit of X gives you less Y. The execution price is the average rate across the whole trade. Larger trades = more slippage = worse execution price.

Slippage

Difference between the price you expect to receive vs what you actually receive.

Example:
– Before swap:
– amount in: 2000 DAI
– calculated amount out: 1 ETH
price of ETH = 2000 DAI / 1 ETH = 2000 DAI
– After swap:
– amount sent: 2000 DAI
– amount received: 0.99 ETH
price of ETH = 2000 DAI / 0.99 ETH = 2000 DAI

The main cause of slippage is market movement and transaction ordering.

Create Pool

We sort the tokens by their int value, then we calculate the bytecode and use create2 to deploy the pair token. There are no constructor arguments for that contract, but there is an initialize function that we call right after deploying the contract, to initialize the token values. Finally we set getPair token for the pair, and also for the reverse pair (which is the same).

Add Liquidity

Here are the maths to calculate the total shares to mint, when a user deposits into a pool.

T= total shares
s = shares to mint
L0 = value of pool before
L1 = value of pool after

s = (L1 - L0 / L0) * T = dx / x0 = dy / y0 -->      s = (dx / x0) · T = (dy / y0) · T

The mint shares should be proportional to increase of L0 to L1.Ç
T+s / T = L1 / L0

Here are the maths to calculate how many tokens to receive (and how many shares to burn).

T = total shares
s = shares to burn
L0 = value of pool before
L1 = value of pool after

L0 - L1 = (s / T) · L0 

How many dx and dy to add?

Price after add liquidity = price before

(y0 + dy) / (x0 + dx) = y0 / x0 --> dy / dx = y0 / x0

How to calculate the value in the pool?

f(x, y) = √ x · y. --> motivation: x · y = L²

Value of the pool in terms of token x

f(x, y) = 2·x -->this comes from f(x0, y0) = x0 + (x0 / y0) · y0 = 2·x0

Code Dive

  1. addLiquidity (user -> router)
  2. getPair and createPair (router -> factory)
  3. create2 (factory –> DAI/WETH)
  4. transferFrom (from the user to the pair contract)
  5. mint (router to DAI/WETH pair contract)
  6. send shares to the user (LP from DAI/WETH-> user)
  • to: the address that will receive the pool shares
  • liquidity: amount of pool shares that were minted

Remove Liquidity

The price after removing liquidity must be equal to the price before

dy / dx = y0 / x0

Instead of minting shares, we’ll burn shares.

If we have a pool value function which is:

f(x, y) = √x·y

Then:

(L0 - L1) / L0 = dx / x0 = dy / y0
  1. Call removeLiquidity from Router
  2. The router calls transferFrom the user, to send the pool shares to the pair contract DAI/WETH (the user will have to first approve to spend the pool shares to the router)
  3. The router calls burn, to burn shares
  4. The pair contract transfers WETH and DAI back to the user

Flash Swap

If a DeFi protocol supports a flashloan, then it allows a smart contract to borrow an amount of tokens from this DeFi protocol, as long as the token is repaid in the same transaction.

Flash Swap Fee

  • borrow x0
  • repay dx1 = dx0 + fee
  • x0 = reserve before

Flash Swap Fee Equation:

x0 - dx + 0.997·dx >= x0. (reserve after >= reserve before)
fee = (3 / 997) · dx

In Uniswap, the fee is collected in the token that is coming in.

Swap dx for dy:
– amount in = dx
– amount in – fee = 0.997 · dx
– swap fee = 0.003 · dx

Flash swap:
– amount out = borrow dx0
– amount in = repay dx1 = dx0 + fee
– amount in – fee = 0.997 · dx1
– flash swap fee = 0.003 · dx1

Leave a comment