Uniswap Part Ⅱ | 提供/移除流动性

提供流动性

在合约内,v3 会保存所有用户的流动性,代码内称作 Position

1
2
3
4
5
User->NonfungiblePositionManager:mint
NonfungiblePositionManager->NonfungiblePositionManager:addLiquidity
NonfungiblePositionManager->UniswapV3Pool:mint
UniswapV3Pool->UniswapV3Pool:_modifyPosition
UniswapV3Pool->UniswapV3Pool:_updatePosition

NonfungiblePositionManager的mint函数实现初始的流动性的添加。increaseLiquidity函数实现了流动性的增加。这两个函数的逻辑基本一致,都是通过调用addLiquidity函数实现。mint需要额外创建ERC721的token。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
    /// @inheritdoc INonfungiblePositionManager
//mint函数实现初始的流动性的添加
function mint(MintParams calldata params)
external
payable
override
checkDeadline(params.deadline) //modifier,确保_blockTimestamp() <= deadline
returns (
uint256 tokenId,
uint128 liquidity,
uint256 amount0,
uint256 amount1
)
{
IUniswapV3Pool pool;
//添加流动性,并完成token0和token1的发送
//核心,在下面进行解释
(liquidity, amount0, amount1, pool) = addLiquidity(
AddLiquidityParams({
token0: params.token0,
token1: params.token1,
fee: params.fee,
recipient: address(this),
tickLower: params.tickLower,
tickUpper: params.tickUpper,
amount0Desired: params.amount0Desired,
amount1Desired: params.amount1Desired,
amount0Min: params.amount0Min,
amount1Min: params.amount1Min
})
);

//铸造 ERC721 token 给用户,用来代表用户所持有的流动性
_mint(params.recipient, (tokenId = _nextId++));

//由创建地址和边界return keccak256(abi.encodePacked(owner, tickLower, tickUpper))
bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
// feeGrowthGlobal0X128 和 feeGrowthGlobal1X128 ,分别表示此 position 内的 token0 和 token1 所累计的手续费总额。它只会在 position 发生变动,或者用户提取手续费时更新
//pool.positions是一个mapping,mapping(bytes32 => Position.Info) public override positions
//Position.Info是一个struct,在Position.sol中
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

// idempotent set
uint80 poolId =
cachePoolKey(
address(pool),
PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee})
);

//更新mapping _positions,用 ERC721 的 token ID 作为键,将用户提供流动性的元信息保存起来
_positions[tokenId] = Position({
nonce: 0,
operator: address(0),
poolId: poolId,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
liquidity: liquidity,
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
feeGrowthInside1LastX128: feeGrowthInside1LastX128,
tokensOwed0: 0,
tokensOwed1: 0
});

emit IncreaseLiquidity(tokenId, liquidity, amount0, amount1);
}

...

//increaseLiquidity函数实现了流动性的增加,与上面的mint类似
function increaseLiquidity(IncreaseLiquidityParams calldata params)
external
payable
override
checkDeadline(params.deadline)
returns (
uint128 liquidity,
uint256 amount0,
uint256 amount1
)
{
Position storage position = _positions[params.tokenId];

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

IUniswapV3Pool pool;
//核心
(liquidity, amount0, amount1, pool) = addLiquidity(
AddLiquidityParams({
token0: poolKey.token0,
token1: poolKey.token1,
fee: poolKey.fee,
tickLower: position.tickLower,
tickUpper: position.tickUpper,
amount0Desired: params.amount0Desired,
amount1Desired: params.amount1Desired,
amount0Min: params.amount0Min,
amount1Min: params.amount1Min,
recipient: address(this)
})
);

bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);

// this is now updated to the current transaction
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

position.tokensOwed0 += uint128(
//计算a×b÷denominator
FullMath.mulDiv(
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, //a
position.liquidity, //b
FixedPoint128.Q128 //denominator,这里等于0x100000000000000000000000000000000
)
);
position.tokensOwed1 += uint128(
FullMath.mulDiv(
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
position.liquidity,
FixedPoint128.Q128
)
);

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

emit IncreaseLiquidity(params.tokenId, liquidity, amount0, amount1);
}

