Uniswap Part Ⅲ | 交易过程

交易过程

SwapRouter 合约中封装了面向用户的交易接口:

  • exactInput:指定交易对路径,付出的 x token 数和预期得到的最小 y token 数(x, y 可以互换)
  • exactOutput:指定交易路径,付出的 x token 最大数和预期得到的 y token 数(x, y 可以互换)

关于 exactInput 这个接口,调用流程如下:

1
2
3
4
User->SwapRouter:exactInput
SwapRouter->SwapRouter:exactInputInternal
SwapRouter->UniswapV3Pool:swap
UniswapV3Pool->UniswapV3Pool:computeSwapStep

原理

image-20220819175714261

在V2中,流动性是均匀分布的,而V3中因为一个交易池中会有多个不同深度的流动池(每一个可以单独设置交易价格区间),因此一次交易的过程可能跨越多个不同的深度

image-20220819175735791

以上是真实情况中的流动性分布

现在假设价格处于一个持续上涨的趋势,当价格开始从左至右穿过这些价格区间时,会不断用一种资产换取另一种资产,而被换出的资产储备是不断减小的,一旦当前价格区间的某种资产被耗尽,价格会穿过当前区间,进入下一个区间,由此产生了价格的变化(因为价格右移了,变大了)。在价格移动消耗池内资产数量的过程中,输入的资产数量也会不断减少,一旦在某个区间输入资产被耗尽,那么价格就会停留在该区间内。

当然只是让价格停留在区间内,是不精确的,这个时候我们就需要借助计算添加流动性推导出来的公式,去反推计算出一个精确的价格(在该价格区间内)。

exactInput

exactInput函数在合约SwapRouter中,其中struct ExactInputParams写在接口ISwapRouter中

这里使用一个循环遍历传入的路径,路径中包含了交易过程中所有的 token,每相邻的两个 token 组成了一个交易对。例如当需要通过 ETH -> USDC -> DAI 路径进行交易时,会经过两个池:ETH/USDCUSDC/DAI,最终得到 DAI 代币。如前所述,这里其实还包含了每个交易对所选择的费率。

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
struct ExactInputParams {
bytes path; //路径
address recipient; //收款地址
uint256 deadline; //交易有效期
uint256 amountIn; //输入的 token 数(输入的 token 地址就是 path 中的第一个地址)
uint256 amountOutMinimum; //预期交易最少获得的 token 数(获得的 token 地址就是 path 中最后一个地址)
}

function exactInput(ExactInputParams memory params)
external
payable
override
checkDeadline(params.deadline)
returns (uint256 amountOut)
{
address payer = msg.sender; // msg.sender pays for the first hop

//遍历传入的路径,进行交易
while (true) {
bool hasMultiplePools = params.path.hasMultiplePools();

// the outputs of prior swaps become the inputs to subsequent ones
//完成当前路径的交易
params.amountIn = exactInputInternal(
params.amountIn,
//如果是中间交易,本合约代为收取和支付中间代币
//这里会判断是否是最后一次交易,当是最后一次交易时,获取的 token 的地址才是用户的指定的地址
hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies
0,
//回调函数
SwapCallbackData({
path: params.path.getFirstPool(), // only the first pool in the path is necessary
payer: payer
})
);

// decide whether to continue or terminate
//如果路径全部遍历完成,则退出循环,交易完成
if (hasMultiplePools) {
payer = address(this); // at this point, the caller has paid
params.path = params.path.skipToken();
} else {
amountOut = params.amountIn;
break;
}
}

//检查交易是否满足预期
//如果交易的获得 token 数满足约束,则本次交易结束。
require(amountOut >= params.amountOutMinimum, 'Too little received');
}

路径

上面输入的参数中 path 字段是 bytes 类型,通过这种类型可以实现更紧凑的编码。Uniswap 会将 bytes 作为一个数组使用,bytes 类型就是一连串的 byte1,但是不会对每一个成员使用一个 word,因此相比普通数组其结构更加紧凑。在 Uniswap V3 中, path 内部编码结构如下图:

image-20220819175811545

图中展示了一个包含 2个路径(pool0, 和 pool1)的 path 编码。Uniswap 将编码解码操作封装在了 Path 库中,本文不再赘述其过程。每次交易时,会取出头部的 tokenIn, tokenOut, fee,使用这三个参数找到对应的交易池,完成交易。

