Розробка стратегічних vault
Yearn Finance у 2021 році втратив $11 мільйонів в атаці на vault стратегію DAI. Атакуючий використовував flash loan для маніпуляції ціною Curve pool, vault виконав harvest в неправильний момент та зафіксував убиток замість прибутку. Це не теоретична уязвимість — це реальний production incident, який змінив те, як будуються strategy vaults.
Strategy vault — це контракт-агрегатор, який приймає депозити користувачів (ERC-4626 стандарт), розміщує капітал в одну або кілька DeFi-стратегій, автоматично реінвестує дохідність та дозволяє переключати стратегії без участі користувачів. Складність — у коректному управлінні lifecycle стратегій та захисту harvest від маніпуляцій.
Архітектура strategy vault
ERC-4626 як базовий стандарт
ERC-4626 (Tokenized Vault Standard) стандартизує інтерфейс: deposit, withdraw, mint, redeem, previewDeposit, previewWithdraw, totalAssets. Це дозволяє агрегаторам (Yearn, Beefy, автоматичним rebalancers) працювати з vault без кастомної інтеграції.
Критична деталь ERC-4626: share price маніпуляція при першому депозиті. Якщо vault пустий, атакуючий робить tiny deposit (1 wei), потім donation атакою перечисляє велику суму безпосередньо в vault (минуючи deposit). Share price різко зростає, наступний депозитор отримує 0 share через округлення. Захист через virtual shares (OpenZeppelin ERC-4626 додає _decimalsOffset()):
function _convertToShares(uint256 assets, Math.Rounding rounding)
internal view virtual override returns (uint256)
{
return assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(),
totalAssets() + 1,
rounding
);
}
10x decimals offset робить donation attack економічно нецілісною.
Pluggable strategy pattern
Ядро архітектури — розділення vault (зберігає капітал, управляє share token) та strategy (розміщує капітал, генерує дохідність). Vault тримає список одобрених стратегій з allocation weights.
interface IStrategy {
function asset() external view returns (address);
function vault() external view returns (address);
function totalAssets() external view returns (uint256);
function harvest() external returns (uint256 profit, uint256 loss);
function withdraw(uint256 amount) external returns (uint256 withdrawn);
function emergencyExit() external;
}
Кожна стратегія — окремий контракт. Додавання нової стратегії не потребує апгрейду vault. Це критично для розширюваності та зменшує ризик аудиту нових стратегій (менший scope).
Управління allocation та автоматичне переключення
Vault зберігає debtRatio для кожної стратегії — відсоток від total assets, який стратегія повинна тримати. При harvest контроллер перевіряє:
- Якщо стратегія тримає менше target — vault дає більше капіталу
- Якщо стратегія тримає більше target — vault забирає надлишок
- Якщо стратегія в убитку — vault зменшує debtRatio, збільшує для більш прибільних
Автоматичне переключення будується на двох механіках:
- APY comparison: The Graph subgraph або Chainlink data feeds для APY різних протоколів
- Risk-adjusted scoring: APY / (volatility × risk_factor) — не завжди найкраща стратегія та, що дає максимум
Захист harvest від маніпуляцій
Це найбільш технічно складне місце. Yearn-інцидент показав: якщо harvest виконується в момент аномальних цін у пулі — vault фіксує потері.
Рішення:
TWAP перевірка при harvest: Перед фіксацією прибутку vault порівнює поточну ціну активу з TWAP. Якщо відхилення >X% — harvest відкладається.
Harvest як привілейована операція: harvest() викликає не хто угодно, а keeper (EOA або Gelato/Keep3r автоматизація) — з додатковими перевірками на аномалії.
Slippage контроль при swap у стратегії: Стратегія, яка продає reward токени за base asset (compound стратегія), повинна перевіряти minAmountOut через Uniswap v3 quoter.
function _sellRewards(uint256 rewardAmount) internal returns (uint256 baseReceived) {
uint256 expectedOut = quoter.quoteExactInputSingle(
REWARD_TOKEN, BASE_ASSET, POOL_FEE, rewardAmount, 0
);
// Не продаємо, якщо ціна аномально погана (>2% від quote)
uint256 minOut = expectedOut * 9800 / 10000;
baseReceived = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: REWARD_TOKEN,
tokenOut: BASE_ASSET,
fee: POOL_FEE,
recipient: address(this),
amountIn: rewardAmount,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
})
);
}
Контроль ризиків
Emergency exit — функція, доступна guardian multisig, яка забирає весь капітал з активних стратегій та повертає в vault idle. Використовується при виявленні уязвимості або рыночної аномалії. Стратегія при цьому переходить у emergency mode та блокує нові вложення.
Debt limit per strategy: Vault ніколи не розміщує >X% капіталу в одну стратегію, навіть якщо debtRatio виставлений вище. Hard cap на рівні контракту.
Withdrawal queue: При крупному виводі vault спочатку забирає з idle баланса, потім з найменш прибільних стратегій, потім з основних. Мінімізує disruption для інших депозиторів.
Стек розробки
- Solidity 0.8.x + OpenZeppelin 5.x — core vault та ERC-4626
- Foundry — тесты, fork-тесты на mainnet (Ethereum, Arbitrum, Polygon)
- Echidna — invariant testing: total assets ≥ total debt, share price монотонно росте
- The Graph — індексація APY, deposit/withdraw подій для фронтенду
- Gelato Network / Keep3r — автоматизовані keeper виклики для harvest
- Tenderly — моніторинг та алерти на аномалії
Процес роботи
Аналітика (3-5 днів). Визначаємо цільові протоколи для стратегій (Aave, Compound, Curve, Balancer), базовий актив vault, допустимі рівні ризику, fee structure (management fee, performance fee).
Архітектура (3-5 днів). Storage layout vault + strategy інтерфейси + governance механізм для додавання стратегій.
Розробка (4-8 тижнів). Vault core + 2-3 початкові стратегії + keeper автоматизація. Пріоритет — спочатку повний цикл однієї простої стратегії (Aave deposit), потім усклаування.
Тестування (1-2 тижні). Fork-тесты, invariant testing, симуляція атак (donation attack, harvest маніпуляція, flash loan).
Аудит + деплой. Для vault з реальним TVL — зовнішній аудит обов'язковий.
Часові орієнтири
Базовий vault з однією стратегією та ERC-4626 — 2-3 тижні. Повнофункціональна мульти-стратегійна система з автоматичним переключенням та keeper — 2-3 місяці. Вартість розраховується індивідуально.







