Фон
18 июня 2026 года в контракте RollupProcessor протокола Aztec Connect (Layer2-приватности для Ethereum) произошла атака. Злоумышленник воспользовался механизмом Escape Hatch («эвакуационный люк»), задействовав уязвимость доверительной границы: на уровне Solidity отсутствуют независимая проверка принадлежности средств и валидация лимитов на вывод, а также нет соответствующих ограничений для связанных схем (электрических/кружевных ограничений). Затем он подал proof для escape hatch, который был принят TurboVerifier, и напрямую вывел из баланса контракта 1 158 ETH (около 2,06 млн долларов США). Кроме того, по той же схеме он похитил 150 000 DAI и 0,47 renBTC, суммарный ущерб составил приблизительно 2,22 млн долларов США.
Обзор атаки

Атакующая транзакция:

Корневая причина уязвимости
Отсутствие границы доверия escapeHatch()
Публичная входная функция RollupProcessor escapeHatch() лишь проверяет, включён ли escape hatch; затем она напрямую переходит в processRollupProof() и не выполняет никаких проверок прав вызывающего:

В отличие от этого обычный путь обработки rollup — processRollup() — проверяет авторизацию rollupProviders[provider] и верифицирует подпись provider. А в этой атаке вызывается escapeHatch(), при этом передаются signatures = 0x, viewingKeys = 0x, полностью обходя процесс авторизации provider.
verifyProofAndUpdateState() слепо доверяет Verifier
После входа в processRollupProof() контракт вызывает TurboVerifier.verify(proofData, 0):

Поведение TurboVerifier само по себе корректно — он выполняет математическую верификацию Plonk-доказательства. Настоящая проблема в том, что RollupProcessor приравнивает успешную математическую верификацию к бизнес-законности и не выполняет дополнительную независимую проверку на уровне Solidity.
processDepositsAndWithdrawals() безусловно выполняет вывод
Ключевая логика выполнения после успешного возврата Verifier:

Функция окончательного вывода средств:

В zkSNARK-схеме отсутствует дверца (equation constraint gate) для равенства (первопричина)
В цепи доказательства Aztec join-split old_data_root разбивается на две независимые ветви для использования:
Внутреннее свидетельство A: передаётся join-split подцепь для проверки принадлежности приватных заметок к меркловскому дереву
Публичный вход B: раскрывается как публичный вход на уровне Solidity для сопоставления validateMerkleRoots() с on-chain dataRoot
В цепи отсутствует ограничение A == B, из-за чего обе величины могут получать независимые значения:
A может быть поддельным корнем меркловского дерева, сконструированным атакующим (в дереве содержатся заметки на любую сумму)
B может быть реальным on-chain dataRoot (чтобы проверка Solidity прошла)
Поскольку система ограничений допускает A ≠ B, весь zkSNARK proof остаётся действительным.
Схема атаки
Подготовительная стадия: подделка меркловского дерева и proof
Атакующий локально строит поддельное data Merkle-дерево, вставляя в него приватную заметку:
Стоимость: 1158 ETH (контракт тогда держал примерно 1158.7598 ETH)
Владелец: атакующий владеет соответствующим приватным ключом
Атакующий сконструировал zkSNARK proof:
Внутреннее свидетельство A (old_data_root) = поддельный корень дерева
Публичный вход B (old_data_root) = on-chain фактический dataRoot = 0x184bea7d9493cd9a5efb6b679d04066a8c92a34ac8ec150e9635133c6010977b
Первая стадия: создание атакующей транзакции
Атакующий EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F напрямую инициирует верхнеуровневый вызов к RollupProcessor (0x737901bea3eeb88459df9ef1be8ff3ae1b42a2ba):
Подпись функции: escapeHatch(bytes,bytes,bytes)
msg.value = 0
signatures = 0x
viewingKeys = 0x
В proofData, сконструированном атакующим, содержатся ключевые public inputs:

