Разработка weighted pool (Balancer-стиль)
Balancer V2 переосмыслил AMM: вместо жёсткого соотношения 50/50 появились пулы с произвольными весами активов — 80/20, 60/20/20, 33/33/34. Это открыло новый класс задач: on-chain управление портфелем с автоматической ребалансировкой через арбитраж. Но реализовать weighted pool с нуля — значит столкнуться с математикой инвариантов, проблемой точности при больших экспонентах и уязвимостями манипуляции ценой.
Математика weighted pool и где она ломается
Инвариант Balancer и вычисления с фиксированной точкой
Weighted pool держится на инварианте:
V = ∏(Bᵢ / Wᵢ)^Wᵢ
где Bᵢ — баланс токена i, Wᵢ — его нормализованный вес. При своп-операции система решает это уравнение относительно выходного баланса. Проблема — x^y при нецелых экспонентах требует LogExpMath, библиотеки с фиксированной точкой для вычисления натурального логарифма и экспоненты в 18-decimal Solidity.
Balancer использует LogExpMath.sol с границами: x должен быть в диапазоне [0.000001e18, 2^255], экспонента — не превышать 130e18. Выход за границы — revert. Это не просто техническая деталь: при добавлении ликвидности с экстремальными соотношениями (например, 99% одного актива в пул 80/20) вычисления могут упереться в лимиты библиотеки. Видели это на форках, где разработчики не проверяли граничные кейсы — addLiquidity реверсился при легитимных операциях.
Spot price и манипуляция через flash loan
Spot price в weighted pool определяется как:
SP = (Bᵢ / Wᵢ) / (Bⱼ / Wⱼ)
Это мгновенная цена до применения swap fee. Если протокол использует getSpotPrice() как оракул — он уязвим к flash loan манипуляции. Атакующий берёт огромный заём, делает своп, который сдвигает spot price в 10 раз, вызывает уязвимую функцию, возвращает заём. Всё в одной транзакции.
Решение — не использовать spot price как ценовой оракул. Для on-chain цен нужен Chainlink или TWAP от Uniswap V3 (IUniswapV3Pool.observe()). Внутри пула spot price используется только для расчёта свопов — это корректно, потому что сам своп меняет балансы и сдвигает цену обратно через swap fee.
Проблема джойна с неравными весами и impermanent loss
В отличие от Uniswap V2, weighted pool позволяет входить с произвольным набором токенов или одним токеном. Single-asset join проходит через внутренний виртуальный своп, который облагается swap fee. Это нужно явно объяснять пользователям: вход через single-asset join с большой суммой — это как сделать своп на половину суммы. При весах 80/20 ETH/USDC и входе только через USDC пользователь неявно покупает ETH.
Impermanent loss в weighted pool меньше, чем в 50/50 пуле, при тех же движениях цены. Для пула 80/20 при росте актива A в 5 раз IL составляет около 4.4% против 25.5% у 50/50. Это математически доказуемо, и мы добавляем в документацию графики IL для конкретных весов.
Как мы строим weighted pool
Архитектура на базе Balancer V2 Vault
Balancer V2 разделил хранение токенов и логику пула. Все токены хранятся в одном контракте Vault, пулы — это только логика расчётов. Это даёт:
- Flash loans из любого токена в Vault без отдельного контракта
- Batch swaps через несколько пулов в одной транзакции
- Единая точка авторизации через
IAuthorizer
При разработке кастомного weighted pool мы реализуем интерфейс IBasePool и регистрируем пул в Vault. Ключевые методы: onSwap(), onJoinPool(), onExitPool(). Логика инварианта живёт в WeightedMath.sol — мы используем проверенную реализацию Balancer, не пишем свою математику.
Управляемые веса (Managed Pool)
Для on-chain индексных фондов нужна возможность менять веса без flash loan-уязвимости. Balancer решает это через gradual weight update: веса линейно интерполируются между стартовыми и конечными значениями по блокам.
function _getNormalizedWeight(IERC20 token) internal view returns (uint256) {
uint256 pctProgress = _calculateWeightChangeProgress();
return _interpolateWeight(_startWeight[token], _endWeight[token], pctProgress);
}
Резкое изменение весов позволяет арбитражникам извлекать ценность за счёт LP. Градуальное изменение даёт арбитражникам возможность торговать по рыночным ценам, что минимизирует потери.
Тестирование на форке mainnet
Перед деплоем гоним fork-тесты через Foundry против реального Balancer Vault на Ethereum:
forge test --fork-url $MAINNET_RPC --match-contract WeightedPoolTest -vvv
Проверяем: своп туда-обратно не теряет более swap fee + 1 wei (инвариант не нарушается), single-asset join/exit корректно считает BPT (Balancer Pool Tokens), граничные кейсы LogExpMath не вызывают revert при реальных объёмах.
Стек и интеграции
| Компонент | Технология |
|---|---|
| Математика | LogExpMath.sol, FixedPoint.sol (Balancer) |
| Тестирование | Foundry fork-tests, Echidna property tests |
| Ценовые оракулы | Chainlink Data Feeds, Uniswap V3 TWAP |
| Управление | Gnosis Safe + Timelock для смены весов |
| Фронтенд | wagmi v2, viem, Balancer SDK |
| Индексация | The Graph (subgraph для событий пула) |
Процесс разработки
Аналитика (2-3 дня). Определяем состав пула, веса, swap fee, нужна ли управляемость весов. Рассчитываем ожидаемые объёмы и IL для LP.
Проектирование (3-5 дней). Выбор между форком Balancer V2 и кастомной реализацией на базе интерфейсов. Для большинства задач — форк с минимальными изменениями, не изобретаем свою математику.
Разработка (1-2 недели). Контракты пула + административные функции + интеграция с Vault. Fuzz-тесты инварианта через Echidna: свойство V_after >= V_before после любой операции (кроме свопа, где V меняется корректно).
Аудит и деплой. Slither + ручной аудит математики. Деплой через forge script с верификацией. Для пулов с TVL > $500K — внешний аудит обязателен.
Ориентиры по срокам
Weighted pool на базе форка Balancer V2 с кастомными весами — от 2 до 4 недель. Managed Pool с градуальным изменением весов и governance — от 4 до 6 недель. Полностью кастомная математика с новым инвариантом — от 6 недель, плюс обязательный внешний аудит.







