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

SimpleDEX Stage 3

Етап 1: Базова версія DEX з повним функціоналом (add/remove liquidity + swap)

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

⚙️ Функції Stage 3

  • Всі функції Stage 2 +
  • ReentrancyGuard захист
  • nonReentrant на всіх функціях
  • DEAD_ADDRESS для MINIMUM_LIQUIDITY
  • Production-ready код

🎯 Ціль етапу

  • 🚀 Stage 3: Безпечний
  • ~180 рядків коду
  • Повний захист від атак
  • 🎯 Безпека: 100%

🔒 Безпека Stage 3

  • ✅ Integer Overflow
  • ✅ Front-Running
  • ✅ Price Manipulation
  • ✅ Reentrancy Attack
  • ✅ Liquidity Drain
📄 SimpleDEX-Stage3.sol
📊 ~130 рядків
🔧 Solidity 0.8.20
⏱️ Stage 3/3
// 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 - Stage 3: Фінальна версія з безпекою
 * @dev Додаємо ReentrancyGuard та додаткові перевірки безпеки
 *
 * Етап 3 (15 хвилин): Додаємо безпеку
 * - ReentrancyGuard захист
 * - Додаткові перевірки
 * - Production-ready код
 * - ~180 рядків коду
 *
 * Educational contract for Conflux Ukraine Webinar
 * "DeFi на Conflux: Створення Simple DEX"
 */
contract SimpleDEX_Stage3 is ERC20, ReentrancyGuard {
    // Токени пари
    IERC20 public immutable token0;
    IERC20 public immutable token1;

    // Резерви токенів
    uint256 public reserve0;
    uint256 public reserve1;

    // Комісія 0.3% (стандарт індустрії)
    uint256 public constant FEE_PERCENT = 30; // 0.3%
    uint256 public constant FEE_DENOMINATOR = 10000;

    // Мінімальна ліквідність для запобігання маніпуляціям ціною
    uint256 private constant MINIMUM_LIQUIDITY = 1000;

    // Dead address для спалення перших LP токенів (OpenZeppelin v5 не дозволяє mint на address(0))
    address private constant DEAD_ADDRESS = address(0x000000000000000000000000000000000000dEaD);

    // 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 Ініціалізація DEX з двома токенами
     */
    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 Додати ліквідність в пул
     * @param amount0 Кількість token0
     * @param amount1 Кількість token1
     * @return liquidity Кількість отриманих LP токенів
     */
    function addLiquidity(uint256 amount0, uint256 amount1)
        external
        nonReentrant
        returns (uint256 liquidity)
    {
        require(amount0 > 0 && amount1 > 0, "Invalid amounts");

        // Переказуємо токени від користувача
        token0.transferFrom(msg.sender, address(this), amount0);
        token1.transferFrom(msg.sender, address(this), amount1);

        // Розраховуємо LP токени
        uint256 _totalSupply = totalSupply();

        if (_totalSupply == 0) {
            // Перший постачальник - назавжди блокуємо MINIMUM_LIQUIDITY
            liquidity = sqrt(amount0 * amount1);
            _mint(DEAD_ADDRESS, MINIMUM_LIQUIDITY); // Спалюємо перші 1000 wei (на dead address, не address(0))
            liquidity = liquidity - MINIMUM_LIQUIDITY;
        } else {
            // Наступні постачальники - пропорційно існуючому supply
            liquidity = min(
                (amount0 * _totalSupply) / reserve0,
                (amount1 * _totalSupply) / reserve1
            );
        }

        require(liquidity > 0, "Insufficient liquidity minted");

        // Мінтимо LP токени постачальнику
        _mint(msg.sender, liquidity);

        // Оновлюємо резерви
        reserve0 += amount0;
        reserve1 += amount1;

        emit LiquidityAdded(msg.sender, amount0, amount1, liquidity);
    }

    /**
     * @dev Видалити ліквідність з пулу
     * @param liquidity Кількість LP токенів для спалення
     * @return amount0 Кількість повернутих token0
     * @return amount1 Кількість повернутих token1
     */
    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();

        // Розраховуємо суму токенів для повернення (пропорційно LP частці)
        amount0 = (liquidity * reserve0) / _totalSupply;
        amount1 = (liquidity * reserve1) / _totalSupply;

        require(amount0 > 0 && amount1 > 0, "Insufficient liquidity burned");

        // Спалюємо LP токени
        _burn(msg.sender, liquidity);

        // Переказуємо токени назад користувачу
        token0.transfer(msg.sender, amount0);
        token1.transfer(msg.sender, amount1);

        // Оновлюємо резерви
        reserve0 -= amount0;
        reserve1 -= amount1;

        emit LiquidityRemoved(msg.sender, amount0, amount1, liquidity);
    }

    /**
     * @dev Обміняти токени використовуючи формулу constant product (x*y=k)
     * @param tokenIn Адреса вхідного токену
     * @param amountIn Кількість вхідних токенів
     * @param minAmountOut Мінімальна кількість вихідних токенів (slippage protection)
     * @return amountOut Кількість отриманих токенів
     */
    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"
        );

        // Визначаємо напрямок свапу
        bool isToken0 = tokenIn == address(token0);
        (IERC20 tokenInContract, IERC20 tokenOutContract, uint256 reserveIn, uint256 reserveOut) =
            isToken0
                ? (token0, token1, reserve0, reserve1)
                : (token1, token0, reserve1, reserve0);

        // Переказуємо вхідні токени
        tokenInContract.transferFrom(msg.sender, address(this), amountIn);

        // Розраховуємо вихідну суму з комісією
        // Формула: 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");

        // Переказуємо вихідні токени
        tokenOutContract.transfer(msg.sender, amountOut);

        // Оновлюємо резерви
        if (isToken0) {
            reserve0 += amountIn;
            reserve1 -= amountOut;
        } else {
            reserve1 += amountIn;
            reserve0 -= amountOut;
        }

        emit Swap(msg.sender, tokenIn, amountIn, amountOut);
    }

    /**
     * @dev Розрахувати скільки токенів отримаєте за swap (без виконання)
     * @param tokenIn Адреса вхідного токену
     * @param amountIn Кількість вхідних токенів
     * @return amountOut Очікувана кількість вихідних токенів
     */
    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 Отримати поточну ціну token0 в одиницях token1
     * @return price Ціна token0 (масштабована на 1e18 для точності)
     */
    function getPrice0() external view returns (uint256 price) {
        require(reserve0 > 0 && reserve1 > 0, "No liquidity");
        price = (reserve1 * 1e18) / reserve0;
    }

    /**
     * @dev Отримати поточну ціну token1 в одиницях token0
     * @return price Ціна token1 (масштабована на 1e18 для точності)
     */
    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;
    }
}