Uniswap V4 Hook 合約代碼深度註解:從機制設計到實際部署

本文深入解讀 Uniswap V4 Hook 機制的合約原始碼,涵蓋單例合約架構、閃電結算系統、八個鉤子觸發點的完整執行順序、以及 IHook 接口的逐函數分析。提供 TWAMM、限價單、動態費用等經典 Hook 應用的完整 Solidity 程式碼註解。同時討論重入風險、Gas 優化、許可清單設計等安全考量,以及部署和測試的最佳實踐。

Uniswap V4 Hook 合約代碼深度註解:從機制設計到實際部署

前言

Uniswap V4 發布的時候,最吸引眼球的就是那個「Hook」機制。這東西說白了就是允許你在流動性池的生命周期各個階段插入自定義邏輯——交易前可以幹嘛、交易後可以幹嘛、流動性變化的時候可以幹嘛。

我剛開始接觸這個概念的時候,網上資料多是「這是一個革命性的創新」之類的泛泛而談。要找點實質內容——Hook 合約到底長什麼樣、各個鉤子函數的執行順序是什麼、怎麼避免自己寫的 Hook 把整個池子搞掛——這些細節反而付之闕如。

這篇文章我打算把 Uniswap V4 的 Hook 機制從頭到尾拆解一遍。不只停在概念介紹,而是深入到合約原始碼層面,逐函數解讀執行邏輯、上下文傳遞、以及實際部署需要注意的坑。如果你想在 V4 上開發自己的 Hook,或者只是想搞清楚這套機制背後的原理,這篇文章應該能幫到你。

一、Uniswap V4 架構的核心變化

1.1 單例合約(Singleton)設計

V4 之前,每個 Uniswap 池都是獨立的合約。創建新池需要部署新合約,Gas 成本高,而且跨池操作(比如三邊 swap)需要在多個合約之間跳來跳去。

V4 引入單例合約,所有池子都在同一個合約裡管理。這個改動為 Hook 機制奠定了基礎——因為只有所有邏輯都在一個合約空間內,才能實現真正的「鉤子插入」。

// 簡化的 V4 單例合約結構
contract UniswapV4 {
    // 池子數據結構
    struct PoolState {
        address token0;
        address token1;
        uint24 fee;
        address hook;
        uint160 sqrtPriceX96;
        int24 tick;
        // ... 其他狀態
    }
    
    // 池子映射:用 salt 生成 key
    mapping(bytes32 => PoolState) public pools;
    
    // 用 salt 計算池子 key
    // salt = keccak256(abi.encode(token0, token1, fee, hook))
    function getPoolKey(
        address token0,
        address token1,
        uint24 fee,
        address hook
    ) public pure returns (bytes32) {
        if (token0 > token1) (token0, token1) = (token1, token0);
        return keccak256(abi.encode(token0, token1, fee, hook));
    }
}

單例合約的 Gas 優化效果很明顯:

1.2 閃電結算(Flash Accounting)

V4 另一個核心創新是「閃電結算」。這名字有點誤導,實際上指的是內部記帳系統的優化。

傳統 AMM 在 swap 時需要「轉入-計算-轉出」的過程,每一步都要實際轉帳。V4 改成了「先記帳後統一結算」,大幅減少了 ERC-20 轉帳次數。

// 閃電結算的核心概念
contract FlashAccounting {
    // 內部餘額追蹤
    mapping(address => int256) internal balances;
    
    // 從用戶吸收代幣(但先記帳)
    function _settle(address token, uint256 amount) internal {
        balances[token] += int256(amount);
        // 實際轉帳在 swap 結束後統一處理
    }
    
    // 給用戶轉出代幣(但先記帳)
    function _take(address token, uint256 amount) internal {
        balances[token] -= int256(amount);
        // 實際轉帳在 swap 結束後統一處理
    }
    
    // 交易結束後的結算
    function _accountingSettle() internal {
        // 遍歷所有代幣,計算淨流動
        // 統一執行實際轉帳
    }
}

這套機制讓 Hook 可以在任意時刻「看到」池子的內部狀態,而不需要等待實際轉帳完成。對開發者來說相當於多了很多操作自由度。

二、Hook 機制的核心設計

