uniswap v4 合约解析2 更新pool流动性

这一章我们分析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:流动性范围的下界。
  • bytes calldata hookData

    钩子(Hooks)所需的附加数据,允许在流动性修改前后执行自定义逻辑。

这些参数都比较简单,唯一的需要解释的是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;
  • 如果更新的是上边界 tickupper == true),则从净流动性中减去 liquidityDelta
  • 如果更新的是下边界 tickupper == 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 位)。简单变量(如 uint256address)直接存储在一个槽中,而复杂数据结构(如 mappingstruct)的存储位置通过哈希计算得出。

对于TickInfo这个结构体,其存储槽位分布如下

struct TickInfo {

    uint128 liquidityGross; // 16 字节

    int128 liquidityNet;    // 16 字节

    uint256 feeGrowthOutside0X128; // 32 字节

    uint256 feeGrowthOutside1X128; // 32 字节

}

  • liquidityGross 和 liquidityNet 被打包到第一个槽位中(总共 32 字节)。
  • feeGrowthOutside0X128 和 feeGrowthOutside1X128 分别占用第二个和第三个槽位。
槽位编号字段名称大小(字节)位置(位)
0liquidityGross16低 128 位(0-127)
0liquidityNet16高 128 位(128-255)
1feeGrowthOutside0X12832整个槽位(0-255)
2feeGrowthOutside1X12832整个槽位(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的最大值是2^{128}-1,超过这个数值,则会造成计算溢出。平均到每个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 位,得到 (2^{128})。
  • sub(shl(128, 1), 1):计算 (2^{128} - 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.slotself 是 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;
}

 应得的手续费计算步骤如下:

  1. 手续费增长的增量:feeGrowth增量 = feeGrowthInside0X128 - self.feeGrowthInside0LastX128
  2. 乘以流动性:手续费总量 = feeGrowth增量 * liquidity
  3. 转换精度: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):计算 (2^{128}),即一个 128 位的掩码,所有高 128 位为 1,低 128 位为 0。

  • sub(shl(128, 1), 1):计算 (2^{128} - 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 表示向下取整。

方法并不复杂,我们只介绍其数学推导,直接上公式:

\bigtriangleup amount0=liquidity*\frac{\sqrt{P_{upper}}-\sqrt{P_{lower}}}{\sqrt{P_{upper}}*\sqrt{P_{lower}}}

怎么来的呢!我么知道uniswap的交易核心时源于 恒定乘积公式:

x*y=k=L^{2}

P= \frac{y}{x}

于是得出:

\sqrt{amount_{x}*amount_{y}}=L

\sqrt{amount_{x}*amount_{x}*P}=L

amount_{x}\sqrt{P}=L

那么amount0在tickLower和tickUpper的位置的数量分别为

amount0_{upper}=\frac{L}{\sqrt{P_{upper}}}
amount0_{lower}=\frac{L}{\sqrt{P_{lower}}}

于是得出:

amount0_{upper}-amount0_{lower}=\frac{L}{\sqrt{P_{upper}}}-\frac{L}{\sqrt{P_{lower}}}

\bigtriangleup amount0=liquidity*\frac{\sqrt{P_{upper}}-\sqrt{P_{lower}}}{\sqrt{P_{upper}}*\sqrt{P_{lower}}}

更新账户余额

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))。这可以用于追踪钩子函数的行为,或者在后续操作中结算这些余额变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值