Hook:
Sau Terra, tôi hiểu: không có “sự cố đơn lẻ” trong thế giới của những hợp đồng thông minh. Nhưng tuần trước, một giao thức lending trên Arbitrum đã hứng chịu 11 đêm khai thác liên tiếp. Mỗi đêm, một lỗ hổng khác nhau được kích hoạt. Kẻ tấn công không dùng reentrancy hay flash loan – họ dùng chính cơ chế lãi suất của giao thức để rút tài sản. Tôi đã kiểm tra bytecode ngay khi nghe tin.
Context: Giao thức X (giấu tên theo yêu cầu khách hàng) là một thị trường tiền tệ phi tập trung, cho phép người dùng vay và cho vay tài sản thế chấp. Nó sử dụng mô hình lãi suất dạng nhảy bậc (kink model) tương tự Compound, nhưng với một tham số bổ sung: phí thanh lý được tính dựa trên khối lượng giao dịch trong block đó. Thiết kế này nhằm khuyến khích thanh lý nhanh khi giá biến động. Tuy nhiên, chính tham số “real-time fee” này đã tạo ra một vòng phản hồi (feedback loop) cho phép kẻ tấn công thao túng phí thanh lý theo ý muốn.
Core: Đêm đầu tiên, tôi xác định vectơ tấn công. Hàm liquidate() trong mã nguồn không kiểm tra tính duy nhất của người gọi trong cùng một block. Nghĩa là: nếu A gọi liquidate() cho khoản vay của B, A có thể kiểm soát thứ tự giao dịch của mình trong block bằng cách đặt gas price cao. Kẻ tấn công đã làm như sau: đầu tiên, hắn tạo một khoản vay nhỏ với tài sản thế chấp là token C. Sau đó, hắn thao túng oracle giá (thông qua một pool thanh khoản mỏng) để giá token C giảm mạnh, đưa khoản vay vào trạng thái có thể thanh lý. Trong cùng block, hắn gọi liquidate() từ 3 hợp đồng khác nhau, mỗi lần gọi làm tăng phí thanh lý lên gấp đôi do công thức tính phí dựa trên số lần thanh lý trong block. Kết quả: hắn nhận được tài sản thế chấp với mức chiết khấu lên tới 40%, thay vì mức 5%-10% thông thường. Lỗ hổng nằm ở chỗ phí không được giới hạn trên (cap) và việc thanh lý nhiều lần trên cùng một vị thế vẫn hợp lệ.
Đêm thứ hai, hắn thay đổi chiến thuật: thay vì thao túng oracle, hắn lợi dụng việc giao thức cho phép vay bằng tài sản không tương thích (non-compatible) – một lỗi do bỏ sót trong whitelist. Hắn dùng token lạ (một meme coin) làm tài sản thế chấp, token này có thanh khoản rất thấp trên DEX. Khi giá giảm nhẹ, vị thế bị thanh lý, nhưng tài sản thế chấp là meme coin lại không thể bán được trên thị trường, dẫn đến việc thanh lý phải giữ lại một phần nợ xấu. Kẻ tấn công lặp lại điều này 9 lần, mỗi lần tạo ra một “bad debt” nhỏ, nhưng tổng cộng hắn đã rút được 2 triệu USD tài sản của giao thức trước khi bị phát hiện.
Tôi đã kiểm tra đối chiếu với các giao thức tương tự. Dựa trên kinh nghiệm audit của tôi, lỗi này xuất phát từ việc nhóm phát triển sao chép code của Aave V2 mà không thích ứng với cơ chế thanh lý động. Trong Aave, phí thanh lý được tính cố định dựa trên tỷ lệ nợ, không phải số lần thanh lý. Nhưng ở đây, họ muốn “cải tiến” để khuyến khích thanh lý nhanh, lại quên mất rằng kẻ tấn công có thể tận dụng chính tính năng “cải tiến” đó.
Contrarian: Giới bảo mật thường đổ lỗi cho oracle giá hay flash loan. Nhưng điểm mù thực sự ở đây là: thiết kế tham số động không có giới hạn. Trong thế giới của những hợp đồng thông minh, bất kỳ tham số nào thay đổi theo hành vi người dùng đều có thể bị thao túng nếu không có cơ chế kiểm soát “chống cùng một người gọi nhiều lần trong một block”. Điều trớ trêu: nhóm phát triển đã dành 3 tuần để audit với công ty bảo mật hàng đầu, nhưng vẫn bỏ sót. Tại sao? Vì auditor chỉ kiểm tra logic liquidate() riêng lẻ, không kiểm tra tính nhất quán giữa các lời gọi trong cùng một block. Đây là lỗi ở tầng giao thức, không phải hợp đồng.
Takeaway: Sau 11 đêm, giao thức mất 5 triệu USD. Họ đã vá lỗi bằng cách thêm mapping(address => uint256) để giới hạn số lần thanh lý mỗi block cho mỗi người. Nhưng tôi tự hỏi: liệu có bao nhiêu giao thức khác đang mang cùng một quả bom hẹn giờ? Khi bạn viết một hàm “cải tiến” mà không hiểu rõ ranh giới giữa động lực học và bảo mật, bạn đang mời kẻ tấn công vào nhà. Hãy nhìn vào bytecode trước khi tin vào whitepaper.