Розробка протоколу флеш-займів
Флеш-займ — атомарний кредит: позичають будь-яку суму, роблять що завгодно, повертають в одній транзакції. Якщо повернення не сталося — вся транзакція скасовується. Без колатералю, без кредитної історії. Aave V3 утримує сотні мільйонів у пулах флеш-займів — і це працює, тому що атомарність EVM — абсолютна гарантія.
Але та ж атомарність робить флеш-займи основним інструментом для атак на DeFi. З 10 найбільших взломів останніх трьох років — мінімум 7 використовували флеш-займи як стартовий капітал для маніпуляції оракулом, маніпуляції цінами або атак на governance.
Архітектура протоколу флеш-займів
Механіка виконання
Класична схема Aave V2: пул викликає executeOperation() на адресі receiver, передає активи, receiver робить що потрібно, повертає активи + премію. Весь сценарій — один виклик flashLoan(), одна транзакція, один блок.
Aave V3 додав flashLoanSimple() для одного активу (дешевше за газ) та переробив інтерфейс receiver на IFlashLoanSimpleReceiver. EIP-3156 формалізував стандарт: flashLoan(receiver, token, amount, data) та обов'язковий callback onFlashLoan(). Будуючи протокол для інтеграції — логічно підтримувати обидва інтерфейси.
Типи реалізацій
Single-asset flash loans (EIP-3156 compliant): найпростіший варіант. Один токен, один receiver. Мінімальний газ. Працює для протоколів, які хочуть монетизувати простаївлячу ліквідність.
Multi-asset batch loans (Aave-style): кілька токенів в одній транзакції. Складніша реалізація, але відкриває сценарії типу «позичити ETH + USDC одночасно для арбітражу в пулі». Реалізуєтся через масиви активів/сум в одному викликі.
Callback-less flash loans (Uniswap V2-style): пул передає токени спочатку, receiver повертає в кінці транзакції через окремий виклик. Менш безпечна конструкція — receiver повинен сам перевіряти, що його не викликають повторно.
Ключові проблеми безпеки
Reentrancy через callback флеш-займу
Стандартна пастка: контракт receiver викликає іншу функцію пула всередині executeOperation(). Якщо пул не захищений reentrancy guard на рівні сховища — атакуючий може змінити стан пула до завершення оригінальної транзакції.
// НЕПРАВИЛЬНО: reentrancy можлива
function flashLoan(address receiver, uint256 amount) external {
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(receiver, amount);
IFlashLoanReceiver(receiver).executeOperation(amount, fee, msg.sender);
// receiver міг би викликати deposit() і змінити balanceBefore-логіку
require(token.balanceOf(address(this)) >= balanceBefore + fee);
}
Рішення: модифікатор nonReentrant від OpenZeppelin на flashLoan() і на всі функції, що змінюють стан пула (deposit, withdraw, borrow).
Перевірка caller у контракті receiver
Receiver повинен перевіряти, що executeOperation() викликає саме довірений пул, а не довільна адреса. Без цієї перевірки атакуючий може прямо викликати executeOperation() на receiver, імітуючи флеш-займ.
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
require(msg.sender == address(LENDING_POOL), "Invalid caller");
require(initiator == address(this), "Invalid initiator");
// логіка
}
Облік комісій та проблеми точності
Типова помилка: комісія розраховується як amount * FEE_BPS / 10000, де FEE_BPS = 9 (0.09%). При малих сумах результат округляється до 0 через цілочисельне ділення. Атакуючий розбиває один великий займ на тисячі маленьких і платить нульову комісію.
Захист: мінімальна комісія в абсолютному значенні (наприклад, 1 wei) та перевірка amountOwed > amount — не просто amountOwed >= amount + fee_calculated.
Як будуємо протокол флеш-займів
Стек: Solidity 0.8.x, OpenZeppelin 5.x для ReentrancyGuard та SafeERC20, Foundry для тестування.
Pool контракт зберігає ліквідність провайдерів. Підтримуємо кілька токенів через mapping token => PoolState. PoolState включає: totalLiquidity, totalBorrowed (для обліку одночасних займів), feeAccumulator для LP-нагород.
Fee distribution: комісія з флеш-займів накопичується в пулі та підвищує exchange rate LP-токенів (як Compound cTokens). LP отримують yield без окремого claim — просто при виводі отримують більше базового токена.
Access control: через OpenZeppelin AccessControl. Ролі: PAUSER_ROLE (circuit breaker при атаці), FEE_SETTER_ROLE (під timelock governance), ASSET_MANAGER_ROLE (додавання нових токенів).
Circuit breaker: якщо за один блок з пула ушло більше X% ліквідності — автоматична пауза. Реалізується через mapping _blockLoanVolume та перевірку на початку flashLoan(). Хибні спрацювання можливі при легітимному використанні — порог встановлюємо через governance.
Тестування
Fork-тесты на Ethereum mainnet критичні. Гоняємо через Foundry:
- Стандартний займ та повернення з комісією
- Спроба невозврату — транзакція повинна скасуватися
- Reentrancy атака на
flashLoan()через шкідливий receiver - Нульова комісія при мінімальній сумі (перевіряємо anti-dust логіку)
- Паніка: 100 послідовних займів максимального обсягу
Fuzzing в Echidna з інваріантом: totalLiquidity після будь-якої послідовності операцій не менша за суму депозитів мінус зняття.
Легітимні use cases — навіщо це будувати
Флеш-займи — не тільки атаки. Протокол відкриває:
Арбітраж без капіталу: трейдер бачить різницю цін між Uniswap та Curve, позичає займ, виконує арбітраж, повертає. Прибуток — спред мінус газ. DEX-арбітраж вирівнює ціни, роблячи ринки ефективнішими.
Self-liquidation: користувач з позицією в Aave, близькою до ліквідації, позичає флеш-займ, погашає борг, виводить залог, повертає займ. Уникає штрафу від зовнішніх ліквідаторів.
Collateral swap: замінити ETH залог на WBTC без закриття позиції. Одна транзакція замість чотирьох.
Leverage unwinding: закрити leveraged позицію в одній транзакції.
Орієнтири за часом
Базовий EIP-3156 compliant протокол для одного токена — 3-5 днів включаючи тести. Multi-asset пул з LP-токенами та fee distribution — 1-1.5 тижня. Інтеграція з існуючим lending протоколом (додавання флеш-займів на top) — 1-2 тижні залежно від архітектури базового протоколу. Вартість розраховується після аналізу вимог.







