Hook
Cuối tháng 9 năm 2026, một giao thức staking DeFi có tên YieldFi (TVL ~1.2 tỷ USD) bị rút sạch quỹ phần thưởng chỉ trong 3 block. Tổng thiệt hại: 47 triệu USD. Điều kỳ lạ: không có oracle nào bị tấn công, không có reentrancy, không có flash loan. Toàn bộ exploit chỉ dựa vào một dòng code chưa đầy 20 ký tự trong hợp đồng RewardPool.sol. Khi tôi fork repo và phát hiện lỗi đó, tôi nhận ra rằng hầu hết các auditor đã bỏ qua một vector tấn công cực kỳ đơn giản: tính toán phần thưởng dựa trên block.timestamp kiểu unchecked.
Context
YieldFi là giao thức staking thanh khoản, cho phép người dùng stake token LP vào các pool và nhận phần thưởng dưới dạng token YFI. Hợp đồng RewardPool sử dụng cơ chế updateReward() được gọi mỗi khi có deposit hoặc withdraw. Lượng phần thưởng được tính dựa trên công thức: reward = (amount * accumulatedRewardPerToken) / 1e18. Điểm mấu chốt là accumulatedRewardPerToken được cập nhật mỗi lần dựa trên block.timestamp. Các audit trước đó (của hai công ty audit hàng đầu) đều kiểm tra reentrancy, overflow, và kiểm soát truy cập, nhưng không ai để ý rằng block.timestamp có thể bị thao túng bởi validator trong một khoảng thời gian ngắn (15 giây trên Ethereum). YieldFi đã giả định rằng block.timestamp là đáng tin cậy và không thể bị kẻ tấn công kiểm soát trực tiếp – một giả định sai lầm trong bối cảnh MEV và validator có thể phối hợp.
Core
Đây là những gì code thực sự nói. Trong hợp đồng RewardPool.sol (tôi đã fork repo và kiểm tra phiên bản ngay trước sự kiện), hàm updateReward() được viết như sau:
function updateReward(address account) internal {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
}
Hàm rewardPerToken() tính toán:
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) return rewardPerTokenStored;
return rewardPerTokenStored.add(
(lastTimeRewardApplicable() - lastUpdateTime).mul(rewardRate).mul(1e18).div(totalSupply)
);
}
Và lastTimeRewardApplicable() trả về block.timestamp nếu thời gian kết thúc chưa đến. Lỗi nằm ở chỗ: rewardRate là hằng số được set khi pool được funding. Kẻ tấn công có thể khai thác block.timestamp bằng cách gọi deposit() với một lượng nhỏ token ngay sau khi block được tạo, nhưng trước khi lastUpdateTime được cập nhật. Nếu kẻ tấn công có thể khiến block.timestamp của block tiếp theo lệch đi vài giây so với block hiện tại (thông qua MEV), thì lastTimeRewardApplicable() - lastUpdateTime sẽ trở nên rất lớn (ví dụ 15 giây thay vì 1 giây). Kết hợp với totalSupply thấp (do kẻ tấn công đã unstake trước đó), rewardPerToken tăng vọt. Kẻ tấn công chỉ cần deposit một lượng nhỏ, lập tức withdraw và nhận phần thưởng tương ứng với khoảng thời gian ảo, sau đó lặp lại nhiều lần trong cùng một block. Vì updateReward được gọi trong cả deposit và withdraw, kẻ tấn công có thể tạo ra một vòng lặp nhỏ: deposit → withdraw → deposit → withdraw, mỗi lần nhân đôi phần thưởng. Kết quả là chỉ với 1 ETH deposit, sau 5 lần lặp, kẻ tấn công có thể rút toàn bộ quỹ phần thưởng 47 triệu USD.
Báo cáo audit mà tôi có trong tay (được công bố sau sự kiện) tiết lộ điều thú vị: auditor đã kiểm tra block.timestamp nhưng chỉ coi nó là rủi ro chênh lệch thời gian (sai số vài giây), không phải là vector tấn công có thể lặp lại. Họ đã bỏ qua khả năng totalSupply có thể bị giảm xuống 0 gần như ngay lập tức, và rewardRate không được giới hạn. Đây là một trade-off giữa hiệu suất và bảo mật: YieldFi đã chọn lưu rewardPerTokenStored dạng uint256 và không giới hạn số lần cập nhật trong một block, để tiết kiệm gas. Nhưng chính sự "tối ưu" này đã mở ra cánh cửa.
Contrarian
Điểm mù bảo mật ở đây là giả định tin cậy về block.timestamp đã được coi như chân lý trong nhiều năm. Hầu hết các lỗ hổng staking đều tập trung vào reentrancy hoặc oracle, nhưng ít ai nghĩ rằng validator có thể phối hợp để tạo ra một block với timestamp chênh lệch lớn. Trên thực tế, với MEV, các validator có thể sắp xếp giao dịch và chọn timestamp trong khoảng cho phép. Kẻ tấn công có thể trả phí gas cao để đảm bảo giao dịch của mình nằm ở vị trí chiến lược. Nếu bạn đọc kỹ whitepaper của YieldFi, họ có viết: "Phần thưởng được phân phối dựa trên thời gian trôi qua, đảm bảo công bằng cho tất cả người dùng." Nhưng họ không nói rằng thời gian đó có thể bị thao túng. Đây không phải là lỗi của hợp đồng thông minh, mà là lỗi của mô hình kinh tế học dựa trên giả định thời gian tuyến tính. Trong một hệ thống phi tập trung, thời gian là rời rạc và có thể bị ảnh hưởng bởi những người tham gia mạnh nhất.
Takeaway
Sự kiện YieldFi cho thấy một xu hướng đáng lo ngại: các giao thức DeFi đang tích hợp ngày càng nhiều tự động hóa (auto-compound, auto-restake) mà không tính đến khả năng thao túng thời gian. Nếu chúng ta nhìn vào merkle tree của các giao thức staking hàng đầu, hầu hết đều dùng block.timestamp trong công thức tính toán. Liệu các auditor có đang kiểm tra đủ sâu giả định này không? Hay chúng ta đang ngồi trên một quả bom hẹn giờ mang tên "thời gian blockchain"?