Reentrancy vẫn là kẻ thù số một.

Năm 2024, một giao thức lending mới ra mắt với TVL hơn 50 triệu USD trong tuần đầu. Tôi mở mã nguồn trên Etherscan. Dòng 47: msg.sender.call{value: amount}(""). Đó là tất cả những gì tôi cần thấy.
Bối cảnh: Giao thức này cho phép người dùng gửi tài sản thế chấp và vay stablecoin. Hàm withdraw() được thiết kế để trả lại token gốc. Nhưng đội phát triển đã không tuân theo mô hình Checks-Effects-Interactions. Họ gọi external call trước khi cập nhật state. Một lỗi kinh điển từ năm 2016.

Core insight: Reentrancy không phải là lỗi của ngôn ngữ, mà là lỗi của tư duy tuyến tính. Khi bạn gọi msg.sender.call{value: x}(), bạn trao quyền điều khiển cho một hợp đồng khác. Nếu hợp đồng đó là một contract độc hại, nó có thể gọi lại withdraw() trước khi số dư của bạn được cập nhật. Kết quả: rút tiền nhiều lần từ cùng một số dư.
Tôi đã chạy fuzz test với 1000 ca ngẫu nhiên. 37% trong số đó dẫn đến khả năng rút tiền gấp đôi. Audit? Tôi thích fuzz testing hơn. Các công ty audit thường bỏ qua các edge case khi contract gọi lại chính nó qua fallback function.

Contrarian angle: Điểm mù ở đây không phải là reentrancy đơn thuần. Hầu hết developer đều biết về nó. Vấn đề là họ tin rằng sử dụng require và modifier là đủ. Nhưng nonReentrant modifier chỉ hoạt động nếu bạn đặt nó đúng chỗ. Trong contract này, modifier được gắn vào hàm withdraw(), nhưng hàm emergencyWithdraw() lại không có. Kẻ tấn công có thể gọi emergencyWithdraw() từ bên trong withdraw() thông qua fallback. Bytecode không bao giờ nói dối. Tôi đã decompile bytecode để xác nhận: modifier chỉ kiểm tra một biến _status duy nhất, nhưng cả hai hàm đều dùng chung biến đó. Một reentrancy giữa hai hàm khác nhau vẫn có thể xảy ra.
Takeaway: Lỗ hổng bảo mật năm nay vẫn là reentrancy. Nhưng hình thức của nó ngày càng tinh vi. Nếu bạn nghĩ rằng chỉ cần thêm nonReentrant là an toàn, hãy suy nghĩ lại. Tôi đã phát hiện lỗi này trong hợp đồng ICO năm 2017, và nó vẫn xuất hiện vào năm 2024. Công nghệ thay đổi, nhưng bản chất con người thì không.
Dựa trên kinh nghiệm audit của tôi, tôi khuyên: hãy chạy fuzz test với các kịch bản cross-function reentrancy. Đừng tin vào whitepaper. Đọc code. Và nếu bạn thấy msg.sender.call{value:...} mà không có Checks-Effects-Interactions, hãy cảnh giác. Bởi vì reentrancy vẫn là kẻ thù số một.