Вторая стадия: обход проверки прав
escapeHatch() после проверки только того, что getEscapeHatchStatus() возвращает true, напрямую вызывает processRollupProof(). Этот путь не выполняет обычную проверку авторизации rollupProviders[provider] в processRollup(), и не проверяет подпись provider. Между идентичностью вызывающего и адресом получателя вывода не существует никакой привязки.
Третья стадия: математическая верификация Verifier
RollupProcessor вызывает STATICCALL к TurboVerifier (0x48cb7ba00d087541dc8e2b3738f80fdd1fee8ce8ce):

Поскольку rollup_size = 0, TurboVerifier выбирает EscapeHatchVk через VerificationKeys.getKeyById(0) (vk.num_inputs = 26) для математической верификации Plonk proof. Verifier проверяет только математическое соответствие proof и public inputs, не читает состояние RollupProcessor и не проверяет принадлежность балансов.
В этой транзакции вызов verifier успешно завершается, что означает: escape hatch proof, предоставленный атакующим, математически корректен.
Четвёртая стадия: выполнение вывода
После успешного возврата Verifier RollupProcessor обновляет состояние rollup:

Затем он переходит в processDepositsAndWithdrawals(); в режиме escape hatch при rollupSize == 0 всё равно обрабатывается 1 внутреняя транзакция. После разбора proofData обнаруживается:

Непосредственно вызывается withdraw(1158 ETH, адрес атакующего, 0); с помощью receiverAddress.call{value: 1158 ETH}("") средства выводятся.
Пятая стадия: атака по той же схеме DAI и renBTC
Атакующий в тот же период использовал полностью тот же уязвимый сценарий: он отдельно сконструировал escape hatch proof для актива ERC20 (assetId ≠ 0) и похитил 150,000 DAI и 0.47 renBTC из RollupProcessor. В ветке withdraw() при assetId ≠ 0 выполняется transfer() для ERC20, завершающий перевод.
Чистая прибыль трёх атак:

Отслеживание средств
Анализ действий атакующего EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F с помощью системы отслеживания по борьбе с отмыванием денег MistTrack (SlowMist):
Источник средств: начальный Gas, использованный адресом, созданным атакующим, поступил с 0x963737c550e70ffe4d59464542a28604edb2ef9a (адрес сущности unionchain.ai). Этот адрес впервые стал активным 2026 年 6 月 18 日 02:21:11 UTC — ровно во время запуска атакующих действий; это разовый адрес, созданный специально для этой атаки.
Куда ушли средства: текущий распределённый вид украденных средств следующий:
Токены 802 ETH всё ещё остаются на основном адресе атакующего 0x6952d9246e9aFE8B887B2877225163436F78E97F, а 150,000 DAI и 0.47 renBTC также не были переведены
300 ETH переведено на 0x15930a0fef3421f48c6553b5691682cc1b22edb3 (MistTrack пометил как связанный с Aztec Exploiter вредоносный адрес)
Около 56 ETH переведено на другой вредоносный адрес (также помечен как связанный с Aztec Exploiter)
AML-риск скоринг 0x33d6a0d9bc210e823e043d604179cd844eb467df — 100/100 (Severe); также связан с инцидентом Aztec Exploiter
Итоги
Ключевой урок этой атаки: «математически корректное» zkSNARK ≠ «безопасное для бизнеса». Верификатор может доказать только то, что «вы знаете свидетельство, удовлетворяющее ограничениям», но если сами ограничения неверные (или отсутствуют), система доказательств становится легитимной машиной для подделок. Команда SlowMist Security рекомендует проектам провести специальный аудит всех путей ограничений для zero-knowledge proof, чтобы обеспечить безопасность схемы.
Эта статья написана командой Threat Intelligence SlowMist в связке с системой Threat Intelligence MistEye, платформой отслеживания MistTrack и анализом, управляемым AI агентом SlowMist Agent. Если есть вопросы, обращайтесь и оставляйте обратную связь.

