Разработка системы защиты от сэндвич-атак
Сэндвич-атака — это форма MEV (Maximal Extractable Value), при которой атакующий видит pending транзакцию пользователя в mempool, выставляет свою транзакцию с более высоким газом перед ней (frontrun) и ещё одну сразу после (backrun). Пользователь получает значительно худший курс обмена, разница уходит атакующему.
В Uniswap-подобных AMM это работает прямолинейно: атакующий покупает токен до пользователя (цена растёт), продаёт после (цена возвращается вниз). Разница — прибыль MEV-бота. По данным EigenPhi, крупные sandwich-боты на Ethereum зарабатывали сотни тысяч долларов в неделю за счёт обычных пользователей.
Механика атаки и слабые места
Для понимания защиты нужно чётко понимать, где именно уязвимость.
Публичный mempool: транзакции видны всем до включения в блок. MEV-бот читает параметры swap (адрес токена, сумму, slippage tolerance) и вычисляет прибыльность атаки.
Высокий slippage tolerance: пользователь указывает допустимое отклонение цены (например, 5%). Чем выше tolerance — тем шире возможное окно для сэндвича. Swap с slippage 1% атаковать значительно сложнее.
Детерминированный price impact: для AMM с известной формулой (x*y=k или стабильная curve) атакующий точно вычисляет, сколько нужно купить перед жертвой для максимальной прибыли.
Защиты на уровне пользователя и протокола
Приватные RPC и MEV-protection провайдеры
Самая простая защита: отправлять транзакции не в публичный mempool, а напрямую блок-строителям через приватные каналы.
Flashbots Protect: транзакции идут в Flashbots bundle. Атакующий не видит их в публичном mempool. Бесплатно для пользователя.
MEV Blocker: сервис от CoW Protocol. Транзакции отправляются нескольким searcher'ам, которые конкурируют за best execution (refund части MEV обратно пользователю).
Bloxroute: платный сервис с защищёнными каналами к майнерам/валидаторам.
// Использование Flashbots Protect через ethers.js
const { FlashbotsBundleProvider } = require('@flashbots/ethers-provider-bundle');
const { ethers } = require('ethers');
async function protectedSwap(swapParams, wallet, provider) {
// Подключаемся к Flashbots relay
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
wallet, // auth signer (может быть отдельный кошелёк)
'https://relay.flashbots.net'
);
// Собираем swap транзакцию как обычно
const swapTx = await buildSwapTransaction(swapParams);
// Отправляем через Flashbots — транзакция не попадает в публичный mempool
const bundleSubmission = await flashbotsProvider.sendPrivateTransaction(
{
signer: wallet,
transaction: swapTx
},
{ maxBlockNumber: (await provider.getBlockNumber()) + 10 }
);
const receipt = await bundleSubmission.wait();
return receipt;
}
Минимизация slippage tolerance
Протоколы должны рекомендовать и применять разумные slippage по умолчанию вместо завышенных.
// В смарт-контракте: жёсткий лимит на максимальный slippage
contract SlippageProtectedRouter {
uint256 public constant MAX_SLIPPAGE_BPS = 300; // 3% максимум
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to
) external returns (uint256[] memory amounts) {
// Вычисляем текущую цену и проверяем slippage
uint256 expectedOut = getAmountOut(amountIn, path);
uint256 slippageBps = (expectedOut - amountOutMin) * 10000 / expectedOut;
require(slippageBps <= MAX_SLIPPAGE_BPS, "Slippage too high");
return _executeSwap(amountIn, amountOutMin, path, to);
}
}
Commit-reveal схема
Пользователь в первой транзакции публикует commitment (хеш параметров swap), во второй — раскрывает. MEV-боты не знают параметры до второй транзакции, когда уже поздно встраиваться перед ней.
contract CommitRevealSwap {
mapping(bytes32 => Commitment) public commitments;
struct Commitment {
address user;
uint256 blockNumber;
bool revealed;
}
uint256 public constant MIN_BLOCKS_BEFORE_REVEAL = 1;
uint256 public constant MAX_BLOCKS_BEFORE_REVEAL = 10;
// Шаг 1: пользователь публикует хеш параметров
function commit(bytes32 commitHash) external {
commitments[commitHash] = Commitment({
user: msg.sender,
blockNumber: block.number,
revealed: false
});
}
// Шаг 2: раскрытие и исполнение
function reveal(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
bytes32 salt
) external {
bytes32 commitHash = keccak256(abi.encodePacked(
msg.sender, amountIn, amountOutMin,
keccak256(abi.encode(path)), salt
));
Commitment storage commitment = commitments[commitHash];
require(commitment.user == msg.sender, "Not your commit");
require(!commitment.revealed, "Already revealed");
require(
block.number >= commitment.blockNumber + MIN_BLOCKS_BEFORE_REVEAL,
"Too early"
);
require(
block.number <= commitment.blockNumber + MAX_BLOCKS_BEFORE_REVEAL,
"Expired"
);
commitment.revealed = true;
_executeSwap(amountIn, amountOutMin, path, msg.sender);
}
}
Недостаток: двухэтапный процесс создаёт UX трения. Применим для крупных свапов, неудобен для частых небольших операций.
Batch Auctions: архитектурное решение CoW Protocol
CoW Protocol решает проблему сэндвичей на архитектурном уровне через batch auctions. Вместо немедленного исполнения каждого swap:
- Заявки на swap собираются за период (несколько блоков)
- Off-chain solver находит оптимальное исполнение для всего батча (CoW — Coincidence of Wants: если A меняет ETH на USDC, а B меняет USDC на ETH — можно исполнить напрямую)
- Единая транзакция с результатами батча публикуется on-chain
- Все участники батча получают uniform clearing price
В batch auction нет смысла для сэндвича: атакующий не может встроиться между заявкой и исполнением, потому что они разнесены во времени и исполняются оптом.
Обычный AMM:
Block N: frontrun_buy → user_swap → backrun_sell
(атакующий заработал за счёт user_swap)
CoW Batch Auction:
Block N: submit_order(user1), submit_order(user2), ...
Block N+3: solver publishes settlement_tx с uniform price
(нет места для frontrun/backrun внутри батча)
Dynamic Slippage на основе on-chain условий
Смарт-контракт или frontend может динамически вычислять разумный slippage на основе текущей рыночной волатильности и объёма свапа.
async function calculateSafeDynamicSlippage(
tokenIn, tokenOut, amountIn, provider
) {
// Получаем TWAP из Uniswap V3 oracle
const [twapPrice, spotPrice] = await Promise.all([
getTWAPPrice(tokenIn, tokenOut, 1800, provider), // 30-min TWAP
getSpotPrice(tokenIn, tokenOut, provider)
]);
// Текущее отклонение spot от TWAP
const currentDeviation = Math.abs(spotPrice - twapPrice) / twapPrice;
// Оценка price impact от нашего свапа
const priceImpact = await estimatePriceImpact(tokenIn, tokenOut, amountIn, provider);
// Базовый slippage + буфер на волатильность
const baseSlippage = priceImpact * 1.2; // 20% буфер сверх price impact
const volatilityBuffer = currentDeviation * 0.5;
const recommendedSlippage = Math.min(
baseSlippage + volatilityBuffer,
0.03 // максимум 3%
);
return {
recommendedSlippageBps: Math.ceil(recommendedSlippage * 10000),
priceImpact: priceImpact,
currentVolatility: currentDeviation
};
}
Мониторинг и детекция атак
Система защиты должна включать компонент мониторинга: отслеживание конкретных MEV-ботов, статистика потерь пользователей, алерты при всплеске сэндвич-активности.
// Детекция сэндвич паттерна через анализ транзакций в блоке
async function detectSandwichInBlock(blockNumber, provider, uniswapAddress) {
const block = await provider.getBlock(blockNumber, true);
const uniswapTxs = block.transactions.filter(
tx => tx.to?.toLowerCase() === uniswapAddress.toLowerCase()
);
const sandwiches = [];
for (let i = 1; i < uniswapTxs.length - 1; i++) {
const prev = uniswapTxs[i - 1];
const current = uniswapTxs[i];
const next = uniswapTxs[i + 1];
// Признаки сэндвича: prev и next от одного адреса,
// current — от другого, противоположные направления
if (prev.from === next.from && prev.from !== current.from) {
const prevDecoded = decodeSwap(prev.data);
const nextDecoded = decodeSwap(next.data);
// Frontrun покупает то, что продаёт жертва
if (prevDecoded && nextDecoded &&
prevDecoded.tokenIn === nextDecoded.tokenOut) {
sandwiches.push({
attacker: prev.from,
victim: current.from,
frontrunTx: prev.hash,
victimTx: current.hash,
backrunTx: next.hash,
estimatedProfit: calculateSandwichProfit(prevDecoded, nextDecoded)
});
}
}
}
return sandwiches;
}
Сроки разработки
Интеграция Flashbots Protect в существующий swap интерфейс — 1–2 недели. Commit-reveal механизм в смарт-контракте — 2–3 недели включая тесты. Динамический slippage с TWAP-оракулом — 2–3 недели. Мониторинговая система детекции сэндвичей — 3–4 недели. Полный batch auction протокол по модели CoW — 2–4 месяца.







