Lỗ hổng cổ điển trong giao thức mới: SolidLend và bài học 8 năm không ai nhớ
Phạm Hải
24 giờ qua, giao thức cho vay phi tập trung SolidLend đã mất 12 triệu USD trong một cuộc tấn công reentrancy. Số tiền không làm tôi ngạc nhiên — điều khiến tôi lạnh sống lưng là cách lỗ hổng này lặp lại y hệt một lỗi đã từng tồn tại trong hợp đồng ICO năm 2017. Tám năm, hàng ngàn dòng audit, hàng trăm bài học đắt giá, nhưng chúng ta vẫn đang dẫm lên vết xe đổ.
SolidLend ra mắt cách đây 4 tháng, huy động 30 triệu USD từ các quỹ đầu tư danh tiếng. TVL của nó đạt 200 triệu USD chỉ trong 3 tháng nhờ cơ chế cho vay linh hoạt và lãi suất hấp dẫn. Giao thức sử dụng oracle giá từ Uniswap v3 với một cơ chế chống thao túng giá độc đáo — hoặc họ nghĩ vậy. Hàm withdraw cho phép người dùng rút tài sản thế chấp dựa trên số dư được tính toán từ oracle. Nghe có vẻ quen, phải không?
Hãy nhìn vào đoạn mã giả mà tôi tái hiện từ hợp đồng của SolidLend sau khi sự cố xảy ra:
Điểm yếu nằm ở việc gọi callback onERC721Received sau khi chuyển token nhưng trước khi hoàn tất cập nhật trạng thái. Kẻ tấn công có thể triển khai một hợp đồng thông minh nhận callback, và trong callback đó gọi lại withdraw một lần nữa. Vì updateBalance đã được thực hiện, nhưng chưa cập nhật số dư oracle hoặc collateral, nên lần gọi thứ hai vẫn tính toán dựa trên trạng thái cũ. Lỗi này lặp lại chính xác cơ chế của vụ tấn công reentrancy vào DAO năm 2016 và TokenHub năm 2017.
Trong thực tế, kẻ tấn công đã thực hiện 15 lần gọi đệ quy trước khi giao thức phát hiện ra sự bất thường. Mỗi lần rút 800,000 USD, tổng cộng 12 triệu USD. Họ đã chuyển tiền qua Tornado Cash và biến mất. Giao thức đã dừng hoạt động và đang đàm phán với hacker — một kịch bản quen thuộc đến nhàm chán.
Tôi tìm kiếm những điểm mù mà người khác bỏ qua. Trong trường hợp này, điểm mù không phải là lỗi kỹ thuật. Đó là sự chủ quan của đội ngũ phát triển. Họ đã copy-paste code từ các giao thức cũ, nhưng bỏ qua lớp bảo vệ reentrancy guard. Họ cho rằng việc sử dụng oracle mới sẽ ngăn chặn mọi hình thức tấn công, nhưng quên rằng oracle không thể bảo vệ khỏi logic sai. Điều trớ trêu: chính tính năng "linh hoạt" của họ — cho phép gọi callback để tương tác với NFT — đã mở ra cánh cửa cho kẻ tấn công.
Mỗi mô hình mới đều mang theo vết nứt từ quá khứ. SolidLend là một giao thức cho vay hiện đại, với giao diện đẹp, tokenomics thông minh, và đội ngũ marketing xuất sắc. Nhưng vết nứt vẫn là vết nứt cũ. Tôi đã chứng kiến điều này nhiều lần: từ DeFi sang NFT, bản chất rủi ro vẫn không đổi. Các lỗ hổng không biến mất, chúng chỉ thay đổi hình thức. Reentrancy, flash loan attack, oracle manipulation — đó là những kẻ thù cũ mặc áo mới.
Từ góc nhìn của một người đã audit hơn 500 hợp đồng, tôi nhận thấy một xu hướng đáng báo động: các dự án mới thường tập trung vào tính năng để cạnh tranh, mà quên mất rằng bảo mật là yếu tố sống còn. Họ thuê các công ty audit nổi tiếng, nhưng audit chỉ là một bức ảnh chụp tại một thời điểm. Nếu code thay đổi sau audit, hoặc nếu auditor bỏ sót một dòng, hậu quả có thể thảm khốc. Trong trường hợp SolidLend, audit báo cáo không phát hiện lỗi reentrancy vì hàm withdraw có vẻ an toàn ở cái nhìn đầu tiên — họ chỉ kiểm tra các reentrancy cổ điển (gọi lại hàm withdraw trực tiếp), nhưng quên mất callback onERC721Received có thể gọi lại contrat thông qua một hàm khác.
Từ DeFi sang NFT, bản chất rủi ro vẫn không đổi. SolidLend tích hợp NFT làm tài sản thế chấp, một xu hướng mới. Nhưng việc sử dụng callback IERC721Receiver là một phần của chuẩn ERC-721, được tạo ra để đảm bảo an toàn cho người nhận NFT. Kẻ tấn công đã lợi dụng chính cơ chế bảo vệ đó. Đây là một bài học kinh điển: mỗi tính năng mới đều mang theo rủi ro tiềm ẩn. Các dev cần hiểu sâu về từng dòng code, không chỉ là cách nó hoạt động mà còn cách nó có thể bị lạm dụng.
Trong quá khứ, khi tôi phát hiện lỗ hổng trong hợp đồng ICO TokenHub năm 2017, chính lỗi reentrancy tương tự đã có thể khiến dự án mất 3,000 ETH. Lúc đó, tôi đã đề xuất sử dụng ReentrancyGuard từ OpenZeppelin, và họ đã vá. Nhưng sau 8 năm, nhiều dev mới không học từ lịch sử. Họ nghĩ rằng reentrancy đã là chuyện cũ, rằng các công cụ hiện đại sẽ tự động bảo vệ. Sai lầm.
SolidLend có thể sẽ sống sót sau cuộc tấn công này, nhưng niềm tin của người dùng đã bị tổn thương. TVL của họ đã giảm 40% chỉ trong 24 giờ. Các đối thủ cạnh tranh đang tận dụng cơ hội để quảng bá sự an toàn của mình. Nhưng liệu họ có thực sự an toàn? Tôi cá rằng trong 6 tháng tới, sẽ có ít nhất 3 giao thức khác bị tấn công theo cách tương tự. Các lỗ hổng không biến mất — chúng chỉ chờ thời cơ thích hợp để lộ diện.
Điểm mù lớn nhất trong bảo mật blockchain không phải là code, mà là tư duy của người phát triển. Họ quá tập trung vào việc xây dựng tính năng mới, vào việc thu hút người dùng, đến nỗi quên mất rằng mỗi dòng code họ viết ra đều có thể là điểm yếu chết người. Tôi đã thấy điều này nhiều lần: các dự án với đội ngũ phát triển tài năng, nhưng thiếu kinh nghiệm bảo mật. Họ nghĩ rằng audit là đủ, nhưng audit không thể thay thế cho văn hóa bảo mật trong toàn bộ quy trình phát triển.
Vậy chúng ta có thể làm gì? Trước hết, cần chuẩn hóa các quy trình kiểm tra bảo mật. Từ năm 2020, tôi đã phát triển một khung kiểm toán cho DeFi dựa trên các lỗi lịch sử. Khung này bao gồm kiểm tra tự động 20 loại hợp đồng phổ biến, tích hợp AI phát hiện mẫu lỗi từ 500 báo cáo trước. SolidLend đã không sử dụng khung này — họ chọn một công ty audit truyền thống. Kết quả là thảm họa.
Thứ hai, cần giáo dục cộng đồng về bảo mật. Người dùng cần hiểu rằng không có giao thức nào là an toàn tuyệt đối. Họ nên đa dạng hóa danh mục đầu tư và không đặt tất cả trứng vào một rổ. Các nhà phát triển cần học từ quá khứ, không chỉ từ sách vở mà từ những vụ tấn công thực tế. Tôi thường xuyên chia sẻ các case study trên Twitter, nhưng có vẻ chưa đủ.
Cuối cùng, cần có một cơ chế phản ứng nhanh khi sự cố xảy ra. SolidLend đã mất 12 triệu USD sau 15 phút, nhưng phải mất 2 giờ để dừng hợp đồng. Trong thời gian đó, kẻ tấn công đã rút sạch. Một hệ thống giám sát thời gian thực và khả năng tạm dừng khẩn cấp (circuit breaker) là cần thiết.
Tôi sẽ kết thúc bài viết này bằng một câu hỏi: Liệu cộng đồng blockchain có thực sự học được bài học từ quá khứ, hay chúng ta sẽ tiếp tục lặp lại những sai lầm cũ? Mỗi mô hình mới đều mang theo vết nứt từ quá khứ. Và những vết nứt đó, nếu không được hàn gắn, sẽ ngày càng lớn hơn theo thời gian.