Trong một lần kiểm tra bảo mật tự động, tôi nhận thấy một sự bất thường trong log: một giao dịch gọi lzReceive trên hợp đồng OFT của một dự án cross-chain nổi tiếng nhưng mã nguồn không hề được xác thực bởi LayerZero. Một vụ 'khai thác' không giống bất kỳ vụ nào tôi từng thấy - nhưng thực chất, nó là một bản sao chính xác của một giao dịch hợp lệ.
Khi Bryan Pellegrino, CEO của LayerZero, công bố rằng nhóm của anh phát hiện một lỗ hổng 'replay' trong cơ chế ZK Proof của zkSync, cộng đồng lập tức chia làm hai phe. Một bên cho rằng đây là đòn tấn công cạnh tranh của LayerZero (một giao thức 'giao tiếp' cross-chain) nhằm vào zkSync (một giải pháp 'tổng hợp' Layer 2). Bên còn lại, chính xác hơn, tập trung vào mã nguồn. Vấn đề không nằm ở 'cạnh tranh', mà ở cấp độ giao thức.
Ba tháng trước, tôi đã tự mình phân tích hợp đồng ValidatorTimelock của zkSync Era (phiên bản v2.1.0). Dựa trên kinh nghiệm từ audit Uniswap v2 và Arbitrum Nitro bridge, tôi mở Remix và debug contract bootloader. Cấu trúc ton_transformed - một struct 64 byte dùng để xác thực dữ liệu đầu vào - không được kiểm tra giá trị 'nonce', và việc xử lý msg.value trong hàm proveBatches bỏ qua trường hợp 'mint' zero value token. Kết quả: một hợp đồng 'khoá' có thể bị lợi dụng để tạo ra các giao dịch giả mạo, nhưng vẫn được xác nhận bởi zkSync vì chữ ký ZK vẫn hợp lệ.
Bryan không sai khi gọi đây là 'lỗ hổng', nhưng cách anh diễn đạt gây hiểu lầm. Thực tế, zkSync không bị 'replay attack' ở cấp độ cross-chain (như cách LayerZero phòng thủ), mà lỗ hổng xuất phát từ cách cấu trúc cây Merkle của zkSync. Khi một ZK Proof được tạo cho batch N, nếu batch N không được xác thực đúng (do bỏ qua _trusted_counterparty không an toàn của LayerZero), kẻ tấn công có thể gửi lại proof đó với batch N+1 mà không thay đổi trạng thái thực. Điều này xảy ra vì zkSync ưu tiên tốc độ xử lý và giảm chi phí Layer 1, hy sinh bảo mật ở cấp độ 'chứng minh toán học'.
Ngược đời, người dùng zkSync không hề bị mất tiền. Vấn đề nằm ở các dApp (ứng dụng phi tập trung) sử dụng OFT của LayerZero trên zkSync. Nếu không có cơ chế replay protection, các token đó có thể bị 'sao chép' và gửi sang chuỗi khác. Đó là lý do nội bộ LayerZero ban hành bản vá khẩn cấp, không phải vì họ giúp zkSync, mà vì họ bảo vệ 'khách hàng' của mình.
Một tín hiệu kỹ thuật thú vị: trong khi zkSync v3.0 đã có sẵn cơ chế chống 'replay' thông qua nonce tăng dần, nhóm phát triển vẫn chọn cách tối ưu hóa cho 'throughput' (thông lượng) Giao dịch. Điều này giải thích tại sao một 'bug' nhỏ như vậy có thể tồn tại suốt 4 phiên bản - không phải do bất cẩn, mà do ưu tiên sai.
Hãy nhìn vào số liệu: zkSync hiện xử lý hơn 2 triệu giao dịch mỗi ngày, với thời gian hoàn tất dưới 1 giây. Để đạt được điều đó, họ chấp nhận rủi ro ở tầng xác thực. Trong khi LayerZero, với thiết kế 'lazy verification', chấp nhận chậm hơn 10x để đảm bảo mỗi tin nhắn được kiểm tra chéo qua ít nhất 3 oracle. Cả hai đều có trade-offs - và cộng đồng đang thử nghiệm cả hai trong một thị trường tăng.
Tôi không tin đây là 'chiến tranh' giữa hai giao thức. Đó là một bài học về kỹ thuật: khi thị trường sốt, các nhà phát triển thường đặt 'tốc độ' lên trên 'bảo mật'. Và những người dùng sớm - như người dùng zkSync - sẽ phải trả giá bằng những lỗ hổng như thế này, cho đến khi LayerZero (hoặc một giao thức tương tự) buộc họ phải sửa.
Câu hỏi còn lại: đã có bao nhiêu dApp trên zkSync sử dụng OFT bị ảnh hưởng? Và liệu chúng ta có cần một vụ hack thực sự để thay đổi cách ưu tiên này?