addLiquidity

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
  struct AddLiquidityParams {
address token0; //token0的地址
address token1; //token1的地址
uint24 fee; //交易费率
address recipient; //流动性所属人地址
int24 tickLower; //流动性价格下限(以token0计价),这里传入的是 tick index
int24 tickUpper; //流动性价格上限(以token0计价),这里传入的是 tick index
uint256 amount0Desired; //
uint256 amount1Desired;
uint256 amount0Min; //提供的token0下限数
uint256 amount1Min; //提供的token1下限数
}

/// @notice Add liquidity to an initialized pool
function addLiquidity(AddLiquidityParams memory params)
internal
returns (
uint128 liquidity,
uint256 amount0,
uint256 amount1,
IUniswapV3Pool pool
)
{
//检索对应的流动性池
PoolAddress.PoolKey memory poolKey =
PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});

//不需要访问factory就可以计算出pool的地址,原理在上面的CREATE2部分
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

// compute the liquidity amount
//计算流动性的大小,详见下文
{
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper);

liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96, //意思是价格P的平方根, 然后左移了96位保存精度
sqrtRatioAX96,
sqrtRatioBX96,
params.amount0Desired,
params.amount1Desired
);
}

(amount0, amount1) = pool.mint(
params.recipient,
params.tickLower,
params.tickUpper,
liquidity,
//用于 pool 合约回调
abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender}))
);

require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
}

getLiquidityForAmounts

用来计算流动性

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
  /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current
/// pool prices and the prices at the tick boundaries
/// @param sqrtRatioX96 A sqrt price representing the current pool prices
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount0 The amount of token0 being sent in
/// @param amount1 The amount of token1 being sent in
/// @return liquidity The maximum amount of liquidity received
function getLiquidityForAmounts(
uint160 sqrtRatioX96, //当前矿池价格的平方根
uint160 sqrtRatioAX96, //第一个tick边界的价格的平方根
uint160 sqrtRatioBX96, //第二个tick边界的价格的平方根
uint256 amount0, //发送的token0的数量
uint256 amount1 //发送的token1的数量
//返回收到的最大流动性金额
) internal pure returns (uint128 liquidity) {
//排序,保证sqrtRatioAX96<sqrtRatioBX96
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
//情况1
if (sqrtRatioX96 <= sqrtRatioAX96) {
liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0);
//情况2
} else if (sqrtRatioX96 < sqrtRatioBX96) {
uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0);
uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1);

liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
//情况3
} else {
liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1);
}
}

情况1:当前池中的价格小于等于价格范围的最小值

image-20220819160147301

此时添加的流动性全部为x token

以上过程表示在getLiquidityForAmount0中

1
2
3
4
5
6
7
8
9
10
11
function getLiquidityForAmount0(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount0
) internal pure returns (uint128 liquidity) {
//排序
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
//sqrtRatioAX96*sqrtRatioBX96/FixedPoint96.Q96
uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96);
return toUint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96));
}

情况二:当前池中的价格在价格范围中

image-20220819160225006

此时添加的流动性包含两个币种,可以通过任意一个 token 数量计算出流动性

1
2
3
4
uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0);
uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1);

liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;

比较两种算法的结果,取更小的一个

情况三:当前池中的价格大于等于价格范围的最大值

image-20220819162404788

此时添加的流动性全部为y token

以上过程表示在getLiquidityForAmount0中

1
2
3
4
5
6
7
8
9
function getLiquidityForAmount1(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint256 amount1
) internal pure returns (uint128 liquidity) {
//排序
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
return toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96));
}

MintCallbackData

这里是为了将Position的owner和实际流动性token的支付者解耦,让合约来管理用户的流动性,并将流动性token化(ERC721)

用户调用NonfungiblePositionManager来提供流动性,所以Position的owner是NonfungiblePositionManager,NonfungiblePositionManager是通过NFT token将Position和用户关联起来的

这个函数可以将指定数量的token0与token1发送到合约中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct MintCallbackData {
PoolAddress.PoolKey poolKey;
address payer; //支付token的地址
}

/// @inheritdoc IUniswapV3MintCallback
function uniswapV3MintCallback(
uint256 amount0Owed,
uint256 amount1Owed,
bytes calldata data
) external override {
MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));
CallbackValidation.verifyCallback(factory, decoded.poolKey);

