Uniswap V3 —— Core 解析

Uniswap V3 引入了**集中流动性(Concentrated Liquidity)**概念,允许 LP 在自定义价格范围内分配资金,极大提高资本效率。

1. 概述

Uniswap V3 Core 包含以下核心合约:

合约 功能
UniswapV3Factory 工厂合约,创建和管理交易池,控制多级费率
UniswapV3Pool 交易池合约,实现 AMM 核心逻辑、管理流动性头寸
UniswapV3PoolDeployer 池部署器,优化部署 Gas 消耗
NoDelegateCall 安全模块,禁用 delegatecall 防范漏洞

2. UniswapV3Factory 合约解析

UniswapV3Factory 合约主要用来创建不同代币对的流动性池。

2.1 状态变量与费率配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 费率与tick间距的映射
mapping(uint24 => int24) public override feeAmountTickSpacing;

// 存储所有池的映射:token0 => token1 => fee => pool
mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;

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

// 初始化三个默认费率层级
feeAmountTickSpacing[500] = 10; // 0.05% 费率,tick间距10
emit FeeAmountEnabled(500, 10);
feeAmountTickSpacing[3000] = 60; // 0.30% 费率,tick间距60
emit FeeAmountEnabled(3000, 60);
feeAmountTickSpacing[10000] = 200; // 1.00% 费率,tick间距200
emit FeeAmountEnabled(10000, 200);
}

2.2 createPool 函数

createPoolUniswapV3Factory 的核心函数,用于创建一个新的流动性池(Pool)

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
function createPool(
address tokenA,
address tokenB,
uint24 fee
) external override noDelegateCall returns (address pool) {

// 禁止使用相同的代币地址(防止创建 A/A 池)
require(tokenA != tokenB);
// 交易对按地址从小到大排序,保证代币对的唯一性
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
// 非零地址检查
require(token0 != address(0));

// 查询费率的 Tick 间距,Tick 间距不能为 0
int24 tickSpacing = feeAmountTickSpacing[fee];
require(tickSpacing != 0);

// 防止相同代币对 + 相同费率的池重复创建
require(getPool[token0][token1][fee] == address(0));

pool = deploy(address(this), token0, token1, fee, tickSpacing);

// 双向存储池地址,eg. ETH/USDC 和 USDC/ETH 共用一个地址方便查询
getPool[token0][token1][fee] = pool;
getPool[token1][token0][fee] = pool;
emit PoolCreated(token0, token1, fee, tickSpacing, pool);
}

3. UniswapV3PoolDeployer 合约解析

createPool 调用了 UniswapV3PoolDeployerdeploy 方法,负责创建新的 UniswapV3Pool 合约,并在部署时传递初始化参数(factory、token0、token1、fee、tickSpacing)

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
contract UniswapV3PoolDeployer is IUniswapV3PoolDeployer {

// 定义结构体
struct Parameters {
address factory;
address token0;
address token1;
uint24 fee;
int24 tickSpacing;
}
// 定义 Parameters 类型的变量
Parameters public override parameters;


function deploy(
address factory,
address token0,
address token1,
uint24 fee,
int24 tickSpacing
) internal returns (address pool) {

// 对 parameters 赋值
parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
// 使用 token0、token1 和 fee 三个字段拼接的哈希值作为盐值
pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
// 删除临时 parameters
delete parameters;
}
}

4. NoDelegateCall 合约解析

回到 UniswapV3Factory 合约的 createPool 函数,函数体里还加了 noDelegateCall 的函数修饰器,在UniswapV3Factory 合约中,createPool 是一个敏感操作,必须确保这个函数只能被 Factory 合约本身调用,不能被恶意合约通过 DelegateCall劫持。当使用 delegatecall 调用 createPool 函数的时候,那 address(this) 将是发起 delegatecall 的地址,而不是当前的工厂合约地址

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
abstract contract NoDelegateCall {
// 存储该合约原始部署时的地址,immutable 类型变量是不可变的编译期常量,会被写进字节码并不可修改
address private immutable original;

// 构造函数:在合约部署时执行一次
constructor() {
// 记录合约自身的地址作为“原始地址”
// 注意:immutable 变量在部署阶段就被写入字节码中,之后无法修改
// Immutables are computed in the init code of the contract, and then inlined into the deployed bytecode.
// In other words, this variable won't change when it's checked at runtime.
original = address(this);
}

// 私有函数:检查当前执行上下文是否为合约本身
// 如果通过 delegatecall 调用该合约,address(this) 会变成调用者的地址
function checkNotDelegateCall() private view {
// 要求当前上下文地址与原始部署地址一致
require(address(this) == original);
}

// 防止函数通过 delegatecall 被调用
modifier noDelegateCall() {
checkNotDelegateCall();
_;
}
}

