Безопасность Web3: с нуляЧасть 7 из 12SmartAlert5 мин чтения

Манипуляция оракулом и ценой, флэш-займы: чему протокол верит про цены, и случай KiloEx

Как атакующий двигает цену, которой верит протокол, почему флэш-займы делают это бесплатным, как выглядел взлом KiloEx. Какие алерты помогают.

*Часть 7 из 12 серии «Безопасность Web3: с нуля».*

Две предыдущие части были про ключи и подписантов. Здесь другой вид поломки: никто не крадёт ключ, ни один параметр не испорчен, контракт делает ровно то, что в нём написано. Просто делает это по ложной цене.

Как это устроено

Контракт не видит внешний мир. Но кредитным рынкам, биржам перпетуалов и хранилищам нужно знать, сколько стоит токен, и они спрашивают оракул, источник цен. Оракул — это то, чему протокол решил верить. Бывает несколько видов:

  • Спот-цена из пула. Контракт читает соотношение двух токенов в DEX-пуле, например по резервам. Дёшево и всегда актуально, но это ровно то, что сделала последняя сделка.
  • Средневзвешенная по времени цена (TWAP). Цена пула, усреднённая за окно. Двигать её труднее: искажение надо удерживать много блоков.
  • Push-фид. Сеть вне цепи, например Chainlink, записывает цены в блокчейн: когда цена сдвигается на заданное отклонение или проходит интервал heartbeat. Контракт читает последнее значение.
  • Цена, которую выставляет собственный keeper или контракт протокола. Быстро, и надёжно ровно настолько, насколько надёжен тот, кто может вызвать функцию установки цены.

Манипуляция ценой — это заставить оракул на мгновение показать неверное число и воспользоваться протоколом в этот момент: взять заём под завышенный залог, открыть позицию по низкой цене и закрыть по высокой, выпустить токены по дешёвому курсу.

Почему важны флэш-займы

Чтобы сдвинуть цену пула, нужны большие деньги, и ещё несколько лет назад это было барьером. Флэш-заём его убрал. Кредитный пул даёт любую сумму без залога при одном условии: заём возвращается внутри той же транзакции. Если нет, вся транзакция откатывается, как будто ничего не было.

Обычная последовательность, и всё в одной транзакции:

  1. Взять большую сумму флэш-займом.
  2. Обменять её в тонком пуле и увести цену далеко от настоящей.
  3. Воспользоваться протоколом-жертвой по ложной цене и вынести из него прибыль.
  4. Обменять обратно, вернуть заём с небольшой комиссией, оставить разницу.

Атакующий рискует только газом. А раз всё происходит в одной транзакции, в цепи нет состояния, где цена неверна: транзакция либо проходит целиком, либо её не существует. Между шагом 2 и шагом 4 нет момента, на который мог бы посмотреть монитор.

Это главное, что нужно унести в следующую часть. Манипуляция внутри одной транзакции оставляет след, когда деньги уже ушли.

KiloEx, апрель 2025

KiloEx — биржа перпетуалов, работавшая в BNB Chain, opBNB и Base. 14 апреля 2025 атакующий сманипулировал её ценовым оракулом и забрал около $7,5 млн12. Первопричину называет собственный пост-мортем KiloEx1; мы её не пересказываем, а показываем только то, что было видно в цепи.

В 18:52:27 UTC из пулов в BNB Chain ушло около $893 тыс., а из opBNB около $2,9 млн, в одну и ту же секунду. В 18:53:27 из Base ушло ещё $3,13 млн. Более мелкие переводы шли до 19:55:33. Атака шла с одного адреса сразу по трём сетям3.

Посчитайте время. Алерт на первый крупный вывод из пула в BNB Chain приходит менее чем через 2 минуты. К этому моменту почти всё уже ушло. По нашему расчёту после такого алерта оставались только хвосты, около $0,4 млн. А opBNB SmartAlert вообще не индексирует, так что $2,9 млн потерь мы в любом случае не видели бы.

Это типичная картина манипуляции с коротким окном. Сигнал — это сама потеря, а не предупреждение до неё.

Что монитор видит и чего не видит

Не видит: цену внутри транзакции. Правило читает подтверждённые блоки (в Ethereum это 12 подтверждений от головы) и спрашивает функцию в лучшем случае раз в минуту. Цена, ложная на время одной транзакции, в подтверждённом блоке как состояние не появляется никогда. Если ваш оракул — пул, поймать манипуляцию чтением цены нельзя.

Видит:

  • Цену, которая остаётся неверной несколько блоков. Если атака меняет цену фида или keeper'а и её не возвращают сразу, правило состояния увидит значение. Так бывает после компрометации ключа, который выставляет цену, или когда фид перестал обновляться.
  • Вывод: токены уходят из пула или хранилища, в долларах.
  • Всплеск: число обменов сильно выше нормы для этого часа.

Как поймать

  1. Следите за источником цены правилом «Состояние контракта». Если протокол читает фид через view-функцию с одним числом, например latestAnswer(), создайте на неё правило: scale — число знаков фида (8 у USD-фидов Chainlink), срабатывание выше или ниже разумного диапазона либо «отклонение от нормы» относительно того же часа прошлых дней. Сначала нажмите «Проверить функцию» и посмотрите текущий ответ. Pro и выше, вызов раз в минуту. Так ловится застывший или неверный фид. Манипуляция в одной транзакции так не ловится.
  2. Следите за выводом в долларах. Перевод больше суммы в $ на токене пула, from = пул или хранилище. Для нативных ETH и BNB, у которых нет события Transfer, добавьте адрес в «Адреса» и используйте «суммарный баланс адреса в $ ниже порога».
  3. Следите за всплеском торговли. Всплеск обменов и вывод ликвидности на пулах в стиле Uniswap V2 сравнивают каждый час с тем же часом прошлых дней. Манипуляция на тонком пуле видна как всплеск.
  4. Поставьте те же правила в каждой сети, где работает протокол. KiloEx был в трёх сетях одновременно. Если ваше развёртывание есть в сети, которую поддерживает SmartAlert, добавьте его и там; у KiloEx разрыв между сетями был в секунды, так что это помогает охвату, а не времени.

Где правила у SmartAlert нет:

  • Спот-цена пула. getReserves() и slot0() в Uniswap V3 возвращают сразу несколько значений, а правила состояния понимают ответ только в виде одного bool, адреса или числа. Следить за ценой пула напрямую мы не можем.
  • Свежесть фида. latestRoundData(), где есть время последнего обновления, тоже возвращает несколько значений. Алерта «фид устарел» у нас нет, можно следить только за самой ценой.
  • Манипуляция внутри одной транзакции, при любом источнике цены. Для этого нужна симуляция до подтверждения транзакции и пауза, которая срабатывает сама. Это другой класс продукта, и часть 8 как раз о нём.

Самая надёжная защита от этого класса атак лежит в устройстве протокола, а не в мониторинге: брать TWAP или внешний фид вместо спот-цены, ограничить, насколько цена может сдвинуться за одно обновление, и ограничить круг тех, кто может её выставлять.

Дальше: часть 8, взлом одной транзакцией и чего мониторинг не может.

Источники

  1. 1.KiloEx, Security incident root cause analysis: причина, $7,5 млн, сети. ↩
  2. 2.KiloEx в X, заявление об инциденте. ↩
  3. 3.Rekt, KiloEx — Rekt: атака по BNB Chain, opBNB и Base. Время транзакций и суммы по сетям: SmartAlert, on-chain. ↩

Попробуйте бесплатно

Достаточно адреса. Первые 14 дней действуют лимиты Pro, а правила проверяются на истории до включения.

Ещё статьи