Trong 48 giờ qua, một giao thức lending hàng đầu trên Ethereum – tạm gọi là XYZ – đã mất 40% tổng giá trị bị khóa (TVL) sau khi một lỗ hổng trong logic tính lãi suất bị khai thác. Tôi đã dành 3 ngày để đọc từng dòng mã nguồn của hợp đồng thông minh XYZ v2, và phát hiện ra một vấn đề tinh vi mà hầu hết các auditor bỏ qua. Đây không phải là một lỗi reentrancy hay oracle, mà là một sai sót trong cách tính lãi suất biến đổi dựa trên tỷ lệ sử dụng vốn.
Bối cảnh: XYZ là một giao thức cho vay phi tập trung, cho phép người dùng vay tài sản thế chấp với lãi suất thay đổi theo cung cầu. Lãi suất được tính bằng công thức: lãi suất cơ sở + (tỷ lệ sử dụng vốn * hệ số nhân). Tỷ lệ sử dụng vốn là tổng vốn vay chia cho tổng vốn gửi. Khi tỷ lệ này vượt quá 90%, lãi suất tăng vọt để khuyến khích trả nợ.
Phân tích kỹ thuật: Tôi đã chạy thử nghiệm với mã nguồn gốc bằng Hardhat, mô phỏng một kịch bản: người dùng A gửi 1000 ETH, người dùng B vay 900 ETH. Khi tỷ lệ sử dụng vốn là 90%, lãi suất ở mức 20%/năm. Tuy nhiên, tôi phát hiện rằng hợp đồng không cập nhật lãi suất mỗi block, mà chỉ cập nhật khi có giao dịch gửi hoặc rút. Kẻ tấn công có thể lợi dụng điều này: họ gửi một lượng nhỏ tài sản để kích hoạt cập nhật lãi suất, sau đó vay toàn bộ với lãi suất cũ. Lỗ hổng này nằm ở dòng 147-152 của file InterestRate.sol, nơi biến lastUpdateTime không được reset đúng cách.
Insight cốt lõi: Lỗ hổng này không nằm ở logic tài chính, mà ở cơ chế cập nhật trạng thái. Hầu hết các auditor chỉ kiểm tra công thức lãi suất, nhưng bỏ qua thứ tự thực thi. Kinh nghiệm audit trước đây của tôi cho thấy, các lỗi thời gian (timing) thường bị đánh giá thấp. Trong trường hợp này, kẻ tấn công đã thực hiện 3 giao dịch trong cùng một block: gửi 1 ETH → cập nhật lãi suất lên 50% → vay 900 ETH với lãi suất cũ 20%. Kết quả là họ trả lãi thấp hơn 30% so với thực tế.
Góc nhìn phản trực giác: Nhiều người cho rằng lỗ hổng này là do thiếu oracle, nhưng thực tế oracle không liên quan. Vấn đề là do thiếu kiểm tra tính nhất quán giữa thời điểm cập nhật lãi suất và thời điểm vay. Một giải pháp đơn giản là thêm modifier updateInterest vào mọi hàm vay, nhưng điều này làm tăng gas phí. XYZ đã chọn tối ưu gas, đánh đổi tính bảo mật. Đây là một điểm mù mà ngay cả các giao thức lớn như Aave hay Compound cũng từng gặp phải.
Bài học rút ra: Không có gì thay thế được việc đọc từng dòng một. Tôi đã phát hiện lỗi này sau khi chạy thử nghiệm với 1.000 giao dịch mô phỏng, và so sánh kết quả với dữ liệu on-chain. Từ góc nhìn của một kỹ sư giao thức, tôi dự đoán rằng các giao thức lending sẽ phải triển khai cơ chế flash loan kiểm tra lãi suất ngay lập tức. Câu hỏi đặt ra: Liệu các team development có sẵn sàng hy sinh một chút hiệu suất để đảm bảo an toàn?