//根据传入的参数,使用transferFrom代用户向Pool中支付token
if (amount0Owed > 0) pay(decoded.poolKey.token0, decoded.payer, msg.sender, amount0Owed);
if (amount1Owed > 0) pay(decoded.poolKey.token1, decoded.payer, msg.sender, amount1Owed);
}

mint

位于UniswapV3Pool.sol

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
/// @inheritdoc IUniswapV3PoolActions
/// @dev noDelegateCall is applied indirectly via _modifyPosition
function mint(
address recipient, //创造流动性的地址
int24 tickLower, //流动性头寸下限
int24 tickUpper, //流动性头寸上限
uint128 amount, //增加的流动性数量
bytes calldata data //回调函数的参数
) external override lock returns (uint256 amount0, uint256 amount1) {
//检查增加的流动性的数量大于0
require(amount > 0);
//修改Position,添加流动性,详见下面的_modifyPosition部分
//返回需要投入的token0和token1的数量
(, int256 amount0Int, int256 amount1Int) =
_modifyPosition(
ModifyPositionParams({
owner: recipient,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: int256(amount).toInt128()
})
);

amount0 = uint256(amount0Int);
amount1 = uint256(amount1Int);

uint256 balance0Before;
uint256 balance1Before;
if (amount0 > 0) balance0Before = balance0();
if (amount1 > 0) balance1Before = balance1();
//回调函数将指定数量的token0和token1发送到合约中
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
//检查投入的token0和token1是否符合预期的数量
if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');

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

_modifyPosition

ModifyPositionParams用于存储Position(流动性)相关的数据信息,包括流动性所有者地址、流动性的上下限、流动性的改动:

1
2
3
4
5
6
7
8
9
struct ModifyPositionParams {
// the address that owns the position
address owner;
// the lower and upper tick of the position
int24 tickLower;
int24 tickUpper;
// any change in liquidity
int128 liquidityDelta;
}

_modifyPosition用于更新当前Position

添加流动性的规则

我们知道V3的核心公式

y = p*x

x*y = L^2

可以得到

x = L / √p

y = L * √p

当价格p在区间内时

image-20220819162436593

橙色区域是我们实际需要添加的流动性,虚拟流动性是绿色区域扣除橙色的部分的宽度和高度。

delta x 就是 p(红点) 和 p_upper 在 x 轴上的距离, delta y 就是 pp_lower 在 y 轴上的距离。

1
2
3
delta x = L / √p - L / √p_{upper} = L * (√p_{upper} - √p) / (√p * √p_{upper}) 

delta y = L * √p - L * √p_{lower} = L * (√p - √p_{lower})

再变换一下,改写成求 L(流动性数量)的等式

1
2
L = delta x * (√p * √p_{upper}) / (√p_{upper} - √p) 
L = delta y / √(p - p_{lower})

当价格p大于区间时

image-20220819162505525

1
L = delta y / √(p_{upper} - p_{lower})

当价格p小于区间时

image-20220819162537608

1
L = delta x * (√p_{upper} * √p_{lower}) / (√p_{upper} - √p_{lower})

转化为代码

注意:这里的amount0与amount1为int256类型,也就是说这里的amount0与amount1这两个返回值可正可负,如果为正则表示流动性提供至需要给池子给予的数量,为负数则表示池子需要给流动性提供者给予的数量

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
73
74
75
76
77
78
79
80
81
82
83
84
  /// @dev Effect some changes to a position
/// @param params the position details and the change to the position's liquidity to effect
/// @return position a storage pointer referencing the position with the given owner and tick range
/// @return amount0 the amount of token0 owed to the pool, negative if the pool should pay the recipient
/// @return amount1 the amount of token1 owed to the pool, negative if the pool should pay the recipient
function _modifyPosition(ModifyPositionParams memory params)
private
noDelegateCall
returns (
Position.Info storage position,
int256 amount0,
int256 amount1
)
{
//检查流动性上下限是否满足条件
checkTicks(params.tickLower, params.tickUpper);

//读入内存,这样后续可以通过MLOAD直接访问而不用重新去加载LOAD,从而节省gas
Slot0 memory _slot0 = slot0; // SLOAD for gas optimization

//创建或修改用户的position,后面有介绍
position = _updatePosition(
params.owner,
params.tickLower,
params.tickUpper,
params.liquidityDelta,
_slot0.tick
);

if (params.liquidityDelta != 0) {
if (_slot0.tick < params.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_ token0 (it's becoming more valuable) so user must provide it
//如果当前的trick小于tricklower,则所有的token1将转变为token0
//即liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower))
amount0 = SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
} else if (_slot0.tick < params.tickUpper) {
// current tick is inside the passed range
//如果当前trick小于trickupper
uint128 liquidityBefore = liquidity; // SLOAD for gas optimization

// write an oracle entry
//增加Oracle条目(预言机)
(slot0.observationIndex, slot0.observationCardinality) = observations.write(
_slot0.observationIndex,
_blockTimestamp(),
_slot0.tick,
liquidityBefore,
_slot0.observationCardinality,
_slot0.observationCardinalityNext
);

//计算amout0和amount1的增量
amount0 = SqrtPriceMath.getAmount0Delta(
_slot0.sqrtPriceX96,
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
_slot0.sqrtPriceX96,
params.liquidityDelta
);

//
liquidity = LiquidityMath.addDelta(liquidityBefore, params.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_ token1 (it's becoming more valuable) so user must provide it
//如果trick超过了trickupper则此时所有的token0将转变为token1
//使用TickMath 库中的 getSqrtRatioAtTick 来通过 tick index 计算其所对应的价格
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
}
}
}

getAmountDelta

SqrtPriceMath 库中

在具体的计算过程中,分成了 RoundUp 和 RoundDown 两种情况,简单来说:

  1. 当提供/增加流动性时,会使用 RoundUp,这样可以保证增加数量为 L 的流动性时,用户提供足够的 token 到 pool 中
  2. 当移除/减少流动性时,会使用 RoundDown,这样可以保证减少数量为 L 的流动性时,不会从 pool 中给用户多余的 token
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
73
74
75
76
77
78
79
80
81
82
83
84
85
    /// @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 sqrtRatioAX96 A sqrt price
/// @param sqrtRatioBX96 Another sqrt price
/// @param liquidity The amount of usable liquidity
/// @param roundUp Whether to round the amount up or down
/// @return amount0 Amount of token0 required to cover a position of size liquidity between the two passed prices
function getAmount0Delta(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint128 liquidity,
bool roundUp
) internal pure returns (uint256 amount0) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);

uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION;
uint256 numerator2 = sqrtRatioBX96 - sqrtRatioAX96;

require(sqrtRatioAX96 > 0);

return
roundUp
//返回ceil(x / y)
? UnsafeMath.divRoundingUp(
//numerator1*numerator2/sqrtRatioBX96并取整
FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96),
sqrtRatioAX96
)
: FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96;
}

/// @notice Gets the amount1 delta between two prices
/// @dev Calculates liquidity * (sqrt(upper) - sqrt(lower))
/// @param sqrtRatioAX96 A sqrt price
/// @param sqrtRatioBX96 Another sqrt price
/// @param liquidity The amount of usable liquidity
/// @param roundUp Whether to round the amount up, or down
/// @return amount1 Amount of token1 required to cover a position of size liquidity between the two passed prices
function getAmount1Delta(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
uint128 liquidity,
bool roundUp
) internal pure returns (uint256 amount1) {
if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);

return
roundUp
? FullMath.mulDivRoundingUp(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96)
: FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96);
}

