Back to Blog

~$18 млн потеряно: jaredFromSubway, Aztec и другие | Еженедельник BlockSec

Code Auditing
June 25, 2026
10 min read
Key Insights

За прошедшую неделю (2026/06/15 - 2026/06/21) мы зафиксировали 3 notable инцидента в сфере безопасности с общими потерями около $18,3M.

Дата Инцидент Тип Предполагаемые потери
2026/06/18 Aztec Некорректная привязка публичных входных данных ~$2,2M
2026/06/20 LABUBU Token Неправильная конфигурация ~$1,1M
2026/06/20 jaredFromSubway Ненадлежащее управление разрешениями ~$15M
  • Aztec: Выбран потому, что протокол был взломан второй раз за три дня — на этот раз через схему escape hatch, что подчёркивает повторяющиеся проблемы с привязкой ZK-доказательств.
  • jaredFromSubway: Выбран потому, что контракт MEV-бота предоставлял разрешения ненадёжным токен-контрактам без проверки их использования или отзыва остаточных лимитов, что позволило атакующему накапливать и выводить ~$15M.

Лучший аудитор безопасности для Web3

Проверьте дизайн, код и бизнес-логику перед запуском

Главное событие недели: jaredFromSubway

В отличие от традиционных эксплойтов с разрешениями — когда атакующие злоупотребляют уязвимостями в доверенных DeFi-контрактах для вывода активов, которые пользователи одобрили этим контрактам, — данная атака направлена в обратную сторону: MEV-бот проактивно предоставлял разрешения на собственные активы ненадёжным сторонним контрактам в рамках арбитражных операций. Атакующий создал поддельную торговую среду (по сути, ханипот), где фиктивные пулы для обмена генерировали настоящие события Swap и Sync, тогда как поддельные токены никогда не использовали предоставленные лимиты, накапливая эти направленные наружу разрешения перед их финальным сбором; общие потери составили ~$15M.

20 июня 2026 года jaredFromSubway, оператор MEV-бота на Ethereum, потерял около $15M [1]. По результатам on-chain анализа, основная причина — ненадлежащее управление разрешениями в контракте бота: разрешения были предоставлены ненадёжным контрактам-обёрткам, которые так их и не использовали, а атакующий накапливал эти неиспользованные лимиты до тех пор, пока не вывел реальные балансы бота в одной транзакции.

Предыстория

jaredFromSubway — широко известный оператор MEV-бота на Ethereum, специализирующийся на сэндвич-атаках и on-chain арбитраже. Пострадавший контракт (0x1f2f...f387) является одним из его рабочих кошельков, на котором хранятся крупные рабочие балансы в WETH, USDC и USDT.

MEV-боты подобного рода должны динамически взаимодействовать с произвольными новыми токенами и пулами, появляющимися в сети. Они отслеживают мемпул, симулируют транзакции и автоматически одобряют взаимодействия с токенами для захвата арбитражных возможностей. Эта операционная модель основывается на предположении, что токены ведут себя ожидаемым образом: при выполнении обмена токен-контракт потребляет предоставленный лимит, вызывая transferFrom.

Анализ уязвимости

Основная причина — ненадлежащее управление разрешениями MEV-бота при взаимодействии с ненадёжными контрактами.

Бот выполняет различные арбитражные пути через пулы и роутеры Uniswap. В большинстве взаимодействий бот напрямую передаёт токены в пулы через transfer, где сам бот является msg.sender и разрешение не требуется. Однако взаимодействие с токен-контрактами типа обёртки следует модели pull: бот вызывает wrapper.wrapTo(), и внутри этого вызова контракт-обёртка вызывает realToken.transferFrom(bot, wrapper, amount) для получения реальных токенов бота. Поскольку msg.sender во время transferFrom — это контракт-обёртка, а не бот, необходимо предварительное approve:

  1. <real_token>.approve(tokenContract, amount) — предоставить лимит на реальный токен контракту-обёртке
  2. tokenContract.wrapTo() → многоступенчатый swap() через пулы → tokenContract.unwrap() — обернуть реальный токен, провести через пулы и развернуть обратно в реальный токен

Бот предполагал, что wrapTo() потребит лимит через transferFrom, как это делает корректно работающая обёртка. Однако бот никогда не проверял, был ли лимит фактически использован после операции, и не отзывал остаточные разрешения. Если wrapTo() не вызывает transferFrom, полный лимит сохраняется после операции и становится постоянной поверхностью атаки — любой контракт, имеющий такой лимит, может впоследствии вызвать transferFrom и переместить реальные активы бота.