exactInputInternal

单个池的交易

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
function exactInputInternal(
uint256 amountIn,
address recipient,
uint160 sqrtPriceLimitX96,
SwapCallbackData memory data
) private returns (uint256 amountOut) {
// allow swapping to the router address with address 0
//如果收款地址为0,则默认收款地址为本合约地址
if (recipient == address(0)) recipient = address(this);

//将 path 解码,获取头部的 tokenIn, tokenOut, fee
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();

//因为交易池只保存了 token x 的价格,这里我们需要知道输入的 token 是交易池 x token 还是 y token
//token0/token为true,token1/token0为false
bool zeroForOne = tokenIn < tokenOut;

//交易
(int256 amount0, int256 amount1) =
getPool(tokenIn, tokenOut, fee).swap(
recipient,
zeroForOne,
amountIn.toInt256(),
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encode(data)
);

return uint256(-(zeroForOne ? amount1 : amount0));
}

swap

image-20220821114016119

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
function swap(
address recipient, //收款地址
bool zeroForOne, //代币交换方向,token0/token为true,token1/token0为false
int256 amountSpecified, //交换的数量,数值可正可负
uint160 sqrtPriceLimitX96, //价格平方根的Q64.96,如果是token0/token1方向的兑换,价格不能低于sqrtPriceLimitX96,如果是token1/token0方向,则不能大于
bytes calldata data //回调函数的参数
) external override noDelegateCall returns (int256 amount0, int256 amount1) {
//检查交易数量是否为0
require(amountSpecified != 0, 'AS');

//将交易前的数据保存在内存中,后续的访问通过 `MLOAD` 完成,节省 gas
Slot0 memory slot0Start = slot0;

//检查是否有正在进行的token交易操作
require(slot0Start.unlocked, 'LOK');
//根据交易方向检查价格是否满足条件
require(
zeroForOne
? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO
: sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO,
'SPL'
);

//更新slot0.unlocked的状态,防止交易过程中回调到合约中其他的函数中修改状态变量
slot0.unlocked = false;

//缓存交易数据
SwapCache memory cache =
SwapCache({
liquidityStart: liquidity,
blockTimestamp: _blockTimestamp(),
feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),
secondsPerLiquidityCumulativeX128: 0,
tickCumulative: 0,
computedLatestObservation: false
});

//检查交换的数量是否大于0
bool exactInput = amountSpecified > 0;

//存储交易状态信息
SwapState memory state =
SwapState({
amountSpecifiedRemaining: amountSpecified,
amountCalculated: 0,
sqrtPriceX96: slot0Start.sqrtPriceX96,
tick: slot0Start.tick,
feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,
protocolFee: 0,
liquidity: cache.liquidityStart
});

// continue swapping as long as we haven't used the entire input/output and haven't reached the price limit
//通过 tokenIn 是否还有余额来判断是否还需要继续循环,进入下一步的进行交易计算。当 tokenIn 全部被耗尽后,交易就结束了。
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
// 交易过程每一次循环的状态变量
StepComputations memory step;

// 交易的起始价格
step.sqrtPriceStartX96 = state.sqrtPriceX96;

// 通过位图找到下一个可以选的交易价格,这里可能是下一个流动性的边界,也可能还是在本流动性中
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
state.tick,
tickSpacing,
zeroForOne
);

// ensure that we do not overshoot the min/max tick, as the tick bitmap is not aware of these bounds
if (step.tickNext < TickMath.MIN_TICK) {
step.tickNext = TickMath.MIN_TICK;
} else if (step.tickNext > TickMath.MAX_TICK) {
step.tickNext = TickMath.MAX_TICK;
}

// get the price for the next tick
//从 tick index 计算 sqrt(price)
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
// 计算当价格到达下一个交易价格时,tokenIn 是否被耗尽,如果被耗尽,则交易结束,还需要重新计算出 tokenIn 耗尽时的价格
// 如果没被耗尽,那么还需要继续进入下一个循环
// 会返回需要的手续费
// 下面有对此函数的分析
(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
);

