DEV Community

Cover image for Uniswap V2 Decoded: A Complete Guide with Code Examples
Aditya41205
Aditya41205

Posted on

Uniswap V2 Decoded: A Complete Guide with Code Examples

Uniswap

Uniswap V2 revolutionized decentralized trading by introducing automated market makers (AMMs) that enable permissionless token swapping and liquidity provision. In this comprehensive guide, we'll decode the core mechanics of Uniswap V2 with practical code examples that you can implement today.

Understanding Uniswap V2 Architecture

Uniswap V2 implements a core/periphery architecture that separates essential functionality from user-friendly interfaces. The core contracts handle the fundamental trading logic and secure liquidity pools, while periphery contracts like the Router provide safety checks and ease-of-use features.

Core vs Periphery Design

The minimalist core contracts contain only logic strictly necessary to secure liquidity, while periphery contracts handle trader security and user experience. This modular approach allows external helpers to be improved and replaced without migrating liquidity.

// Core: Minimal, secure, immutable
contract UniswapV2Pair {
    // Essential trading logic only
}

// Periphery: User-friendly, upgradeable
contract UniswapV2Router02 {
    // Safety checks and convenience functions
}
Enter fullscreen mode Exit fullscreen mode

Setting Up Your Contract

To interact with Uniswap V2, you need to create an instance of the router contract:

pragma solidity ^0.8.0;

import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract UniswapV2Integration {
    address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    IUniswapV2Router02 public uniswapV2Router;

    constructor() {
        uniswapV2Router = IUniswapV2Router02(ROUTER);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Components:

  • Router Address: The official Ethereum mainnet address of UniswapV2Router02
  • Interface Declaration: Defines all available router functions
  • Instance Creation: Enables calling router functions in your contract

Adding Liquidity: Becoming a Liquidity Provider

The addLiquidity() function allows you to provide liquidity to trading pairs and earn fees from every trade.

Function Signature

function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);
Enter fullscreen mode Exit fullscreen mode

Parameter Breakdown

  • tokenA/tokenB: Contract addresses of the tokens to pair
  • amountADesired/amountBDesired: Your preferred deposit amounts
  • amountAMin/amountBMin: Minimum amounts (slippage protection)
  • to: Address receiving LP tokens
  • deadline: Transaction expiration timestamp

Practical Implementation

function addLiquidityToPool(
    address tokenA,
    address tokenB,
    uint256 amountA,
    uint256 amountB
) external {
    // Approve router to spend tokens
    IERC20(tokenA).approve(address(uniswapV2Router), amountA);
    IERC20(tokenB).approve(address(uniswapV2Router), amountB);

    // Add liquidity with 5% slippage tolerance
    uniswapV2Router.addLiquidity(
        tokenA,
        tokenB,
        amountA,
        amountB,
        amountA * 95 / 100,  // 5% slippage protection
        amountB * 95 / 100,  // 5% slippage protection
        msg.sender,          // LP tokens to caller
        block.timestamp + 300 // 5 minute deadline
    );
}
Enter fullscreen mode Exit fullscreen mode

Internal Mechanics: How addLiquidity() Works

Understanding the internal _addLiquidity() function reveals the sophisticated logic behind liquidity provision:

Pair Creation Check

if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
    IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
Enter fullscreen mode Exit fullscreen mode

Reserve-Based Calculations

For existing pairs, the function maintains price ratios:

amountBOptimal = amountADesired.mul(reserveB) / reserveA;

if (amountBOptimal <= amountBDesired) {
    (amountA, amountB) = (amountADesired, amountBOptimal);
} else {
    amountAOptimal = amountBDesired.mul(reserveA) / reserveB;
    (amountA, amountB) = (amountAOptimal, amountBDesired);
}
Enter fullscreen mode Exit fullscreen mode

CREATE2 Address Calculation

Uniswap V2 uses deterministic addresses for gas efficiency:

pair = address(uint(keccak256(abi.encodePacked(
    hex'ff',
    factory,
    keccak256(abi.encodePacked(token0, token1)),
    hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'
))));
Enter fullscreen mode Exit fullscreen mode

Removing Liquidity: Exiting Your Position

The removeLiquidity() function allows you to withdraw your tokens by burning LP tokens:

function removeLiquidity(
    address tokenA,
    address tokenB,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external returns (uint amountA, uint amountB);
Enter fullscreen mode Exit fullscreen mode

Implementation Example

function removeLiquidityFromPool(
    address tokenA,
    address tokenB,
    uint256 liquidityAmount
) external {
    address pair = IUniswapV2Factory(factory).getPair(tokenA, tokenB);

    // Approve router to spend LP tokens
    IERC20(pair).approve(address(uniswapV2Router), liquidityAmount);

    // Remove liquidity
    uniswapV2Router.removeLiquidity(
        tokenA,
        tokenB,
        liquidityAmount,
        0, // Accept any amount of tokenA
        0, // Accept any amount of tokenB
        msg.sender,
        block.timestamp + 300
    );
}
Enter fullscreen mode Exit fullscreen mode

Token Swapping: The Heart of DeFi Trading

The swapExactTokensForTokens() function enables precise token exchanges:

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external returns (uint[] memory amounts);
Enter fullscreen mode Exit fullscreen mode

Advanced Swap Implementation

function swapTokens(
    address tokenIn,
    address tokenOut,
    uint256 amountIn,
    uint256 slippagePercent
) external {
    // Approve router
    IERC20(tokenIn).approve(address(uniswapV2Router), amountIn);

    // Calculate minimum output with slippage
    address[] memory path = new address[](2);
    path[0] = tokenIn;
    path[1] = tokenOut;

    uint[] memory amountsOut = uniswapV2Router.getAmountsOut(amountIn, path);
    uint amountOutMin = amountsOut[1] * (100 - slippagePercent) / 100;

    // Execute swap
    uniswapV2Router.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        path,
        msg.sender,
        block.timestamp + 300
    );
}
Enter fullscreen mode Exit fullscreen mode

Multi-Hop Swapping

For tokens without direct pairs, Uniswap routes through intermediate tokens:

function multiHopSwap(uint256 amountIn) external {
    address[] memory path = new address[](3);
    path[0] = USDC_ADDRESS;  // Start with USDC
    path[1] = WETH_ADDRESS;  // Route through WETH
    path[2] = DAI_ADDRESS;   // End with DAI

    IERC20(USDC_ADDRESS).approve(address(uniswapV2Router), amountIn);

    uniswapV2Router.swapExactTokensForTokens(
        amountIn,
        0, // Calculate proper minimum in production
        path,
        msg.sender,
        block.timestamp + 300
    );
}
Enter fullscreen mode Exit fullscreen mode

LP Token Economics: Understanding Your Returns

When you provide liquidity, you receive ERC-20 LP tokens representing your pool share:

LP Token Characteristics

  • Proportional ownership of the entire pool
  • Fee accumulation from every trade (0.3% in V2)
  • Redeemable for underlying tokens plus earned fees

Profitability Factors

// Your share calculation
uint256 yourPoolShare = (yourLPTokens * 100) / totalLPSupply;
uint256 yourTokenAShare = (poolReserveA * yourPoolShare) / 100;
uint256 yourTokenBShare = (poolReserveB * yourPoolShare) / 100;
Enter fullscreen mode Exit fullscreen mode

Security Considerations and Best Practices

Always Use Slippage Protection

// Bad: No slippage protection
uniswapV2Router.swapExactTokensForTokens(amountIn, 0, path, to, deadline);

// Good: Proper slippage protection
uint256 amountOutMin = expectedAmount * 95 / 100; // 5% slippage
uniswapV2Router.swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline);
Enter fullscreen mode Exit fullscreen mode

