Mở đầu: Một con số bất thường trong hàm mint
Trong lúc kiểm tra mã nguồn của dự án XYZ mới huy động 150 triệu USD chỉ trong 48 giờ, tôi gặp một dòng code khiến cảm giác quen thuộc của một auditor lập tức reo lên. Tại dòng 247 của file Token.sol, một phép so sánh dùng <= thay vì < trong vòng lặp phân phối token. Chỉ một dấu bằng thừa, nhưng với tôi, đó là dấu hiệu đầu tiên của một lỗ hổng nghiêm trọng có thể làm hao hụt hàng triệu USD. Không phải ngẫu nhiên mà tôi dành hai tháng liền chỉ để audit hợp đồng của OmiseGO hồi 2017 – từ đó, tôi học được rằng mã nguồn không biết nói dối, nhưng logic thì có thể.
Bối cảnh: Cơ chế giao thức và vòng đời của một cuộc audit
Dự án XYZ tự xưng là giao thức cho vay trên Layer 2 với lời hứa về lãi suất siêu thực 45% APY. Họ tuyên bố đã được hai công ty audit nổi tiếng kiểm tra, nhưng như tôi thường nói với các bạn trẻ trong workshop: audit không phải là giấy chứng nhận an toàn, nó chỉ là một bức ảnh chụp mã nguồn ở một thời điểm. Dự án sử dụng cơ chế mint token dựa trên vòng lặp while để phân bổ lãi cho người gửi. Nhưng thay vì giới hạn số lần lặp bằng một biến đếm chặt chẽ, họ dùng một mảng động với độ dài không được kiểm soát. Đây là một thiết kế có nguy cơ tràn gas hoặc tệ hơn, bị kẻ tấn công thao túng để rút hết thanh khoản.
Tôi đã nhìn thấy mô hình này từ thời DeFi Summer 2020, khi Uniswap V2 có lỗi logic trong cặp thanh khoản. Khi đó, tôi đã dành 3 tháng để vừa phân tích vừa dạy hơn 200 lập trình viên trong một workshop online cách phát hiện lỗi tương tự. Và hôm nay, với XYZ, tôi biết mình phải làm một bài audit khẩn cấp trước khi họ lên mainnet.
Phân tích kỹ thuật cốt lõi: Mã nguồn và những trade-offs ẩn
Hãy đi thẳng vào mã nguồn. Hàm distributeRewards() trong file RewardPool.sol sử dụng một vòng lặp while không có giới hạn tuyệt đối:
while (i < depositors.length) {
uint256 reward = _calculateReward(depositors[i]);
_transfer(reward, depositors[i]);
i++;
}
Có vẻ vô hại đúng không? Nhưng vấn đề nằm ở chỗ depositors.length có thể bị thao túng nếu có ai đó deposit liên tục với số lượng nhỏ trước khi hàm chạy. Trong môi trường cross-chain, kẻ tấn công có thể gửi hàng nghìn deposit trong một block thông qua Layer 2 sequencer. Nếu danh sách depositors dài tới 10.000, gas phí cho một lần gọi distributeRewards() sẽ vượt quá gas limit của block, làm cho giao dịch revert và toàn bộ phần thưởng bị đóng băng. Đó là một cuộc tấn công từ chối dịch vụ (DoS) tinh vi.
Họ biện minh rằng họ đã đặt modifier onlyOwner để kiểm soát ai được phép gọi hàm này. Nhưng như chúng ta biết, admin key có thể bị lộ hoặc bị khai thác qua các tấn công trong quá khứ (xem ví dụ về Ronin bridge). Một hệ thống dựa vào giả định "chúng tôi sẽ tin tưởng team" không bao giờ là an toàn trong crypto. Tôi đã thấy điều đó quá nhiều lần kể từ năm 2017.
Trong quá trình audit, tôi phát hiện thêm một điều kỳ lạ: hợp đồng RewardPool có một biến public lastUpdated nhưng không có bất kỳ cơ chế kiểm tra thời gian nào để tránh gọi hàm nhiều lần trong cùng một block. Điều này mở ra cơ hội cho tấn công flash loan kết hợp với DoS. Kẻ tấn công có thể mượn một lượng lớn token, deposit rồi withdraw liên tục để làm phì đại mảng deposits, sau đó kích hoạt hàm distributeRewards() và khiến nó fail do tràn gas. Hậu quả là những người dùng chân chính không thể nhận phần thưởng của họ.
Đối chiếu với Uniswap V2, lỗi của họ là ở cơ chế xử lý phí khi giá thay đổi đột ngột. Ở đây, lỗi hoàn toàn khác: nó đến từ việc thiếu một thiết kế phòng ngừa dựa trên kích cỡ mảng động. Một giải pháp đơn giản là thêm giới hạn cứng (hard cap) cho deposit list và sử dụng cơ chế batch processing. Nhưng team XYZ đã chọn trade-off: họ tin tưởng vào tốc độ của Layer 2 và sự trung thực của chính mình.
Góc nhìn trái ngược: Điểm mù bảo mật mà không ai muốn nói
Cộng đồng thường ca ngợi các dự án có lãi suất cao và tokenomics hấp dẫn. Nhưng theo kinh nghiệm của tôi, những dự án hứa APR trên 30% thường che giấu một lỗ hổng kỹ thuật nguy hiểm phía sau. Tại sao? Bởi vì họ có thể không cố tình lừa đảo, nhưng áp lực tăng trưởng và thời gian gấp rút khiến họ cắt giảm quy trình bảo mật. Tôi đã chứng kiến điều này trong thị trường tăng giá 2021: Bored Ape Yacht Club suýt sập vì lỗi random generation trong smart contract, nếu không có sự can thiệp kịp thời từ auditors.
Điểm mù lớn nhất mà tôi thấy trong XYZ là họ không public audit report đầu tiên. Khi tôi hỏi, họ nói "đây là phiên bản nội bộ". Đối với một dự án huy động 150 triệu USD, việc không công khai báo cáo audit là một dấu hiệu đỏ rực. Tôi không cần phải nói thêm.
Nhưng có một điều thú vị: lỗi <= thay vì < mà tôi thấy ở Token.sol thực ra là một bug riêng biệt, nó cho phép mint thêm một token so với giới hạn. Có vẻ vô hại, nhưng trong một hệ thống với hơn 1 triệu token đã mint, một token dư vẫn có thể được dùng để phá vỡ các cơ chế tính toán phần thưởng. Kẻ tấn công có thể tận dụng điều này để tạo ra một NFT đặc biệt (nếu dự án tích hợp NFT cho reward), hoặc dùng để chiếm quyền vote trong DAO. Các lỗi small cap thường bị bỏ qua, nhưng chúng là mảnh ghép cho các cuộc tấn công phức tạp hơn.
Kết luận: Dự báo lỗ hổng và lời kêu gọi hành động
Dựa trên 8 năm làm audit, tôi tin rằng XYZ sẽ gặp sự cố trong vòng 3 tháng đầu tiên nếu không sửa những lỗi trên. Nhưng thị trường tăng đang khiến nhiều người mù quáng. Một lần nữa, chúng ta thấy rằng công nghệ luôn đi sau marketing. Hãy hỏi: liệu bạn có muốn gửi tiền vào một giao thức mà ngay cả admin key cũng không được bảo vệ bởi multisig không? Tôi đã thấy câu trả lời từ lịch sử: cộng đồng sẽ quên khi bull chạy. Nhưng tôi vẫn viết, vì đó là trách nhiệm của người đã chứng kiến quá nhiều vụ sập.
Bài học: Hãy kiểm tra mã nguồn trước khi kiểm tra APY. Đừng để FOMO biến bạn thành nạn nhân của một lỗi chỉ có một dấu bằng.
Tags: #DeFiSecurity #SmartContractAudit #Layer2 #Reentrancy #DoSAttack #Vulnerability #BlockchainVietnam
Prompt cho hình minh họa: Một auditor nam trung niên với kính mắt, tay cầm kính lúp phóng to dòng code có dấu "<=>" lỗi, phía sau là màn hình xanh lấp lánh các dòng mã, xung quanh có các biểu đồ APY và logo dự án XYZ.