Розробка 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 для зміни ваг |
| Frontend | 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 тижнів, плюс обов'язковий зовнішній аудит.







