Hook
Đào mã thấy lỗi, im lặng là vàng.
Hai tuần trước, tôi nhận được một contract staking từ một dự án DeFi Việt Nam khá nổi – vốn hóa từng chạm 50 triệu USD. Tôi mở Remix, compile, deploy thử trên testnet. Chạy thử hàm withdraw với số dư 0.01 ETH. Hợp đồng trả về 0.01 ETH. Rồi tôi thử gọi lại withdraw ngay trong cùng một transaction, trước khi số dư được cập nhật. Hợp đồng trả về thêm 0.01 ETH nữa. Đào mã thấy lỗi, im lặng là vàng. Lỗi reentrancy cổ điển – nhưng nó tồn tại trong một codebase đã qua hai lần audit bởi các công ty bảo mật quốc tế.
Context
Giao thức cho vay này (tạm gọi là VN-Lend) ra mắt năm 2023, huy động 10 triệu USD từ các quỹ đầu tư mạo hiểm. Mô hình: người dùng gửi tài sản thế chấp (USDT, ETH, BTC) để vay stablecoin VNDC. Lãi suất biến động theo cung cầu. Tổng giá trị khóa (TVL) từng đạt 200 triệu USD. Thị trường giảm 2024 khiến TVL lao dốc còn 30 triệu USD. Dự án vẫn sống, nhưng thanh khoản cạn, bẫy còn đó. Khi tôi audit, tôi phát hiện một lỗi reentrancy trong module withdrawCollateral. Cụ thể, hàm withdraw gọi msg.sender.call{value: amount}("") trước khi cập nhật số dư nội bộ. Điều này cho phép kẻ tấn công tạo một contract độc hại, gọi lại withdraw từ fallback function, rút toàn bộ tài sản chỉ trong một giao dịch.
Core
Phân tích code: Hàm withdrawCollateral trong VN-Lend viết bằng Solidity 0.8.0 (có bảo vệ reentrancy mặc định?), nhưng họ dùng address.call{value: amount}("") thay vì transfer hoặc send. Đây là lỗi phổ biến. Sau khi gửi ETH, họ mới cập nhật balanceOf[msg.sender] -= amount. Trong khi đó, fallback của contract tấn công gọi lại withdrawCollateral với cùng msg.sender, và hợp đồng vẫn còn số dư cũ. Kết quả: kẻ tấn công rút được gấp nhiều lần.
Tôi đã kiểm tra lịch sử giao dịch của VN-Lend trên mainnet. Trong 6 tháng qua, có 3 giao dịch bất thường với cùng pattern: một contract tương tác nhiều lần với hợp đồng trong cùng block. Tổng thiệt hại ước tính 1.2 triệu USD. Nhưng dự án không công bố, vì họ âm thầm bồi thường cho người dùng bị ảnh hưởng bằng token quản trị. Thanh khoản cạn, bẫy còn đó – lỗi vẫn tồn tại trong codebase cho đến khi tôi phát hiện.
Thử nghiệm của tôi: Tôi deploy contract tấn công mẫu, nạp 1 ETH, gọi withdraw. Contract gốc trả 1 ETH, sau đó fallback gọi lại, lấy thêm 1 ETH. Tổng 2 ETH từ 1 ETH. Đào mã thấy lỗi. Nếu tôi thực hiện trên mainnet với 100 ETH, tôi có thể rút toàn bộ pool thanh khoản.
Contrarian
Điểm mù ở đây không phải là lỗi reentrancy – ai cũng biết đó là lỗi cổ điển. Điểm mù là: tại sao audit không phát hiện? Báo cáo audit từ công ty X chỉ kiểm tra các module chính như lending, borrowing, bỏ qua module withdrawCollateral vì nó được cho là không quan trọng (ít người dùng). Sai lầm: các auditor tập trung vào logic kinh doanh, bỏ qua các hàm helper. Thực tế, kẻ tấn công luôn nhắm vào các lỗ hổng ít ngờ tới.
Hơn nữa, lỗi này không thể phát hiện bằng phân tích tĩnh thông thường, vì nó phụ thuộc vào thứ tự gọi contract bên ngoài. Các công cụ phân tích tĩnh hiện tại (Slither, Mythril) bỏ qua fallback function trong các cuộc gọi động. Đây là một lỗ hổng kiến trúc: thiết kế của hợp đồng không tuân thủ mô hình checks-effects-interactions.
Takeaway
Bài học: Thị trường giảm không chỉ giết chết dự án yếu, mà còn phơi bày những lỗ hổng bảo mật bị che đậy giữa thị trường tăng. Khi thanh khoản cạn, bẫy còn đó – và chỉ những kẻ đào mã mới thấy. Tôi từng audit ICO năm 2017, Uniswap V2 năm 2020, Bittensor năm 2025. Kinh nghiệm của tôi: lỗi reentrancy vẫn là kẻ thù số một, ngay cả khi có nhiều lớp bảo vệ. Đừng tin vào audit, hãy tin vào code.
DeFi không tha thứ cho mã nguồn cẩu thả. Nhưng thị trường giảm tha thứ cho kẻ biết im lặng. Tôi đã báo cáo lỗi lên GitHub của VN-Lend và nhận được bounty 50,000 VNDC (khoảng 2,000 USD). Họ vá lỗi trong 24 giờ. Nhưng còn bao nhiêu lỗi khác đang ngủ yên trong các hợp đồng Việt Nam? Câu hỏi đó không có lời giải.