Skip to content
落叶的博客
Go back

如何实现 Uniswap V4 流动性的白名单添加控制

Table of contents

Open Table of contents

Hook 的基本思路

V4 的 hook 不是随便部署一个合约地址就能用。

Hook 合约地址的低位会编码它启用的权限。也就是说,某个 hook 是否启用 beforeSwap、afterSwap、beforeAddLiquidity 等能力,不只是由合约代码决定,还由合约地址本身决定。

比如 beforeAddLiquidity 对应的权限位是:

0x0800

所以如果一个 hook 只想拦截“添加流动性前”的行为,那么它的部署地址必须满足:

uint160(hookAddress) & 0x3fff == 0x0800

这也是为什么部署 hook 时通常需要 CREATE2:通过不断尝试不同的 salt,提前计算部署地址,直到找到一个低位满足权限要求的地址。

白名单控制目标

目标很简单:

  1. 部署者作为 owner。
  2. owner 默认进入白名单。
  3. 只有白名单里的钱包可以给池子加流动性。
  4. 非白名单地址调用加池时,在 beforeAddLiquidity 阶段直接 revert。
  5. Swap 不受影响,因为我们没有启用 beforeSwap / afterSwap。

核心思路是:池子添加流动性前,PoolManager 会调用 hook 的 beforeAddLiquidity;hook 在这里检查最终发起交易的钱包是否在白名单中。如果不在,就拒绝本次添加流动性的操作。

核心 Solidity 实现

下面是一份简化版实现,用来展示主要逻辑:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract WhitelistBeforeAddLiquidityHook {
    struct PoolKey {
        address currency0;
        address currency1;
        uint24 fee;
        int24 tickSpacing;
        address hooks;
    }

    struct ModifyLiquidityParams {
        int24 tickLower;
        int24 tickUpper;
        int256 liquidityDelta;
        bytes32 salt;
    }

    uint160 public constant HOOK_FLAGS_MASK = 0x3fff;
    uint160 public constant BEFORE_ADD_LIQUIDITY_FLAG = 0x0800;

    address public immutable poolManager;
    address public owner;

    mapping(address account => bool allowed) public whitelist;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event WhitelistUpdated(address indexed account, bool allowed);

    error InvalidAddress();
    error InvalidHookAddress(address hook, uint160 expectedFlag);
    error NotOwner();
    error NotPoolManager();
    error NotWhitelisted(address origin);

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    constructor(address poolManager_, address initialOwner) {
        if (poolManager_ == address(0)) revert InvalidAddress();
        if (initialOwner == address(0)) revert InvalidAddress();

        if ((uint160(address(this)) & HOOK_FLAGS_MASK) != BEFORE_ADD_LIQUIDITY_FLAG) {
            revert InvalidHookAddress(address(this), BEFORE_ADD_LIQUIDITY_FLAG);
        }

        poolManager = poolManager_;
        owner = initialOwner;

        whitelist[initialOwner] = true;

        emit OwnershipTransferred(address(0), initialOwner);
        emit WhitelistUpdated(initialOwner, true);
    }

    function setWhitelist(address account, bool allowed) external onlyOwner {
        if (account == address(0)) revert InvalidAddress();
        whitelist[account] = allowed;
        emit WhitelistUpdated(account, allowed);
    }

    function setWhitelistBatch(address[] calldata accounts, bool allowed) external onlyOwner {
        for (uint256 i = 0; i < accounts.length; i++) {
            address account = accounts[i];
            if (account == address(0)) revert InvalidAddress();

            whitelist[account] = allowed;
            emit WhitelistUpdated(account, allowed);
        }
    }

    function beforeAddLiquidity(
        address,
        PoolKey calldata,
        ModifyLiquidityParams calldata,
        bytes calldata
    ) external view returns (bytes4) {
        if (msg.sender != poolManager) revert NotPoolManager();

        if (!whitelist[tx.origin]) {
            revert NotWhitelisted(tx.origin);
        }

        return WhitelistBeforeAddLiquidityHook.beforeAddLiquidity.selector;
    }
}

这里有一个实现细节:检查的是 tx.origin,也就是最终发起交易的钱包地址。

这样即使用户通过 PositionManager、路由合约或前端工具间接加池,最终钱包仍然必须在白名单里。

如果业务更偏向合约钱包或账户抽象,也可以改成检查 sender,或者同时支持 sender 和 tx.origin。但这样权限语义会变化,需要谨慎设计。

为什么要用 CREATE2

V4 hook 地址必须匹配权限位,所以普通部署方式不可控。

使用 CREATE2 的原因,是它可以提前计算合约地址:

address = keccak256(
  0xff ++ factory ++ salt ++ keccak256(initCode)
)

只要不断尝试不同的 salt,就可以找到一个地址满足:

address & 0x3fff == 0x0800

实际部署时可以使用通用 CREATE2 factory:

0x4e59b44847b379578588920cA78FbF26c0B4956C

部署流程大概是:

  1. 编译 hook 合约,得到 creation bytecode。
  2. ABI encode 构造参数,例如 poolManager 和 initialOwner。
  3. 拼出完整 initCode。
  4. 枚举 salt,计算预测地址。
  5. 找到满足 0x0800 权限位的地址。
  6. 把 salt + initCode 发给 CREATE2 factory。
  7. 部署后检查:
    • 地址是否有 code;
    • owner() 是否正确;
    • whitelist(owner) 是否为 true;
    • poolManager() 是否正确;
    • address & 0x3fff 是否等于 0x0800。

白名单维护

部署后,owner 可以单独添加地址:

setWhitelist(account, true)

也可以批量添加:

setWhitelistBatch(accounts, true)

建议维护一份本地白名单文件,记录:

后续新增地址时,只需要添加新行,然后让批量脚本只提交还没同步的地址即可。

适用场景与限制

白名单流动性池适合一些特殊策略场景,例如:

但它不是无风险优势。

白名单池只有在特定情况下才可能赚取超额利润。对于成熟的流动性市场,深度通常来自大量 LP 的共同参与。单个地址或少数地址加池,很难长期比过一个开放市场里大量资金共同形成的深度和价格效率。

换句话说,白名单 hook 更适合“控制流动性结构”,而不是天然保证更高收益。它是一种权限工具,不是收益承诺。

小结

Uniswap V4 hook 让池子的权限控制和策略定制变得更灵活。

通过只启用 beforeAddLiquidity,并在 hook 中维护白名单,我们可以实现一个相对清晰的控制模型:Swap 保持开放,但加池需要授权。

这个设计适合需要控制 LP 结构的场景,但不应该被理解为收益增强工具。它真正提供的是权限边界,以及围绕流动性管理的更细粒度控制能力。


Share this post:

Previous Post
为什么我最后选择了 Flarum:一次轻量论坛的技术选型
Next Post
从零部署个人博客:一次轻量化技术选型记录