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)
internal
returns (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 chain
IPoolManager 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 CREATE2
vm.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 address
uint160 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)
internal
view
returns (address, bytes32)
{
flags = flags & FLAG_MASK; // mask for only the bottom 14 bits
bytes 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 match
if (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)
internal
pure
returns (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();
之后,所有的合约调用或部署操作都会被视为真实交易,并广播到链上。