Uniswap v3 詳解(四):交易手續(xù)費(fèi)

本文copy自https://liaoph.com/uniswap-v3-4/

以普通用戶(hù)的視角來(lái)看,對(duì)比 Uniswap v2厉膀,Uniswap v3 在手續(xù)費(fèi)方面做了如下改動(dòng):
添加流動(dòng)性時(shí)豹芯,手續(xù)費(fèi)可以有 3個(gè)級(jí)別供選擇:0.05%, 0.3% 和 1%付魔,未來(lái)可以通過(guò)治理加入更多可選的手續(xù)費(fèi)率
Uniswap v2 中手續(xù)費(fèi)會(huì)在收取后自動(dòng)復(fù)投稱(chēng)為 LP 的一部分,即每次手續(xù)費(fèi)都自動(dòng)變成流動(dòng)性加入池子中孙蒙,而 Uniswap v3 中收取的手續(xù)費(fèi)不會(huì)自動(dòng)復(fù)投(主要是為了方便合約的計(jì)算),需要手動(dòng)取出手續(xù)費(fèi)
不同手續(xù)費(fèi)級(jí)別悲雳,在添加流動(dòng)性時(shí)挎峦,價(jià)格可選值的最小粒度也不一樣(這個(gè)是因?yàn)?tick spacing 的影響),一般來(lái)說(shuō)合瓢,手續(xù)費(fèi)越低坦胶,價(jià)格可選值越精細(xì),因此官方推薦價(jià)格波動(dòng)小的交易對(duì)使用低費(fèi)率(例如穩(wěn)定幣交易對(duì))
以開(kāi)發(fā)者的視角來(lái)看晴楔,Uniswap v3 的手續(xù)費(fèi)計(jì)算相對(duì)會(huì)比較復(fù)雜顿苇, 因?yàn)樗枰槍?duì)每一個(gè) position 來(lái)進(jìn)行單獨(dú)的計(jì)算,為了方便計(jì)算税弃,在代碼中會(huì)將手續(xù)費(fèi)相關(guān)的元數(shù)據(jù)記錄在 position 的邊界 tick 上(這些 tick 上還存儲(chǔ)了 ΔL 等元數(shù)據(jù))纪岁。

手續(xù)費(fèi)的計(jì)算和存儲(chǔ)

在之前的文章中說(shuō)過(guò),一個(gè)交易對(duì)池的流動(dòng)性则果,是由不同的流動(dòng)性組合而成幔翰,每一個(gè)流動(dòng)性的提供者都可以設(shè)置獨(dú)立的價(jià)格范圍區(qū)間,這個(gè)被稱(chēng)為 positon. 當(dāng)我們計(jì)算交易的手續(xù)費(fèi)時(shí)西壮,我們需要計(jì)算如下值:

每一個(gè) position 收取的手續(xù)費(fèi)(token0, token1 需要分別單獨(dú)計(jì)算)
用戶(hù)如果提取了手續(xù)費(fèi)遗增,需要記錄用戶(hù)已提取的數(shù)值
v3 中有以下幾個(gè)關(guān)于手續(xù)費(fèi)的變量:

交易池中手續(xù)費(fèi)的費(fèi)率值,這里記錄的值時(shí)以 1000000 為基數(shù)的值茸时,例如當(dāng)手續(xù)費(fèi)為 0.03% 時(shí)贡定,費(fèi)率值為 300
全局狀態(tài)變量 feeGrowthGlobal0X128 和 feeGrowthGlobal1X128 ,分別表示 token0 和 token1 所累計(jì)的手續(xù)費(fèi)總額可都,使用了 Q128.128 浮點(diǎn)數(shù)來(lái)記錄
對(duì)于每個(gè) tick缓待,記錄了 feeGrowthOutside0X128 和 feeGrowthOutside1X128,這兩個(gè)變量記錄了發(fā)生在此 tick 「外側(cè)」的手續(xù)費(fèi)總額渠牲,那么什么「外側(cè)」呢旋炒,后文會(huì)詳細(xì)說(shuō)明
對(duì)于每個(gè) position,記錄了此 position 內(nèi)的手續(xù)費(fèi)總額 feeGrowthInside0LastX128 和 feeGrowthInside1LastX128签杈,這個(gè)值不需要每次都更新瘫镇,它只會(huì)在 position 發(fā)生變動(dòng),或者用戶(hù)提取手續(xù)費(fèi)時(shí)更新
需要注意的時(shí)答姥,上面這些手續(xù)費(fèi)狀態(tài)變量都是每一份 LP 所對(duì)應(yīng)的手續(xù)費(fèi)铣除,在計(jì)算真正的手續(xù)費(fèi)時(shí),需要使用 LP 數(shù)相乘來(lái)得出實(shí)際手續(xù)費(fèi)數(shù)額鹦付,又因?yàn)?LP 數(shù)在不同價(jià)格可能時(shí)不同的(因?yàn)榱鲃?dòng)性深度不同)尚粘,所以在計(jì)算手續(xù)費(fèi)時(shí)只能針對(duì) position 進(jìn)行計(jì)算(同一個(gè) position 內(nèi) LP 總量不變)。

計(jì)算過(guò)程

我們用 fg 表示代幣池收取的手續(xù)費(fèi)總額敲长,對(duì)于一個(gè) tick郎嫁,其索引為 i秉继,使用 fo(i) 表示此 tick 「外側(cè)」的手續(xù)費(fèi)總額,使用 fb(i) 表示低于此 tick 價(jià)格發(fā)生的交易的手續(xù)費(fèi)總額泽铛,使用 fa(i) 表示高于此 tick 價(jià)格發(fā)生的交易的手續(xù)費(fèi)總額尚辑。


1637902293(1).png

1637902313(1).png

流動(dòng)性?xún)r(jià)格區(qū)間中的手續(xù)費(fèi)計(jì)算

1637902354(1).png

fo 的更新

我們知道了某一個(gè) tick 的 fo(i) 的值,與當(dāng)前 tick ic 和 i 之間的位置關(guān)系有關(guān)(大于或者小于)盔腔,在發(fā)生交易時(shí)杠茬,當(dāng)前價(jià)格的 ic 是會(huì)不斷變化的。因此弛随,當(dāng) ic 和 i 的位置關(guān)系發(fā)生了變化時(shí)澈蝙,我們需要更新 fo(i) 的值。

具體來(lái)說(shuō)撵幽,當(dāng)前價(jià)格穿過(guò)某一個(gè) tick 時(shí),需要更新此 tick 上的 fo(i)礁击,更新的方式時(shí)將其值修改為另一側(cè)的手續(xù)費(fèi)總和盐杂,即:

fo(i):=fg?fo(i)

core 倉(cāng)庫(kù)代碼分析

手續(xù)費(fèi)計(jì)算的代碼的計(jì)算主要出現(xiàn)在以下行為相關(guān)的代碼中:

提供流動(dòng)性時(shí),需要初始化 tick 對(duì)應(yīng)的 fo 值
發(fā)生交易時(shí)哆窿,需要更新 fg
當(dāng)交易過(guò)程中链烈,當(dāng)前價(jià)格穿過(guò)某一個(gè) tick 時(shí),需要更新此 tick 上的 fo 值
當(dāng)流動(dòng)性發(fā)生變動(dòng)時(shí)挚躯,更新此 position 中手續(xù)費(fèi)的總和

提供流動(dòng)性

在添加流動(dòng)性時(shí)强衡,我們會(huì)初始化或更新此 position 對(duì)應(yīng)的 lower/upper tick,在 Tick.update 函數(shù)中:

function update(
    mapping(int24 => Tick.Info) storage self,
    int24 tick,
    int24 tickCurrent,
    int128 liquidityDelta,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128,
    bool upper,
    uint128 maxLiquidity
) internal returns (bool flipped) {
    Tick.Info storage info = self[tick];

    // 獲取此 tick 更新之前的流動(dòng)性
    uint128 liquidityGrossBefore = info.liquidityGross;
    uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);

    ...

    // 如果 tick 在更新之前的 liquidityGross 為 0码荔,那么表示我們本次為初始化操作
    // 這里會(huì)初始化 tick 中的 f_o
    if (liquidityGrossBefore == 0) {
        // by convention, we assume that all growth before a tick was initialized happened _below_ the tick
        if (tick <= tickCurrent) {
            info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
            info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
        }
    }
    ...
    info.liquidityGross = liquidityGrossAfter;
    ...
}
1637902499(1).png

