Система виявлення sandwich та front-running: захист DeFi від вилучення цінності
Уявіть: користувач відправляє swap на Uniswap з slippage 5%, а бот-перехоплювач витягує 3% вартості угоди. Для протоколу це не лише репутаційні ризики — регулярні MEV-атаки знижують TVL на 15-20% за квартал. Прямі фінансові втрати від sandwich-атак на протоколах з TVL > $10 млн можуть перевищувати $500 000 на рік. Щорічні збитки від MEV на Ethereum перевищують $1 млрд. Ми розробили систему, яка постфактум аналізує ланцюжки транзакцій та проактивно блокує MEV-патерни. Система використовує комбінацію публічного та private mempool даних, що підвищує точність детекції до 85%. Наш досвід — понад 5 років у блокчейн-розробці та десятки реалізованих протоколів. Замовте безкоштовний аудит вашого протоколу — ми виявимо вразливості та запропонуємо рішення.
Чому MEV-атаки небезпечні для DeFi-протоколів?
Типова ситуація: користувач відправляє swap on Uniswap з slippage 5%, бот-searcher перехоплює транзакцію в mempool, front-run піднімає ціну, жертва втрачає 3-4% вартості угоди. Для протоколу це не лише репутаційні ризики — регулярні атаки знижують TVL на 15-20% за квартал, оскільки користувачі йдуть на захищені платформи. Ми запобігаємо цьому. Згідно з Flashbots MEV-Share, близько 90% всіх MEV-транзакцій проходять через private mempool.
Типи MEV-атак, які ми детектуємо
- Sandwich attack (класика): три транзакції в одному блоці: front-run купівля — жертва — back-run продаж з тим же атакуючим. Ідентифікація за патерном
sender == back.sender та інвертованим токенам. Типовий збиток — 0.3–0.5% від суми угоди.
- Liquidation front-running: на Aave або Compound боти бачать позицію під ліквідацією і змагаються за нагороду. Більш небезпечний варіант — flash loan маніпуляція оракулом.
- Arbitrage MEV: корисне явище, арбітраж вирівнює ціни на DEX. Ми детектуємо його для розуміння потоків ліквідності.
- JIT liquidity: в Uniswap v3 атакуючий додає концентровану ліквідність прямо перед великим swap і забирає комісію, завдаючи шкоди чесним LP.
- Time-bandit attack: рідкісна загроза, реорганізація ланцюга заради вилучення MEV з минулих блоків. На Ethereum post-Merge практично неможлива економічно.
Як детектувати MEV через mempool у реальному часі?
Використовуємо API boost-relay.flashbots.net для історичних даних про MEV-bid. Для детального вмісту — MEV-Share API, де builder'и публікують bundle post-execution. Це дає доступ до даних, які не видно в публічному mempool.
Підключаємося через eth_subscribe("pendingTransactions"):
import { createPublicClient, webSocket } from 'viem';
const client = createPublicClient({
transport: webSocket('wss://mainnet.infura.io/ws/v3/KEY'),
});
const unwatch = client.watchPendingTransactions({
onTransactions: async (hashes) => {
for (const hash of hashes) {
const tx = await client.getTransaction({ hash });
if (tx && isLargeSwap(tx)) {
await analyzeMevRisk(tx);
}
}
},
});
Однак більшість MEV-ботів використовує private mempool (Flashbots bundles). Публічний mempool бачить лише частину картини — приблизно 20-30% всіх MEV-транзакцій. Тому ми комбінуємо його з даними MEV-Share, що підвищує точність детекції до 85%.
Детекція 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)
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:
attacker_profit = back.amount_out - front.amount_in
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. Різниця між реальним amount_out та симульованим — MEV, вилучене у користувача.
Як захистити протокол від MEV: приклад реалізації
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
}
}
TWAP oracle замість spot price
Uniswap v3 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);
}
Порівняння методів захисту від MEV
| Метод |
Захист від sandwich |
Складність інтеграції |
Вплив на UX |
| Tight slippage + deadline |
Знижує втрати на 50-70% |
Низька |
Високий (користувач вказує точні параметри) |
| Commit-reveal |
Повний захист (100%) |
Середня |
Середній (затримка на 2 блоки) |
| MEV Blocker (Flashbots Protect) |
Запобігає front-run на 99% |
Низька (зміна RPC) |
Немає |
| TWAP oracle |
Стійкий до flash loan |
Середня |
Немає |
Commit-reveal схема в 10 разів надійніша за простий tight slippage проти sandwich-атак, але потребує зміни UX.
Процес роботи та терміни
| Етап |
Результат |
| Аналіз поточних контрактів |
Звіт про вразливості до MEV |
| Розробка детектора |
Інтегрований модуль детекції з API |
| Налаштування дашборда |
Дашборд з візуалізацією атак |
| Інтеграція захисту |
Підключення MEV Blocker / commit-reveal |
| Навчання команди |
Документація та воркшоп |
Терміни:
- Базова детекція sandwich post-hoc: 4-6 тижнів.
- Повна система з real-time моніторингом та дашбордом: 10-16 тижнів.
- Інтеграція Flashbots Protect у frontend: 1-2 дні.
Стек:
- Backend: Python (detection engine), TypeScript (real-time mempool), PostgreSQL, Dune Analytics.
- Для точних симуляцій використовуємо self-hosted Erigon або Alchemy Archive.
Замовте аудит вашого протоколу для виявлення вразливостей до MEV. Зв'яжіться з нами для впровадження системи детекції — ми безкоштовно оцінимо ризики та запропонуємо оптимальне рішення. Отримайте консультацію вже сьогодні.
Аудит смарт-контрактів: як знаходять те, що не бачить компілятор
Коли протокол втрачає значні кошти через flash loan атаку на функцію, яку аудитори дивилися наживо — це не випадковість. Це системна прогалина в методології. Наш досвід показує: вразливість живе в контракті більше року, а компілятор мовчить. Ми перебудували процес аудиту так, щоб ловити такі кейси до деплою.
Що не знайде статичний аналіз?
Slither — стандартний перший інструмент. Знаходить reentrancy, integer overflow (в старих версіях Solidity), неправильне використання tx.origin, shadowing змінних, неініціалізовані сховища. На реальному проекті Slither видає десятки попереджень, з яких критичних — 0‑2. Решта — інформаційний шум.
Slither не знайде логічну вразливість. Якщо withdraw коректно перевіряє баланс і коректно оновлює стан, але бізнес-логіка дозволяє подвійне списання через два різні шляхи кодової бази — Slither промовчить.
Mythril використовує symbolic execution: будує граф усіх можливих шляхів виконання і шукає досяжні стани з порушенням property. Працює добре на ізольованих контрактах. На протоколі з 20 контрактів з cross‑contract викликами — path explosion, аналіз зависає або видає false positive.
Обидва інструменти обов'язкові як перший pass. Але вони не замінюють ручний аналіз.
Fuzzing: де Echidna та Foundry знаходять реальні баги?
Echidna — property‑based fuzzer від Trail of Bits. Ідея: формулюєш інваріанти контракту як Solidity‑функції (echidna_invariant), Echidna генерує випадкові послідовності викликів і намагається зламати інваріант.
Приклад інваріанта для lending протоколу:
function echidna_total_assets_ge_liabilities() public view returns (bool) {
return totalAssets() >= totalLiabilities();
}
Echidna знайде послідовність deposit → borrow → liquidate → repay, яка порушує цей інваріант. Руками такий кейс не побудуєш — комбінацій занадто багато.
Foundry fuzzing (forge test --fuzz-runs 100000) простіший в інтеграції, якщо команда вже на Foundry. Підтримує stateful fuzzing через invariant тести. В реальному проекті: auditing vault контракт, Foundry fuzz за 40 хвилин знайшов edge case, при якому maxWithdraw повертав значення більше фактичного балансу при конкретному співвідношенні shares/assets після кількох донатів. Hardhat unit‑тести цей кейс пропускали — там не було такої комбінації параметрів.
Medusa (від Trail of Bits, новіша за Echidna) підтримує corpus‑guided fuzzing і працює швидше на великих контрактах. Якщо обсяг кодової бази > 5000 рядків Solidity — дивимося на Medusa.
Як інваріанти допомагають виявити критичні уразливості?
Формальна верифікація доводить, що контракт задовольняє специфікації для всіх можливих вхідних даних — не для N випадкових, а математично для всіх. Інструменти: Certora Prover, K Framework, Halmos.
Certora працює з CVL (Certora Verification Language): пишеш rules і invariants, Prover транслює їх у SMT‑формули і перевіряє через Z3/CVC5. MakerDAO, Aave, Uniswap використовують Certora в CI/CD pipeline — кожен PR верифікується автоматично.
Обмеження: не працює з необмеженими циклами, складно справляється з hash functions і signature verification. Для контрактів з простою математикою (AMM, lending) — відмінно. Для контрактів з довільними зовнішніми викликами — складно написати достатньо повну специфікацію.
Formal verification має сенс для контрактів, які: керують значним TVL, оновлюються рідко, мають чітко формалізовані інваріанти. Для продуктів, що швидко ітеруються, співвідношення витрат і користі не на користь верифікації.
Вектори атак, які пропускають джуніор‑аудитори
Storage collision в proxy патерні. Transparent proxy і UUPS використовують конкретні слоти для зберігання адреси імплементації (EIP‑1967). Якщо в імплементації випадково оголошена змінна в слоті 0, яка перетинається з proxy storage — отримуємо silent override. Slither це не впіймає, якщо proxy та імплементація в різних файлах.
Read‑only reentrancy. Класичний reentrancy guard захищає від зміни стану при рекурсивному виклику. Але якщо зовнішній контракт читає стан через view-функцію в середині транзакції — guard не допомагає. Кілька років тому Curve pools стали вектором атаки саме через це: зовнішній протокол читав get_virtual_price під час reentrancy‑вразливого стану Curve.
Oracle manipulation через TWAP. Spot price — стандартна ціль для flash loan атаки. TWAP складніше маніпулювати, але не неможливо: на малоліквідних парах Uniswap v2 можна зсунути TWAP за кілька блоків при достатньому капіталі. Правильний захист — використовувати Chainlink як primary oracle з TWAP як fallback, з перевіркою deviation threshold.
Gas griefing на unbounded loop. Функція ітерується по масиву користувачів. Атакуючий додає тисячі адрес з нульовими балансами — вартість виклику функції зростає до gas limit, функція стає недоступною. Захист: pull‑pattern замість push, обмеження довжини масивів, batch‑обробка зі збереженням позиції.
Front‑running на MEV. Транзакція видна в mempool до включення в блок. MEV‑бот бачить addLiquidity на значну суму, вставляє свій swap перед нею (sandwich attack). Для AMM це частина моделі. Для протоколів з ціновими функціями — потрібен minAmountOut / deadline параметр і його обов'язкова перевірка.
Структура повного аудиту
-
Scope definition і автоматичний аналіз (1‑2 дні). Фіксуємо commit hash, версію компілятора, список out‑of‑scope. Запускаємо Slither, Mythril, Aderyn. Triage: відокремлюємо реальні критичні баги від false positive. Складаємо карту залежностей контрактів.
-
Ручний аналіз (5‑15 днів). Кожен контракт порядково. Особлива увага: всі external і public функції, всі transfer/call/delegatecall, всі місця, де змінюється стан перед перевіркою або після зовнішнього виклику, всі математичні операції з участю користувацьких inputs. В середньому 95% знайдених уразливостей — логічні, а не технічні.
-
Fuzzing і тестування (2‑5 днів). Echidna або Foundry invariant tests для критичних інваріантів. Fork mainnet тести — перевіряємо поведінку в реальному оточенні з реальними оракулами. Наприклад, за 4 дні fuzzing знаходить в середньому 3 edge cases, не покритих unit‑тестами.
-
Звіт і мітигація. Звіт з severity (Critical/High/Medium/Low/Informational), описом вектора атаки, PoC‑кодом для Critical/High. Розробники виправляють, аудитори роблять re‑audit виправлень.
| Severity |
Приклади |
Чи потребує re‑audit |
| Critical |
Виведення коштів, несанкціоноване перенесення власності |
Завжди |
| High |
Маніпуляція, DoS на ключові функції |
Завжди |
| Medium |
Некоректна поведінка при edge cases |
Рекомендується |
| Low |
Газ‑неефективність, опечатки в events |
За бажанням |
Аудит у CI/CD
Нормальна практика для зрілих протоколів: Slither і Aderyn запускаються в GitHub Actions на кожен PR. Certora Prover — на merge в main. Це не замінює повний аудит перед деплоєм, але ловить регресії.
# .github/workflows/audit.yml
- name: Run Slither
uses: crytic/[email protected]
with:
target: 'src/'
slither-args: '--filter-paths "test|mock|script"'
Чек‑лист обов'язкових перевірок перед деплоєм
- Всі external функції мають перевірки доступу (
onlyOwner, onlyRole)
- Використання
SafeERC20 для зовнішніх токенів
- Відсутність
delegatecall на невідомі адреси
- Перевірка на reentrancy у всіх функціях з зовнішніми викликами
- Наявність
minAmountOut і deadline в AMM‑функціях
- Використання перевіреного оракула (Chainlink) з deviation threshold
Інструменти аудиту: порівняння
| Інструмент |
Тип аналізу |
Що знаходить |
Обмеження |
| Slither |
Статичний |
Reentrancy, integer overflow, access control |
Пропускає логічні уразливості |
| Mythril |
Symbolic execution |
Досяжні стани з порушенням property |
Path explosion на великих базах |
| Echidna |
Fuzzing (property‑based) |
Порушення інваріантів |
Потребує написання інваріантів |
| Certora |
Formal verification |
Математичне доведення властивостей |
Не працює з хешами/підписами |
Що входить в роботу (deliverables)
- Повний звіт у PDF з CVSS‑оцінками кожної уразливості
- PoC‑код для всіх Critical і High (відтворюваний в тестовому середовищі)
- Рекомендації щодо виправлення з прикладом коду
- Re‑audit після внесення правок (до двох ітерацій)
- Коротка пам'ятка для розробників щодо подальшої експлуатації
- Підтримка після деплою протягом 30 днів (консультації та розбір інцидентів)
Терміни
Аудит простого токена або NFT‑контракту — 3‑5 робочих днів. DeFi протокол з lending/AMM — 2‑4 тижні. Повний стек з кількома протоколами, cross‑chain, proxy upgrades — 4‑8 тижнів. Re‑audit виправлень — 3‑7 днів окремо.
Наша команда має 7+ років досвіду в безпеці смарт‑контрактів, перевірила 100+ проектів з сумарним TVL понад значну суму. Гарантуємо, що в процесі ми не пропустимо жоден відомий вектор — використовуємо ліцензовані версії Slither та найкращі конфігурації fuzzer'ів. Запобігнуті збитки для клієнтів оцінюються в значну суму.
Оцініть ваш проект — ми безкоштовно проаналізуємо код і запропонуємо комерційну пропозицію протягом 2 днів. Замовте аудит з гарантією якості та отримайте знижку на re‑audit при повторному зверненні.