近期Robinhood Chain的熱潮吸引了大量用戶在其鏈上的Uniswap交易。當用戶發現某個Uniswap v4池子顯示0%的手續費,報價比市場最優價格還低幾個基點時,將它選為最佳路由進行交易。但交易完成後他實際收到的代幣,比報價少了接近1%。檢查鏈上數據後可以發現,這個損失實際是因為隱藏在Hook合約里的「暗稅」,Hook通過afterSwapReturnsDelta權限在交易結算前截留了一部分代幣。
9月流動性聚合基礎設施0x Protocol對6條鏈上的84,163個Uniswap v4 Hook進行了靜態分析、動態分析和實際成交觀察,發現僅19.4%的Hook被判定為安全,54.2%被歸類為惡意,另有26.4%存在疑似惡意行為。本文Beosin安全團隊將對Uniswap v4 Hook的攻擊向量進行解析,幫助用戶和開發者理解其中可能存在的惡意行為。
在 Uniswap v4 中,流動性池子由 PoolManager 統一管理,Hook 合約可以在池子初始化、添加流動性、移除流動性、Swap 和 Donate 等關鍵節點插入自定義邏輯,如修改交易費、交易前後餘額和結算邏輯。其安全風險主要集中在以下位置:
Hook 返回異常的餘額變化
Hook 在回調中調用外部合約
Hook 讀取瞬時價格並據此決定費率
Hook 保存跨池、跨用戶的狀態
Hook 利用回調順序和交易回滾機制製造拒絕服務
攻擊者可能首先建立一個看起來正常的流動性池,在 Hook 中加入隱藏收費、黑名單邏輯,然後通過前端、空投、聚合器或社交媒體吸引用戶。在用戶進行交易或提供流動性後,即可通過自定義邏輯、外部調用惡意獲取其他用戶資產。據Beosin分析,目前常見的惡意Hook形式有:
下面的Hook展示了「對指定地址收取額外費用」的邏輯:開發者通過privilegedUsers給自己、做市商或指定機器人免除費用,而其他用戶需承擔額外交易費用。
contract SuspiciousFeeHook {
address public immutable manager;
address public immutable feeRecipient;
mapping(address => bool) public privilegedUsers;
constructor(address _manager, address _feeRecipient) {
manager = _manager;
feeRecipient = _feeRecipient;
}
function beforeSwap(
address sender,
uint256 amountIn
) external returns (uint256 adjustedAmountIn) {
require(msg.sender == manager, "only manager");
if (privilegedUsers[sender]) {
return amountIn;
}
uint256 hiddenFee = amountIn * 30 / 10_000;
adjustedAmountIn = amountIn - hiddenFee;
// demo, in reality, Hook should not bypass the settlement rules of the PoolManager
IERC20(token()).transferFrom(
sender,
feeRecipient,
hiddenFee
);
}
function token() internal view returns (address) {
return address(0);
}
}在真實 v4 架構中,Hook 通常不能任意繞過 PoolManager 的結算規則,但它可以通過自定義 delta、外部代幣調用或池配置影響最終結算。因此,審計重點是檢查 Hook 的所有餘額變化路徑。
contract AddressBasedHook {
mapping(address => bool) public blocked;
mapping(address => uint24) public customFee;
function beforeSwap(
address sender,
uint256 amountSpecified
) external returns (uint24 fee) {
if (blocked[sender]) {
revert("blocked trader");
}
uint24 userFee = customFee[sender];
if (userFee != 0) {
return userFee;
}
return 10_000; // default 100%
}
}以上代碼存在黑名單機制且根據用戶地址設置不同費率。如果費率沒有被前端完整展示,用戶看到的池子可能與實際交易條件不一致。在審計或開發時必須確認所有費率返回值均滿足協議允許範圍,而不是只檢查常規路徑。
contract ReentrantHook {
address public callbackTarget;
bool private entered;
function beforeSwap(address sender) external {
require(!entered, "reentered");
entered = true;
// 外部調用,可能回調當前 Hook 或其他協議
ICallback(callbackTarget).onSwap(sender);
entered = false;
}
}ERC-777 或自定義 ERC-20 回調;
ERC-721/ERC-1155 接收回調;
借貸協議回調;
聚合器回調;
當前 Hook 的再次調用;
其他池子的 Swap。
如果狀態更新和外部調用順序錯誤,可能發生重複計算結果、繞過單次交易限制、重複領取手續費、跨池狀態污染、價格或額度檢查失效等問題。需要注意,v4 的核心合約具有自身的解鎖和結算約束,但這不意味著 Hook 可以安全地進行任意外部調用。安全邊界必須在每個 Hook 中單獨建立。
contract FragileOracleHook {
uint256 public lastObservedPrice;
function beforeSwap(
uint160 currentSqrtPriceX96
) external {
uint256 spotPrice = priceFromSqrtPrice(currentSqrtPriceX96);
// 錯誤做法:直接使用當前池子的瞬時價格
if (spotPrice > 1_100e18) {
triggerLiquidation();
}
lastObservedPrice = spotPrice;
}
function triggerLiquidation() internal {
// 教學占位:根據價格執行清算或發放獎勵
}
}攻擊者可以在同一交易中通過閃電貸獲得大量資產;然後對目標池進行大額 Swap 將瞬時價格推向極端,由此觸發 Hook 的清算、獎勵或抵押品估值邏輯。如果 Hook 把當前池價格當作可信預言機,攻擊者可能在一個區塊內完成操縱。
contract ExitBlockingHook {
mapping(address => bool) public blockedLP;
function beforeRemoveLiquidity(address provider) external {
if (blockedLP[provider]) {
revert("withdraw disabled");
}
}
function setExitLock(address user, bool locked) external onlyOwner {
blockedLP[user] = locked;
}
}這類 Hook可以讓用戶加入流動性,卻無法移除流動性。常見觸發條件包括:
用戶地址被加入黑名單
池子達到某個 TVL
某個時間戳之後
Hook 管理員調用隱藏開關
用戶曾經通過聚合器交易
用戶沒有支付額外費用
此類攻擊其實在以往的合約攻擊中也非常常見,通過在交易中消耗異常 Gas導致交易失敗。
contract GasGriefHook {
function beforeSwap(uint256 n) external {
for (uint256 i = 0; i