5. UniswapV3Pool 合约解析

5.1 Slot0 - 池的核心状态

1
2
3
4
5
6
7
8
9
struct Slot0 {
uint160 sqrtPriceX96; // 当前价格的平方根(Q64.96格式)
int24 tick; // 当前价格对应的tick
uint16 observationIndex; // Oracle观察点索引
uint16 observationCardinality; // Oracle数组大小
uint16 observationCardinalityNext;
uint8 feeProtocol; // 协议费率
bool unlocked; // 重入锁
}

5.2 Position - 流动性头寸

1
2
3
4
5
6
7
8
9
10
mapping(bytes32 => Position.Info) public positions;
// key = keccak256(owner, tickLower, tickUpper)

struct Info {
uint128 liquidity; // 该头寸的流动性
uint256 feeGrowthInside0LastX128; // 费用累计快照
uint256 feeGrowthInside1LastX128;
uint128 tokensOwed0; // 待提取的token0
uint128 tokensOwed1; // 待提取的token1
}

5.3 核心函数

UniswapV3Pool 核心的函数在 IUniswapV3PoolActions 接口里有定义,该接口共定义了 7 个函数:

  • initialize:初始化 slot0 状态
  • mint:添加流动性
  • collect:提取收益
  • burn:移除流动性
  • swap:兑换
  • flash:闪电贷
  • increaseObservationCardinalityNext:扩展 observations 数组可存储的容量

5.3.1 initialize - 初始化函数

initialize 通常会在第一次添加流动性时被调用,主要会初始化 slot0 状态变量,其中 sqrtPriceX96 是直接作为入参传入的,因为第一次添加流动性时,价格其实是由 LP 自己定的。初始的 tick 则是根据 sqrtPriceX96 计算出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function initialize(uint160 sqrtPriceX96) external override {
// Pool 只能被初始化一次,确保当前 sqrtPriceX96 是 0
require(slot0.sqrtPriceX96 == 0, 'AI');

// 根据 sqrtPriceX96 推算当前 tick
int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);

// 初始化 observations(价格预言机数据)
(uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());

// 设置 pool 的 Slot0 主状态
slot0 = Slot0({
sqrtPriceX96: sqrtPriceX96, // 当前价格(Q96 定点数)
tick: tick, // 当前 tick
observationIndex: 0, // 当前 observation 指针
observationCardinality: cardinality, // 有效 observation 数量
observationCardinalityNext: cardinalityNext,
feeProtocol: 0, // 协议费
unlocked: true // true: 解锁(允许交互)
});

// 触发事件,记录初始化数据
emit Initialize(sqrtPriceX96, tick);
}

5.3.2 mint - 提供流动性

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
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);

// 修改头寸(增加流动性)
(, int256 amount0Int, int256 amount1Int) =
_modifyPosition(
ModifyPositionParams({
owner: recipient, // 头寸属于谁
tickLower: tickLower, // 下界
tickUpper: tickUpper, // 上界
liquidityDelta: int256(amount).toInt128() // 增加的流动性
})
);

// 返回需要用户支付的 token0/token1 数量
amount0 = uint256(amount0Int);
amount1 = uint256(amount1Int);

uint256 balance0Before;
uint256 balance1Before;

// 记录池子余额,为后续检查使用
if (amount0 > 0) balance0Before = balance0();
if (amount1 > 0) balance1Before = balance1();

// 调用 mint callback,让用户转 token 给池子
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);

