Skip to content

A Solidity-based decentralized exchange (DEX) for trustless token swaps and liquidity provision. Includes Factory, Pair, and Router contracts inspired by Uniswap V2. Supports ERC-20 tokens with slippage protection and gas optimisations. A solid foundation for building DeFi apps.

Notifications You must be signed in to change notification settings

Enricrypto/Decentralised-Exchange

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

37 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Decentralized Exchange (DEX) - Uniswap V2 Architecture

A production-ready Solidity implementation of an Automated Market Maker (AMM) DEX featuring Uniswap V2 architecture. Includes liquidity pools, multi-hop routing, and constant product market making with comprehensive security patterns and gas optimizations.

Solidity Foundry License: MIT GitHub last commit

🌟 Overview

This DEX implements the proven Uniswap V2 architecture with automated market-making capabilities. Users can create liquidity pools, provide liquidity to earn fees, and swap tokens through an intuitive router interface with multi-hop support for complex trading paths.

Key Features

  • 🏊 Liquidity Pools - Create and manage token pair pools
  • πŸ’± Token Swaps - Automated market-making with 0.3% fee
  • 🌊 Multi-Hop Routing - Complex swap paths through multiple pools
  • πŸ’° LP Tokens - ERC-20 tokens representing pool ownership
  • πŸ›‘οΈ Slippage Protection - User-defined minimum output amounts
  • ⚑ Gas Optimized - Efficient storage and computation patterns
  • 🎯 Deterministic Deployment - CREATE2 for predictable addresses

πŸ“Š AMM Mechanism

Constant Product Formula

The DEX uses the constant product market maker (x Γ— y = k):

Reserve_A Γ— Reserve_B = k (constant)

Where:
- Reserve_A: Amount of Token A in pool
- Reserve_B: Amount of Token B in pool
- k: Constant product (invariant)

Example Swap:

Initial: 100 ETH Γ— 200,000 USDC = 20,000,000
User swaps: 10 ETH
New reserves: 110 ETH Γ— ~181,818 USDC = 20,000,000
User receives: ~18,182 USDC (minus 0.3% fee)

Fee Structure

  • Swap Fee: 0.3% (goes to liquidity providers)
  • Fee Distribution: Proportional to LP token ownership
  • No Protocol Fee: 100% of fees to LPs

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Router.sol                        β”‚
β”‚         (User-Facing Interface)                     β”‚
β”‚  β€’ Add/Remove Liquidity                             β”‚
β”‚  β€’ Token Swaps                                      β”‚
β”‚  β€’ Multi-Hop Routing                                β”‚
β”‚  β€’ Slippage Protection                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
        β”‚ Factory.sol  β”‚
        β”‚              β”‚
        β”‚ β€’ Create     β”‚
        β”‚   Pairs      β”‚
        β”‚ β€’ Track      β”‚
        β”‚   All Pools  β”‚
        β”‚ β€’ CREATE2    β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  Pair.sol   β”‚  β”‚ Pair.sol β”‚  β”‚ Pair.sol β”‚
        β”‚  ETH/USDC   β”‚  β”‚ ETH/DAI  β”‚  β”‚ DAI/USDC β”‚
        β”‚             β”‚  β”‚          β”‚  β”‚          β”‚
        β”‚ β€’ Reserves  β”‚  β”‚ β€’ Swaps  β”‚  β”‚ β€’ LP     β”‚
        β”‚ β€’ Swaps     β”‚  β”‚ β€’ LP     β”‚  β”‚   Tokens β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“‚ Core Contracts

1. Pair.sol - Liquidity Pool

The fundamental building block representing a liquidity pool for two tokens.

Key Responsibilities:

  • Manages reserves for token pairs
  • Handles token swaps with fee calculation
  • Mints/burns LP tokens for liquidity providers
  • Maintains constant product invariant

Core Functions:

function swap(
    uint amount0Out,
    uint amount1Out,
    address to
) external;

function mint(address to) external returns (uint liquidity);

