Decentralized Exchange (DEX) is a core component of the DeFi ecosystem. Implementing a basic DEX requires understanding Automatic Market Maker (AMM) mechanisms, liquidity pools, price calculations, and other core concepts.
1. DEX Core Concepts
solidity/* DEX core components: 1. Liquidity Pool - Contains reserves of two tokens - Uses constant product formula: x * y = k 2. Automatic Market Maker (AMM) - No order book needed - Algorithmic automatic pricing - Liquidity providers earn fees 3. Price Calculation - Based on reserve ratio - Considers slippage and fees 4. Liquidity Tokens (LP Token) - Represents liquidity share - Redeemable for underlying assets */
2. Basic AMM Implementation
soliditycontract BasicAMM { // Token interfaces IERC20 public token0; IERC20 public token1; // Reserves uint256 public reserve0; uint256 public reserve1; // Total liquidity tokens uint256 public totalSupply; mapping(address => uint256) public balanceOf; // Fee (0.3% = 30 / 10000) uint256 public constant FEE = 30; uint256 public constant FEE_DENOMINATOR = 10000; // Minimum liquidity (prevent division by zero) uint256 public constant MINIMUM_LIQUIDITY = 1000; // Events event Mint(address indexed sender, uint256 amount0, uint256 amount1); event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); event Swap( address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to ); event Sync(uint256 reserve0, uint256 reserve1); constructor(address _token0, address _token1) { token0 = IERC20(_token0); token1 = IERC20(_token1); } // Add liquidity function addLiquidity( uint256 _amount0Desired, uint256 _amount1Desired, uint256 _amount0Min, uint256 _amount1Min, address _to ) external returns (uint256 liquidity) { // Calculate amounts to add (uint256 amount0, uint256 amount1) = _calculateLiquidity( _amount0Desired, _amount1Desired, _amount0Min, _amount1Min ); // Transfer tokens token0.transferFrom(msg.sender, address(this), amount0); token1.transferFrom(msg.sender, address(this), amount1); // Calculate liquidity tokens if (totalSupply == 0) { liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY; _mint(address(0), MINIMUM_LIQUIDITY); // Permanently lock } else { liquidity = min( (amount0 * totalSupply) / reserve0, (amount1 * totalSupply) / reserve1 ); } require(liquidity > 0, "Insufficient liquidity minted"); _mint(_to, liquidity); // Update reserves _updateReserves(); emit Mint(msg.sender, amount0, amount1); } // Remove liquidity function removeLiquidity( uint256 _liquidity, uint256 _amount0Min, uint256 _amount1Min, address _to ) external returns (uint256 amount0, uint256 amount1) { // Calculate redeemable amounts uint256 balance0 = token0.balanceOf(address(this)); uint256 balance1 = token1.balanceOf(address(this)); amount0 = (_liquidity * balance0) / totalSupply; amount1 = (_liquidity * balance1) / totalSupply; require(amount0 >= _amount0Min, "Insufficient amount0"); require(amount1 >= _amount1Min, "Insufficient amount1"); // Burn liquidity tokens _burn(msg.sender, _liquidity); // Transfer tokens token0.transfer(_to, amount0); token1.transfer(_to, amount1); // Update reserves _updateReserves(); emit Burn(msg.sender, amount0, amount1, _to); } // Swap tokens (token0 -> token1) function swap0For1( uint256 _amount0In, uint256 _amount1OutMin, address _to ) external returns (uint256 amount1Out) { require(_amount0In > 0, "Insufficient input amount"); // Calculate output amount (considering fees) amount1Out = getAmountOut(_amount0In, reserve0, reserve1); require(amount1Out >= _amount1OutMin, "Insufficient output amount"); // Transfer tokens token0.transferFrom(msg.sender, address(this), _amount0In); token1.transfer(_to, amount1Out); // Update reserves _updateReserves(); emit Swap(msg.sender, _amount0In, 0, 0, amount1Out, _to); } // Swap tokens (token1 -> token0) function swap1For0( uint256 _amount1In, uint256 _amount0OutMin, address _to ) external returns (uint256 amount0Out) { require(_amount1In > 0, "Insufficient input amount"); amount0Out = getAmountOut(_amount1In, reserve1, reserve0); require(amount0Out >= _amount0OutMin, "Insufficient output amount"); token1.transferFrom(msg.sender, address(this), _amount1In); token0.transfer(_to, amount0Out); _updateReserves(); emit Swap(msg.sender, 0, _amount1In, amount0Out, 0, _to); } // Calculate output amount (constant product formula) function getAmountOut( uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut ) public pure returns (uint256 amountOut) { require(_amountIn > 0, "Insufficient input amount"); require(_reserveIn > 0 && _reserveOut > 0, "Insufficient liquidity"); uint256 amountInWithFee = _amountIn * (FEE_DENOMINATOR - FEE); uint256 numerator = amountInWithFee * _reserveOut; uint256 denominator = (_reserveIn * FEE_DENOMINATOR) + amountInWithFee; amountOut = numerator / denominator; } // Calculate input amount function getAmountIn( uint256 _amountOut, uint256 _reserveIn, uint256 _reserveOut ) public pure returns (uint256 amountIn) { require(_amountOut > 0, "Insufficient output amount"); require(_reserveIn > 0 && _reserveOut > 0, "Insufficient liquidity"); uint256 numerator = _reserveIn * _amountOut * FEE_DENOMINATOR; uint256 denominator = (_reserveOut - _amountOut) * (FEE_DENOMINATOR - FEE); amountIn = (numerator / denominator) + 1; } // Calculate liquidity function _calculateLiquidity( uint256 _amount0Desired, uint256 _amount1Desired, uint256 _amount0Min, uint256 _amount1Min ) internal view returns (uint256 amount0, uint256 amount1) { if (reserve0 == 0 && reserve1 == 0) { // First liquidity addition (amount0, amount1) = (_amount0Desired, _amount1Desired); } else { // Add proportionally uint256 amount1Optimal = (_amount0Desired * reserve1) / reserve0; if (amount1Optimal <= _amount1Desired) { require(amount1Optimal >= _amount1Min, "Insufficient amount1"); (amount0, amount1) = (_amount0Desired, amount1Optimal); } else { uint256 amount0Optimal = (_amount1Desired * reserve0) / reserve1; assert(amount0Optimal <= _amount0Desired); require(amount0Optimal >= _amount0Min, "Insufficient amount0"); (amount0, amount1) = (amount0Optimal, _amount1Desired); } } } // Update reserves function _updateReserves() internal { reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Sync(reserve0, reserve1); } // Liquidity token operations function _mint(address _to, uint256 _amount) internal { totalSupply += _amount; balanceOf[_to] += _amount; } function _burn(address _from, uint256 _amount) internal { balanceOf[_from] -= _amount; totalSupply -= _amount; } // Math functions function sqrt(uint256 y) internal pure returns (uint256 z) { if (y > 3) { z = y; uint256 x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } } interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); }
3. Router Contract
soliditycontract Router { address public factory; constructor(address _factory) { factory = _factory; } // Add liquidity function addLiquidity( address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity) { require(block.timestamp <= _deadline, "Expired"); // Get or create trading pair address pair = Factory(factory).getPair(_tokenA, _tokenB); if (pair == address(0)) { pair = Factory(factory).createPair(_tokenA, _tokenB); } // Transfer tokens to pair IERC20(_tokenA).transferFrom(msg.sender, pair, _amountADesired); IERC20(_tokenB).transferFrom(msg.sender, pair, _amountBDesired); // Add liquidity liquidity = BasicAMM(pair).addLiquidity( _amountADesired, _amountBDesired, _amountAMin, _amountBMin, _to ); // Return actual amounts added amountA = _amountADesired; amountB = _amountBDesired; } // Remove liquidity function removeLiquidity( address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline ) external returns (uint256 amountA, uint256 amountB) { require(block.timestamp <= _deadline, "Expired"); address pair = Factory(factory).getPair(_tokenA, _tokenB); require(pair != address(0), "Pair does not exist"); // Transfer LP tokens to pair IERC20(pair).transferFrom(msg.sender, pair, _liquidity); // Remove liquidity (amountA, amountB) = BasicAMM(pair).removeLiquidity( _liquidity, _amountAMin, _amountBMin, _to ); } // Swap tokens (exact input) function swapExactTokensForTokens( uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline ) external returns (uint256[] memory amounts) { require(block.timestamp <= _deadline, "Expired"); require(_path.length >= 2, "Invalid path"); amounts = getAmountsOut(_amountIn, _path); require(amounts[amounts.length - 1] >= _amountOutMin, "Insufficient output"); // Transfer first token to first pair IERC20(_path[0]).transferFrom( msg.sender, Factory(factory).getPair(_path[0], _path[1]), amounts[0] ); // Execute swap _swap(amounts, _path, _to); } // Swap tokens (exact output) function swapTokensForExactTokens( uint256 _amountOut, uint256 _amountInMax, address[] calldata _path, address _to, uint256 _deadline ) external returns (uint256[] memory amounts) { require(block.timestamp <= _deadline, "Expired"); require(_path.length >= 2, "Invalid path"); amounts = getAmountsIn(_amountOut, _path); require(amounts[0] <= _amountInMax, "Excessive input"); IERC20(_path[0]).transferFrom( msg.sender, Factory(factory).getPair(_path[0], _path[1]), amounts[0] ); _swap(amounts, _path, _to); } // Internal swap function function _swap( uint256[] memory _amounts, address[] memory _path, address _to ) internal { for (uint i = 0; i < _path.length - 1; i++) { (address input, address output) = (_path[i], _path[i + 1]); address pair = Factory(factory).getPair(input, output); uint256 amountOut = _amounts[i + 1]; (uint256 amount0Out, uint256 amount1Out) = input < output ? (uint256(0), amountOut) : (amountOut, uint256(0)); address to = i < _path.length - 2 ? Factory(factory).getPair(output, _path[i + 2]) : _to; BasicAMM(pair).swap(amount0Out, amount1Out, to); } } // Calculate output amounts (multi-hop) function getAmountsOut( uint256 _amountIn, address[] memory _path ) public view returns (uint256[] memory amounts) { require(_path.length >= 2, "Invalid path"); amounts = new uint256[](_path.length); amounts[0] = _amountIn; for (uint i = 0; i < _path.length - 1; i++) { address pair = Factory(factory).getPair(_path[i], _path[i + 1]); require(pair != address(0), "Pair does not exist"); (uint256 reserveIn, uint256 reserveOut) = getReserves(pair, _path[i], _path[i + 1]); amounts[i + 1] = BasicAMM(pair).getAmountOut(amounts[i], reserveIn, reserveOut); } } // Calculate input amounts (multi-hop) function getAmountsIn( uint256 _amountOut, address[] memory _path ) public view returns (uint256[] memory amounts) { require(_path.length >= 2, "Invalid path"); amounts = new uint256[](_path.length); amounts[amounts.length - 1] = _amountOut; for (uint i = _path.length - 1; i > 0; i--) { address pair = Factory(factory).getPair(_path[i - 1], _path[i]); require(pair != address(0), "Pair does not exist"); (uint256 reserveIn, uint256 reserveOut) = getReserves(pair, _path[i - 1], _path[i]); amounts[i - 1] = BasicAMM(pair).getAmountIn(amounts[i], reserveIn, reserveOut); } } // Get reserves function getReserves( address _pair, address _tokenA, address _tokenB ) internal view returns (uint256 reserveA, uint256 reserveB) { (uint256 reserve0, uint256 reserve1) = BasicAMM(_pair).getReserves(); (reserveA, reserveB) = _tokenA < _tokenB ? (reserve0, reserve1) : (reserve1, reserve0); } } // Factory contract contract Factory { mapping(address => mapping(address => address)) public getPair; address[] public allPairs; event PairCreated(address indexed token0, address indexed token1, address pair, uint256); function createPair(address _tokenA, address _tokenB) external returns (address pair) { require(_tokenA != _tokenB, "Identical addresses"); (address token0, address token1) = _tokenA < _tokenB ? (_tokenA, _tokenB) : (_tokenB, _tokenA); require(token0 != address(0), "Zero address"); require(getPair[token0][token1] == address(0), "Pair exists"); // Create pair contract (simplified, actual use create2) bytes memory bytecode = type(BasicAMM).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) } BasicAMM(pair).initialize(token0, token1); getPair[token0][token1] = pair; getPair[token1][token0] = pair; allPairs.push(pair); emit PairCreated(token0, token1, pair, allPairs.length); } }
4. Price Oracle
soliditycontract PriceOracle { // Cumulative prices uint256 public price0CumulativeLast; uint256 public price1CumulativeLast; // Last update time uint32 public blockTimestampLast; // Reserves uint112 public reserve0; uint112 public reserve1; // Update cumulative prices function _update( uint256 _balance0, uint256 _balance1, uint112 _reserve0, uint112 _reserve1 ) internal { require( _balance0 <= type(uint112).max && _balance1 <= type(uint112).max, "Overflow" ); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // Cumulative price = price * time price0CumulativeLast += uint256(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint256(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(_balance0); reserve1 = uint112(_balance1); blockTimestampLast = blockTimestamp; } // Get current price function getCurrentPrice() external view returns (uint256 price0, uint256 price1) { price0 = (uint256(reserve1) * 1e18) / reserve0; price1 = (uint256(reserve0) * 1e18) / reserve1; } // Calculate TWAP (Time-Weighted Average Price) function consult( address _token, uint256 _amountIn ) external view returns (uint256 amountOut) { // Simplified TWAP calculation if (_token == token0) { amountOut = (_amountIn * reserve1) / reserve0; } else { amountOut = (_amountIn * reserve0) / reserve1; } } } // UQ112x112 library (fixed-point arithmetic) library UQ112x112 { uint224 constant Q112 = 2**112; function encode(uint112 y) internal pure returns (uint224 z) { z = uint224(y) * Q112; } function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { z = x / uint224(y); } }
5. Flash Loan Functionality
soliditycontract FlashSwap { // Flash loan callback interface interface IFlashSwapCallee { function uniswapV2Call( address sender, uint256 amount0, uint256 amount1, bytes calldata data ) external; } // Execute flash loan function swap( uint256 _amount0Out, uint256 _amount1Out, address _to, bytes calldata _data ) external { require(_amount0Out > 0 || _amount1Out > 0, "Insufficient output"); // Transfer to recipient if (_amount0Out > 0) token0.transfer(_to, _amount0Out); if (_amount1Out > 0) token1.transfer(_to, _amount1Out); // If there's data, execute callback if (_data.length > 0) { IFlashSwapCallee(_to).uniswapV2Call( msg.sender, _amount0Out, _amount1Out, _data ); } // Verify repayment uint256 balance0 = token0.balanceOf(address(this)); uint256 balance1 = token1.balanceOf(address(this)); uint256 amount0In = balance0 > reserve0 - _amount0Out ? balance0 - (reserve0 - _amount0Out) : 0; uint256 amount1In = balance1 > reserve1 - _amount1Out ? balance1 - (reserve1 - _amount1Out) : 0; require(amount0In > 0 || amount1In > 0, "Insufficient input"); // Verify constant product uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3; uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3; require( balance0Adjusted * balance1Adjusted >= uint256(reserve0) * reserve1 * 1000**2, "K" ); _update(balance0, balance1, reserve0, reserve1); } } // Flash loan arbitrage example contract Arbitrageur is IFlashSwapCallee { address public owner; constructor() { owner = msg.sender; } function executeArbitrage( address _pair, uint256 _amount0, uint256 _amount1, bytes calldata _data ) external { // Trigger flash loan FlashSwap(_pair).swap(_amount0, _amount1, address(this), _data); } function uniswapV2Call( address _sender, uint256 _amount0, uint256 _amount1, bytes calldata _data ) external override { // Decode data (address targetPair, uint256 repayAmount) = abi.decode(_data, (address, uint256)); // Execute arbitrage logic // 1. Sell on other exchange // 2. Get profit // 3. Repay flash loan // Repay loan (plus fee) if (_amount0 > 0) { IERC20(token0).transfer(msg.sender, repayAmount); } if (_amount1 > 0) { IERC20(token1).transfer(msg.sender, repayAmount); } // Send profit to owner // ... } }
6. DEX Security Considerations
soliditycontract DEXSecurity { /* DEX security points: 1. Reentrancy attack protection - Use Checks-Effects-Interactions pattern - Use ReentrancyGuard 2. Price manipulation protection - Use TWAP oracle - Set maximum slippage - Transaction limits 3. Impermanent loss management - Liquidity provider education - Incentive design 4. Front-running protection - Commit-reveal pattern - Batch auction 5. Overflow checks - Solidity 0.8.0+ automatic checks - Or use SafeMath */ } // Security-enhanced AMM contract SecureAMM is ReentrancyGuard { // Maximum slippage (1%) uint256 public constant MAX_SLIPPAGE = 100; uint256 public constant SLIPPAGE_DENOMINATOR = 10000; // Transaction limits uint256 public maxSwapAmount; // Pause functionality bool public paused; address public guardian; modifier whenNotPaused() { require(!paused, "Paused"); _; } modifier onlyGuardian() { require(msg.sender == guardian, "Not guardian"); _; } // Swap with slippage protection function swapWithSlippageProtection( uint256 _amountIn, uint256 _minAmountOut, address _to ) external nonReentrant whenNotPaused { require(_amountIn <= maxSwapAmount, "Exceeds max swap"); uint256 amountOut = getAmountOut(_amountIn); // Calculate expected output uint256 expectedOut = (_amountIn * reserve1) / reserve0; uint256 slippage = ((expectedOut - amountOut) * SLIPPAGE_DENOMINATOR) / expectedOut; require(slippage <= MAX_SLIPPAGE, "Slippage too high"); require(amountOut >= _minAmountOut, "Insufficient output"); // Execute swap... } // Emergency pause function pause() external onlyGuardian { paused = true; } function unpause() external onlyGuardian { paused = false; } } import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
7. Summary
Key points for implementing DEX core functionality:
-
AMM Mechanism:
- Constant product formula x * y = k
- Automatic price discovery
- No order book needed
-
Liquidity Management:
- Add/remove liquidity
- LP tokens represent shares
- Deposit proportionally
-
Trading Functions:
- Exact input/output swaps
- Multi-hop routing
- Slippage protection
-
Advanced Features:
- Price oracle
- Flash loans
- Governance integration
-
Security Considerations:
- Reentrancy protection
- Price manipulation protection
- Emergency pause
- Access control
-
Gas Optimization:
- Use custom errors
- Batch operations
- Storage optimization