Aztec Connect зламали на $2,19 млн через вразливість ZKRollup

14 червня 2026 року зловмисник експлуатував застарілий контракт RollupProcessor Aztec Connect (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455), вивівши близько $2,19 млн із пулу L1 в межах однієї атомарної транзакції. Aztec Connect вивели з експлуатації у березні 2024 року, але незмінний (immutable) контракт залишався доступним і містив залишкові активи користувачів, що й створило поверхню атаки. Нижче наведено реконструкцію технічної механіки інциденту на основі вихідного коду контрактів і ончейн calldata. Ключова причина: розрив між межами обробки L1 та комітом у ZK Вразливість виникла через структурний "зазор" між діапазоном L1-циклів сетлменту, які проходить RollupProcessorV3, та діапазоном, на який фактично робиться коміт через ZK public input hash. Цим зазором скористалися, щоб закомітити в L2 state root через ZK-докази вміст 31 із 32 слотів public input без будь-якої валідації на рівні сетлменту L1. Як це стало можливим на рівні декодування У Decoder.sol параметр numRealTxs повністю контролюється атакером: його зчитують із calldata за офсетом 4516 без ончейн-обмежень. Параметр decoded_slots округляється вгору до найближчого кратного numTxsPerRollup відповідно до формату даних SHA256 precompile. Це округлення і створює область розриву між numRealTxs та decoded_slots, яку можна довільно заповнити. У RollupProcessorV3.sol цикл сетлменту охоплює лише numRealTxs слотів. Отже, L1 обробляє тільки "реальні" слоти, тоді як SHA256-коміт охоплює весь набір. Порушення базових припущень безпеки Типова модель безпеки передбачає, що кожен слот public input або: 1) перевіряється на рівні L1-контракту (наприклад, через зменшення pendingDepositBalance під час депозиту), або 2) жорстко обмежений у ZK-схемі так, що publicValue == 0. У цьому випадку обидва рівні роз'єдналися: - SHA256 precompile охопив усі 32 слоти (перевірялося інпутом 8192 байти = 32 × 256 байтів), і вміст "gap"-слотів був закомічений ZK-доказом. - L1-цикл сетлменту обробив лише перший слот; слоти розриву [2..32] не підпали під L1-валідацію. - Обмеження ZK-схеми для publicValue в gap-слотах (яке мало б примушувати значення до 0) було обійдене або не було enforced, що й дозволило "намалювати" депозити. Фактично спрацювала модель "dual-path divergence": одна і та сама calldata споживається двома шляхами з різними верхніми межами. ZK-частина "бачить" 32 слоти, L1 "бачить" лише 1. Саме ця розбіжність і призвела до "minting out of thin air". Хід атаки: 14 викликів processRollup() в одній транзакції Атака виконана транзакцією 0x074ec931…aee1 і містила 14 викликів processRollup(), організованих у двофазний шаблон: "спершу 7 мінтів, потім 7 виведень". Усе виконано атомарно в одному виконанні (gasUsed = 4,513,539), частковий відкат на рівні контракту неможливий. Фаза 1 — Minting: нарощення балансу в L2 (Rollup #13277–13283) 1) EOA атакера 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 звернувся до master-контракту 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd, викликавши селектор 0x6f3ce701. 2) Master-контракт послідовно викликав три relay-контракти з жорстко прописаними шкідливими calldata для ролапів. Ключові параметри кожної calldata: - numRealTxs = 1 - rollupSize = 1024 - numInnerRollups = 32 Слот 1 (видимий для L1): proofId = 0 (noop), publicValue = 0 Слоти 2–32 (31 gap-слот, невидимі для L1): proofId = 1 (deposit), publicValue = N, publicOwner = L2-адреса атакера Додано відповідний ZK-доказ (схема не обмежує publicValue у gap-слоті до 0). 3) Relay Contract A п'ять разів викликав RollupProcessor.processRollup() (Rollup #13277–13281): - Verifier підтвердив валідність ZK-доказу — SHA256-коміт охоплює всі 32 слоти. - L1-сетлмент завершився на 1 × TX_PUBLIC_INPUT_LENGTH = 1 слот, обробивши лише noops. - Фейкові "депозити" в gap-слотах [2..32] були закомічені в новий Merkle root, баланс атакера в L2 зріс на 5 × 31N. 4) Relay Contract B двічі повторив схему для Rollup #13282–13283, додавши ще 2 × 31N. Після цього L2-акаунт атакера накопичив 7 × 31N непідтверджених депозитів, тоді як L1-вольт не змінювався. Фаза 2 — Withdrawal: конвертація роздутого L2-балансу в активи L1 (Rollup #13284–13290) Далі атакер обміняв увесь штучно створений L2-баланс на реальні активи з L1 через сім ролапів на виведення: - Rollup #13284 (DAI): withdraw() → RollupProcessor напряму переказав 270,513.054 DAI на 0x0f18…edd17 - Rollup #13285 (wstETH): 167.890 wstETH → атакеру - Rollup #13286 (yvDAI): 4,873.857 yvDAI → атакеру - Rollup #13287 (yvWETH, керує relay contract C): 16.570 yvWETH → атакеру - Rollup #13288 (LUSD): 9,273.734 LUSD → атакеру - Rollup #13289 (yvLUSD): 359.047 yvLUSD → атакеру - Rollup #13290 (ETH, фінальна дія): RollupProcessor переказав 908.987 ETH через внутрішній CALL → атакеру За оцінкою, чистий результат склав близько $2,19 млн, усі кошти вилучено з легітимного пулу активів користувачів у RollupProcessor. Трекінг коштів За даними ончейн-розслідування станом на 15 червня 2026 року (приблизно через добу після інциденту): - Усі активи виведені однією транзакцією з RollupProcessor через проміжний атакувальний контракт 0x06f585…d0fcD безпосередньо на EOA атакера 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17. - Проміжний контракт не утримував залишків коштів. - Викрадені активи на 100% зберігаються на EOA атакера, ознак відмивання або розпорошення коштів на момент спостереження немає. Висновки та рекомендації Головний урок: верхня межа циклу сетлменту в контракті ZKRollup повинна строго збігатися з діапазоном слотів, що комітяться у ZK public inputs. Якщо існує розрив між межою numRealTxs на рівні L1 та decoded_slots у SHA256-коміті, будь-яке припущення, що ZK-схема самостійно забезпечить обмеження для gap-слотів, може бути зламане. L1 має незалежно перевіряти кожен слот public input, який покривається ZK-доказом, і не перекладати цю відповідальність на рівень схеми. Команда SlowMist рекомендує проєктам проводити повний зовнішній аудит перед запуском rollup-систем, з фокусом на логічній узгодженості меж L1/L2, довірчих межах декодування calldata та ончейн-вторинній верифікації ZK public inputs. Для контрактів, які вже виведені з експлуатації, але продовжують утримувати історичні активи, доцільно виконати керовану міграцію або знищення/виведення активів, щоб прибрати тривалі ризики. Матеріал підготовлено командою Threat Intelligence SlowMist із використанням MistEye Threat Intelligence System, платформи трекінгу MistTrack та AI-аналізу SlowMist Agent. Запитання й відгуки можна надсилати команді.