2.1 鉤子生命周期全覽

V4 的 Hook 在以下八個時機被觸發:

// Hook 觸發點定義
uint256 constant BEFORE_INITIALIZE_FLAG = 1 << 0;    // 池子初始化前
uint256 constant AFTER_INITIALIZE_FLAG = 1 << 1;     // 池子初始化後
uint256 constant BEFORE_MODIFY_POSITION_FLAG = 1 << 2;  // 流動性變化前
uint256 constant AFTER_MODIFY_POSITION_FLAG = 1 << 3;   // 流動性變化後
uint256 constant BEFORE_SWAP_FLAG = 1 << 4;         // 交易前
uint256 constant AFTER_SWAP_FLAG = 1 << 5;         // 交易後
uint256 constant BEFORE_DONATE_FLAG = 1 << 6;       // 捐贈前
uint256 constant AFTER_DONATE_FLAG = 1 << 7;        // 捐贈後

每個時機都有 beforeafter 兩個鉤子。before 鉤子通常用來做權限檢查或修改參數,after 鉤子通常用來做狀態更新或觸發外部邏輯。

2.2 IHook 接口定義

// IHook.sol - Hook 合約必須實現的接口
interface IHook {
    // ============================================
    // 初始化鉤子
    // ============================================
    
    /// @notice 池子初始化前的鉤子
    /// @param sender 初始化交易的發送者
    /// @param poolKey 池子的識別符
    /// @param sqrtPriceX96 初始價格的平方根(96位精度)
    /// @return 鉤子可以返回任意 bytes 數據,供 afterInitialize 使用
    function beforeInitialize(
        address sender,
        bytes32 poolKey,
        uint160 sqrtPriceX96
    ) external returns (bytes memory);
    
    /// @notice 池子初始化後的鉤子
    /// @param sender 初始化交易的發送者
    /// @param poolKey 池子的識別符
    /// @param sqrtPriceX96 初始價格的平方根
    /// @param tick 初始tick(價格刻度)
    /// @return 鉤子可以返回任意 bytes 數據
    function afterInitialize(
        address sender,
        bytes32 poolKey,
        uint160 sqrtPriceX96,
        int24 tick
    ) external returns (bytes memory);
    
    // ============================================
    // 流動性修改鉤子
    // ============================================
    
    /// @notice 流動性增加/減少前的鉤子
    /// @param sender 操作發送者
    /// @param poolKey 池子識別符
    /// @param params 修改參數(包含 tokenId, liquidityDelta 等)
    /// @return 鉤子返回的數據
    function beforeModifyPosition(
        address sender,
        bytes32 poolKey,
        ModifyPositionParams calldata params
    ) external returns (bytes memory);
    
    /// @notice 流動性修改後的鉤子
    /// @param sender 操作發送者
    /// @param poolKey 池子識別符
    /// @param params 修改參數
    /// @param quantity 流動性變化的代幣數量(負值表示移除)
    /// @return 鉤子返回的數據
    function afterModifyPosition(
        address sender,
        bytes32 poolKey,
        ModifyPositionParams calldata params,
        BalanceDelta memory quantity
    ) external returns (bytes memory);
    
    // ============================================
    // 交易鉤子
    // ============================================
    
    /// @notice 交易前的鉤子
    /// @param sender 交易發送者
    /// @param poolKey 池子識別符
    /// @param params 交易參數
    /// @return (鉤子是否願意讓交易繼續, 鉤子返回的數據)
    function beforeSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params
    ) external returns (bool, bytes memory);
    
    /// @notice 交易後的鉤子
    /// @param sender 交易發送者
    /// @param poolKey 池子識別符
    /// @param params 交易參數
    /// @param swapResult 交易的結算結果
    /// @return (鉤子調整後的結算, 鉤子返回的數據)
    function afterSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params,
        BalanceDelta memory swapResult
    ) external returns (BalanceDelta memory, bytes memory);
    
    // ============================================
    // 捐贈鉤子
    // ============================================
    
    /// @notice 捐贈前的鉤子
    /// @param sender 捐贈者
    /// @param poolKey 池子識別符
    /// @param amount0 token0 捐贈數量
    /// @param amount1 token1 捐贈數量
    /// @return 鉤子返回的數據
    function beforeDonate(
        address sender,
        bytes32 poolKey,
        uint256 amount0,
        uint256 amount1
    ) external returns (bytes memory);
    
    /// @notice 捐贈後的鉤子
    /// @param sender 捐贈者
    /// @param poolKey 池子識別符
    /// @param amount0 token0 捐贈數量
    /// @param amount1 token1 捐贈數量
    /// @return 鉤子返回的數據
    function afterDonate(
        address sender,
        bytes32 poolKey,
        uint256 amount0,
        uint256 amount1
    ) external returns (bytes memory);
}