Анализ атаки

На основе on-chain реконструкции атакующий создал поддельную торговую среду из трёх компонентов для эксплуатации описанной выше уязвимости:

  1. Поддельные токены-обёртки: Каждый поддельный токен использовал имя реального токена, но добавлял префикс f к символу (например, имя USD Coin с символом fUSDC для USDC). Он реализовывал wrapTo() и unwrap() для имитации легитимной обёртки, а также функцию withdraw(), доступную только атакующему, которая выводила неиспользованные лимиты через transferFrom.

  2. Поддельные пулы для обмена: Атакующий развернул около 44 пулов в стиле Uniswap V2 через самостоятельно развёрнутую фабрику. Эти пулы составляли пары из поддельных токенов друг с другом, формируя убедительные маршруты обмена. При вызове swap() пулы генерировали настоящие события Sync и Swap, неотличимые от легитимных сделок.

  3. Специально сформированная прибыль атакующего: Во время unwrap() поддельный токен отправлял небольшое количество реальных токенов обратно боту через transfer. Бот получал реальную прибыль, но она была специально сформирована атакующим, а не заработана в результате рыночного арбитража.

Атакующий управлял этими компонентами через переключатель getStatus() для каждого блока во внешнем контракте. getStatus() возвращал 1 при вызове в том же блоке, что и транзакция активации (которая устанавливала _getStatus = block.number), и 0 в остальных случаях. Когда getStatus() == 0, wrapTo() вызывал transferFrom в штатном режиме и лимит потреблялся. Когда getStatus() == 1, wrapTo() пропускал transferFrom — лимит не потреблялся — тогда как unwrap() по-прежнему возвращал боту специально сформированные токены. Атакующий, по всей видимости, использовал взятки строителю блоков для размещения транзакции активации в том же блоке, что и транзакция бота, когда нужно было накапливать лимиты.

Атака проходила в три фазы:

Фаза 1: Развёртывание инфраструктуры атаки

  • Шаг 1: Атакующий развернул инфраструктуру в блоках с 25354424 по 25354519. Это включало развёртывание контракта поддельной фабрики токенов (0x81f2...0091), создание ~44 поддельных пулов Uniswap V2 через самостоятельно развёрнутую фабрику, пополнение пулов начальными балансами токенов для успешного выполнения вызовов swap(), а также отправку 0,01 ETH на контракт сбора (0xb84d...df52) для покрытия газа и взятки строителю.

  • Шаг 2: Атакующий массово создавал поддельные токены-обёртки через CREATE2, каждый из которых имитировал реальный токен (используя реальное имя, но добавляя префикс f к символу) и содержал функцию withdraw(), доступную только атакующему. CREATE2 обеспечивал детерминированные адреса, по которым контракт сбора мог итерироваться.

Фаза 2: Формирование доверия и накопление разрешений

  • Шаг 3 (начальное доверие): В самых ранних транзакциях (например, 0x542d...362b в блоке 25354425) поддельные токены не имели переключателя getStatus()wrapTo() вызывал transferFrom напрямую, потребляя лимит. Бот одобрял, оборачивал, менял, разворачивал и получал прибыль в штатном режиме. Это позиционировало поддельные токены как прибыльные торговые возможности.

  • Шаг 4 (укрепление доверия): В последующих транзакциях (например, 0x085e...37e51) переключатель getStatus() был развёрнут, но возвращал 0 (другой блок от активации). wrapTo() по-прежнему вызывал transferFrom и потреблял лимит. Бот продолжал получать прибыль и сохранял взаимодействие.

  • Шаг 5 (накопление): Начиная с 0x8560...1915 в блоке 25360519, атакующий размещал транзакцию активации в том же блоке, что и транзакция бота, через взятку строителю, заставляя getStatus() возвращать 1. В этом режиме wrapTo() пропускал transferFrom — лимит не потреблялся — но unwrap() по-прежнему отправлял боту небольшое количество реальных токенов. Бот видел прибыльную операцию и оставлял разрешение на месте. Примерно за 600 блоков (~13 транзакций) бот повторял эту схему для WETH, USDC и USDT, накапливая неиспользованные лимиты на все три реальных актива.

