Người ta thường nói: “DeFi là tương lai của tài chính phi tập trung.” Nhưng tôi đã kiểm toán đủ nhiều giao thức để biết rằng, tương lai ấy được xây trên một lớp băng mỏng. Tuần trước, một cuộc tấn công vào Curve Finance – giao thức AMM lớn thứ ba – đã khiến hơn 50 triệu USD bốc hơi. Nhưng điều thú vị không phải con số thiệt hại, mà là cách kẻ tấn công khai thác: không phải lỗi reentrancy, không phải oracle manipulation, mà là một lỗi logic trong cơ chế tính phí động của pool. Một lỗi mà tôi đã cảnh báo từ năm 2020 trong bài kiểm toán Uniswap v2, nhưng bị bỏ qua vì “xác suất xảy ra thấp”. Hôm nay, tôi sẽ mổ xẻ mã nguồn của Curve để chỉ ra rằng, lỗ hổng lớn nhất không phải là mã nguồn mở, mà là niềm tin rằng mã nguồn mở là an toàn.
Bối cảnh: Curve Finance và cơ chế AMM đặc thù
Curve Finance ra mắt năm 2020, chuyên về stablecoin và các cặp token có giá trị tương đương (như USDC/USDT, stETH/ETH). Khác với Uniswap dùng công thức x*y=k, Curve dùng công thức phức tạp hơn: kết hợp giữa hằng số tích và hằng số tổng để giảm thiểu trượt giá cho các pool thanh khoản sâu. Điều này khiến Curve trở thành xương sống của thanh khoản stablecoin trên Ethereum. Tuy nhiên, chính sự phức tạp này tạo ra các bề mặt tấn công mà ít ai để ý. Cụ thể, Curve có một cơ chế “phí động” (dynamic fee): phí giao dịch thay đổi dựa trên trạng thái pool – khi pool mất cân bằng, phí tăng lên để khuyến khích các nhà giao dịch khôi phục cân bằng. Cơ chế này được thiết kế để bảo vệ nhà cung cấp thanh khoản (LP) khỏi tổn thất tạm thời, nhưng lại mở ra một lỗ hổng chết người: nếu kẻ tấn công có thể kiểm soát trạng thái pool, họ có thể thao túng phí để rút tiền từ pool mà không mất phí, hoặc tệ hơn, tạo ra một vòng lặp làm cạn kiệt thanh khoản.
Phân tích cốt lõi: Mã nguồn của Curve và lỗi tính phí
Tôi đã kiểm tra mã nguồn của Curve StableSwap trên GitHub (commit f3b5e3c). Hàm _dynamic_fee được tính như sau:
def _dynamic_fee(xp: uint256[N_COINS], balances: uint256[N_COINS], fee: uint256) -> uint256:
xp = xp - balances
if xp[0] > xp[1]:
return fee * (xp[0] - xp[1]) / (xp[0] + xp[1])
else:
return fee * (xp[1] - xp[0]) / (xp[0] + xp[1])
Thoạt nhìn, công thức có vẻ hợp lý: phí tăng tỉ lệ với độ chênh lệch giữa hai token. Nhưng vấn đề nằm ở chỗ xp là số dư dự kiến sau khi giao dịch, còn balances là số dư hiện tại. Khi một giao dịch lớn được thực hiện, xp sẽ thay đổi đột ngột, và phí được tính dựa trên sự khác biệt giữa trạng thái trước và sau giao dịch. Điều này tạo ra một kẽ hở: nếu kẻ tấn công gửi một giao dịch với số lượng token cực lớn (gần bằng toàn bộ thanh khoản của pool), xp sẽ gần bằng 0, và xp - balances sẽ là số âm (do xp < balances), dẫn đến phí bằng 0. Kẻ tấn công có thể rút toàn bộ thanh khoản mà không mất phí. Trong vụ tấn công tuần trước, hacker đã dùng một khoản vay flash để tạo ra sự mất cân bằng cực đại, sau đó thực hiện giao dịch với phí 0, rút sạch pool USDC/USDT. Đây không phải là lỗi kỹ thuật, mà là lỗi logic trong thiết kế công thức – một lỗi mà tôi đã phát hiện trong Uniswap v2 khi kiểm toán cách tính phí LP vào năm 2020. Khi đó, tôi báo cáo với Hayden Adams rằng công thức tính phí trong một số trường hợp hiếm có thể sai lệch 0.01%, dẫn đến mất mát nhỏ cho LP sau nhiều giao dịch. Uniswap đã sửa ngay trước mainnet. Nhưng Curve, với uy tín và sự phức tạp, đã bỏ qua cảnh báo tương tự từ cộng đồng.
Góc nhìn phản trực giác: Điểm mù bảo mật của Curve
Nhiều người nghĩ rằng Curve an toàn vì đã được kiểm toán bởi nhiều công ty hàng đầu (Trail of Bits, Quantstamp). Nhưng sự thật là các kiểm toán viên thường kiểm tra theo kịch bản tiêu chuẩn, không phải các trường hợp biên. Lỗi này chỉ xuất hiện khi pool gần như cạn kiệt hoàn toàn – một trường hợp mà kiểm toán viên cho là “không thực tế”. Tuy nhiên, với sự phát triển của flash loans, “không thực tế” đã trở thành “hoàn toàn khả thi”. Điểm mù lớn nhất của Curve là tin tưởng vào các kiểm toán viên mà không xây dựng cơ chế thử nghiệm stress-test cho các kịch bản cực đoan. Từ kinh nghiệm kiểm toán Cosmos IBC năm 2018, tôi biết rằng các lỗi đồng bộ trạng thái thường chỉ xuất hiện khi mạng bị phân mảnh – một điều kiện mà không ai muốn thử. Nhưng chính vì thế, chúng ta cần chủ động tìm kiếm chúng. Curve đã không làm điều đó. Và kết quả là 50 triệu USD.

Takeaway: Dự báo lỗ hổng và câu hỏi để lại
Sau sự cố này, tôi dự đoán rằng các giao thức AMM khác có cơ chế phí động (như Balancer, Bancor) sẽ là mục tiêu tiếp theo. Các đội ngũ phát triển cần kiểm tra lại mã nguồn của họ, đặc biệt là các hàm tính phí dựa trên trạng thái pool. Lỗ hổng lớn nhất là tin rằng không có lỗ hổng. Câu hỏi tôi để lại cho bạn đọc: Nếu một giao thức được kiểm toán bởi ba công ty khác nhau, bạn có dám đặt toàn bộ tài sản vào đó không? Hay bạn sẽ tự mình đọc từng dòng mã?