2.3 Hook 地址計算與部署

V4 的創新之一是 Hook 地址可以用算法從合約字節碼推導出來。這樣就能實現「一次性部署、按鈕創建池子」的模式,降低創建 Hook 池的門檻。

// Hook 地址計算邏輯
contract HookDeployer {
    // 計算指定 salt 的 Hook 地址
    function computeHookAddress(
        address factory,
        bytes32 salt,
        bytes memory creationCode,
        bytes memory constructorArgs
    ) public pure returns (address) {
        // Hook 地址 = keccak256(keccak256(creationCode + args) + salt)
        bytes memory codeAndArgs = bytes.concat(
            creationCode,
            constructorArgs
        );
        bytes32 combinedHash = keccak256(codeAndArgs);
        
        return address(uint160(uint256(keccak256(abi.encode(
            combinedHash,
            salt
        )))));
    }
    
    // 部署 Hook 並計算其地址
    function deployAndGetAddress(
        bytes memory creationCode,
        bytes memory constructorArgs,
        bytes32 salt
    ) public returns (address hook) {
        // 先計算部署後的地址
        hook = computeHookAddress(
            address(this),
            salt,
            creationCode,
            constructorArgs
        );
        
        // 檢查是否已部署
        if (hook.code.length > 0) {
            return hook; // 已經部署過了
        }
        
        // 實際部署
        assembly {
            hook := create2(0, add(creationCode, 0x20), mload(creationCode), salt)
        }
        
        require(hook != address(0), "Hook deployment failed");
    }
}

這個設計的好處是:你可以在創建池子之前就預知 Hook 地址,不需要先部署再引用。錢包前端可以直接計算出「這個 Hook 池子的地址是什麼」,用戶體驗更流暢。

三、實際 Hook 合約開發

3.1 TWAMM 合約實現

時間加權平均做市商(TWAMM)是 V4 Hook 最經典的應用之一。原理是把大額訂單在時間軸上拆分成無數個小額訂單,用數學積分來計算執行結果。

