Розробка пулів зосередженої ліквідності
Команда запускає DEX і хоче не просто скопіювати Uniswap V2 з його x·y=k, а зробити щось конкурентоспроможне. Перехід на concentrated liquidity — правильне рішення, але реалізація значно складніша: тики, діапазони, віртуальні резерви, piecewise-криві. Одна помилка в математиці — і LP втрачають гроші непомітно для себе, а арбітражники стригають протокол.
Чому x·y=k більше не працює для серйозних AMM
Класична constant product формула ефективно використовує 5% до 20% ліквідності при реалістичних цінових діапазонах. Інші 80% капіталу LP «сплять» на хвостах, які ніколи не торгуються. Uniswap V3 вирішив це через концентрацію: LP вибирає діапазон [tickLower, tickUpper], і його ліквідність працює тільки всередині нього — але працює в багато разів інтенсивніше.
Архітектурно це означає перехід від однієї глобальної кривої до piecewise-лінійної апроксимації. Кожен тик — границя діапазону, в якому діє локальна формула. Перетин тика перераховує активну ліквідність.
Математика, яка ламається при неправильній реалізації
Tick math і Q64.96 fixed-point арифметика
Uniswap V3 зберігає ціни як sqrtPriceX96 — корінь з ціни в форматі Q64.96. Не випадково: множення двох Q64.96 чисел дає Q128.192, що умістилося в uint256. Будь-яке відхилення від цієї схеми — overflow або втрата точності.
Функція TickMath.getSqrtRatioAtTick(int24 tick) — критично важлива: вона переводить номер тика в sqrtPrice через таблицю прекомпільованих констант з бітовими зсувами. Самостійна реалізація без точного відтворення цих констант дає накопичувальну помилку, видиму при граничних значеннях тиків (MIN_TICK = -887272, MAX_TICK = 887272).
Практичний кейс: при тестуванні на Foundry fuzz-тестами з int24 параметрами ми спіймали розбіжність в 1 wei на крайніх тиках — здавалось, нічого. Але на функції спалювання ліквідності це давало underflow в uint256 і revert. На mainnet це блокувало б виведення ліквідності.
Fee accumulation через глобальні аккумулятори
Механізм збору комісій у концентрованих пулах працює через глобальні аккумулятори feeGrowthGlobal0X128 і feeGrowthGlobal1X128, плюс per-tick значення feeGrowthOutside. Формула розрахунку fees всередину діапазону:
feeGrowthInside = feeGrowthGlobal - feeGrowthBelow(tickLower) - feeGrowthAbove(tickUpper)
Де feeGrowthBelow і feeGrowthAbove залежать від того, знаходиться ли поточний тик вище або нижче границі. Помилка в умові currentTick >= tickLower vs currentTick > tickLower дає неверний розрахунок fees на граничних тиках. Це тиха помилка — LP отримують чуть менше або чуть більше комісій, протокол накопичує борг або профіцит.
Reentrancy через callback в swap
Swap-функція в concentrated liquidity AMM використовує callback-паттерн: пул контракт спочатку відпускає токени, потім викликає uniswapV3SwapCallback у msg.sender, в якому очікує отримати вхідні токени. Це відкриває reentrancy-вектор: в момент callback стан пула вже змінений (ціна зсунута), але транзакція ще не завершена.
OpenZeppelin ReentrancyGuard не допомагає прямо — callback викликається самим пулом в рамках тієї ж транзакції. Захист: lock-флаг в storage пула, який встановлюється на початку swap і знімається в кінці. Uniswap V3 використовує unlocked флаг в slot0 саме для цього.
Як ми будуємо concentrated liquidity pools
Архітектурні рішення
Розробляємо на базі Uniswap V3 Core як референцу, але не форкаємо прямо — ліцензія BSL 1.1 до 2023 року мала обмеження на комерційне використання (тепер закінчилась, але аудитори все ще запитують). Використовуємо Uniswap V4's hooks архітектуру для розширень, якщо потрібна кастомна fee-логіка.
Стек: Foundry для всієї розробки та тестування, Hardhat для deploy-скриптів з hardhat-deploy. Математичні бібліотеки — портуємо з @uniswap/v3-core/contracts/libraries: FullMath, TickMath, SqrtPriceMath, LiquidityMath. Тести включають property-based fuzzing з invariant-тестами в Foundry:
- Інваріант 1: сума всієї ліквідності в активних діапазонах завжди >= virtualReserves
- Інваріант 2: після будь-якого swap з нульовим slippage sqrtPrice не виходить за границі вказаного діапазону
- Інваріант 3: collected fees не перевищують accumulated feeGrowth * liquidity
Tick bitmap оптимізація
Пошук наступного ініціалізованого тика при cross-tick операціях — гарячий шлях. Uniswap V3 використовує bitmap: 256 тиків упаковані в один uint256. Пошук наступного set bit через BitMath.mostSignificantBit — O(1) замість O(n) по всім тикам.
Реалізація bitmap для tickSpacing > 1 вимагає маппінгу з tickIndex в bitPosition: compressed = tick / tickSpacing, wordPos = compressed >> 8, bitPos = uint8(compressed). Помилка в зсувах дає неверний пошук тика і пропуск cross-tick логіки при swaps через кілька діапазонів.
Тестування на реальних даних
Fork-тесты на Ethereum mainnet через vm.createFork дозволяють відтворити реальні стани пулів USDC/ETH 0.05% fee tier з реальним розподілом ліквідності. Прогоняємо історичні swaps з Uniswap V3 subgraph через The Graph і порівнюємо результати з референсною реалізацією. Розбіжність > 1 wei на будь-якому swap — сигнал до розслідування.
| Компонент | Інструмент | Покриття |
|---|---|---|
| TickMath | Foundry fuzz, порівняння з V3 core | 100% граничних тиків |
| Fee accumulation | Property-based invariant tests | 50k ітерацій |
| Swap через кілька тиків | Fork-тести на mainnet даних | 1000+ історичних swaps |
| Liquidity mint/burn | Статичний аналіз Slither + ручний review | Всі публічні функції |
Періферія та інтеграції
Сам пул — це тільки ядро. Для повноцінного продукту потрібен NonfungiblePositionManager (або аналог) для управління LP-позиціями як NFT (ERC-721), SwapRouter для агрегації маршрутів, і quoter-контракт для off-chain симуляції swaps без gas.
Інтеграція з Chainlink Price Feeds як sanity check: якщо ціна в пулі відхиляється від oracle більш ніж на X%, circuit breaker приостанавлює swaps. Це захист від oracle manipulation через flash loans — вектор, який використовувався в атаках на протоколи, що будувалися поверх AMM-цін.
Frontend будуємо на Uniswap SDK v3 + wagmi + viem. SDK абстрагує tick math і route finding, але для кастомних пулів його потрібно розширювати — підключати власні pool factories і переозначати computePoolAddress.
Етапи роботи
Аналітика (3-5 днів). Визначаємо параметри: fee tiers (0.01% / 0.05% / 0.3% / 1%), tickSpacing, потрібні ли кастомні hooks (V4-стиль), мультичейн деплой (Ethereum + Arbitrum + Optimism типово). Перевіряємо, потрібна ли апгрейдовність пула або immutable з admin-функціями тільки в періферії.
Проектування (5-7 днів). Storage layout, інтерфейси, математичні бібліотеки. Формальна верифікація інваріантів на папері до написання коду.
Розробка (4-8 тижнів). Core pool → математичні бібліотеки → position manager → router → quoter. Порядок важливий: кожен шар тестується незалежно.
Аудит. Concentrated liquidity — один з найскладніших класів DeFi-контрактів. Зовнішній аудит обов'язків для будь-якого об'єму TVL. Внутрішній аудит через Slither + Echidna закриває low/medium перед відправкою.
Деплой. Foundry forge script + Gnosis Safe мультисиг. Деплой на Sepolia/Arbitrum Goerli, нагрузочні тесты, потім mainnet.
Орієнтири по термінам
MVP з одним fee tier і базовою періферією — 6-8 тижнів. Повноцінний multi-tier DEX з custom hooks і агрегатором маршрутів — 2-3 місяці. Без урахування часу зовнішнього аудиту (зазвичай 3-6 тижнів для протоколу такої складності).
Вартість рахується індивідуально після технічного брифінгу.







