Розроблення системи маршрутизації ордерів через кілька DEX
Коли 1inch був запущений, він показав просту ідею: якщо ти шукаєш кращу ціну тільки на Uniswap — ти залишаєш гроші на столі. За кілька років агрегатори виросли до складних систем з split routing, multi-hop маршрутами і спеціалізованими алгоритмами оптимізації. Але суть не змінилася: завдання роутера — знайти шлях від token A до token B з мінімальними втратами при заданому обсязі.
Розроблення власної системи маршрутизації потрібна коли: стандартні агрегатори (1inch, Paraswap, 0x) не підтримують потрібний чейн; потрібна інтеграція кастомних протоколів; потрібен контроль над джерелами ліквідності; або наявні API занадто повільні для торгового бота.
Граф ліквідності як основа маршрутизації
Маршрутизація — це задача пошуку шляху у зважаному спрямованому графі. Вершини — токени. Ребра — пули (кожен пул створює два спрямовані ребра: A→B і B→A з ціною в кожному напрямку).
Для пошуку кращого шляху при фіксованому amountIn — задача пошуку шляху з максимальним добутком обмінних курсів (або еквівалентно — мінімальною сумою від'ємних логарифмів). Це модифікація алгоритму Беллмана-Форда або Дейкстри.
Але є нюанс, який робить задачу складнішою: ціна в пулі залежить від обсягу. Для amountIn = 100 USDC найкращим маршрутом може бути Uniswap V3 пул 0.05%. Для amountIn = 1,000,000 USDC той же пул дає 3% slippage, а split між кількома пулами дає 0.3%.
Це перетворює задачу пошуку шляху в графі з фіксованими вагами в оптимізаційну задачу з обсягово-залежними вагами.
Алгоритм split routing
Для великих ордерів оптимальне рішення — не єдиний маршрут, а розподіл обсягу по кількох шляхах.
Підхід через бінарний пошук оптимального split для двох маршрутів:
function findOptimalSplit(
routeA: Route,
routeB: Route,
totalAmount: bigint,
steps: number = 20
): { splitA: bigint; splitB: bigint; totalOut: bigint } {
let bestSplit = { splitA: 0n, splitB: totalAmount, totalOut: 0n }
for (let i = 0; i <= steps; i++) {
const fraction = i / steps
const amountA = BigInt(Math.floor(Number(totalAmount) * fraction))
const amountB = totalAmount - amountA
const outA = amountA > 0n ? simulateRoute(routeA, amountA) : 0n
const outB = amountB > 0n ? simulateRoute(routeB, amountB) : 0n
const totalOut = outA + outB
if (totalOut > bestSplit.totalOut) {
bestSplit = { splitA: amountA, splitB: amountB, totalOut }
}
}
return bestSplit
}
Для N маршрутів задача стає N-мірною оптимізацією — применяють gradient descent або Nelder-Mead з обмеженнями (сума долей = 1, усі долі ≥ 0).
Симуляція пулів: точність vs швидкість
Uniswap V2: точна формула
function getAmountOutV2(amountIn: bigint, reserveIn: bigint, reserveOut: bigint): bigint {
const amountInWithFee = amountIn * 997n
const numerator = amountInWithFee * reserveOut
const denominator = reserveIn * 1000n + amountInWithFee
return numerator / denominator
}
Uniswap V3: tick traversal
V3 потребує ітерації по tick bitmap для знаходження найближчих активних tick-ів. Повна симуляція точна, але повільна — кілька мілісекунд для великого своп з traversal через багато tick-ів.
Для швидкої оцінки (при скринінгу маршрутів) користуємо наближенням через поточний sqrtPriceX96 і liquidity без tick traversal — точно для малих обсягів, з похибкою для великих. Точну симуляцію запускаємо тільки для фінальних кандидатів.
Curve StableSwap: ітераційна формула
Curve використовує інваріант A * n^n * sum(x_i) + D = A * D * n^n + D^(n+1) / (n^n * prod(x_i)). Розрахунок amountOut — ітераційний (метод Ньютона). Для JavaScript/TypeScript — BigInt арифметика з 18-decimal точністю.
Balancer WeightedPool
Balancer з вагомими пулами (наприклад, 80/20 BAL/ETH) використовує інший інваріант. getAmountOut залежить від ваг токенів у пулі — складніша формула, ніж V2.
On-chain vs off-chain маршрутизація
Маршрутизація може відбуватися повністю on-chain (смарт-контракт знаходить маршрут прямо в транзакції) або off-chain (обчислення поза чейном, результат передається в контракт).
On-chain маршрутизація: повна прозорість, неможливість маніпуляції зі сторони aggregator-а. Проблема: обмежений gas, неможливо перевірити всі маршрути. Застосовується для простих випадків (2–3 пула максимум).
Off-chain маршрутизація (підхід 1inch, Paraswap): обчислення в backend, контракту передається готовий маршрут. Контракт тільки виконує. Газ ефективніше, маршрут складніше. Ризик: backend може повернути субоптимальний маршрут. Захист через slippage protection: minAmountOut у транзакції гарантує мінімум користувачу.
Router контракт: виконання складних маршрутів
Контракт повинен підтримувати гетерогенні маршрути: частина через Uniswap V2, частина через V3, частина через Curve.
struct SwapStep {
address pool;
address tokenIn;
address tokenOut;
uint24 fee; // Для V3
uint8 dexType; // 0=V2, 1=V3, 2=Curve, 3=Balancer
bytes extraData; // Додаткові параметри під тип DEX
}
function multiSwap(
SwapStep[] calldata steps,
uint256 amountIn,
uint256 minAmountOut,
address recipient
) external returns (uint256 amountOut) {
IERC20(steps[0].tokenIn).transferFrom(msg.sender, address(this), amountIn);
uint256 currentAmount = amountIn;
for (uint256 i = 0; i < steps.length; i++) {
currentAmount = _executeStep(steps[i], currentAmount);
}
require(currentAmount >= minAmountOut, "Slippage exceeded");
IERC20(steps[steps.length-1].tokenOut).transfer(recipient, currentAmount);
return currentAmount;
}
_executeStep диспатчить до конкретної DEX-реалізації по dexType. Кожна реалізація — окрема бібліотека (Solidity library pattern) для економії bytecode size.
Кеш стану пулів
Для швидкої маршрутизації без RPC-викликів на кожний запит потрібен кеш актуального стану пулів:
WebSocket subscriptions на события Sync (V2 пули) і Swap (V3 пули) через eth_subscribe("logs"). При кожній события оновлюємо reserves/sqrtPrice у пам'яті.
Для 500–1000 активних пулів це ~50–100 событій/блок на Ethereum mainnet. Обробка через event-driven архітектуру (Node.js EventEmitter або Rust tokio channel) з ≤1ms затримкою оновлення.
Cold start: при запуску сервісу потрібно завантажити поточний стан усіх пулів через multicall. Для 1000 пулів — 5–10 multicall транзакцій (до 200 calls кожен), займає 1–3 секунди.
Порівняння архітектурних підходів
| Підхід | Коли підходить | Складність | Latency |
|---|---|---|---|
| Простий multi-hop | 3–5 чейнів, топ-5 DEX | Низька | 200–500ms |
| Split routing | Великі ордера ($50K+) | Середня | 500ms–1s |
| З кешем пулів | Торговий бот, < 50ms | Висока | 10–50ms |
| On-chain router | Максимальна прозорість | Середня | 1 блок |
Процес роботи
Аналітика (1–2 дні). Список цільових DEX та чейнів, вимоги до latency, очікуваний обсяг ордерів.
Розроблення routing engine (5–7 днів). Граф пулів, алгоритм пошуку шляху, симуляція пулів, split routing.
Router контракт (3–5 днів). Multi-step execution, Solidity, Foundry fork-тесты.
Кеш та інфраструктура (3–5 днів при необхідності). WebSocket підписки, in-memory кеш пулів.
Орієнтири за часом
Базовий off-chain роутер через 3–5 DEX з простим multi-hop — 1 тиждень. Повноцінна система з split routing, кешем 500+ пулів, кастомним router контрактом та підтримкою V2/V3/Curve/Balancer — 2–3 тижні.







