Theo phân tích từ công ty kiểm toán Certik, bằng cách tận dụng sự thiếu hụt thanh khoản ở các phạm vi “tick” rỗng trên một số pool của KyberSwap, tin tặc đã khai thác thành công qua việc hoán đổi chéo để lấy đi số tiền lớn trên các pool chứa ít thanh khoản này.
Vào ngày 22/11, KyberNetwork đã bị tấn công cho vay nhanh (flashloan) trên nhiều chuỗi, dẫn đến thiệt hại khoảng 47 triệu USD. Theo Certik, để hiểu rõ lỗ hổng này, cần hiểu cách thức hoạt động của những nhà cung cấp thanh khoản tập trung.
Các nhà cung cấp thanh khoản tự động cơ bản triển khai một đường cong sản phẩm không đổi (x*y=k), nơi tất cả các giao dịch diễn ra.
Certik phát hiện, thanh khoản có thể được tận dụng tốt hơn nếu các pool được tạo ra với đường cong giá có dải giá hẹp hơn. Việc thêm thanh khoản hỗ trợ trong phạm vi giá hẹp cũng giúp giảm rủi ro trượt giá, vì kích thước giao dịch sẽ phải tăng lên theo tỷ lệ của pool mới để có cùng tác động giá.
Với mô hình nhà cung cấp thanh khoản tập trung, người dùng có thể thêm thanh khoản vào phạm vi giá mà họ muốn. Certik chỉ ra, do thiết kế này, mỗi vị trí cung cấp thanh khoản phải được theo dõi riêng lẻ vì thanh khoản trong pool trở nên không tương đồng. Các khoảng giá có thể chia thành các “tick” rời rạc, cho phép người dùng có thể đóng góp thanh khoản giữa hai “tick” bất kỳ. Chỉ số của một “tick”, được ký hiệu là i, được định nghĩa là logarithm của giá tương ứng.
Tổng thanh khoản của nhà cung cấp thanh khoản và các tham số khác được lưu trữ trong một cấu trúc danh sách liên kết.
Mỗi vị trí này về cơ bản tạo thành đường cong giá do người dùng tự xác định. Bằng cách tổng hợp tất cả các vị trí khác nhau thành một đường cong giá duy nhất, nó cho phép một pool duy nhất hỗ trợ đa dạng các yêu cầu của người cung cấp thanh khoản.
Khi thêm hoặc loại bỏ thanh khoản vào một phạm vi “tick”, pool thanh khoản phải ghi lại lượng thanh khoản ảo được thêm vào hoặc loại bỏ chúng khi vượt qua những “tick” này, và có bao nhiêu token được sử dụng để kích hoạt thanh khoản trong phạm vi đó. Hoán đổi token 0 để lấy token 1 khiến giá pool hiện tại và “tick” hiện tại di chuyển xuống, trong khi hoán đổi token 1 để lấy token 0 khiến giá pool hiện tại và “tick” hiện tại di chuyển lên. Khi hoán đổi, tổng lượng thanh khoản được thêm vào hoặc loại bỏ khi “tick” vượt qua phạm vi này để di chuyển về trái hoặc phải, và đây là lỗ hổng trong KyberSwap, nơi bị tin tặc khai thác.
Nhược điểm
Nói một cách đơn giản, lỗ hổng này nằm trong quá trình triển khai hàm computeSwapStep() của KyberSwap Elastic. Hàm này chịu trách nhiệm tính toán số tiền đầu vào và đầu ra thực tế của giao dịch sẽ cần trừ hay thêm, phí hoán đổi sẽ được thu thập và giá trị căn bậc hai kết quả là hàm sqrtP.
Hàm được gọi trước tiên là calcReachAmount(), và kết luận rằng giao dịch của tin tặc sẽ không vượt qua phạm vi “tick”, nhưng sai lầm xảy ra khi tạo ra một giá trị ảo lớn hơn so với targetSqrtP, được tính toán bằng cách gọi hàm “calcFinalPrice”. Kết quả là thanh khoản không được loại bỏ và dẫn đến cuộc tấn công tiếp theo.
Quá trình tấn công
Ví dụ này dựa trên giao dịch Ethereum có txhash là: 0x396a83df7361519416a6dc960d394e689dd0f158095cbc6a6c387640716f5475.
Giao dịch này đại diện cho sáu cuộc tấn công đều sử dụng cùng một phương pháp. Vì vậy, Certik đã lấy cuộc tấn công vào cặp USDC-ETHx làm ví dụ.
1. Đầu tiên, tin tặc sử dụng flashloan vay 500 ETHx từ Uniswap và thao túng pool KS2-RT ((KyberSwap v2 Reinvestment Token), vốn chỉ chứa 2,8 ETHx, bằng cách hoán đổi một lượng ETHx quá mức. Tin tặc đã đổi 246,754 ETHx lấy 32389,63 USDC, làm cạn thanh khoản của pool và đẩy currentTick tăng lên 305,000.
Sau khi hoán đổi, còn lại 249,5 ETHX và 13,2 USDC trong pool.
Có thể thấy, tin tặc ban đầu muốn đổi 500 ETHx lấy USDC, nhưng 246,754 ETHx là đủ để có được 32.389,63 USDC và làm tăng currentTick lên 305,000. Có nghĩa là không có thanh khoản nào khả dụng trên phạm vi “tick” 305,000 để tin tặc hoán đổi vào thời điểm đó, nó được coi là vùng chân không.
2. Sau đó, tin tặc gọi hàm mint() từ hợp đồng KyberSwap: Elastic Anti-Snippingposition Manager, để tạo ra một pool thanh khoản mới với 16 USDC và 5,87e-3 ETHx. “Tick” được đặt trong phạm vi hẹp từ 305,000 đến 305,408, có nghĩa là tin tặc đã tạo pool thanh khoản riêng để theo dõi “tick” ở mức 305,000.
Sau đó, tin tặc đã gỡ một phần thanh khoản, nhưng vẫn để lại một phần thanh khoản trong biên độ “tick” từ 305,000 đến 305,408.
3. Tin tặc tiến hành hoán đổi lần hai từ ETHx sang USDC. Chúng đã đổi 244.08 ETHx ở biên độ “tick” là 305,000 để nhận 13.6 USDC và làm tăng biên độ “tick” lên 305,408.
Nhìn bề ngoài, đây có vẻ là một giao dịch hoán đổi kỳ lạ vì tin tặc là kẻ duy nhất cung cấp thanh khoản trong khoảng từ 305,000 đến 305,408, nhưng đó là tiền đề cho bước tiếp theo.
4. KyberSwap đã sử dụng hàm computeSwapStep() để xác định xem giao dịch có vượt một khoảng “tick” hay không. Một phần của hàm được hiển thị dưới đây:
Ở đây, tập trung vào hàm calcReachAmount(). Hàm này tính toán số lượng token cần thiết cho một giao dịch nếu currentSqrtP (giá căn bậc hai hiện tại) đạt đến targetSqrtP.
Cerik nhận định, trong giao dịch này, giá trị của usedAmount là 244080034447360000000, trong khi lượng ETHx mà tin tặc nhập vào để đổi là 244080034447359999999, ít hơn một so với giá trị của usedAmount. Hàm computeSwapStep() xác định rằng giao dịch này không đủ để làm cạn thanh khoản trong biên độ “tick” hiện tại, và không cần phải đổi qua phạm vi “tick” khác. Có nghĩa là nextSqrtP không được cập nhật thành targetSqrtP.
Hàm calcFinalPrice sau đó được gọi để tính toán giá trị nextSqrtP tiếp theo. Phần quan trọng ở đây là trong quá trình tính toán, phí hoán đổi được tính vào trong thanh khoản. Dẫn đến việc giá cuối cùng của nextSqrtP thực tế cao hơn dự kiến kiến ban đầu, cụ thể là cao hơn giá tại phạm vi “tick” 305,408.
Quay lại hàm hoán đổi ở trên, Certik cho biết giá trị của sqrtP và nextSqrtP được so sánh để xác định xem có cần phải đổi phạm vi “tick” hay không. Điều kiện ở đây chỉ xác định xem sqrtP có ‘bằng’ nextSqrtP hay không, và chỉ khi nó bằng nhau, hàm thấp hơn là updateLiquidityAndCrossTick() mới được gọi để thêm hoặc bớt thanh khoản (swapData.baseL) và vượt qua biên độ “tick”.
Tại điểm này, sqrtP đang lớn hơn nextSqrtP. Có nghĩa tin tặc đã tạo ra tình huống trong đó giá hiện tại đã vượt qua giới hạn trên của khoảng “tick”, nhưng không kích hoạt hàm updateLiquidityAndCrossTick() để bớt thanh khoản, dẫn đến sự tồn tại của thanh khoản giả mạo.
5. Cuối cùng, tin tặc thực hiện một giao dịch trả ngược, đổi USDC thành ETHx, làm giảm giá một chút trên phạm vi “tick” 305,408 – giới hạn trên của phạm vi thanh khoản do tin tặc cung cấp (từ 305,000 đến 305,408) xuống một chút dưới giá ở phạm vi “tick” 304,982.
Khi giá rơi vào phạm vi “tick” từ 305,000 – 305,408, nơi tin tặc cung cấp thanh khoản, hàm updateLiquidityAndCrossTick() được gọi khi giá cao hơn một chút so với “tick” 305,408. Thanh khoản giả mạo được thêm vào nằm trong phạm vi “tick” 305,000 – 305,408, làm tăng giá trị thanh khoản trong phạm vi này so với thanh khoản thực tế. Tin tặc sau đó đổi 493,638 ETHx thành 27,517 USDC trong khoảng giá này (bao gồm khoảng 250 ETHx không được tính trong phạm vi “tick” 305,000 – 305,408). Kết quả là các chi phí trước đó đã được khôi phục và đồng thời pool thanh khoản bị rút toàn bộ USDC, Certik kết luận.
6. Trả lại khoản vay nhanh và hoàn thành cuộc tấn công.
Quá trình này được lặp lại đối với nhiều cặp giao dịch trên giao thức KyberSwap ở nhiều blockchain khác nhau, dẫn đến những tổn thất sau đây.
POLY: 1.180.097 USD; ETH: 7.486.868 USD; OP: 15.504.542 USD; BASE: 318.413 USD; ARB: 16.833.861USD; AVAX: 23.526 USD.
Các vụ tấn công được Certik xác nhận đến từ ba địa chỉ Ethereum (EOAs) khác nhau, trong đó địa chỉ 0x502 chiếm phần lớn tài sản. Ban đầu, địa chỉ 0x502 đã liên hệ với dự án để thông báo đàm phán sau một khoảng thời gian nghỉ ngơi. Đội ngũ KyberSwap sau đó cũng liên hệ để đề xuất một phần thưởng 10% với hạn chót là 6 giờ sáng UTC ngày 25/11.