Phân tích bảo mật Hook của Uniswap v4: Kiến trúc, lỗ hổng và các thực hành tốt nhất

icon MarsBit
Chia sẻ
AI summary iconTóm tắt

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 như SATO, uPEG (Unipeg), Slonks dựa trên các cách chơi liên quan đến Hook 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 trong 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 lẻ cho từng pool

- Kế toán chớp nhoáng: Các thay đổi số dư trung gian trong quá trình giao dịch chỉ được ghi nhận trong bộ lưu trữ tạm thời và được thanh 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池子 đượ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池子 không thể được sửa đổi, nhưng việc có thể nâng cấp hợp đồng Hook hay không phụ thuộc vào cách triển khai của nó)

Image

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 pool phụ thuộc vào Hook được gắn 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 pool”.

2. Kiến trúc Hook

2.1 PoolManager và mô hình unlock/callback

Hợp đồng cốt lõi của v4 là PoolManager đơn thể. 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:

Image

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:

Image

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 có từng được sử dụng cho pool khác hay không, cùng một hợp đồng Hook có thể được liên kết đồng thời với nhiều pool.

Image

2.2 Mã hóa bit quyền Hook trong địa chỉ

Một thiết kế không trực quan trong v4 là: quyền hạ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:

Image

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 cuối 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:

Image

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 tương ứng 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 modifier onlyPoolManager cho hàm unlockCallback():

Image

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 cho các hàm callback hook khác (beforeSwap, afterSwap, beforeAddLiquidity, v.v.). Việc kiểm soát truy cập cho các hàm này phải được cá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ộ call stack 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:

Image

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 hồ bơi nào khác hay không, 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 hồ bơi đơn lẻ trong beforeInitialize, bất kỳ ai cũng có thể tạo một hồ bơi 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():

Image

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 lập tức

- 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:

Image

Cần lưu ý rằng ngữ nghĩa của BeforeSwapDelta là Hook thu phí phải trả về giá trị dương, Hook hoàn lại token phải trả về giá trị âm. 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 params.zeroForOne và dấu của amountSpecified, cách viết hơi sai lệch sẽ dẫn đến lỗi token.

PoolManager sẽ cộng trực tiếp specifiedDelta trả về từ beforeSwap vào amountToSwap:

Image

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 nắm quyền kiểm soát giao dịch swap này—đây được gọi là Async Hook hoặc Custom Curve Hook.

Async Hook là một 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, tiề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à chủ yếu phụ thuộc vào tính chính xác của chính Hook.

3.3 Delta thanh toán và NonzeroDeltaCount

delta trả về từ 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ổ kế toán nội bộ của PoolManager:

Image

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):

Image

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 sổ sách”, không đảm bảo “sổ sách chính xác”. Nếu Hook trả về một delta được tạo ra một cách độc hại trong beforeSwap, PoolManager sẽ trung thành ghi sổ 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 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 có tính hướng đích 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ự 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.

4. Suy ngẫm

Khi so sánh cơ chế giao thức và cuộc tấn công Cork, có thể rút ra 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 kỳ 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 miền 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 sự nâng cấp về phương pháp luận cho cả phía dự án lẫn phía kiểm toán.

Xem bài gốc

Tuyên bố miễn trừ trách nhiệm: Thông tin trên trang này có thể được lấy từ bên thứ ba và không nhất thiết phản ánh quan điểm hoặc ý kiến của KuCoin. Nội dung này chỉ được cung cấp cho mục đích thông tin chung, không có bất kỳ đại diện hay bảo đảm nào dưới bất kỳ hình thức nào và cũng không được hiểu là lời khuyên tài chính hay đầu tư. KuCoin sẽ không chịu trách nhiệm về bất kỳ sai sót hoặc thiếu sót nào hoặc về bất kỳ kết quả nào phát sinh từ việc sử dụng thông tin này. Việc đầu tư vào tài sản kỹ thuật số có thể tiềm ẩn nhiều rủi ro. Vui lòng đánh giá cẩn thận rủi ro của sản phẩm và khả năng chấp nhận rủi ro của bạn dựa trên hoàn cảnh tài chính của chính bạn. Để biết thêm thông tin, vui lòng tham khảo Điều khoản sử dụngTiết lộ rủi ro của chúng tôi.