// 安全检查:callback 之后余额必须增加正确的数量
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 -

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
function _modifyPosition(ModifyPositionParams memory params)
private
noDelegateCall
returns (
Position.Info storage position,
int256 amount0,
int256 amount1
)
{
checkTicks(params.tickLower, params.tickUpper);

Slot0 memory _slot0 = slot0; // SLOAD for gas optimization

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
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
uint128 liquidityBefore = liquidity; // SLOAD for gas optimization

// write an oracle entry
(slot0.observationIndex, slot0.observationCardinality) = observations.write(
_slot0.observationIndex,
_blockTimestamp(),
_slot0.tick,
liquidityBefore,
_slot0.observationCardinality,
_slot0.observationCardinalityNext
);

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
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
}
}
}

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
function _updatePosition(
address owner,
int24 tickLower,
int24 tickUpper,
int128 liquidityDelta,
int24 tick
) private returns (Position.Info storage 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
bool flippedLower;
bool flippedUpper;
if (liquidityDelta != 0) {
uint32 time = _blockTimestamp();
(int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =
observations.observeSingle(
time,
0,
slot0.tick,
slot0.observationIndex,
liquidity,
slot0.observationCardinality
);

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
);

if (flippedLower) {
tickBitmap.flipTick(tickLower, tickSpacing);
}
if (flippedUpper) {
tickBitmap.flipTick(tickUpper, tickSpacing);
}
}

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

position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);

// clear any tick data that is no longer needed
if (liquidityDelta < 0) {
if (flippedLower) {
ticks.clear(tickLower);
}
if (flippedUpper) {
ticks.clear(tickUpper);
}
}
}

5.3.3 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) {
// 修改头寸(减少流动性)
(Position.Info storage position, int256 amount0Int, int256 amount1Int) =
_modifyPosition(
ModifyPositionParams({
owner: msg.sender, // 当前 LP
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: -int256(amount).toInt128() // 负数 → 移除流动性
})
);

// LP 应得的 token0 和 token1
amount0 = uint256(-amount0Int);
amount1 = uint256(-amount1Int);

// 如果有应得的 token,就记录到 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);
}

5.3.4 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) {
// 获取用户在该 tick 区间的头寸信息(储存了 tokensOwed)
Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);

// 应支付给用户的 token0 数量 = min(请求数量, 实际可领取数量)
amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;
amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;

// 如果 token0 有应领取的,先从 owed 中减去,再转账
if (amount0 > 0) {
position.tokensOwed0 -= amount0;
TransferHelper.safeTransfer(token0, recipient, amount0);
}

// token1 同理
if (amount1 > 0) {
position.tokensOwed1 -= amount1;
TransferHelper.safeTransfer(token1, recipient, amount1);
}

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

5.3.5 swap - 交易核心

