Solidity
// 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;
}
}