Năm 2017, tôi 27 tuổi, ngồi trong căn hộ nhỏ ở Đà Nẵng với một project ICO mang tên EtherLend. Hợp đồng thông minh dài 500 dòng, Solidity thuần, không test fuzz, không audit bên thứ ba. Tôi được giao kiểm tra mã nguồn trong 2 tuần. Tuần đầu tiên, tôi chỉ đọc logic cơ bản: lending pool, cơ chế lãi suất, và hàm rút token. Mọi thứ trông ổn.
Cho đến khi tôi mở hàm `withdraw()` lần thứ ba.
Dòng 47: require(balanceOf[msg.sender] >= amount); Dòng 48: msg.sender.call.value(amount)(""); Dòng 49: balanceOf[msg.sender] -= amount;
Reentrancy cổ điển. Kiểu tấn công năm 2016 đã hạ gục The DAO, nhưng ở đây nó vẫn tồn tại nguyên vẹn. Chỉ cần kẻ tấn công tạo một contract fallback gọi lại withdraw() trước khi số dư được cập nhật, họ có thể rút toàn bộ pool token.
Tôi báo cáo ba lỗi nghiêm trọng lên GitHub: (1) reentrancy trong withdraw, (2) thiếu kiểm tra toán học tràn số trong tính lãi, (3) access control cho phép owner mint token không giới hạn. Dự án buộc phải hoãn ICO 3 tháng để vá lỗi. Kết quả? Họ mời tôi làm cố vấn kỹ thuật. Đó là lần đầu tiên tôi nhận ra: code là chứng cứ duy nhất trong thế giới phi tập trung.
Bối cảnh thị trường: ICO mania và văn hóa "ship first, fix later"
Năm 2017, thị trường crypto chứng kiến cơn sốt ICO. Hàng trăm dự án huy động hàng triệu USD chỉ với một whitepaper và một hợp đồng thông minh sơ sài. EtherLend không phải ngoại lệ. Họ có đội ngũ 10 người, 3 developer, và không ai từng audit smart contract trước đó. Lý do phổ biến: "Chúng tôi đã test trên testnet và mọi thứ hoạt động."
Vấn đề: testnet không có kẻ tấn công có động cơ kinh tế. Reentrancy chỉ lộ diện khi có ETH thật và gas thật. Đây là điểm mù của toàn bộ ngành: tin tưởng vào test thủ công thay vì formal verification và fuzz testing.

Core: Phân tích kỹ thuật lỗ hổng reentrancy và cách phòng tránh
Hợp đồng EtherLend sử dụng mô hình checks-effects-interactions nhưng sai thứ tự. Cụ thể:
// Lỗi: interaction trước effect
function withdraw(uint amount) public {
require(balanceOf[msg.sender] >= amount); // checks
msg.sender.call.value(amount)(""); // interaction (gửi ETH trước)
balanceOf[msg.sender] -= amount; // effect (cập nhật sau)
}
Khi call.value(amount) được thực thi, nếu msg.sender là một contract, fallback function của nó sẽ chạy. Fallback này có thể gọi lại withdraw() lần nữa, vì balanceOf[msg.sender] vẫn chưa thay đổi. Vòng lặp này lặp lại cho đến khi cạn pool.
Giải pháp: Sử dụng ReentrancyGuard từ OpenZeppelin hoặc đơn giản là đảo thứ tự:
function withdraw(uint amount) public {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount; // effect trước
msg.sender.call.value(amount)(""); // interaction sau
}
Nhưng đó chỉ là miếng dán. Vấn đề sâu hơn: thiếu kiểm tra trạng thái toàn cục. EtherLend không có nonReentrant modifier, không có transfer (chỉ giới hạn gas) thay vì call. Trong Solidity 0.4.x (thời điểm đó), transfer chỉ gửi 2300 gas, không đủ để fallback chạy phức tạp. Nhưng call mặc định gửi toàn bộ gas, mở cánh cửa cho reentrancy.
Tôi còn phát hiện một lỗi tinh vi hơn: hàm calculateInterest sử dụng now (block.timestamp) để tính lãi, nhưng không cập nhật lastUpdateTime trước khi gửi ETH. Kẻ tấn công có thể khai thác reentrancy để gọi calculateInterest nhiều lần, mỗi lần tính lãi trên cùng một khoảng thời gian, dẫn đến nhân lãi gấp nhiều lần. Đây là kiểu tấn công kết hợp giữa reentrancy và tính toán thời gian.
Contrarian: Điểm mù bảo mật mà 99% developer bỏ qua
Nhiều người nghĩ reentrancy là chuyện cũ. Nhưng đến năm 2025, tôi vẫn thấy lỗi tương tự trong các giao thức DeFi mới. Lý do? Áp lực thời gian và văn hóa "move fast". Khi thị trường tăng, các dự án đốt cháy giai đoạn để ra mắt, cắt giảm audit. Họ dùng call thay vì transfer vì muốn linh hoạt, nhưng quên thêm guard.
Một điểm mù khác: reentrancy cross-function. Trong EtherLend, tôi phát hiện rằng nếu hàm withdraw() gọi một hàm khác (ví dụ updateReward()) và hàm đó lại gọi withdraw() thông qua fallback, thì reentrancy vẫn xảy ra dù đã đảo thứ tự. Giải pháp: dùng mutex cấp contract, không chỉ cấp function.
Kinh nghiệm từ vụ EtherLend đã thay đổi cách tôi audit. Tôi không còn tin vào bất kỳ dòng code nào mà chưa chạy qua fuzz testing ít nhất 1000 lần. Tôi cũng bắt đầu viết blog kỹ thuật chi tiết, chia sẻ từng lỗi mình tìm thấy, bởi vì im lặng không phải là vàng – code mới là chứng cứ.
Takeaway: Dự báo lỗ hổng và lời khuyên cho developer 2026
Thị trường gấu hiện tại (2025-2026) đang loại bỏ các dự án yếu kém. Nhưng những dự án sống sót thường có xu hướng "cắt giảm chi phí" bằng cách bỏ qua audit hoặc dùng các công cụ tự động rẻ tiền. Tôi dự đoán: trong 6 tháng tới, ít nhất 3 giao thức DeFi top 100 sẽ bị tấn công reentrancy cổ điển vì chủ quan. Lời khuyên: hãy dùng ReentrancyGuard, test fuzz bằng Echidna, và luôn kiểm tra thứ tự checks-effects-interactions. Đừng để lịch sử lặp lại.
Bạn có chắc code của bạn an toàn? Hãy mở debugger và nhìn vào hàm withdraw() lần nữa.