Разработка системы защиты от 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 pool)
3. Эксплойтить протокол (который читает манипулированные данные)
4. Вернуть flash loan + fee, оставить profit
Конкретный пример — price oracle manipulation:
1. Flash loan: 50M DAI
2. Dump DAI в Uniswap V2 пуле DAI/ETH (spot price DAI падает)
3. Вызов протокола, который читает Uniswap V2 spot price для оценки collateral
→ Collateral в DAI теперь «дешевле», можно получить discount на ликвидацию
или оценить долг в 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 → human-readable
}
}
Выбор периода TWAP — критичный параметр. Слишком короткий (1–5 минут) — атакующий с достаточным капиталом может удерживать манипулированную цену несколько блоков. Слишком длинный (4–8 часов) — TWAP сильно отстаёт от рынка в волатильные периоды, вызывая неправильные ликвидации.
Практика: 30 минут — разумный default для большинства DeFi протоколов. Для высоковолатильных активов — 1–2 часа.
Chainlink как основной oracle
Chainlink price feed — агрегированная цена от множества независимых узлов с heartbeat обновлением. Манипуляция требует компрометации большинства oracle нод — экономически нецелесообразно.
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 требует удерживать токены через 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 state manipulation.
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 защищает от reentrancy в state-changing функциях. Но view функции не защищены — их можно вызывать из середины другой транзакции.
Если view функция используется внешним протоколом для получения цены или TVL — манипуляция state через 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 value 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 атаки обычно оперируют сотнями миллионов. Ограничение единичной транзакции снижает максимальный damage от любой атаки.
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% — протокол автоматически pausе. Это не предотвращает атаку, но ограничивает её масштаб.
Time-weighted balances
Вместо current balance использовать time-weighted average balance для критических расчётов:
// ERC-20Votes checkpoint подход применённый к liquidity
function getTimeWeightedLiquidity(address provider, uint256 lookback)
external view returns (uint256)
{
// Усреднённая ликвидность за lookback период
// Манипуляция в одной транзакции минимально влияет на average
}
Мониторинг on-chain
Система защиты неполна без мониторинга. Forta Network — decentralized detection network с ботами, которые мониторят 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) {
// Проверяем значительные изменения state нашего протокола
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 limits | Damage limitation |
| Circuit breaker | Auto-pause при TVL anomaly | Early stop на атаку |
| Monitoring | Forta bots | Detection и алертинг |
Разработка полной системы защиты: аудит существующих oracle зависимостей и governance механизмов — 1 неделя, разработка защитных контрактов — 2–3 недели, интеграция мониторинга — 1 неделя, тесты атак в fork environment — 2 недели.
Стоимость зависит от сложности протокола и количества точек oracle integration.