function burn(address to) external returns (uint amount0, uint amount1);

function sync() external; // Re-sync reserves

State Variables:

uint112 private reserve0;  // Token0 reserves (gas optimized)
uint112 private reserve1;  // Token1 reserves (gas optimized)
uint32 private blockTimestampLast;  // TWAP oracle support

2. Factory.sol - Pair Factory

Creates and tracks all liquidity pairs with deterministic addressing.

Key Features:

  • Ensures one unique pair per token combination
  • Uses CREATE2 for predictable addresses
  • Maintains registry of all pairs

Core Functions:

function createPair(
    address tokenA,
    address tokenB
) external returns (address pair);

function getPair(
    address tokenA,
    address tokenB
) external view returns (address pair);

CREATE2 Benefits:

  • Address predictability before deployment
  • Same pair address across different chains
  • No need to query factory for pair address

3. Router.sol - User Interface

Simplifies interactions with pairs and implements advanced features.

Key Features:

  • User-friendly liquidity management
  • Slippage protection on all operations
  • Multi-hop swap routing
  • Optimal liquidity calculations

Core Functions:

Liquidity Management:

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);

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

Token Swaps:

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

function swapTokensForExactTokens(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    address to,
    uint deadline
) external returns (uint[] memory amounts);

Helper Functions:

function getAmountOut(
    uint amountIn,
    uint reserveIn,
    uint reserveOut
) public pure returns (uint amountOut);

function getAmountsOut(
    uint amountIn,
    address[] memory path
) public view returns (uint[] memory amounts);

πŸ’‘ Key Features Explained

1. Constant Product AMM

Formula: x Γ— y = k

How It Works:

Initial Pool: 100 ETH Γ— 200,000 USDC

Swap 10 ETH for USDC:
1. k = 100 Γ— 200,000 = 20,000,000
2. New ETH reserve = 110
3. New USDC reserve = k / 110 = 181,818.18
4. USDC out = 200,000 - 181,818 = 18,182
5. Apply 0.3% fee
6. User receives: ~18,127 USDC

Price Impact: Larger trades relative to pool size have greater price impact (slippage).


2. Liquidity Provision

Adding Liquidity:

// 1. Approve tokens
tokenA.approve(router, amountA);
tokenB.approve(router, amountB);

// 2. Add liquidity
router.addLiquidity(
    tokenA,
    tokenB,
    amountA,  // 10 ETH
    amountB,  // 20,000 USDC
    minA,     // Slippage protection
    minB,     // Slippage protection
    msg.sender,
    deadline
);

// 3. Receive LP tokens representing pool share

LP Token Value:

LP_tokens = sqrt(amountA Γ— amountB)  // For initial deposit
LP_tokens = min(
    (amountA / reserveA) Γ— totalSupply,
    (amountB / reserveB) Γ— totalSupply
)  // For subsequent deposits

3. Multi-Hop Swaps

Example: Swap USDT β†’ USDC (no direct pair)

Route: USDT β†’ ETH β†’ USDC

address[] memory path = new address[](3);
path[0] = USDT;
path[1] = WETH;
path[2] = USDC;

router.swapExactTokensForTokens(
    1000e6,      // 1000 USDT in
    minUSDCOut,  // Minimum USDC out
    path,
    msg.sender,
    deadline
);

Benefits:

  • Access to indirect trading pairs
  • Capital efficiency (fewer pools needed)
  • Automatic route finding

4. Slippage Protection

Why It Matters: Large trades move prices. Protection ensures fair execution.

Implementation:

// Example: Swap with max 1% slippage
uint expectedOut = getAmountOut(amountIn, reserveIn, reserveOut);
uint minOut = expectedOut * 99 / 100;  // 1% slippage tolerance

router.swapExactTokensForTokens(
    amountIn,
    minOut,  // ← Reverts if actual output < minOut
    path,
    msg.sender,
    deadline
);

5. Deterministic Pair Addresses (CREATE2)

Benefits:

// Calculate pair address off-chain
address predictedPair = computePairAddress(tokenA, tokenB);