/// @notice Helper that gets signed token0 delta
/// @param sqrtRatioAX96 A sqrt price
/// @param sqrtRatioBX96 Another sqrt price
/// @param liquidity The change in liquidity for which to compute the amount0 delta
/// @return amount0 Amount of token0 corresponding to the passed liquidityDelta between the two prices
function getAmount0Delta(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
int128 liquidity
) internal pure returns (int256 amount0) {
return
liquidity < 0
? -getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), false).toInt256()
: getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), true).toInt256();
}

/// @notice Helper that gets signed token1 delta
/// @param sqrtRatioAX96 A sqrt price
/// @param sqrtRatioBX96 Another sqrt price
/// @param liquidity The change in liquidity for which to compute the amount1 delta
/// @return amount1 Amount of token1 corresponding to the passed liquidityDelta between the two prices
function getAmount1Delta(
uint160 sqrtRatioAX96,
uint160 sqrtRatioBX96,
int128 liquidity
) internal pure returns (int256 amount1) {
return
liquidity < 0
? -getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), false).toInt256()
: getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), true).toInt256();
}
}

_updatePosition

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/// @dev Gets and updates a position with the given liquidity delta
/// @param owner the owner of the position
/// @param tickLower the lower tick of the position's tick range
/// @param tickUpper the upper tick of the position's tick range
/// @param tick the current tick, passed to avoid sloads
function _updatePosition(
address owner,
int24 tickLower,
int24 tickUpper,
int128 liquidityDelta,
int24 tick
) private returns (Position.Info storage position) {
//获取用户的position
position = positions.get(owner, tickLower, tickUpper);

uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128; // SLOAD for gas optimization
uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; // SLOAD for gas optimization

// if we need to update the ticks, do it
//根据传入的参数修改position中的lower/upper tick
bool flippedLower;
bool flippedUpper;
if (liquidityDelta != 0) {
uint32 time = _blockTimestamp();
//获取请求时间点的Oracle数据
(int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =
observations.observeSingle(
time,
0,
slot0.tick,
slot0.observationIndex,
liquidity,
slot0.observationCardinality
);

// 更新 lower tikc 和 upper tick
// fippedX 变量表示是此 tick 的引用状态是否发生变化,即
// 被引用 -> 未被引用 或
// 未被引用 -> 被引用
// 后续需要根据这个变量的值来更新 tick 位图
flippedLower = ticks.update(
tickLower,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
false,
maxLiquidityPerTick
);
flippedUpper = ticks.update(
tickUpper,
tick,
liquidityDelta,
_feeGrowthGlobal0X128,
_feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
true,
maxLiquidityPerTick
);

// 如果一个 tick 第一次被引用,或者移除了所有引用
// 那么更新 tick 位图
if (flippedLower) {
tickBitmap.flipTick(tickLower, tickSpacing);
}
if (flippedUpper) {
tickBitmap.flipTick(tickUpper, tickSpacing);
}
}

(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);

//更新position
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);

// clear any tick data that is no longer needed
// 如果移除了对 tick 的引用,那么清除之前记录的元数据
// 这只会发生在移除流动性的操作中
if (liquidityDelta < 0) {
if (flippedLower) {
ticks.clear(tickLower);
}
if (flippedUpper) {
ticks.clear(tickUpper);
}
}
}

