您的位置:首页 > 新闻 > 热点要闻 > 北京环球影城可以带水果吗_百度 搜索热度_鹤壁seo推广_外链推广是什么意思

北京环球影城可以带水果吗_百度 搜索热度_鹤壁seo推广_外链推广是什么意思

2025/7/17 16:40:34 来源:https://blog.csdn.net/gambool/article/details/148435746  浏览:    关键词:北京环球影城可以带水果吗_百度 搜索热度_鹤壁seo推广_外链推广是什么意思
北京环球影城可以带水果吗_百度 搜索热度_鹤壁seo推广_外链推广是什么意思

这一章我们分析swap的过程:

在阅读本章之前一定要先通读uniswap v3/v4 中pool的状态管理

在阅读本章之前一定要先通读uniswap v3/v4 中pool的状态管理

在阅读本章之前一定要先通读uniswap v3/v4 中pool的状态管理

 代码位置如下:

 这个 swap 方法是 Uniswap v4 中池子(Pool)的核心交换函数,负责处理一次 swap(兑换)操作的完整流程。下面逐步解析其主要逻辑,先上代码:
 

    function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData)externalonlyWhenUnlockednoDelegateCallreturns (BalanceDelta swapDelta){if (params.amountSpecified == 0) SwapAmountCannotBeZero.selector.revertWith();PoolId id = key.toId();Pool.State storage pool = _getPool(id);pool.checkPoolInitialized();BeforeSwapDelta beforeSwapDelta;{int256 amountToSwap;uint24 lpFeeOverride;(amountToSwap, beforeSwapDelta, lpFeeOverride) = key.hooks.beforeSwap(key, params, hookData);// execute swap, account protocol fees, and emit swap event// _swap is needed to avoid stack too deep errorswapDelta = _swap(pool,id,Pool.SwapParams({tickSpacing: key.tickSpacing,zeroForOne: params.zeroForOne,amountSpecified: amountToSwap,sqrtPriceLimitX96: params.sqrtPriceLimitX96,lpFeeOverride: lpFeeOverride}),params.zeroForOne ? key.currency0 : key.currency1 // input token);}BalanceDelta hookDelta;(swapDelta, hookDelta) = key.hooks.afterSwap(key, params, swapDelta, hookData, beforeSwapDelta);// if the hook doesn't have the flag to be able to return deltas, hookDelta will always be 0if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks));_accountPoolBalanceDelta(key, swapDelta, msg.sender);}

先看入参:

/// @notice Parameter struct for `Swap` pool operations
struct SwapParams {/// Whether to swap token0 for token1 or vice versabool zeroForOne;/// The desired input amount if negative (exactIn), or the desired output amount if positive (exactOut)int256 amountSpecified;/// The sqrt price at which, if reached, the swap will stop executinguint160 sqrtPriceLimitX96;
}
  • bool zeroForOne:表示兑换方向。true:用 token0 换 token1(token0 → token1);false:用 token1 换 token0(token1 → token0)。

  • int256 amountSpecified:表示本次 swap 的数量和类型。如果为负数(< 0):表示“exactIn”,即指定输入数量,用户希望用多少 token 进行兑换。如果为正数(> 0):表示“exactOut”,即指定输出数量,用户希望获得多少 token。

  • uint160 sqrtPriceLimitX96:表示本次 swap 允许到达的价格极限(以 sqrt(price) 的 Q96 定点数表示)。当池子的价格变动到该极限时,swap 会停止执行,防止滑点过大或价格异常。

其处理交换主要分为以下几个步骤:

  • 参数与前置检查
  • beforeSwap 钩子
  • 执行 swap
  • afterSwap 钩子
  • 资产变化结算

我们逐一分析:

参数与前置检查

if (params.amountSpecified == 0) SwapAmountCannotBeZero.selector.revertWith();
PoolId id = key.toId();
Pool.State storage pool = _getPool(id);
pool.checkPoolInitialized();
  • 检查兑换数量不能为 0。
  • 通过 key 获取池子的唯一 ID 和状态。
  • 检查池子是否已初始化。

swap处理

在正式开始swap之前先进行beforeSwap钩子函数的调用,具体可以看这篇文章uniswap v4 hooks详解中beforeSwap相关的部分,调用这个钩子后会返回3个参数:

  • amountToSwap:实际要 swap 的数量,可能被钩子调整。
  • beforeSwapDelta:钩子返回的 delta 信息(如特殊调整)。
  • lpFeeOverride:如果池子支持动态费率,钩子可以覆盖 LP 费率。

用返回的参数结合SwapParams传参调用相应pool的swap函数:

    /// @notice Executes a swap against the state, and returns the amount deltas of the pool/// @dev PoolManager checks that the pool is initialized before callingfunction swap(State storage self, SwapParams memory params)internalreturns (BalanceDelta swapDelta, uint256 amountToProtocol, uint24 swapFee, SwapResult memory result){Slot0 slot0Start = self.slot0;bool zeroForOne = params.zeroForOne;uint256 protocolFee =zeroForOne ? slot0Start.protocolFee().getZeroForOneFee() : slot0Start.protocolFee().getOneForZeroFee();// the amount remaining to be swapped in/out of the input/output asset. initially set to the amountSpecifiedint256 amountSpecifiedRemaining = params.amountSpecified;// the amount swapped out/in of the output/input asset. initially set to 0int256 amountCalculated = 0;// initialize to the current sqrt(price)result.sqrtPriceX96 = slot0Start.sqrtPriceX96();// initialize to the current tickresult.tick = slot0Start.tick();// initialize to the current liquidityresult.liquidity = self.liquidity;// if the beforeSwap hook returned a valid fee override, use that as the LP fee, otherwise load from storage// lpFee, swapFee, and protocolFee are all in pips{uint24 lpFee = params.lpFeeOverride.isOverride()? params.lpFeeOverride.removeOverrideFlagAndValidate(): slot0Start.lpFee();swapFee = protocolFee == 0 ? lpFee : uint16(protocolFee).calculateSwapFee(lpFee);}// a swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the feeif (swapFee >= SwapMath.MAX_SWAP_FEE) {// if exactOutputif (params.amountSpecified > 0) {InvalidFeeForExactOut.selector.revertWith();}}// swapFee is the pool's fee in pips (LP fee + protocol fee)// when the amount swapped is 0, there is no protocolFee applied and the fee amount paid to the protocol is set to 0if (params.amountSpecified == 0) return (BalanceDeltaLibrary.ZERO_DELTA, 0, swapFee, result);if (zeroForOne) {if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96()) {PriceLimitAlreadyExceeded.selector.revertWith(slot0Start.sqrtPriceX96(), params.sqrtPriceLimitX96);}// Swaps can never occur at MIN_TICK, only at MIN_TICK + 1, except at initialization of a pool// Under certain circumstances outlined below, the tick will preemptively reach MIN_TICK without swapping thereif (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_PRICE) {PriceLimitOutOfBounds.selector.revertWith(params.sqrtPriceLimitX96);}} else {if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96()) {PriceLimitAlreadyExceeded.selector.revertWith(slot0Start.sqrtPriceX96(), params.sqrtPriceLimitX96);}if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_PRICE) {PriceLimitOutOfBounds.selector.revertWith(params.sqrtPriceLimitX96);}}StepComputations memory step;step.feeGrowthGlobalX128 = zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128;// continue swapping as long as we haven't used the entire input/output and haven't reached the price limitwhile (!(amountSpecifiedRemaining == 0 || result.sqrtPriceX96 == params.sqrtPriceLimitX96)) {step.sqrtPriceStartX96 = result.sqrtPriceX96;(step.tickNext, step.initialized) =self.tickBitmap.nextInitializedTickWithinOneWord(result.tick, params.tickSpacing, zeroForOne);// ensure that we do not overshoot the min/max tick, as the tick bitmap is not aware of these boundsif (step.tickNext <= TickMath.MIN_TICK) {step.tickNext = TickMath.MIN_TICK;}if (step.tickNext >= TickMath.MAX_TICK) {step.tickNext = TickMath.MAX_TICK;}// get the price for the next tickstep.sqrtPriceNextX96 = TickMath.getSqrtPriceAtTick(step.tickNext);// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted(result.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(result.sqrtPriceX96,SwapMath.getSqrtPriceTarget(zeroForOne, step.sqrtPriceNextX96, params.sqrtPriceLimitX96),result.liquidity,amountSpecifiedRemaining,swapFee);// if exactOutputif (params.amountSpecified > 0) {unchecked {amountSpecifiedRemaining -= step.amountOut.toInt256();}amountCalculated -= (step.amountIn + step.feeAmount).toInt256();} else {// safe because we test that amountSpecified > amountIn + feeAmount in SwapMathunchecked {amountSpecifiedRemaining += (step.amountIn + step.feeAmount).toInt256();}amountCalculated += step.amountOut.toInt256();}// if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFeeif (protocolFee > 0) {unchecked {// step.amountIn does not include the swap fee, as it's already been taken from it,// so add it back to get the total amountIn and use that to calculate the amount of fees owed to the protocol// cannot overflow due to limits on the size of protocolFee and params.amountSpecified// this rounds down to favor LPs over the protocoluint256 delta = (swapFee == protocolFee)? step.feeAmount // lp fee is 0, so the entire fee is owed to the protocol instead: (step.amountIn + step.feeAmount) * protocolFee / ProtocolFeeLibrary.PIPS_DENOMINATOR;// subtract it from the total fee and add it to the protocol feestep.feeAmount -= delta;amountToProtocol += delta;}}// update global fee trackerif (result.liquidity > 0) {unchecked {// FullMath.mulDiv isn't needed as the numerator can't overflow uint256 since tokens have a max supply of type(uint128).maxstep.feeGrowthGlobalX128 +=UnsafeMath.simpleMulDiv(step.feeAmount, FixedPoint128.Q128, result.liquidity);}}// Shift tick if we reached the next price, and preemptively decrement for zeroForOne swaps to tickNext - 1.// If the swap doesn't continue (if amountRemaining == 0 or sqrtPriceLimit is met), slot0.tick will be 1 less// than getTickAtSqrtPrice(slot0.sqrtPrice). This doesn't affect swaps, but donation calls should verify both// price and tick to reward the correct LPs.if (result.sqrtPriceX96 == step.sqrtPriceNextX96) {// if the tick is initialized, run the tick transitionif (step.initialized) {(uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = zeroForOne? (step.feeGrowthGlobalX128, self.feeGrowthGlobal1X128): (self.feeGrowthGlobal0X128, step.feeGrowthGlobalX128);int128 liquidityNet =Pool.crossTick(self, step.tickNext, feeGrowthGlobal0X128, feeGrowthGlobal1X128);// if we're moving leftward, we interpret liquidityNet as the opposite sign// safe because liquidityNet cannot be type(int128).minunchecked {if (zeroForOne) liquidityNet = -liquidityNet;}result.liquidity = LiquidityMath.addDelta(result.liquidity, liquidityNet);}unchecked {result.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;}} else if (result.sqrtPriceX96 != step.sqrtPriceStartX96) {// recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't movedresult.tick = TickMath.getTickAtSqrtPrice(result.sqrtPriceX96);}}self.slot0 = slot0Start.setTick(result.tick).setSqrtPriceX96(result.sqrtPriceX96);// update liquidity if it changedif (self.liquidity != result.liquidity) self.liquidity = result.liquidity;// update fee growth globalif (!zeroForOne) {self.feeGrowthGlobal1X128 = step.feeGrowthGlobalX128;} else {self.feeGrowthGlobal0X128 = step.feeGrowthGlobalX128;}unchecked {// "if currency1 is specified"if (zeroForOne != (params.amountSpecified < 0)) {swapDelta = toBalanceDelta(amountCalculated.toInt128(), (params.amountSpecified - amountSpecifiedRemaining).toInt128());} else {swapDelta = toBalanceDelta((params.amountSpecified - amountSpecifiedRemaining).toInt128(), amountCalculated.toInt128());}}}

 这个函数可以说是整个uniswap的核心:

初始化和参数准备

Slot0 slot0Start = self.slot0;
bool zeroForOne = params.zeroForOne;
uint256 protocolFee = zeroForOne ? slot0Start.protocolFee().getZeroForOneFee() : slot0Start.protocolFee().getOneForZeroFee();
int256 amountSpecifiedRemaining = params.amountSpecified;
int256 amountCalculated = 0;
result.sqrtPriceX96 = slot0Start.sqrtPriceX96();
result.tick = slot0Start.tick();
result.liquidity = self.liquidity;

这部分代码主要做了以下几个步骤:

  • 读取当前池子的 slot0 状态(价格、tick、fee 等)。
  • 判断兑换方向(token0→token1 还是 token1→token0)。
  • 计算协议费率。
  • 初始化剩余要兑换的数量(amountSpecifiedRemaining )、已兑换数量(amountCalculated )、当前价格(result.sqrtPriceX96)、tick(result.tick)、流动性(result.liquidity)。

确定 LP 费率和 swap 总费率

uint24 lpFee = params.lpFeeOverride.isOverride()? params.lpFeeOverride.removeOverrideFlagAndValidate(): slot0Start.lpFee();
swapFee = protocolFee == 0 ? lpFee : uint16(protocolFee).calculateSwapFee(lpFee);
  • 如果有钩子覆盖费率,使用覆盖值,否则用池子当前的 LP 费率。
  • 计算本次 swap 的总费率(LP fee + protocol fee)。
        /// @notice the second bit of the fee returned by beforeSwap is used to signal if the stored LP fee should be overridden in this swap// only dynamic-fee pools can return a fee via the beforeSwap hookuint24 public constant OVERRIDE_FEE_FLAG = 0x400000;/// @notice mask to remove the override fee flag from a fee returned by the beforeSwaphookuint24 public constant REMOVE_OVERRIDE_MASK = 0xBFFFFF;/// @notice returns true if the fee has the override flag set (2nd highest bit of the uint24)/// @param self The fee to check/// @return bool True of the fee has the override flag setfunction isOverride(uint24 self) internal pure returns (bool) {return self & OVERRIDE_FEE_FLAG != 0;}/// @notice returns a fee with the override flag removed/// @param self The fee to remove the override flag from/// @return fee The fee without the override flag setfunction removeOverrideFlag(uint24 self) internal pure returns (uint24) {return self & REMOVE_OVERRIDE_MASK;}/// @notice Removes the override flag and validates the fee (reverts if the fee is too large)/// @param self The fee to remove the override flag from, and then validate/// @return fee The fee without the override flag set (if valid)function removeOverrideFlagAndValidate(uint24 self) internal pure returns (uint24 fee) {fee = self.removeOverrideFlag();fee.validate();}

LP fee 的 override 标志位(OVERRIDE_FEE_FLAG)是 0x400000,即:

0b0100_0000_0000_0000_0000_0000

也就是第 23 位(从右往左,最低位为第 0 位)。

掩码 0xBFFFFF 的作用是:保留所有位,除了第 23 位(override 标志位)被清零

这样与 fee 做按位与(&)操作后,override 标志位就被清除了,其余位不变。

calculateSwapFee 这个函数的作用是计算一次 swap 实际要收取的总手续费(swapFee),它综合了协议费(protocolFee)和 LP 费(lpFee)。

function calculateSwapFee(uint16 self, uint24 lpFee) internal pure returns (uint24 swapFee) {// protocolFee + lpFee - (protocolFee * lpFee / 1_000_000)assembly ("memory-safe") {self := and(self, 0xfff)lpFee := and(lpFee, 0xffffff)let numerator := mul(self, lpFee)swapFee := sub(add(self, lpFee), div(numerator, PIPS_DENOMINATOR))}
}

公式:

swapFee = protocolFee + lpFee - (protocolFee * lpFee / 1_000_000)

先从输入金额里收取 protocolFee(协议费)。剩下的金额再收取 lpFee(LP 费)。两者不是简单相加,因为 LP 费是从扣除协议费后的金额里收取,所以要减去重叠部分(protocolFee * lpFee / 1_000_000

假设 protocolFee = 1000(0.1%),lpFee = 3000(0.3%):

swapFee = 1000 + 3000 - (1000 * 3000 / 1_000_000) = 3997

费率校验

接下来是一步校验:

uint256 internal constant MAX_SWAP_FEE = 1e6;// a swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the fee
if (swapFee >= SwapMath.MAX_SWAP_FEE) {// if exactOutputif (params.amountSpecified > 0) {InvalidFeeForExactOut.selector.revertWith();}
}

这里的MAX_SWAP_FEE = 1e6;代表1000000, 代表swap 总手续费的最大值为 1,000,000 个“pips”,也就是100%

在 Uniswap v4 中,手续费(fee)用“pips”表示,1 pip = 0.0001%1e6 pips = 1,000,000 pips = 100%。所以 MAX_SWAP_FEE 就是允许的最大手续费比例,即 100%。

params.amountSpecified > 0:代表当前交换是“exact output”模式 即:用户指定想要获得多少输出资产,系统反推需要多少输入资产。当手续费为100%时,由于所有的输入用作手续费,此时可以提前判定不可能满足用户指定的amountSpecified 。所以此时提前中断交易。

但是如果params.amountSpecified < 0:代表这是“exact input”模式即用户指定本次 swap 想要花费多少 tokenIn(输入币种),系统会计算能换到多少 tokenOut(输出币种)。用户指定愿意花多少输入,这时虽然用户的所有输入都会被手续费吃掉,输出为 0,但这完全符合“exact input”语义:你愿意花这些钱,结果就是啥都换不到,协议不会多扣你的钱,也不会中断交易,更不会给你输出。

所以当用户的交易模式是exact input需要格外注意,你交换的池子如果包含恶意钩子,可能会突然提高手续费,让你的输入金额有去无回,最好的办法就是通过路由合约进行交换,交换的时候指定最小输出,当不满足的时候会回滚交易。路由合约比较简单,有机会的话再写文章介绍。

if (params.amountSpecified == 0) return (BalanceDeltaLibrary.ZERO_DELTA, 0, swapFee, result);

如果本次 swap 的指定兑换数量为 0,则直接返回,不做任何实际兑换和状态变更。

价格校验

接下来是校验价格限制参数:

uint160 internal constant MIN_SQRT_PRICE = 4295128739;if (zeroForOne) {if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96()) {PriceLimitAlreadyExceeded.selector.revertWith(slot0Start.sqrtPriceX96(), params.sqrtPriceLimitX96);}// Swaps can never occur at MIN_TICK, only at MIN_TICK + 1, except at initialization of a pool// Under certain circumstances outlined below, the tick will preemptively reach MIN_TICK without swapping thereif (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_PRICE) {PriceLimitOutOfBounds.selector.revertWith(params.sqrtPriceLimitX96);}
} else {if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96()) {PriceLimitAlreadyExceeded.selector.revertWith(slot0Start.sqrtPriceX96(), params.sqrtPriceLimitX96);}if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_PRICE) {PriceLimitOutOfBounds.selector.revertWith(params.sqrtPriceLimitX96);}
}