// Create pair
factory.createPair(tokenA, tokenB);

// Address matches prediction!
assert(factory.getPair(tokenA, tokenB) == predictedPair);

Use Cases:

  • Frontend can display pair info before creation
  • Cross-chain address consistency
  • Reduced RPC calls

πŸš€ Getting Started

Prerequisites

# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup

Installation

# Clone repository
git clone https://github.com/Enricrypto/Decentralised-Exchange.git
cd Decentralised-Exchange

# Install dependencies
forge install

# Build contracts
forge build

Testing

# Run all tests
forge test

# Run with verbosity
forge test -vvv

# Run specific test file
forge test --match-path test/Router.t.sol

# Coverage
forge coverage

# Gas report
forge test --gas-report

πŸ“– Usage Examples

Deploy Contracts

// 1. Deploy Factory
Factory factory = new Factory();

// 2. Deploy Router
Router router = new Router(address(factory));

// 3. Create pair
address pairAddress = factory.createPair(tokenA, tokenB);

Add Liquidity (First Time)

// 1. Approve tokens
IERC20(tokenA).approve(address(router), 100 ether);
IERC20(tokenB).approve(address(router), 200000 ether);

// 2. Add liquidity
(uint amountA, uint amountB, uint liquidity) = router.addLiquidity(
    tokenA,
    tokenB,
    100 ether,      // Desired amount A
    200000 ether,   // Desired amount B
    95 ether,       // Min amount A (5% slippage)
    190000 ether,   // Min amount B (5% slippage)
    msg.sender,
    block.timestamp + 300  // 5 min deadline
);

// 3. Receive LP tokens
// liquidity = sqrt(100 * 200000) β‰ˆ 4,472 LP tokens

Swap Tokens (Single Hop)

// Swap 10 tokenA for tokenB
IERC20(tokenA).approve(address(router), 10 ether);

address[] memory path = new address[](2);
path[0] = tokenA;
path[1] = tokenB;

uint[] memory amounts = router.swapExactTokensForTokens(
    10 ether,       // Amount in
    18000 ether,    // Min amount out
    path,
    msg.sender,
    block.timestamp + 300
);

// amounts[0] = 10 ether (amount in)
// amounts[1] = ~19,636 ether (amount out, minus fees)

Swap Tokens (Multi-Hop)

// Swap USDT β†’ WETH β†’ DAI
address[] memory path = new address[](3);
path[0] = USDT;
path[1] = WETH;
path[2] = DAI;

router.swapExactTokensForTokens(
    1000e6,         // 1000 USDT
    minDAIOut,      // Calculated minimum
    path,
    msg.sender,
    deadline
);

Remove Liquidity

// 1. Approve LP tokens
Pair pair = Pair(factory.getPair(tokenA, tokenB));
pair.approve(address(router), lpAmount);

// 2. Remove liquidity
(uint amountA, uint amountB) = router.removeLiquidity(
    tokenA,
    tokenB,
    lpAmount,       // LP tokens to burn
    minAmountA,     // Slippage protection
    minAmountB,     // Slippage protection
    msg.sender,
    deadline
);

// 3. Receive underlying tokens proportional to pool share

πŸ§ͺ Testing Strategy

Test Coverage

  • Unit Tests: Individual function testing
  • Integration Tests: Multi-contract interactions
  • Fuzz Tests: Random input testing
  • Invariant Tests: Mathematical guarantees
  • Scenario Tests: Real-world user flows

Key Invariants

// 1. Constant Product (after fee)
assert(reserve0 * reserve1 >= k);

// 2. LP Token Supply
assert(totalSupply <= sqrt(reserve0 * reserve1));

// 3. No Value Extraction
assert(userBalanceAfter <= userBalanceBefore + fairOutput);

// 4. Reserve Sync
assert(pair.balance(token0) >= reserve0);
assert(pair.balance(token1) >= reserve1);

πŸ“Š Gas Optimizations