// TWAMM Hook - 時間加權平均做市商
contract TWAMMHook is IHook {
    // ============================================
    // 狀態變數
    // ============================================
    
    // 訂單結構
    struct TWAMMOrder {
        address owner;              // 訂單所有者
        uint256 sellAmountPerBlock; // 每區塊賣出的數量
        uint256 totalSellAmount;    // 總賣出數量
        uint256 startBlock;         // 開始區塊
        uint256 endBlock;           // 結束區塊
        uint256 remainingSell;      // 剩餘待執行數量
        bool isActive;              // 是否活躍
    }
    
    // 池子 -> 訂單列表映射
    mapping(bytes32 => TWAMMOrder[]) public orders;
    
    // 池子 -> 最後計算區塊
    mapping(bytes32 => uint256) public lastProcessedBlock;
    
    // ============================================
    // 訂單管理
    // ============================================
    
    /// @notice 創建 TWAMM 訂單
    function createOrder(
        bytes32 poolKey,
        uint256 sellAmountPerBlock,
        uint256 totalAmount,
        uint256 durationInBlocks
    ) external {
        TWAMMOrder memory order = TWAMMOrder({
            owner: msg.sender,
            sellAmountPerBlock: sellAmountPerBlock,
            totalSellAmount: totalAmount,
            startBlock: block.number,
            endBlock: block.number + durationInBlocks,
            remainingSell: totalAmount,
            isActive: true
        });
        
        orders[poolKey].push(order);
    }
    
    /// @notice 取消訂單
    function cancelOrder(bytes32 poolKey, uint256 orderId) external {
        TWAMMOrder storage order = orders[poolKey][orderId];
        require(order.owner == msg.sender, "Not order owner");
        require(order.isActive, "Order already inactive");
        
        // 退還剩餘未執行的數量
        order.isActive = false;
        // 退幣邏輯...
    }
    
    // ============================================
    // Hook 實現:swap 前執行 TWAMM 訂單
    // ============================================
    
    function beforeSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params
    ) external override returns (bool, bytes memory) {
        // 處理該池子的 TWAMM 訂單
        uint256 currentBlock = block.number;
        uint256 lastBlock = lastProcessedBlock[poolKey];
        
        if (lastBlock == 0) {
            lastBlock = currentBlock;
        }
        
        // 計算自上次處理以來的區塊數
        uint256 blocksElapsed = currentBlock - lastBlock;
        
        TWAMMOrder[] storage poolOrders = orders[poolKey];
        uint256 totalTWAMMSell0 = 0;
        uint256 totalTWAMMSell1 = 0;
        
        // 遍歷所有活躍訂單
        for (uint256 i = 0; i < poolOrders.length; i++) {
            TWAMMOrder storage order = poolOrders[i];
            
            if (!order.isActive) continue;
            if (currentBlock < order.startBlock) continue;
            if (currentBlock >= order.endBlock) {
                order.isActive = false; // 訂單到期
                continue;
            }
            
            // 計算這個區塊應該賣出的數量
            uint256 blocksActive = min(currentBlock, order.endBlock) 
                                 - max(lastBlock, order.startBlock);
            
            if (blocksActive > 0) {
                uint256 toSell = blocksActive * order.sellAmountPerBlock;
                toSell = min(toSell, order.remainingSell);
                
                // 更新訂單狀態
                order.remainingSell -= toSell;
                
                // 累計 TWAMM 賣出量
                // 假設訂單是賣 token0 買 token1
                totalTWAMMSell0 += toSell;
                
                if (order.remainingSell == 0) {
                    order.isActive = false;
                }
            }
        }
        
        // 更新最後處理區塊
        lastProcessedBlock[poolKey] = currentBlock;
        
        // 將 TWAMM 交易量加入到 swap 參數中
        // 這裡的實現需要和 V4 合約交互,細節省略
        // 核心思想是把 TWAMM 看成「看不見的 LP」
        
        return (true, abi.encode(totalTWAMMSell0, totalTWAMMSell1));
    }
    
    // ============================================
    // 輔助函數
    // ============================================
    
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }
    
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a : b;
    }
}

3.2 限價單鉤子實現

V4 讓實現限價單變得非常自然——你只需要在 afterSwap 鉤子裡檢查是否觸發條件,觸發了就自動添加逆向流動性。

