Разработка системы детекции MEV-атак
MEV (Maximal Extractable Value) — это стоимость, которую валидаторы и searcher-ы извлекают, переупорядочивая транзакции внутри блока. Технически это не всегда «атака» — часть MEV безвредна (арбитраж между DEX восстанавливает ценовое равновесие). Но sandwich attacks, liquidation front-running и time-bandit атаки наносят прямой ущерб пользователям и протоколам. Система детекции MEV решает две задачи: постфактум анализ (что произошло с пользователем) и проактивная защита (предотвращение конкретных типов атак).
Типология MEV атак
Sandwich attack
Классика и наиболее распространённый тип. Схема:
- Жертва отправляет swap на DEX с large slippage tolerance
- Атакующий видит транзакцию в mempool
- Вставляет buy ПЕРЕД жертвой (front-run), поднимая цену
- Жертва исполняется по более высокой цене
- Атакующий продаёт ПОСЛЕ жертвы (back-run), фиксируя прибыль
Идентификатор в блоке: три транзакции подряд в одном блоке, где адреса и пары образуют характерный паттерн.
Liquidation front-running
Позиция на lending протоколе (Aave, Compound) становится ликвидируемой. Несколько ботов видят это и гонятся за liquidation reward. Первый выиграл — остальные потратили gas впустую. Это не атака на пользователя, но создаёт waste и MEV гонку.
Более агрессивная версия: атакующий манипулирует ценовым оракулом (через flash loan в пуле, используемом как oracle), чтобы принудительно вывести позицию под liquidation.
Arbitrage MEV
Технически не атака: если цена ETH на Uniswap v3 отличается от Curve на 0.3% — арбитражный бот выравнивает цену, получая прибыль. Это полезно для рынка. Тем не менее для протоколов важно понимать арбитражные потоки.
JIT (Just-in-Time) liquidity
Специфично для Uniswap v3. Атакующий видит крупный swap в mempool, добавляет concentrated liquidity точно в нужный range перед swap, получает fees, убирает ликвидность после. Честные LP теряют fees. Это спорная форма MEV — технически разрешённая протоколом.
Time-bandit attack
Реорг атака: если MEV в нескольких блоках назад превышает стоимость реорганизации цепи — атакующий валидатор может попытаться переписать историю. На Ethereum post-Merge это крайне сложно и дорого, но остаётся теоретической угрозой для PoS цепей с малым stake.
Детекция sandwich attack on-chain
Алгоритм идентификации
Sandwich — три транзакции в одном блоке с определёнными связями:
from dataclasses import dataclass
from typing import Optional
import pandas as pd
@dataclass
class SwapEvent:
tx_hash: str
block_number: int
tx_index: int # позиция в блоке
sender: str
token_in: str
token_out: str
amount_in: int
amount_out: int
pool: str
def detect_sandwiches(swaps_in_block: list[SwapEvent]) -> list[dict]:
sandwiches = []
# Группируем по пулу
by_pool = {}
for swap in swaps_in_block:
by_pool.setdefault(swap.pool, []).append(swap)
for pool, pool_swaps in by_pool.items():
# Сортируем по позиции в блоке
pool_swaps.sort(key=lambda s: s.tx_index)
# Ищем паттерн: A buys → victim buys → A sells (одинаковый token_in/out)
for i, front in enumerate(pool_swaps[:-2]):
victim = pool_swaps[i + 1]
back = pool_swaps[i + 2]
is_sandwich = (
front.sender == back.sender and # один атакующий
front.sender != victim.sender and # жертва другая
front.token_in == victim.token_in and # одно направление
back.token_in == front.token_out and # разворот
back.token_out == front.token_in
)
if is_sandwich:
# Считаем profit атакующего
attacker_profit = (
back.amount_out - front.amount_in
)
# Считаем ущерб жертвы: разница в execution price
victim_price = victim.amount_out / victim.amount_in
# fair_price нужно считать без front-run транзакции (simulation)
sandwiches.append({
'front_tx': front.tx_hash,
'victim_tx': victim.tx_hash,
'back_tx': back.tx_hash,
'attacker': front.sender,
'pool': pool,
'attacker_profit_tokens': attacker_profit,
'block': front.block_number,
})
return sandwiches
Измерение ущерба жертвы
Для точного расчёта ущерба нужно симулировать как выполнилась бы транзакция жертвы без front-run транзакции. Это требует доступа к archive node и replay с state override:
async def simulate_swap_without_frontrun(
victim_tx: dict,
frontrun_tx: dict,
block_number: int,
rpc_url: str
) -> dict:
"""
Симулирует victim_tx на состоянии блока ДО frontrun_tx
"""
w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(rpc_url))
# Получаем состояние на предыдущем блоке
prev_block_state = block_number - 1
# eth_call с block_number = prev_block (без frontrun)
result = await w3.eth.call(
{
'from': victim_tx['from'],
'to': victim_tx['to'],
'data': victim_tx['input'],
'gas': victim_tx['gas'],
},
prev_block_state
)
return {'simulated_output': result}
Разница между реальным amount_out и симулированным — это MEV извлечённое у жертвы.
Детекция через mempool мониторинг
Flashbots MEV-Share и MEV-Boost данные
Flashbots публикует MEV-Boost данные через API: https://boost-relay.flashbots.net/relay/v1/data/bidtraces/proposer_payload_delivered. Это исторические данные о блоках, прошедших через MEV-Boost, с информацией о MEV-bid.
Более детальные данные — через Flashbots MEV-Share API: информация о bundle содержимом, которую builder'ы публикуют post-execution.
Собственный mempool наблюдатель
Для реального времени: подключение к mempool через eth_subscribe("pendingTransactions"):
import { createPublicClient, webSocket, parseAbi, decodeEventLog } from 'viem';
const client = createPublicClient({
transport: webSocket('wss://mainnet.infura.io/ws/v3/KEY'),
});
// Подписка на pending транзакции
const unwatch = client.watchPendingTransactions({
onTransactions: async (hashes) => {
for (const hash of hashes) {
const tx = await client.getTransaction({ hash });
if (tx && isLargeSwap(tx)) {
// Это транзакция с большим slippage - возможная жертва
await analyzeMevRisk(tx);
}
}
},
});
Проблема: большинство MEV ботов сейчас используют private mempools (Flashbots bundles, MEV Blocker). Публичный mempool видит только часть картины.
Защитные механизмы для протоколов
Commit-reveal для чувствительных операций
Пользователь сначала публикует хэш транзакции (commit), потом reveal. Атакующий не знает параметры до reveal:
contract CommitRevealSwap {
mapping(bytes32 => uint256) public commits;
uint256 public constant REVEAL_DELAY = 2; // блоков
function commit(bytes32 commitment) external {
commits[commitment] = block.number;
}
function reveal(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut,
bytes32 salt
) external {
bytes32 commitment = keccak256(
abi.encodePacked(msg.sender, tokenIn, tokenOut, amountIn, minAmountOut, salt)
);
require(commits[commitment] != 0, "No commit");
require(block.number >= commits[commitment] + REVEAL_DELAY, "Too early");
delete commits[commitment];
// исполнить swap
}
}
Tight slippage + deadline
Пользователи с minAmountOut = 0 или очень большим slippage — лёгкие жертвы. Протокол должен:
- Рассчитывать рекомендуемый slippage на основе pool depth
- Предупреждать о high slippage в UI
- Короткий deadline (не больше 10-15 минут) для предотвращения stale транзакций
MEV Blocker интеграция
Для frontend: routing транзакций через MEV Blocker (CoW Protocol) или Flashbots Protect вместо публичного mempool. Транзакции видят только доверенные builders, не публичные searcher-ы.
// Отправка через Flashbots Protect RPC
const provider = new ethers.JsonRpcProvider(
'https://rpc.flashbots.net',
1 // mainnet
);
Это не требует изменений в смарт-контрактах — только смена RPC endpoint.
TWAP oracle вместо spot price
Для протоколов, использующих цены из AMM: Uniswap v3 TWAP (Time-Weighted Average Price) устойчив к flash loan манипуляциям — нельзя сдвинуть TWAP за один блок.
function getTWAP(address pool, uint32 secondsAgo) public view returns (uint256 price) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = secondsAgo;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(secondsAgo)));
price = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
}
Аналитическая часть: мониторинг и reporting
MEV dashboard: визуализация исторических MEV событий по протоколу. Отвечает на вопросы: сколько пользователей пострадали от sandwich за последний месяц? Какие пулы наиболее атакуемы? Какой средний ущерб на событие?
Eigenφhi, Flashbots MEV-Explore, EigenPhi — публичные инструменты для исторического анализа. Для собственного мониторинга — The Graph subgraph + Dune Analytics.
| Тип атаки | Метод детекции | Сложность | Точность |
|---|---|---|---|
| Sandwich | Паттерн в блоке | Средняя | Высокая |
| JIT liquidity | LP + swap timing | Средняя | Высокая |
| Oracle manipulation | Price deviation + flash loan | Высокая | Средняя |
| Liquidation front-run | Race transactions | Низкая | Высокая |
| Time-bandit | Reorg detection | Очень высокая | Низкая |
Стек и сроки
Backend: Python (detection engine) + TypeScript (real-time mempool). Archive node доступ (Erigon self-hosted или Alchemy archive). PostgreSQL для хранения событий. Dune Analytics интеграция для визуализации.
Разработка full-stack MEV detection системы: 10-16 недель. Только post-hoc on-chain анализ sandwich attacks: 4-6 недель. Интеграция Flashbots Protect в frontend: 1-2 дня.