交易過(guò)程中的手續(xù)費(fèi)

[上一篇]文章中講過(guò)整個(gè)交易過(guò)程時(shí)分步進(jìn)行的漩勤,每一步都在一個(gè)相同的流動(dòng)性區(qū)間,那么手續(xù)費(fèi)的計(jì)算也需要在每一步的交易中計(jì)算出這一步的手續(xù)費(fèi)總數(shù)缩搅。在交易步驟的結(jié)構(gòu)體中有定義:

struct StepComputations {
    ...
    // 當(dāng)前交易步驟的手續(xù)費(fèi)
    uint256 feeAmount;
}

只計(jì)算一個(gè)值時(shí)因?yàn)樵桨埽掷m(xù)費(fèi)只會(huì)在輸入的 token 中收取,而不會(huì)在輸出的 token 中重復(fù)收取硼瓣。

計(jì)算過(guò)程在 SwapMath.computeSwapStep 中:

function computeSwapStep(
    ...
)
    internal
    pure
    returns (
        ...
        uint256 feeAmount
    )
{
    ...
    if (exactIn) {
        // 在交易之前究飞,先計(jì)算當(dāng)價(jià)格移動(dòng)到交易區(qū)間邊界時(shí),所需要的手續(xù)費(fèi)
        // 即此步驟最多需要的手續(xù)費(fèi)數(shù)額
        uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6);
        ...
    } else {
        ...
    }

    ...

    // 根據(jù)交易是否移動(dòng)到價(jià)格邊界來(lái)計(jì)算手續(xù)費(fèi)的數(shù)額
    if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) {
        // 當(dāng)沒(méi)有移動(dòng)到價(jià)格邊界時(shí)(即余額不足以讓價(jià)格移動(dòng)到邊界)堂鲤,直接把余額中剩余的資金全部作為手續(xù)費(fèi)
        feeAmount = uint256(amountRemaining) - amountIn;
    } else {
        // 當(dāng)價(jià)格移動(dòng)到邊界時(shí)亿傅,計(jì)算相應(yīng)的手續(xù)費(fèi)
        feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips);
    }
}
1637902593(1).png
// 交易步驟的循環(huán)
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    // 計(jì)算這一步的手續(xù)費(fèi)總額
    (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
        ...
    );

    // 更新交易的 f_g,這里需要除以流動(dòng)性 L
    if (state.liquidity > 0)
        state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);

    ...
 }

 ...

// 在交易步驟完成后瘟栖,更新合約的 f_g
if (zeroForOne) {
    feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
    if (state.protocolFee > 0) protocolFees.token0 += state.protocolFee;
} else {
    feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
    if (state.protocolFee > 0) protocolFees.token1 += state.protocolFee;
}
...

更新時(shí)使用此步驟的手續(xù)費(fèi)總額除以此步驟的流動(dòng)性 L 狮荔,以得出每一份流動(dòng)性所對(duì)應(yīng)的手續(xù)費(fèi)數(shù)值。

當(dāng) tick 被穿過(guò)時(shí)

前面說(shuō)過(guò)汉操,當(dāng) tick 被穿過(guò)時(shí),需要更新這個(gè) tick 對(duì)應(yīng)的 fo奔穿,這部分操作也是在 UniswapV3Pool.swap 中:

while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    ...
    // 當(dāng)價(jià)格到達(dá)當(dāng)前步驟價(jià)格區(qū)間的邊界時(shí),可能需要穿過(guò)下一個(gè) tick
    if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
        // 查看下一個(gè) tick 是否初始化
        if (step.initialized) {
            int128 liquidityNet =
                // 在這里需要更新 tick 的 f_o
                ticks.cross(
                    step.tickNext,
                    (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
                    (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128)
                );
            // if we're moving leftward, we interpret liquidityNet as the opposite sign
            // safe because liquidityNet cannot be type(int128).min
            ...
        }

        ...
    } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
        ...
    }
    ...
}

這里通過(guò) ticks.cross 來(lái)更新被穿過(guò)的 tick:

function cross(
    mapping(int24 => Tick.Info) storage self,
    int24 tick,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128
) internal returns (int128 liquidityNet) {
    Tick.Info storage info = self[tick];
    info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
    info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
    liquidityNet = info.liquidityNet;
}
1637902706(1).png

position 維度

position 由 lower tick 和 upper tick 兩個(gè) tick 組成敏晤,當(dāng) positino 更新時(shí)贱田,就可以更新從上次更新以來(lái)此 position 中累積的手續(xù)費(fèi)數(shù)額。只在 position 的流動(dòng)性更新時(shí)才更新 position 中的手續(xù)費(fèi)可以讓交易過(guò)程不用更新過(guò)多的變量嘴脾,節(jié)省交易所消耗的 gas 費(fèi)用男摧。在 UniswapV3Pool._updatePosition 中:

function _updatePosition(
    address owner,
    int24 tickLower,
    int24 tickUpper,
    int128 liquidityDelta,
    int24 tick
) private returns (Position.Info storage position) {
    ...

    // 計(jì)算出此 position 中的手續(xù)費(fèi)總額
    (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
        ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);

    // 更新 position 中記錄的值
    position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);

    ...
}

通過(guò) ilower, iupper, icurrent 和 fg,調(diào)用 ticks.getFeeGrowthInside 可以計(jì)算出 position 中的手續(xù)費(fèi)總額译打,代碼為:

function getFeeGrowthInside(
    mapping(int24 => Tick.Info) storage self,
    int24 tickLower,
    int24 tickUpper,
    int24 tickCurrent,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128
) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
    Info storage lower = self[tickLower];
    Info storage upper = self[tickUpper];

    // 計(jì)算 f_b(i)
    uint256 feeGrowthBelow0X128;
    uint256 feeGrowthBelow1X128;
    if (tickCurrent >= tickLower) {
        feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
    } else {
        feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
    }

    // 計(jì)算 f_a(i)
    uint256 feeGrowthAbove0X128;
    uint256 feeGrowthAbove1X128;
    if (tickCurrent < tickUpper) {
        feeGrowthAbove0X128 = upper.feeGrowthOutside0X128;
        feeGrowthAbove1X128 = upper.feeGrowthOutside1X128;
    } else {
        feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
        feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128;
    }

    feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
    feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128;
}

這部分代碼使用前面說(shuō)過(guò)的[公式耗拓,這里不再詳述。

Position.update 函數(shù)中:

function update(
    Info storage self,
    int128 liquidityDelta,
    uint256 feeGrowthInside0X128,
    uint256 feeGrowthInside1X128
) internal
    ...
    // 計(jì)算 token0 和 token1 的手續(xù)費(fèi)總數(shù)
    uint128 tokensOwed0 =
        uint128(
            FullMath.mulDiv(
                feeGrowthInside0X128 - _self.feeGrowthInside0LastX128,
                _self.liquidity,
                FixedPoint128.Q128
            )
        );
    uint128 tokensOwed1 =
        uint128(
            FullMath.mulDiv(
                feeGrowthInside1X128 - _self.feeGrowthInside1LastX128,
                _self.liquidity,
                FixedPoint128.Q128
            )
        );

    // update the position
    if (liquidityDelta != 0) self.liquidity = liquidityNext;
    self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
    self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
    if (tokensOwed0 > 0 || tokensOwed1 > 0) {
        // overflow is acceptable, have to withdraw before you hit type(uint128).max fees
        self.tokensOwed0 += tokensOwed0;
        self.tokensOwed1 += tokensOwed1;
    }
}