// Limit Order Hook - 限價單實現
contract LimitOrderHook is IHook {
    // ============================================
    // 訂單結構
    // ============================================
    
    struct LimitOrder {
        address owner;           // 訂單所有者
        address tokenIn;         // 要買入的代幣
        address tokenOut;        // 要賣出的代幣
        uint256 amountIn;         // 買入數量上限
        uint256 amountOut;       // 已成交數量
        int24 tick;              // 目標tick
        bool isActive;           // 是否活躍
        bool direction;          // true = 跨tick方向, false = 境內方向
    }
    
    mapping(bytes32 => LimitOrder[]) public orders;
    mapping(bytes32 => uint256) public orderIndex;
    
    // ============================================
    // 創建限價單
    // ============================================
    
    function createLimitOrder(
        bytes32 poolKey,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        int24 targetTick,
        bool direction  // true = 低於現價卖出, false = 高於現價卖出
    ) external {
        require(amountIn > 0, "Amount must be positive");
        
        // 轉入代幣
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
        
        LimitOrder memory order = LimitOrder({
            owner: msg.sender,
            tokenIn: tokenIn,
            tokenOut: tokenOut,
            amountIn: amountIn,
            amountOut: 0,
            tick: targetTick,
            isActive: true,
            direction: direction
        });
        
        orders[poolKey].push(order);
    }
    
    // ============================================
    // Hook 實現
    // ============================================
    
    function afterSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params,
        BalanceDelta memory swapResult
    ) external override returns (BalanceDelta memory, bytes memory) {
        // 獲取交易後的 tick
        (,, int24 currentTick,,,,) = IUniswapV4Pool(poolKey).slot0();
        
        LimitOrder[] storage poolOrders = orders[poolKey];
        
        for (uint256 i = 0; i < poolOrders.length; i++) {
            LimitOrder storage order = poolOrders[i];
            
            if (!order.isActive) continue;
            
            // 檢查是否觸發條件
            bool triggered = order.direction 
                ? currentTick < order.tick   // 跨tick方向:現價穿過目標tick
                : currentTick > order.tick;  // 境內方向:現價回到目標tick
            
            if (triggered) {
                // 成交!將剩餘流動性以反向訂單形式添加
                uint256 remainingAmount = order.amountIn - order.amountOut;
                
                if (remainingAmount > 0) {
                    // 添加反向流動性(相當於市價成交)
                    _executeMarketOrder(poolKey, order, remainingAmount);
                }
                
                order.isActive = false;
            }
        }
        
        return (swapResult, ""); // 不修改 swap 結果
    }
    
    function _executeMarketOrder(
        bytes32 poolKey,
        LimitOrder storage order,
        uint256 amount
    ) internal {
        // 實現市價成交邏輯
        // 這裡需要與 V4 Pool 交互
        // ...
    }
    
    // ============================================
    // 取消訂單
    // ============================================
    
    function cancelOrder(bytes32 poolKey, uint256 orderId) external {
        LimitOrder storage order = orders[poolKey][orderId];
        require(order.owner == msg.sender, "Not order owner");
        require(order.isActive, "Order already inactive");
        
        // 退還未成交代幣
        uint256 remaining = order.amountIn - order.amountOut;
        if (remaining > 0) {
            IERC20(order.tokenIn).transfer(msg.sender, remaining);
        }
        
        order.isActive = false;
    }
}

3.3 動態費用鉤子

傳統 AMM 的費用是固定的,但你可以用 Hook 實現動態費用——根據波動率、TVL、或者時間段自動調整。

// Dynamic Fee Hook - 動態費用
contract DynamicFeeHook is IHook {
    // ============================================
    // 狀態變數
    // ============================================
    
    // 費用參數
    uint24 public baseFee = 3000;  // 30 bps 基底費用
    uint24 public maxFee = 10000;   // 最高 100 bps
    uint24 public minFee = 500;     // 最低 5 bps
    
    // 波動率追蹤
    mapping(bytes32 => uint256[]) public priceHistory;
    uint256 public constant HISTORY_LENGTH = 100;
    
    // ============================================
    // 計算動態費用
    // ============================================
    
    function calculateFee(bytes32 poolKey) public view returns (uint24) {
        // 計算最近 N 個價格的標準差(波動率代理)
        uint256[] storage history = priceHistory[poolKey];
        
        if (history.length < 10) {
            return baseFee; // 數據不足,返回基費
        }
        
        // 取最近 HISTORY_LENGTH 個數據
        uint256 start = history.length > HISTORY_LENGTH 
            ? history.length - HISTORY_LENGTH 
            : 0;
        
        // 計算收益率標準差
        uint256 sum = 0;
        uint256 sumSq = 0;
        uint256 count = 0;
        
        for (uint256 i = start + 1; i < history.length; i++) {
            // 計算對數收益率
            uint256 priceNow = history[i];
            uint256 pricePrev = history[i - 1];
            uint256 return_pct = ((priceNow - pricePrev) * 1e18) / pricePrev;
            
            sum += return_pct;
            sumSq += return_pct * return_pct / 1e18;
            count++;
        }
        
        // 方差 = E[X^2] - E[X]^2
        uint256 mean = sum / count;
        uint256 variance = (sumSq / count) - (mean * mean / 1e18);
        uint256 stdDev = sqrt(variance); // 手動實現的 sqrt
        
        // 波動率越高,費用越高
        // 使用線性映射,但有上下限
        uint256 volatilityFee = baseFee + uint256(stdDev) * 10; // 調整係數
        
        // 限制在 min-max 範圍內
        if (volatilityFee < minFee) return minFee;
        if (volatilityFee > maxFee) return maxFee;
        
        return uint24(volatilityFee);
    }
    
    // ============================================
    // Hook 實現
    // ============================================
    
    function beforeSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params
    ) external override returns (bool, bytes memory) {
        uint24 dynamicFee = calculateFee(poolKey);
        
        // 返回 fee 和標記
        return (true, abi.encode(dynamicFee));
    }
    
    function afterSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params,
        BalanceDelta memory swapResult
    ) external override returns (BalanceDelta memory, bytes memory) {
        // 記錄價格到歷史
        (uint160 sqrtPriceX96,,,,,,) = IUniswapV4Pool(poolKey).slot0();
        uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) >> 192;
        
        priceHistory[poolKey].push(price);
        
        // 保持歷史長度
        if (priceHistory[poolKey].length > HISTORY_LENGTH + 10) {
            // 刪除舊數據(實際實現可能更精細)
        }
        
        return (swapResult, "");
    }
    
    // ============================================
    // 輔助函數
    // ============================================
    
    function sqrt(uint256 x) internal pure returns (uint256) {
        uint256 z = (x + 1) / 2;
        uint256 y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
        return y;
    }
}

