Aztec Connect ถูกแฮ็กสูญ 2.19 ล้านดอลลาร์ จากช่องโหว่ ZKRollup

เมื่อวันที่ 14 มิถุนายน 2026 สัญญา RollupProcessor ของ Aztec Connect ที่ถูกยุติการใช้งานแล้ว (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455) ถูกโจมตี ส่งผลให้ผู้ไม่หวังดีดึงสินทรัพย์จากพูลบน L1 ออกไปราว 2.19 ล้านดอลลาร์ภายในธุรกรรมแบบ atomic เพียงครั้งเดียว โดยอาศัยการสร้างช่องว่างของขอบเขต (boundary gap) ระหว่างตัวแปร numRealTxs กับ decoded_slots Aztec Connect ถูกประกาศเลิกใช้งานตั้งแต่มีนาคม 2024 แต่สัญญาแบบ immutable นี้ยังคงเปิดรับความเสี่ยงเพราะมีสินทรัพย์คงค้างของผู้ใช้งานหลงเหลืออยู่ บทความนี้สรุปภาพรวมเชิงเทคนิคของเหตุการณ์จากซอร์สโค้ดของสัญญาและ calldata บนเชน ภาพรวมการโจมตี ต้นตอช่องโหว่ ปัญหาหลักคือ "ช่วงของรอบการชำระบัญชี (L1 settlement cycles)" ที่ RollupProcessorV3 วิ่งตรวจสอบ ไม่สอดคล้องกับ "ช่วงของข้อมูล public input" ที่ถูกผูกมัด (commit) ผ่านแฮชของ ZK public input ผู้โจมตีใช้ช่องว่างนี้ทำให้ 31 จาก 32 ช่องของ public input ถูก commit เข้าไปใน L2 state root ผ่าน ZK proof ได้ โดยไม่ต้องผ่านการตรวจสอบการชำระบัญชีบนเลเยอร์สัญญา L1 Decoder.sol: numRealTxs ถูกควบคุมโดยผู้โจมตีได้ทั้งหมด ค่า numRealTxs ถูกอ่านจาก calldata ที่ offset 4516 โดยไม่มีข้อจำกัดตรวจสอบบนเชน ขณะที่ decoded_slots ถูกปัดขึ้นให้เป็นจำนวนเท่าของ numTxsPerRollup ตามรูปแบบข้อมูลของ SHA256 precompile การปัดขึ้นนี้เองก่อให้เกิด "พื้นที่ช่องว่าง" ระหว่าง numRealTxs กับ decoded_slots ซึ่งผู้โจมตีสามารถยัดข้อมูลใส่ได้ตามต้องการ RollupProcessorV3.sol: รอบชำระบัญชีครอบคลุมแค่ numRealTxs ลูปการชำระบัญชีบน L1 ประมวลผลเพียงจำนวนสล็อตตาม numRealTxs ทำให้สล็อตในโซนช่องว่างไม่ถูกตรวจสอบที่ L1 การแตกของสมมติฐานด้านความปลอดภัย โดยปกติ ระบบจะถือว่าทุกสล็อตของ public input ต้องถูกคุมความปลอดภัยด้วยอย่างใดอย่างหนึ่ง: (1) ตรวจสอบที่เลเยอร์สัญญา L1 (เช่น ลด pendingDepositBalance ตอนฝาก) หรือ (2) ถูกจำกัดใน ZK circuit ให้ publicValue == 0 แต่ในเหตุการณ์นี้: - SHA256 precompile ครอบคลุมทั้ง 32 สล็อต (ทดสอบด้วยอินพุต 8192 ไบต์ = 32 × 256 ไบต์) และข้อมูลในสล็อตช่องว่างถูก commit ผ่าน ZK proof - วงรอบชำระบัญชีบน L1 ประมวลผลแค่สล็อตแรก ส่วนสล็อตช่องว่าง [2..32] ไม่โดนตรวจสอบระดับ L1 - ข้อจำกัดของ ZK circuit ที่ควรบังคับให้ publicValue ของสล็อตช่องว่างเป็น 0 ถูกผู้โจมตีหลบเลี่ยงหรือไม่ได้ถูกบังคับใช้ กลไกป้องกัน 3 ชั้นพึ่งพากัน แต่ไม่มีชั้นใดชั้นหนึ่งที่ป้องกันสล็อตช่องว่างได้เอง เมื่อ ZK circuit ไม่คุม L1 ก็ตรวจจับไม่ได้เช่นกัน โมเดล "dual-path divergence" calldata ชุดเดียวกันถูกอ่านไปคนละเส้นทางด้วยเพดานบนต่างกัน: ฝั่ง ZK เห็น 32 สล็อต ขณะที่ฝั่ง L1 เห็นเพียง 1 สล็อต ความไม่ตรงกันว่า "สล็อตไหนถูกนับ" คือสาเหตุของการสร้างยอดคงเหลือ (mint) จากศูนย์ ลำดับการโจมตี ธุรกรรมโจมตี 0x074ec931…aee1 มีการเรียก processRollup() รวม 14 ครั้ง จัดเป็นแพตเทิร์น 2 เฟส "7 ครั้งเพื่อ mint ตามด้วย 7 ครั้งเพื่อถอน" ทั้งหมดเกิดในธุรกรรม atomic เดียว เฟส 1: Mint — เพิ่มสินทรัพย์บน L2 จากศูนย์ (Rollup #13277–13283 รวม 7 ครั้ง) 1) EOA ของผู้โจมตี 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 เรียกสัญญาควบคุมหลัก 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd ด้วย selector 0x6f3ce701 2) สัญญาหลักเรียกต่อไปยังรีเลย์ 3 สัญญา โดยแต่ละสัญญามี calldata rollup ที่เป็นอันตรายแบบ hardcode พารามิเตอร์สำคัญของแต่ละ calldata คือ numRealTxs = 1, rollupSize = 1024, numInnerRollups = 32 - สล็อต 1 (L1 มองเห็น): proofId = 0 (noop), publicValue = 0 - สล็อต 2–32 (31 สล็อตช่องว่าง L1 มองไม่เห็น): proofId = 1 (deposit), publicValue = N, publicOwner = ที่อยู่ L2 ของผู้โจมตี พร้อม ZK proof ที่สอดคล้องกัน (โดย circuit ไม่ได้บังคับให้ publicValue ของสล็อตช่องว่างเป็น 0) 3) รีเลย์สัญญา A เรียก RollupProcessor.processRollup() ต่อเนื่อง (Rollup #13277–13281 รวม 5 ครั้ง): - ตัว verifier ยืนยันว่า ZK proof ผ่าน และ SHA256 commitment ครอบคลุมทั้ง 32 สล็อต - ลูปชำระบัญชีบน L1 จบที่ 1 × TX_PUBLIC_INPUT_LENGTH = 1 สล็อต จึงประมวลผลเฉพาะ noop - "การฝากปลอม" ในสล็อตช่องว่าง [2..32] ถูก commit เข้า Merkle root ใหม่ ทำให้ยอดคงเหลือบน L2 ของผู้โจมตีเพิ่มขึ้น 5 × 31N 4) รีเลย์สัญญา B ทำ Rollup #13282–13283 ด้วยรูปแบบเดียวกันอีก 2 ครั้ง เพิ่มยอด L2 อีก 2 × 31N เมื่อจบเฟสนี้ บัญชี L2 ของผู้โจมตีสะสมยอดฝากที่ไม่มีสินทรัพย์หนุนหลังรวม 7 × 31N ขณะที่ vault บน L1 ไม่เปลี่ยนแปลง เฟส 2: ถอน — แปลงยอด L2 ที่พองเป็นสินทรัพย์บน L1 (Rollup #13284–13290 รวม 7 ครั้ง) ผู้โจมตีถอนสินทรัพย์ออกจากพูลผู้ใช้งานจริงบน L1 ด้วย rollup ถอน 7 ครั้ง: - 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, รีเลย์สัญญา C เข้าควบคุม): โอน 16.570 yvWETH → ผู้โจมตี - Rollup #13288 (LUSD): โอน 9,273.734 LUSD → ผู้โจมตี - Rollup #13289 (yvLUSD): โอน 359.047 yvLUSD → ผู้โจมตี - Rollup #13290 (ETH, รายการสุดท้าย): RollupProcessor โอน 908.987 ETH ผ่าน internal CALL → ผู้โจมตี ธุรกรรม atomic เดียวสำเร็จ (gasUsed = 4,513,539) และไม่สามารถทำ partial rollback ระดับสัญญาได้ ผู้โจมตีได้กำไรราว 2.19 ล้านดอลลาร์ โดยเงินทั้งหมดออกจากพูลสินทรัพย์ของผู้ใช้งานที่ถูกต้องใน RollupProcessor การติดตามเงิน (Fund tracking) จากการติดตามเชิงนิติเวชบนเชน ณ วันที่ 15 มิถุนายน 2026 (ราว 1 วันหลังเหตุการณ์): - สินทรัพย์ทั้งหมดถูกโอนภายในธุรกรรมเดียว จาก RollupProcessor ผ่านสัญญาตัวกลางของการโจมตี 0x06f585…d0fcD ไปยัง EOA ของผู้โจมตี 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17 - สัญญาตัวกลางไม่เหลือยอดคงค้าง - เงินที่ถูกขโมยยังคงอยู่ครบ 100% ใน EOA ของผู้โจมตี และยังไม่พบพฤติกรรมฟอกเงิน สรุป บทเรียนสำคัญคือ เพดานบนของลูปการชำระบัญชีในสัญญา ZKRollup ต้องสอดคล้องอย่างเคร่งครัดกับช่วงของ ZK public inputs ที่ถูก commit หากเกิดช่องว่างระหว่างขอบเขต numRealTxs บนเลเยอร์สัญญา L1 กับ decoded_slots ของ commitment (SHA256) สมมติฐานที่ฝากให้ ZK circuit เป็นผู้คุมข้อจำกัดของสล็อตช่องว่างสามารถถูกหลบเลี่ยงได้ L1 จำเป็นต้องตรวจสอบทุกสล็อตของ public input ที่ ZK proof commit ไว้ด้วยตนเอง ไม่ควรถ่ายโอนความรับผิดชอบนี้ให้เลเยอร์ circuit ทีม SlowMist แนะนำให้โครงการที่พัฒนา Rollup ทำการตรวจสอบความปลอดภัยโดยผู้เชี่ยวชาญภายนอกอย่างครอบคลุมก่อนนำไปใช้งานจริง โดยเน้นความสอดคล้องเชิงตรรกะที่ขอบเขตสถานะ L1/L2 ขอบเขตความน่าเชื่อถือของการถอดรหัส calldata และการตรวจสอบซ้ำบนเชนของ ZK public inputs สำหรับสัญญาที่เลิกใช้งานแล้วแต่ยังถือครองสินทรัพย์เดิม แนะนำให้ดำเนินการย้ายสินทรัพย์หรือทำลายอย่างเป็นระเบียบเพื่อลดความเสี่ยงที่คงค้าง บทความนี้จัดทำโดยทีม Threat Intelligence ของ SlowMist โดยใช้ MistEye Threat Intelligence System, MistTrack Tracking Platform และการวิเคราะห์ด้วย SlowMist Agent ที่ขับเคลื่อนด้วย AI หากมีคำถามหรือข้อเสนอแนะสามารถติดต่อได้