// 更新 tokenIn 的余额,以及 tokenOut 数量,注意当指定 tokenIn 的数量进行交易时,这里的 tokenOut 是负数.
//如果exactInput为true则表示input不为负数
if (exactInput) {
//amountSpecifiedRemaining:剩余期望兑换的数量
//amountCalculated:已经兑换的数量
state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
} else {
state.amountSpecifiedRemaining += step.amountOut.toInt256();
state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());
}

// if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee
//检查协议费用是否开启
if (cache.feeProtocol > 0) {
//计算欠费多少
uint256 delta = step.feeAmount / cache.feeProtocol;
step.feeAmount -= delta;
state.protocolFee += uint128(delta);
}

// update global fee tracker
// 更新交易的 f_g,这里需要除以流动性 L
// 更新时使用此步骤的手续费总额除以此步骤的流动性 L ,以得出每一份流动性所对应的手续费数值。
if (state.liquidity > 0)
state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);

// shift tick if we reached the next price
// 按需决定是否需要更新流动性 L 的值
// 当价格到达当前步骤价格区间的边界时,可能需要穿过下一个 tick
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
// if the tick is initialized, run the tick transition
//检查 tick index 是否为另一个流动性的边界
if (step.initialized) {
// check for the placeholder value, which we replace with the actual value the first time the swap
// crosses an initialized tick
if (!cache.computedLatestObservation) {
(cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle(
cache.blockTimestamp,
0,
slot0Start.tick,
slot0Start.observationIndex,
cache.liquidityStart,
slot0Start.observationCardinality
);
cache.computedLatestObservation = true;
}
int128 liquidityNet =
// 在这里需要更新 tick 的 f_o
ticks.cross(
step.tickNext,
(zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
(zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
cache.secondsPerLiquidityCumulativeX128,
cache.tickCumulative,
cache.blockTimestamp
);
// if we're moving leftward, we interpret liquidityNet as the opposite sign
// safe because liquidityNet cannot be type(int128).min
// 根据价格增加/减少,即向左或向右移动,增加/减少相应的流动性
if (zeroForOne) liquidityNet = -liquidityNet;

//更新流动性
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}

//在这里更 tick 的值,使得下一次循环时让 tickBitmap 进入下一个 word 中查询
state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
// recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved
//如果 tokenIn 被耗尽,那么计算当前价格对应的 tick
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
}

// update tick and write an oracle entry if the tick change
//当trick改变时写入一个oracle条目
if (state.tick != slot0Start.tick) {
(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 {
// otherwise just update the price
//否则只更新价格
slot0.sqrtPriceX96 = state.sqrtPriceX96;
}

// update liquidity if it changed
//如果流动性发生变化则更新流动性
if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;

// update fee growth global and, if necessary, protocol fees
// overflow is acceptable, protocol has to withdraw before it hits type(uint128).max fees
// 更新费用
// 在交易步骤完成后,更新合约的 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;
}

//确定最终用户支付的 token 数和得到的 token 数
(amount0, amount1) = zeroForOne == exactInput
? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
: (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);

// do the transfers and collect payment
//转账和费用收取
if (zeroForOne) {
// 将 tokenOut 支付给用户,前面说过 tokenOut 记录的是负数
if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));

uint256 balance0Before = balance0();
// 还是通过回调的方式,扣除用户需要支持的 token
//这里还是通过回调完成用户支付 token 的费用。因为发送用户 token 是在回调函数之前完成的,因此这个 swap 函数是可以被当作 flash swap 来使用的。
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
// 校验扣除是否成功
require(balance0Before.add(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.add(uint256(amount1)) <= balance1(), 'IIA');
}

//记录日志
emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.liquidity, state.tick);
//解除防止重入的锁
slot0.unlocked = true;
}

由于这里还是通过回调完成用户支付 token 的费用。因为发送用户 token 是在回调函数之前完成的,因此这个 swap 函数是可以被当作 flash swap 来使用的。

需要注意,如果本次交易是交易路径中的一次中间交易,那么扣除的 token 是从 SwapRouter 中扣除的,交易完成获得的 token 也会发送给 SwapRouter 以便其进行下一步的交易

computeSwapStep

