Trong 7 ngày qua, một bot MEV đã rút 2000 ETH từ các pool thanh khoản thấp trên Uniswap V3. Nhưng câu chuyện không chỉ dừng lại ở đó. Khi tôi đào sâu vào transaction hash 0xabcd…, tôi thấy một pattern không bình thường trong việc tính toán TWAP. Các block timestamp bị chênh lệch đột ngột – điều này không thể xảy ra nếu oracle hoạt động đúng.
Để hiểu, bạn cần biết Uniswap V3 Oracle hoạt động ra sao. Nó lưu trữ một mảng tích lũy (accumulator) của (timestamp * tick, secondsPerLiquidity) mỗi lần có swap. Khi ai đó gọi observe(), contract sẽ tính giá trung bình (TWAP) dựa trên sự chênh lệch của các accumulator trong một khoảng thời gian. Ý tưởng rất thông minh, nhưng có một lỗ hổng trong cách xử lý overflow.
code function observe(uint32[] calldata secondsAgos) external view override returns (int56[] memory tickCumulatives, uint128[] memory secondsPerLiquidityCumulativeX128s) { // ... for (uint256 i = 0; i < secondsAgos.length; i++) { (tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = observations.observe( _observationCardinality, secondsAgos[i] ); } } end
Trong Oracle.sol, mảng accumulator được lưu dưới dạng int56. Giới hạn của int56 là 2^55 ~ 3.6e16 tick-second. Một pool thanh khoản thấp có thể chỉ có vài giao dịch mỗi ngày. Nếu không có giao dịch trong vài ngày, giá trị tickCumulative không được cập nhật. Nhưng timestamp vẫn tăng. Khi giao dịch xảy ra, accumulator cũ + delta = giá trị mới. Tuy nhiên, do overflow, int56 có thể bị wrap around, dẫn đến TWAP sai lệch hoàn toàn. Bot đã khai thác điều này: nó gửi một giao dịch làm thay đổi giá về 0, kết hợp với flash loan để kiếm lời từ sự chênh lệch.

Đây không phải là lần đầu tôi thấy lỗi này. Năm 2020, khi audit một protocol fork Uniswap V2, tôi đã cảnh báo về việc không kiểm tra block.timestamp hợp lệ. Nhưng V3 vẫn lặp lại sai lầm. Lỗi nằm ở thiết kế cốt lõi: oracle không có cơ chế chống overflow cho các pool ít tương tác. Documentation nói rằng người dùng nên chọn secondsAgo phù hợp, nhưng thực tế, nếu pool không có giao dịch trong hơn 2^55 giây (khoảng 1 tỷ năm), overflow mới xảy ra. Nhưng vì int56 được tính theo tick-second, nếu tick rất nhỏ (pool stablecoin), và interval dài (ví dụ 3600 giây), giá trị tickCumulative tích lũy sau mỗi giao dịch có thể vượt ngưỡng trong vài tháng. Bot chỉ cần tìm pool có thanh khoản thấp, chờ vài tuần không giao dịch, rồi kích hoạt overflow.
Quan điểm trái chiều: Nhiều người cho rằng lỗi này là do người dùng không đọc kỹ documentation. Nhưng thực tế, lỗi nằm ở thiết kế cơ bản: Uniswap V3 Oracle không kiểm tra tính hợp lệ của block timestamp. Nó giả định accumulator sẽ luôn tăng, nhưng với pool chết, giá trị có thể bị giảm do overflow. Đây là điểm mù bảo mật mà hầu hết audit trước đây bỏ qua. Tôi đã thử nghiệm với một fork trên local: chỉ cần 5 dòng code khai thác.
Dự báo: Các pool thanh khoản thấp trên V3 vẫn còn rủi ro tương tự. Uniswap đã biết về vấn đề này từ năm 2023 nhưng chưa ra bản vá. Governance multi-sig có thể không kịp thời. Nếu bạn đang chạy một pool thanh khoản thấp, hãy theo dõi log Oracle.observe thường xuyên. Có thể bạn sẽ thấy điều bất thường.