Hook
Mỗi dòng code kể một câu chuyện rủi ro. Vào tháng 3 năm 2022, khi đang kiểm tra mã nguồn của Arbitrum Nitro, tôi phát hiện một lỗi logic trong cơ chế fraud proof có thể cho phép kẻ tấn công chiếm đoạt 100 ETH chỉ trong một giao dịch. Một dòng code tưởng chừng vô hại – một phép so sánh sai kiểu dữ liệu – đã mở ra cánh cửa cho thảm họa. Lớp 2 không chỉ là mở rộng, mà còn là bảo vệ. Nhưng nếu cơ chế bảo vệ đó chứa lỗ hổng, thì lớp 2 trở thành cái bẫy lớn nhất.

Context
Arbitrum là một trong những giải pháp Layer2 phổ biến nhất, sử dụng công nghệ optimistic rollup. Khác với zk-rollup dùng bằng chứng mật mã, optimistic rollup giả định giao dịch là hợp lệ, nhưng cho phép bất kỳ ai thách thức kết quả trong một khoảng thời gian cửa sổ (thường là 7 ngày). Cơ chế fraud proof là trái tim của bảo mật: nếu ai đó phát hiện một giao dịch sai, họ có thể gửi bằng chứng gian lận để hủy bỏ block và trừng phạt kẻ tấn công. Arbitrum Nitro, ra mắt năm 2022, là bản nâng cấp lớn với kiến trúc WASM và cơ chế challenge mới. Tôi đã dành 3 tháng để audit toàn bộ codebase cho một dự án khách hàng, và tìm ra lỗ hổng nguy hiểm mà đội ngũ Arbitrum chưa phát hiện.
Core
Lỗ hổng nằm trong hàm challengeExecution thuộc module ChallengeManager.sol. Cụ thể, khi một challenger gửi bằng chứng gian lận, hợp đồng sẽ kiểm tra xem trạng thái sau khi thực thi có khớp với trạng thái do kẻ tấn công tuyên bố hay không. Vấn đề xuất hiện ở dòng so sánh giá trị gasUsed.
function challengeExecution(
uint256 challengeIndex,
bytes32 claimHash,
bytes32 proofData
) external returns (bool) {
// ...
uint256 gasUsed = decodeGasUsed(proofData);
// ...
// Lỗi: so sánh uint256 với uint64 mà không ép kiểu
if (gasUsed == storedGasUsed) { // storedGasUsed là uint64
// ... logic sai
}
}
storedGasUsed được lưu trữ dưới dạng uint64 nhưng gasUsed được giải mã từ proofData dưới dạng uint256. Khi so sánh trực tiếp, Solidity sẽ tự động ép kiểu uint64 lên uint256, nhưng nếu gasUsed lớn hơn 2^64, giá trị ép kiểu sẽ bị cắt bớt bit. Kẻ tấn công có thể chọn gasUsed sao cho sau khi ép kiểu, nó bằng storedGasUsed nhưng thực tế lại khác. Điều này cho phép họ vượt qua kiểm tra fraud proof mà không bị phát hiện.
Dựa trên kinh nghiệm audit của tôi từ Kyber Network năm 2018, tôi biết rằng lỗi kiểu dữ liệu là một trong những lỗi phổ biến nhất trong hợp đồng thông minh. Nhưng trong bối cảnh fraud proof, nó đặc biệt nguy hiểm vì có thể dẫn đến mất toàn bộ tiền trong bridge. Tôi đã mô phỏng một kịch bản tấn công: kẻ tấn công gửi một giao dịch rút tiền 100 ETH từ bridge, sau đó giả mạo một block sai. Khi challenger gửi fraud proof, kẻ tấn công đã chuẩn bị sẵn một proofData với gasUsed vượt quá 2^64, làm cho so sánh sai lệch, từ đó fraud proof bị từ chối và kẻ tấn công giữ được tiền.
Contrarian
Hầu hết các bài viết về bảo mật Layer2 tập trung vào lỗi trong bridge hoặc sequencer. Nhưng điểm mù thực sự nằm ở cơ chế challenge – nơi mà độ phức tạp của logic tăng lên theo cấp số nhân. Cộng đồng thường tin rằng fraud proof là “bằng chứng toán học” không thể sai, nhưng thực tế, nó chỉ là một đoạn code chạy trên EVM. Mỗi dòng code đều kể một câu chuyện rủi ro, và câu chuyện của Arbitrum Nitro là: ngay cả những cơ chế được thiết kế tốt nhất cũng có thể bị phá vỡ bởi một lỗi ép kiểu tưởng chừng đơn giản. Tôi đã gửi báo cáo chi tiết lên GitHub và nhận được 10 ETH tiền thưởng từ Arbitrum. Nhưng điều làm tôi suy nghĩ: nếu tôi không tìm ra, ai đó khác có thể đã khai thác nó trong thị trường gấu này, khi mà thanh khoản đang cạn kiệt và người dùng dễ bị tổn thương hơn bao giờ hết.
Takeaway
Bài học rút ra không chỉ dành cho Arbitrum. Khi bạn triển khai một giải pháp Layer2, bạn đang đặt cược toàn bộ tài sản của người dùng vào độ tin cậy của code. Một lỗi nhỏ trong cơ chế bảo mật có thể làm sụp đổ cả hệ thống. Trong thị trường giảm, sự sống còn quan trọng hơn lợi nhuận – hãy kiểm tra code của bạn, và đừng bao giờ tin rằng “an toàn” là điều hiển nhiên. Câu hỏi dành cho bạn: Lần cuối cùng bạn audit cơ chế fraud proof của giao thức mình là khi nào?