За прошедшую неделю (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:
<real_token>.approve(tokenContract, amount)— предоставить лимит на реальный токен контракту-обёрткеtokenContract.wrapTo()→ многоступенчатыйswap()через пулы →tokenContract.unwrap()— обернуть реальный токен, провести через пулы и развернуть обратно в реальный токен
Бот предполагал, что wrapTo() потребит лимит через transferFrom, как это делает корректно работающая обёртка. Однако бот никогда не проверял, был ли лимит фактически использован после операции, и не отзывал остаточные разрешения. Если wrapTo() не вызывает transferFrom, полный лимит сохраняется после операции и становится постоянной поверхностью атаки — любой контракт, имеющий такой лимит, может впоследствии вызвать transferFrom и переместить реальные активы бота.
Анализ атаки
На основе on-chain реконструкции атакующий создал поддельную торговую среду из трёх компонентов для эксплуатации описанной выше уязвимости:
-
Поддельные токены-обёртки: Каждый поддельный токен использовал имя реального токена, но добавлял префикс
fк символу (например, имяUSD Coinс символомfUSDCдля USDC). Он реализовывалwrapTo()иunwrap()для имитации легитимной обёртки, а также функциюwithdraw(), доступную только атакующему, которая выводила неиспользованные лимиты черезtransferFrom. -
Поддельные пулы для обмена: Атакующий развернул около 44 пулов в стиле Uniswap V2 через самостоятельно развёрнутую фабрику. Эти пулы составляли пары из поддельных токенов друг с другом, формируя убедительные маршруты обмена. При вызове
swap()пулы генерировали настоящие событияSyncиSwap, неотличимые от легитимных сделок. -
Специально сформированная прибыль атакующего: Во время
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,58WETH+ 2 870 573USDC+ 2 035 760USDT(~$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 для одного и того же значения являются пробелом в привязке. Аудиты схем должны систематически проверять, что каждое публичное входное значение связано с внутрисхемным значением, которое оно представляет.
Ссылки
- [1] Официальное заявление jaredFromSubway (потери ~$15M)
- [2] Оповещение Phalcon: эксплойт Aztec Escape Hatch
- [3] Официальное заявление Aztec Labs
- [4] Удаление схемы Escape Hatch, коммит aztec-connect 8c3953a
О BlockSec
BlockSec — поставщик комплексных решений в области безопасности блокчейна и крипто-комплаенса. Мы создаём продукты и услуги, которые помогают клиентам проводить аудит кода (включая смарт-контракты, блокчейн и кошельки), перехватывать атаки в режиме реального времени, анализировать инциденты, отслеживать незаконные средства и выполнять требования AML/CFT на протяжении всего жизненного цикла протоколов и платформ.
BlockSec опубликовал множество работ по безопасности блокчейна на престижных конференциях, сообщил о нескольких атаках нулевого дня на DeFi-приложения, предотвратил несколько взломов, спасая более 20 миллионов долларов, и обеспечил безопасность криптовалют на миллиарды долларов.
-
Официальный сайт: https://blocksec.com/
-
Официальный аккаунт в Twitter: https://twitter.com/BlockSecTeam



