Uniswap v4 sử dụng Hook để thực hiện thanh khoản có thể lập trình, nhưng do các lỗ hổng về mã hóa quyền hạn, kiểm soát truy cập, v.v., trở thành mục tiêu tấn công phổ biến, cần cảnh giác với rủi ro lạm dụng Hook Async và logic kế toán.Tác giả bài viết, nguồn: Beosin
Sau khi Uniswap v4 chính thức ra mắt trên mainnet, cơ chế Hook đã trở thành một trong những sáng tạo được quan tâm nhất trong DeFi. Nền tảng phát hành memecoin trên chuỗi Base, Flaunch, sử dụng Hook để triển khai giá bán trước cố định và cơ chế thanh lý tự động; giao thức thanh khoản Bunni v2 dùng Hook để xây dựng mô hình thanh khoản có thể lập trình và tái thế chấp; trong năm nay, các token dựa trên Hook như SATO, uPEG (Unipeg), Slonks cũng đã tăng giá hàng chục lần trong thời gian ngắn.
Bên cạnh sự phát triển sôi động của hệ sinh thái Hook, các cuộc tấn công khai thác lỗ hổng trong triển khai Hook cũng đang gia tăng đáng kể. Bài viết này sẽ bắt đầu từ cơ chế Hook của Uniswap v4, phân tích từng bước stack gọi lõi, giúp các dự án hiểu rõ các lỗ hổng tiềm ẩn trong đó.
Bảo mật Uniswap v4 Hook
1. Giới thiệu
Sự thay đổi kiến trúc nổi bật nhất của Uniswap v4 so với v3 là việc giới thiệu cơ chế Hook: cho phép các nhà phát triển gắn các hợp đồng tùy chỉnh vào các sự kiện trong vòng đời của hồ sơ thanh khoản, để chèn bất kỳ logic nào tại các điểm như swap, thêm/bớt thanh khoản, khởi tạo, v.v.
Các thay đổi chính của v4 như sau:
Mô hình Singleton: Trạng thái của tất cả các pool được quản lý tập trung bởi hợp đồng PoolManager duy nhất, thay vì triển khai hợp đồng riêng biệt cho từng pool
- Kế toán tức thời: Các thay đổi số dư trung gian trong quá trình giao dịch chỉ được ghi nhận trong bộ nhớ tạm thời và được kết toán tập trung khi giao dịch kết thúc.
Cơ chế Hook: Mỗi hồ sơ có thể liên kết với một hợp đồng Hook, PoolManager sẽ gọi lại hợp đồng này tại các điểm quan trọng (beforeInitialize, beforeSwap, afterAddLiquidity, v.v.)
- Hook không thể thay đổi: Sau khi pool được khởi tạo, địa chỉ Hook được liên kết sẽ cố định vĩnh viễn (địa chỉ Hook được liên kết với pool không thể được sửa đổi, nhưng việc nâng cấp chính hợp đồng Hook phụ thuộc vào cách triển khai của nó)
Trong giai đoạn v3, các nhà phát triển chỉ cần tin tưởng vào chính giao thức Uniswap; trong giai đoạn v4, tính bảo mật của mỗi hồ chứa phụ thuộc vào Hook được liên kết với nó. Hook đã mở rộng AMM từ một nguyên tố tài chính cố định thành một cơ sở hạ tầng tài chính có thể lập trình, nhưng mô hình bảo mật đã bị phân mảnh từ “cấp giao thức” sang “cấp hồ chứa”.
2. Kiến trúc Hook 2.1 Mô hình PoolManager và unlock/callback
Hợp đồng cốt lõi của v4 là PoolManager duy nhất. Mọi thao tác thay đổi trạng thái hồ sơ (hoán đổi, thêm/bớt thanh khoản) đều phải gọi trước PoolManager.unlock() để nhận quyền gọi lại một lần, sau đó thực hiện các hành động cụ thể trong unlockCallback(). Khi toàn bộ quy trình kết thúc, PoolManager sẽ xác minh xem sổ sách có cân bằng không:
Khi NonzeroDeltaCount != 0, hãy revert ngay lập tức; đây là ràng buộc cốt lõi của flash accounting v4. Bất kỳ Hook nào trong quá trình thực thi có thể tạm thời gây mất cân bằng sổ sách, nhưng phải tự cân bằng trước khi kết thúc giao dịch, nếu không toàn bộ giao dịch sẽ bị hoàn lại.
Mỗi hồ được xác định duy nhất bởi cấu trúc PoolKey, bao gồm trường hooks:
PoolId được tính bằng keccak256(PoolKey), do đó các địa chỉ hooks khác nhau sẽ tạo ra các pool khác nhau. Điều này cũng có nghĩa là PoolManager sẽ không kiểm tra xem một địa chỉ Hook nào đó đã từng được sử dụng cho pool khác hay chưa, cùng một hợp đồng Hook có thể được liên kết đồng thời với nhiều pool.
2.2 Mã hóa quyền hạn Hook trong địa chỉ
Một thiết kế không trực quan trong v4 là: quyền của Hook không được xác định bởi một biến nào đó trong hợp đồng, mà được xác định bởi địa chỉ triển khai của hợp đồng Hook.
PoolManager kiểm tra 14 bit thấp nhất của địa chỉ Hook để xác định xem Hook đó có cần được gọi tại một điểm nào đó trong chu kỳ sống hay không:
Ví dụ: BEFORE_SWAP_FLAG = 1 << 7. Nếu bit thứ 7 của địa chỉ Hook là 1, PoolManager sẽ gọi beforeSwap() của Hook trước khi swap; nếu không, ngay cả khi hợp đồng Hook đã triển khai beforeSwap(), nó cũng sẽ không bao giờ được PoolManager gọi.
Điều này có nghĩa là khi triển khai Hook, địa chỉ phải được tính toán thông qua CREATE2 + salt để tạo ra một địa chỉ có phần thấp trùng hoàn toàn với quyền mục tiêu. Uniswap cung cấp công cụ HookMiner để thực hiện mục đích này:
Khi các bit quyền không nhất quán với việc triển khai hàm, sẽ phát sinh hai loại vấn đề:
(1) Đã triển khai một hàm hook, nhưng địa chỉ không được mã hóa để khớp với bit quyền — PoolManager sẽ không bao giờ gọi hàm này, logic trở nên vô nghĩa
(2) Địa chỉ được mã hóa với một bit quyền, nhưng hook không triển khai hàm tương ứng — PoolManager có thể bị revert khi gọi lại, dẫn đến thực hiện DOS hoặc kiểm tra giá trị trả về thất bại, khiến các thao tác liên quan không thể thực hiện.
Đây cũng là rào cản tự nhiên đối với việc nâng cấp Hook: nếu Hook có thể được nâng cấp thông qua proxy, địa chỉ triển khai sẽ không thay đổi trong quá trình nâng cấp, do đó sau khi nâng cấp, chỉ có thể sửa đổi triển khai của các hàm Hook hiện có, chứ không thể thêm loại Hook mới. Để dự phòng khả năng mở rộng trong tương lai, phải预先 đào sẵn tất cả các bit quyền có thể sử dụng trong lần triển khai ban đầu.
2.3 BaseHook và một bẫy kiểm soát truy cập thường bị bỏ qua
Hợp đồng trừu tượng BaseHook do phần mở rộng Uniswap v4 phiên bản cũ cung cấp, các nhà phát triển có thể kế thừa nó để triển khai Hook tùy chỉnh. Một vai trò quan trọng của BaseHook là cung cấp bộ sửa đổi onlyPoolManager cho hàm unlockCallback():
Tuy nhiên—ở đây tồn tại một bẫy thiết kế rất dễ bị bỏ qua—phiên bản đầu tiên của BaseHook chỉ thêm onlyPoolManager cho unlockCallback, không có bất kỳ bảo vệ nào đối với các hàm callback hook khác (beforeSwap, afterSwap, beforeAddLiquidity, v.v.). Kiểm soát truy cập cho các hàm này phải được nhà phát triển Hook thêm một cách rõ ràng.
3. Đọc mã vòng đời của Hook
Ví dụ với một giao dịch exact-input swap, dưới đây là phân tích toàn bộ stack gọi từ khi người dùng khởi tạo giao dịch đến khi kết thúc.
3.1 Khởi tạo pool và liên kết Hook
Bất kỳ ai cũng có thể gọi PoolManager.initialize() để tạo hồ sơ mới:
isValidHookAddress chỉ kiểm tra tính tương thích giữa bit quyền của địa chỉ và trường fee, không kiểm tra xem Hook đã được sử dụng trong pool khác hay chưa, cũng không kiểm tra xem Hook có “sẵn sàng” chấp nhận PoolKey này hay không. Nếu khi thiết kế Hook, không có logic danh sách trắng hoặc ràng buộc một pool trong beforeInitialize, bất kỳ ai cũng có thể tạo một pool mới sử dụng cùng Hook nhưng với cặp token tùy ý và kích hoạt tất cả các callback tiếp theo của Hook.
3.2 beforeSwap và BeforeSwapDelta
Điểm vào quy trình swap là PoolManager.swap(), trước khi thực hiện logic swap cốt lõi, nó sẽ gọi Hooks.beforeSwap():
Giá trị trả về của beforeSwap là một bộ ba (bytes4, BeforeSwapDelta, uint24):
- bytes4: phải bằng IHooks.beforeSwap.selector, nếu không PoolManager sẽ revert ngay
- BeforeSwapDelta: Hook điều chỉnh delta cho token được chỉ định và token không được chỉ định trong giao dịch swap này
- uint24: Giá trị bao phủ tỷ lệ phí LP động (chỉ có hiệu lực khi池子 kích hoạt tỷ lệ phí động)
BeforeSwapDelta là tên gọi khác của int256, 128 bit cao là delta của token được chỉ định (tức là token mà người dùng chỉ định số lượng), 128 bit thấp là delta của token không được chỉ định:
Cần lưu ý rằng ngữ nghĩa của BeforeSwapDelta là Hook nên trả về giá trị dương khi thu phí và trả về giá trị âm khi hoàn lại token. Các nhà phát triển dễ nhầm lẫn dấu; đồng thời, mối quan hệ tương ứng giữa specified và unspecified phụ thuộc vào dấu của params.zeroForOne và amountSpecified, cách viết sai chút ít sẽ dẫn đến sai lệch token.
PoolManager sẽ cộng trực tiếp specifiedDelta trả về từ beforeSwap vào amountToSwap:
Dòng này ẩn chứa một ý nghĩa then chốt: Hook có thể giữ lại số lượng swap. Khi hookDeltaSpecified bằng -params.amountSpecified, amountToSwap sẽ trực tiếp về không, tương đương với việc Hook hoàn toàn kiểm soát giao dịch swap này—đó chính là所谓的 Async Hook hoặc Custom Curve Hook.
Async Hook là một kiểu mẫu thiết kế nguy hiểm nhất trong v4: về bản chất, nó thay thế logic hoán đổi của Uniswap bằng logic riêng của Hook. Nếu Hook chứa lỗ hổng hoặc vốn dĩ là độc hại, vốn của người dùng sẽ không còn được bảo vệ bởi logic định giá bản địa của Uniswap, mà thay vào đó phụ thuộc hoàn toàn vào tính chính xác của chính Hook.
3.3 Delta thanh toán và NonzeroDeltaCount
delta trả về bởi beforeSwap và afterSwap sẽ không kích hoạt chuyển khoản ngay lập tức, mà được ghi lại vào sổ cái nội bộ của PoolManager:
Mỗi khi delta tích lũy của một token thay đổi từ zero sang khác zero, NonzeroDeltaCount sẽ tăng lên; khi trở về zero, nó sẽ giảm xuống. Như đã nêu ở mục 2.1, khi unlock() kết thúc, nếu NonzeroDeltaCount != 0, toàn bộ giao dịch sẽ bị revert.
Hook cân bằng delta của mình thông qua hai hành động: settle() (chuyển tiền đến PoolManager) và take() (rút tiền từ PoolManager):
Cơ chế này mang lại ngữ nghĩa bảo mật rõ ràng: mọi người cuối cùng đều phải cân bằng tài khoản. Tuy nhiên, nó chỉ đảm bảo “bảo toàn tài khoản”, chứ không đảm bảo “tài khoản chính xác”. Nếu Hook trả về một delta bị tạo ra một cách độc hại trong beforeSwap, PoolManager sẽ trung thành ghi chép theo delta đó, miễn là cuối cùng nó được thanh toán cân bằng, giao dịch sẽ thành công — ngay cả khi điều này có nghĩa là Hook có thể giả mạo trạng thái nghiệp vụ, khiến hệ thống hiểu nhầm rằng kẻ tấn công sở hữu một số quyền lợi tài sản, trong khi PoolManager không thể phát hiện ra những lỗi nghiệp vụ này.
Sự cố bảo mật trước đây của Cork Protocol là do lỗ hổng trong Hook của nó, và trước khi bị tấn công, nó đã được bốn công ty kiểm toán đánh giá. Sau sự cố, chúng tôi nhận thấy:
Trong bốn cuộc kiểm toán, có ba cuộc không bao gồm hợp đồng CorkHook
- Chỉ có một cuộc kiểm toán duy nhất đối với CorkHook, phát hiện một số vấn đề về mã và đề xuất cải tiến, nhưng chưa bao quát đầy đủ các vấn đề về kiểm soát truy cập
Một đơn vị kiểm toán khác đã đề xuất rõ ràng trong báo cáo của mình: “một cuộc hợp tác theo dõi thú vị sẽ là chứng minh các bất biến cho các hàm CorkHook được gọi bởi các thành phần khác nhau đã được xác minh trong phạm vi cuộc hợp tác này”. Đề xuất này mang tính mục tiêu cao khi xem xét lại sau sự việc.
Điều này phơi bày một lỗ hổng kiểm toán mới trong thời đại v4 Hook: sự gia tăng bùng nổ về độ phức tạp của giao thức khiến việc xác định phạm vi kiểm toán trở thành một quyết định bảo mật. Chuỗi tương tác giữa Hook và các hợp đồng khác của giao thức rất dài; việc chỉ kiểm toán riêng hợp đồng Hook không đủ để phát hiện các vấn đề tổ hợp giữa các hợp đồng; ngược lại, nếu kiểm toán các hợp đồng xung quanh mà loại bỏ Hook ra khỏi phạm vi, sẽ bỏ sót bề mặt tấn công lớn nhất trong thời đại v4.
5. Suy ngẫm
Khi so sánh cơ chế giao thức và cuộc tấn công Cork, có thể tổng hợp được vài điểm cốt lõi của mô hình bảo mật v4 Hook:
(1) Nếu hàm callback của Hook phụ thuộc vào ngữ cảnh gọi do PoolManager cung cấp, cần rõ ràng giới hạn chỉ được PoolManager gọi. BaseHook sẽ không thực hiện việc này thay cho nhà phát triển; đây là bẫy thiết kế dễ gây xung đột nhất với kinh nghiệm kiểm toán hợp đồng thông thường trong v4.
(2) Mối quan hệ liên kết giữa Hook và pool không bị giới hạn bởi PoolManager. Nhà phát triển phải tự triển khai danh sách trắng pool hoặc liên kết đơn pool trong beforeInitialize.
(3) Quyền của địa chỉ Hook phải hoàn toàn nhất quán với phần thực hiện hàm. Địa chỉ được tính toán phải bao gồm trước tất cả các quyền có thể được mở rộng trong tương lai.
(4) Hook Async / Custom Curve về bản chất là một triển khai swap hoàn toàn tùy chỉnh. Nó không có bất kỳ bảo vệ nào từ cấp độ giao thức Uniswap và phải được kiểm toán theo tiêu chuẩn “hợp đồng tài chính tự chủ hoàn toàn”.
(5) Delta kế toán “bảo toàn” không đồng nghĩa với “chính xác”. NonzeroDeltaCount == 0 chỉ đảm bảo sổ sách cuối cùng cân bằng, không đảm bảo nội dung sổ sách không bị thao túng xấu ý.
(6) Sự nhầm lẫn về loại token giữa các thị trường là một bề mặt tấn công mới trong thời đại v4. Khi giao thức cho phép người dùng tạo thị trường, việc kiểm tra ngữ nghĩa của token là bắt buộc, không thể chỉ dựa vào kiểm tra giao diện.
Mỗi Hook là một vùng tin cậy độc lập, và mức độ an toàn của mỗi hồ sơ được xác định bởi Hook được liên kết với nó. Do đó, độ phức tạp trong việc kiểm toán an toàn Hook không còn là “kiểm toán một đoạn mã”, mà là “kiểm toán một giao thức con hoàn chỉnh” — sự thay đổi này mang lại yêu cầu nâng cấp phương pháp luận cho cả phía dự án và phía kiểm toán.
Tài liệu tham khảo
(1) Cork Protocol. “Báo cáo phân tích sau sự cố ngày 28 tháng 5 năm 2025.” 2025-06-04. https://www.cork.tech/blog/post-mortem
(2) OWASP Top 10 Bảo mật Hợp đồng Thông minh 2026, SC01: Lỗ hổng Kiểm soát Truy cập. https://scs.owasp.org/sctop10/SC01-AccessControlVulnerabilities/
(3) Sách trắng Uniswap v4. https://app.uniswap.org/whitepaper-v4.pdf
(4) Uniswap v4-core. https://github.com/Uniswap/v4-core
(5) Uniswap v4-periphery. https://github.com/Uniswap/v4-periphery
Beosin là một trong những công ty bảo mật blockchain sớm nhất toàn cầu chuyên về xác minh hình thức, tập trung vào toàn bộ hệ sinh thái “an toàn + tuân thủ”. Công ty đã thiết lập chi nhánh tại hơn 10 quốc gia và vùng lãnh thổ, cung cấp các dịch vụ “một cửa” bao gồm kiểm toán bảo mật mã nguồn trước khi dự án lên sàn, giám sát và ngăn chặn rủi ro bảo mật trong quá trình vận hành, truy tìm tài sản bị đánh cắp, chống rửa tiền (AML) cho tài sản ảo, cùng các đánh giá tuân thủ đáp ứng yêu cầu pháp lý tại từng khu vực. Vui lòng nhắn tin qua khung bình luận trên公众号 để liên hệ với chúng tôi.