Фаза 3: Сбор

  • Шаг 6: Атакующий вызвал withdraw() на всех поддельных токенах в транзакции сбора 0x2be870...cf3e65, используя неиспользованные лимиты для вызова transferFrom и перемещения реальных балансов бота к атакующему. Взятка строителю в размере 0,01 ETH была включена для гарантии включения в блок. В результате сбора из пострадавшего контракта было извлечено 1 474,58 WETH + 2 870 573 USDC + 2 035 760 USDT (~$7,5M).

Выявленная транзакция атаки приводит к потерям ~$7,5M, а общий ущерб составляет около $15M согласно заявлению jaredFromSubway [1].

Заключение

Основной причиной этого инцидента стало ненадлежащее управление разрешениями MEV-бота при взаимодействии с ненадёжными контрактами. В отличие от традиционных эксплойтов с разрешениями, когда атакующие злоупотребляют уязвимостями в доверенных DeFi-контрактах для вывода одобренных пользователями активов, данная атака работает в обратном направлении: бот проактивно одобрял собственные активы ненадёжным сторонним контрактам в рамках арбитражных операций. Атакующий накапливал эти направленные наружу неиспользованные разрешения и собрал их в одной транзакции.

Для снижения аналогичных рисков в будущем контракты ботов, взаимодействующие с ненадёжными токен-контрактами, должны проверять, что разрешения были использованы после каждой операции, и отзывать любые остаточные лимиты. Неиспользованные лимиты после внешне успешной сделки — явный признак вредоносного поведения токена.

Начните работу с Phalcon Explorer

Погружайтесь в транзакции, чтобы действовать разумно

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

Другие инциденты недели

Aztec

18 июня 2026 года отсутствующее ограничение равенства в ZK-схеме escape hatch протокола Aztec позволило атакующему вывести около $2,2M (1 158 ETH, 150K DAI и ~0,47 renBTC) из устаревшего контракта RollupProcessor на Ethereum [2], [3]. Это был второй эксплойт Aztec за три дня (первый, описанный в нашем предыдущем отчёте, был направлен против обновлённого RollupProcessorV3) через связанную ошибку привязки публичных входных данных ZK-схемы.

Предыстория

Устаревший RollupProcessor Aztec включает функцию escapeHatch: механизм безопасности, позволяющий любому пользователю отправить доказательство в рамках одной транзакции, когда оператор роллапа прекращает обработку. В отличие от processRollup (требующего авторизованного провайдера), escape hatch открывается в периодических окнах на основе номера блока и может быть вызван кем угодно:

function escapeHatch(
    bytes calldata proofData,
    bytes calldata signatures,
    bytes calldata viewingKeys
) external override whenNotPaused {
    (bool isOpen, ) = getEscapeHatchStatus();
    require(isOpen, 'Rollup Processor: ESCAPE_BLOCK_RANGE_INCORRECT');
    processRollupProof(proofData, signatures, viewingKeys);
}

Escape hatch использует специальную ZK-схему (escape_hatch_circuit), которая обрабатывает транзакцию join-split: потребляет входные заметки из дерева Меркла и создаёт выходные заметки. Схема должна проверять, что входные заметки существуют в текущем дереве данных (используя old_data_root для проверки членства в дереве Меркла), а затем предоставлять тот же корень в качестве публичного входного значения для проверки L1-контрактом по сравнению с on-chain состоянием.

Анализ уязвимости

Уязвимость находится в схеме escape hatch (escape_hatch_circuit.cpp). Значение old_data_root превращается в два независимых свидетеля без ограничения равенства, связывающего их.

Первый свидетель (строка 33) передаётся в компонент схемы join-split, где используется для доказательств членства в дереве Меркла для проверки существования входных заметок в дереве данных:

join_split_inputs inputs = {
    // ...
    witness_ct(&composer, tx.js_tx.old_data_root),        // строка 33: первый свидетель
    // ...
};
auto outputs = join_split_circuit_component(composer, inputs);

Второй свидетель (строка 50) создаётся независимо и предоставляется как публичное входное значение (строка 88), которое Solidity-контракт извлекает и проверяет по on-chain корню данных:

auto old_data_root = field_ct(witness_ct(&composer, tx.js_tx.old_data_root));  // строка 50: второй свидетель
// ...
composer.set_public_input(old_data_root.witness_index);    // строка 88: предоставлен как публичное входное значение

В ZK-схеме каждый вызов witness_ct создаёт независимую переменную. Без явного assert_equal между строками 33 и 50 доказывающий может присваивать разные значения этим двум свидетелям. На стороне Solidity validateMerkleRoots проверяет require(oldDataRoot == dataRoot), используя только публичное входное значение из строки 50, без доступа к значению, используемому в строке 33.

