文章首發于公眾號:Keegan小鋼
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()
函數的內部實現。
首先,看看其函數聲明,如下:
function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)externaloverridenoDelegateCallonlyByLockerreturns (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
,其組成里的兩個數,正常情況下就是一個正數,一個負數。
接下來,看看函數體了。先看前面一段代碼:
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})
);
這個內部函數的具體實現比較復雜,我們待會再講,先繼續講完外部函數剩下的代碼。
接下來一行代碼就是進行記賬了:
_accountPoolBalanceDelta(key, delta);
之后是對協議費和 hook 費用的處理:
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();}
}
最后,發送事件:
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)internalreturns (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
是最新的狀態。
接著,開始查看函數體的代碼實現,先看前面一段:
// 指定價格不能為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);}
}
接下來是這段代碼:
// 臨時的緩存數據
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,計算結束。這就是這兩個字段的作用。
之后的代碼會做循環判斷,就是上面所說的計算邏輯:
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 stepuint160 sqrtPriceStartX96;// the next tick to swap to from the current tick in the swap directionint24 tickNext;// whether tickNext is initialized or notbool initialized;// sqrt(price) for the next tick (1/0)uint160 sqrtPriceNextX96;// how much is being swapped in in this stepuint256 amountIn;// how much is being swapped outuint256 amountOut;// how much fee is being paid inuint256 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);
之后,執行當前這步的具體計算:
// 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
為需要支付的手續費。
繼續看下一段代碼:
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();
}
之后的一段代碼則是計算幾個費用了:
// 協議費用
if (cache.protocolFee > 0) {// A: calculate the amount of the fee that should go to the protocoluint256 delta = step.feeAmount / cache.protocolFee;// A: subtract it from the regular fee and add it to the protocol feeunchecked {step.feeAmount -= delta;feeForProtocol += delta;}
}
// hook費用
if (cache.hookFee > 0) {// step.feeAmount has already been updated to account for the protocol feeuint256 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已經初始化,則執行移動tickif (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);}// 更新tickunchecked {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
的全部代碼邏輯講解了。