正如我們上一篇文章所強調的,在 Awesome Uniswap v4 Hooks 儲存庫[1]中,超過 30% 的專案存在漏洞。值得注意的是,我們在此提到的漏洞是 Uniswap v4 互動所特有的。因此,在本文中,我們將從以下兩個角度審查安全的 Hook 互動邏輯:
- 存取控制缺陷 (Flawed Access control)
- 輸入驗證不當 (Improper Input Validation)
對於每個類別,我們將首先分析漏洞,並透過提供相應的概念驗證 (PoC) 來演示其潛在的利用方式。隨後將討論潛在的緩解策略。
存取控制缺陷
一般來說,與 Uniswap v4 的 Hook 相關的互動可以根據 Hook 是否充當鎖定者 (Locker) 來分類,即在 PoolManager 中獲取鎖定權以在資金池中執行操作。兩個主要的互動場景需要正確的存取控制:
- Hook-PoolManager 互動:這涉及官方回呼函數與
PoolManager之間的互動。回呼函數包括八個資金池操作回呼(即initialize、modifyPosition、swap和donate)以及鎖定回呼(即lockAcquired)。
- Hook-Internal 互動:這涉及在 Hook 合約內部發生的互動(充當鎖定者)。
Hook-PoolManager 互動相對簡單。在這種情況下,Hook 純粹作為一個 Hook,接受八個資金池操作回呼。Hook 中的邏輯不會影響相關的資金池,這意味著 Hook 和資金池之間沒有資金流動。回呼函數提供的參數用於修改必要的儲存空間或作為重要的函數參數。關鍵考慮因素是 回呼參數是否可以被篡改。
Hook-Internal 互動則比較複雜。實際上,許多 Hook 原型不僅僅是作為純粹的 Hook。一些開發人員允許 Hook 為其使用者提供資金管理功能。這些功能可能沒有在 Hook 合約中實現,但我們仍然可以將它們在此上下文中統稱為 Hook。在這些情況下,Hook 接受使用者資金並執行資金池操作,例如流動性管理或交易。這意味著合約必須從 PoolManager 獲取鎖定權,從而使 Hook 變成一個鎖定者。
Uniswap 基金會已經考慮到了這種情況,並在其 Hook 模板中整合了一個函數。具體來說,BaseHook 模板提供了 lockAcquired 函數作為鎖定回呼,如下所示:
function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
(bool success, bytes memory returnData) = address(this).call(data);
if (success) return returnData;
if (returnData.length == 0) revert LockFailure();
// if the call failed, bubble up the reason
/// @solidity memory-safe-assembly
assembly {
revert(add(returnData, 32), mload(returnData))
}
}
為了執行自訂邏輯,lockAcquired 接受 data 位元組,並使用該 data 對自身進行底層呼叫 (low-level call)。data 取決於 Hook 的業務邏輯,並且可以被使用者操縱,這可能會導致由 lockAcquired 觸發的 Hook-Internal 互動引發安全問題。請注意,Hook 的設計非常靈活,我們無法涵蓋這種情況下的所有可能場景。我們這裡的主要重點是 Hook 獲取鎖定及其隨後的內部互動。深入研究其他潛在的業務邏輯會使情況變得過於複雜,不適合本次討論。
在這兩種場景中,優先事項都是解決任何可能導致被利用的存取控制缺陷,因為這些函數具有明確的互動實體。在接下來的小節中,我們將依次檢查每個場景,並討論確保更安全互動邏輯所需的必要存取控制。
漏洞分析
存取控制對於許多專案來說是高效且直接的安全解決方案。如果一個函數設計為由特定實體呼叫,它就應該包含存取控制。存取控制最著名的例子是 OpenZeppelin 函式庫的 Ownable 合約,它要求特權函數只能由合約所有者呼叫。很明顯,我們上面討論的兩個場景都是這類控制的適用案例。
Hook-PoolManager 互動:為了與 PoolManager 進行安全互動,Hook 應該對這些回呼函數實施必要的存取控制。具體來說,這些回呼應該僅能由 PoolManager 呼叫,而不能被任何其他帳戶呼叫。未能建立此類控制可能會使這些敏感介面暴露給惡意行為者,從而導致潛在的利用。
除了八個資金池操作回呼之外,從 PoolManager 獲取鎖定後執行自訂邏輯的鎖定回呼(即 lockAcquired)也需要解決這個問題。
Hook-Internal 互動:Hook 內部互動所涉及的函數同樣設計為由特定呼叫者呼叫。正如我們之前所說,這種場景包含兩個階段。首先,鎖定者的 lockAcquired 函數由 PoolManager 呼叫,這表示該函數應該要求 msg.sender 為 PoolManager。其次,Hook 相應地分派函數呼叫。基於 BaseHook 的設計,它是通過對 Hook 本身進行底層呼叫來實現的。這表示這些函數必須定義為 external,並限制呼叫者必須是 Hook 的地址。
以 Awesome Uniswap v4 Hooks 儲存庫列出的範例之一,即 止損單 (Stop Loss Order) 為例[2]:
止損單直接整合到 Uniswap V4 資金池中,在鏈上發布並通過
afterSwap()Hook 執行。無需外部機器人或參與者來保證執行。
讓我們檢查它的 afterSwap 回呼函數:
![Figure 1: The afterSwap function of Stop Loss Order[2]](https://assets.blocksec.com/frontend/blocksec-strapi-online/1_09ba973d9e.webp)
顯然,上述函數旨在執行敏感操作。然而,由於存取控制缺陷,它可能會被惡意行為者操縱參數(例如 key 和 params)所利用,從而導致意外行為。例如,afterSwap 回呼可能預設交易已經在 PoolManager 中發生。在此之後,它可以啟動操作來記錄必要的基本狀態資訊,例如當前價格或收取的交易費用。但是,如果 afterSwap 沒有嚴格限制其呼叫來源僅限於 PoolManager,惡意行為者可能會偽造 params 參數,導致記錄的狀態出現偏差。
利用與 PoC
為了簡單起見,我們將使用一個基本的 PoC 來演示這個存取控制問題。通常,Hook 的 beforeInitialize 接受一個 PoolKey 類型的參數,該參數必須在其 hooks 欄位中包含此 Hook 地址(因為 PoolManager 將使用此欄位來確定要呼叫的 Hook 地址)。
截圖提供了一個 PoC,展示了對具有存取控制缺陷的 Hook 的利用,如 DiamondHookPoC [3] 中所示。
在沒有對 beforeInitialize 回呼函數實施存取限制的情況下,惡意行為者可以向此函數提供任意的 poolKey。Hook 不會驗證此 poolKey 的 Hook 是否與當前 Hook 地址匹配。

雖然值得注意的是,這種場景下的利用可能不會對 Hook 造成資金損失,但它確實突顯了如何通過未受保護的回呼函數篡改 Hook 的狀態。
如何緩解
為了確保 Hook-PoolManager 互動的安全性,Hook 回呼和鎖定回呼都應該將其存取權限嚴格限制為僅限 PoolManager。
幸運的是,Uniswap v4 通過其 v4-periphery 儲存庫中的 BaseHook 提供了最佳實踐[4]。
BaseHook 提供了 poolManagerOnly 修飾符,用於嚴格限制呼叫僅來自 PoolManager:
/// @dev Only the pool manager may call this function
modifier poolManagerOnly() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}
這個修飾符可以有效地用於對敏感的 Hook 和鎖定回呼強制執行正確的存取控制。
另一方面,Hook-Internal 互動的存在要求任何通過 BaseHook 指定的 lockAcquired 回呼函數呼叫的重大狀態變更函數,都不應被任意呼叫。
為了滿足這一要求,BaseHook 提供了 selfOnly 修飾符。此修飾符將宣告函數的存取權限限制在 Hook 本身,禁止外部合約直接呼叫這些敏感函數以用於惡意目的。
/// @dev Only this address may call this function
modifier selfOnly() {
if (msg.sender != address(this)) revert NotSelf();
_;
}
總之,通過繼承 BaseHook,自訂 Hook 可以利用這些內建的存取控制修飾符和回呼來強制執行正確的存取控制。
輸入驗證不當
v4-periphery[4] 中的 BaseHook 為更安全的互動邏輯提供了解決方案,Hook 開發人員可以利用它。然而,我們仍然觀察到不當使用的實例,這些實例為現有 Hook 中的攻擊向量開啟了新的可能。
預設情況下,Hook 允許任何資金池通過 PoolManager 中的 initialize 函數進行註冊。但是,如果 Hook 未能驗證註冊資金池中的底層資產,惡意使用者可能會註冊一個包含偽造代幣的資金池,從而使他們能夠通過代幣的 transfer 函數重新進入 Hook。
此漏洞很微妙,因為 Hook 本身可能不會執行惡意邏輯。然而,當 Hook 呼叫 PoolManager 時,PoolManager 和惡意資金池底層資產之間的互動可能會通過 PoolManager 中的 take 函數將控制流交給攻擊者。
/// @inheritdoc IPoolManager
function take(Currency currency, address to, uint256 amount) external override
noDelegateCall onlyByLocker {
_accountDelta(currency, amount.toInt128());
reservesOf[currency] -= amount;
currency.transfer(to, amount);
}
本質上,該漏洞源於對 Hook 使用者計畫與之互動的註冊資金池缺乏正確驗證。我們將使用一個具體範例深入探討此漏洞,並討論潛在的緩解策略。
漏洞分析
獲利結算 Hook (Take Profits Hook)[5] 是 Awesome Uniswap v4 Hooks 列出的 Hook 之一:
在此範例中,我們構建了一個允許使用者放置「獲利結算 (take-profit)」倉位的 Hook。例如,在 ETH/DAI 資金池中,如果當前 1 ETH = 1500 DAI,您可以放置一個「當 1 ETH = 2000 DAI 時賣出我所有 ETH」的獲利結算訂單,該訂單將自動執行。
讓我們看看這個 Hook 中的 _handleSwap 函數。該函數在獲取鎖定後執行交易以完成獲利結算訂單。
![Figure 3: The _handleSwap function of Take Profits Hook[5]](https://assets.blocksec.com/frontend/blocksec-strapi-online/3_53b046f4c7.webp)
您可能會注意到該函數沒有受到任何存取控制修飾符的保護。但是,第 250 行有效地限制了存取,使得該函數只能在從 PoolManager 獲取鎖定後才能被呼叫。否則,poolManager.swap 將會失敗,因為操作者不是最近的鎖定者。換句話說,_handleSwap 必須以特定的順序呼叫,前提是已註冊的資金池經過驗證。遺憾的是,該 Hook 並沒有實現這種驗證。
由於這種有缺陷的實現,該 Hook 容易受到重入攻擊 (reentrancy attack)。此漏洞可能允許攻擊者使用使用者存入的資金強制執行任意交易。
利用與 PoC
具體來說,攻擊可以通過以下步驟發起:
- 攻擊者註冊一個帶有假代幣的惡意資金池,並指定 獲利結算 Hook 作為該資金池的 Hook。
- 攻擊者通過該 Hook 在惡意資金池中放置一個 止盈 (stop-profit) 訂單。
- 攻擊者在惡意資金池中進行交易,觸發
afterSwap回呼中的fillOrder以填寫攻擊者的止盈訂單。 - Hook 呼叫
PoolManager的lock函數來請求鎖定,並在lockAcquired回呼中呼叫_handleSwap函數。 - 在
_handleSwap函數中,代幣的轉帳會觸發假代幣合約中的惡意邏輯,從而重入_handleSwap函數。這是可能的,因為_handleSwap是一個沒有任何存取限制的external函數。由於鎖定已經獲得,攻擊者可以強制 Hook 在任何資金池上執行任意交易,只要 Hook 持有足夠的底層資產。然後,攻擊者可以套利這些交易,以犧牲其他使用者的利益為代價來獲取利潤。
下圖詳細說明了攻擊流程。

如前所述,Hook 本身並不會呼叫惡意邏輯。唯一的錯誤是 Hook 沒有阻止不受信任的代幣資金池註冊到 PoolManager 合約中。間接地,假代幣合約中的惡意邏輯是通過代幣轉帳操作呼叫的,這也是一種不受信任的外部呼叫。
如何緩解
有三種可行的方法來緩解由於輸入驗證不當而導致的潛在攻擊:
-
正確的存取控制。通過利用
BaseHook中的構建塊,Hook 可以嚴格管理函數存取權限。這可以防止任意帳戶呼叫敏感函數。 -
重入鎖 (Reentrancy Lock)。在上述攻擊場景中,此方法無疑可以防止惡意代幣邏輯重入敏感函數。然而,在某些情況下,Hook 的設計需要 Hook 本身是可重入的。 具體來說,當 Hook 需要執行某些資金池操作時,它應該允許
PoolManager重入其回呼以完成這些操作。重入鎖可能會破壞此預期功能。 -
白名單方法 (Whitelisting Approach)。這需要特權管理員在 Hook 中建立已批准資金池的白名單。管理員確保列入白名單的資金池不會引入潛在風險。然而,其限制是 Hook 使用者只能通過該 Hook 在有限數量的管理員批准的資金池上執行操作。 雖然白名單方法提高了安全性,但它嚴格限制了 Hook 的功能。
要在 Hook 的安全性和可用性之間找到完美的平衡點具有挑戰性。雖然我們討論了幾種緩解方法,但開發人員需要在其 Hook 設計中認真權衡。目標應該是在保留預期功能的同時,儘可能降低潛在風險。此外,我們的討論僅涵蓋了可能存在於 Uniswap v4 特有功能的互動中的漏洞。實際應用無疑會更加全面。請務必確保您了解合約的每一行代碼,並保持資產安全 (SAFU)!
結論
在本文中,我們探討了在 Hook 互動邏輯過程中出現的漏洞,特別集中於兩個場景:存取控制缺陷和輸入驗證不當。我們提出了詳細的漏洞分析,演示了潛在的利用方式及其 PoC,並討論了潛在的緩解策略。我們相信,這些見解有助於 Hook 的安全開發與使用,並為未來的漏洞檢測工作提供指導。
參考文獻
[2] Stop Loss Order
[3] DiamondHookPoC
[4] v4-periphery
[5] Take Profits