swap 函数是 V3 最复杂的部分,实现了逐 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
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// 核心 swap 函数:
// - zeroForOne = true 表示 token0 → token1
// - zeroForOne = false 表示 token1 → token0
// - amountSpecified > 0 表示 exactInput(输入固定)
// - amountSpecified < 0 表示 exactOutput(输出固定)
function swap(
address recipient, // 最终接收输出代币的地址
bool zeroForOne, // 交易方向,token0→token1 为 true
int256 amountSpecified, // exactInput 或 exactOutput 的数量
uint160 sqrtPriceLimitX96, // 本次 swap 可允许触达的价格下限/上限
bytes calldata data // 传给 callback 的数据
) external override noDelegateCall returns (int256 amount0, int256 amount1) {

// 不能为 0,否则 swap 没意义
require(amountSpecified != 0, 'AS');

// 读取插槽变量 slot0(包含 tick、价格、锁状态、观察者指针等)
Slot0 memory slot0Start = slot0;

// 必须未锁(防止重入)
require(slot0Start.unlocked, 'LOK');

// 检查 sqrtPriceLimit 是否朝正确方向移动,避免 invalid price 移动
require(
zeroForOne
? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96
&& sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO
: sqrtPriceLimitX96 > slot0Start.sqrtPriceX96
&& sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO,
'SPL'
);

// 上锁(整个 swap 期间锁住 pool)
slot0.unlocked = false;

// 初始化 SwapCache,用于缓存 swap 过程中的共享信息
SwapCache memory cache =
SwapCache({
liquidityStart: liquidity, // 初始总流动性
blockTimestamp: _blockTimestamp(), // 当前时间戳,用于 oracle
feeProtocol: zeroForOne
? (slot0Start.feeProtocol % 16) // token0 手续费协议分成
: (slot0Start.feeProtocol >> 4), // token1 手续费协议分成
secondsPerLiquidityCumulativeX128: 0, // 初始化
tickCumulative: 0, // 初始化
computedLatestObservation: false // 标记是否查询过 oracle
});

// exactInput = true => 用户提供输入 A,求输出 B
bool exactInput = amountSpecified > 0;

// 初始化 swap 状态
SwapState memory state =
SwapState({
amountSpecifiedRemaining: amountSpecified, // 输入/输出剩余数量
amountCalculated: 0, // 已计算的输出/输入数量
sqrtPriceX96: slot0Start.sqrtPriceX96, // 当前价格
tick: slot0Start.tick, // 当前 tick
feeGrowthGlobalX128: zeroForOne
? feeGrowthGlobal0X128 // 使用哪一边的 feeGrowth
: feeGrowthGlobal1X128,
protocolFee: 0, // 累计协议收入
liquidity: cache.liquidityStart // 当前活跃区间的流动性
});

// 主循环:只要没完成指定数量,并且价格没到限制,就继续 swap
while (state.amountSpecifiedRemaining != 0
&& state.sqrtPriceX96 != sqrtPriceLimitX96) {

StepComputations memory step;

// 当前步骤的起始价格
step.sqrtPriceStartX96 = state.sqrtPriceX96;

// 查找下一个初始化 tick(是否存在流动性边界)
(step.tickNext, step.initialized) = tickBitmap
.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);

// 限制 tickNext 不越界(bitmap 不知道 min/max)
if (step.tickNext < TickMath.MIN_TICK) {
step.tickNext = TickMath.MIN_TICK;
} else if (step.tickNext > TickMath.MAX_TICK) {
step.tickNext = TickMath.MAX_TICK;
}

// 计算 tickNext 对应的 sqrt(price)
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

// 调用 SwapMath,计算该步:
// - 新价格
// - 本 step 的输入 amountIn
// - 输出 amountOut
// - 本 step 手续费 feeAmount
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) =
SwapMath.computeSwapStep(
state.sqrtPriceX96,
(
zeroForOne
? step.sqrtPriceNextX96 < sqrtPriceLimitX96
: step.sqrtPriceNextX96 > sqrtPriceLimitX96
)
? sqrtPriceLimitX96
: step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
fee
);

//
// 处理 exactInput / exactOutput 两种计算方式
//
if (exactInput) {
// 输入用掉(含 fee),输出累加
state.amountSpecifiedRemaining -=
(step.amountIn + step.feeAmount).toInt256();

state.amountCalculated -= step.amountOut.toInt256();
} else {
// 输出减少,输入累加
state.amountSpecifiedRemaining +=
step.amountOut.toInt256();

state.amountCalculated +=
(step.amountIn + step.feeAmount).toInt256();
}

// 协议分成(如 feeProtocol > 0)
if (cache.feeProtocol > 0) {
uint256 delta = step.feeAmount / cache.feeProtocol; // 分给协议的部分
step.feeAmount -= delta; // 剩余的计入 LP feeGrowth
state.protocolFee += uint128(delta);
}

// 更新 feeGrowthGlobal
if (state.liquidity > 0)
state.feeGrowthGlobalX128 += FullMath.mulDiv(
step.feeAmount,
FixedPoint128.Q128,
state.liquidity
);

