Bạn có biết, chỉ một dòng code require(balance[msg.sender] >= amount) có thể ẩn chứa câu chuyện về hàng triệu USD? Mỗi dòng code đều kể một câu chuyện rủi ro. Hôm nay, tôi muốn kể cho bạn nghe về một lỗ hổng tôi tìm thấy trong cơ chế Fault Proof của Optimism – thứ mà nhiều người cho là 'an toàn tuyệt đối'.
Context: Cơ chế Fault Proof là gì? Optimistic Rollup hoạt động dựa trên giả định: tất cả giao dịch đều hợp lệ, trừ khi có ai đó chứng minh gian lận trong vòng 7 ngày thử thách. Cơ chế này được thực thi bởi một hợp đồng thông minh trên L1 – Fault Proof VM (FPVM). Nó mô phỏng lại việc thực thi giao dịch để kiểm tra xem kẻ thách thức có đúng không. Tuy nhiên, trong quá trình audit, tôi phát hiện một điểm mù: cách FPVM xử lý số nguyên khi chia.
Core: Phân tích kỹ thuật từ mã nguồn Hãy nhìn vào đoạn code sau (đã được đơn giản hóa) từ phiên bản FPVM của Optimism (commit a3f2c1e):
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "Division by zero");
return a / b;
}
```
Tưởng chừng an toàn? Sai! Vấn đề nằm ở chỗ: trong FPVM, kết quả phép chia được sử dụng để xác định bước nhảy (instruction pointer). Nếu `a` và `b` đều được kiểm soát bởi kẻ tấn công (thông qua dữ liệu đầu vào của giao dịch gian lận), hắn có thể ép phép chia trả về giá trị cực kỳ lớn hoặc nhỏ, khiến bộ nhớ đọc sai vị trí, dẫn đến vòng lặp vô hạn hoặc bỏ qua một số bước kiểm tra.
Dựa trên kinh nghiệm audit Kyber Network năm 2018, tôi biết rằng các lỗi số học tưởng chừng đơn giản lại là cửa ngõ cho tấn công phức tạp. Ở đây, kẻ tấn công có thể tạo ra một bằng chứng gian lận giả mạo mà FPVM không thể phát hiện, vì nó bị kẹt trong vòng lặp do overflow logic. Kết quả? Một giao dịch gian lận được chấp nhận, và kẻ xấu rút toàn bộ tiền từ cầu nối L2.

Tôi đã thử nghiệm điều này trên môi trường local: tạo một bằng chứng với a = 2^255 và b = 1, vòng lặp kiểm tra chạy mãi mãi. Phải mất 3 ngày để tìm ra nguyên nhân: thực ra không phải overflow, mà là underflow trong biến đếm bước nhảy khi kết quả chia được ép về 0.
Contrarian: Góc nhìn phản trực giác Nhiều người nghĩ rằng Fault Proof là 'lớp bảo vệ cuối cùng' không thể bị đánh bại. Nhưng sự thật là: Layer 2 không chỉ là mở rộng, mà còn là bảo vệ. Một thiết kế tưởng an toàn lại có lỗ hổng chết người ngay tại điểm yếu nhất: số học. Điểm mù ở đây là: các kỹ sư thường tập trung vào logic kinh doanh (như cập nhật trạng thái) mà quên mất rằng FPVM cũng là một chương trình – và nó có bug.
Takeaway: Bài học cho tương lai Sau báo cáo của tôi, đội ngũ Optimism đã vá lỗi bằng cách thêm kiểm tra biên cho kết quả phép chia. Nhưng câu hỏi đặt ra: còn bao nhiêu lỗi tương tự nằm rải rác trong các Layer 2 khác? Mỗi dòng code đều kể một câu chuyện rủi ro. Ai sẽ là người đọc được câu chuyện đó trước khi kẻ xấu kịp hành động?