Повний код децентралізованого обмінника з AMM (Automated Market Maker)
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; /** * @title SimpleDEX * @dev A simple decentralized exchange (DEX) implementation using constant product AMM (x*y=k) * @notice This contract allows users to swap between two ERC20 tokens and provide liquidity * * Educational contract for Conflux Ukraine Webinar * "DeFi на Conflux: Створення Simple DEX" */ contract SimpleDEX is ERC20, ReentrancyGuard { // Token addresses for the pair IERC20 public immutable token0; IERC20 public immutable token1; // Reserves for each token uint256 public reserve0; uint256 public reserve1; // Fee percentage (0.3% = 30 basis points) uint256 public constant FEE_PERCENT = 30; // 0.3% uint256 public constant FEE_DENOMINATOR = 10000; // Minimum liquidity to prevent pool share price manipulation uint256 private constant MINIMUM_LIQUIDITY = 1000; // Events event LiquidityAdded( address indexed provider, uint256 amount0, uint256 amount1, uint256 liquidity ); event LiquidityRemoved( address indexed provider, uint256 amount0, uint256 amount1, uint256 liquidity ); event Swap( address indexed user, address indexed tokenIn, uint256 amountIn, uint256 amountOut ); /** * @dev Constructor to initialize the DEX with two tokens * @param _token0 Address of the first token * @param _token1 Address of the second token */ constructor(address _token0, address _token1) ERC20("SimpleDEX LP Token", "SDEX-LP") { require(_token0 != address(0) && _token1 != address(0), "Invalid token addresses"); require(_token0 != _token1, "Identical tokens"); token0 = IERC20(_token0); token1 = IERC20(_token1); } /** * @dev Add liquidity to the pool * @param amount0 Amount of token0 to add * @param amount1 Amount of token1 to add * @return liquidity Amount of LP tokens minted */ function addLiquidity(uint256 amount0, uint256 amount1) external nonReentrant returns (uint256 liquidity) { require(amount0 > 0 && amount1 > 0, "Invalid amounts"); // Transfer tokens from user token0.transferFrom(msg.sender, address(this), amount0); token1.transferFrom(msg.sender, address(this), amount1); // Calculate liquidity to mint uint256 totalSupply = totalSupply(); if (totalSupply == 0) { // First liquidity provider - permanently lock MINIMUM_LIQUIDITY liquidity = sqrt(amount0 * amount1); _mint(address(0), MINIMUM_LIQUIDITY); // Burn first 1000 wei to prevent manipulation liquidity = liquidity - MINIMUM_LIQUIDITY; } else { // Subsequent liquidity providers liquidity = min( (amount0 * totalSupply) / reserve0, (amount1 * totalSupply) / reserve1 ); } require(liquidity > 0, "Insufficient liquidity minted"); // Mint LP tokens to provider _mint(msg.sender, liquidity); // Update reserves reserve0 += amount0; reserve1 += amount1; emit LiquidityAdded(msg.sender, amount0, amount1, liquidity); } /** * @dev Remove liquidity from the pool * @param liquidity Amount of LP tokens to burn * @return amount0 Amount of token0 returned * @return amount1 Amount of token1 returned */ function removeLiquidity(uint256 liquidity) external nonReentrant returns (uint256 amount0, uint256 amount1) { require(liquidity > 0, "Invalid liquidity amount"); require(balanceOf(msg.sender) >= liquidity, "Insufficient LP tokens"); uint256 totalSupply = totalSupply(); // Calculate token amounts to return amount0 = (liquidity * reserve0) / totalSupply; amount1 = (liquidity * reserve1) / totalSupply; require(amount0 > 0 && amount1 > 0, "Insufficient liquidity burned"); // Burn LP tokens _burn(msg.sender, liquidity); // Transfer tokens to user token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); // Update reserves reserve0 -= amount0; reserve1 -= amount1; emit LiquidityRemoved(msg.sender, amount0, amount1, liquidity); } /** * @dev Swap tokens using constant product formula (x*y=k) * @param tokenIn Address of input token * @param amountIn Amount of input tokens * @param minAmountOut Minimum amount of output tokens (slippage protection) * @return amountOut Amount of output tokens received */ function swap(address tokenIn, uint256 amountIn, uint256 minAmountOut) external nonReentrant returns (uint256 amountOut) { require(amountIn > 0, "Invalid input amount"); require( tokenIn == address(token0) || tokenIn == address(token1), "Invalid input token" ); // Determine which token is input and which is output bool isToken0 = tokenIn == address(token0); (IERC20 tokenInContract, IERC20 tokenOutContract, uint256 reserveIn, uint256 reserveOut) = isToken0 ? (token0, token1, reserve0, reserve1) : (token1, token0, reserve1, reserve0); // Transfer input tokens from user tokenInContract.transferFrom(msg.sender, address(this), amountIn); // Calculate output amount with fee // Formula: amountOut = (amountIn * 0.997 * reserveOut) / (reserveIn + amountIn * 0.997) uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_PERCENT); amountOut = (amountInWithFee * reserveOut) / (reserveIn * FEE_DENOMINATOR + amountInWithFee); require(amountOut >= minAmountOut, "Slippage exceeded"); require(amountOut > 0, "Insufficient output amount"); // Transfer output tokens to user tokenOutContract.transfer(msg.sender, amountOut); // Update reserves if (isToken0) { reserve0 += amountIn; reserve1 -= amountOut; } else { reserve1 += amountIn; reserve0 -= amountOut; } emit Swap(msg.sender, tokenIn, amountIn, amountOut); } /** * @dev Get the amount of output tokens for a given input * @param tokenIn Address of input token * @param amountIn Amount of input tokens * @return amountOut Expected amount of output tokens */ function getAmountOut(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut) { require(amountIn > 0, "Invalid input amount"); require( tokenIn == address(token0) || tokenIn == address(token1), "Invalid input token" ); bool isToken0 = tokenIn == address(token0); (uint256 reserveIn, uint256 reserveOut) = isToken0 ? (reserve0, reserve1) : (reserve1, reserve0); uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - FEE_PERCENT); amountOut = (amountInWithFee * reserveOut) / (reserveIn * FEE_DENOMINATOR + amountInWithFee); } /** * @dev Get current price of token0 in terms of token1 * @return price Price of token0 (scaled by 1e18 for precision) */ function getPrice0() external view returns (uint256 price) { require(reserve0 > 0 && reserve1 > 0, "No liquidity"); price = (reserve1 * 1e18) / reserve0; } /** * @dev Get current price of token1 in terms of token0 * @return price Price of token1 (scaled by 1e18 for precision) */ function getPrice1() external view returns (uint256 price) { require(reserve0 > 0 && reserve1 > 0, "No liquidity"); price = (reserve0 * 1e18) / reserve1; } // Helper 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; } }