Aztec Connect, exploatat pentru 2,19 mil. USD printr-o vulnerabilitate ZKRollup

Pe 14 iunie 2026, contractul RollupProcessor Aztec Connect scos din uz (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455) a fost exploatat, iar atacatorul a extras aproximativ 2,19 milioane USD din pool-ul de pe L1 într-o singură tranzacție atomică. Deși Aztec Connect fusese retras în martie 2024, contractul imuabil a rămas expus deoarece păstra încă active reziduale ale utilizatorilor. Analiza de mai jos reconstruiește mecanismul atacului din codul sursă și din calldata on-chain. Prezentare generală și cauză Vulnerabilitatea pornește dintr-un decalaj structural între intervalul ciclurilor de decontare L1 pe care îl parcurge RollupProcessorV3 și intervalul pe care îl "angajează" hash-ul de intrare publică din ZK. Atacatorul a profitat de acest decalaj pentru a introduce conținut în 31 din 32 de sloturi de intrare publică, care au fost incluse în rădăcina de stare L2 prin dovezi ZK, fără ca acele sloturi să treacă prin validarea de decontare la nivelul contractului L1. În Decoder.sol, variabila numRealTxs este controlată integral de atacator: ea este citită din calldata la offset 4516 fără constrângeri on-chain. Sloturile decodate (decoded_slots) sunt rotunjite la cel mai apropiat multiplu de numTxsPerRollup, conform layout-ului de date cerut de precompilarea SHA256. Rotunjirea creează o zonă "gap" între numRealTxs și decoded_slots, zonă pe care atacatorul o poate umple arbitrar. În RollupProcessorV3.sol, ciclul de decontare acoperă doar numRealTxs sloturi. Ipoteze de securitate care au cedat Modelul normal presupune că fiecare slot de intrare publică este fie validat la nivel L1 (de exemplu, prin reducerea pendingDepositBalance la depunere), fie constrâns în circuitul ZK astfel încât publicValue == 0. În acest caz: - Precompilarea SHA256 a acoperit toate cele 32 de sloturi (input de 8192 bytes = 32 × 256 bytes), iar conținutul sloturilor din "gap" a fost angajat prin dovada ZK. - Ciclul de decontare L1 a procesat doar primul slot; sloturile din "gap" [2..32] nu au fost supuse niciunei validări la nivel L1. - Constrângerea din circuitul ZK pentru publicValue în sloturile din "gap" (care ar fi trebuit să fie 0) a fost ocolită sau nu a fost impusă. Cele trei linii de apărare depind una de alta, iar pentru sloturile din "gap" niciuna nu oferă, de una singură, garanții suficiente: când lipsesc constrângeri în circuit, stratul L1 nu detectează abaterea. Modelul de divergență pe două căi Aceeași calldata este consumată pe două căi cu limite superioare diferite. Discrepanța de interpretare a "sloturilor care contează" (ZK tratează 32, L1 tratează 1) a permis "minting" din nimic. Derularea atacului Tranzacția 0x074ec931…aee1 conține 14 apeluri processRollup(), organizate în două faze "7 mint" urmate de "7 retrageri", executate într-o singură tranzacție atomică. Faza 1: Minting – generare de active pe L2 (Rollup #13277–13283, de 7 ori) 1) EOA-ul atacatorului 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 a apelat contractul de control 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd, declanșând selectorul 0x6f3ce701. 2) Contractul master a invocat secvențial trei contracte "relay", fiecare cu mai multe calldatas malițioase hardcodate. Parametri cheie: numRealTxs = 1, rollupSize = 1024, numInnerRollups = 32. - Slotul 1 (vizibil pe L1): proofId = 0 (noop), publicValue = 0 - Sloturile 2–32 (31 sloturi din "gap", invizibile pe L1): proofId = 1 (deposit), publicValue = N, publicOwner = adresa L2 a atacatorului Acestea au fost însoțite de dovada ZK corespunzătoare (circuitul nu constrânge publicValue la 0 pentru sloturile din "gap"). 3) Relay Contract A a apelat secvențial RollupProcessor.processRollup() (Rollup #13277–13281, de 5 ori). Verificatorul a acceptat dovada ZK, iar angajamentul SHA256 a acoperit toate cele 32 de sloturi. Decontarea L1 s-a oprit la 1 × TX_PUBLIC_INPUT_LENGTH (1 slot), procesând doar noop-uri. Depunerile false din sloturile [2..32] au fost angajate ZK într-un nou Merkle root, crescând soldul L2 al atacatorului cu 5 × 31N. 4) Relay Contract B a procesat Rollup #13282–13283 identic (de două ori), adăugând încă 2 × 31N. În total, atacatorul a acumulat 7 × 31N depuneri neacoperite, în timp ce seiful L1 a rămas neschimbat. Faza 2: Retrageri – conversia soldurilor umflate L2 în active L1 (Rollup #13284–13290, de 7 ori) Atacatorul și-a convertit întregul sold L2 în active L1 prin șapte rollup-uri de retragere: - Rollup #13284 (DAI): withdraw() → RollupProcessor a transferat direct 270.513,054 DAI către 0x0f18…edd17 - Rollup #13285 (wstETH): 167,890 wstETH către atacator - Rollup #13286 (yvDAI): 4.873,857 yvDAI către atacator - Rollup #13287 (yvWETH, preluat de relay contract C): 16,570 yvWETH către atacator - Rollup #13288 (LUSD): 9.273,734 LUSD către atacator - Rollup #13289 (yvLUSD): 359,047 yvLUSD către atacator - Rollup #13290 (ETH, ultima operațiune): RollupProcessor a transferat 908,987 ETH prin CALL intern către atacator Tranzacția atomică a reușit (gasUsed = 4.513.539), iar un rollback parțial la nivel de contract nu este posibil. Câștigul net a fost estimat la ~2,19 mil. USD, provenit din pool-ul legitim de active al utilizatorilor din RollupProcessor. Urmărirea fondurilor Conform urmăririi forensice on-chain (stare la 15 iunie 2026, la aproximativ o zi după incident): toate activele au fost transferate într-o singură tranzacție din RollupProcessor, prin contractul intermediar 0x06f585…d0fcD, direct către EOA-ul atacatorului 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17. Contractul intermediar nu a păstrat fonduri. Sumele furate rămân intacte și nemișcate în portofelul atacatorului, fără semne de spălare a banilor până în prezent. Concluzii și recomandări Lecția centrală este că limita superioară a buclei de decontare din contractul ZKRollup trebuie aliniată strict cu intervalul de intrări publice ZK la care se face angajamentul. Când apare un decalaj între numRealTxs (la nivel L1) și decoded_slots (în angajamentul SHA256), orice model de securitate care se bazează pe circuitul ZK pentru a impune constrângeri asupra sloturilor din "gap" poate fi ocolit. L1 trebuie să verifice independent fiecare slot de intrare publică acoperit de dovada ZK, fără a externaliza această responsabilitate către circuit. Echipa SlowMist recomandă audit extern complet înainte de implementarea unui sistem Rollup, cu accent pe consistența logică la granița de stare L1/L2, limitele de încredere în decodarea calldata și verificarea secundară on-chain a intrărilor publice ZK. Pentru contractele scoase din uz care încă păstrează active vechi, se recomandă migrare ordonată sau distrugere controlată a activelor pentru eliminarea expunerii. Material realizat de Threat Intelligence Team din cadrul SlowMist, folosind MistEye Threat Intelligence System, MistTrack Tracking Platform și analiza bazată pe AI SlowMist Agent. Pentru întrebări sau feedback, echipa poate fi contactată direct.