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.
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.
- π 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
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)
- Swap Fee: 0.3% (goes to liquidity providers)
- Fee Distribution: Proportional to LP token ownership
- No Protocol Fee: 100% of fees to LPs
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
βββββββββββββββ ββββββββββββ ββββββββββββ
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 reservesState Variables:
uint112 private reserve0; // Token0 reserves (gas optimized)
uint112 private reserve1; // Token1 reserves (gas optimized)
uint32 private blockTimestampLast; // TWAP oracle supportCreates 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
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);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).
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 shareLP Token Value:
LP_tokens = sqrt(amountA Γ amountB) // For initial deposit
LP_tokens = min(
(amountA / reserveA) Γ totalSupply,
(amountB / reserveB) Γ totalSupply
) // For subsequent deposits
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
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
);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
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup# Clone repository
git clone https://github.com/Enricrypto/Decentralised-Exchange.git
cd Decentralised-Exchange
# Install dependencies
forge install
# Build contracts
forge build# 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// 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);// 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 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 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
);// 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- 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
// 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);-
Packed Storage:
uint112 private reserve0; // Instead of uint256 uint112 private reserve1; uint32 private blockTimestampLast; // All fit in one storage slot
-
Minimal External Calls:
- Batch operations where possible
- Cache frequently accessed values
-
Efficient Math:
- Use bit shifts for multiplications by powers of 2
- Minimize division operations
-
CREATE2 Deployment:
- Deterministic addresses eliminate lookups
| 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 |
- β 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
- Sandwich Attacks - Mitigated by slippage protection
- Flash Loan Attacks - Invariant checks prevent manipulation
- Reentrancy - Guards on all state-changing functions
- Price Manipulation - Large trades have proportional impact
- LP Token Inflation - Minimum liquidity locked forever
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
- 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
- Pair contract with swaps
- Factory for pair creation
- Router with multi-hop
- LP token system
- Flash swaps (flash loans)
- Price oracle (TWAP)
- Fee switch for protocol
- Concentrated liquidity
- Gas optimizations
- L2 deployment
- Cross-chain support
Contributions welcome! Please:
- Fork the repository
- Create feature branch (
git checkout -b feature/Enhancement) - Commit changes (
git commit -m 'Add Enhancement') - Push to branch (
git push origin feature/Enhancement) - Open Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Inspired by Uniswap V2
- Built with Foundry
- Uses OpenZeppelin standards
GitHub: @Enricrypto
Project Link: https://github.com/Enricrypto/Decentralised-Exchange
β If you find this project useful, please consider giving it a star!