computeSwapStep
是 Uniswap v4/v3 swap 撮合的核心单步计算方法,用于在一个 tick 区间内,给定当前价格、目标价格、流动性、剩余兑换量和手续费,计算本 step 的价格变化、实际兑换数量和手续费。
function computeSwapStep(
uint160 sqrtPriceCurrentX96,
uint160 sqrtPriceTargetX96,
uint128 liquidity,
int256 amountRemaining,
uint24 feePips
) internal pure returns (uint160 sqrtPriceNextX96, uint256 amountIn, uint256 amountOut, uint256 feeAmount) {
unchecked {
uint256 _feePips = feePips; // upcast once and cache
bool zeroForOne = sqrtPriceCurrentX96 >= sqrtPriceTargetX96;
bool exactIn = amountRemaining < 0;
if (exactIn) {
uint256 amountRemainingLessFee =
FullMath.mulDiv(uint256(-amountRemaining), MAX_SWAP_FEE - _feePips, MAX_SWAP_FEE);
amountIn = zeroForOne
? SqrtPriceMath.getAmount0Delta(sqrtPriceTargetX96, sqrtPriceCurrentX96, liquidity, true)
: SqrtPriceMath.getAmount1Delta(sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, true);
if (amountRemainingLessFee >= amountIn) {
// `amountIn` is capped by the target price
sqrtPriceNextX96 = sqrtPriceTargetX96;
feeAmount = _feePips == MAX_SWAP_FEE
? amountIn // amountIn is always 0 here, as amountRemainingLessFee == 0 and amountRemainingLessFee >= amountIn
: FullMath.mulDivRoundingUp(amountIn, _feePips, MAX_SWAP_FEE - _feePips);
} else {
// exhaust the remaining amount
amountIn = amountRemainingLessFee;
sqrtPriceNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(
sqrtPriceCurrentX96, liquidity, amountRemainingLessFee, zeroForOne
);
// we didn't reach the target, so take the remainder of the maximum input as fee
feeAmount = uint256(-amountRemaining) - amountIn;
}
amountOut = zeroForOne
? SqrtPriceMath.getAmount1Delta(sqrtPriceNextX96, sqrtPriceCurrentX96, liquidity, false)
: SqrtPriceMath.getAmount0Delta(sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity, false);
} else {
amountOut = zeroForOne
? SqrtPriceMath.getAmount1Delta(sqrtPriceTargetX96, sqrtPriceCurrentX96, liquidity, false)
: SqrtPriceMath.getAmount0Delta(sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, false);
if (uint256(amountRemaining) >= amountOut) {
// `amountOut` is capped by the target price
sqrtPriceNextX96 = sqrtPriceTargetX96;
} else {
// cap the output amount to not exceed the remaining output amount
amountOut = uint256(amountRemaining);
sqrtPriceNextX96 =
SqrtPriceMath.getNextSqrtPriceFromOutput(sqrtPriceCurrentX96, liquidity, amountOut, zeroForOne);
}
amountIn = zeroForOne
? SqrtPriceMath.getAmount0Delta(sqrtPriceNextX96, sqrtPriceCurrentX96, liquidity, true)
: SqrtPriceMath.getAmount1Delta(sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity, true);
// `feePips` cannot be `MAX_SWAP_FEE` for exact out
feeAmount = FullMath.mulDivRoundingUp(amountIn, _feePips, MAX_SWAP_FEE - _feePips);
}
}
}
参数说明
入参
sqrtPriceCurrentX96
:当前池子的 sqrt(price)(Q64.96 格式)sqrtPriceTargetX96
:本 step 允许到达的目标 sqrt(price)(tick 边界或用户价格极限)liquidity
:当前 tick 区间内的流动性amountRemaining
:剩余要兑换的数量(正数 exact output,负数 exact input)feePips
:手续费(以百万分之一为单位)
返回
sqrtPriceNextX96
:本 step 结束后的价格amountIn
:本 step 实际消耗的输入币数量amountOut
:本 step 实际获得的输出币数量feeAmount
:本 step 实际收取的手续费
逻辑解析
首先是判断方向和模式
zeroForOne = sqrtPriceCurrentX96 >= sqrtPriceTargetX96
方向:价格下降代表:token0 → token1;否则反之exactIn = amountRemaining < 0
模式:exact input(指定输入)还是 exact output(指定输出)
本文只解析指定输入的模式exact input(amountRemaining < 0
)
先计算去掉手续费后,实际可用的输入量 amountRemainingLessFee
uint256 amountRemainingLessFee =
FullMath.mulDiv(uint256(-amountRemaining), MAX_SWAP_FEE - _feePips, MAX_SWAP_FEE);
我们拆解以下这段代码
amountRemaining
:用户本次 swap 还剩下要兑换的输入数量(exact input 时为负数,所以取负号变成正数)。MAX_SWAP_FEE
:最大手续费分母,等于 1e6(代表 100%)。_feePips
:实际手续费(如 3000 表示 0.3%)。MAX_SWAP_FEE - _feePips
:兑换后剩下的比例(如 997000,表示 99.7%)。FullMath.mulDiv(a, b, c)
:高精度计算(a * b) / c
,防止溢出。
这行代码等价于:
amountRemainingLessFee = amountRemaining * (MAX_SWAP_FEE - _feePips) / MAX_SWAP_FEE
进一步简化就是:
amountRemainingLessFee = amountRemaining * (1 - fee)
假设用户输入 1000 USDC,手续费 0.3%(_feePips = 3000);amountRemaining = -1000
(exact input 语义,负数);MAX_SWAP_FEE = 1_000_000
则:
amountRemainingLessFee = 1000 * (1_000_000 - 3000) / 1_000_000
= 1000 * 997_000 / 1_000_000
= 997
接着计算算如果价格从当前 sqrtPrice 变化到目标 sqrtPrice,所需要的输入币数量(amountIn)是多少。
amountIn = zeroForOne
? SqrtPriceMath.getAmount0Delta(sqrtPriceTargetX96, sqrtPriceCurrentX96, liquidity, true)
: SqrtPriceMath.getAmount1Delta(sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity, true);
getAmountXDelta的代码比较简单,这里直接上公式和推导:
getAmount0Delta:amountIn = liquidity * |sqrtPriceBX96 - sqrtPriceAX96| / 2^96
getAmount1Delta:amountIn = liquidity /|sqrtPriceBX96 - sqrtPriceAX96| / 2^96
推导步骤:
于是得出:
所以:
后面/ 2^96的原因是sqrtPriceX96=sqrtPrice*2^96,所以需要还原回去。
如果去除手续费的输入可以覆盖价格变化所需要的输入,直接返回sqrtPriceTargetX96,之前计算的amountIn,并计算相应的手续费
if (amountRemainingLessFee >= amountIn) {
// `amountIn` is capped by the target price
sqrtPriceNextX96 = sqrtPriceTargetX96;
feeAmount = _feePips == MAX_SWAP_FEE ?
amountIn
: FullMath.mulDivRoundingUp(amountIn, _feePips, MAX_SWAP_FEE -_feePips);
}
如果当前输入的金额无法让价格到达目标价格则需要计算当前的输入金额到达的价格。
else {
// exhaust the remaining amount
amountIn = amountRemainingLessFee;
sqrtPriceNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(
sqrtPriceCurrentX96, liquidity, amountRemainingLessFee, zeroForOne
);
// we didn't reach the target, so take the remainder of the maximum input as fee
feeAmount = uint256(-amountRemaining) - amountIn;
}
如果去除手续费后的输入金额不足以让价格到达目标价格,价格只能到达中间某个点则需要通过getNextSqrtPriceFromInput计算当前的输入金额可以到达的价格是多少。
getNextSqrtPriceFromInput这个方法的代码并不复杂,这里只介绍其公式推导。公式如下:
sqrtPx96_next = L*2^96 * sqrtPx96 / (L*2^96 ± amount * sqrtPx96)
- 输入 amount 个 token0,x 变为 x' = x ± amount
- 新价格 sqrtP_next 满足:x' = L / sqrtP_next
- x ± amount = L / sqrtP_next
- L / sqrtP ± amount = L / sqrtP_next
- sqrtP_next = L/ (L / sqrtP ± amount)=L * sqrtP / (L ± amount * sqrtP)
由于 Uniswap 内部 sqrtP 用 Q64.96 定点数表示,左右两边同时乘以 2^96
sqrtPx96_next = L * sqrtPx96 / (L ± amount * sqrtP)
进一步把sqrtP转换成sqrtPx96得出:
sqrtPx96_next = L * sqrtPx96 / (L ± amount * sqrtPx96/ 2^96)
等式右边分子分母同时乘以2^96,最终得到
sqrtPx96_next = L*2^96 * sqrtPx96 / (L*2^96 ± amount * sqrtPx96)
getNextSqrtPriceFromInput这个方法就是完全按照这一步公式实现的,有兴趣的同学可以自行查阅!
amountRemainingLessFee作为去除手续费后的输入,feeAmount = uint256(-amountRemaining) - amountIn;就是本步交易完后需要花费的手续费。
amountOut = zeroForOne
? SqrtPriceMath.getAmount1Delta(sqrtPriceNextX96, sqrtPriceCurrentX96, liquidity, false)
: SqrtPriceMath.getAmount0Delta(sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity, false);
最后计算amountOut和公式和之前计算amountIn一致,这里不再赘述!