uniswap v4 hooks标志位
hooks的代码位置在这,它是是组织校验,调用用户创建钩子的类库。
首先看其中定义的常量:
uint160 internal constant ALL_HOOK_MASK = uint160((1 << 14) - 1);uint160 internal constant BEFORE_INITIALIZE_FLAG = 1 << 13;uint160 internal constant AFTER_INITIALIZE_FLAG = 1 << 12;uint160 internal constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 11;uint160 internal constant AFTER_ADD_LIQUIDITY_FLAG = 1 << 10;uint160 internal constant BEFORE_REMOVE_LIQUIDITY_FLAG = 1 << 9;uint160 internal constant AFTER_REMOVE_LIQUIDITY_FLAG = 1 << 8;uint160 internal constant BEFORE_SWAP_FLAG = 1 << 7;uint160 internal constant AFTER_SWAP_FLAG = 1 << 6;uint160 internal constant BEFORE_DONATE_FLAG = 1 << 5;uint160 internal constant AFTER_DONATE_FLAG = 1 << 4;uint160 internal constant BEFORE_SWAP_RETURNS_DELTA_FLAG = 1 << 3;uint160 internal constant AFTER_SWAP_RETURNS_DELTA_FLAG = 1 << 2;uint160 internal constant AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 1;uint160 internal constant AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 0;
我们知道IHooks
接口中定义了 10 个核心方法,分别对应上面的标志位:
标志位 | 描述 | 对应的接口方法 |
---|---|---|
BEFORE_INITIALIZE_FLAG | 初始化前触发 | beforeInitialize |
AFTER_INITIALIZE_FLAG | 初始化后触发 | afterInitialize |
BEFORE_ADD_LIQUIDITY_FLAG | 添加流动性前触发 | beforeAddLiquidity |
AFTER_ADD_LIQUIDITY_FLAG | 添加流动性后触发 | afterAddLiquidity |
BEFORE_REMOVE_LIQUIDITY_FLAG | 移除流动性前触发 | beforeRemoveLiquidity |
AFTER_REMOVE_LIQUIDITY_FLAG | 移除流动性后触发 | afterRemoveLiquidity |
BEFORE_SWAP_FLAG | 交换前触发 | beforeSwap |
AFTER_SWAP_FLAG | 交换后触发 | afterSwap |
BEFORE_DONATE_FLAG | 捐赠前触发 | beforeDonate |
AFTER_DONATE_FLAG | 捐赠后触发 | afterDonate |
这些标志位直接对应 IHooks
接口中的方法,用于标识 Hook 合约是否支持这些方法。
RETURNS_DELTA_FLAG
除了上述 10 个标志位,还有 4 个额外的标志位用于标识扩展功能。这些标志位与返回 Delta 值 的能力相关:
标志位 | 描述 | 扩展功能 |
---|---|---|
BEFORE_SWAP_RETURNS_DELTA_FLAG | 交换前返回 Delta 值 | 返回 BeforeSwapDelta |
AFTER_SWAP_RETURNS_DELTA_FLAG | 交换后返回 Delta 值 | 返回 int256 Delta 值 |
AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG | 添加流动性后返回 Delta 值 | 返回 BalanceDelta |
AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG | 移除流动性后返回 Delta 值 | 返回 BalanceDelta |
这几个RETURNS_DELTA标志位用于标识 Hook 合约是否支持在特定操作后返回一个 Delta 值。Delta 值通常表示某种状态的变化,例如交易金额的调整、流动性变化或其他相关的数值。
举个例子,假设我们有一个 Hook 合约,它在交易(swap)之前动态调整交易金额。这个 Hook 合约会返回一个 Delta 值,用于修改交易的输入金额。
contract ExampleHook is IHooks {function beforeSwap(address sender,PoolKey calldata key,IPoolManager.SwapParams calldata params,bytes calldata hookData) external returns (bytes4, int256 delta) {// 返回一个 Delta 值,用于调整交易金额delta = params.amountSpecified / 10; // 调整金额的 10%return (this.beforeSwap.selector, delta);}
}
此时我们创建合约的时候就要将BEFORE_SWAP_RETURNS_DELTA_FLAG设置为1。
这个时候hooks库在调用用户钩子的beforeSwap方法后,就可以从Hook 返回的数据中解析出 Delta 值并正确调整流动性提供者的余额。我们可以看到hooks库的beforeSwap方法对于BEFORE_SWAP_RETURNS_DELTA_FLAG的运用。
function beforeSwap(IHooks self, PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)internalreturns (int256 amountToSwap, BeforeSwapDelta hookReturn, uint24 lpFeeOverride)
{amountToSwap = params.amountSpecified;if (self.hasPermission(BEFORE_SWAP_FLAG)) {bytes memory result = callHook(self, abi.encodeCall(IHooks.beforeSwap, (msg.sender, key, params, hookData)));// 检查返回数据的长度是否符合预期if (result.length != 96) InvalidHookResponse.selector.revertWith();// 如果启用了 RETURNS_DELTA 标志位,则解析 Delta 值if (self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) {hookReturn = BeforeSwapDelta.wrap(result.parseReturnDelta());// 使用 Delta 值调整交易金额int128 hookDeltaSpecified = hookReturn.getSpecifiedDelta();if (hookDeltaSpecified != 0) {bool exactInput = amountToSwap < 0;amountToSwap += hookDeltaSpecified;// 确保交易类型(exact input/output)不发生变化if (exactInput ? amountToSwap > 0 : amountToSwap < 0) {HookDeltaExceedsSwapAmount.selector.revertWith();}}}}
}
如果我们在 Hooks 合约中,改变了输入的交易金额但是有并没有返回delta值,或者返回了delta值但是没有设置相应的RETURNS_DELTA_FLAG
,则会导致没有调整预期的状态,致使交易失败。所以当我们在钩子中动态调整了交易金额,则务必返回delta,并设置相应的RETURNS_DELTA_FLAG。
我们看一下
Hooks类库中的hasPermission方法:
function hasPermission(IHooks self, uint160 flag) internal pure returns (bool) {return uint160(address(self)) & flag != 0;}
钩子合约的发布
可以看出对标志位的校验是通过用户钩子合约的地址进行的,也就是说,当我们发布一个hooks,其地址承载力此钩子实现了那些方法的标志。主要逻辑我们可以参考一下DeployHooks的脚本文件:
这里演示了用户如何发布一个自定义钩子合约。
pragma solidity ^0.8.19;import "forge-std/Script.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {HookMiner} from "../src/utils/HookMiner.sol";/// @dev Replace import with your own hook
import {MockCounterHook} from "../test/mocks/MockCounterHook.sol";/// @notice Mines the address and deploys the Counter.sol Hook contract
contract DeployHookScript is Script {address constant CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C);/// @dev Replace with the desired PoolManager on its corresponding chainIPoolManager constant POOLMANAGER = IPoolManager(address(0xE03A1074c86CFeDd5C142C4F04F1a1536e203543));function setUp() public {}function run() public {// 钩子合约的地址必须编码特定的标志。uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG| Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG);bytes memory constructorArgs = abi.encode(POOLMANAGER);// Mine a salt that will produce a hook address with the correct flags(address hookAddress, bytes32 salt) =HookMiner.find(CREATE2_DEPLOYER, flags, type(MockCounterHook).creationCode, constructorArgs);// Deploy the hook using CREATE2vm.broadcast();MockCounterHook counter = new MockCounterHook{salt: salt}(IPoolManager(POOLMANAGER));require(address(counter) == hookAddress, "CounterScript: hook address mismatch");}
}
我们通过修改flags后面的标志位来设置自定义合约支持的钩子方法。
接下来会通过HookMiner.find方法计算符合用户自定义标志位的地址和salt
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";/// @title HookMiner
/// @notice a minimal library for mining hook addresses
library HookMiner {// mask to slice out the bottom 14 bit of the addressuint160 constant FLAG_MASK = Hooks.ALL_HOOK_MASK; // 0000 ... 0000 0011 1111 1111 1111// Maximum number of iterations to find a salt, avoid infinite loops or MemoryOOG// (arbitrarily set)uint256 constant MAX_LOOP = 160_444;/// @notice Find a salt that produces a hook address with the desired `flags`/// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address/// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy)/// @param flags The desired flags for the hook address. Example `uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | ...)`/// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode`/// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))`/// @return (hookAddress, salt) The hook deploys to `hookAddress` when using `salt` with the syntax: `new Hook{salt: salt}(<constructor arguments>)`function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs)internalviewreturns (address, bytes32){flags = flags & FLAG_MASK; // mask for only the bottom 14 bitsbytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs);address hookAddress;for (uint256 salt; salt < MAX_LOOP; salt++) {hookAddress = computeAddress(deployer, salt, creationCodeWithArgs);// if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a matchif (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) {return (hookAddress, bytes32(salt));}}revert("HookMiner: could not find salt");}/// @notice Precompute a contract address deployed via CREATE2/// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address/// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy)/// @param salt The salt used to deploy the hook/// @param creationCodeWithArgs The creation code of a hook contract, with encoded constructor arguments appended. Example: `abi.encodePacked(type(Counter).creationCode, abi.encode(constructorArg1, constructorArg2))`function computeAddress(address deployer, uint256 salt, bytes memory creationCodeWithArgs)internalpurereturns (address hookAddress){return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), deployer, salt, keccak256(creationCodeWithArgs))))));}
}
方法并不难,就是不断地调整salt生成address,直到address的地位,符合用户设置的flag为止。
再看下发布合约的最后一步:
vm.broadcast();
MockCounterHook counter = new MockCounterHook{salt: salt}(IPoolManager(POOLMANAGER));
这里的new MockCounterHook{salt: salt}是solidity的语法糖,这么写会隐式的调用create2方法创建合约,而 Foundry 的脚本中,默认情况下操作是模拟执行的,并不会发布到链上。在调用 vm.broadcast();
之后,所有的合约调用或部署操作都会被视为真实交易,并广播到链上。