交易是否能够结束的关键计算在 SwapMath.computeSwapStep 中完成,这里计算了交易是否能在目标价格范围内结束,以及消耗的 tokenIn 和得到的 tokenOut.

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
function computeSwapStep(
uint160 sqrtRatioCurrentX96,
uint160 sqrtRatioTargetX96,
uint128 liquidity,
int256 amountRemaining,
uint24 feePips
)
internal
pure
returns (
uint160 sqrtRatioNextX96,
uint256 amountIn,
uint256 amountOut,
uint256 feeAmount
)
{
// 判断交易的方向,即价格降低或升高
bool zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96;
// 判断是否指定了精确的 tokenIn 数量
bool exactIn = amountRemaining >= 0;

if (exactIn) {
// 先将 tokenIn 的余额扣除掉最大所需的手续费
uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6);
// 通过公式计算出到达目标价所需要的 tokenIn 数量,这里对 x token 和 y token 计算的公式是不一样的
amountIn = zeroForOne
//getAmount0Delta和getAmount1Delta的具体实现在上一章有详细解释
? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true)
: SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true);
// 判断余额是否充足,如果充足,那么这次交易可以到达目标交易价格,否则需要计算出当前 tokenIn 能到达的目标交易价
if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = sqrtRatioTargetX96;
else
sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(
sqrtRatioCurrentX96,
liquidity,
amountRemainingLessFee,
zeroForOne
);
} else {
amountOut = zeroForOne
? SqrtPriceMath.getAmount1Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false)
: SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false);
if (uint256(-amountRemaining) >= amountOut) sqrtRatioNextX96 = sqrtRatioTargetX96;
else
// 当余额不充足的时候计算能够到达的目标交易价
sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput(
sqrtRatioCurrentX96,
liquidity,
uint256(-amountRemaining),
zeroForOne
);
}

// 判断是否能够到达目标价
bool max = sqrtRatioTargetX96 == sqrtRatioNextX96;

// get the input/output amounts
if (zeroForOne) {
// 根据是否到达目标价格,计算 amountIn/amountOut 的值
amountIn = max && exactIn
? amountIn
: SqrtPriceMath.getAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true);
amountOut = max && !exactIn
? amountOut
: SqrtPriceMath.getAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false);
} else {
amountIn = max && exactIn
? amountIn
: SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true);
amountOut = max && !exactIn
? amountOut
: SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, false);
}

// cap the output amount to not exceed the remaining output amount
// 这里对 Output 进行 cap 是因为前面在计算 amountOut 时,有可能会使用 sqrtRatioNextX96 来进行计算
// 而 sqrtRatioNextX96 可能被 Round 之后导致 sqrt_P 偏大,从而导致计算的 amountOut 偏大
if (!exactIn && amountOut > uint256(-amountRemaining)) {
amountOut = uint256(-amountRemaining);
}

// 根据交易是否移动到价格边界来计算手续费的数额
if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) {
// we didn't reach the target, so take the remainder of the maximum input as fee
// 如果没能到达目标价,即交易结束,剩余的 tokenIn 将全部作为手续费
// 为了不让计算进一步复杂化,这里直接将剩余的 tokenIn 将全部作为手续费
// 因此会多收取一部分手续费,即按本次交易的最大手续费收取
feeAmount = uint256(amountRemaining) - amountIn;
} else {
// 当价格移动到边界时,计算相应的手续费
feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips);
}
}

在进行交易输入/输出的计算时,和流动性的计算一样,也会遇到 rounding 的问题,处理的原则是:

  1. 当计算 output 时,使用 RoundDown,保证 pool 不会出现坏账
  2. 当计算 input 时,使用 RoundUp,保证 pool 不会出现坏账
  3. 当通过 input 计算 √P 时,如果 √P 会减少,那么使用 RoundUp,这样可以保证 √ΔP 被 RoundDown,在后续计算 output 时不会使 pool 出现坏账。反之 如果 √P 会增大, 那么使用 RoundDown
  4. 当通过 output 计算 √P 时,如果 √P 会减少,那么使用 RoundDown,这样可以保证 √ΔP 被 RoundUp,在后续计算 input 时不会使 pool 出现坏账。反之 如果 √P 会增大, 那么使用 RoundUp

uniswapV3SwapCallback

