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,提前计算部署地址,直到找到一个低位满足权限要求的地址。
白名单控制目标
目标很简单:
- 部署者作为
owner。 owner默认进入白名单。- 只有白名单里的钱包可以给池子加流动性。
- 非白名单地址调用加池时,在
beforeAddLiquidity阶段直接revert。 - 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
部署流程大概是:
- 编译 hook 合约,得到 creation bytecode。
- ABI encode 构造参数,例如
poolManager和initialOwner。 - 拼出完整
initCode。 - 枚举
salt,计算预测地址。 - 找到满足
0x0800权限位的地址。 - 把
salt + initCode发给CREATE2factory。 - 部署后检查:
- 地址是否有 code;
owner()是否正确;whitelist(owner)是否为true;poolManager()是否正确;address & 0x3fff是否等于0x0800。
白名单维护
部署后,owner 可以单独添加地址:
setWhitelist(account, true)
也可以批量添加:
setWhitelistBatch(accounts, true)
建议维护一份本地白名单文件,记录:
- 地址;
- 来源;
- 是否已经同步到链上。
后续新增地址时,只需要添加新行,然后让批量脚本只提交还没同步的地址即可。
适用场景与限制
白名单流动性池适合一些特殊策略场景,例如:
- 项目方想控制早期 LP;
- 特定钱包或策略地址才能加池;
- 避免外部 LP 干扰价格区间、手续费分配或做市策略;
- 配合激励、空投、内测池等权限系统。
但它不是无风险优势。
白名单池只有在特定情况下才可能赚取超额利润。对于成熟的流动性市场,深度通常来自大量 LP 的共同参与。单个地址或少数地址加池,很难长期比过一个开放市场里大量资金共同形成的深度和价格效率。
换句话说,白名单 hook 更适合“控制流动性结构”,而不是天然保证更高收益。它是一种权限工具,不是收益承诺。
小结
Uniswap V4 hook 让池子的权限控制和策略定制变得更灵活。
通过只启用 beforeAddLiquidity,并在 hook 中维护白名单,我们可以实现一个相对清晰的控制模型:Swap 保持开放,但加池需要授权。
这个设计适合需要控制 LP 结构的场景,但不应该被理解为收益增强工具。它真正提供的是权限边界,以及围绕流动性管理的更细粒度控制能力。