MEV, оракулы и мосты: три места, где деньги утекают мимо кода
Прошлый урок был про ошибки внутри контракта. Теперь про потери, которые случаются, когда контракт написан безупречно. Три источника: тот, кто решает порядок транзакций, зарабатывает на этом; данные о внешнем мире приходят из ненадёжного места; а деньги между цепями ходят через конструкции, на которых индустрия потеряла больше, чем на всех остальных взломах вместе.
MEV: цена права упорядочивать
Начнём с наблюдения. Транзакции попадают в блок не в том порядке, в каком отправлены, а в том, какой выбрал тот, кто собирает блок. Этот выбор стоит денег, и его стоимость называют MEV.
Это не уязвимость, которую можно закрыть патчем. Пока кто-то решает порядок, у этого решения есть цена. Вопрос лишь в том, кому она достаётся и какая часть её отбирается у пользователя.
Виды MEV полезно делить по тому, кто платит.
Арбитраж. Цена в двух пулах разъехалась, арбитражёр выравнивает. Платят те, кто держал неверную цену, и системе это полезно: без арбитражёров курсы бы разъезжались.
Ликвидации. Залог подешевел, позиция стала недообеспеченной, ликвидатор её закрывает и берёт премию. Тоже необходимая функция: без неё протокол копил бы безнадёжные долги.
Сэндвич. Атакующий видит крупную сделку, покупает перед ней, продаёт после. Платит конкретный пользователь, и никакой пользы системе тут нет. Это чистое изъятие.
Ликвидность вовремя. Поставщик вкладывает средства в узкий диапазон прямо перед крупной сделкой и забирает сразу после, снимая комиссию и не неся риска. Платят обычные поставщики ликвидности, которые сидели в позиции всё время.
Сэндвич в цифрах
Посчитаем, сколько это стоит жертве. Пул с постоянным произведением разберём подробно в уроке про математику AMM, пока достаточно правила: чем больше сделка относительно пула, тем хуже курс.
Главный параметр здесь допуск проскальзывания: пользователь указывает, какой минимум он согласен получить. Этот же параметр говорит атакующему, насколько сильно ему разрешено испортить пользователю цену.
/// Пул с постоянным произведением: x * y = k. Комиссия берётся с входа.
#[derive(Clone, Copy)]
struct Pool { x: f64, y: f64, fee: f64 }
impl Pool {
/// Обмен dx токена X на токен Y. Возвращает полученный Y и новый пул.
fn swap_x_for_y(self, dx: f64) -> (f64, Pool) {
let dx_eff = dx * (1.0 - self.fee);
let dy = self.y * dx_eff / (self.x + dx_eff);
(dy, Pool { x: self.x + dx, y: self.y - dy, ..self })
}
fn swap_y_for_x(self, dy: f64) -> (f64, Pool) {
let dy_eff = dy * (1.0 - self.fee);
let dx = self.x * dy_eff / (self.y + dy_eff);
(dx, Pool { x: self.x - dx, y: self.y + dy, ..self })
}
}
/// Сэндвич: атакующий покупает перед жертвой и продаёт сразу после.
/// Возвращает (прибыль атакующего в X, выход жертвы).
fn sandwich(pool: Pool, victim_dx: f64, attacker_dx: f64) -> (f64, f64) {
let (bought, pool) = pool.swap_x_for_y(attacker_dx);
let (victim_out, pool) = pool.swap_x_for_y(victim_dx);
let (returned, _) = pool.swap_y_for_x(bought);
(returned - attacker_dx, victim_out)
}
fn main() {
let pool = Pool { x: 1_000_000.0, y: 1_000_000.0, fee: 0.003 };
let victim_dx = 10_000.0;
let (honest_out, _) = pool.swap_x_for_y(victim_dx);
println!("без атаки жертва получает {honest_out:.2}");
for tolerance in [0.001, 0.005, 0.01, 0.05] {
let min_out = honest_out * (1.0 - tolerance);
// Атакующий подбирает максимальный вход, при котором жертва ещё не
// сорвётся по проскальзыванию: сделка обязана пройти.
let mut best = (0.0, 0.0, 0.0);
let mut dx = 0.0;
while dx < 400_000.0 {
dx += 100.0;
let (profit, victim_out) = sandwich(pool, victim_dx, dx);
if victim_out >= min_out && profit > best.0 {
best = (profit, dx, victim_out);
}
}
println!(
"допуск {:.1}%: прибыль {:.2}, вход атакующего {:.0}, жертва получила {:.2} (потеряла {:.2})",
tolerance * 100.0, best.0, best.1, best.2, honest_out - best.2
);
}
}
Вывод:
без атаки жертва получает 9871.58
допуск 0.1%: прибыль 6.97, вход атакующего 500, жертва получила 9861.78 (потеряла 9.80)
допуск 0.5%: прибыль 34.75, вход атакующего 2500, жертва получила 9822.72 (потеряла 48.86)
допуск 1.0%: прибыль 69.20, вход атакующего 5000, жертва получила 9774.23 (потеряла 97.35)
допуск 5.0%: прибыль 348.54, вход атакующего 26100, жертва получила 9378.85 (потеряла 492.73)
Зависимость прямая: допуск это разрешение. Поставил пять процентов на всякий случай, чтобы сделка точно прошла, и подарил атакующему возможность забрать почти пять процентов. Стандартный интерфейсный совет «увеличьте slippage, если транзакция срывается» на деле означает «увеличьте вознаграждение тому, кто вас обгонит».
Обрати внимание и на то, чего в листинге нет: никакого взлома. Все три транзакции полностью законны, контракт отработал по правилам. Атака целиком лежит в порядке.
Цепочка поставки блока
Как атакующий вообще вставляет свои транзакции вплотную к чужой? Через рынок, который вырос вокруг права упорядочивать.
Роли такие. Поисковик (searcher) следит за очередью и собирает связку транзакций, которая приносит прибыль. Сборщик (builder) складывает из таких связок самый доходный блок. Ретранслятор (relay) передаёт блок валидатору, не раскрывая содержимого заранее. Валидатор просто выбирает самый доходный блок из предложенных и подписывает его.
Смысл разделения в том, что валидатору не нужно уметь извлекать MEV самому: иначе выигрывали бы крупные профессиональные операторы, и это выдавило бы мелких. Разделение выравнивает поле, но взамен создаёт узкое место: несколько сборщиков и ретрансляторов пропускают через себя основную часть блоков, и это своя форма централизации со своей политикой (например, отказ включать транзакции из санкционных списков).
Ключевая деталь для пользователя: связку можно отправить приватно, минуя открытую очередь. Это и есть основной способ защиты, к которому мы сейчас перейдём.
Как защищается пользователь
Приватная отправка. Транзакция уходит не в общую очередь, а напрямую сборщикам. Никто её не видит, обгонять нечего. Плата за это доверие к тому, кому отправляешь.
Разумный допуск. См. таблицу выше: допуск это то, сколько ты согласен подарить. Ставить его большим ради надёжности означает платить за надёжность деньгами.
Аукцион вместо гонки. Пользователь подписывает намерение («хочу обменять A на B не хуже такого-то курса»), а исполнители соревнуются, кто исполнит выгоднее. Тогда конкуренция работает на пользователя, а не против него. По этой модели живут батч-аукционы и системы решателей.
Возврат части MEV. Некоторые кошельки и сети возвращают пользователю часть прибыли, которую заработали на его транзакции. Это признание простой мысли: раз изъятие неустранимо, пусть оно хотя бы делится.
Направления развития: шифрование содержимого до момента включения в блок (тогда упорядочивающий не видит, что упорядочивает), доверенные аппаратные среды и справедливое упорядочивание по времени получения. Ни одно пока не стало стандартом.
Отдельно про L2. Там порядок определяет секвенсер, обычно один, и MEV достаётся ему. Формально это выглядит спокойнее (нет публичного аукциона), фактически означает, что вся экономика упорядочивания сосредоточена в руках оператора цепи.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.