tick

V3使用的等幂数列

1
2
3
p_{i}=1.0001^i
//调整一下
√p_{i}=(√1.0001)^i

这里的 i 也就是价格的序号,我们称之为 tick,而由所有序号组成的集合称之为 Ticks。在合约代码中,主要是以 tick 来记录流动性的区间。

UniswapV3Pool 合约中有两个状态变量记录了 tick 相关的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // tick 元数据管理的库
using Tick for mapping(int24 => Tick.Info);
// tick 位图槽位的库
using TickBitmap for mapping(int16 => uint256);

// 记录了一个 tick 包含的元数据,这里只会包含所有 Position 的 lower/upper ticks
mapping(int24 => Tick.Info) public override ticks;
// tick 位图,因为这个位图比较长(一共有 887272x2 个位),大部分的位不需要初始化
// 因此分成两级来管理,每 256 位为一个单位,一个单位称为一个 word
// map 中的键是 word 的索引
mapping(int16 => uint256) public override tickBitmap;

library Tick {
...
// tick 中记录的数据
struct Info {
// 记录了所有引用这个 tick 的 position 流动性的和
uint128 liquidityGross;
// 当此 tick 被越过时(从左至右),池子中整体流动性需要变化的值
int128 liquidityNet;
...
}

tick 位图用于记录所有被引用的 lower/upper tick index,我们可以用过 tick 位图,从当前价格找到下一个(从左至右或者从右至左)被引用的 tick index。

tick 位图有以下几个特性:

  • 对于不存在的 tick,不需要初始值,因为访问 map 中不存在的 key 默认值就是 0
  • 通过对位图的每个 word(uint256) 建立索引来管理位图,即访问路径为 word index -> word -> tick bit

liquidityGross: 很好理解,每当有流动性将该 tick 设为价格区间时,不论是价格上限还是价格下限, liquidityGross 都会增加。换言之,当 liquidityGross > 0 说明该 tick 已经初始化,正在被流动性使用,而 liquidityGross == 0 则该 tick 未初始化,没有流动性使用,计算时可以忽略。

liquidityNet 表示当价格从左至右经过此 tick 时整体流动性需要变化的净值。在单个流动性中,对于 lower tick 来说,它的值为正,对于 upper tick 来说它的值为 负。

在注入或移除数量为 l 的流动性时,具体规则如下:

  • 注入流动性,tick 是价格下限,liquidityNet 增加 l
  • 注入流动性,tick 是价格上限,liquidityNet 减少 l
  • 移除流动性,tick 是价格下限,liquidityNet 减少 l
  • 移除流动性,tick 是价格上限,liquidityNet 增加 l

在Tick.sol中,update用于更新 tick 元数据,此函数返回的 flipped 表示此 tick 的引用状态是否发生变化,之前的 _updatePosition 中的代码会根据这个返回值去更新 tick 位图

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
function update(
mapping(int24 => Tick.Info) storage self,
int24 tick,
int24 tickCurrent,
int128 liquidityDelta,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
uint160 secondsPerLiquidityCumulativeX128,
int56 tickCumulative,
uint32 time,
bool upper,
uint128 maxLiquidity
) internal returns (bool flipped) {
Tick.Info storage info = self[tick];

uint128 liquidityGrossBefore = info.liquidityGross;
uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);

require(liquidityGrossAfter <= maxLiquidity, 'LO');

//通过 liquidityGross 在进行 position 变化前后的值来判断 tick 是否仍被引用
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 <= tickCurrent) {
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128;
info.tickCumulativeOutside = tickCumulative;
info.secondsOutside = time;
}
info.initialized = true;
}

