这一章我们分析pool中流动性的修改过程,
在阅读本章之前一定要先通读uniswap v3/v4 中pool的状态管理
在阅读本章之前一定要先通读uniswap v3/v4 中pool的状态管理
在阅读本章之前一定要先通读uniswap v3/v4 中pool的状态管理
代码位置如下:
这个方法提供增加或减少流动性的功能,并计算用户应得的手续费(feesAccrued
)和流动性变化(callerDelta
),同时支持钩子(Hooks)机制,允许在流动性修改前后执行自定义逻辑。
先看代码:
function modifyLiquidity(
PoolKey memory key,
IPoolManager.ModifyLiquidityParams memory params,
bytes calldata hookData
) external onlyWhenUnlocked noDelegateCall returns (BalanceDelta callerDelta, BalanceDelta feesAccrued) {
PoolId id = key.toId();
{
Pool.State storage pool = _getPool(id);
pool.checkPoolInitialized();
key.hooks.beforeModifyLiquidity(key, params, hookData);
BalanceDelta principalDelta;
(principalDelta, feesAccrued) = pool.modifyLiquidity(
Pool.ModifyLiquidityParams({
owner: msg.sender,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
liquidityDelta: params.liquidityDelta.toInt128(),
tickSpacing: key.tickSpacing,
salt: params.salt
})
);
// fee delta and principal delta are both accrued to the caller
callerDelta = principalDelta + feesAccrued;
}
// event is emitted before the afterModifyLiquidity call to ensure events are always emitted in order
emit ModifyLiquidity(id, msg.sender, params.tickLower, params.tickUpper, params.liquidityDelta, params.salt);
BalanceDelta hookDelta;
(callerDelta, hookDelta) = key.hooks.afterModifyLiquidity(key, params, callerDelta, feesAccrued, hookData);
// if the hook doesn't have the flag to be able to return deltas, hookDelta will always be 0
if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks));
_accountPoolBalanceDelta(key, callerDelta, msg.sender);
}
入参
-
PoolKey memory key
:
表示池的唯一标识,这里面池的基本信息(代币对、手续费、tick 间距等)。通过 key.toId()
转换为池的唯一 ID。
-
具体参数如下:IPoolManager.ModifyLiquidityParams memory params
:salt
:用于唯一标识流动性位置的附加参数。liquidityDelta
:流动性变化量(正值表示增加流动性,负值表示减少流动性)。tickUpper
:流动性范围的上界。tickLower
:流动性范围的下界。
-
钩子(Hooks)所需的附加数据,允许在流动性修改前后执行自定义逻辑。bytes calldata hookData
:
这些参数都比较简单,唯一的需要解释的是salt参数,这里包含一个v4版本相对v3版本的升级,在v3版本中,增减流动性只需要提供tick的范围,合约根据发送者的地址和tick范围就可以定位到用户所拥有的流动性,并对其做增减。但是我们试想这样一个场景。用户可能希望在相同的价格范围内应用不同的策略,一部分资金用于长期流动性提供,另一部分用于短期套利。这样的话在同一个tick范围内统一做流动性管理就不太合适了。所以uniswap v4版本在此基础上对于同一用户,同一tick范围的流动性又做了一层分区,允许用户在不同的分区采取不同的交易策略,而这些分区就是用salt来区分位置的。需要注意的是salt
是一个 用户定义的参数,Uniswap 并不会自动生成 salt
。用户需要自己管理它。
前面几行代码比较简单
- Pool.State storage pool = _getPool(id); //获取相应的pool
- pool.checkPoolInitialized();//校验当前pool是否初始化
- key.hooks.beforeModifyLiquidity(key, params, hookData);//执行更新流动性的相应钩子
更新pool
这个方法的核心步骤就是
(principalDelta, feesAccrued) = pool.modifyLiquidity( Pool.ModifyLiquidityParams({
owner: msg.sender,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
liquidityDelta: params.liquidityDelta.toInt128(),
tickSpacing: key.tickSpacing,
salt: params.salt
}));
我们仔细分析一下:
function modifyLiquidity(State storage self, ModifyLiquidityParams memory params)
internal
returns (BalanceDelta delta, BalanceDelta feeDelta)
{
int128 liquidityDelta = params.liquidityDelta;
int24 tickLower = params.tickLower;
int24 tickUpper = params.tickUpper;
checkTicks(tickLower, tickUpper);
{
ModifyLiquidityState memory state;
// if we need to update the ticks, do it
if (liquidityDelta != 0) {
(state.flippedLower, state.liquidityGrossAfterLower) =
updateTick(self, tickLower, liquidityDelta, false);
(state.flippedUpper, state.liquidityGrossAfterUpper) = updateTick(self, tickUpper, liquidityDelta, true);
// `>` and `>=` are logically equivalent here but `>=` is cheaper
if (liquidityDelta >= 0) {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(params.tickSpacing);
if (state.liquidityGrossAfterLower > maxLiquidityPerTick) {
TickLiquidityOverflow.selector.revertWith(tickLower);
}
if (state.liquidityGrossAfterUpper > maxLiquidityPerTick) {
TickLiquidityOverflow.selector.revertWith(tickUpper);
}
}
if (state.flippedLower) {
self.tickBitmap.flipTick(tickLower, params.tickSpacing);
}
if (state.flippedUpper) {
self.tickBitmap.flipTick(tickUpper, params.tickSpacing);
}
}
{
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
getFeeGrowthInside(self, tickLower, tickUpper);
Position.State storage position = self.positions.get(params.owner, tickLower, tickUpper, params.salt);
(uint256 feesOwed0, uint256 feesOwed1) =
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
// Fees earned from LPing are calculated, and returned
feeDelta = toBalanceDelta(feesOwed0.toInt128(), feesOwed1.toInt128());
}
// clear any tick data that is no longer needed
if (liquidityDelta < 0) {
if (state.flippedLower) {
clearTick(self, tickLower);
}
if (state.flippedUpper) {
clearTick(self, tickUpper);
}
}
}
if (liquidityDelta != 0) {
Slot0 _slot0 = self.slot0;
(int24 tick, uint160 sqrtPriceX96) = (_slot0.tick(), _slot0.sqrtPriceX96());
if (tick < tickLower) {
// current tick is below the passed range; liquidity can only become in range by crossing from left to
// right, when we'll need _more_ currency0 (it's becoming more valuable) so user must provide it
delta = toBalanceDelta(
SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), liquidityDelta
).toInt128(),
0
);
} else if (tick < tickUpper) {
delta = toBalanceDelta(
SqrtPriceMath.getAmount0Delta(sqrtPriceX96, TickMath.getSqrtPriceAtTick(tickUpper), liquidityDelta)
.toInt128(),
SqrtPriceMath.getAmount1Delta(TickMath.getSqrtPriceAtTick(tickLower), sqrtPriceX96, liquidityDelta)
.toInt128()
);
self.liquidity = LiquidityMath.addDelta(self.liquidity, liquidityDelta);
} else {
// current tick is above the passed range; liquidity can only become in range by crossing from right to
// left, when we'll need _more_ currency1 (it's becoming more valuable) so user must provide it
delta = toBalanceDelta(
0,
SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), liquidityDelta
).toInt128()
);
}
}
}
该方法的入参前面分析过这里不再赘述,出参有两个
-
BalanceDelta delta
:表示池的余额变化(即用户需要存入或取出的代币数量)。 -
BalanceDelta feeDelta
:表示流动性提供者在该位置的手续费收益。
更新tick
前几行代码都是取参数和校验,我们直接从updateTick方法看起:
function updateTick(State storage self, int24 tick, int128 liquidityDelta, bool upper)
internal
returns (bool flipped, uint128 liquidityGrossAfter)
{
TickInfo storage info = self.ticks[tick];
uint128 liquidityGrossBefore = info.liquidityGross;
int128 liquidityNetBefore = info.liquidityNet;
liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);
flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0);
if (liquidityGrossBefore == 0) {
// by convention, we assume that all growth before a tick was initialized happened _below_ the tick
if (tick <= self.slot0.tick()) {
info.feeGrowthOutside0X128 = self.feeGrowthGlobal0X128;
info.feeGrowthOutside1X128 = self.feeGrowthGlobal1X128;
}
}
// when the lower (upper) tick is crossed left to right, liquidity must be added (removed)
// when the lower (upper) tick is crossed right to left, liquidity must be removed (added)
int128 liquidityNet = upper ? liquidityNetBefore - liquidityDelta : liquidityNetBefore + liquidityDelta;
assembly ("memory-safe") {
// liquidityGrossAfter and liquidityNet are packed in the first slot of `info`
// So we can store them with a single sstore by packing them ourselves first
sstore(
info.slot,
// bitwise OR to pack liquidityGrossAfter and liquidityNet
or(
// Put liquidityGrossAfter in the lower bits, clearing out the upper bits
and(liquidityGrossAfter, 0xffffffffffffffffffffffffffffffff),
// Shift liquidityNet to put it in the upper bits (no need for signextend since we're shifting left)
shl(128, liquidityNet)
)
)
}
}
这个方法有4个入参
State storage self
:pool的当前状态,详见之前的文章-
int24 tick
:要更新的 tick 的索引。 -
int128 liquidityDelta
:流动性变化量,负值表示减少流动性。正值表示增加流动性。 -
bool upper
:该 tick 是流动性范围的上界还是下界
2个出参
-
bool flipped
:表示 tick 是否从未初始化变为已初始化,或从已初始化变为未初始化:false
:tick 的状态未发生变化。true
:tick 的状态发生了变化。
-
uint128 liquidityGrossAfter
:更新后的 tick 的总流动性(liquidityGross
)。
我们稍微回忆一下之前文章提到过tick中的两个属性:
liquidityGross
:tick 的当前总流动性(所有引用当前tick的流动性总和)。liquidityNet
:tick 的当前净流动性(当价格穿过当前tick时流动性需要增加或减少的净值)。
liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);
首先计算变化后的总流动性,不多做解释。
flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0);
这行代码的意思是
- 如果
liquidityGrossAfter
为0
,而liquidityGrossBefore
不为0
,表示 tick 从已初始化变为未初始化。此时flipped=true - 如果
liquidityGrossAfter
不为0
,而liquidityGrossBefore
为0
,表示 tick 从未初始化变为已初始化。此时flipped=true
接下来是tick中feeGrowthOutside属性的初始化
if (liquidityGrossBefore == 0) {
// by convention, we assume that all growth before a tick was initialized happened _below_ the tick
if (tick <= self.slot0.tick()) {
info.feeGrowthOutside0X128 = self.feeGrowthGlobal0X128;
info.feeGrowthOutside1X128 = self.feeGrowthGlobal1X128;
}
}
如果 tick 是第一次被初始化(liquidityGrossBefore == 0
),需要设置 feeGrowthOutside。
如果 tick 位于当前价格(self.slot0.tick()
)的左侧,则认为 tick 的外部手续费增长等于当前的全局手续费增长(feeGrowthGlobal
)。
这步处理其实很难理解,所以官方在这里加了一行注释
by convention, we assume that all growth before a tick was initialized happened _below_ the tick
按照惯例,我们假设所有在tick初始化之前的手续费都发生在该tick“外部”
既然是假设,也就是说当前的手续费总量,即可能发生在tick的外部,也可能有一部分发生在内部,直接把feeGrowthGlobal作为外部手续费赋值给feeGrowthOutside实际上是不对的!但是我们要明确一点:feeGrowthOutside这个参数记录的其实是从tick初始化的那一刻起的外部手续费增量!也就是说之前在该tick外部产生的手续费由于其没有初始化,所以与其无关。当下一次价格穿过该tick时,会有这么一步操作:
feeGrowthOutside = feeGrowthGloba - feeGrowthOutside
这样就可以抵消tick初始化之前产生的手续费,只记录初始化后的手续费增量。
价格穿过tick时,更新tick上feeGrowthOutside的代码如下:
function crossTick(State storage self, int24 tick, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128)
internal
returns (int128 liquidityNet)
{
unchecked {
TickInfo storage info = self.ticks[tick];
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
liquidityNet = info.liquidityNet;
}
}
同样的道理,当前初始化tick在pool的currentTick左边时不进行feeGrowthOutside赋值,并不代表没有手续费产生在tick的外部,而是在没有初始化之前不管产生多少手续费都与其无关,只要下一次穿过它的时候可以正确计算即可。(怀疑没几个人能看懂!!!)
接下来是更新净流动性
int128 liquidityNet = upper ? liquidityNetBefore - liquidityDelta : liquidityNetBefore + liquidityDelta;
- 如果更新的是上边界
tick
(upper == true
),则从净流动性中减去liquidityDelta
。 - 如果更新的是下边界
tick
(upper == false
),则向净流动性中加上liquidityDelta
。
最后需要将 liquidityNet 和liquidityGross更新到slot中
assembly ("memory-safe") {
// liquidityGrossAfter and liquidityNet are packed in the first slot of `info`
// So we can store them with a single sstore by packing them ourselves first
sstore(
info.slot,
// bitwise OR to pack liquidityGrossAfter and liquidityNet
or(
// Put liquidityGrossAfter in the lower bits, clearing out the upper bits
and(liquidityGrossAfter, 0xffffffffffffffffffffffffffffffff),
// Shift liquidityNet to put it in the upper bits (no need for signextend since we're shifting left)
shl(128, liquidityNet)
)
)
}
liquidityGrossAfter 和 liquidityNet 被打包在 `info` 的第一个槽位中因此我们可以先自己打包,再用一次 sstore 操作将它们一起存储起来
由代码的注释可知这里的slot并不是tickInfo中的一个属性。这里涉及到一个 Solidity 中存储布局的一个概念,slot用于表示变量在以太坊存储中的位置。
在以太坊中每个变量都占用一个唯一的存储槽(slot
),用来标识它在存储中的位置。每个存储槽的大小为 32 字节(256 位)。简单变量(如 uint256
、address
)直接存储在一个槽中,而复杂数据结构(如 mapping
、struct
)的存储位置通过哈希计算得出。
对于TickInfo这个结构体,其存储槽位分布如下
struct TickInfo {
uint128 liquidityGross; // 16 字节
int128 liquidityNet; // 16 字节
uint256 feeGrowthOutside0X128; // 32 字节
uint256 feeGrowthOutside1X128; // 32 字节
}
liquidityGross
和liquidityNet
被打包到第一个槽位中(总共 32 字节)。feeGrowthOutside0X128
和feeGrowthOutside1X128
分别占用第二个和第三个槽位。
槽位编号 | 字段名称 | 大小(字节) | 位置(位) |
---|---|---|---|
0 | liquidityGross | 16 | 低 128 位(0-127) |
0 | liquidityNet | 16 | 高 128 位(128-255) |
1 | feeGrowthOutside0X128 | 32 | 整个槽位(0-255) |
2 | feeGrowthOutside1X128 | 32 | 整个槽位(0-255) |
所以代码中的info.slot获取的是当前tick的第一个槽位,也就是liquidityGross和liquidityNet的组合值。处理过程如下:
- 将 liquidityGrossAfter 放在低 128 位
- 将 liquidityNet 左移 128 位放在高 128 位
- 使用or操作将 liquidityGrossAfter 和 liquidityNet 打包
- 使用单次 `sstore` 操作将它们存储info.slot中
流动性校验
接下来是一步校验,当增加流动性时,防止单个tick上的流动性超过上限造成后续的计算溢出。
if (liquidityDelta >= 0) {
uint128 maxLiquidityPerTick = tickSpacingToMaxLiquidityPerTick(params.tickSpacing);
if (state.liquidityGrossAfterLower > maxLiquidityPerTick) {
TickLiquidityOverflow.selector.revertWith(tickLower);
}
if (state.liquidityGrossAfterUpper > maxLiquidityPerTick) {
TickLiquidityOverflow.selector.revertWith(tickUpper);
}
}
我们看一下tickSpacingToMaxLiquidityPerTick这个方法:
function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128 result) {
// Equivalent to:
// int24 minTick = (TickMath.MIN_TICK / tickSpacing);
// if (TickMath.MIN_TICK % tickSpacing != 0) minTick--;
// int24 maxTick = (TickMath.MAX_TICK / tickSpacing);
// uint24 numTicks = maxTick - minTick + 1;
// return type(uint128).max / numTicks;
int24 MAX_TICK = TickMath.MAX_TICK;
int24 MIN_TICK = TickMath.MIN_TICK;
// tick spacing will never be 0 since TickMath.MIN_TICK_SPACING is 1
assembly ("memory-safe") {
tickSpacing := signextend(2, tickSpacing)
let minTick := sub(sdiv(MIN_TICK, tickSpacing), slt(smod(MIN_TICK, tickSpacing), 0))
let maxTick := sdiv(MAX_TICK, tickSpacing)
let numTicks := add(sub(maxTick, minTick), 1)
result := div(sub(shl(128, 1), 1), numTicks)
}
}
该方法是根据tick之间的间隔space计算 每个tick上流动性的最大承载量。
内联汇编的的代码可读性不高,所以官方非常贴心的添加了一段等价的代码注释。
function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) {
int24 minTick = TickMath.MIN_TICK / tickSpacing;
if (TickMath.MIN_TICK % tickSpacing != 0) {
minTick--;
}
int24 maxTick = TickMath.MAX_TICK / tickSpacing;
uint24 numTicks = uint24(maxTick - minTick + 1);
return type(uint128).max / numTicks;
}
uniswap中单个pool中Liquidity的最大值是,超过这个数值,则会造成计算溢出。平均到每个tick上就是type(uint128).max / numTicks;
numTicks
是根据tickSpacing
计算出的tick最大数量,也就是说numTicks
的大小取决于 tickSpacing
:
tickSpacing
越大,numTicks
越少(因为tick
的间隔变大),每个tick
分配的最大流动性越多。tickSpacing
越小,numTicks
越多(因为tick
的间隔变小),每个tick
分配的最大流动性越少。
内联汇编的代码注释:
tickSpacing := signextend(2, tickSpacing)
- 将
tickSpacing
的符号位扩展到 256 位,以便后续计算
let minTick := sub(sdiv(MIN_TICK, tickSpacing), slt(smod(MIN_TICK, tickSpacing), 0))
sdiv(MIN_TICK, tickSpacing)
:计算MIN_TICK
除以tickSpacing
的商(有符号整数除法)。smod(MIN_TICK, tickSpacing)
:计算MIN_TICK
除以tickSpacing
的余数。slt(smod(MIN_TICK, tickSpacing), 0)
:检查余数是否小于 0。如果余数为负,则需要将minTick
减 1。
let maxTick := sdiv(MAX_TICK, tickSpacing)
- 计算
MAX_TICK
除以tickSpacing
的商。
let numTicks := add(sub(maxTick, minTick), 1)
maxTick - minTick
:计算从minTick
到maxTick
的tick
数量。+1
:包括minTick
和maxTick
本身。
result := div(sub(shl(128, 1), 1), numTicks)
shl(128, 1)
:将数字1
左移 128 位,得到 ()。
sub(shl(128, 1), 1)
:计算 (),这是
uint128
的最大值。div(..., numTicks)
:将最大值除以numTicks
,得到每个tick
的最大允许流动性。
更新tickBitMap
我们知道tickBitMap是一个位图组成的map,887272*2个tick用int24来表示,并且用map做二级管理。高16位作为key,剩下的第八位转换成一个265位二进制数,每一位代表一个tick(0:未初始化;1:初始化)。接下来的步骤就是当tick出现初始化状态变化时,反转相应的tick状态位。
if (state.flippedLower) {
self.tickBitmap.flipTick(tickLower, params.tickSpacing);
}
if (state.flippedUpper) {
self.tickBitmap.flipTick(tickUpper, params.tickSpacing);
}
flipTick代码如下
function flipTick(mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing) internal {
// Equivalent to the following Solidity:
// if (tick % tickSpacing != 0) revert TickMisaligned(tick, tickSpacing);
// (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing);
// uint256 mask = 1 << bitPos;
// self[wordPos] ^= mask;
assembly ("memory-safe") {
tick := signextend(2, tick)
tickSpacing := signextend(2, tickSpacing)
// ensure that the tick is spaced
if smod(tick, tickSpacing) {
let fmp := mload(0x40)
mstore(fmp, 0xd4d8f3e6) // selector for TickMisaligned(int24,int24)
mstore(add(fmp, 0x20), tick)
mstore(add(fmp, 0x40), tickSpacing)
revert(add(fmp, 0x1c), 0x44)
}
tick := sdiv(tick, tickSpacing)
// calculate the storage slot corresponding to the tick
// wordPos = tick >> 8
mstore(0, sar(8, tick))
mstore(0x20, self.slot)
// the slot of self[wordPos] is keccak256(abi.encode(wordPos, self.slot))
let slot := keccak256(0, 0x40)
// mask = 1 << bitPos = 1 << (tick % 256)
// self[wordPos] ^= mask
sstore(slot, xor(sload(slot), shl(and(tick, 0xff), 1)))
}
}
先看下官方提供的等效代码:
function flipTick(mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing) internal {
// 确保 tick 是 tickSpacing 的整数倍
if (tick % tickSpacing != 0) {
revert TickMisaligned(tick, tickSpacing);
}
// 计算压缩后的 tick 值
tick /= tickSpacing;
// 计算 wordPos 和 bitPos
int16 wordPos = int16(tick >> 8); // tick / 256
uint8 bitPos = uint8(tick % 256);
// 计算掩码并翻转对应位
uint256 mask = 1 << bitPos;
self[wordPos] ^= mask;
}
结合之前的文章应该不难理解。
下面是汇编代码的解析:
tick := signextend(2, tick)
tickSpacing := signextend(2, tickSpacing)
- 使用
signextend
将tick
和tickSpacing
从 24 位扩展为 256 位。这是因为 EVM 的操作数默认是 256 位,符号扩展可以确保负数的符号位正确保留。
if smod(tick, tickSpacing) {
let fmp := mload(0x40)
mstore(fmp, 0xd4d8f3e6) // selector for TickMisaligned(int24,int24)
mstore(add(fmp, 0x20), tick)
mstore(add(fmp, 0x40), tickSpacing)
revert(add(fmp, 0x1c), 0x44)
}
-
smod(tick, tickSpacing)
:计算tick % tickSpacing
。如果结果不为 0,说明tick
不是tickSpacing
的整数倍。此时需要抛出异常!
tick := sdiv(tick, tickSpacing)
- 将
tick
除以tickSpacing
,得到压缩后的tick
值。tickBitmap
只存储压缩后的tick
状态,而不是原始的tick
。
mstore(0, sar(8, tick))
mstore(0x20, self.slot)
let slot := keccak256(0, 0x40)
-
sar(8, tick)
:计算wordPos
,即tickBitMap的key。等价于tick >> 8
,将tick
右移 8 位取高16位。 -
self.slot
:self
是tickBitmap
的存储槽位置。 -
keccak256(0, 0x40)
:计算存储槽位置slot
,表示tickBitmap[wordPos]
的存储位置。
在 Solidity 中,某一个key在
mapping
的存储位置是通过这个公式计算的:slot= keccak256(abi.encode(key, mappingSlot))
abi.encode(key, mappingSlot)的意思就是把key和mappingSlot连接到一起,前面两个部分攻占用了64个字节:[key (32 字节)][mappingSlot (32 字节)]
故而:keccak256(0, 0x40)=keccak256(abi.encode(key, mappingSlot))
也就说keccak256(0, 0x40)计算的是tick所在位图的存储位置
sstore(slot, xor(sload(slot), shl(and(tick, 0xff), 1)))
-
and(tick, 0xff)
:计算bitPos
,即tick
在 256 位位图中的具体位置。等价于tick % 256
。 -
shl(bitPos, 1)
:计算掩码mask = 1 << bitPos
,用于定位tick
对应的位。 -
sload(slot)
:加载存储槽slot
的当前值。 -
xor(sload(slot), mask)
:使用按位异或(^
)翻转tick
对应的位:如果该位原来是0
,则变为1
。如果该位原来是1
,则变为0
。 -
sstore(slot, ...)
:将更新后的值存储回slot
。
头寸更新
接下来是计算流动性提供者(LP)在指定的 tickLower
和 tickUpper
范围内的手续费收益,并更新其头寸(position
)的状态:
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
getFeeGrowthInside(self, tickLower, tickUpper);
Position.State storage position = self.positions.get(params.owner, tickLower, tickUpper, params.salt);
(uint256 feesOwed0, uint256 feesOwed1) =
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
// Fees earned from LPing are calculated, and returned
feeDelta = toBalanceDelta(feesOwed0.toInt128(), feesOwed1.toInt128());
1. 获取区间内的手续费增长(之前的文章说过,这里不再赘述!)
2. 获取并更新头寸状态,并计算 该头寸内应得的手续费
function update(
State storage self,
int128 liquidityDelta,
uint256 feeGrowthInside0X128,
uint256 feeGrowthInside1X128
) internal returns (uint256 feesOwed0, uint256 feesOwed1) {
uint128 liquidity = self.liquidity;
if (liquidityDelta == 0) {
// disallow pokes for 0 liquidity positions
if (liquidity == 0) CannotUpdateEmptyPosition.selector.revertWith();
} else {
self.liquidity = LiquidityMath.addDelta(liquidity, liquidityDelta);
}
// calculate accumulated fees. overflow in the subtraction of fee growth is expected
unchecked {
feesOwed0 =
FullMath.mulDiv(feeGrowthInside0X128 - self.feeGrowthInside0LastX128, liquidity, FixedPoint128.Q128);
feesOwed1 =
FullMath.mulDiv(feeGrowthInside1X128 - self.feeGrowthInside1LastX128, liquidity, FixedPoint128.Q128);
}
// update the position
self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
}
应得的手续费计算步骤如下:
- 手续费增长的增量:feeGrowth增量 = feeGrowthInside0X128 - self.feeGrowthInside0LastX128
- 乘以流动性:手续费总量 = feeGrowth增量 * liquidity
- 转换精度:feesOwed0 = 手续费总量 / FixedPoint128.Q128
3.将头寸应得的手续费转换为 BalanceDelta
类型,表示池子中两种代币的余额变化
function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) {
assembly ("memory-safe") {
balanceDelta := or(shl(128, _amount0), and(sub(shl(128, 1), 1), _amount1))
}
}
1. shl(128, _amount0):
将 _amount0
左移 128 位。结果是 _amount0
被移到 int256
的高 128 位,低 128 位填充为 0。
2. sub(shl(128, 1), 1)
-
shl(128, 1)
:计算 (),即一个 128 位的掩码,所有高 128 位为 1,低 128 位为 0。
-
sub(shl(128, 1), 1)
:计算 (),即一个低 128 位全为 1 的掩码,高 128 位为 0。结果是
0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
。
3. and(sub(shl(128, 1), 1), _amount1)
:将 _amount1
的值限制在低 128 位,高 128 位清零。
4. or(shl(128, _amount0), and(..., _amount1))
:将 _amount0
的高 128 位和 _amount1
的低 128 位合并成一个 int256
。
根据liquidityDelta 计算tokenDelta
最后一段代码是根据当前价格(tick
和 sqrtPriceX96
)与流动性范围(tickLower
和 tickUpper
)的关系,计算流动性变动(liquidityDelta
)对池子中两种代币余额的影响,并更新全局流动性。
if (liquidityDelta != 0) {
Slot0 _slot0 = self.slot0;
(int24 tick, uint160 sqrtPriceX96) = (_slot0.tick(), _slot0.sqrtPriceX96());
if (tick < tickLower) {
// current tick is below the passed range; liquidity can only become in range by crossing from left to
// right, when we'll need _more_ currency0 (it's becoming more valuable) so user must provide it
delta = toBalanceDelta(
SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), liquidityDelta
).toInt128(),
0
);
} else if (tick < tickUpper) {
delta = toBalanceDelta(
SqrtPriceMath.getAmount0Delta(sqrtPriceX96, TickMath.getSqrtPriceAtTick(tickUpper), liquidityDelta)
.toInt128(),
SqrtPriceMath.getAmount1Delta(TickMath.getSqrtPriceAtTick(tickLower), sqrtPriceX96, liquidityDelta)
.toInt128()
);
self.liquidity = LiquidityMath.addDelta(self.liquidity, liquidityDelta);
} else {
// current tick is above the passed range; liquidity can only become in range by crossing from right to
// left, when we'll need _more_ currency1 (it's becoming more valuable) so user must provide it
delta = toBalanceDelta(
0,
SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), liquidityDelta
).toInt128()
);
}
}
- 当tick < tickLower时代表前价格低于我们调整流动性的范围,此时tickLower和tickUpper的范围内只拥有
token0
。流动性只能通过价格从左向右穿过tickLower
进入范围。变化流动性只需要调整token0的数量,此时
调用SqrtPriceMath.getAmount1Delta
计算token0
的变化量。 - 当tickLower<tick <tickUpper,表示范围内同时拥有
token0
和token1
。流动性变化会同时影响token0
和token1
的数量。此时需要调用SqrtPriceMath.getAmount0Delta
和SqrtPriceMath.getAmount1Delta
分别计算token0
和token1
的变化量。 - 当tick > tickUpper,表示tickLower和tickUpper的范围内只拥有
token1
。流动性只能通过价格从右向左穿过tickUpper
进入范围。变化流动性只需要调整token1的数量,此时
调用SqrtPriceMath.getAmount1Delta
计算token1
的变化量。
价格在tickLower和tickUpper中间移动的过程,就是token0逐渐变少,token1逐渐变多的过程,当刚进入tickLower和tickUpper范围内,token0的数量为0,此时如果用户继续提供token0交换token1,则价格会往左移动离开范围,进入其他的tick区间,而随着价格逐渐右移当达到tickUpper的位置时,token0的数量为0,当前tick范围内仅存在token1,继续提供token1交换token0,则会进入下一个tick范围。
我们看一下getAmount0Delta方法:
/// @notice Gets the amount0 delta between two prices
/// @dev Calculates liquidity / sqrt(lower) - liquidity / sqrt(upper),
/// i.e. liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower))
/// @param sqrtPriceAX96 A sqrt price
/// @param sqrtPriceBX96 Another sqrt price
/// @param liquidity The amount of usable liquidity
/// @param roundUp Whether to round the amount up or down
/// @return uint256 Amount of currency0 required to cover a position of size liquidity between the two passed prices
function getAmount0Delta(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint128 liquidity, bool roundUp)
internal
pure
returns (uint256)
{
unchecked {
if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96);
// equivalent: if (sqrtPriceAX96 == 0) revert InvalidPrice();
assembly ("memory-safe") {
if iszero(and(sqrtPriceAX96, 0xffffffffffffffffffffffffffffffffffffffff)) {
mstore(0, 0x00bfc921) // selector for InvalidPrice()
revert(0x1c, 0x04)
}
}
uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION;
uint256 numerator2 = sqrtPriceBX96 - sqrtPriceAX96;
return roundUp
? UnsafeMath.divRoundingUp(FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtPriceBX96), sqrtPriceAX96)
: FullMath.mulDiv(numerator1, numerator2, sqrtPriceBX96) / sqrtPriceAX96;
}
}
这个方法的作用是计算在两个价格之间(sqrtPriceAX96
和 sqrtPriceBX96
)的区间内,提供或移除一定流动性(liquidity
)所需的 token0
数量(amount0
)
共有4个参数:
sqrtPriceAX96
:区间的起始价格(平方根形式,Q64.96 格式)。sqrtPriceBX96
:区间的结束价格(平方根形式,Q64.96 格式)。liquidity
:提供或移除的流动性数量。roundUp
:是否向上取整。true
表示向上取整,false
表示向下取整。
方法并不复杂,我们只介绍其数学推导,直接上公式:
怎么来的呢!我么知道uniswap的交易核心时源于 恒定乘积公式:
于是得出:
那么amount0在tickLower和tickUpper的位置的数量分别为
于是得出:
更新账户余额
BalanceDelta hookDelta;
(callerDelta, hookDelta) = key.hooks.afterModifyLiquidity(key, params, callerDelta, feesAccrued, hookData);
// if the hook doesn't have the flag to be able to return deltas, hookDelta will always be 0
if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks));
_accountPoolBalanceDelta(key, callerDelta, msg.sender);
经过pool得调整产生了两部分的金额调整,一部分是调整流动性后池中所属用户的代币数量需要调整,还有一部分是手续费部分,也要累计上去。
首先是调用afterModifyLiquidity钩子,调用钩子函数后调整的额度callerDelta会有所变化。
最后一行代码就是,把本次更新流动新所产生的产生的余额调整,更新到用户在这个pool中的存储上。
具体按代码如下:
function _accountPoolBalanceDelta(PoolKey memory key, BalanceDelta delta, address target) internal {
_accountDelta(key.currency0, delta.amount0(), target);
_accountDelta(key.currency1, delta.amount1(), target);
}
/// @notice Adds a balance delta in a currency for a target address
function _accountDelta(Currency currency, int128 delta, address target) internal {
if (delta == 0) return;
(int256 previous, int256 next) = currency.applyDelta(target, delta);
//动态维护一个计数器(NonzeroDeltaCount),用于跟踪当前非零余额(delta)的账户数量。
if (next == 0) {
NonzeroDeltaCount.decrement();
} else if (previous == 0) {
NonzeroDeltaCount.increment();
}
}
function applyDelta(Currency currency, address target, int128 delta)
internal
returns (int256 previous, int256 next)
{
bytes32 hashSlot = _computeSlot(target, currency);
assembly ("memory-safe") {
previous := tload(hashSlot)
}
next = previous + delta;
assembly ("memory-safe") {
tstore(hashSlot, next)
}
}
把delta上的两个token的deltaAmount分别与用户在这个池子中的余额进行累加,调整成最终的余额。
同时将钩子函数(hookDelta
)返回的余额变化记录到池子中(倒数第二行),并将其归属于钩子合约地址(address(key.hooks)
)。这可以用于追踪钩子函数的行为,或者在后续操作中结算这些余额变化。