sqrtPriceLimitX96 这个参数,其在不同交易方向上含义不同:

首先回忆一下价格和token数量的关系:price = amount1 / amount0

当zeroForOne为true时,代表用户提供token0 换出 token1,amount0 变多,amount1变少,价格会随着交换而变低,此时的sqrtPriceLimitX96 是价格的下限,即本次 swap 过程中,价格不能低于这个值。如果当前价格已经低于用户设置的价格下限,此时不能再继续 swap
并且价格下限 低于 最小允许价格MIN_SQRT_PRICE  (也就是\sqrt{1.0001^{-887272}}*2^{96}),说明超出了协议允许的价格范围此时也需要中断交易。

反之当zeroForOne为false时,价格会随着交换而变高,此时的sqrtPriceLimitX96 是价格的上限,需要校验当前价格是否已经高于sqrtPriceLimitX96 ,以及sqrtPriceLimitX96 是否高于MAX_SQRT_PRICE。

循环撮合

接下来的处理是 Uniswap v4 池子 swap 的核心撮合循环,负责在价格区间内逐步撮合、收取手续费、跨越 tick 并更新状态。先看外层代码:

初始化 step 变量

StepComputations memory step;
step.feeGrowthGlobalX128 = zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128;

初始化本次 swap 步骤的全局手续费累计变量。

    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;// the global fee growth of the input token. updated in storage at the end of swapuint256 feeGrowthGlobalX128;}

