uniswap v4 合约解析1 pool初始化
当我们创建一个pool时,其入口函数位PoolManager合约的initialize方法:
代码如下:
/// @inheritdoc IPoolManagerfunction initialize(PoolKey memory key, uint160 sqrtPriceX96) external noDelegateCall returns (int24 tick) {// see TickBitmap.sol for overflow conditions that can arise from tick spacing being too largeif (key.tickSpacing > MAX_TICK_SPACING) TickSpacingTooLarge.selector.revertWith(key.tickSpacing);if (key.tickSpacing < MIN_TICK_SPACING) TickSpacingTooSmall.selector.revertWith(key.tickSpacing);if (key.currency0 >= key.currency1) {CurrenciesOutOfOrderOrEqual.selector.revertWith(Currency.unwrap(key.currency0), Currency.unwrap(key.currency1));}if (!key.hooks.isValidHookAddress(key.fee)) Hooks.HookAddressNotValid.selector.revertWith(address(key.hooks));uint24 lpFee = key.fee.getInitialLPFee();key.hooks.beforeInitialize(key, sqrtPriceX96);PoolId id = key.toId();tick = _pools[id].initialize(sqrtPriceX96, lpFee);// event is emitted before the afterInitialize call to ensure events are always emitted in order// emit all details of a pool key. poolkeys are not saved in storage and must always be provided by the caller// the key's fee may be a static fee or a sentinel to denote a dynamic fee.emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks, sqrtPriceX96, tick);key.hooks.afterInitialize(key, sqrtPriceX96, tick);}
校验
第一步是校验
if (key.tickSpacing > MAX_TICK_SPACING) TickSpacingTooLarge.selector.revertWith(key.tickSpacing);
if (key.tickSpacing < MIN_TICK_SPACING) TickSpacingTooSmall.selector.revertWith(key.tickSpacing);
根据MAX_TICK_SPACING和MIN_TICK_SPACING的定义,tickSpacing的范围是[1, 32767]
此提供了足够的灵活性,允许开发者根据需求选择适合的tickSpacing
。
-
较小的
tickSpacing
刻度间距更密集,允许更精细的价格范围。适合高精度的交易场景,但会增加存储和计算成本。 -
较大的
tickSpacing
刻度间距更稀疏,减少存储和计算成本。适合低精度的交易场景,但可能限制流动性提供者的灵活性。
if (key.currency0 >= key.currency1) {CurrenciesOutOfOrderOrEqual.selector.revertWith(Currency.unwrap(key.currency0), Currency.unwrap(key.currency1));
}
这段代码的作用是对 key.currency0
和 key.currency1
进行验证,确保它们的顺序正确且不相等。如果 key.currency0
大于或等于 key.currency1
,则抛出 CurrenciesOutOfOrderOrEqual
错误。
用户在手动调用合约创建交易对时,需要注意poolKey参数里代币地址的大小顺序。
Currency.unwrap(key.currency0)
将 Currency
类型的值解包为其底层的原始值address便于输出和调试。
if (!key.hooks.isValidHookAddress(key.fee)) Hooks.HookAddressNotValid.selector.revertWith(address(key.hooks));
这里主要是对pool中包含的钩子地址进行校验和交易费用进行校验,代码如下:
/// @notice Ensures that the hook address includes at least one hook flag or dynamic fees, or is the 0 address/// @param self The hook to verify/// @param fee The fee of the pool the hook is used with/// @return bool True if the hook address is validfunction isValidHookAddress(IHooks self, uint24 fee) internal pure returns (bool) {// The hook can only have a flag to return a hook delta on an action if it also has the corresponding action flagif (!self.hasPermission(BEFORE_SWAP_FLAG) && self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) return false;if (!self.hasPermission(AFTER_SWAP_FLAG) && self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)) return false;if (!self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG) && self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG)){return false;}if (!self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG)&& self.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)) return false;// If there is no hook contract set, then fee cannot be dynamic// If a hook contract is set, it must have at least 1 flag set, or have a dynamic feereturn address(self) == address(0)? !fee.isDynamicFee(): (uint160(address(self)) & ALL_HOOK_MASK > 0 || fee.isDynamicFee());}
代码比较简单,但是需要先了解一下hooks地址的标志位
这里需要介绍一下动态费用,我们知道uniswap v3支持的交易费用为0.01%
、0.05%
、0.3%
和 1%,而v4在此基础上则增加了动态费用,支持根据交易情况在钩子里动态调整交易费用,也就是说当我们把交易费用设置为0x800000(0x代表16进制),转换成2进制为0b100000000000000000000000,则代表当前的交易费需要通过钩子调整,所以如用户设置了动态费用但是没有设置任何hooks,则校验失败。
uint24 lpFee = key.fee.getInitialLPFee();
这里是获取用户的设置的交易费,如果是动态费用则返回0,同时这里也包含了一步校验,如果费用超过100%,会抛出异常。
/// @notice An lp fee of exactly 0b1000000... signals a dynamic fee pool. This isn't a valid static fee as it is > MAX_LP_FEEuint24 public constant DYNAMIC_FEE_FLAG = 0x800000;/// @notice the second bit of the fee returned by beforeSwap is used to signal if the stored LP fee should be overridden in this swap// only dynamic-fee pools can return a fee via the beforeSwap hookuint24 public constant OVERRIDE_FEE_FLAG = 0x400000;/// @notice mask to remove the override fee flag from a fee returned by the beforeSwaphookuint24 public constant REMOVE_OVERRIDE_MASK = 0xBFFFFF;/// @notice the lp fee is represented in hundredths of a bip, so the max is 100%uint24 public constant MAX_LP_FEE = 1000000; function isDynamicFee(uint24 self) internal pure returns (bool) {return self == DYNAMIC_FEE_FLAG;}/// @notice returns true if an LP fee is valid, aka not above the maximum permitted fee/// @param self The fee to check/// @return bool True of the fee is validfunction isValid(uint24 self) internal pure returns (bool) {return self <= MAX_LP_FEE;}/// @notice validates whether an LP fee is larger than the maximum, and reverts if invalid/// @param self The fee to validatefunction validate(uint24 self) internal pure {if (!self.isValid()) LPFeeTooLarge.selector.revertWith(self);}/// @notice gets and validates the initial LP fee for a pool. Dynamic fee pools have an initial fee of 0./// @dev if a dynamic fee pool wants a non-0 initial fee, it should call `updateDynamicLPFee` in the afterInitialize hook/// @param self The fee to get the initial LP from/// @return initialFee 0 if the fee is dynamic, otherwise the fee (if valid)function getInitialLPFee(uint24 self) internal pure returns (uint24) {// the initial fee for a dynamic fee pool is 0if (self.isDynamicFee()) return 0;self.validate();return self;}
初始化
接下来就是正式创建交易对
key.hooks.beforeInitialize(key, sqrtPriceX96);PoolId id = key.toId();tick = _pools[id].initialize(sqrtPriceX96, lpFee);// event is emitted before the afterInitialize call to ensure events are always emitted in order
// emit all details of a pool key. poolkeys are not saved in storage and must always be provided by the caller
// the key's fee may be a static fee or a sentinel to denote a dynamic fee.
emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks, sqrtPriceX96, tick);key.hooks.afterInitialize(key, sqrtPriceX96, tick);
创建交易对前后需要执行相应的钩子beforeInitialize和afterInitialize
tick = _pools[id].initialize(sqrtPriceX96, lpFee);这部分是真正初始化交易对。
_pools
是一个映射(mapping
),在 Solidity 中,映射的默认行为是:如果访问一个未初始化的键(id
),它会返回该键对应值类型的默认值。
根据_pools的定义
mapping(PoolId id => Pool.State) internal _pools;
如果 id
对应的池(Pool.State
)从未被初始化,_pools[id]
会返回一个默认值(即 Pool.State
的默认状态)。
struct State {Slot0 slot0;uint256 feeGrowthGlobal0X128;uint256 feeGrowthGlobal1X128;uint128 liquidity;mapping(int24 tick => TickInfo) ticks;mapping(int16 wordPos => uint256) tickBitmap;mapping(bytes32 positionKey => Position.State) positions;}
相应的基本类型和结构体(slot0)中的基本类型都会初始化为0
pool库通过using Pool for State
将 Pool
库中的函数与 State
结构体相关联,所以_pools[id].initialize(sqrtPriceX96, lpFee);会直接调用pool的initialize方法。
function initialize(State storage self, uint160 sqrtPriceX96, uint24 lpFee) internal returns (int24 tick) {if (self.slot0.sqrtPriceX96() != 0) PoolAlreadyInitialized.selector.revertWith();tick = TickMath.getTickAtSqrtPrice(sqrtPriceX96);// the initial protocolFee is 0 so doesn't need to be setself.slot0 = Slot0.wrap(bytes32(0)).setSqrtPriceX96(sqrtPriceX96).setTick(tick).setLpFee(lpFee);}
- 第一行是校验
- 第二行是通过初始的价格获取相应的tick,
- 第三行设置当前pool的价格和tick。
第二行代码的实现是最复杂的,详见:uniswap getTickAtSqrtPrice 方法解析