Tôi mở terminal, clone repo Uniswap v4 từ GitHub về. Chạy thử test suite, 3 test fail. Không phải do code của tôi – là do một hook mà ai đó viết để kiếm MEV. Tôi cười. Đây chính xác là cái tôi sợ nhất.
Đầu năm 2024, Uniswap v4 chính thức lên mainnet với lời hứa: "Customizable liquidity pools through hooks". Nghe hay đấy. Nhưng tôi là Vũ Yến, 33 tuổi, đã audit hơn 2000 contract trong 8 năm qua. Tôi biết: khi bạn cho phép bất kỳ ai viết hook, bạn đang mở cánh cửa cho cả thiên tài lẫn thằng ngốc. Vấn đề là: ai phân biệt được hai đứa trước khi hook chạy?
Context
Uniswap v4 khác v3 ở điểm cốt lõi: nó cho phép các nhà phát triển đính kèm hook – những đoạn code thực thi trước/sau khi swap, thanh khoản thay đổi, hoặc phí tích lũy. Điều này biến mỗi pool thành một máy tính nhỏ có thể tùy chỉnh hành vi. Nghe giống như một bước tiến vĩ đại? Đúng vậy. Nhưng cũng giống như bạn cho mượn chìa khóa xe Ferrari để chở hàng – nếu tài xế không biết lái, cái xe sẽ đâm vào tường.
Trong mã nguồn của v4, hook được định nghĩa qua interface IHooks. Mỗi hook có thể override 8 callback khác nhau. Điều tôi thấy thú vị là cơ chế "flag" – hook không cần implement tất cả callback, chỉ cần set bit tương ứng. Gas tối ưu. Nhưng cũng đồng nghĩa: nếu hook set sai flag, pool có thể bị khóa vĩnh viễn. Tôi đã thấy lỗi này trong một audit thực tế.
Core
Tôi dành 3 ngày cuối tuần để đọc mã nguồn v4 của Uniswap. Cụ thể, tôi tập trung vào file PoolManager.sol và Hooks.sol. Điều đầu tiên tôi nhận ra: hook không có cơ chế cập nhật. Một khi triển khai, hook là bất biến. Điều đó có nghĩa: nếu hook có bug, pool đó chết. Không có bản vá. Tôi nhớ lại bài học từ năm 2017 – dự án TokenBridge mất 50 ETH vì oracle không được kiểm tra kỹ. Lỗi tương tự có thể xảy ra ở đây: hook gọi oracle giá bên ngoài mà không có fallback, dẫn đến reentrancy.
Tôi không build protocol, tôi chơi với code. Và code của v4 cho thấy một trade-off rõ ràng: linh hoạt đổi lấy bảo mật. Uniswap team đã cố gắng giảm thiểu rủi ro bằng cách giới hạn hook chỉ được gọi trong một số điều kiện nhất định (ví dụ: chỉ khi pool có thanh khoản). Nhưng trong thực tế, tôi thấy có hook có thể thao túng giá bằng cách thực thi lệnh swap nội bộ trước khi callback kết thúc. Đây là kiểu tấn công "sandwich" cấp độ giao thức.
Hãy nhìn vào một dòng code cụ thể trong PoolManager.sol: