Повернутися до матеріалів

📄 SimpleDEX.sol

Повний код децентралізованого обмінника з AMM (Automated Market Maker)

✅ Код скопійовано в буфер обміну!

⚙️ Основні функції

  • addLiquidity() - додати ліквідність
  • removeLiquidity() - вивести ліквідність
  • swap() - обміняти токени
  • getAmountOut() - розрахунок обміну
  • getPrice0/1() - поточні ціни

🔒 Безпека

  • ReentrancyGuard захист
  • Slippage protection
  • MINIMUM_LIQUIDITY burn
  • Integer overflow перевірки
  • OpenZeppelin standards

💎 Особливості

  • AMM формула x*y=k
  • 0.3% комісія (стандарт індустрії)
  • LP токени (ERC20)
  • Повна EVM сумісність
  • Готовий для Conflux eSpace
📄 SimpleDEX.sol
📊 ~260 рядків
🔧 Solidity 0.8.20
Production-ready
// 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;
    }
}