Tôi nhận được một smart contract kiểm toán cách đây ba tuần. Đó là một hợp đồng cho vay cross-chain, mã nguồn trông rất sạch sẽ, được comment bằng tiếng Anh chuẩn chỉnh. Nhưng có một dòng khiến tôi dừng lại. Ở hàm withdraw(), dòng thứ 47, thay vì require(balanceOf[msg.sender] >= amount), code viết là require(balanceOf[address(this)] >= amount). Một lỗi copy-paste kinh điển từ một template fork từ năm 2021.
Lỗi này không phải do hacker phát hiện. Nó nằm im trong codebase suốt 8 tháng, qua hai vòng audit của các công ty bảo mật có tên tuổi. Vấn đề không phải là kỹ thuật ZK hay mật mã phức tạp. Vấn đề là con người đã sao chép code mà không hiểu nó. Và đây chính là điểm mù lớn nhất của DeFi hiện tại: chúng ta đang xây nhà trên nền cát của những đoạn code fork.
Trong thị trường đi ngang này, khi TVL sụt giảm và các dự án đua nhau cắt giảm chi phí, tôi thấy một xu hướng đáng báo động: các đội ngũ đang fork mã nguồn của Uniswap, Compound hay Aave, rồi gửi đi audit với tâm lý 'chỉ cần qua audit là an toàn'. Họ quên mất rằng audit chỉ kiểm tra những gì bạn viết, không kiểm tra những gì bạn không viết. Một bản fork có thể chứa lỗ hổng từ giao thức gốc, hoặc tệ hơn, lỗ hổng được tạo ra do việc kết hợp không đồng bộ giữa các module code khác nhau.
Tôi gọi đây là 'lỗ hổng di truyền'. Giống như bệnh di truyền trong sinh học, những lỗi trong code gốc sẽ được sao chép y nguyên qua các thế hệ fork, và đôi khi, chúng còn kết hợp với nhau để tạo ra những lỗi mới mà không ai ngờ tới. Hãy nhìn vào vụ exploit $10 triệu của Euler Finance năm 2023. Lỗi không phải ở logic cho vay cốt lõi, mà ở cách họ xử lý callback trong hàm donateToReserves() - một hàm tưởng chừng như vô hại, được copy từ codebase cũ. Kẻ tấn công đã khai thác chính xác điểm kết nối giữa module cũ và module mới, một điểm mà không audit nào có thể phát hiện nếu họ chỉ kiểm tra từng module riêng lẻ.
Đây là lúc ZK-proof phát huy tác dụng, nhưng không phải theo cách mà mọi người thường nghĩ. ZK không chỉ là công cụ để mở rộng quy mô hay bảo vệ quyền riêng tư. Nó là một cơ chế để xác minh rằng code thực thi đúng như thiết kế, ngay cả khi code đó là một bản fork. Nếu mỗi giao thức DeFi đều có một ZK-circuit xác minh tính toàn vẹn của quá trình thực thi, thì những lỗi copy-paste như dòng 47 kia sẽ bị phát hiện ngay lập tức, bởi vì circuit sẽ kiểm tra mọi ràng buộc logic, bao gồm cả những ràng buộc mà con người bỏ quên.
Nhưng thực tế phũ phàng hơn nhiều. Chi phí để xây dựng một ZK-circuit cho mỗi giao thức là rất lớn. Theo tính toán của tôi, để triển khai ZK-rollup cho một giao thức cho vay đơn giản, bạn cần ít nhất 3 tháng phát triển và một đội ngũ 5 kỹ sư chuyên về ZK. Với tình hình thị trường hiện tại, khi mà các dự án đang cắt giảm 30% nhân sự, không ai dám đầu tư vào thứ 'xa xỉ' này. Họ chọn con đường fork code có sẵn, audit nhanh, rồi hy vọng không có ai khai thác. Đây là một canh bạc, và tôi đã thấy quá nhiều dự án thua cuộc.
Hãy nhìn vào một case study cụ thể. Năm 2022, tôi audit một giao thức farm yield fork từ Trader Joe. Code gốc của Trader Joe đã được audit kỹ lưỡng bởi Halborn và Trail of Bits. Nhưng khi fork, đội ngũ này đã thay đổi cơ chế phân phối phần thưởng từ transfer sang mint để tiết kiệm gas. Thay đổi nhỏ này đã tạo ra một lỗ hổng reentrancy mới, bởi vì hàm mint trong ERC-20 của họ không implement đúng chuẩn OpenZeppelin. Kết quả là 2 triệu USD bị rút sạch trong vòng 3 block. Audit của họ đã kiểm tra logic farm, nhưng không kiểm tra logic token. Họ cho rằng token 'đã được audit', nhưng thực tế token đó là một phiên bản sửa đổi không qua audit.
Điểm mù ở đây là gì? Đó là sự phụ thuộc tuyến tính vào audit. Thị trường đang đặt quá nhiều niềm tin vào audit như một 'con dấu vàng', trong khi audit chỉ là một bức ảnh chụp tại một thời điểm. Code thay đổi, cấu hình thay đổi, và ngay cả môi trường runtime cũng thay đổi. Một ZK-circuit, nếu được thiết kế đúng, có thể xác minh rằng không có thay đổi nào phá vỡ các ràng buộc bảo mật. Nhưng vì chi phí cao, chúng ta đang bỏ qua công cụ này.
Từ góc nhìn của một kẻ tấn công, họ không cần phải là thiên tài. Họ chỉ cần đọc code, tìm ra những điểm bất thường giữa bản fork và bản gốc, và khai thác sự khác biệt đó. Trong vụ Cream Finance bị hack $130 triệu, kẻ tấn công đã khai thác lỗi trong logic borrow() của Iron Bank, một fork của Compound. Lỗi đến từ việc Cream thêm tính năng flashLoan vào code mà không cập nhật logic kiểm tra nợ. Compound không có flash loan, nên code gốc an toàn. Cream thêm vào, và mọi thứ đổ vỡ.
Vậy giải pháp là gì? Tôi không tin vào giải pháp 'tất cả hoặc không có gì'. Chúng ta không thể yêu cầu mọi dự án đều phải xây dựng ZK-circuit từ đầu. Nhưng chúng ta có thể bắt đầu bằng những bước nhỏ. Thứ nhất, bất kỳ dự án nào fork code nên có một 'diff audit' - một audit tập trung vào sự khác biệt giữa bản fork và bản gốc. Điều này rẻ hơn nhiều so với audit toàn diện. Thứ hai, các công cụ formal verification như Certora hoặc Scribble nên được tích hợp vào CI/CD pipeline, không chỉ dùng một lần. Thứ ba, cộng đồng cần xây dựng một thư viện các ZK-circuit tiêu chuẩn cho các module phổ biến (như router, pool, vault), để các dự án chỉ cần plug-in thay vì code từ đầu.
Trong kinh nghiệm audit 5 năm của tôi, tôi chưa bao giờ thấy một dự án nào bị hack bởi một lỗ hổng hoàn toàn mới. Tất cả đều là biến thể của những lỗi cũ: reentrancy, oracle manipulation, access control sai. Những lỗi này tồn tại vì chúng ta không học từ quá khứ. Chúng ta fork code, nhưng không fork tư duy bảo mật. Chúng ta audit, nhưng không xác minh liên tục.
Đến đây, tôi muốn đặt một câu hỏi cho bạn: Nếu kẻ tấn công có thể dùng chính mã nguồn của bạn để tấn công bạn, thì ZK có phải là lời giải duy nhất không, hay chỉ là một miếng vá cho một vấn đề sâu hơn nhiều? Mỗi lỗ hổng là một bài học. Và bài học hôm nay là: đừng chỉ fork code. Hãy hiểu nó. Nếu không, chính code của bạn sẽ là vũ khí chống lại bạn.