Một giao thức lending vừa mất 500 ETH chỉ vì quên một dòng check trong hàm withdraw. Tôi sẽ chỉ cho bạn dòng code đó. Đây không phải chuyện đùa. Đêm qua, tôi nhận được log từ một bot cảnh báo on-chain. Pool thanh khoản của giao thức ‘LendFast’ trên Arbitrum giảm 40% TVL trong 3 block. Tôi lập tức mở Etherscan, trace giao dịch. Kẻ tấn công đã gọi hàm withdraw 7 lần trong một transaction duy nhất, mỗi lần rút thêm 70 ETH. Tổng cộng 500 ETH biến mất. Lỗi? Reentrancy cổ điển. Hàm withdraw cập nhật số dư người dùng sau khi gửi ETH. Sai lầm cơ bản nhưng vẫn xuất hiện ở năm 2026. Hãy cùng tôi phân tích từng dòng.
Bối cảnh giao thức LendFast là một giao thức cho vay phi tập trung trên Arbitrum, TVL đạt 2 triệu USD trước khi bị tấn công. Nó cho phép người dùng gửi tài sản thế chấp (ETH, USDC) và vay stablecoin. Hợp đồng thông minh được viết bằng Solidity 0.8.20, có sử dụng OpenZeppelin ReentrancyGuard nhưng… không áp dụng vào hàm withdraw. Đây là điểm mù. Tôi đã audit bốn hợp đồng tương tự trong quá khứ và biết rằng lỗi này thường xảy ra khi dev copy code từ ví dụ cũ mà quên modifier. LendFast không phải ngoại lệ.
Phân tích kỹ thuật cốt lõi Hãy nhìn vào hàm withdraw đơn giản hóa của họ:
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount; // Cập nhật sau khi gửi
}
Dòng cuối cùng là tử huyệt. Kẻ tấn công tạo một hợp đồng tấn công với hàm fallback gọi lại withdraw. Khi LendFast gửi ETH đến hợp đồng tấn công, fallback kích hoạt, gọi withdraw lần nữa trước khi balances[msg.sender] được cập nhật. Vòng lặp tiếp diễn cho đến khi gas hết hoặc pool cạn. Kết quả: kẻ tấn công rút được nhiều hơn số dư thực tế. Cách fix: thêm nonReentrant modifier từ OpenZeppelin hoặc chuyển dòng cập nhật balances[msg.sender] -= amount; lên trước lệnh gửi ETH. Đây là bài học cơ bản mà tôi đã dạy team DeFi Nigeria vào năm 2022.
Trade-off và góc nhìn phản trực giác Nhiều người cho rằng dùng call thay vì transfer là an toàn hơn vì không giới hạn gas. Sai. call mở ra reentrancy nếu không có guard. Nhưng transfer cũng bị deprecated vì gas limit 2300 không đủ cho các fallback phức tạp. Giải pháp thực sự là thiết kế checks-effects-interactions pattern. Tôi đã thấy quá nhiều dự án bỏ qua nguyên tắc này vì nghĩ “chỉ là lending nhỏ, không ai hack”. Thực tế, bot săn lỗi luôn quét mạng. LendFast mất 500 ETH chỉ sau 3 block kể từ khi deploy. Điểm mù bảo mật thứ hai: họ không dùng proxy upgradeable, nên không thể vá nhanh. Họ phải triển khai hợp đồng mới và cầu nối lại TVL. Đây là lý do tôi luôn khuyên dùng UUPS proxy.
Dự báo lỗ hổng và takeaway Trong 6 tháng tới, tôi dự đoán sẽ có thêm ít nhất 3 vụ tấn công reentrancy tương tự trên các giao thức fork từ codebase cũ. Lý do: thị trường giảm khiến dev cắt giảm chi phí audit, bot tấn công tự động hóa ngày càng tinh vi. Nếu bạn đang giữ LP trong các giao thức lending nhỏ, hãy kiểm tra xem họ có dùng ReentrancyGuard ở mọi hàm thay đổi trạng thái không. Một câu hỏi cho bạn: liệu giao thức bạn đang stake có dùng call mà không có guard không? Nếu không, hãy rút ngay.