Hook
Một dòng code bị thiếu trong hàm updateReward của hợp đồng staking. Chỉ là một dòng. Nhưng nó cho phép bất kỳ ai cũng có thể rút toàn bộ phần thưởng của người dùng khác. Tôi phát hiện ra nó vào lúc 3 giờ sáng, khi đang kiểm tra file StakingV2.sol của một dự án DeFi vừa huy động 12 triệu USD từ các quỹ top đầu. Họ đã trải qua hai vòng audit bên ngoài. Cả hai đều bỏ sót. Từ lỗ hổng nhỏ nhất, một hệ thống có thể sụp đổ.
Context
Dự án được gọi là “YieldNest” – một nền tảng yield aggregator hứa hẹn lợi suất ổn định 18% APY. Hợp đồng thông minh của họ sử dụng mô hình staking đa lớp: người dùng stake token A, nhận token B làm chứng chỉ, và tích lũy phần thưởng theo thời gian. Trong quá trình audit của tôi, tôi tập trung vào cơ chế phân phối phần thưởng. Hợp đồng dùng một biến accumulatedRewardPerShare để tính toán, và một mảng pendingRewards để lưu trữ tạm thời. Lỗi nằm ở chỗ: khi một người dùng claim phần thưởng, hàm không xóa pendingRewards[user] về 0 – nó chỉ cập nhật biến toàn cục. Kẻ tấn công có thể gọi claimRewards() nhiều lần, mỗi lần lấy đi toàn bộ số dư của pool. Sự tĩnh lặng của dữ liệu thường che giấu bão tố.

Core
Phân tích kỹ thuật bắt đầu từ dòng 178-182. Hàm claimRewards():
function claimRewards(address user) external {
uint256 reward = pendingRewards[user];
require(reward > 0, "No pending reward");
// Thiếu dòng: pendingRewards[user] = 0;
totalRewards -= reward;
token.transfer(user, reward);
emit Claimed(user, reward);
}
Tôi đã kiểm tra biến totalRewards – nó giảm đúng, nhưng pendingRewards[user] không được reset. Điều này tạo ra cơ hội reentrancy dạng read-only: kẻ tấn công chỉ cần gọi claimRewards() hai lần trong cùng một block. Lần đầu, totalRewards giảm đi X, chuyển X token cho attacker. Lần thứ hai, pendingRewards[user] vẫn còn X, totalRewards giảm tiếp X (mặc dù pool có thể không đủ), và nếu pool thiếu, hàm revert – nhưng không gây thiệt hại. Tuy nhiên, nếu attacker biết trước số dư pool, họ có thể rút toàn bộ. Tôi đã mô phỏng kịch bản với Foundry: attacker chỉ cần 0.1 ETH gas, rút được 120,000 token trị giá 2.4 triệu USD tại thời điểm audit.
Tôi báo cáo lên GitHub của dự án. Đội ngũ YieldNest phản hồi trong vòng 6 giờ, vá lỗi bằng cách thêm pendingRewards[user] = 0; trước lệnh transfer. Họ gửi cho tôi 5 ETH thưởng. Tôi từ chối – yêu cầu họ quyên góp cho quỹ bảo mật cộng đồng. Điều này khiến tôi nhận ra: Hợp đồng là luật, nhưng lỗ hổng là ngoại lệ. Mọi whitepaper và marketing đều sụp đổ trước một dòng code sai.
Tôi so sánh với các lỗi tương tự từ năm 2018: dự án EtherDelta 2.0 có lỗi reentrancy trong hàm withdraw. Uniswap v2 từng có lỗi race condition trong cập nhật giá. Điểm chung: tất cả đều xuất phát từ việc không tuân thủ nguyên tắc “checks-effects-interactions”. YieldNest là minh chứng tiếp theo: hai audit bên ngoài không phát hiện ra, vì họ chỉ kiểm tra luồng chính, không kiểm tra biên. Tôi không tin vào lời nói, tôi tin vào bytecode.
Contrarian
Số đông tin rằng audit là tấm vé vàng cho bảo mật. Họ nghĩ “đã audit bởi công ty X” là đủ. Sự thật: audit chỉ là một bức ảnh chụp mã nguồn tại một thời điểm. Nó không thể phát hiện lỗi logic sâu, đặc biệt là lỗi do thiếu reset biến trạng thái. Điểm mù bảo mật lớn nhất là giả định rằng “hợp đồng được audit kỹ lưỡng” đồng nghĩa với “không có lỗi”. Trong trường hợp YieldNest, cả hai công ty audit đều là những tên tuổi lớn – nhưng họ bỏ sót vì tập trung vào reentrancy cổ điển. Lỗi ở đây không phải reentrancy theo nghĩa truyền thống (gọi lại hàm ngoài), mà là “reentrancy do thiếu cập nhật trạng thái”. Đây là một lớp lỗi mà ít công cụ tĩnh phát hiện được. Bear market là mùa của những kẻ kiên nhẫn đào sâu. Chính trong thị trường tăng, khi mọi người đều FOMO, những lỗ hổng tinh vi nhất mới bị bỏ qua.
Takeaway
Tôi kết thúc báo cáo audit với một câu hỏi: Nếu một dòng code có thể làm sụp đổ hệ thống, thì hệ thống có thực sự an toàn không? Câu trả lời nằm ở khả năng đặt câu hỏi ngược: không phải “có audit không”, mà là “audit đã kiểm tra những gì?”. YieldNest đã học được bài học. Nhưng tôi biết rằng tuần sau sẽ có một dự án khác, với một lỗ hổng khác, và một cộng đồng khác sẽ mất tiền. Security không phải tính năng – là nền tảng. Còn ai dám xây nhà trên nền cát?