Trong 48 giờ qua, một giao thức cho vay trên Arbitrum đã mất 1.2 triệu USD vì một lỗi reentrancy. Điều kỳ lạ là: đội ngũ phát triển đã có báo cáo kiểm toán từ hai công ty hàng đầu. Cả hai báo cáo đều ghi nhận "không phát hiện lỗ hổng nghiêm trọng". Nhưng khi tôi mở file PDF, tôi thấy phần "Phân tích luồng dữ liệu" của cả hai đều trống — chỉ có dòng chữ "Chưa có dữ liệu đầu vào từ giao thức". Khoảng trống ấy, giống như một cánh cửa sập vô hình, đã nuốt chửng 1.2 triệu USD.
Đây không phải là trường hợp cá biệt. Trong 3 năm làm Layer2 Research Lead, tôi đã kiểm tra hơn 40 báo cáo kiểm toán. Gần 30% trong số đó có ít nhất một phần phân tích bị bỏ trống vì thiếu dữ liệu đầu vào. Nguyên nhân không phải do trình độ kiểm toán viên, mà đến từ chính kiến trúc của giao thức: khi các hợp đồng thông minh không cung cấp đủ hàm getter, hoặc khi dữ liệu on-chain bị phân mảnh qua nhiều layer, việc tái tạo trạng thái hệ thống trở nên bất khả thi. Điều này dẫn đến một nghịch lý: giao thức càng an toàn về mặt thiết kế (nhiều module, nhiều proxy), thì càng khó kiểm toán một cách đầy đủ.
Hãy nhìn vào cơ chế của các rollup hiện đại. Một giao thức Layer2 điển hình có sequencer, DA layer, prover, và bridge. Mỗi thành phần đều có hợp đồng riêng, thường được deploy dưới dạng proxy với upgradeable logic. Để kiểm toán toàn bộ, bạn cần mô phỏng trạng thái của cả 4 module trong cùng một môi trường. Nhưng vì dữ liệu blob trên Ethereum đã được nén và phân tán, việc lấy được dữ liệu gốc để xây dựng trạng thái ban đầu là một bài toán NP-hard. Trong thực tế, tôi từng mất 3 tuần chỉ để viết script đồng bộ dữ liệu từ blob sang local node. Hầu hết các công ty kiểm toán không có đủ thời gian và ngân sách để làm việc đó. Họ kiểm tra từng module riêng lẻ, và để lại phần "tích hợp" như một khoảng trống trong báo cáo.
Dựa trên kinh nghiệm audit của tôi, lỗ hổng nghiêm trọng nhất thường nằm ở chính khoảng trống đó. Năm 2022, khi kiểm toán một giao thức cho vay trên Solana, tôi phát hiện 12 lỗi. Trong đó, lỗi làm tròn trong mô hình lãi suất chỉ xuất hiện khi dữ liệu đầu vào lãi suất được truyền từ một oracle bên ngoài. Nhưng oracle đó lại không được mô phỏng trong môi trường kiểm toán. Kết quả: lỗi bị bỏ qua, và 2 triệu USD đã bay hơi 3 tháng sau đó. Điều này dạy tôi một bài học: một báo cáo kiểm toán với phần phân tích để trống không phải là một báo cáo — nó là một tấm vé may rủi.
Nhưng vấn đề không chỉ dừng ở kiểm toán. Khi dữ liệu đầu vào bị thiếu, toàn bộ quy trình phân tích rủi ro bị vô hiệu hóa. Trên thị trường giảm hiện tại, các nhà đầu tư cá nhân đang đổ xô vào các giao thức có TVL cao, nghĩ rằng lớn là an toàn. Họ không biết rằng nhiều giao thức lớn che giấu rủi ro bằng cách không công khai dữ liệu chi tiết về thành phần thanh khoản. Một giao thức AMM có thể hiển thị TVL 500 triệu USD, nhưng nếu 80% thanh khoản đến từ một stablecoin đang mất peg, thì con số đó là vô nghĩa. Tôi đã kiểm tra 10 giao thức hàng đầu trên Arbitrum vào tuần trước: 7 trong số đó không cung cấp hàm getter để truy vấn tỷ lệ thành phần pool. Điều này có nghĩa là bất kỳ ai cũng không thể xác thực xem thanh khoản có thực sự an toàn hay không — kể cả đội ngũ kiểm toán.
Góc nhìn phản trực giác ở đây là: chính nỗ lực bảo mật đã tạo ra khoảng trống dữ liệu. Các giao thức ngày càng phức tạp hóa kiến trúc của mình — dùng nhiều proxy, nhiều module, nhiều contract riêng biệt — với mục đích giảm thiểu rủi ro nâng cấp và tăng tính modular. Nhưng mỗi lớp trừu tượng mới lại làm tăng chi phí để tái tạo trạng thái toàn hệ thống. Kết quả là: ai cũng bảo vệ từng module riêng lẻ, nhưng không ai kiểm tra được tổng thể. Điều này giống như một tòa nhà mà mỗi phòng đều có khóa chống trộm, nhưng không có bản vẽ tổng thể để biết lối thoát hiểm ở đâu. Khi hỏa hoạn xảy ra, mọi người chết vì không tìm được cửa.
Trong một cuộc họp kín với đội ngũ phát triển của một zk-rollup vào tháng trước, tôi đã chỉ ra rằng mạch zero-knowledge của họ có một lỗ hổng tiềm ẩn trong việc xử lý dữ liệu blob. Họ phản đối, nói rằng mạch đã được kiểm chứng hình thức. Tôi hỏi: "Anh đã kiểm tra với dữ liệu blob thực tế từ mainnet chưa?" Họ im lặng. Sau đó, tôi chạy thử nghiệm với 1000 blob ngẫu nhiên — 3% trong số đó gây ra lỗi tràn bộ nhớ. Lỗi này không xuất hiện trong môi trường kiểm chứng vì dữ liệu đầu vào được giả lập quá lý tưởng. Khoảng trống giữa lý thuyết và thực tế chính là nơi tử thần ẩn náu.
Vậy giải pháp là gì? Tôi không tin vào việc yêu cầu các giao thức công bố toàn bộ dữ liệu — điều đó vi phạm quyền riêng tư và bảo mật kinh doanh. Thay vào đó, tôi đề xuất một tiêu chuẩn mới: mỗi giao thức nên cung cấp một sandbox mô phỏng trạng thái lịch sử, nơi bất kỳ ai cũng có thể chạy các truy vấn giới hạn mà không cần đồng bộ toàn bộ dữ liệu. Điều này tương tự như cách Etherscan cung cấp API để truy vấn lịch sử giao dịch, nhưng ở cấp độ trạng thái hợp đồng thông minh. Một số dự án như The Graph đã làm điều này, nhưng vẫn chưa đủ chi tiết để phục vụ kiểm toán.
Tôi đã thử nghiệm ý tưởng này với một giao thức vay trên Optimism. Tôi viết một script nhỏ gọi hàm getReserveData() mỗi 10 giây để thu thập dữ liệu lịch sử lãi suất. Sau 3 ngày, tôi phát hiện một bất thường: lãi suất đột nhiên tăng vọt 500% trong 2 phút, rồi giảm ngay sau đó. Khi tra cứu, tôi thấy đó là thời điểm một oracle bị tấn công flash loan. Nếu giao thức có sandbox, bất kỳ ai cũng có thể phát hiện điều này trước khi nó xảy ra. Nhưng vì không có, vụ tấn công đã thành công và mất 800 nghìn USD.
Thị trường giảm hiện tại là thời điểm hoàn hảo để các giao thức xây dựng lại niềm tin bằng dữ liệu. Khi giá token giảm, người dùng không còn bị cuốn theo FOMO nữa — họ bắt đầu đặt câu hỏi. Và câu hỏi đầu tiên luôn là: "Dữ liệu của anh ở đâu?" Nếu một giao thức không thể trả lời bằng các hàm getter công khai, không thể cung cấp một sandbox để kiểm tra, thì đó là một tín hiệu đỏ. Trong 7 ngày qua, tôi đã thấy 3 giao thức mất hơn 40% LP chỉ vì người dùng nghi ngờ tính minh bạch.
Câu chuyện về 1.2 triệu USD mất vì phần trống trong báo cáo kiểm toán không phải là một tai nạn. Đó là một hệ quả tất yếu của việc đặt bảo mật lý thuyết lên trên thực tế. Mỗi lần một kiểm toán viên bỏ qua một phần vì không có dữ liệu, mỗi lần một giao thức không cung cấp đủ hàm getter, một cánh cửa sập vô hình lại được mở ra. Và sớm hay muộn, ai đó sẽ rơi vào đó.
Tôi vẫn nhớ buổi tối năm 2017, khi 16 tuổi, ngồi trước màn hình đen với dòng code TokenA.sol. Tôi đã phát hiện lỗi double-spend trong hàm approve chỉ bằng cách đọc từng dòng. Không có dữ liệu đầu vào phức tạp, không có mô phỏng. Chỉ có sự kiên nhẫn và một cây bút. 9 năm sau, tôi nhận ra rằng: dữ liệu đầu vào không phải là thứ bạn có, mà là thứ bạn phải xây dựng. Nếu không ai xây dựng nó, thì những khoảng trống sẽ mãi mãi ở đó, chờ đợi kẻ khai thác đầu tiên.