结构体 StepComputations用于 swap 过程中单步撮合计算过程中临时存储每一步撮合的中间变量。各字段含义如下:

字段名含义说明
sqrtPriceStartX96本 step 开始时的价格(Q64.96 格式的 sqrt(price))
tickNext当前 swap 方向上,下一个会被跨越的 tick 索引
initialized下一个 tick 是否已初始化(即是否有流动性变化点)
sqrtPriceNextX96下一个 tick 的 sqrt(price)(Q64.96 格式)
amountIn本 step 实际 swap 进入池子的 token 数量(输入币种)
amountOut本 step 实际 swap 从池子换出的 token 数量(输出币种)
feeAmount本 step 实际收取的手续费(以输入币种计)
feeGrowthGlobalX128本 step 结束后,输入币种的全局手续费累计值(用于 LP 分配手续费,Q128 定点数)

2. 主循环:分步撮合

while (!(amountSpecifiedRemaining == 0 || result.sqrtPriceX96 == params.sqrtPriceLimitX96)) {...
}

只要还有剩余兑换量、且价格未触及极限,就继续撮合。

计算下一个tick

下一步是根据当前的tick和方向计算下一个tick

(step.tickNext, step.initialized) =self.tickBitmap.nextInitializedTickWithinOneWord(result.tick, params.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;
}
if (step.tickNext >= TickMath.MAX_TICK) {step.tickNext = TickMath.MAX_TICK;
}// get the price for the next tick
step.sqrtPriceNextX96 = TickMath.getSqrtPriceAtTick(step.tickNext);