info.liquidityGross = liquidityGrossAfter;

// when the lower (upper) tick is crossed left to right (right to left), liquidity must be added (removed)
//更新liquidityNet的值
info.liquidityNet = upper
? int256(info.liquidityNet).sub(liquidityDelta).toInt128()
: int256(info.liquidityNet).add(liquidityDelta).toInt128();
}

tickspacing

V3 引入了费率三档可选等级和相应的 tick 疏密程度,也就是 tickspacing 。对于每一种交易对而言,都有三档可选费率等级,0.05%, 0.3%, 1%,并且以后通过社区治理,还有可能永久增加可选的挡位。每种交易费率等级都由给定的 tickspacing,比如稳定币交易对,就是 tick 之间需要间隔 10 个才是有效的可使用的 tick 。位于间隔内的 tick 虽然存在,但程序不会去初始化和使用,也就不会产生 gas 费用。因此,我们在等幂数列的基础上,进一步节省了计算消耗。

费率 tickspacing 建议的使用范围
0.05% 10 稳定币交易对
0.3% 60 适用大多数交易对
1% 200 波动极大的交易对

在UniswapV3Factory.sol中设定

1
2
3
4
5
6
7
8
9
10
11
12
13
mapping(uint24 => int24) public override feeAmountTickSpacing;

constructor() {
owner = msg.sender;
emit OwnerChanged(address(0), msg.sender);

feeAmountTickSpacing[500] = 10;
emit FeeAmountEnabled(500, 10);
feeAmountTickSpacing[3000] = 60;
emit FeeAmountEnabled(3000, 60);
feeAmountTickSpacing[10000] = 200;
emit FeeAmountEnabled(10000, 200);
}

移除流动性

在合约UniswapV3Pool中,burn用来实现流动性的移除

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
function burn(
int24 tickLower,
int24 tickUpper,
uint128 amount
) external override lock returns (uint256 amount0, uint256 amount1) {
//计算需要移除的token数
(Position.Info storage position, int256 amount0Int, int256 amount1Int) =
_modifyPosition(
ModifyPositionParams({
owner: msg.sender,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: -int256(amount).toInt128()
})
);

amount0 = uint256(-amount0Int);
amount1 = uint256(-amount1Int);

//注意这里,移除流动性后,将移出的 token 数记录到了 position.tokensOwed 上
if (amount0 > 0 || amount1 > 0) {
(position.tokensOwed0, position.tokensOwed1) = (
position.tokensOwed0 + uint128(amount0),
position.tokensOwed1 + uint128(amount1)
);
}

emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);
}


Uniswap Part Ⅱ | 提供/移除流动性
http://sissice.github.io/2022/08/20/uniswap-v3-2/
作者
Sissice
发布于
2022年8月20日
许可协议