Uniswap Part Ⅳ | 交易手续费

交易手续费

计算原理

以普通用户的视角来看,对比 Uniswap v2,Uniswap v3 在手续费方面做了如下改动:

  • 添加流动性时,手续费可以有 3个级别供选择:0.05%, 0.3% 和 1%,未来可以通过治理加入更多可选的手续费率
  • Uniswap v2 中手续费会在收取后自动复投称为 LP 的一部分,即每次手续费都自动变成流动性加入池子中,而 Uniswap v3 中收取的手续费不会自动复投(主要是为了方便合约的计算),需要手动取出手续费
  • 不同手续费级别,在添加流动性时,价格可选值的最小粒度也不一样(这个是因为 tick spacing 的影响),一般来说,手续费越低,价格可选值越精细,因此官方推荐价格波动小的交易对使用低费率(例如稳定币交易对)

以开发者的视角来看,Uniswap v3 的手续费计算相对会比较复杂, 因为它需要针对每一个 position 来进行单独的计算,为了方便计算,在代码中会将手续费相关的元数据记录在 position 的边界 tick 上(这些 tick 上还存储了 ΔLΔL 等元数据)。

当我们计算交易的手续费时,我们需要计算如下值:

  • 每一个 position 收取的手续费(token0, token1 需要分别单独计算)
  • 用户如果提取了手续费,需要记录用户已提取的数值

v3 中有以下几个关于手续费的变量:

  • 交易池中手续费的费率值,这里记录的值时以 1000000 为基数的值,例如当手续费为 0.03% 时,费率值为 300
  • 全局状态变量 feeGrowthGlobal0X128feeGrowthGlobal1X128 ,分别表示 token0 和 token1 所累计的手续费总额,使用了 Q128.128 浮点数来记录
  • 对于每个 tick,记录了 feeGrowthOutside0X128feeGrowthOutside1X128,这两个变量记录了发生在此 tick 「外侧」的手续费总额,外侧指的是与当前价格所对应的 tick 相对于 tick i 的相反侧。
  • 对于每个 position,记录了此 position 内的手续费总额 feeGrowthInside0LastX128feeGrowthInside1LastX128,这个值不需要每次都更新,它只会在 position 发生变动,或者用户提取手续费时更新

需要注意的时,上面这些手续费状态变量都是每一份 LP 所对应的手续费,在计算真正的手续费时,需要使用 LP 数相乘来得出实际手续费数额,又因为 LP 数在不同价格可能时不同的(因为流动性深度不同),所以在计算手续费时只能针对 position 进行计算(同一个 position 内 LP 总量不变)。

image-20220925211502549

image-20220925211529269

image-20220925211548605

image-20220925211622048

image-20220925211636485

代码实现

手续费更新的代码在前面有提到过

这里看一下手续费的提取

core中手续费的提取是以 position 为单位进行提取的。使用 UniswapV3Pool.collect 提取手续费:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function collect(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount0Requested,
uint128 amount1Requested
) external override lock returns (uint128 amount0, uint128 amount1) {
// we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1}
// 获取 position 数据
Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);

// 根据参数调整需要提取的手续费
amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;
amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;

// 将手续费发送给用户
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);
}

但是这里 posiiton 中的手续费可能并不是最新的(手续费总数只会在 position 的流动性更新时更新)。因此在提取手续费前,需要主动触发一次手续费的更新,这些操作已经在 uniswap-v3-periphery 仓库中进行了封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function collect(CollectParams calldata params)
external
payable
override
isAuthorizedForToken(params.tokenId)
returns (uint256 amount0, uint256 amount1)
{
require(params.amount0Max > 0 || params.amount1Max > 0);
// allow collecting to the nft position manager address with address 0
address recipient = params.recipient == address(0) ? address(this) : params.recipient;

// 查询 position 信息
Position storage position = _positions[params.tokenId];

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

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

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

// trigger an update of the position fees owed and fee growth snapshots if it has any liquidity
// 这里再次更新一次手续费累积总额
if (position.liquidity > 0) {
// 使用 pool.burn() 来触发手续费的更新
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;
}

// compute the arguments to give to the pool#collect method
// 提取手续费的最大值,不能超过手续费总额
(uint128 amount0Collect, uint128 amount1Collect) =
(
params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max,
params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max
);

// the actual amounts collected are returned
// 调用 pool.collect 将手续费发送给 recipient
(amount0, amount1) = pool.collect(
recipient,
position.tickLower,
position.tickUpper,
amount0Collect,
amount1Collect
);

// 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 - amount0Collect, tokensOwed1 - amount1Collect);

emit Collect(params.tokenId, recipient, amount0Collect, amount1Collect);
}

这个函数就是先用 pool.burn 函数来触发 pool 中 position 内手续费总额的更新,使其更新为当前的最新值。调用时传入参数的 Liquidity 为 0,表示只是用来触发手续费总额的更新,并没有进行流动性的更新。更新完成后,再调用 pool.collect 提取手续费。


Uniswap Part Ⅳ | 交易手续费
http://sissice.github.io/2022/09/29/uniswap-v3-4/
作者
Sissice
发布于
2022年9月29日
许可协议