Чистая экономика MEV сводится к жесткой конкуренции за блоки транзакций, где миллисекундная задержка сети уничтожает весь потенциальный профит. Запуск собственного бота на Ethereum или EVM-сетях второго уровня давно не приносит легких денег. Рынок поделен между специализированными поисковиками, арендующими серверы в дата-центрах Франкфурта и Токио прямо у узлов валидаторов.
Реальные цифры и доходность
Одиночный бот, запущенный с домашнего сервера или стандартного облачного VPS, в 95% случаев работает в минус из-за газовых аукционов. Доходность зависит от стратегии:
- Атомарный арбитраж (Atomic Arbitrage): Маржинальность сделок упала до доли процента. Чистый дневной профит успешного бота составляет от $100 до $3000 в периоды волатильности. Риск потери капитала нулевой, так как транзакция заворачивается в один атомарный смарт-контракт: если условия не сошлись, вся цепочка откатывается.
- Сэндвич-атаки (Sandwich Attacks): Теоретическая доходность выше, от $500 до $10000 в сутки на парах с низкой ликвидностью в Uniswap v2 или аналогичных AMM. Практика жестче. Конкурентные боты выстраивают очереди через частные пулы транзакций, перебивая чужие взятки валидаторам.
- Ликвидации (Liquidations): Спот-рынок кредитных протоколов вроде Aave приносит крупный куш, но требует миллионов долларов ликвидности для покрытия долгов и мгновенного погашения позиций. Одиночкам здесь делать нечего.
Структура расходов на газ и работу через билдеры
Главная статья расходов при запуске MEV-бота не успешные транзакции, а неудачные попытки и аукционы за приоритет включения (Gas Priority Auction).
- Публичный мемпул: Отправка транзакции в обход частных релеев гарантирует убыток. Обычный бот видит транзакцию жертвы, шлет свою с более высоким газом, но боты-конкуренты перехватывают ее первыми через приватные RPC. В итоге ваша транзакция исполняется по худшей цене или вовсе ревертится, сжигая реальный газ в блокчейне.
- Билдеры блоков и Flashbots: Современный арбитраж строится через eth_sendBundle. Вы формируете бандл из нескольких транзакций и отправляете его напрямую валидаторам (Beaverbuild, Titan, Flashbots).
- Плата за неудачу: Плюс частных бандлов в том, что при несостоявшемся арбитраже транзакция просто не попадает в блок. Вы не платите комиссию сети за промах. Минус - валидатор забирает львиную долю прибыли в виде прямой взятки (coinbase.transfer), оставляя разработчику от 5 до 15% грязного профита.
Таблица метрик MEV-стратегий
| Стратегия | Капитал на старт | Средний дневной чистый профит | Вероятность успеха | Основная причина убытка |
|---|---|---|---|---|
| Атомарный арбитраж | от $5,000 | $50 - $500 | 15-25% | Опережение конкурентов по задержке сети (latency) |
| Сэндвич-атаки | от $20,000 | $200 - $3,000 | 5-10% | Переплата за газ в аукционе валидаторов |
| Ликвидации | от $100,000+ | $1,000 - $10,000+ | < 2% | Нехватка капитала для погашения крупных займов |
Пример отправки приватного бандла на Python
Ниже представлен рабочий каркас скрипта на Python с использованием библиотеки web3.py для отправки бандла через Flashbots-совместимый провайдер. Код очищен от лишних абстракций.
import os
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from flashbots import flashbot
# Ключ для подписи запросов к relay (репутация в Flashbots).
# НЕ используйте ключ, на котором лежат средства.
signature_account: LocalAccount = Account.from_key(os.environ["FLASHBOTS_SIGNER_KEY"])
# Ключ отправителя транзакции
sender: LocalAccount = Account.from_key(os.environ["SENDER_PRIVATE_KEY"])
w3 = Web3(Web3.HTTPProvider(os.environ["ETH_NODE_URL"]))
flashbot(w3, signature_account, "https://relay-sepolia.flashbots.net")
def build_signed_tx(nonce: int) -> bytes:
base_fee = w3.eth.get_block("latest")["baseFeePerGas"]
tx = {
"to": sender.address, # пример: перевод самому себе
"value": 0,
"gas": 21_000,
"maxFeePerGas": base_fee * 2 + Web3.to_wei(2, "gwei"),
"maxPriorityFeePerGas": Web3.to_wei(2, "gwei"),
"nonce": nonce,
"chainId": w3.eth.chain_id,
"type": 2,
}
return sender.sign_transaction(tx).rawTransaction # в новых версиях: .raw_transaction
def send_private_bundle(target_block: int):
nonce = w3.eth.get_transaction_count(sender.address)
bundle = [{"signed_transaction": build_signed_tx(nonce)}]
# Симуляция перед отправкой
w3.flashbots.simulate(bundle, target_block)
result = w3.flashbots.send_bundle(bundle, target_block_number=target_block)
result.wait() # ждёт, пока блок target_block будет обработан
try:
return result.receipts()
except Exception:
return None # бандл не включён в блок: можно повторить на следующем блоке
if __name__ == "__main__":
target = w3.eth.block_number + 1
receipts = send_private_bundle(target)
print("Включён в блок" if receipts else "Бандл не включён, повторите на следующем блоке")Бандл действует только на один целевой блок, поэтому на практике отправку необходимо делать в цикле на несколько блоков подряд. Отсутствие включения не означает ошибку: конкуренция идёт по размеру чаевых билдеру.
Рентабельность собственного бота упирается в стоимость инфраструктуры. Аренда выделенного сервера в стойке с валидатором в дата-центре обходится в сумму от $500 в месяц. Без такого аппаратного ускорения бот физически не успевает просчитать состояние пула раньше соперников. Выигрывает тот, у кого короче кабель до валидатора и тоньше алгоритм поиска спреда.
Флеш-займы дают возможность проводить масштабные арбитражные операции без собственного крупного капитала, используя беззалоговое заимствование внутри единой транзакции с гарантированным откатом при убытках.
Флеш-займы и архитектура смарт-контракта
Иметь собственный капитал в несколько сотен тысяч долларов для арбитража не обязательно. Контракты интеграции с пулами Aave v3 или Balancer позволяют брать беззалоговые флеш-займы в рамках одной транзакции. Бот заимствует актив, прогоняет его через DEX-пары, возвращает долг с учетом комиссии 0.09% и забирает остаток на баланс создателя. Если баланс после всех расчетов уходит в минус, вся транзакция откатывается через revert, а эмитент теряет только газ за вызов функции.
Написание арбитражного контракта на Solidity требует жесткой оптимизации под байт-код EVM. Использование стандартных интерфейсов ERC-20 через SafeERC20 добавляет лишние проверки, которые съедают ценные единицы газа при проверке лимитов транзакции. Опытные разработчики пишут прямые низкоуровневые вызовы через call, минимизируя операции чтения и записи в постоянную память (SLOAD/SSTORE).
Пример Solidity-шаблона
Ниже представлен шаблон на флеш-займе Aave V3 для пулов Uniswap V2, без сторонних библиотек:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
}
/// Aave V3: flash loan одного актива
interface IPool {
function flashLoanSimple(
address receiverAddress,
address asset,
uint256 amount,
bytes calldata params,
uint16 referralCode
) external;
}
interface IUniswapV2Pair {
function token0() external view returns (address);
function getReserves() external view returns (uint112, uint112, uint32);
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}
/// Арбитраж на флеш-займе Aave V3: asset -> tokenB (pair1) -> asset (pair2).
/// Пример для пулов типа Uniswap V2. Не аудировался.
contract FlashArbitrage {
error Unauthorized();
error InvalidCaller();
error InvalidInitiator();
error TransferFailed();
error ApproveFailed();
error InsufficientProfit(uint256 balanceAfter, uint256 required);
address private immutable OWNER;
IPool private immutable POOL;
constructor(address aavePool) {
OWNER = msg.sender;
POOL = IPool(aavePool); // адрес Pool берите из PoolAddressesProvider нужной сети
}
modifier onlyOwner() {
if (msg.sender != OWNER) revert Unauthorized();
_;
}
/// Точка входа. Занимает `amount` токена `asset` и запускает цикл в executeOperation.
function executeArbitrage(
address asset,
uint256 amount,
address tokenB,
address pair1,
address pair2,
uint256 minProfit
) external onlyOwner {
bytes memory params = abi.encode(tokenB, pair1, pair2, minProfit);
POOL.flashLoanSimple(address(this), asset, amount, params, 0);
}
/// Колбэк Aave V3. Вызывается пулом после выдачи займа.
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool) {
// 1. Колбэк может вызвать только пул Aave...
if (msg.sender != address(POOL)) revert InvalidCaller();
// 2. ...и только по запросу самого контракта
if (initiator != address(this)) revert InvalidInitiator();
(address tokenB, address pair1, address pair2, uint256 minProfit) =
abi.decode(params, (address, address, address, uint256));
// Баланс уже включает занятые средства
uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
uint256 amountB = _swap(pair1, asset, amount);
_swap(pair2, tokenB, amountB);
// Прибыль = balanceAfter - balanceBefore - premium (комиссия займа)
uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
uint256 required = balanceBefore + premium + minProfit;
if (balanceAfter < required) revert InsufficientProfit(balanceAfter, required);
// Aave забирает долг сам через transferFrom, поэтому нужен approve
_safeApprove(asset, address(POOL), amount + premium);
return true;
}
function withdraw(address token, uint256 amount) external onlyOwner {
_safeTransfer(token, OWNER, amount);
}
function _swap(address pair, address tokenIn, uint256 amountIn) private returns (uint256 amountOut) {
(uint256 r0, uint256 r1, ) = IUniswapV2Pair(pair).getReserves();
bool zeroForOne = tokenIn == IUniswapV2Pair(pair).token0();
(uint256 reserveIn, uint256 reserveOut) = zeroForOne ? (r0, r1) : (r1, r0);
// Формула Uniswap V2 (комиссия 0,3%)
uint256 amountInWithFee = amountIn * 997;
amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
_safeTransfer(tokenIn, pair, amountIn);
IUniswapV2Pair(pair).swap(
zeroForOne ? 0 : amountOut,
zeroForOne ? amountOut : 0,
address(this),
"" // пустой data: это обычный swap, не flash swap
);
}
function _safeTransfer(address token, address to, uint256 amount) private {
if (token.code.length == 0) revert TransferFailed();
(bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));
if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed();
}
function _safeApprove(address token, address spender, uint256 amount) private {
if (token.code.length == 0) revert ApproveFailed();
(bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.approve, (spender, amount)));
if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert ApproveFailed();
}
}
Инфраструктурный тупик AWS
Размещение поискового бота на виртуальных серверах Amazon Web Services или Google Cloud гарантирует слив бюджета. Виртуализация вносит задержки в сетевой стек, а общие каналы связи подвержены потере пакетов при пиковых нагрузках мемпула.
Конкурентоспособный сетап требует физического выделенного сервера (bare-metal) с процессором высокой тактовой частоты на одно ядро, быстрым NVMe-накопителем под локальную ноду Geth или Reth и прямым гигабитным каналом до дата-центров во Франкфурте. Запуск собственной легковесной ноды обязателен, так как публичные RPC-провайдеры вроде Infura или Alchemy искусственно режут скорость отдачи новых блоков и задерживают нотификации о pending-транзакциях на критические 100-300 миллисекунд.
Прибыль в MEV забирает не тот, у кого сложнее математическая модель, а тот, кто физически ближе к валидатору и умеет считать газовые издержки до каждой операции ассемблера в байткоде.