Розробка системи захисту від flash loan атак
Flash loan—це необеспечений займ, який повинен бути повернений в тій же транзакції. Якщо не повернений—вся транзакція реверсується. З точки зору протоколу, що видає flash loan (Aave, Uniswap V3), це безрисикова операція: або гроші повернулися, або транзакція не сталася.
Проблема не у flash loan як таких—це легітимний інструмент для арбітражу, ліквідацій, рефінансування. Проблема в тому, що вони дають зловмиснику тимчасовий доступ до величезного капіталу (сотень мільйонів доларів) без забезпечення. Якщо протокол приймає економічні рішення на основі легко маніпулюючих даних (spot price DEX, не-TWAP oracle)—одна транзакція з flash loan може принести зловмиснику мільйони.
Beanstalk ($182M, 2022), Cream Finance ($130M, 2021), Mango Markets ($114M, 2022)—усі взломи використовували тимчасовий контроль над капіталом. Спільна ознака: протоколи використовували дані, які можна було зсунути однією транзакцією.
Анатомія flash loan атаки
Розуміння вектора атаки необхідне для побудови захисту. Типова атака складається з чотирьох кроків:
1. Взяти flash loan (наприклад, 100M USDC від Aave)
2. Маніпулювати станом (pump/dump ціни в DEX пулі)
3. Експлойтити протокол (який читає маніпульовані дані)
4. Повернути flash loan + fee, залишити прибуток
Конкретний приклад—маніпуляція цінового оракула:
1. Flash loan: 50M DAI
2. Dump DAI в Uniswap V2 пулі DAI/ETH (spot price DAI падає)
3. Виклик протоколу, який читає Uniswap V2 spot price для оцінки collateral
→ Collateral у DAI тепер «дешевше», можна отримати знижку на ліквідацію
або оцінити борг у DAI як менший
4. Прибуток → повернути flash loan
Інший тип—governance flash loan:
1. Flash loan governance токенів
2. Моментальне створення пропозала + голосування з величезною вагою
3. Виконання пропозала (drain treasury)
4. Повернення flash loan
(Саме так був атакований Beanstalk—зловмисник однією governance голосом прийняв пропозал про переведення treasury собі.)
Захист 1: Price Oracle—TWAP замість Spot
Це найпоширеніший та найкритичніший захист.
Чому Spot Price вразливий
Uniswap V2/V3 spot price = поточне співвідношення резервів. Великий swap у пулі миттєво змінює spot price. В одній транзакції можна зсунути ціну на 50–80% у пулі з помірною ліквідністю.
TWAP (Time-Weighted Average Price)
TWAP—середнє арифметичне ціни за період. Uniswap V2/V3 зберігає cumulative price accumulators, з яких можна обчислити TWAP за довільний період.
contract TWAPOracle {
IUniswapV3Pool public pool;
uint32 public constant TWAP_PERIOD = 30 minutes;
function getTWAP() external view returns (uint256 price) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = TWAP_PERIOD; // 30 хвилин назад
secondsAgos[1] = 0; // зараз
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(TWAP_PERIOD)));
// Конвертуємо tick у price
price = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
// ... конвертація sqrtPrice → людиночитаєма
}
}
Вибір періоду TWAP—критичний параметр. Занадто короткий (1–5 хвилин)—зловмисник з достатнім капіталом може утримувати маніпульовану ціну кілька блоків. Занадто довгий (4–8 годин)—TWAP сильно відстає від ринку в волатильні періоди, викликаючи неправильні ліквідації.
Практика: 30 хвилин—розумний default для більшості DeFi протоколів. Для високовол ватильних активів—1–2 години.
Chainlink як основний oracle
Chainlink price feed—агрегована ціна від множества незалежних вузлів з heartbeat оновленням. Маніпуляція вимагає компрометації більшості oracle nodes—економічно недоцільно.
contract PriceConsumer {
AggregatorV3Interface public priceFeed;
uint256 public constant HEARTBEAT = 3600; // 1 година
uint256 public constant MAX_STALENESS = HEARTBEAT * 2; // 2 години—максимум
function getPrice() external view returns (uint256) {
(
uint80 roundId,
int256 answer,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// Перевірка staleness: дані не старші за MAX_STALENESS
require(block.timestamp - updatedAt <= MAX_STALENESS, "Stale price");
// Перевірка правильності раунду
require(answeredInRound >= roundId, "Stale round");
// Перевірка позитивності ціни
require(answer > 0, "Invalid price");
return uint256(answer);
}
}
Загальна рекомендація: використовувати Chainlink як primary oracle, Uniswap TWAP як sanity check. Якщо два джерела розходяться більше ніж на X%—приостановити операції.
Захист 2: Snapshot Voting Power
Governance flash loan атаки використовують той факт, що voting power = поточний баланс токенів. ERC-20Votes вирішує це через checkpoint систему.
// Voting power фіксується на блоці snapshot (до початку голосування)
uint256 votePower = token.getPastVotes(voter, proposalSnapshot);
// Flash loan ПІСЛЯ snapshot не дає voting power
// Flash loan ДО snapshot вимагає утримування borrowed токенів через voting delay
Voting delay—мінімальний період між створенням пропозала та початком голосування. Якщо voting delay = 2 дні, зловмисник повинен утримувати borrowed токени 2 дні—економічно невигідно (fee + opportunity cost).
// OpenZeppelin Governor
constructor(...) GovernorSettings(
2 days, // votingDelay—захист від flash loan governance attacks
5 days, // votingPeriod
threshold
) {}
Beanstalk був атакований саме тому, що не використовував voting delay: пропозал можна було створити та виконати в одній транзакції.
Захист 3: Reentrancy Guard та Same-Block Checks
Деякі flash loan атаки експлойтують reentrancy або same-block маніпуляцію стану.
Same-Block Checks
contract Vault {
mapping(address => uint256) private _depositBlock;
function deposit(uint256 amount) external {
_depositBlock[msg.sender] = block.number;
// ...
}
function withdraw(uint256 amount) external {
// Не можна deposit та withdraw в одному блоці
require(
_depositBlock[msg.sender] < block.number,
"Flash loan protection: same block"
);
// ...
}
}
Це блокує паттерн: flash_loan → deposit → виклик функції яка читає баланс vault → withdraw → repay_loan.
Недостаток: legitimate користувачі також не можуть deposit+withdraw в одному блоці. Для більшості протоколів це прийнятно.
Nonreentrant + View функції
nonReentrant захищає state-changing функції від reentrancy. Але view функції не захищені—їх можна викликати з середини іншої транзакції.
Якщо view функція використовується зовнішнім протоколом для отримання ціни або TVL—маніпуляція стану через reentrancy змінює те, що бачить ця view функція.
// ВРАЗЛИВО: стан може бути маніпульований через reentrancy
function getSharePrice() external view returns (uint256) {
return totalAssets() * 1e18 / totalSupply();
}
// totalAssets() читає баланс контракту—який може бути тимчасово надутий
function totalAssets() public view returns (uint256) {
return IERC20(asset).balanceOf(address(this));
}
Рішення: зберігати cached значення total assets, оновлене тільки в protected функціях.
Захист 4: Circuit Breakers та Rate Limiting
Максимальний обсяг за транзакцію
uint256 public constant MAX_SINGLE_DEPOSIT = 1_000_000e6; // $1M max
function deposit(uint256 amount) external {
require(amount <= MAX_SINGLE_DEPOSIT, "Exceeds single tx limit");
// ...
}
Flash loan атаки зазвичай оперують сотнями мільйонів. Обмеження однієї транзакції зменшує максимальну шкоду від будь-якої атаки.
Pause механізм з автотриггером
contract ProtectedProtocol is Pausable {
uint256 public lastTVL;
uint256 public constant TVL_DROP_THRESHOLD = 20; // 20% за транзакцію
modifier checkTVLAnomaly() {
uint256 tvlBefore = totalValueLocked();
_;
uint256 tvlAfter = totalValueLocked();
if (tvlBefore > 0) {
uint256 dropPercent = ((tvlBefore - tvlAfter) * 100) / tvlBefore;
if (dropPercent > TVL_DROP_THRESHOLD) {
_pause();
emit EmergencyPause(tvlBefore, tvlAfter, dropPercent);
}
}
}
}
Circuit breaker: якщо за одну транзакцію TVL падає більше ніж на N%—протокол автоматично паузує. Це не запобігає атаці, але обмежує її масштаб.
Time-weighted Balances
Замість поточного баланса використовуйте time-weighted average balance для критичних розрахунків:
// ERC-20Votes checkpoint підхід застосований до ліквідності
function getTimeWeightedLiquidity(address provider, uint256 lookback)
external view returns (uint256)
{
// Усереднена ліквідність за lookback період
// Маніпуляція в одній транзакції мінімально впливає на average
}
On-chain мониторинг
Система захисту неповна без мониторингу. Forta Network—децентралізована detection мережа з ботами, які мониторять on-chain активність.
// Forta бот: детекція потенційної flash loan атаки
async function handleTransaction(txEvent) {
const findings = [];
// Перевіряємо наявність flash loan calldata у транзакції
const flashLoanCalls = txEvent.filterFunction([
'flashLoan(address,address,uint256,bytes)',
'flash(address,address,uint256,uint256,bytes)'
]);
if (flashLoanCalls.length > 0) {
// Перевіряємо значні зміни стану нашого протоколу
const protocolEvents = txEvent.filterLog(PROTOCOL_EVENTS, PROTOCOL_ADDRESS);
if (protocolEvents.length > 0) {
findings.push(Finding.fromObject({
name: "Flash loan + protocol interaction",
description: `Flash loan detected in same tx as protocol events`,
alertId: "FLASH-LOAN-INTERACTION",
severity: FindingSeverity.Medium,
type: FindingType.Suspicious
}));
}
}
return findings;
}
Forta алерти можна відправляти в PagerDuty / Telegram через webhook, давши команді 1–2 хвилини на відповідь до розповсюдження атаки.
Комплексна архітектура захисту
Ніщо з заходів окремо недостатньо. Ефективна система захисту—це шари:
| Рівень | Механізм | Захищає від |
|---|---|---|
| Oracle | Chainlink primary + TWAP sanity | Price manipulation |
| Governance | Voting delay (2+ дні) + ERC-20Votes | Flash loan governance |
| State | Same-block check на withdraw | Deposit-exploit-withdraw |
| Flow | Max per-tx лімити | Damage limitation |
| Circuit breaker | Auto-pause при TVL anomaly | Early stop на атаку |
| Monitoring | Forta bots | Detection та alerting |
Розробка повної системи захисту: аудит існуючих oracle залежностей та governance механізмів—1 тиж, розробка protective контрактів—2–3 тиж, інтеграція мониторингу—1 тиж, тести атак у fork environment—2 тиж.
Вартість залежить від складності протоколу та кількості oracle integration точок.







