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 миллионов долларов, и обеспечил безопасность криптовалют на миллиарды долларов.

Sign up for the latest updates
Web3 Компаньон: Открытый Безопасный Агентный Кошелёк

Web3 Компаньон: Открытый Безопасный Агентный Кошелёк

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

~$5.98M Потеряно: Aztec, Raydium и другие | Еженедельник BlockSec
Security Insights

~$5.98M Потеряно: Aztec, Raydium и другие | Еженедельник BlockSec

Еженедельный отчёт о безопасности блокчейна (8–15 июня 2026 г.): 4 инцидента в Ethereum и Solana, общие потери ~$5,98 млн. Aztec Connect: отсутствие валидации входных данных привело к рассинхронизации rollup и L1. Raydium: уязвимость в AMM v3 позволила дренировать 4 пула.

Анализ уязвимости Zcash Orchard | Еженедельник BlockSec
Security Insights

Анализ уязвимости Zcash Orchard | Еженедельник BlockSec

Критическая уязвимость в цепи Orchard Zcash: отсутствие ограничения равенства в гаджете ECC halo2 позволяло незаметно подделывать ZEC через двойное расходование. Уязвимость существовала 4+ лет, обнаружена ИИ-аудитом (Anthropic Opus 4.8, исследователь Тейлор Хорнби), устранена экстренным обновлением NU6.2.

Best Security Auditor for Web3

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

BlockSec Audit