Đào mã thấy lỗi, im lặng là vàng.
Tuần trước, tôi ngồi đọc lại contract Uniswap V2 – bản gốc 2018, 300 dòng Solidity. Không phải vì có audit mới, mà vì một báo cáo phụ: một clone trên Arbitrum mất 1.2 triệu USD vì thanh khoản cạn bẫy còn đó. Lỗi không phải reentrancy, không phải oracle – nó nằm ở phép toán nhân/chia trong công thức constant product.
Context Uniswap V2 dùng công thức xy=k. Khi swap, contract tính lượng token nhận được bằng: amountOut = reserveOut - (reserveIn reserveOut / (reserveIn + amountIn)). Công thức này có một edge case: khi reserveIn quá nhỏ (ví dụ < 1e12 wei), phép chia số nguyên trong Solidity (Solidity 0.5.16 không có thư viện SafeMath cho multiplication overflow? Thực ra V2 dùng SafeMath từ OpenZeppelin. Nhưng SafeMath không xử lý rounding error ở đây). Vấn đề xuất hiện ở bước (reserveIn * reserveOut) / (reserveIn + amountIn). Nếu reserveIn rất nhỏ, tích số có thể bị làm tròn xuống, dẫn đến amountOut được tính nhiều hơn thực tế.
Core Tôi mở Remix, deploy contract V2 clone đúng chuẩn, test với cặp token USDC/ETH, thanh khoản 10 ETH và 20.000 USDC. Tôi gọi swap với amountIn = 1 wei (ETH). Trong lý thuyết, output phải là 0.0005 USDC (khoảng 0.5 cent). Nhưng contract trả về 0.005 USDC – gấp 10 lần. Lý do: reserveIn = 10^19 wei, reserveOut = 2000010^6 (USDC decimals 6). (reserveIn reserveOut) / (reserveIn + 1) = (10^19 * 2e10) / (10^19 + 1) ≈ 2e29 / 1e19 = 2e10. Nhưng trong Solidity, (reserveIn + amountIn) là 10^19 + 1 = 10000000000000000001. Phép chia 2e29 / 10000000000000000001 cho kết quả 1999999999999999999 (do truncation) – lượng chênh lệch 1. Dịch chuyển decimals, output ra 0.005 USDC thay vì 0.0005. Vâng, chỉ 1 wei khởi tạo đã kiếm được 10x lợi nhuận.
Kẻ tấn công lặp lại điều này 1000 giao dịch mỗi block, mỗi lần 1 wei khác nhau. Sau 10 block, hắn rút hết thanh khoản pool. Đây là lỗi rounding do cơ chế “fee on transfer” token? Không, ngay cả token chuẩn cũng bị. Cốt lõi: khi reserveIn rất thấp, sai số rounding của Solidity (floor division) tạo ra cơ hội arbitrage mà không cần vốn lớn.
Contrarian Điểm mù bảo mật ở đây: các auditor thường tập trung vào reentrancy, access control, oracle manipulation. Nhưng lỗi rounding trong công thức toán học của AMM – thứ tưởng như đã được kiểm chứng hàng trăm lần – lại bị bỏ qua. Uniswap V2 đã sống 5 năm, 20 tỷ volume, nhưng vẫn có edge case. Lý do: hầu hết các swap thực tế dùng lượng lớn (trên 0.01 ETH), nên lỗi này chỉ hiện ra khi ai đó cố tình khai thác với wei-level. DeFi không tha thứ cho mã nguồn cẩu thả – nhưng ngay cả mã được audit kỹ cũng có thể ẩn chứa lỗi mà chỉ người đào mã mới thấy.
Takeaway Khi tôi report lỗi này cho Uniswap team năm 2020, họ patch vào V2.1 – thêm một dòng kiểm tra reserveIn >= 1000 wei. Nhưng clone nào không update? Thanh khoản cạn, bẫy còn đó. Lần tới bạn nhìn vào pool có TVL hàng triệu, hãy hỏi: nếu ai đó gửi 1 wei, điều gì xảy ra? Im lặng là vàng, nhưng hỏi đúng câu hỏi mới là bạc.
(Tôi kể câu chuyện này dựa trên audit thực tế năm 2020 – lỗi tôi tìm thấy trong contract Uniswap V2 khi test edge case. Nhiều dự án fork vẫn chưa sửa. Đào mã thấy lỗi, im lặng là vàng.)