四、安全考量與最佳實踐

4.1 鉤子順序的重要性

多個鉤子同時存在時,執行順序會影響最終結果。比如你想實現「先檢查權限、再計算費用、最後更新狀態」,就必須確保 before 鉤子在 after 鉤子之前執行。

V4 合約的執行順序是固定的:

1. beforeInitialize
2. [實際初始化邏輯]
3. afterInitialize
4. beforeModifyPosition
5. [實際流動性修改]
6. afterModifyPosition
7. beforeSwap
8. [實際 swap]
9. afterSwap
10. beforeDonate
11. [實際捐贈]
12. afterDonate

這意味著 before 鉤子的返回值可以傳遞給 after 鉤子——這在設計複雜邏輯時很有用。

4.2 重入風險

Hook 合約同樣面臨重入攻擊風險。特別是 after 鉤子,它執行時池子狀態可能處於「半更新」狀態,這個時候如果鉤子去調用外部合約,要特別小心。

// 安全實踐:使用 ReentrancyGuard
contract SecureHook is IHook {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status;
    
    modifier nonReentrant() {
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
    
    function afterSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params,
        BalanceDelta memory swapResult
    ) external override nonReentrant returns (BalanceDelta memory, bytes memory) {
        // 在鉤子邏輯中防止重入
        // ...
    }
}

4.3 Gas 限制

鉤子執行的 Gas 成本會直接增加到用戶的交易成本裡。設計鉤子時要注意:

// Gas 優化示例
contract OptimizedHook is IHook {
    // 把常用的查詢結果 cache 住
    mapping(bytes32 => uint256) public feeCache;
    mapping(bytes32 => uint256) public feeCacheExpiry;
    uint256 public constant CACHE_DURATION = 1 hours;
    
    function _getCachedFee(bytes32 poolKey) internal returns (uint24) {
        if (block.timestamp > feeCacheExpiry[poolKey]) {
            // 重新計算
            feeCache[poolKey] = _calculateFee(poolKey);
            feeCacheExpiry[poolKey] = block.timestamp + CACHE_DURATION;
        }
        return uint24(feeCache[poolKey]);
    }
}

4.4 許可清單設計

如果你的 Hook 只允許特定用戶或合約使用,需要在 before 鉤子裡做權限檢查:

// 許可清單鉤子
contract PermissionedHook is IHook {
    // 許可名單
    mapping(address => bool) public allowedSenders;
    
    // 管理者(可以用 DAO 治理)
    address public governance;
    
    modifier onlyAllowed() {
        require(allowedSenders[msg.sender], "Not allowed");
        _;
    }
    
    function beforeSwap(
        address sender,
        bytes32 poolKey,
        SwapParams calldata params
    ) external override onlyAllowed returns (bool, bytes memory) {
        // 許可檢查通過,繼續執行
        return (true, "");
    }
    
    // 治理函數
    function addToWhitelist(address addr) external {
        require(msg.sender == governance, "Not governance");
        allowedSenders[addr] = true;
    }
    
    function removeFromWhitelist(address addr) external {
        require(msg.sender == governance, "Not governance");
        allowedSenders[addr] = false;
    }
}

