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)