Розробка DEX (децентралізованої біржі)
Децентралізована біржа — це смарт-контракти, які дозволяють торгувати без посередника. Користувач ніколи не передає контроль над коштами третій стороні: усі торги відбуваються on-chain, розрахунки атомарні, кастодія — в гаманці користувача. Uniswap V3 торгує понад $1B на день. Завдання: розробити DEX з нуля — від механізму AMM до фронтенду.
Вибір моделі DEX
AMM (Automated Market Maker)
Ліквідність надається до пулів замість книги ордерів. Ціна визначається математичною формулою.
Constant Product (x * y = k) — модель Uniswap V2. Проста, працює для будь-якої пари. Недолік: низька ефективність капіталу — більшість ліквідності ніколи не використовується.
Concentrated Liquidity — модель Uniswap V3. LP вказують діапазон цін для своєї ліквідності. Ефективність капіталу в 10–100 разів вища. Складніше для LP — потрібне активне управління.
Stableswap (Curve) — для активів з подібною ціною (USDC/USDT, stETH/ETH). Мінімальний слліпаж при великих обсягах.
Order Book DEX
On-chain книга ордерів дорога в газі (кожне розміщення/скасування = транзакція). Рішення: off-chain orderbook з on-chain розрахунком (dYdX v3, Serum), або L2-based CLOB (dYdX v4 на Cosmos).
Для більшості DEX на EVM — AMM з концентрованою ліквідністю.
Constant Product AMM: смарт-контракт
// 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/utils/math/Math.sol";
contract ConstantProductPool is ERC20 {
address public immutable token0;
address public immutable token1;
uint256 private reserve0;
uint256 private reserve1;
uint256 private constant FEE_NUMERATOR = 997; // 0.3% fee
uint256 private constant FEE_DENOMINATOR = 1000;
event Swap(address indexed sender, uint256 amount0In, uint256 amount1In,
uint256 amount0Out, uint256 amount1Out, address indexed to);
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
constructor(address _token0, address _token1) ERC20("LP Token", "LP") {
token0 = _token0;
token1 = _token1;
}
// Додавання ліквідності
function mint(address to) external returns (uint256 liquidity) {
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
uint256 amount0 = balance0 - reserve0;
uint256 amount1 = balance1 - reserve1;
uint256 totalSupply_ = totalSupply();
if (totalSupply_ == 0) {
// Перше додавання ліквідності: геометричне середнє мінус MINIMUM_LIQUIDITY
liquidity = Math.sqrt(amount0 * amount1) - 1000;
_mint(address(0xdead), 1000); // заблокувати мінімум для запобігання інфляційній атаці
} else {
liquidity = Math.min(
amount0 * totalSupply_ / reserve0,
amount1 * totalSupply_ / reserve1
);
}
require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED");
_mint(to, liquidity);
reserve0 = balance0;
reserve1 = balance1;
emit Mint(msg.sender, amount0, amount1);
}
// Видалення ліквідності
function burn(address to) external returns (uint256 amount0, uint256 amount1) {
uint256 liquidity = balanceOf(address(this));
uint256 totalSupply_ = totalSupply();
amount0 = liquidity * reserve0 / totalSupply_;
amount1 = liquidity * reserve1 / totalSupply_;
require(amount0 > 0 && amount1 > 0, "INSUFFICIENT_LIQUIDITY_BURNED");
_burn(address(this), liquidity);
IERC20(token0).transfer(to, amount0);
IERC20(token1).transfer(to, amount1);
reserve0 = IERC20(token0).balanceOf(address(this));
reserve1 = IERC20(token1).balanceOf(address(this));
emit Burn(msg.sender, amount0, amount1, to);
}
// Swap
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external {
require(amount0Out > 0 || amount1Out > 0, "INSUFFICIENT_OUTPUT_AMOUNT");
require(amount0Out < reserve0 && amount1Out < reserve1, "INSUFFICIENT_LIQUIDITY");
// Оптимістичний трансфер (flash loan паттерн)
if (amount0Out > 0) IERC20(token0).transfer(to, amount0Out);
if (amount1Out > 0) IERC20(token1).transfer(to, amount1Out);
// Flash loan callback якщо потрібен
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(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_AMOUNT");
// Перевіряємо інваріант з урахуванням комісії: (x + 0.997*dx)(y - dy) >= x*y
uint256 balance0Adjusted = balance0 * FEE_DENOMINATOR - amount0In * (FEE_DENOMINATOR - FEE_NUMERATOR);
uint256 balance1Adjusted = balance1 * FEE_DENOMINATOR - amount1In * (FEE_DENOMINATOR - FEE_NUMERATOR);
require(
balance0Adjusted * balance1Adjusted >= reserve0 * reserve1 * FEE_DENOMINATOR ** 2,
"K_INVARIANT_VIOLATED"
);
reserve0 = uint256(balance0);
reserve1 = uint256(balance1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
// Обчислення вихідної кількості за формулою AMM
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
public pure returns (uint256)
{
require(amountIn > 0, "INSUFFICIENT_INPUT_AMOUNT");
require(reserveIn > 0 && reserveOut > 0, "INSUFFICIENT_LIQUIDITY");
uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * FEE_DENOMINATOR + amountInWithFee;
return numerator / denominator;
}
}
Factory і Router
Архітектура стилю Uniswap: Factory створює пули, Router — точка входу для користувачів.
Безпека AMM
Захист від reentrancy
Всі зовнішні виклики після оновлення стану (Checks-Effects-Interactions) + ReentrancyGuard від OpenZeppelin.
Маніпуляція цінами (TWAP)
Spot ціна легко маніпулюється через flash loan на один блок. Для oracle — Time-Weighted Average Price.
Захист від MEV
Атаки frontrunning на DEX — стандартна проблема. Пом'якшення:
- Slippage tolerance: користувач встановлює макс. слліпаж (0.5–1%), транзакція відновлюється при перевищенні
- Deadline: транзакція відновлюється, якщо не виконана до deadline
- Flashbots / private mempool: для великих свопів — відправка через Flashbots для запобігання frontrunning
Фронтенд DEX інтерфейс
Критичні компоненти фронтенду для AMM DEX:
- Swap interface з real-time ціновою впливом і мінімальним отриманим
- Liquidity management з візуалізацією діапазону (для V3-style)
- Charts — інтеграція з The Graph для історичних даних пула
- Wallet integration — wagmi + viem, підтримка WalletConnect v2
- Transaction simulation — tenderly simulate перед відправкою
The Graph індексує події пула (Swap, Mint, Burn) і надає GraphQL API для аналітики.
Таймлайн розробки
| Компонент | Термін |
|---|---|
| AMM core контракти | 4–6 тижнів |
| Factory + Router | 3–4 тижні |
| TWAP oracle | 1–2 тижні |
| Subgraph | 2–3 тижні |
| Frontend (swap + ліквідність) | 4–6 тижнів |
| Аудит смарт-контрактів | 4–8 тижнів |
MVP DEX на mainnet: 4–6 місяців з урахуванням аудиту.