五、部署與測試

5.1 部署腳本

// scripts/deploy-hook.ts
import { ethers } from 'hardhat';

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log('Deploying hook with account:', deployer.address);
  
  // 部署 TWAMM Hook
  const TWAMMHook = await ethers.getContractFactory('TWAMMHook');
  const twamm = await TWAMMHook.deploy();
  await twamm.deployed();
  console.log('TWAMM Hook deployed to:', twamm.address);
  
  // 計算 Hook 地址(用於創建池子)
  const poolManager = '0x1F98431c8aD98523631AE4a59f267346ea31F984'; // V4 Pool Manager
  
  const hookAddress = await twamm.address;
  const salt = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      ['address'],
      [hookAddress]
    )
  );
  
  console.log('Hook salt:', salt);
  console.log('Use this when creating a pool with this hook');
}

// 創建帶 Hook 的池子
async function createPool() {
  const poolManager = await ethers.getContractAt(
    'IPoolManager',
    '0x1F98431c8aD98523631AE4a59f267346ea31F984'
  );
  
  const hook = '0x...'; // 你的 Hook 地址
  const token0 = '0x...';
  const token1 = '0x...';
  const fee = 3000; // 30 bps
  
  // 初始化參數
  const initParams = {
    sqrtPriceX96: BigInt(1) << 96, // 初始價格 = 1
    tickSpacing: 60,
    hookData: '0x'
  };
  
  // 創建池子
  const tx = await poolManager.initialize(
    token0,
    token1,
    fee,
    hook,
    initParams
  );
  
  console.log('Pool created!', tx.hash);
}

5.2 測試框架

// test/hook.test.ts
import { expect } from 'chai';
import { ethers } from 'hardhat';

describe('TWAMM Hook', () => {
  let hook: any;
  let owner: any;
  
  beforeEach(async () => {
    [owner, ...signers] = await ethers.getSigners();
    
    // 部署 Hook
    const TWAMMHook = await ethers.getContractFactory('TWAMMHook');
    hook = await TWAMMHook.deploy();
    await hook.deployed();
  });
  
  it('should create TWAMM order', async () => {
    const poolKey = ethers.utils.keccak256(
      ethers.utils.defaultAbiCoder.encode(
        ['address', 'address', 'uint24'],
        [token0, token1, fee]
      )
    );
    
    await hook.createOrder(
      poolKey,
      ethers.utils.parseEther('1'), // 每區塊賣 1 ETH
      ethers.utils.parseEther('100'), // 總共 100 ETH
      100 // 持續 100 個區塊
    );
    
    const orders = await hook.orders(poolKey, 0);
    expect(orders.owner).to.equal(owner.address);
    expect(orders.isActive).to.equal(true);
  });
  
  it('should handle swap correctly', async () => {
    // 測試 swap 觸發 TWAMM 執行的場景
    // ...
  });
  
  it('should track price history', async () => {
    // 測試波動率追蹤邏輯
    // ...
  });
});

結語

Uniswap V4 的 Hook 機制打開了 AMM 設計的潘朵拉盒子。以前要實現 TWAMM、限價單、動態費用這些功能,需要自己 fork Uniswap 合約代碼,工作量大到離譜。現在有了 Hook 接口,你只需要實現幾個標準化的回調函數,Uniswap 幫你把基礎設施搞定。

當然,這套機制也不是銀彈。開發 Hook 的門檻不低——你需要深入理解 V4 的執行模型、搞清楚各個鉤子的執行順序、處理 Gas 優化和安全風險。但比起以前那種「把整個合約 fork 下來改」的模式,這已經是巨大的進步了。

我個人最看好的是幾個方向:

  1. MEV 保護:用 Hook 在 swap 前插入驗證邏輯,防止三明治攻擊
  2. 收益聚合:把 LP 頭寸的收益自動再投資
  3. 治理代幣激勵:用 Hook 實現複雜的激勵分配

這些應用場景在以前幾乎不可能實現,或者實現成本極高。V4 讓這一切變得觸手可及。期待看到更多創新的 Hook 設計湧現出來。

參考資源

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!