這里計(jì)算了此 position 自上次更新以來(lái) token0 和 token1 的手續(xù)費(fèi)總數(shù)奏司,計(jì)算時(shí)使用的 feeGrowthInside0X128 的含義時(shí)每一份流動(dòng)性所對(duì)應(yīng)的手續(xù)費(fèi)份額乔询,因此在計(jì)算總額時(shí)需要使用此值乘以 position 的流動(dòng)性總數(shù)。最后將這些手續(xù)費(fèi)總數(shù)更新到 tokensOwed0 和 tokensOwed0 字段中韵洋。

手續(xù)費(fèi)的提取

手續(xù)費(fèi)的提取也是以 position 為單位進(jìn)行提取的竿刁。使用 UniswapV3Pool.collect 提取手續(xù)費(fèi):

function collect(
    address recipient,
    int24 tickLower,
    int24 tickUpper,
    uint128 amount0Requested,
    uint128 amount1Requested
) external override lock returns (uint128 amount0, uint128 amount1) {
    // 獲取 position 數(shù)據(jù)
    Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);

    // 根據(jù)參數(shù)調(diào)整需要提取的手續(xù)費(fèi)
    amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;
    amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;

    // 將手續(xù)費(fèi)發(fā)送給用戶(hù)
    if (amount0 > 0) {
        position.tokensOwed0 -= amount0;
        TransferHelper.safeTransfer(token0, recipient, amount0);
    }
    if (amount1 > 0) {
        position.tokensOwed1 -= amount1;
        TransferHelper.safeTransfer(token1, recipient, amount1);
    }

    emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);
}

這個(gè)函數(shù)比較簡(jiǎn)單,即根據(jù) position 中已經(jīng)記錄的手續(xù)費(fèi)和用戶(hù)請(qǐng)求的數(shù)額搪缨,發(fā)送指定數(shù)額的手續(xù)費(fèi)給用戶(hù)食拜。

但是這里 posiiton 中的手續(xù)費(fèi)可能并不是最新的(上面說(shuō)過(guò)手續(xù)費(fèi)總數(shù)只會(huì)在 position 的流動(dòng)性更新時(shí)更新)。因此在提取手續(xù)費(fèi)前副编,需要主動(dòng)觸發(fā)一次手續(xù)費(fèi)的更新负甸,這些操作已經(jīng)在 uniswap-v3-periphery 倉(cāng)庫(kù)中進(jìn)行了封裝。

peirphery 倉(cāng)庫(kù)代碼分析

流動(dòng)性對(duì)應(yīng)手續(xù)費(fèi)的更新

NonfungiblePositionManager 中保存了用戶(hù)提供的流動(dòng)性痹届,并使用 NFT token 將這個(gè)流動(dòng)性代幣化呻待。在更新流動(dòng)性時(shí),也會(huì)更新其累積的手續(xù)費(fèi)數(shù)額队腐,例如增加流動(dòng)性時(shí):