nextInitializedTickWithinOneWord方法返回了两个参数,一个是下一个tick,以及返回的nextTick是否是已经初始化的(nexttick是在当前tick所在的位图中寻找,如果当前的tick已经是所在位图的最后一个初始化的tick,那么就会返回位图的最后一个tick,该tick是没有被初始化的)

TickMath.getSqrtPriceAtTick就是根据nextTick的值计算价格。

  • bool zeroForOne = sqrtPriceCurrentX96 >= sqrtPriceTargetX96 //方向:token0 → token1(价格下降)还是反之
  • bool exactIn = amountRemaining < 0 //模式:exact input(指定输入)还是 exact output(指定输出)

单步交换

接下来的这个方法是swap过程中 单步撮合的核心计算方法,对于computeSwapStep方法的解析需要仔细理解。

// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
(result.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(result.sqrtPriceX96,SwapMath.getSqrtPriceTarget(zeroForOne, step.sqrtPriceNextX96, params.sqrtPriceLimitX96),result.liquidity,amountSpecifiedRemaining,swapFee
);

简而言之就是计算当前的交易步骤所需要消耗tokenIn的数量amountIn,可以得到tokenOut的数量的amountOut,以及交易完成后价格的位置sqrtPriceNextX96和本 step 实际收取的手续费feeAmount

更新剩余兑换量和累计兑换量

下一步是更新剩余兑换量和累计兑换量的逻辑。根据 swap 的模式(exact output 或 exact input)分别处理。

// if exactOutput
if (params.amountSpecified > 0) {unchecked {amountSpecifiedRemaining -= step.amountOut.toInt256();}amountCalculated -= (step.amountIn + step.feeAmount).toInt256();
} else {// safe because we test that amountSpecified > amountIn + feeAmount in SwapMathunchecked {amountSpecifiedRemaining += (step.amountIn + step.feeAmount).toInt256();}amountCalculated += step.amountOut.toInt256();
}
  • params.amountSpecified > 0exact output(用户指定想要获得多少输出币)
  • params.amountSpecified < 0exact input(用户指定想要输入多少币)
  • amountSpecifiedRemaining:还需要兑换的数量(剩余目标)
  • amountCalculated:累计已兑换的数量(实际消耗或获得)
  • step.amountIn:本 step 实际消耗的输入币数量
  • step.amountOut:本 step 实际获得的输出币数量
  • step.feeAmount:本 step 实际收取的手续费

如果用户指定想要获得多少输出币(amountSpecified > 0)。每撮合一步,实际获得的输出币 step.amountOut,从剩余目标中减去。实际消耗的输入币(包括手续费)step.amountIn + step.feeAmount,累计到 amountCalculated(为负数,表示实际消耗)。

用户指定想要输入多少币(amountSpecified < 0)。每撮合一步,实际消耗的输入币(包括手续费)step.amountIn + step.feeAmount,从剩余目标中减去(这里是加,因为 amountSpecified 是负数)。实际获得的输出币 step.amountOut,累计到 amountCalculated(为正数,表示实际获得)。

协议手续费

接下来是计算协议手续费:

if (protocolFee > 0) {unchecked {// step.amountIn does not include the swap fee, as it's already been taken from it,// so add it back to get the total amountIn and use that to calculate the amount of fees owed to the protocol// cannot overflow due to limits on the size of protocolFee and params.amountSpecified// this rounds down to favor LPs over the protocolint256 delta = (swapFee == protocolFee)? step.feeAmount // lp fee is 0, so the entire fee is owed to the protocol instead: (step.amountIn + step.feeAmount) * protocolFee / ProtocolFeeLibrary.PIPS_DENOMINATOR;// subtract it from the total fee and add it to the protocol feestep.feeAmount -= delta;amountToProtocol += delta;}
}

在协议费开启时(protocolFee > 0),计算本 step 应分给协议的手续费,并从 LP 的 fee 中扣除,累加到协议费池。

如果swapFee == protocolFee:说明 LP fee 为 0,所有手续费都归协议所有。这时协议直接拿走全部 fee:delta = step.feeAmount

否则协议只拿总手续费的一部分,按比例分配。step.amountIn + step.feeAmount 是本 step 的总输入(含手续费),乘以协议费比例,得到协议应得的 fee。ProtocolFeeLibrary.PIPS_DENOMINATOR 是分母(通常为 1e6,表示百万分之一精度)。

step.feeAmount -= delta:从 LP 的 fee 中扣除协议应得部分(delta),剩下的 fee 才归 LP。

amountToProtocol += delta:协议应得部分累加到 amountToProtocol,用于后续结算。

全局手续费

接下来是更新全局的 fee 累计变量(feeGrowthGlobalX128),用于后续 LP 按照自己流动性份额分配手续费。

if (result.liquidity > 0) {unchecked {// FullMath.mulDiv isn't needed as tokens有最大供应量,不会溢出step.feeGrowthGlobalX128 +=UnsafeMath.simpleMulDiv(step.feeAmount, FixedPoint128.Q128, result.liquidity);}
}

 新增fee增长 = 本step手续费 * 2^128 / 当前流动性

流动性和手续费结算

接下来是撮合循环的最后一步。处理 swap 过程中价格推进到 tick 边界时的 tick 跨越、流动性变化和手续费结算。

if (result.sqrtPriceX96 == step.sqrtPriceNextX96) {// if the tick is initialized, run the tick transitionif (step.initialized) {(uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = zeroForOne? (step.feeGrowthGlobalX128, self.feeGrowthGlobal1X128): (self.feeGrowthGlobal0X128, step.feeGrowthGlobalX128);int128 liquidityNet =Pool.crossTick(self, step.tickNext, feeGrowthGlobal0X128, feeGrowthGlobal1X128);// if we're moving leftward, we interpret liquidityNet as the opposite sign// safe because liquidityNet cannot be type(int128).minunchecked {if (zeroForOne) liquidityNet = -liquidityNet;}result.liquidity = LiquidityMath.addDelta(result.liquidity, liquidityNet);}unchecked {result.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;}
} else if (result.sqrtPriceX96 != step.sqrtPriceStartX96) {// recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't movedresult.tick = TickMath.getTickAtSqrtPrice(result.sqrtPriceX96);
}

在之前computeSwapStep方法的解析的文章中我们知道,单步交换所到达的价格(result.sqrtPriceX96)和交换时设定的目标价格(step.sqrtPriceNextX96)未必是一致的,因为tokenIn的剩余数量可能不足支持交易的价格推进到sqrtPriceNextX96,此时交换完成的价格(sqrtPriceX96)会位于交易前价格和目标价格(sqrtPriceNextX96)的中间,所以
if (result.sqrtPriceX96 == step.sqrtPriceNextX96) 就是判断单步撮合后是否到达了目标价格,如果条件成立代表本次 swap 已经推进到了下一个 tick 的边界(价格正好到达 tick 边界)

if (step.initialized) 的判断值来自于之前的处理

(step.tickNext, step.initialized) =
self.tickBitmap.nextInitializedTickWithinOneWord(result.tick, params.tickSpacing, zeroForOne);

nextInitializedTickWithinOneWord 会在当前 tick 所在的 256 位 bitmap word 内,查找下一个已初始化的 tick(即 bitmap 中下一个为 1 的 bit)。如果当前 word 内没有下一个已初始化的 tick,就会返回该 word 的边界 tick(比如 255 或 0),但此时 initialized 会是 false,表示这个 tick 实际上没有被初始化。

如果tickNext已经被初始化则需要处理手续费和流动性。

  • 选择正确的全局 fee 增长变量(token0 或 token1),用于 tick 跨越时的手续费结算。
  • 调用 crossTick,处理 tick 跨越的逻辑(包括更新 tick 的 feeGrowthOutside、返回该 tick 的净流动性变化)。

对于这两步的理解需要仔细阅读uniswap v3/v4 中pool的状态管理

if (zeroForOne) liquidityNet = -liquidityNet;        //方向修正:如果是 zeroForOne(token0→token1,tick 向左),需要把流动性变化符号取反。

result.liquidity = LiquidityMath.addDelta(result.liquidity, liquidityNet);        //更新当前区间流动性tick 跨越的净流动性变化,更新当前区间的流动性。

如果没有到达价格并没有到达已初始化的下一个tick,但价格又发生变化,证明此时交换完成的价格(sqrtPriceX96)位于交易前价格和目标价格(sqrtPriceNextX96)的中间需要重新计算一下tick

else if (result.sqrtPriceX96 != step.sqrtPriceStartX96) {result.tick = TickMath.getTickAtSqrtPrice(result.sqrtPriceX96);
}

每次循环都会消耗一部分amountSpecifiedRemaining,当amountSpecifiedRemaining消耗完或者价格到达极限,则会完成交换。

更新池子的全局状态

        self.slot0 = slot0Start.setTick(result.tick).setSqrtPriceX96(result.sqrtPriceX96);// update liquidity if it changedif (self.liquidity != result.liquidity) self.liquidity = result.liquidity;// update fee growth globalif (!zeroForOne) {self.feeGrowthGlobal1X128 = step.feeGrowthGlobalX128;} else {self.feeGrowthGlobal0X128 = step.feeGrowthGlobalX128;}

这部分代码是在 swap 结束后更新池子的全局状态,确保池子的价格、tick、流动性和手续费分配变量都与最新状态同步。比较直观的代码,不多做解释。

计算 token的净变化量

unchecked {// "if currency1 is specified"if (zeroForOne != (params.amountSpecified < 0)) {swapDelta = toBalanceDelta(amountCalculated.toInt128(), (params.amountSpecified - amountSpecifiedRemaining).toInt128());} else {swapDelta = toBalanceDelta((params.amountSpecified - amountSpecifiedRemaining).toInt128(), amountCalculated.toInt128());}
}

这段代码的作用是根据 swap 的方向和模式,最终计算本次 swap 的 token0 和 token1 的净变化量(BalanceDelta),即池子资产的实际增减。

首先zeroForOne != (params.amountSpecified < 0) 判断本次 swap 的主币种是 token1 还是 token0,具体如下:

zeroForOneamountSpecified < 0方向模式主币种(输入币)
truetrue0 → 1exact inputtoken0
truefalse0 → 1exact outputtoken1
falsetrue1 → 0exact inputtoken1
falsefalse1 → 0exact outputtoken0

zeroForOne != (params.amountSpecified < 0) 为 true 时,主币种是 token1,否则主币种是 token0

再看toBalanceDelta操作,这里涉及到两个参数:
amountCalculated

amountSpecified - amountSpecifiedRemaining

分别代表交换完成后两个token的净变化量:

swap方向 (zeroForOne)swap模式 (amountSpecified)amountCalculated amountSpecified - amountSpecifiedRemainingamountCalculated 符号params.amountSpecified - amountSpecifiedRemaining 符号
true (0→1)< 0 (exact input)token1 (out)token0 (in)正(用户获得)负(用户支付)
true (0→1)> 0 (exact output)token0 (in)token1 (out)负(用户支付)正(用户获得)
false (1→0)< 0 (exact input)token0 (out)token1 (in)正(用户获得)负(用户支付)
false (1→0)> 0 (exact output)token1 (in)token0 (out)负(用户支付)正(用户获得)

toBalanceDelta操作就是把两个净变化量拼接成256位的数,高128位代表token0,低128位代表token1

function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) {assembly ("memory-safe") {balanceDelta := or(shl(128, _amount0), and(sub(shl(128, 1), 1), _amount1))}
}
  • shl(128, _amount0):把 amount0 左移 128 位,放到 int256 的高 128 位。
  • and(sub(shl(128, 1), 1), _amount1):把 amount1 保留在低 128 位(掩码只保留低 128 位)。
  • or(...):把高 128 位的 amount0 和低 128 位的 amount1 合并成一个 int256。

这其中and(sub(shl(128, 1), 1), _amount1)

  • shl(128, 1):把 1 左移 128 位,得到 2^128,即 0x100000000000000000000000000000000

  • sub(shl(128, 1), 1):用 2^128 - 1,即 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,这是一个低 128 位全为 1 的掩码。

  • and(..., _amount1):用掩码和 _amount1 做按位与,只保留 _amount1 的低 128 位,高位全部清零。

之所以这么是因为amount1 是 int128,在 Solidity 中,int128 转成 int256 时会符号扩展,也就是如果 amount1 是负数,高 128 位会被填充为 1(即 0xFFFF...)。如果直接 or(shl(128, _amount0), _amount1),那么当 amount1 为负数时,高 128 位会被污染,破坏了原本应该只属于 amount0 的高 128 位。

返回

到这里pool的swap函数就全部讲完了,返回参数如下:

  1.  BalanceDelta swapDelta:本次 swap 结束后,池子的 token0 和 token1 的净变化量(带符号,正数为池子流入,负数为池子流出)。高128位为 token0 变化,低128位为 token1 变化,均为 int128

  2. uint256 amountToProtocol:本次 swap 过程中应分给 Uniswap 协议的手续费总额(通常为输入币种的一部分)。用于协议费池的累加,后续可被协议治理提取。

  3. uint24 swapFee本次 swap 实际使用的总手续费率(单位为 pips,1e6 = 100%),包含 LP fee 和 protocol fee。

  4. SwapResult memory result

  • uint160 sqrtPriceX96:swap 结束后的池子 sqrt 价格

  • int24 tick:swap 结束后的池子 tick

  • uint128 liquidity:swap 结束后的池子当前 tick 区间流动性

后续处理

pool.swap函数处理完的第一个步骤就是协议的手续费处理

if (amountToProtocol > 0) _updateProtocolFees(inputCurrency, amountToProtocol);function _updateProtocolFees(Currency currency, uint256 amount) internal {unchecked {protocolFeesAccrued[currency] += amount;}
}

把本次 swap 过程中应分给 Uniswap 协议的手续费(protocol fee)累加到协议费池中。

最后一步是账本的记录:

BalanceDelta hookDelta;
(swapDelta, hookDelta) = key.hooks.afterSwap(key, params, swapDelta, hookData, beforeSwapDelta);// if the hook doesn't have the flag to be able to return deltas, hookDelta will always be 0
if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks));_accountPoolBalanceDelta(key, swapDelta, msg.sender);
  • 首先是调用 afterSwap 钩子(详情见uniswap v4 hooks详解)。允许 hook 合约对 swap 结果(swapDelta)进行自定义调整。返回两个值:

        swapDelta:最终池子和用户的资产变化(可能被钩子调整过)。

        hookDelta:钩子合约自身的资产变化(奖励、返现、额外收费等)。

  • 如果钩子合约返回了非零的 hookDelta(即钩子需要收/付 token),就把这部分资产变化记到账本上,归属于钩子合约地址。这样可以实现钩子合约奖励、收取额外费用等功能。
  • 记账用户的 swap 结果:把最终的 swapDelta 记到账本上,归属于发起 swap 的用户(msg.sender)。这一步实现了用户和池子的资产结算。

账本到相关文章:uniswap v4 账本式结算与账户余额管理机制解析

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com