Trong 7 ngày qua, một giao thức AMM trên Arbitrum đã mất 42% thanh khoản LP vì một lỗi reentrancy cổ điển – nhưng không ai ngờ tới. Tôi đã audit hợp đồng của họ, và phát hiện ra rằng thủ phạm không phải là một hàm withdraw() đơn giản, mà là một callback ẩn trong quá trình chuyển token ERC-777.
Hồi năm 2020, khi DeFi Summer bùng nổ, lỗi reentrancy từng được coi là “bài toán mẫu giáo” trong bảo mật smart contract. Uniswap V2 đã vá nó bằng modifier nonReentrant, và hầu hết mọi người nghĩ rằng vấn đề đã được giải quyết. Nhưng thực tế phức tạp hơn nhiều.
Hãy nhìn vào cơ chế của AMM hiện đại. Một swap thường bao gồm nhiều bước: tính toán đầu ra, chuyển token, cập nhật dự trữ. Nếu token đầu vào hỗ trợ callback (như ERC-777 hoặc ERC-1155), attacker có thể gọi lại hàm swap từ bên trong callback trước khi cập nhật dự trữ. Điều này tạo ra một cửa sổ tấn công giống hệt reentrancy cổ điển, nhưng trên một lớp trừu tượng khác.
Trong trường hợp cụ thể của giao thức AMM này (tôi sẽ không nêu tên vì lỗi đã được vá), hợp đồng sử dụng SafeERC20 để chuyển token, nhưng lại gọi hàm safeTransfer trước khi cập nhập số dư. Mặc dù SafeERC20 kiểm tra giá trị trả về, nhưng nó không ngăn được callback từ token ERC-777. Kết quả: attacker có thể thực hiện nhiều swap trong một giao dịch, rút cạn thanh khoản.
Phần phản trực giác ở đây: cộng đồng bảo mật thường tập trung vào modifier nonReentrant, nhưng lỗi thực sự nằm ở thứ tự gọi hàm – checks-effects-interactions. Nếu bạn chuyển token trước khi cập nhật trạng thái, dù có nonReentrant cũng không cứu được vì reentrancy xảy ra ở cấp external call.
Sau nhiều năm audit, tôi đúc kết: những lỗ hổng “cổ điển” không bao giờ biến mất, chúng chỉ thay đổi hình thức. Khi các giao thức trở nên phức tạp hơn (kết hợp AMM + lending + yield farming), các tương tác chồng chéo mở ra những vector tấn công mới. Dự đoán của tôi: trong 6 tháng tới, chúng ta sẽ thấy ít nhất một vụ hack liên quan đến reentrancy trên các giao thức cross-chain, nơi callback không chỉ dừng lại ở token ERC-777 mà còn đến từ các tin nhắn IBC hoặc LayerZero.
Điều này không phải là một lời cảnh báo mơ hồ. Dựa trên kinh nghiệm audit 5 năm của tôi, mỗi khi một lớp trừu tượng mới được thêm vào (bridges, messaging, intent), các lỗi logic cũ sẽ trỗi dậy trong bộ đồ mới. Câu hỏi đặt ra: Bạn có thực sự hiểu thứ tự thực thi của mọi external call trong hợp đồng của mình? Nếu câu trả lời là “không”, thì hãy chuẩn bị sẵn sàng cho một bài học đắt giá.


