Разработка concentrated liquidity pools
Команда запускает DEX и хочет не просто скопировать Uniswap V2 с его x·y=k, а сделать что-то конкурентоспособное. Переход на concentrated liquidity — правильное решение, но реализация значительно сложнее: тики, диапазоны, виртуальные резервы, piecewise-кривые. Одна ошибка в математике — и LP теряют деньги незаметно для себя, а арбитражники стригут протокол.
Почему x·y=k больше не работает для серьёзных AMM
Классический constant product formula эффективно использует от 5% до 20% ликвидности при реалистичных ценовых диапазонах. Остальные 80% капитала LP «спят» на хвостах, которые никогда не торгуются. Uniswap V3 решил это через концентрацию: LP выбирает диапазон [tickLower, tickUpper], и его ликвидность работает только внутри него — но работает во много раз интенсивнее.
Архитектурно это означает переход от единой глобальной кривой к кусочно-линейной аппроксимации. Каждый тик — граница диапазона, в котором действует локальная формула. При пересечении тика активная ликвидность пересчитывается.
Математика, которая ломается при неправильной реализации
Tick math и Q64.96 fixed-point арифметика
Uniswap V3 хранит цены как sqrtPriceX96 — корень из цены в формате Q64.96. Это не случайность: умножение двух Q64.96 чисел даёт Q128.192, что укладывается в uint256. Любое отклонение от этой схемы — переполнение или потеря точности.
Функция 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 использует slot0 с unlocked флагом именно для этого.
Как мы строим concentrated liquidity pools
Архитектурные решения
Разрабатываем на базе Uniswap V3 Core как референса, но не форкаем напрямую — лицензия BSL 1.1 до 2023 года имела ограничения на коммерческое использование (сейчас истекла, но аудиторы всё равно спрашивают). Используем Uniswap V4's hooks архитектуру для расширений, если нужны кастомные fee-логики или range orders.
Стек: Foundry для всей разработки и тестирования, Hardhat для деплой-скриптов с hardhat-deploy. Математические библиотеки — портируем из @uniswap/v3-core/contracts/libraries: FullMath, TickMath, SqrtPriceMath, LiquidityMath. Тесты включают property-based fuzzing с invariant-тестами в Foundry:
- Инвариант 1: сумма всей ликвидности в активных диапазонах всегда >= virtualReserves
- Инвариант 2: после любого swap с нулевым слиппажем 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 логики при свапах через несколько диапазонов.
Тестирование на реальных данных
Fork-тесты на Ethereum mainnet через vm.createFork позволяют воспроизвести реальные состояния пулов USDC/ETH 0.05% fee tier с реальным распределением ликвидности. Прогоняем исторические свапы из Uniswap V3 subgraph через The Graph и сравниваем результаты с референсной реализацией. Расхождение > 1 wei на любом свапе — сигнал к расследованию.
| Компонент | Инструмент | Покрытие |
|---|---|---|
| TickMath | Foundry fuzz, сравнение с V3 core | 100% граничных тиков |
| Fee accumulation | Property-based invariant tests | 50k итераций |
| Swap через несколько тиков | Fork-тесты на mainnet данных | 1000+ исторических свапов |
| Liquidity mint/burn | Статический анализ Slither + ручной review | Все публичные функции |
Периферия и интеграции
Сам пул — это только ядро. Для полноценного продукта нужен NonfungiblePositionManager (или аналог) для управления LP-позициями как NFT (ERC-721), SwapRouter для агрегации маршрутов, и quoter-контракт для off-chain симуляции свапов без gas.
Интеграция с Chainlink Price Feeds как sanity check: если цена в пуле отклоняется от oracle более чем на X%, circuit breaker приостанавливает свапы. Это защита от 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 недель для протокола такой сложности).
Стоимость рассчитывается индивидуально после технического брифинга.