Тот же шаблон без привязки существует также для input_owner и output_owner: эти значения засвидетельствованы в строках 38–39 (переданы в join_split_circuit_component для проверки владения) и снова в строках 111–112 (предоставлены как независимые публичные свидетели). Однако мы не обнаружили практического пути эксплуатации этого пробела.

Анализ атаки

Схема escape hatch была удалена из кодовой базы aztec-connect [4], но развёрнутый контракт верификатора по-прежнему содержит ключ верификации EscapeHatchVk, поэтому доказательства, сгенерированные с помощью уязвимой схемы, могут проходить on-chain верификацию. На момент атаки контракт был неактивен 142 дня, и окно escape hatch было открыто. Адрес атакующего был создан всего за 14 часов до эксплойта через Union Chain [2]. Атака состоит из трёх основных шагов:

  • Шаг 1: Атакующий построил поддельное дерево Меркла, содержащее заметки произвольной стоимости, принадлежащие самому атакующему. Эти заметки не существовали в реальном on-chain дереве данных (дереве Меркла, хранящем все действительные заметки).

  • Шаг 2: Атакующий сгенерировал доказательство escape hatch, эксплуатируя несвязанные свидетели, описанные выше. Свидетель строки 33 (используемый для проверки членства в дереве Меркла в компоненте join-split) был установлен на поддельный корень дерева Меркла (проверка членства прошла успешно, поскольку сфабрикованные заметки существовали в поддельном дереве, а проверки владения прошли успешно, поскольку атакующий держал ключи подписи). Свидетель строки 50 (предоставленный как публичное входное значение, проверяемое Solidity) был установлен на реальный on-chain корень данных (проверка Solidity require(oldDataRoot == dataRoot) прошла успешно, поскольку это значение совпадало с сохранённым корнем контракта).

  • Шаг 3: При выполнении обеих проверок — схемы и Solidity — доказательство было успешно верифицировано. Контракт обработал транзакцию escape hatch как легитимную и освободил средства.

Атакующий повторил этот процесс в трёх транзакциях (0x9e1d6a...6b03ca, 0xab306c...59c2b5, 0x5c196c...4705c3), нацеливаясь на разные активы и извлекая 1 158 ETH, 150K DAI и ~0,47 renBTC соответственно, что в сумме составляет около $2,2M.

Заключение

Основной причиной этого инцидента стало отсутствующее ограничение равенства между двумя свидетелями old_data_root в схеме escape hatch. Один свидетель использовался для проверки членства приватных заметок внутри компонента join-split, другой предоставлялся как публичное входное значение, проверяемое Solidity. Без ограничения, связывающего их, атакующий доказал владение сфабрикованными заметками по отношению к поддельному дереву Меркла, тогда как L1-контракт видел действительный on-chain корень. Примечательно, что удаление уязвимой схемы из исходного кода не нейтрализовало уже развёрнутый контракт верификатора — функция escapeHatch в устаревшем RollupProcessor остаётся вызываемой всякий раз, когда открыто её окно по номеру блока.

Для снижения аналогичных рисков в будущем: когда одно и то же логическое значение присутствует в нескольких точках ZK-схемы, все экземпляры должны быть явно ограничены как равные — независимые вызовы witness_ct для одного и того же значения являются пробелом в привязке. Аудиты схем должны систематически проверять, что каждое публичное входное значение связано с внутрисхемным значением, которое оно представляет.

Начните работу с Phalcon Security

Обнаруживайте каждую угрозу, получайте важные оповещения и блокируйте атаки.

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

Ссылки

О BlockSec

BlockSec — поставщик комплексных решений в области безопасности блокчейна и крипто-комплаенса. Мы создаём продукты и услуги, которые помогают клиентам проводить аудит кода (включая смарт-контракты, блокчейн и кошельки), перехватывать атаки в режиме реального времени, анализировать инциденты, отслеживать незаконные средства и выполнять требования AML/CFT на протяжении всего жизненного цикла протоколов и платформ.

BlockSec опубликовал множество работ по безопасности блокчейна на престижных конференциях, сообщил о нескольких атаках нулевого дня на DeFi-приложения, предотвратил несколько взломов, спасая более 20 миллионов долларов, и обеспечил безопасность криптовалют на миллиарды долларов.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit