Розробка системи захисту від сендвіч-атак
Сендвіч-атака—форма 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 або stable curve) зловмисник точно обчислює, скільки потрібно купити перед жертвою для максимального прибутку.
Захист на рівні користувача та протоколу
Приватні RPC та MEV-protection провайдери
Найпростіший захист: відправляти транзакції не в публічний mempool, а напрямку блок-будівельникам через приватні канали.
Flashbots Protect: транзакції йдуть у Flashbots bundle. Зловмисник не бачить їх у публічному mempool. Безплатно для користувача.
MEV Blocker: сервіс від CoW Protocol. Транзакції відправляються кільком searcher-ам, які конкурують за best execution (повернення частини 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 всередині батчу)
Динамичний 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 місяці.







