Hook
Trong 72 giờ qua, một dự án tự xưng là "Tokenized Physical LNG" bỗng dưng xuất hiện như nấm sau mưa trên các group Telegram Việt Nam. Whitepaper của chúng khoe một cơ chế "Proof-of-Reserve" siêu việt, kết nối trực tiếp hợp đồng thông minh với bồn chứa khí hóa lỏng ở Mỹ. Nhưng tôi vừa mò vào mã nguồn của audit đầu tiên. Và tôi phát hiện một lỗi logic trong hàm withdraw() tưởng chừng như đơn giản. Một lỗi mà nếu không vá, bất kỳ ai cũng có thể rút sạch pool thanh khoản. ICO 2018: lỗi đa ký, bài học nhức nhối. Lại thêm một lần lịch sử lặp lại.
Context
Dự án này không phải là một shitcoin rác thông thường. Họ kể một câu chuyện nghe có vẻ hợp lý: giá khí đốt ở West Texas đang rẻ mạt vì thừa cung, trong khi giá LNG toàn cầu lại cao ngất ngưởng vì chiến tranh và nhu cầu châu Âu. Họ muốn token hóa quyền mua khí đốt giá rẻ đó, bán cho các nhà đầu tư nhỏ lẻ ở châu Á, hứa hẹn lợi nhuận chênh lệch. Cơ chế của chúng dựa trên một kho dự trữ ảo (Virtual Reserve) được cập nhật giá bằng oracle. Nghe có vẻ giống một quỹ ETF hàng hóa, nhưng trên chain. Vấn đề là: code không biết nói dối. EVM không tha thứ cho sự cẩu thả. Và cái audit đầu tiên của chúng, do một công ty tên "QuickAudit" (mà tôi chưa từng nghe tên), đã bỏ sót một attack vector kinh điển.
Core
Vấn đề nằm ở hàm claimReserveValue(). Hợp đồng cho phép người dùng gửi USDC vào, mint ra token rLNG đại diện cho quyền sở hữu một lượng khí nhất định. Khi muốn rút, họ đốt rLNG để lấy lại USDC, dựa trên giá oracle mới nhất. Quay trở lại vấn đề lưu ý: Họ dùng một biến toàn cục totalReserveValue để theo dõi tổng giá trị tài sản thế chấp. Mỗi lần có người withdraw(), hợp đồng trừ đi số USDC tương ứng từ biến totalReserveValue. Nhưng họ quên mất một thứ: cập nhật biến này trước khi chuyển token ra ngoài. Đây là lỗi reentrancy cổ điển nhất. Tôi đã viết một PoC (Proof of Concept) nhanh bằng Foundry. Kịch bản: kẻ tấn công gửi 1000 USDC, mint 1000 rLNG. Sau đó, hắn gọi withdraw() với lượng 1 rLNG. Hợp đồng bắt đầu thực thi: gọi oracle, tính giá, kiểm tra số dư. Nó thấy totalReserveValue vẫn còn đủ, và bắt đầu chuyển USDC cho hacker. Nhưng trước khi cập nhật totalReserveValue xuống còn 999, hacker đã gọi lại withdraw() từ contract của hắn. Lúc này, totalReserveValue vẫn là 1000, hợp đồng lại tưởng vẫn còn đủ tiền, và lại chuyển tiếp 1 USDC. Cứ lặp lại như vậy cho đến khi cạn pool. Một kỹ thuật cũ như trái đất. Dự án này claim là "đã được kiểm toán bởi công ty uy tín", nhưng cái audit của QuickAudit chỉ là một file PDF 5 trang, không hề có test coverage cụ thể. Họ không test case "reentrancy" vì nghĩ rằng dùng OZ ReentrancyGuard là đủ. Nhưng lỗi lại nằm ở chỗ họ dùng modifier nonReentrant cho hàm withdraw(), nhưng lại quên apply nó cho hàm fallback của contract attacker. Nếu attacker deploy một contract có hàm receive() gọi lại withdraw(), thì nonReentrant bị pass vì nó chỉ block cuộc gọi đầu, không block cuộc gọi lồng nhau từ bên ngoài. Lại một lỗi nữa, dùng cơ chế bảo vệ nhưng không hiểu cơ chế hoạt động của nó. Theo kinh nghiệm audit 5 năm của tôi, đây là một trong những lỗi gây chết người nhất. Nó không phải là lỗi của thuật toán phức tạp, mà là lỗi của sự cẩu thả cơ bản. Tôi đã từng thấy một dự án DeFi mất 12 triệu USD chỉ vì một lỗi y hệt như vậy vào năm 2021.
Contrarian
Nhưng nếu bạn nghĩ rằng đây chỉ là một scam project tầm thường, thì bạn đã bỏ lỡ điểm mù thú vị nhất của dự án này. Họ có một cơ chế "Dynamic Redemption Fee" trong cùng hàm withdraw(). Phí này được tính toán dựa trên mức độ chênh lệch giữa giá on-chain và giá off-chain. Nếu chênh lệch lớn, fee sẽ tăng vọt để chống lại arbitrage. Và chính cái fee này mới là chìa khóa. Nếu bạn exploit lỗi reentrancy, bạn không chỉ rút được USDC, mà còn kích hoạt tính toán lại cái fee này mỗi lần gọi lại. Logic tính toán fee nằm trước lệnh chuyển token và trước lệnh cập nhật totalReserveValue. Điều này có nghĩa là: mỗi lần gọi lại, fee được tính dựa trên cùng một giá oracle cũ, dẫn đến việc hacker có thể rút ra một lượng token nhiều hơn giá trị thực tế của chúng, vì fee không được cập nhật kịp. Thay vì chỉ rút 1 USDC, mỗi lần reentrancy, hacker rút ra 1 USDC trừ đi một khoản fee rất nhỏ (do giá chưa kịp thay đổi). Kết quả là anh ta có thể rút gần như toàn bộ pool chỉ với một lần deposit nhỏ. Góc nhìn của kẻ tấn công ở đây rõ ràng: không chỉ tấn công reentrancy, mà còn khai thác lỗ hổng trong thiết kế thuật toán fee. Đây là sự kết hợp của hai lỗi: lỗi logic reentrancy + lỗi thiết kế trạng thái không nhất quán. Không một công ty audit nào tử tế bỏ sót cả hai.
Takeaway
Câu chuyện này là một lời cảnh báo rõ ràng cho mùa altcoin sắp tới. Làn sóng "Real World Asset" (RWA) đang tràn vào blockchain Việt Nam. Các dự án kể những câu chuyện về dầu mỏ, khí đốt, bất động sản. Nhưng mã nguồn của chúng vẫn chỉ là những hợp đồng Solidity cơ bản, thậm chí còn yếu kém hơn cả những dự án DeFi từ năm 2020. Chúng ta đang bước vào một chu kỳ mà sự phức tạp của câu chuyện marketing tỷ lệ nghịch với chất lượng của code. Nếu không có một đội ngũ audit thực thụ, và bản thân mỗi nhà đầu tư không tự mình đọc mã nguồn, thì những token "khí đốt" này sẽ nhanh chóng trở thành những quả bom nổ chậm. Liệu bạn có dám nhấp vào link "mint" khi chưa hiểu hết dòng code đầu tiên? Liệu bạn có tin vào một "QuickAudit" chỉ vì nó rẻ?