Swap 可分为两种场景:单池交易和跨池交易。在 PoolManager 合约里,要完成交易流程,会涉及到 lock()
、swap()
、settle()
、take()
四个函数。单池交易时只需要调一次 swap()
函数,而跨池交易时则需要多次调用 swap()
函数来完成。
我们先来聊聊单池交易如何实现,以下是流程图:
第一步,和其他操作一样,先执行 lock()
,锁定住接下来的系列操作。
第二步,就是在 lockAcquired()
回调函数里执行 swap()
函数。这一步执行完之后,记账系统中会记录用户欠池子的资产数量,即用户需要支付的代币;以及池子欠用户的资产数量,即用户此次交易可得的代币。
第三步,执行 settle()
函数,完成代币的支付。
第四步,执行 take()
函数,取回所得的代币。
最后,lock()
函数完成,返回结果。
而如果是跨池交易的话,则需要在 Router 层面确定好交易路径,然后根据路径执行多次 swap
。举个例子,现在要用 A 兑换成 C,但是 A 和 C 之间没有直接配对的池子,但是有中间代币 B,存在 A 和 B 配对的池子,也存在 B 和 C 配对的池子。那交易路径就可以先用 A 换成 B,再将 B 换成 C,最终实现了 A 换成 C。而不管中间经过了多少次 swap
,最后,只需要完成一次 settle
操作,即支付 A,也只需要执行一次 take
操作,即取回最后所得的 C。整个流程大致如下图所示:
下面,我们主要剖析讲解 swap()
函数的内部实现。
首先,看看其函数声明,如下:
代码语言:javascript复制function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)
external
override
noDelegateCall
onlyByLocker
returns (BalanceDelta delta)
key
指定了要进行交易的池子,params
是具体的交易参数,hookData
即需要回传给 hooks 合约的数据。
来看看 params
具体有哪些参数:
struct SwapParams {
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
}
zeroForOne
指名了要用 currency0
兑换 currency1
,为 false
的话则反过来用 currency1
兑换 currency0
。amountSpecified
是指定的确定数额,正数表示输入,负数表示输出。sqrtPriceLimitX96
是滑点保护的限定价格。如果之前已经了解过 UniswapV3,那对这几个字段应该不陌生。
两个函数修饰器 noDelegateCall
和 onlyByLocker
,和之前文章介绍的一样,就不赘述了。
返回值 delta
,其组成里的两个数,正常情况下就是一个正数,一个负数。
接下来,看看函数体了。先看前面一段代码:
代码语言:javascript复制PoolId id = key.toId();
_checkPoolInitialized(id);
if (key.hooks.shouldCallBeforeSwap()) {
bytes4 selector = key.hooks.beforeSwap(msg.sender, key, params, hookData);
// Sentinel return value used to signify that a NoOp occurred.
if (key.hooks.isValidNoOpCall(selector)) return BalanceDeltaLibrary.MAXIMUM_DELTA;
else if (selector != IHooks.beforeSwap.selector) revert Hooks.InvalidHookResponse();
}
这部分逻辑很简单,前两行代码,检查池子是否已经初始化过了,未初始化的则 revert
。之后是执行 hooks 合约的 beforeSwap
钩子函数。
接下来这段代码是执行 swap
的内部函数:
uint256 feeForProtocol;
uint256 feeForHook;
uint24 swapFee;
Pool.SwapState memory state;
(delta, feeForProtocol, feeForHook, swapFee, state) = pools[id].swap(
Pool.SwapParams({
tickSpacing: key.tickSpacing,
zeroForOne: params.zeroForOne,
amountSpecified: params.amountSpecified,
sqrtPriceLimitX96: params.sqrtPriceLimitX96
})
);
这个内部函数的具体实现比较复杂,我们待会再讲,先继续讲完外部函数剩下的代码。
接下来一行代码就是进行记账了:
代码语言:javascript复制_accountPoolBalanceDelta(key, delta);
之后是对协议费和 hook 费用的处理:
代码语言:javascript复制unchecked {
if (feeForProtocol > 0) {
protocolFeesAccrued[params.zeroForOne ? key.currency0 : key.currency1] = feeForProtocol;
}
if (feeForHook > 0) {
hookFeesAccrued[address(key.hooks)][params.zeroForOne ? key.currency0 : key.currency1] = feeForHook;
}
}
接着执行 afterSwap
的钩子函数:
if (key.hooks.shouldCallAfterSwap()) {
if (key.hooks.afterSwap(msg.sender, key, params, delta, hookData) != IHooks.afterSwap.selector) {
revert Hooks.InvalidHookResponse();
}
}
最后,发送事件:
代码语言:javascript复制emit Swap(
id, msg.sender, delta.amount0(), delta.amount1(), state.sqrtPriceX96, state.liquidity, state.tick, swapFee
);
整个外部函数的逻辑还是比较清晰的。复杂的其实是内部函数的实现。下面就来看看 swap
内部函数的实现逻辑。还是先看函数声明:
function swap(State storage self, SwapParams memory params)
internal
returns (
BalanceDelta result,
uint256 feeForProtocol,
uint256 feeForHook,
uint24 swapFee,
SwapState memory state
)
self
是 storage
类型的,其实就是外部函数的 pools[id]
。而第二个参数的 SwapParams
不同于外部函数的同名参数,这个内部函数的此参数具体如下:
struct SwapParams {
int24 tickSpacing;
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
}
相比外部函数的此参数,多了 tickSpacing
,其他参数则和外部函数的一样。
返回值比较多。result
就是变动的净余额,feeForProtocol
是协议费,feeForHook
是 hook 费用,包括 hook 交易费用和提现费用,swapFee
就是池子本身的交易费,最后的 state
是最新的状态。
接着,开始查看函数体的代码实现,先看前面一段:
代码语言:javascript复制// 指定价格不能为0
if (params.amountSpecified == 0) revert SwapAmountCannotBeZero();
// 读取出swap前的状态
Slot0 memory slot0Start = self.slot0;
swapFee = slot0Start.swapFee;
if (params.zeroForOne) { // token0兑换token1
// 滑点价格的判断
if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96) {
revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);
}
if (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_RATIO) {
revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);
}
} else { // token1兑换token0
// 滑点价格的判断
if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96) {
revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);
}
if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_RATIO) {
revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);
}
}
接下来是这段代码:
代码语言:javascript复制// 临时的缓存数据
SwapCache memory cache = SwapCache({
liquidityStart: self.liquidity,
protocolFee: params.zeroForOne
? (getSwapFee(slot0Start.protocolFees) % 64)
: (getSwapFee(slot0Start.protocolFees) >> 6),
hookFee: params.zeroForOne ? (getSwapFee(slot0Start.hookFees) % 64) : (getSwapFee(slot0Start.hookFees) >> 6)
});
// 是否为确定的输入
bool exactInput = params.amountSpecified > 0;
// 初始化返回值的state
state = SwapState({
amountSpecifiedRemaining: params.amountSpecified,
amountCalculated: 0,
sqrtPriceX96: slot0Start.sqrtPriceX96,
tick: slot0Start.tick,
feeGrowthGlobalX128: params.zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128,
liquidity: cache.liquidityStart
});
cache
是一个临时状态的缓存数据,包括三个字段:
liquidityStart
:流动性protocolFee
:协议费用hookFee
:hook 费用
amountSpecified
大于 0 则说明是指定的输入,即 exactInput
为 true
。
初始化返回值 state
也都是用当前状态的值进行初始化。这里前两个字段需要介绍一下,即 amountSpecifiedRemaining
和 amountCalculated
。第一个字段表示当前还有多少指定的金额未进行交易计算的,第二个字段表示已经交易计算累加的数额。为了理解这两个字段,我们举个例子来说明。假设用户指定的是输出的数额,假设为 1000,那 amountSpecifiedRemaining
初始值即为 1000。但是,当前有效的流动性剩余量并不足 1000,假设只剩下 400,所以在当前 tick 下的计算只能用到 400,假设计算所得的输入数额为 200,那么,次轮计算后,amountSpecifiedRemaining
剩下 1000 - 400 = 600,而 amountCalculated
变为 200。之后,tick 会移动到下一个有流动性的区间内。剩下的 600 继续计算所得,假设这时的流动性剩余已经超过 600 了,这 600 计算所得的输入值为 250,那计算完后的 amountSpecifiedRemaining
就变成了 0,而 amountCalculated
则为 200 250 = 450,计算结束。这就是这两个字段的作用。
之后的代码会做循环判断,就是上面所说的计算逻辑:
代码语言:javascript复制StepComputations memory step;
// continue swapping as long as we haven't used the entire input/output and haven't reached the price limit
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != params.sqrtPriceLimitX96) {
...
}
while
条件里除了判断 amountSpecifiedRemaining
不为 0 之外,还判断了最新价格不能等于滑点价格。如果等于滑点价格了,也会结束循环。
step
用来存储 while
循环里每一步的计算用到的临时变量,具体包含以下字段:
struct StepComputations {
// the price at the beginning of the step
uint160 sqrtPriceStartX96;
// the next tick to swap to from the current tick in the swap direction
int24 tickNext;
// whether tickNext is initialized or not
bool initialized;
// sqrt(price) for the next tick (1/0)
uint160 sqrtPriceNextX96;
// how much is being swapped in in this step
uint256 amountIn;
// how much is being swapped out
uint256 amountOut;
// how much fee is being paid in
uint256 feeAmount;
}
接着,来看看 while
循环里面的逻辑,先来看前面一段代码:
// 初始化当前这一步的价格
step.sqrtPriceStartX96 = state.sqrtPriceX96;
// 获取出下一个tick
(step.tickNext, step.initialized) =
self.tickBitmap.nextInitializedTickWithinOneWord(state.tick, params.tickSpacing, params.zeroForOne);
// 确保下一个tick不会超出边界
if (step.tickNext < TickMath.MIN_TICK) {
step.tickNext = TickMath.MIN_TICK;
} else if (step.tickNext > TickMath.MAX_TICK) {
step.tickNext = TickMath.MAX_TICK;
}
// 计算出下一个tick对应的根号价格
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
之后,执行当前这步的具体计算:
代码语言:javascript复制// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
state.sqrtPriceX96,
(
params.zeroForOne
? step.sqrtPriceNextX96 < params.sqrtPriceLimitX96
: step.sqrtPriceNextX96 > params.sqrtPriceLimitX96
) ? params.sqrtPriceLimitX96 : step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
swapFee
);
计算返回四个值,sqrtPriceX96
为计算后的最新价格,amountIn
为输入的数额,amountOut
为输出的金额,feeAmount
为需要支付的手续费。
继续看下一段代码:
代码语言:javascript复制if (exactInput) { //指定输入时
unchecked {
//remaining减去输入额和手续费
state.amountSpecifiedRemaining -= (step.amountIn step.feeAmount).toInt256();
}
//calculated加上输出额,因为amountOut为负数,所以用减法
state.amountCalculated = state.amountCalculated - step.amountOut.toInt256();
} else { //指定输出时
unchecked {
//remaining减去输出额,因为amountOut为负数,所以用加法
state.amountSpecifiedRemaining = step.amountOut.toInt256();
}
//calculated加上输入额和手续费
state.amountCalculated = state.amountCalculated (step.amountIn step.feeAmount).toInt256();
}
之后的一段代码则是计算几个费用了:
代码语言:javascript复制// 协议费用
if (cache.protocolFee > 0) {
// A: calculate the amount of the fee that should go to the protocol
uint256 delta = step.feeAmount / cache.protocolFee;
// A: subtract it from the regular fee and add it to the protocol fee
unchecked {
step.feeAmount -= delta;
feeForProtocol = delta;
}
}
// hook费用
if (cache.hookFee > 0) {
// step.feeAmount has already been updated to account for the protocol fee
uint256 delta = step.feeAmount / cache.hookFee;
unchecked {
step.feeAmount -= delta;
feeForHook = delta;
}
}
// 更新全局费用跟踪器
if (state.liquidity > 0) {
unchecked {
state.feeGrowthGlobalX128 = FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);
}
}
while
循环体里的最后一段代码则如下:
// 如果计算后的新价格到达下一个tick价格就移动tick
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
// 如果tick已经初始化,则执行移动tick
if (step.initialized) {
int128 liquidityNet = Pool.crossTick(
self,
step.tickNext,
(params.zeroForOne ? state.feeGrowthGlobalX128 : self.feeGrowthGlobal0X128),
(params.zeroForOne ? self.feeGrowthGlobal1X128 : state.feeGrowthGlobalX128)
);
// 如果向左移动,把liquidityNet理解为相反的符号
unchecked {
if (params.zeroForOne) liquidityNet = -liquidityNet;
}
// 更新流动性
state.liquidity = liquidityNet < 0
? state.liquidity - uint128(-liquidityNet)
: state.liquidity uint128(liquidityNet);
}
// 更新tick
unchecked {
state.tick = params.zeroForOne ? step.tickNext - 1 : step.tickNext;
}
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
// 重新计算,除非我们处于较低的刻度边界(即已经转换过刻度),并且没有移动
state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
整个 while
循环跑完之后,一般来说,可能会存在两种情况。第一种,指定的金额全部完成兑换,即 amountSpecifiedRemaining
没有剩余。第二种,兑换到一半,触发到了滑点保护价格,那 amountSpecifiedRemaining
将会有剩余,只有部分成交。
那么,循环结束之后,整个内部的 swap
函数就只剩下最后的一部分代码了,如下:
// 将临时状态的价格和tick转为storage状态
(self.slot0.sqrtPriceX96, self.slot0.tick) = (state.sqrtPriceX96, state.tick);
// 更新storage状态的流动性
if (cache.liquidityStart != state.liquidity) self.liquidity = state.liquidity;
// 更新全局的手续费跟踪器
if (params.zeroForOne) {
self.feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
} else {
self.feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
}
// 净余额变动值赋值给返回值result
unchecked {
if (params.zeroForOne == exactInput) {
result = toBalanceDelta(
(params.amountSpecified - state.amountSpecifiedRemaining).toInt128(),
state.amountCalculated.toInt128()
);
} else {
result = toBalanceDelta(
state.amountCalculated.toInt128(),
(params.amountSpecified - state.amountSpecifiedRemaining).toInt128()
);
}
}
至此,就完成了 swap
的全部代码逻辑讲解了。