//
// 是否跨过 tickNext
//
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
// tickNext 是已初始化的 tick,需要计算 liquidityNet
if (step.initialized) {

// 第一次跨 tick 才计算 oracle(节省 gas)
if (!cache.computedLatestObservation) {
(cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) =
observations.observeSingle(
cache.blockTimestamp,
0,
slot0Start.tick,
slot0Start.observationIndex,
cache.liquidityStart,
slot0Start.observationCardinality
);
cache.computedLatestObservation = true;
}

// 计算 liquidityNet(可能会增加或减少)
int128 liquidityNet =
ticks.cross(
step.tickNext,
(zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
(zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
cache.secondsPerLiquidityCumulativeX128,
cache.tickCumulative,
cache.blockTimestamp
);

// 当 zeroForOne 时(价格左移),liquidityNet 要取反
if (zeroForOne) liquidityNet = -liquidityNet;

// 更新当前活跃区间流动性
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}

// next tick 成为 current tick(左移时 -1)
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;

} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
// 若未跨到完整 tick,则根据 sqrtPrice 反算 tick
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
}

//
// swap 结束,更新 slot0
//
if (state.tick != slot0Start.tick) {
// tick 变了,需要写 oracle observation
(uint16 observationIndex, uint16 observationCardinality) =
observations.write(
slot0Start.observationIndex,
cache.blockTimestamp,
slot0Start.tick,
cache.liquidityStart,
slot0Start.observationCardinality,
slot0Start.observationCardinalityNext
);

// 更新存储
(
slot0.sqrtPriceX96,
slot0.tick,
slot0.observationIndex,
slot0.observationCardinality
) = (
state.sqrtPriceX96,
state.tick,
observationIndex,
observationCardinality
);
} else {
// tick 未变化只更新价格
slot0.sqrtPriceX96 = state.sqrtPriceX96;
}

// 如果流动性变化,则写回
if (cache.liquidityStart != state.liquidity)
liquidity = state.liquidity;

// 更新 feeGrowthGlobal 和协议费用
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;
}

// 计算最终的交易结果
(amount0, amount1) = (
zeroForOne == exactInput
? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
: (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining)
);

//
// 最终的外部代币转账与回调校验
//
if (zeroForOne) {
// 如果 output token1 需要转给 recipient
if (amount1 < 0)
TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));

// 检查 callback 是否真的支付了 amount0
uint256 balance0Before = balance0();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
require(balance0Before + uint256(amount0) <= balance0(), 'IIA');

} else {
if (amount0 < 0)
TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));

uint256 balance1Before = balance1();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
require(balance1Before + uint256(amount1) <= balance1(), 'IIA');
}

// 记录事件
emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.liquidity, state.tick);

// 解锁 pool
slot0.unlocked = true;
}

5.3.6 flash - 闪电贷

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
function flash(
address recipient,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external override lock noDelegateCall {
uint128 _liquidity = liquidity;
// 要求池子必须有流动性,否则不能闪电贷
require(_liquidity > 0, 'L');

// 计算闪电贷手续费: fee * amount / 1e6 (fee 是池子 swap 手续费)
uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);
uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);
// 在借出前记录池子余额,之后用于比较是否还款足够
uint256 balance0Before = balance0();
uint256 balance1Before = balance1();

// 借出 token 给调用者
if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);
if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);

// 调用用户的 flash callback,让用户执行套利或其他逻辑并归还借款
IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);

// 借款者逻辑执行后,重新检查余额
uint256 balance0After = balance0();
uint256 balance1After = balance1();

// 必须归还 ≥ 本金 + 手续费,否则 revert
require(balance0Before.add(fee0) <= balance0After, 'F0');
require(balance1Before.add(fee1) <= balance1After, 'F1');

// 支付金额 = 余额增长部分(因为借出时余额减少,在 callback 归还时会增加)
uint256 paid0 = balance0After - balance0Before;
uint256 paid1 = balance1After - balance1Before;

// 将手续费加入全局 feeGrowth,按 LP 流动性积分到每个位置
if (paid0 > 0) {
uint8 feeProtocol0 = slot0.feeProtocol % 16;
uint256 fees0 = feeProtocol0 == 0 ? 0 : paid0 / feeProtocol0;
if (uint128(fees0) > 0) protocolFees.token0 += uint128(fees0);
feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity);
}
// 同理对 token1
if (paid1 > 0) {
uint8 feeProtocol1 = slot0.feeProtocol >> 4;
uint256 fees1 = feeProtocol1 == 0 ? 0 : paid1 / feeProtocol1;
if (uint128(fees1) > 0) protocolFees.token1 += uint128(fees1);
feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);
}

emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1);
}