Implemented Optimizations

  1. Packed Storage:

    uint112 private reserve0;  // Instead of uint256
    uint112 private reserve1;
    uint32 private blockTimestampLast;
    // All fit in one storage slot
  2. Minimal External Calls:

    • Batch operations where possible
    • Cache frequently accessed values
  3. Efficient Math:

    • Use bit shifts for multiplications by powers of 2
    • Minimize division operations
  4. CREATE2 Deployment:

    • Deterministic addresses eliminate lookups

Gas Benchmarks

Operation Gas Cost (approx)
Create Pair ~2.5M gas
Add Liquidity (first) ~180k gas
Add Liquidity (subsequent) ~130k gas
Swap (single hop) ~90k gas
Swap (multi-hop, 3 pairs) ~250k gas
Remove Liquidity ~120k gas

πŸ”’ Security Features

Implemented Protections

  • βœ… Reentrancy Guards on critical functions
  • βœ… Deadline Checks prevent stale transactions
  • βœ… Slippage Protection on all user operations
  • βœ… Overflow Protection (Solidity 0.8+)
  • βœ… Minimum Liquidity Lock (first deposit)
  • βœ… Balance Verification before swaps
  • βœ… Invariant Checks after operations

Attack Vectors Considered

  1. Sandwich Attacks - Mitigated by slippage protection
  2. Flash Loan Attacks - Invariant checks prevent manipulation
  3. Reentrancy - Guards on all state-changing functions
  4. Price Manipulation - Large trades have proportional impact
  5. LP Token Inflation - Minimum liquidity locked forever

Audit Status

⚠️ Not professionally audited. This is an educational/portfolio project. Do not use in production with real funds without a security audit.


πŸ“ Project Structure

decentralised-exchange/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ Factory.sol              # Pair creation & registry
β”‚   β”œβ”€β”€ Pair.sol                 # Liquidity pool logic
β”‚   β”œβ”€β”€ Router.sol               # User-facing interface
β”‚   └── interfaces/
β”‚       β”œβ”€β”€ IFactory.sol
β”‚       β”œβ”€β”€ IPair.sol
β”‚       └── IRouter.sol
β”œβ”€β”€ test/
β”‚   β”œβ”€β”€ Factory.t.sol
β”‚   β”œβ”€β”€ Pair.t.sol
β”‚   β”œβ”€β”€ Router.t.sol
β”‚   └── Integration.t.sol
β”œβ”€β”€ script/
β”‚   └── Deploy.s.sol
└── README.md

πŸŽ“ Learning Resources

Understanding AMMs

  • Constant Product Formula: x Γ— y = k
  • Impermanent Loss: Risk for liquidity providers
  • Arbitrage: Keeps prices aligned with other exchanges
  • Slippage: Price impact of trade size

Recommended Reading


🚧 Roadmap

Phase 1: Core AMM βœ…

  • Pair contract with swaps
  • Factory for pair creation
  • Router with multi-hop
  • LP token system

Phase 2: Advanced Features (Planned)

  • Flash swaps (flash loans)
  • Price oracle (TWAP)
  • Fee switch for protocol
  • Concentrated liquidity

Phase 3: Optimization (Planned)

  • Gas optimizations
  • L2 deployment
  • Cross-chain support

🀝 Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create feature branch (git checkout -b feature/Enhancement)
  3. Commit changes (git commit -m 'Add Enhancement')
  4. Push to branch (git push origin feature/Enhancement)
  5. Open Pull Request

πŸ“ License

This project is licensed under the MIT License - see the LICENSE file for details.


πŸ™ Acknowledgments


πŸ“§ Contact

GitHub: @Enricrypto

Project Link: https://github.com/Enricrypto/Decentralised-Exchange


⭐ If you find this project useful, please consider giving it a star!

About

A Solidity-based decentralized exchange (DEX) for trustless token swaps and liquidity provision. Includes Factory, Pair, and Router contracts inspired by Uniswap V2. Supports ERC-20 tokens with slippage protection and gas optimisations. A solid foundation for building DeFi apps.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published