支付的回调函数

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 uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata _data
) external override {
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

// 判断函数的参数中哪个是本次支付需要支付的代币
(bool isExactInput, uint256 amountToPay) =
amount0Delta > 0
? (tokenIn < tokenOut, uint256(amount0Delta))
: (tokenOut < tokenIn, uint256(amount1Delta));
if (isExactInput) {
// 调用 pay 函数支付代币
pay(tokenIn, data.payer, msg.sender, amountToPay);
} else {
// either initiate the next swap or pay
if (data.path.hasMultiplePools()) {
data.path = data.path.skipToken();
exactOutputInternal(amountToPay, msg.sender, 0, data);
} else {
amountInCached = amountToPay;
tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
pay(tokenIn, data.payer, msg.sender, amountToPay);
}
}
}

回调完成后,swap 函数会返回本次交易得到的代币数量。exactInput 将判断是否进行下一个路径的交易,直至所有的交易完成,进行输入约束的检查:

1
require(amountOut >= params.amountOutMinimum, 'Too little received');

如果交易的获得 token 数满足约束,则本次交易结束。

交易预计算

当用户和 uniswap 前端进行交互时,前端需要预先计算出用户输入 token 能够预期得到的 token 数量。

这个功能在 uniswap v2 有非常简单的实现,只需要查询处合约中两个代币的余额就可以完成预计算。

但是在 v3 版本中,由于交易的计算需要使用合约内的 tick 信息,预计算只能由 uniswap v3 pool 合约来完成,但是 pool 合约中的计算函数都是会更改合约状态的 external 函数,那么如何把这个函数当作 view/pure 函数来使用呢?uniswap v3 periphery 仓库中给出了一个非常 tricky 的实现,代码在 contracts/lens/Quoter.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
function quoteExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256 amountIn,
uint160 sqrtPriceLimitX96
) public override returns (uint256 amountOut) {
bool zeroForOne = tokenIn < tokenOut;

try
// 调用 pool 合约的 swap 接口来模拟一次真实的交易
getPool(tokenIn, tokenOut, fee).swap(
address(this), // address(0) might cause issues with some tokens
zeroForOne,
amountIn.toInt256(),
sqrtPriceLimitX96 == 0
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
: sqrtPriceLimitX96,
abi.encodePacked(tokenIn, fee, tokenOut)
)
{} catch (bytes memory reason) {
return parseRevertReason(reason);
}
}

函数中调用了 getPool(tokenIn, tokenOut, fee).swap(),即 pool 合约的真实交易函数,但是实际上我们并不想让交易发生,这个交易调用必定也会失败,因此合约使用了 try/catch 的方式捕获错误,并且在回调函数中获取到模拟交易的结果,存入内存中。

uniswapV3SwapCallback回调函数的实现:

这个回调函数主要的作用就是将 swap() 函数计算处的结果保存到内存中

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
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes memory path
) external view override {
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

(bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
amount0Delta > 0
? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
: (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));
if (isExactInput) {
// 这里代码需要将结果保存在内存中
assembly {
// 0x40 是 solidity 定义的 free memory pointer
let ptr := mload(0x40)
// 将结果保存起来
mstore(ptr, amountReceived)
// revert 掉交易,并将内存中的数据作为 revert data
revert(ptr, 32)
}
} else {
// if the cache has been populated, ensure that the full output amount has been received
if (amountOutCached != 0) require(amountReceived == amountOutCached);
assembly {
let ptr := mload(0x40)
mstore(ptr, amountToPay)
revert(ptr, 32)
}
}
}

将结果保存到内存中时候就将交易 revert 掉,然后在 quoteExactInputSingle 中捕获这个错误,并将内存中的信息读取出来,返回给调用者:

1
2
3
4
5
6
7
8
9
10
11
/// @dev Parses a revert reason that should contain the numeric quote
function parseRevertReason(bytes memory reason) private pure returns (uint256) {
if (reason.length != 32) { // swap 函数正常 revert 的情况
if (reason.length < 68) revert('Unexpected error');
assembly {
reason := add(reason, 0x04)
}
revert(abi.decode(reason, (string)));
}
return abi.decode(reason, (uint256)); // 这里捕获前面回调函数保存在内存中的结果。
}

总结:通过 try/catch 结合回调函数,模拟计算结果,实现了交易预计算的功能,这样 uniswap 前端就能够在获取用户输入后进行交易的预计算了,这部分前端的实现在这里


Uniswap Part Ⅲ | 交易过程
http://sissice.github.io/2022/09/29/uniswap-v3-3/
作者
Sissice
发布于
2022年9月29日
许可协议