Implement Proper Deadlines

// Bad: No deadline
uint deadline = type(uint).max;

// Good: Reasonable deadline
uint deadline = block.timestamp + 300; // 5 minutes
Enter fullscreen mode Exit fullscreen mode

Handle Approvals Securely

function safeApprove(address token, address spender, uint256 amount) internal {
    IERC20(token).approve(spender, 0); // Reset to 0 first
    IERC20(token).approve(spender, amount);
}
Enter fullscreen mode Exit fullscreen mode

Complete Integration Example

Here's a comprehensive contract that demonstrates all major Uniswap V2 interactions:

pragma solidity ^0.8.0;

import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract UniswapV2Manager {
    IUniswapV2Router02 public immutable uniswapV2Router;
    IUniswapV2Factory public immutable uniswapV2Factory;

    constructor() {
        uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
        uniswapV2Factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
    }

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountA,
        uint256 amountB,
        uint256 slippage
    ) external returns (uint256, uint256, uint256) {
        IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);
        IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);

        IERC20(tokenA).approve(address(uniswapV2Router), amountA);
        IERC20(tokenB).approve(address(uniswapV2Router), amountB);

        uint256 amountAMin = amountA * (100 - slippage) / 100;
        uint256 amountBMin = amountB * (100 - slippage) / 100;

        return uniswapV2Router.addLiquidity(
            tokenA,
            tokenB,
            amountA,
            amountB,
            amountAMin,
            amountBMin,
            msg.sender,
            block.timestamp + 300
        );
    }

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 slippage
    ) external returns (uint256, uint256) {
        address pair = uniswapV2Factory.getPair(tokenA, tokenB);
        IERC20(pair).transferFrom(msg.sender, address(this), liquidity);
        IERC20(pair).approve(address(uniswapV2Router), liquidity);

        // Get current reserves to calculate minimums
        (uint256 reserveA, uint256 reserveB,) = IUniswapV2Pair(pair).getReserves();
        uint256 totalSupply = IERC20(pair).totalSupply();

        uint256 amountAMin = (reserveA * liquidity / totalSupply) * (100 - slippage) / 100;
        uint256 amountBMin = (reserveB * liquidity / totalSupply) * (100 - slippage) / 100;

        return uniswapV2Router.removeLiquidity(
            tokenA,
            tokenB,
            liquidity,
            amountAMin,
            amountBMin,
            msg.sender,
            block.timestamp + 300
        );
    }

    function swapExactTokensForTokens(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 slippage
    ) external returns (uint256[] memory) {
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        IERC20(tokenIn).approve(address(uniswapV2Router), amountIn);

        address[] memory path = new address[](2);
        path[0] = tokenIn;
        path[1] = tokenOut;

        uint256[] memory amountsOut = uniswapV2Router.getAmountsOut(amountIn, path);
        uint256 amountOutMin = amountsOut[1] * (100 - slippage) / 100;

        return uniswapV2Router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            msg.sender,
            block.timestamp + 300
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Uniswap V2's elegant design combines simplicity with powerful functionality. By understanding the core mechanics of liquidity provision, token swapping, and the underlying mathematical models, developers can build sophisticated DeFi applications that leverage automated market makers.

The key to successful Uniswap V2 integration lies in:

  • Proper slippage protection to prevent unfavorable trades
  • Secure token approvals to maintain user fund safety
  • Efficient routing for optimal swap execution
  • Understanding LP economics for sustainable liquidity provision

Whether you're building a DEX aggregator, yield farming protocol, or simple token swap interface, Uniswap V2's battle-tested infrastructure provides the foundation for reliable decentralized trading.

Remember to always test your implementations thoroughly on testnets before deploying to mainnet, and consider the gas costs and user experience implications of your design choices.

Top comments (0)