Không có 'lỗi ngẫu nhiên' trong thế giới của những hợp đồng thông minh. Mọi vụ hack đều là kết quả của một chuỗi quyết định thiết kế sai lầm – và tôi đã nhìn thấy điều đó từ bên trong.
Hai tuần trước, giao thức lending ABC trên Arbitrum mất 20 triệu USD trong một cuộc tấn công flash loan. Tin tức tràn ngập Twitter: 'Lỗ hổng oracle', 'Tấn công chớp nhoáng', 'Mất tiền không thể cứu vãn'. Nhưng sau Terra, tôi hiểu: không có thứ gọi là 'rủi ro bất ngờ' trong thiết kế hệ thống tài chính phi tập trung. Chỉ có những lỗi logic được tích lũy qua các phiên bản, và những nhà phát triển chọn cách bỏ qua cảnh báo.
Tôi đã dành 72 giờ để đào ngược mã nguồn của ABC – từ phiên bản V1 đến V3. Kết quả cho thấy một câu chuyện khác xa những gì báo chí viết. Câu chuyện về một lỗ hổng tái nhập (reentrancy) tinh vi, được che giấu dưới lớp cơ chế 'an toàn' tích hợp sẵn. Và điều đáng sợ nhất: nó đã tồn tại trong mã nguồn suốt 8 tháng, qua ba lần kiểm toán từ các công ty uy tín.
Bối cảnh giao thức: Kiến trúc tưởng như hoàn hảo
ABC là một giao thức cho vay đa tài sản, sử dụng mô hình thanh khoản tập trung (concentrated liquidity) tương tự Uniswap V3, nhưng kết hợp với cơ chế lãi suất động. Điểm khác biệt chính: họ tích hợp một bộ oracle lai – kết hợp Chainlink với TWAP nội bộ – để định giá tài sản thế chấp. Về lý thuyết, thiết kế này giảm thiểu rủi ro thao túng giá. Nhưng trong thực tế, chính lớp 'bảo vệ kép' này lại tạo ra một mặt phẳng tấn công mới.

Theo kinh nghiệm kiểm toán của tôi, bất kỳ cơ chế nào có hai nguồn dữ liệu trở lên đều tiềm ẩn lỗ hổng do bất đồng bộ (asynchrony). Trong hợp đồng OracleAggregator của ABC, hàm getPrice() thực hiện so sánh giá từ Chainlink và TWAP, sau đó lấy giá trị trung bình có trọng số. Tuy nhiên, họ quên rằng TWAP của Uniswap V3 có thể bị thao túng trong cùng một block nếu lượng thanh khoản đủ mỏng. Kẻ tấn công chỉ cần một khoản vay flash 10 triệu USDC để tạo ra chênh lệch giá tạm thời 5% trong một block, đủ để kích hoạt oracle lai tính sai giá.
Phân tích mã nguồn: Lỗ hổng nằm ở đâu?
Tôi bắt đầu bằng cách kiểm tra hàm borrow() trong LendingPool.sol. Trong phiên bản V3.2, họ đã thêm một modifier nonReentrant – tưởng như an toàn. Nhưng khi đào sâu, tôi phát hiện: modifier này chỉ áp dụng cho các hàm bên ngoài (external), để ngỏ các hàm internal.