function increaseLiquidity(
    uint256 tokenId,
    uint128 amount,
    uint256 amount0Max,
    uint256 amount1Max,
    uint256 deadline
) external payable override checkDeadline(deadline) returns (uint256 amount0, uint256 amount1) {
    ...
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

    // 更新 token0 和 tokne1 累積的手續(xù)費(fèi)
    position.tokensOwed0 += uint128(
        FullMath.mulDiv(
            feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );
    position.tokensOwed1 += uint128(
        FullMath.mulDiv(
            feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
    position.liquidity += amount;
}

因此一個(gè)流動(dòng)性對(duì)應(yīng) NFT token 的手續(xù)費(fèi)也會(huì)在流動(dòng)性變化時(shí)更新带污。

提取手續(xù)費(fèi)

提取手續(xù)費(fèi)使用 NonfungiblePositionManager.collet:

function collect(
    uint256 tokenId,
    address recipient,
    uint128 amount0Max,
    uint128 amount1Max
) external payable override isAuthorizedForToken(tokenId) returns (uint256 amount0, uint256 amount1) {
    require(amount0Max > 0 || amount1Max > 0);
    // 查詢(xún) postion 信息
    Position storage position = _positions[tokenId];

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];

    IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

    (uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1);

    // 這里會(huì)再次更新一次手續(xù)費(fèi)累計(jì)總額
    if (position.liquidity > 0) {
        // 使用 pool.burn() 來(lái)觸發(fā)手續(xù)費(fèi)的更新
        pool.burn(position.tickLower, position.tickUpper, 0);
        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) =
            pool.positions(PositionKey.compute(address(this), position.tickLower, position.tickUpper));

        tokensOwed0 += uint128(
            FullMath.mulDiv(
                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        );
        tokensOwed1 += uint128(
            FullMath.mulDiv(
                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        );

        position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
        position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
    }

    // 提取手續(xù)費(fèi)的最大值,不能超過(guò)手續(xù)費(fèi)總額
    (amount0Max, amount1Max) = (
        amount0Max > tokensOwed0 ? tokensOwed0 : amount0Max,
        amount1Max > tokensOwed1 ? tokensOwed1 : amount1Max
    );

    // 調(diào)用 pool.collect 將手續(xù)費(fèi)發(fā)送給 recipient
    (amount0, amount1) = pool.collect(recipient, position.tickLower, position.tickUpper, amount0Max, amount1Max);

    // sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected
    // instead of the actual amount so we can burn the token
    (position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Max, tokensOwed1 - amount1Max);
}

這個(gè)函數(shù)就是先用 pool.burn 函數(shù)來(lái)觸發(fā) pool 中 position 內(nèi)手續(xù)費(fèi)總額的更新香到,使其更新為當(dāng)前的最新值鱼冀。調(diào)用時(shí)傳入?yún)?shù)的 Liquidity 為 0,表示只是用來(lái)觸發(fā)手續(xù)費(fèi)總額的更新悠就,并沒(méi)有進(jìn)行流動(dòng)性的更新千绪。更新完成后,再調(diào)用 pool.collect 提取手續(xù)費(fèi)梗脾。

至此手續(xù)費(fèi)相關(guān)的管理就全部介紹完了荸型。Uniswap v3 還記錄了一個(gè) position 中發(fā)生交易的總時(shí)長(zhǎng),這個(gè)值可以用來(lái)計(jì)算一個(gè) position 處于活躍狀態(tài)的總時(shí)間數(shù)炸茧,用于 position 倉(cāng)位調(diào)整參考瑞妇,這部分計(jì)算因?yàn)楹唾M(fèi)率計(jì)算類(lèi)似稿静,內(nèi)容本文不再贅述,感興趣的讀者可以自行研究辕狰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末改备,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蔓倍,更是在濱河造成了極大的恐慌悬钳,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偶翅,死亡現(xiàn)場(chǎng)離奇詭異默勾,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)聚谁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)母剥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人形导,你說(shuō)我怎么就攤上這事媳搪。” “怎么了骤宣?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)序愚。 經(jīng)常有香客問(wèn)我憔披,道長(zhǎng),這世上最難降的妖魔是什么爸吮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任芬膝,我火速辦了婚禮,結(jié)果婚禮上形娇,老公的妹妹穿的比我還像新娘锰霜。我一直安慰自己,他們只是感情好桐早,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布癣缅。 她就那樣靜靜地躺著,像睡著了一般哄酝。 火紅的嫁衣襯著肌膚如雪友存。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天陶衅,我揣著相機(jī)與錄音屡立,去河邊找鬼。 笑死搀军,一個(gè)胖子當(dāng)著我的面吹牛膨俐,可吹牛的內(nèi)容都是我干的勇皇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼焚刺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼敛摘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起檩坚,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤着撩,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后匾委,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體拖叙,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年赂乐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了薯鳍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挨措,死狀恐怖挖滤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浅役,我是刑警寧澤斩松,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站觉既,受9級(jí)特大地震影響惧盹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瞪讼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一钧椰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧符欠,春花似錦嫡霞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至曾撤,卻和暖如春娄徊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盾戴。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工寄锐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓橄仆,卻偏偏與公主長(zhǎng)得像剩膘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盆顾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容