# 深入理解 Uniswap v3 合约代码 (一) ###### tags: `uniswap` `solidity` `logarithm` `uniswap-v3` `tick` `core` ## 概述 与Uniswap v2一样,Uniswap v3的合约也分为两类: * [Uniswap-v3-core](#Uniswap-v3-core) - Uniswap v3的核心代码,实现了协议定义的所有功能,外部合约可直接与core合约交互 * [Uniswap-v3-periphery](https://hackmd.io/cTPg4x2TR4WthYEF8anLug) - 基于使用场景封装接口,如头寸(Position)管理、多路径代币交换等功能,Uniswap界面即与periphery合约交互 如果你希望以用户场景角度阅读本文,请直接从[Uniswap-v3-periphery](https://hackmd.io/cTPg4x2TR4WthYEF8anLug)开始,它包含了创建头寸、修改头寸流动性、交换代币等常用功能。 如果你希望从底层核心模块开始阅读,请从[Uniswap-v3-core](#Uniswap-v3-core)开始。 ## Uniswap-v3-core ### UniswapV3Factory.sol 工厂合约主要包含三个功能: * [createPool](#createPool):创建交易对池子 * [setOwner](#setOwner):设置工厂合约Owner * [enableFeeAmount](#enableFeeAmount):添加手续费等级 #### createPool 创建一个Uniswap v3交易对池子,注意,由于Uniswap v3支持不同手续费等级,如0.05%、0.30%、1.00%等,因此一个交易对合约由`tokenA`、`tokenB`和`fee`(手续费)唯一确定。 > 计算交易对合约还需要:factory工厂合约地址、合约初始化代码的hash。 ```solidity /// @inheritdoc IUniswapV3Factory function createPool( address tokenA, address tokenB, uint24 fee ) external override noDelegateCall returns (address pool) { require(tokenA != tokenB); (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0)); int24 tickSpacing = feeAmountTickSpacing[fee]; require(tickSpacing != 0); require(getPool[token0][token1][fee] == address(0)); pool = deploy(address(this), token0, token1, fee, tickSpacing); getPool[token0][token1][fee] = pool; // populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses getPool[token1][token0][fee] = pool; emit PoolCreated(token0, token1, fee, tickSpacing, pool); } ``` 因为传入的`tokenA`和`tokenB`是无序的,首先对`tokenA`、`tokenB`排序,确保`tokenA < tokenB`。 通过手续费等级获取对应的`tickSpacing`: ```solidity int24 tickSpacing = feeAmountTickSpacing[fee]; ``` 我们在《深入理解Uniswap v3白皮书》中介绍过`tickSpacing`的作用,每个手续费等级对应一个`tickSpacing`,只有被`tickSpacing`整除的`tick`才允许被初始化,`tickSpacing`越大,每个`tick`流动性越多,`tick`之间滑点越大,但会节省跨`tick`操作的gas。这里作为`Pool`的参数保存起来。 确认该交易对对应的手续费等级没有创建过: ```solidity require(getPool[token0][token1][fee] == address(0)); ``` 创建(部署)交易对合约: ```solidity pool = deploy(address(this), token0, token1, fee, tickSpacing); ``` deploy代码如下: ```solidity /// @dev Deploys a pool with the given parameters by transiently setting the parameters storage slot and then /// clearing it after deploying the pool. /// @param factory The contract address of the Uniswap V3 factory /// @param token0 The first token of the pool by address sort order /// @param token1 The second token of the pool by address sort order /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip /// @param tickSpacing The spacing between usable ticks function deploy( address factory, address token0, address token1, uint24 fee, int24 tickSpacing ) internal returns (address pool) { parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing}); pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}()); delete parameters; } ``` 我们在Uniswap v2中提到,为了确保交易对合约地址的可计算性和唯一性,Uniswap v2使用`CREATE2`操作码创建交易对合约;从Solidity 0.6.2版本开始([Github PR](https://github.com/ethereum/solidity/pull/8177)),支持在`new`方法中传递`salt`参数实现`CREATE2`功能;`salt`参数确保了合约地址的唯一性和可计算性。从代码可知,Uniswap v3交易对合约使用token0、token1、fee唯一确定一个交易对合约,比如,根据ETH-USDC 0.05%手续费(以及工厂合约地址、初始化代码hash)等信息,可计算交易对合约地址。 最后,保存交易对合约地址到`getPool`变量中: ```solidity getPool[token0][token1][fee] = pool; // populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses getPool[token1][token0][fee] = pool; ``` #### setOwner 设置工厂合约owner,owner具有以下权限: * [setOwner](#setOwner):修改owner * [enableFeeAmount](#enableFeeAmount):添加手续费等级 * [setFeeProtocol](#setFeeProtocol):修改某个交易对的协议手续费比例 * [collectProtocol](#collectProtocol):收集某个交易对的协议手续费 首先判断请求由当前owner发起,确认后修改owner: ```solidity /// @inheritdoc IUniswapV3Factory function setOwner(address _owner) external override { require(msg.sender == owner); emit OwnerChanged(owner, _owner); owner = _owner; } ``` #### enableFeeAmount Uniswap v3默认支持三种手续费等级:0.05%、0.30%和1.00%,对应的fee值分别为500、3000和10000;fee的基本单位是百分之一基点,即0.01 bp = $10^{-6}$。 手续费百分比计算公式为: $$ f_{ratio} = \frac{fee}{1,000,000} $$ ```solidity /// @inheritdoc IUniswapV3Factory function enableFeeAmount(uint24 fee, int24 tickSpacing) public override { require(msg.sender == owner); require(fee < 1000000); // tick spacing is capped at 16384 to prevent the situation where tickSpacing is so large that // TickBitmap#nextInitializedTickWithinOneWord overflows int24 container from a valid tick // 16384 ticks represents a >5x price change with ticks of 1 bips require(tickSpacing > 0 && tickSpacing < 16384); require(feeAmountTickSpacing[fee] == 0); feeAmountTickSpacing[fee] = tickSpacing; emit FeeAmountEnabled(fee, tickSpacing); } ``` ### UniswapV3Pool.sol 这是Uniswap v3的主要代码,定义了交易对池子的功能: * [initialize](#initialize):初始化交易对 * [mint](#mint):添加流动性 * [burn](#burn):移除流动性 * [swap](#swap):交换代币 * [flash](#flash):闪电贷 * [collect](#collect):取回代币 * [increaseObservationCardinalityNext](#increaseObservationCardinalityNext):扩展预言机空间 * [observe](#observe):获取预言机数据 此外,factory(工厂合约)owner还可以调用以下两个方法: * [setFeeProtocol](#setFeeProtocol):修改某个交易对的协议手续费比例 * [collectProtocol](#collectProtocol):收集某个交易对的协议手续费 #### initialize 创建完交易对后,需要调用`initialize`方法初始化合约,才能正常使用交易对功能。 该方法初始化`slot0`变量: ```solidity /// @inheritdoc IUniswapV3PoolActions /// @dev not locked because it initializes unlocked function initialize(uint160 sqrtPriceX96) external override { require(slot0.sqrtPriceX96 == 0, 'AI'); int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); (uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp()); slot0 = Slot0({ sqrtPriceX96: sqrtPriceX96, tick: tick, observationIndex: 0, observationCardinality: cardinality, observationCardinalityNext: cardinalityNext, feeProtocol: 0, unlocked: true }); emit Initialize(sqrtPriceX96, tick); } ``` `slot0`定义如下: * `sqrtPriceX96`:交易对当前的开根号价格$\sqrt{P}$ * `tick`:当前$\sqrt{P}$对应的tick,使用[getTickAtSqrtRatio](#getTickAtSqrtRatio)计算得出 * `observationIndex`:最近更新的(预言机)观测点数组序号 * `observationCardinality`:(预言机)观测点数组容量,最大65536,初始时为1 * `observationCardinalityNext`:下一个(预言机)观测点数组容量,如果手动扩容容量,会更新这个值,初始时为1 * `feeProtocol`:协议手续费比例,可以分别为`token0`和`token1`设置交易手续费中分给协议的比例 * `unlocked`:当前交易对合约是否非锁定状态 #### mint 该方法实现添加流动性功能。实际上,首次添加流动性和后续增加流动性,都会使用该方法。 `mint`方法参数如下: * `recipient`:头寸接收者(owner) * `tickLower`:流动性区间低点 * `tickUpper`:流动性区间高点 * `amount`:流动性数量 * `data`:回调参数 ```solidity /// @inheritdoc IUniswapV3PoolActions /// @dev noDelegateCall is applied indirectly via _modifyPosition function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external override lock returns (uint256 amount0, uint256 amount1) { require(amount > 0); (, int256 amount0Int, int256 amount1Int) = _modifyPosition( ModifyPositionParams({ owner: recipient, tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: int256(amount).toInt128() }) ); amount0 = uint256(amount0Int); amount1 = uint256(amount1Int); uint256 balance0Before; uint256 balance1Before; if (amount0 > 0) balance0Before = balance0(); if (amount1 > 0) balance1Before = balance1(); IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data); if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0'); if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1'); emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1); } ``` `mint`方法主要的逻辑都在[_modifyPosition](#_modifyPosition)中,其返回的`amount0Int`和`amount1Int`表示:如果添加`amount`数量的流动性,则需要分别向交易对合约转入的`token0`和`token1`的代币数量。 调用方需在`uniswapV3MintCallback`完成代币的转入操作;调用`mint`方法的合约需要实现`IUniswapV3MintCallback`接口,Uniswap v3在[periphery](https://hackmd.io/cTPg4x2TR4WthYEF8anLug)合约的`NonfungiblePositionManager.sol`实现该接口。 > 因为`mint`调用方需要实现接口方法,因此个人ETH账户无法调用该方法。 ```solidity IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data); ``` ##### _modifyPosition 继续来看`_modifyPosition`: ```solidity /// @dev Effect some changes to a position /// @param params the position details and the change to the position's liquidity to effect /// @return position a storage pointer referencing the position with the given owner and tick range /// @return amount0 the amount of token0 owed to the pool, negative if the pool should pay the recipient /// @return amount1 the amount of token1 owed to the pool, negative if the pool should pay the recipient function _modifyPosition(ModifyPositionParams memory params) private noDelegateCall returns ( Position.Info storage position, int256 amount0, int256 amount1 ) { checkTicks(params.tickLower, params.tickUpper); Slot0 memory _slot0 = slot0; // SLOAD for gas optimization position = _updatePosition( params.owner, params.tickLower, params.tickUpper, params.liquidityDelta, _slot0.tick ); ``` 先通过[_updatePosition](#_updatePosition)更新头寸信息,我们在下一节会具体介绍。 ```solidity if (params.liquidityDelta != 0) { if (_slot0.tick < params.tickLower) { // current tick is below the passed range; liquidity can only become in range by crossing from left to // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it amount0 = SqrtPriceMath.getAmount0Delta( TickMath.getSqrtRatioAtTick(params.tickLower), TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); } else if (_slot0.tick < params.tickUpper) { // current tick is inside the passed range uint128 liquidityBefore = liquidity; // SLOAD for gas optimization // write an oracle entry (slot0.observationIndex, slot0.observationCardinality) = observations.write( _slot0.observationIndex, _blockTimestamp(), _slot0.tick, liquidityBefore, _slot0.observationCardinality, _slot0.observationCardinalityNext ); amount0 = SqrtPriceMath.getAmount0Delta( _slot0.sqrtPriceX96, TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); amount1 = SqrtPriceMath.getAmount1Delta( TickMath.getSqrtRatioAtTick(params.tickLower), _slot0.sqrtPriceX96, params.liquidityDelta ); liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta); } else { // current tick is above the passed range; liquidity can only become in range by crossing from right to // left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it amount1 = SqrtPriceMath.getAmount1Delta( TickMath.getSqrtRatioAtTick(params.tickLower), TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); } } } ``` 代码的下半部分则主要通过[getAmount0Delta](#getAmount0Delta)和[getAmount1Delta](#getAmount1Delta)计算该流动性需要分别提供的`token0`和`token1`的数量,即`amount0`和`amount1`。 具体地,当你提供流动性的区间大于当前`tick`$i_c$时,因为`tick`大小与$\sqrt{P}$(即$\sqrt{\frac{y}{x}}$)成正比,意味着在大于$i_c$的区间,$x$的价值更高(需要更少的$x$),因此添加流动性时需在该部分提供$x$代币,即`amount0`数量的`token0`;反之,则提供$y$代币,即`amount1`的`token1`。 如下所示: $$ \begin{cases}i_c, ..., \overbrace{i_l, ..., i_u}^{amount0} & \text{$i_c < i_l$}\\ \overbrace{i_l, ...}^{amount1}, i_c, \overbrace{..., i_u}^{amount0} & \text{$i_l \leq i_c < i_u$}\\ \overbrace{i_l, ..., i_u}^{amount1}, ..., i_c & \text{$i_u \leq i_c$}\end{cases} $$ 其中,$i_l$, $i_u$为提供流动性价格区间的边界,$i_c$为当前价格对应的`tick`。 如果当前价格在区间中,即$i_l \leq i_c < i_u$时,`_modifyPosition`会记录一次(预言机)观测点数据,因为此时区间的流动性发生了变化,需要记录每流动性的持续时间`secondsPerLiquidityCumulativeX128`: ```solidity // write an oracle entry (slot0.observationIndex, slot0.observationCardinality) = observations.write( _slot0.observationIndex, _blockTimestamp(), _slot0.tick, liquidityBefore, _slot0.observationCardinality, _slot0.observationCardinalityNext ); ``` 在计算`amount0`和`amount1`后,更新当前交易对的全局活跃流动性`liquidity`: ```solidity liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta); ``` 这个全局流动性会在[swap](#swap)时用到。 ##### _updatePosition `_modifyPosition`中的`_updatePosition`代码如下: ```solidity /// @dev Gets and updates a position with the given liquidity delta /// @param owner the owner of the position /// @param tickLower the lower tick of the position's tick range /// @param tickUpper the upper tick of the position's tick range /// @param tick the current tick, passed to avoid sloads function _updatePosition( address owner, int24 tickLower, int24 tickUpper, int128 liquidityDelta, int24 tick ) private returns (Position.Info storage position) { position = positions.get(owner, tickLower, tickUpper); uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128; // SLOAD for gas optimization uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; // SLOAD for gas optimization // if we need to update the ticks, do it bool flippedLower; bool flippedUpper; if (liquidityDelta != 0) { uint32 time = _blockTimestamp(); (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observations.observeSingle( time, 0, slot0.tick, slot0.observationIndex, liquidity, slot0.observationCardinality ); ``` `observations.observeSingle`计算从最后一次观测点到现在的累积tick`tickCumulative`和累积每份流动性的持续时间`secondsPerLiquidityCumulativeX128`。 ```solidity flippedLower = ticks.update( tickLower, tick, liquidityDelta, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128, secondsPerLiquidityCumulativeX128, tickCumulative, time, false, maxLiquidityPerTick ); flippedUpper = ticks.update( tickUpper, tick, liquidityDelta, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128, secondsPerLiquidityCumulativeX128, tickCumulative, time, true, maxLiquidityPerTick ); if (flippedLower) { tickBitmap.flipTick(tickLower, tickSpacing); } if (flippedUpper) { tickBitmap.flipTick(tickUpper, tickSpacing); } } ``` 接着使用`ticks.update`分别更新`tickLower`(价格区间低点)和`tickUpper`(价格区间高点)的状态,具体请参考[Tick.update](#update)。 如果对应`tick`的流动性从从0到有,或从有到0,则表示该`tick`需要被翻转。如果该`tick`未被标记为初始化,则标记为初始化;否则,将其取消初始化;这里用到`tickBitmap.flipTick`方法,请参考[TickBitmap.flipTick](#flipTick)。 ```solidity (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128); ``` 接着,计算该价格区间的累积每流动性手续费。 ```solidity position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128); ``` 更新头寸(Position)信息,这里主要更新了头寸的应收手续费`tokensOwed0`和`tokensOwed1`,以及头寸流动性`liquidity`,请参考[Position.update](#update1)。 ```solidity // clear any tick data that is no longer needed if (liquidityDelta < 0) { if (flippedLower) { ticks.clear(tickLower); } if (flippedUpper) { ticks.clear(tickUpper); } } } ``` 如果是移除流动性,并且`tick`被翻转,则调用[clear](#Tick.clear)清空`tick`状态。 最后,回到`mint`方法,调用者需要确保在`uniswapV3MintCallback`方法中,将这里计算出的`amount0`和`amount1`数量的`token0`和`token1`代币转入交易对合约。 总结`mint`方法的主要工作如下: 1. 更新价格区间端点(lower, upper)的信息:`ticks.update` 2. 如果Tick状态翻转,则更新位图的标识位,设置为“已初始化”或“未初始化”:`tickBitmap.flipTick` 3. 更新头寸(Position)信息:`positions.update` 4. 如果当前tick位于价格区间中,则: - 写入一次预言机观测点:`observations.write` - 更新全局活跃流动性:`liquidity` 5. 调用方向交易对合约转账:`uniswapV3MintCallback` #### burn 销毁流动性(`burn`)的逻辑与添加流动性(`mint`)几乎完全一样,唯一的区别是`liquidityDelta`是负的。 ```solidity /// @inheritdoc IUniswapV3PoolActions /// @dev noDelegateCall is applied indirectly via _modifyPosition function burn( int24 tickLower, int24 tickUpper, uint128 amount ) external override lock returns (uint256 amount0, uint256 amount1) { (Position.Info storage position, int256 amount0Int, int256 amount1Int) = _modifyPosition( ModifyPositionParams({ owner: msg.sender, tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: -int256(amount).toInt128() }) ); amount0 = uint256(-amount0Int); amount1 = uint256(-amount1Int); if (amount0 > 0 || amount1 > 0) { (position.tokensOwed0, position.tokensOwed1) = ( position.tokensOwed0 + uint128(amount0), position.tokensOwed1 + uint128(amount1) ); } emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1); } ``` 这里与`mint`使用同一个[_modifyPosition](#_modifyPosition)方法。 需注意,当销毁(部分)流动性后,代币并没有转回到调用方,而是以未领取代币的形式记在头寸(Position)上。 #### swap `swap`方法是Uniswap v3代码的核心,该方法实现两个代币的交换,从`token0`交换到`token1`,或者相反。 相比Uniswap v2的同质化流动性,我们重点关注在`swap`过程中,价格如何变化,以及如何影响流动性的。 先来看`swap`方法的几个参数: * `recipient`:交易后的代币接收者 * `zeroForOne`:如果从`token0`交换`token1`则为`true`,从`token1`交换`token0`则为`false` * `amountSpecified`: 指定的代币数量,如果为正,表示希望输入的代币数量;如果为负,则表示希望输出的代币数量 * `sqrtPriceLimitX96`:能够承受的价格上限(或下限),格式为`Q64.96`;如果从`token0`到`token1`,则表示`swap`过程中的价格下限;如果从`token1`到`token0`,则表示价格上限;如果价格超过该值,则`swap`失败 * `data`:回调参数 ```solidity /// @inheritdoc IUniswapV3PoolActions function swap( address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) external override noDelegateCall returns (int256 amount0, int256 amount1) { require(amountSpecified != 0, 'AS'); Slot0 memory slot0Start = slot0; 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 = false; SwapCache memory cache = SwapCache({ liquidityStart: liquidity, blockTimestamp: _blockTimestamp(), feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4), secondsPerLiquidityCumulativeX128: 0, tickCumulative: 0, computedLatestObservation: false }); 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 }); ``` 上面代码主要是初始化状态相关的。 因为$\sqrt{P} = \sqrt{\frac{y}{x}}$,当`zeroForOne = true`,即从`token0`到`token1`时,swap过程中$x$变多,$y$变少,因此$\sqrt{P}$逐渐减小,所以指定的价格极限`sqrtPriceLimitX96`需要小于当前市场价格`sqrtPriceX96`。 另外,需要注意几个关键数据: * 初始交易价格`state.sqrtPriceX96`为:`slot0.sqrtPriceX96` - 这里并没有使用`slot0.tick`计算初始价格,因为计算出来的值与`slot0.sqrtPriceX96`可能不一致,我们在后面代码会看到,`slot0.tick`不能作为当前价格 * 初始可用流动性`state.liquidity`为:`liquidity`,也就是我们在[mint](#mint)或[burn](#burn)时更新的全局可用流动性 根据`zeroForOne`和`exactInput`,可以有四种`swap`组合: |zeroForOne|exactInput|swap| |---|---|---| |true|true|输入固定数量`token0`,输出最大数量`token1`| |true|false|出入最小数量`token0`,输出固定数量`token1`| |false|true|输入固定数量`token1`,输出最大数量`token0`| |false|false|输入最小数量`token1`,输出固定数量`token0`| 一个完整的`swap`可以由多个`step`组成,代码如下: ```solidity // 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 != 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 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, (zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96) ? sqrtPriceLimitX96 : step.sqrtPriceNextX96, state.liquidity, state.amountSpecifiedRemaining, fee ); if (exactInput) { 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 if (state.liquidity > 0) state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity); // shift tick if we reached the next price if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { // if the tick is initialized, run the tick transition 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 = 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); } 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 state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); } } ``` 整理成伪代码(pseudo code)如下: ```python loop if 剩余代币 != 0 and 当前价格 != 最小(或最大)价格: // step 初始价格 := 上一个step的价格 下一个tick := 根据当前tick,寻找最近的已初始化的tick,或者本组最后一个未初始化的tick 目标价格 := 根据下一个tick计算的价格 交换后的价格, 消耗的输入代币数量, 得到的输出代币数量, 交易手续费 := 完成一步交换(初始价格, 目标价格, 可用流动性, 剩余代币) 更新 剩余代币 更新 协议手续费 if 交换后价格 == 目标价格: if tick已初始化: 价格穿越该tick,更新tick相关字段 更新 可用流动性 当前tick := 下一个tick - 1 else if 交换后价格 != 初始价格: 当前tick := 根据交换后价格计算tick ``` 首先需要根据当前`tick`寻找下一个`tick`,即`tickNext`,具体逻辑可参考:[tickBitmap.nextInitializedTickWithinOneWord](#nextInitializedTickWithinOneWord)。 计算当前step的目标价格: ```solidity // get the price for the next tick step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext); ``` 计算本次(step)交换的输入输出(即执行一次交换): ```solidity // 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, (zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96) ? sqrtPriceLimitX96 : step.sqrtPriceNextX96, state.liquidity, state.amountSpecifiedRemaining, fee ); ``` `SwapMath.computeSwapStep`将根据当前价格、目标价格、可用流动性、可用输入代币等数据,计算本次交换能最多成交的输入代币数量(`amountIn`),输出代币数量(`amountOut`),手续费(`feeAmount`)和成交后价格(`sqrtRatioNextX96`)。请参考[computeSwapStep](#computeSwapStep)。 保存本次交易的`amountIn`和`amountOut`: ```solidity if (exactInput) { 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()); } ``` * 如果是指定输入代币数量(`token0`或`token1`) - `amountSpecifiedRemaining`表示(扣除手续费后)剩余可用输入代币数量 - `amountCalculated`表示已输出代币数量(注意,这里是负值) * 如果是指定输出代币数量(`token0`或`token1`) - `amountSpecifiedRemaining`表示剩余需要输出的代币数量(初始为负值,因此每次交换后需要`+= step.amountOut`),直到为0 - `amountCalculated`表示(加入手续费后)已使用的输入代币数量 计算协议手续费: ```solidity // 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); } ``` 如果开启了协议手续费,则从交易手续费中拆出协议手续费。注意,协议手续费的值`feeProtocol`表示交易手续费的$\frac{1}{n}$。 ```solidity // update global fee tracker if (state.liquidity > 0) state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity); ``` 计算(每流动性)全局累积手续费。 ```solidity // shift tick if we reached the next price if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { // if the tick is initialized, run the tick transition 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 = 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); } 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 state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); } ``` * 如果本次交换后的价格达到目标价格(即根据寻找的下一个`tick`计算的价格): * 如果该`tick`已经初始化,则: - 通过`ticks.cross`方法穿越该`tick`,反向设置相关`Outside`变量的数据 - 使用`tick`净流动性`liquidityNet`更新可用流动性`state.liquidity` - 关于`liquidityNet`,请参考[Tick.update](#update) * 移动当前`tick`到下一个`tick` * 如果交换后的价格没有达到本次目标价格,但是又不等于初始价格,即表示此时交易结束: * 使用交换后的价格计算最新的`tick`值 重复上述step,直到交换完全结束。 完成交换后,更新全局状态: ```solidity // update tick and write an oracle entry if the tick change 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; } ``` * 如果交换后的`tick`与交换前的`tick`不同: * 记录一次(预言机)观测点数据,因为`tickCumulative`发生了改变 * 更新`slot0.sqrtPriceX96`, `slot0.tick`等值,注意此时`sqrtPriceX96`与`tick`并不一定对应,`sqrtPriceX96`才能准确反映当前价格 * 如果交换前后`tick`值相同,则只需要修改价格: * 更新`slot0.sqrtPriceX96` 同样,如果全局流动性发生改变,则更新`liquidity`: ```solidity // update liquidity if it changed if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity; ``` 更新累积手续费和协议手续费: ```solidity // update fee growth global and, if necessary, protocol fees // overflow is acceptable, protocol has to withdraw before it hits type(uint128).max fees 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; } ``` 注意,如果是从`token0`交换`token1`,则只能收取`token0`作为手续费;反之,只能收取`token1`作为手续费。 ```solidity (amount0, amount1) = zeroForOne == exactInput ? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated) : (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining); ``` 计算本次交换需要的具体`amount0`和`amount1`。 ```solidity // do the transfers and collect payment if (zeroForOne) { if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1)); uint256 balance0Before = balance0(); 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; ``` 合约将输出代币转账给`recipient`,同时,调用方需要在`uniswapV3SwapCallback`方法将输入代币转给交易对合约: 至此,整个`swap`流程就结束了。 #### flash 本方法实现Uniswap v3闪电贷功能。 方法参数: * `recipient`:闪电贷接收者 * `amount0`:借出`token0`的数量 * `amount1`:借出`token1`的数量 * `data`:回调方法参数 ```solidity /// @inheritdoc IUniswapV3PoolActions function flash( address recipient, uint256 amount0, uint256 amount1, bytes calldata data ) external override lock noDelegateCall { uint128 _liquidity = liquidity; require(_liquidity > 0, 'L'); uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6); uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6); ``` 闪电贷手续费与`swap`手续费相同,都是$\frac{fee}{10^6}$。 ```solidity uint256 balance0Before = balance0(); uint256 balance1Before = balance1(); if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0); if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1); IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data); uint256 balance0After = balance0(); uint256 balance1After = balance1(); require(balance0Before.add(fee0) <= balance0After, 'F0'); require(balance1Before.add(fee1) <= balance1After, 'F1'); ``` 向收款人转入贷出的代币数量,`flash`方法的调用方需实现`IUniswapV3FlashCallback.uniswapV3FlashCallback`接口方法,并在该方法中归还代币,包含手续费。 ```solidity // sub is safe because we know balanceAfter is gt balanceBefore by at least fee uint256 paid0 = balance0After - balance0Before; uint256 paid1 = balance1After - balance1Before; if (paid0 > 0) { uint8 feeProtocol0 = slot0.feeProtocol % 16; uint256 fees0 = feeProtocol0 == 0 ? 0 : paid0 / feeProtocol0; if (uint128(fees0) > 0) protocolFees.token0 += uint128(fees0); feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity); } if (paid1 > 0) { uint8 feeProtocol1 = slot0.feeProtocol >> 4; uint256 fees1 = feeProtocol1 == 0 ? 0 : paid1 / feeProtocol1; if (uint128(fees1) > 0) protocolFees.token1 += uint128(fees1); feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity); } emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1); } ``` 根据收取的手续费,计算协议手续费(注意,`token0`和`token1`的协议手续费是单独设置的,参考:[setFeeProtocol](#setFeeProtocol)),最后更新`protocolFees`和`feeGrowthGlobal1X128`。 #### collect 该方法实现取回代币功能,包括销毁流动性记录的代币和手续费代币。 参数如下: * `recipient`:代币接收者 * `tickLower`:头寸低点 * `tickUpper`:头寸高点 * `amount0Requested`:请求取回的`token0`数量 * `amount1Requested`:请求取回的`token1`数量 ```solidity /// @inheritdoc IUniswapV3PoolActions function collect( address recipient, int24 tickLower, int24 tickUpper, uint128 amount0Requested, uint128 amount1Requested ) external override lock returns (uint128 amount0, uint128 amount1) { // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1} Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper); amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested; amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested; if (amount0 > 0) { position.tokensOwed0 -= amount0; TransferHelper.safeTransfer(token0, recipient, amount0); } if (amount1 > 0) { position.tokensOwed1 -= amount1; TransferHelper.safeTransfer(token1, recipient, amount1); } emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1); } ``` 上述代码比较简单,这里不再展开。需要注意,如果希望取回所有代币,则需要指定比`tokensOwned`更大的数,比如可以使用`type(uint128).max`。 #### increaseObservationCardinalityNext 扩容预言机观测点的可写入空间,该方法调用[Oracle.sol](#Oracle.sol)的[grow](#grow)方法实现扩容。 ```solidity /// @inheritdoc IUniswapV3PoolActions function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external override lock noDelegateCall { uint16 observationCardinalityNextOld = slot0.observationCardinalityNext; // for the event uint16 observationCardinalityNextNew = observations.grow(observationCardinalityNextOld, observationCardinalityNext); slot0.observationCardinalityNext = observationCardinalityNextNew; if (observationCardinalityNextOld != observationCardinalityNextNew) emit IncreaseObservationCardinalityNext(observationCardinalityNextOld, observationCardinalityNextNew); } ``` #### observe 批量获取指定时间的观测点数据,该方法调用[Oracle.sol](#Oracle.sol)的[observe](#observe2)实现。 ```solidity /// @inheritdoc IUniswapV3PoolDerivedState function observe(uint32[] calldata secondsAgos) external view override noDelegateCall returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) { return observations.observe( _blockTimestamp(), secondsAgos, slot0.tick, slot0.observationIndex, liquidity, slot0.observationCardinality ); } ``` #### setFeeProtocol 设置协议手续费的比例,该方法仅允许工厂合约的owner执行。 注意,需要分别设置`token0`和`token1`的协议手续费比例,该比例是交易手续费的占比,合法值为0(不开启协议手续费)或者$4 \leq n \leq 10$,也就是可以设置协议手续费为交易手续费的$\frac{1}{n}$。 ```solidity /// @inheritdoc IUniswapV3PoolOwnerActions function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external override lock onlyFactoryOwner { require( (feeProtocol0 == 0 || (feeProtocol0 >= 4 && feeProtocol0 <= 10)) && (feeProtocol1 == 0 || (feeProtocol1 >= 4 && feeProtocol1 <= 10)) ); uint8 feeProtocolOld = slot0.feeProtocol; slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 4); emit SetFeeProtocol(feeProtocolOld % 16, feeProtocolOld >> 4, feeProtocol0, feeProtocol1); } ``` `slot0.feeProtocol`类型为`uint8`,保存两种代币的协议手续费比例,高4位为`token1`,低4位为`token0`: $$ slot0.feeProtocol = \overbrace{0000}^{fee1}\overbrace{0000}^{fee0} $$ 因此,`feeProtocolOld % 16`表示`token0`的协议手续费比例`fee0`,`feeProtocolOld >> 4`表示`token1`的协议手续费比例`fee1`。 #### collectProtocol 取回协议手续费,该方法仅允许工厂合约的owner执行。 协议手续费有两个来源: * `swap`产生的交易手续费 * `flash`产生的闪电贷手续费 ```solidity /// @inheritdoc IUniswapV3PoolOwnerActions function collectProtocol( address recipient, uint128 amount0Requested, uint128 amount1Requested ) external override lock onlyFactoryOwner returns (uint128 amount0, uint128 amount1) { amount0 = amount0Requested > protocolFees.token0 ? protocolFees.token0 : amount0Requested; amount1 = amount1Requested > protocolFees.token1 ? protocolFees.token1 : amount1Requested; if (amount0 > 0) { if (amount0 == protocolFees.token0) amount0--; // ensure that the slot is not cleared, for gas savings protocolFees.token0 -= amount0; TransferHelper.safeTransfer(token0, recipient, amount0); } if (amount1 > 0) { if (amount1 == protocolFees.token1) amount1--; // ensure that the slot is not cleared, for gas savings protocolFees.token1 -= amount1; TransferHelper.safeTransfer(token1, recipient, amount1); } emit CollectProtocol(msg.sender, recipient, amount0, amount1); } ``` 上述代码比较简单,此处不再展开。 ### Tick.sol Tick.sol管理Tick内部状态。 #### tickSpacingToMaxLiquidityPerTick 根据`tickSpacing`计算每个`tick`最大流动性,只有能够被`tickSpacing`整除的`tick`才能够存放流动性: ```solidity /// @notice Derives max liquidity per tick from given tick spacing /// @dev Executed within the pool constructor /// @param tickSpacing The amount of required tick separation, realized in multiples of `tickSpacing` /// e.g., a tickSpacing of 3 requires ticks to be initialized every 3rd tick i.e., ..., -6, -3, 0, 3, 6, ... /// @return The max liquidity per tick function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) { int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing; int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing; uint24 numTicks = uint24((maxTick - minTick) / tickSpacing) + 1; return type(uint128).max / numTicks; } ``` #### getFeeGrowthInside 计算两个`tick`区间内部的每流动性累积手续费,该方法实现白皮书公式6.17-6.19: $$ f_a(i) = \begin{cases} f_g - f_o(i) & \text{$i_c \geq i$}\\ f_o(i) & \text{$i_c < i$} \end{cases} \tag{6.17} $$ $$ f_b(i) = \begin{cases} f_o(i) & \text{$i_c \geq i$}\\ f_g - f_o(i) & \text{$i_c < i$}\end{cases} \tag{6.18} $$ $$ f_r = f_g - f_b(i_l) - f_a(i_u) \tag{6.19} $$ 代码如下: ```solidity /// @notice Retrieves fee growth data /// @param self The mapping containing all tick information for initialized ticks /// @param tickLower The lower tick boundary of the position /// @param tickUpper The upper tick boundary of the position /// @param tickCurrent The current tick /// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0 /// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1 /// @return feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries /// @return feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries function getFeeGrowthInside( mapping(int24 => Tick.Info) storage self, int24 tickLower, int24 tickUpper, int24 tickCurrent, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128 ) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) { Info storage lower = self[tickLower]; Info storage upper = self[tickUpper]; // calculate fee growth below uint256 feeGrowthBelow0X128; uint256 feeGrowthBelow1X128; if (tickCurrent >= tickLower) { feeGrowthBelow0X128 = lower.feeGrowthOutside0X128; feeGrowthBelow1X128 = lower.feeGrowthOutside1X128; } else { feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128; feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128; } // calculate fee growth above uint256 feeGrowthAbove0X128; uint256 feeGrowthAbove1X128; if (tickCurrent < tickUpper) { feeGrowthAbove0X128 = upper.feeGrowthOutside0X128; feeGrowthAbove1X128 = upper.feeGrowthOutside1X128; } else { feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128; feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128; } feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128; feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128; } ``` 首先根据当前`tickCurrent`,分别计算`tickLower`和`tickUpper`的$f_a$, $f_b$,最后计算出区间内手续费$f_r$: $$ \underbrace{\overbrace{..., i_l - 1}^{f_b(i_l)}, \overbrace{i_l, i_l + 1, ..., i_u - 1, i_u}^{f_r}, \overbrace{i_u + 1, ...}^{f_a(i_u)}}_{f_g} $$ 为什么需要计算区间内每流动性累计手续费呢?因为每个头寸(Position)会在`mint`/`burn`时根据该值计算自己的应收手续费: $$ liquidityDelta \cdot (feeGrowthInside - feeGrowthInsideLast) $$ #### update 更新`tick`状态,并返回该`tick`是否翻转`flipped`: ```solidity /// @notice Updates a tick and returns true if the tick was flipped from initialized to uninitialized, or vice versa /// @param self The mapping containing all tick information for initialized ticks /// @param tick The tick that will be updated /// @param tickCurrent The current tick /// @param liquidityDelta A new amount of liquidity to be added (subtracted) when tick is crossed from left to right (right to left) /// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0 /// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1 /// @param secondsPerLiquidityCumulativeX128 The all-time seconds per max(1, liquidity) of the pool /// @param tickCumulative The tick * time elapsed since the pool was first initialized /// @param time The current block timestamp cast to a uint32 /// @param upper true for updating a position's upper tick, or false for updating a position's lower tick /// @param maxLiquidity The maximum liquidity allocation for a single tick /// @return flipped Whether the tick was flipped from initialized to uninitialized, or vice versa function update( mapping(int24 => Tick.Info) storage self, int24 tick, int24 tickCurrent, int128 liquidityDelta, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128, uint160 secondsPerLiquidityCumulativeX128, int56 tickCumulative, uint32 time, bool upper, uint128 maxLiquidity ) internal returns (bool flipped) { Tick.Info storage info = self[tick]; uint128 liquidityGrossBefore = info.liquidityGross; uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta); require(liquidityGrossAfter <= maxLiquidity, 'LO'); flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0); ``` 如果`tick`从无流动性到有流动性,或者从有流动性变成无流动性,则表示`tick`需要翻转`flipped`。 ```solidity if (liquidityGrossBefore == 0) { // by convention, we assume that all growth before a tick was initialized happened _below_ the tick if (tick <= tickCurrent) { info.feeGrowthOutside0X128 = feeGrowthGlobal0X128; info.feeGrowthOutside1X128 = feeGrowthGlobal1X128; info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128; info.tickCumulativeOutside = tickCumulative; info.secondsOutside = time; } info.initialized = true; } ``` 如果`tick`之前没有流动性,则进行初始化;对于小于当前`tickCurrent`的`tick`,设置`Outside`等变量。 ```solidity info.liquidityGross = liquidityGrossAfter; ``` `liquidityGross`表示总流动性,用于判断`tick`是否需要翻转: * 如果`mint`,则增加流动性;如果`burn`,则减少流动性 * 该变量与`tick`在不同头寸中是否作为边界低点或高点无关,只与`mint`或`burn`操作有关 ```solidity // when the lower (upper) tick is crossed left to right (right to left), liquidity must be added (removed) info.liquidityNet = upper ? int256(info.liquidityNet).sub(liquidityDelta).toInt128() : int256(info.liquidityNet).add(liquidityDelta).toInt128(); } ``` `liquidityNet`表示净流动性,当`swap`穿越`tick`时,用于更新全局可用流动性`liquidity`: * 如果作为`tickLower`,即边界低点(左边界点),则增加`liquidityDelta`(`mint`时为正,`burn`时为负) * 如果作为`tickUpper`,即边界高点(右边界点),则减少`liquidityDelta`(`mint`时为正,`burn`时为负) #### clear 当`tick`翻转后,如果没有流动性关联该`tick`,即`liquidityGross = 0`,则清空`tick`状态: ```solidity /// @notice Clears tick data /// @param self The mapping containing all initialized tick information for initialized ticks /// @param tick The tick that will be cleared function clear(mapping(int24 => Tick.Info) storage self, int24 tick) internal { delete self[tick]; } ``` #### cross 当`tick`被穿越时,需要翻转`Outside`等变量的方向,如白皮书公式6.20: $$ f_o(i) := f_g - f_o(i) \tag{6.20} $$ 这些变量在[getFeeGrowthInside](#getFeeGrowthInside)等方法被用到。 ```solidity /// @notice Transitions to next tick as needed by price movement /// @param self The mapping containing all tick information for initialized ticks /// @param tick The destination tick of the transition /// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0 /// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1 /// @param secondsPerLiquidityCumulativeX128 The current seconds per liquidity /// @param tickCumulative The tick * time elapsed since the pool was first initialized /// @param time The current block.timestamp /// @return liquidityNet The amount of liquidity added (subtracted) when tick is crossed from left to right (right to left) function cross( mapping(int24 => Tick.Info) storage self, int24 tick, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128, uint160 secondsPerLiquidityCumulativeX128, int56 tickCumulative, uint32 time ) internal returns (int128 liquidityNet) { Tick.Info storage info = self[tick]; info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128; info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128; info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128 - info.secondsPerLiquidityOutsideX128; info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside; info.secondsOutside = time - info.secondsOutside; liquidityNet = info.liquidityNet; } ``` ### TickMath.sol TickMath主要包含两个方法: * [getSqrtRatioAtTick](#getSqrtRatioAtTick):根据tick计算开根号价格$\sqrt{P}$ * [getTickAtSqrtRatio](#getTickAtSqrtRatio):根据开根号价格$\sqrt{P}$计算tick #### getSqrtRatioAtTick 该方法对应白皮书公式6.2: $$ \sqrt{p}(i) = \sqrt{1.0001}^i = 1.0001^{\frac{i}{2}} $$ 其中,$i$即为`tick`。 因为Uniswap v3支持的价格($\frac{token1}{token0}$)区间为$[2^{-128}, 2^{128}]$,根据白皮书公式6.1: $$ p(i) = 1.0001^i $$ 因此,对应的最大tick(MAX_TICK)为: $$ i = \lfloor log_{1.0001}{p(i)} \rfloor = \lfloor log_{1.0001}{2^{128}} \rfloor = \lfloor 887272.7517970635 \rfloor = 887272 $$ 最小tick(MIN_TICK)为: $$ i = \lceil log_{1.0001}{2^{-128}} \rceil = \lceil -887272.7517970635 \rceil = -887272 $$ 假设$i$ $\geq 0$,对于一个给定的tick $i$,它总可以表示为二进制,因此以下式子总是成立: $$ \begin{cases} i = \sum_{n=0}^{19}{(x_n \cdot 2^n)} = x_0 \cdot 1 + x_1 \cdot 2 + x_2 \cdot 4 + ... + x_{19}\cdot 524288 \\ \forall x_n \in \{0, 1\} \end{cases} \tag{1.1} $$ 其中,$x_n$为$i$的二进制位。如$i=6$,其对应的二进制为:`000000000000000000000110`,则$x_1 = 1, x_2 = 1$,其余$x_n$均为0。 同样可以推出$i < 0$也可以用类似的公式表示。 我们先看$i < 0$的情况: 如果 $i < 0$,则: $$ \sqrt{p}(i) = 1.0001^{\frac{i}{2}} = 1.0001^{-\frac{|i|}{2}} = \frac{1}{1.0001^{\frac{|i|}{2}}} = \frac{1}{1.0001^{\frac{1}{2}(\sum_{n=0}^{19}{(x_n \cdot 2^n)})}} \\ = \frac{1}{1.0001^{\frac{1}{2} \cdot x_0}} \cdot \frac{1}{1.0001^{\frac{2}{2} \cdot x_1}} \cdot \frac{1}{1.0001^{\frac{4}{2} \cdot x_2}} \cdot ... \cdot \frac{1}{1.0001^{\frac{524288}{2} \cdot x_{19}}} $$ 根据二进制位$x_n$的值,可以总结如下: $$ \frac{1}{1.0001^{\frac{x_n \cdot 2^n}{2}}} \begin{cases} = 1 & \text{$x_n = 0, n \geq 0, i < 0$}\\ < 1 & \text{$x_n = 1, n \geq 0, i < 0$} \end{cases} $$ 为了最小化精度误差,在计算过程中,使用`Q128.128`(128位定点数)表示中间价格,对于每一个价格$p$,均需要左移128位。由于$i < 0, x_n = 1$时,$\frac{1}{1.0001^{\frac{x_n \cdot 2^n}{2}}} < 1$,因此在连续乘积过程中不会有溢出问题。 可以总结计算$\sqrt{p}(i)$的方法: * 初始值为1,从第0位开始,从低位到高位(从右往左)循环遍历$i$的二进制比特位 * 如果该位不为0,则乘以对应的$\frac{2^{128}}{1.0001^{\frac{2^n}{2}}}$,其中$2^{128}$表示左移128位 * 如果该位为0,则乘以1,可以省略 ```solidity /// @notice Calculates sqrt(1.0001^tick) * 2^96 /// @dev Throws if |tick| > max tick /// @param tick The input tick for the above formula /// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0) /// at the given tick function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); require(absTick <= uint256(MAX_TICK), 'T'); // 如果第0位非0,则ratio = 0xfffcb933bd6fad37aa2d162d1a594001 ,即:2^128 / 1.0001^0.5 uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000; // 如果第1位非0,则乘以 0xfff97272373d413259a46990580e213a ,即:2^128 / 1.0001^1,因为两个乘数均为Q128.128,最终结果多乘了2^128,因此需要右移128 if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; // 如果第2位非0,则乘以 0xfff2e50f5f656932ef12357cf3c7fdcc ,即:2^128 / 1.0001^2, if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; // 以此类推 if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; // 如果第19位非0,因为(2^19 = 0x80000=524288),则乘以 0x2216e584f5fa1ea926041bedfe98,即:2^128 / 1.0001^(524288/2) // tick的最大值为887272,因此其二进制最多只需要20位表示,从0开始计数,最后一位为第19位。 if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; if (tick > 0) ratio = type(uint256).max / ratio; // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. // we then downcast because we know the result always fits within 160 bits due to our tick input constraint // we round up in the division so getTickAtSqrtRatio of the output price is always consistent sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); } ``` 假设 $i > 0$ 时: $$ \sqrt{p_{Q128128}(i)} = 2^{128} \cdot \sqrt{p(i)} = 2^{128} \cdot 1.0001^{\frac{i}{2}} \\ = \frac{2^{128}}{1.0001^{-\frac{i}{2}}} = \frac{2^{256}}{2^{128} \cdot \sqrt{p(-i)}} = \frac{2^{256}}{\sqrt{p_{Q128128}(-i)}} $$ 因此,只需要算出 $i < 0$ 时的 ratio 值,使用$2^{256}$除以ratio即可得出 $i > 0$ 时,使用`Q128.128`表示的ratio值: ```solidity if (tick > 0) ratio = type(uint256).max / ratio; ``` 代码最后一行将ratio右移32位,转化为`Q128.96`格式的定点数: ```solidity sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); ``` 这里算的是开根号价格$\sqrt{p}$,由于价格$p$最大为$2^{128}$,因此$\sqrt{p}$最大为$2^{64}$,也就是整数部分最大只需要64位表示,因此最终的sqrtPriceX96一定可以用160位(64+96,即`Q64.96`格式的定点数)表示。 #### getTickAtSqrtRatio 该方法对应白皮书中的公式6.8: $$ i_c = \lfloor \log_{\sqrt{1.0001}} \sqrt{P} \rfloor $$ 本方法涉及在Solidity中计算对数,根据对数公式,可以推出: $$ \log_{\sqrt{1.0001}} \sqrt{P} = \frac{log_2{\sqrt{P}}}{log_2{\sqrt{1.0001}}} = log_2{\sqrt{P}} \cdot log_{\sqrt{1.0001}}{2} $$ 由于 $log_{\sqrt{1.0001}}{2}$ 是一个常数,因此我们只需要计算 $log_2{\sqrt{P}}$ 即可。 将输入的参数为$\sqrt{P}$看作$x$,问题转化为求 $log_2{x}$。 把结果分为整数部分$n$和小数部分$m$,则: $$ n \leq log_2{x} = n + m < n + 1 $$ ##### 整数部分 对于$n$,因为: $$ 2^n \leq x < 2^{n+1} $$ 可以通过二分查找找到$n$值: * 对于256位的数,$0 \leq n < 256$,可以用8位比特表示$n$ * 从二进制表示的第8位(k=7)到第1位(k=0)(从最高位到最低位),依次比较$x$是否大于$2^{2^{k}} - 1$,如果大于则标记该位为1,并右移$2^k$位;否则标记0 * 最终标记后的8位二进制即为$n$值 使用Python代码描述如下: ```python def find_msb(x): msb = 0 for k in reversed(range(8)): // k = 7, 6, 5. 4, 3, 2, 1, 0 if x > 2 ** (2 ** k) - 1: msb += 2 ** k // 标记该位为1,即加上 2 ** k x /= 2 ** (2 ** k) // 右移 2 ** k 位 return msb ``` Uniswap v3中的Solidity代码如下: ```solidity /// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio /// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may /// ever return. /// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96 /// @return tick The greatest tick for which the ratio is less than or equal to the input ratio function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) { // second inequality must be < because the price can never reach the price at the max tick require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 'R'); uint256 ratio = uint256(sqrtPriceX96) << 32; // 右移32位,转化为Q128.128格式 uint256 r = ratio; uint256 msb = 0; assembly { // 如果大于2 ** (2 ** 7) - 1,则保存临时变量:2 ** 7 let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) // msb += 2 ** 7 msb := or(msb, f) // r /= (2 ** (2 ** 7)),即右移 2 ** 7 r := shr(f, r) } assembly { let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF)) msb := or(msb, f) r := shr(f, r) } assembly { let f := shl(5, gt(r, 0xFFFFFFFF)) msb := or(msb, f) r := shr(f, r) } assembly { let f := shl(4, gt(r, 0xFFFF)) msb := or(msb, f) r := shr(f, r) } assembly { let f := shl(3, gt(r, 0xFF)) msb := or(msb, f) r := shr(f, r) } assembly { let f := shl(2, gt(r, 0xF)) msb := or(msb, f) r := shr(f, r) } assembly { let f := shl(1, gt(r, 0x3)) msb := or(msb, f) r := shr(f, r) } assembly { let f := gt(r, 0x1) msb := or(msb, f) } ``` ##### 小数部分 对于小数部分$m$: $$ 0 \leq m = log_2{x} - n = log_2{\frac{x}{2^n}} < 1 \tag{1.2} $$ 其中,$n$为上文算出的msb,即整数部分。 我们先将$\frac{x}{2^n}$看做一个整体$r$,则: $$ 0 \leq log_2{r} < 1 $$ $$ 1 \leq r = \frac{x}{2^n} < 2 $$ 这里我们希望求出$log_2{r}$,如果能够将$log_2{r}$表示成一个不断收敛的数列,当小数位足够多时,就可以近似求出$log_2{r}$的值。 根据对数公式,我们可以推导以下两个等式: $$ log_2{r} = \frac{2 \cdot log_2{r}}{2} = \frac{log_2{r^2}}{2} \tag{1.3} $$ $$ log_2{r} = log_2{2 \cdot \frac{r}{2}} = 1 + log_2{\frac{r}{2}} \tag{1.4} $$ 我们循环套用上述两个公式,可以整理以下方法: 1. 因为初始时 $log_2{r} < 1$,因此先应用公式1.3,将问题转化为求$log_2{r^2}$,注意此时基数为$\frac{1}{2}$; - 事实上,每一次进入步骤1,新的基数都是上一次基数的$\frac{1}{2}$,比如第二次进入步骤1的基数为$\frac{1}{4}$,以此类推。 2. 如果$r^2$ >= 2,则应用公式1.4,分离出1,并将问题转化为求$log_2{\frac{r^2}{2}}$; - 因为公式1.4是在公式1.3之后判断,因此这里的1需要乘以上一次步骤1的基数,如果是第一次则记录$\frac{1}{2}$,第二次则记录$\frac{1}{4}$,以此类推; - 因为$1 \leq r < 2$,且$2 \leq r^2 < 4$,因此$1 \leq \frac{r^2}{2} < 2$,将$\frac{r^2}{2}$看做一个整体$r$,又回到步骤1求解$log_2{r}$,并且$1 \leq r < 2$。 3. 如果$r^2 < 2$,则回到步骤1继续。 可以将上述步骤总结为以下公式: $$ log_2{r} = m_1 \cdot \frac{1}{2} + m_2 \cdot \frac{1}{4} + ... + m_n \cdot \frac{1}{2^n} = \sum^{\infty}_{i=1}(m_i \cdot \frac{1}{2^i}) \tag{1.5} $$ 其中,$\forall m_i \in \{0, 1\}$。 这其实就是小数的二进制表示法,小数的二进制第一位表示为$2^{-1}$,第二位为$2^{-2}$,以此类推。而在我们上述计算$log_2{r}$的步骤中,如果进入步骤2,则相当于标记该位为1;如果进入步骤3,则相当于标记该位为0。 重复以上步骤的过程,即为确认小数部分二进制位从高位到低位(从左到右)每一位的值,每一个循环确认一位。循环次数越多,计算得出的$log_2{r}$精度越高。 我们继续看Uniswap v3中计算小数部分的代码: ```solidity if (msb >= 128) r = ratio >> (msb - 127); else r = ratio << (127 - msb); ``` 这里msb即为整数部分$n$。因为ratio是`Q128.128`,如果`msb >= 128`则表示`ratio >= 1`,因此需要右移整数位数得到小数部分`ratio >> msb`;`-127`表示左移127位,使用`Q129.127`表示小数部分;同样,如果`msb < 128`,则表示`ratio < 1`,其本身就只有小数部分,因此通过左移`127 - msb`位,将小数部分凑齐127位,也用`Q129.127`表示小数部分。 实际上,`ratio >> msb`即为公式1.2中的$\frac{x}{2^n}$,也就是步骤1中的$r$,在后续迭代算法(步骤1-3)中需要用到。 ```solidity int256 log_2 = (int256(msb) - 128) << 64; ``` 因为msb是基于`Q128.128`的ratio计算的,`int256(msb) - 128`表示$n$的真正值。`<< 64`使用`Q192.64`表示$n$。 这一行代码实际上是使用`Q192.64`保存整数部分的值。 下面代码循环计算二进制表示的小数部分的前14位小数: ```solidity assembly { // 根据步骤1,计算r^2,右移127位是因为两个r都是Q129.127 r := shr(127, mul(r, r)) // 因为1 <= r^2 < 4,仅需2位表示r^2的整数, // 因此从右往左数第129和128位表示r^2的整数部分, // 右移128位,仅剩129位, // 该值为1,则表示r >= 2;该值为0,则表示r < 2 let f := shr(128, r) // 如果f == 1,则log_2 += Q192.64的1/2 log_2 := or(log_2, shl(63, f)) // 根据步骤2(即公式1.4),如果r >= 2(即f == 1),则r /= 2;否则不操作,即步骤3 r := shr(f, r) } // 重复进行上述过程 assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(62, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(61, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(60, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(59, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(58, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(57, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(56, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(55, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(54, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(53, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(52, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(51, f)) r := shr(f, r) } assembly { r := shr(127, mul(r, r)) let f := shr(128, r) log_2 := or(log_2, shl(50, f)) } ``` 上述计算的log_2即为`Q192.64`表示的$log_2{\sqrt{P}}$,精度为$2^{-14}$。 ```solidity int256 log_sqrt10001 = log_2 * 255738958999603826347141; // 128.128 number ``` 因为: $$ \log_{\sqrt{1.0001}} \sqrt{P} = \frac{log_2{\sqrt{P}}}{log_2{\sqrt{1.0001}}} = log_2{\sqrt{P}} \cdot log_{\sqrt{1.0001}}{2} $$ 这里`255738958999603826347141`即为$log_{\sqrt{1.0001}}{2} \cdot 2^{64}$,两个`Q192.64`乘以的结果为`Q128.64`(不会发生溢出)。 由于这里算出的$log_2{\sqrt{P}}$精度为$2^{-14}$,乘以`255738958999603826347141`后误差进一步放大,因此需要修正并确保结果是最接近给定价格的tick。 ```solidity int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128); int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128); tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow; ``` 其中,`3402992956809132418596140100660247210`表示`0.01000049749154292 << 128`,`291339464771989622907027621153398088495`表示`0.8561697375276566 << 128`。 参考[abdk的这篇文章](https://hackmd.io/@abdk/SkVJeHK9v),当精度为$2^{-14}$时,tick的最小误差为$− 0.85617$,最大误差为$0.0100005$。同时,这篇文章也从理论上证明了,只有当精度等于(或高于)$2^{-14}$时,只需要一次计算即可得出所需的tick值。 我们的目的是寻找满足当前条件的最大tick,使得tick对应的$\sqrt{P}$小于等于传入的值。因此如果补偿后的tickHi满足要求,则优先使用tickHi;否则使用tickLow。 以下是本节参考文章,有兴趣的朋友请扩展阅读: * [Solidity中的对数计算](https://liaoph.com/logarithm-in-solidity/) * [Math in Solidity](https://medium.com/coinmonks/math-in-solidity-part-5-exponent-and-logarithm-9aef8515136e) * [Logarithm Approximation Precision](https://hackmd.io/@abdk/SkVJeHK9v) ### TickBitmap.sol TickBitmap使用Bitmap(位图)保存Tick的初始化状态,提供以下几个方法: * [position](#position):根据tick返回位图索引数据 * [flipTick](#flipTick):翻转tick状态 * [nextInitializedTickWithinOneWord](#nextInitializedTickWithinOneWord):寻找同组内下一个初始化的tick #### position ```solidity /// @notice Computes the position in the mapping where the initialized bit for a tick lives /// @param tick The tick for which to compute the position /// @return wordPos The key in the mapping containing the word in which the bit is stored /// @return bitPos The bit position in the word where the flag is stored function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { wordPos = int16(tick >> 8); bitPos = uint8(tick % 256); } ``` 只有能被`tickSpacing`整除的`tick`才能记录在位图中,因此此处的参数:$tick = \frac{tick}{tickSpacing}$。 `tick`类型为`int24`,其二进制从右到左,从低位到高位,前8位表示`bitPos`,后16位表示`wordPos`,如下图所示: $$ \overbrace{23,...,8}^{wordPos},\overbrace{7,...,0}^{bitPos} $$ `tick`的bitmap表示为:`self[wordPos] ^= 1 << bitPos`。 #### flipTick 当`tick`翻转初始化状态时,如果其位图的值为0,则需要修改为1;否则,修改为0;即对该位“取反”。 ```solidity /// @notice Flips the initialized state for a given tick from false to true, or vice versa /// @param self The mapping in which to flip the tick /// @param tick The tick to flip /// @param tickSpacing The spacing between usable ticks function flipTick( mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing ) internal { require(tick % tickSpacing == 0); // ensure that the tick is spaced (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing); uint256 mask = 1 << bitPos; self[wordPos] ^= mask; } ``` 首先获取`tick`对应的`wordPos`和`bitPos`,由于最后针对`bitPos`执行按位“异或”操作: > 因为1与任何值b(0或1)异或等于~b;0与任何值b(0或1)异或等于b。 * 对于`tick`对应的位(bit),mask为1 - 如果旧值为1,`1^1=0`,因此`tick`状态由“初始化”变成“未初始化”; - 如果旧值为0,`0^1=1`,因此`tick`状态由“未初始化”变成“初始化” * 对于非`tick`对应的位,mask为0 - 如果旧值为1,`1^0=1`,因此状态不变 - 如果旧值为0,`0^0=0`,因此状态不变 所以,上述代码实现了`tick`位取反的效果。 #### nextInitializedTickWithinOneWord 根据参数`tick`,寻找位图上最近一个已初始化的`tick`,如未找到,返回本组最后一个未初始化的tick。 ```solidity /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either /// to the left (less than or equal to) or right (greater than) of the given tick /// @param self The mapping in which to compute the next initialized tick /// @param tick The starting tick /// @param tickSpacing The spacing between usable ticks /// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks function nextInitializedTickWithinOneWord( mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing, bool lte ) internal view returns (int24 next, bool initialized) { int24 compressed = tick / tickSpacing; if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity if (lte) { (int16 wordPos, uint8 bitPos) = position(compressed); // all the 1s at or to the right of the current bitPos uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); uint256 masked = self[wordPos] & mask; // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word initialized = masked != 0; // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick next = initialized ? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing : (compressed - int24(bitPos)) * tickSpacing; } else { // start from the word of the next tick, since the current tick state doesn't matter (int16 wordPos, uint8 bitPos) = position(compressed + 1); // all the 1s at or to the left of the bitPos uint256 mask = ~((1 << bitPos) - 1); uint256 masked = self[wordPos] & mask; // if there are no initialized ticks to the left of the current tick, return leftmost in the word initialized = masked != 0; // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick next = initialized ? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) * tickSpacing : (compressed + 1 + int24(type(uint8).max - bitPos)) * tickSpacing; } } ``` 如果`lte == true`,即寻找小于等于当前`tick`的值,因此问题转化为:寻找低比特位上是否有`1`。 ```solidity if (lte) { (int16 wordPos, uint8 bitPos) = position(compressed); // all the 1s at or to the right of the current bitPos uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); uint256 masked = self[wordPos] & mask; } ``` 其中,mask的值为所有小于等于`bitPos`的位全部置1,比如`bitPos = 7`,则`mask二进制 = 1111111`;masked保留位图中小于等于`bitPos`的位值,比如`self[wordPos] = 110101011`,则`masked = 110101011 & 1111111 = 000101011`。 如果`masked != 0`,则表示当前方向(`lte`)该`wordPos`上有初始化的`tick`;否则,则表示该方向都是未初始化的tick。 ```solidity // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word initialized = masked != 0; ``` 如果存在已初始化的`tick`,则需要定位到`masked`中最高位的1;如果不存在,则返回当前方向最后一个`tick`。 ```solidity // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick next = initialized ? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing : (compressed - int24(bitPos)) * tickSpacing; ``` `BitMath.mostSignificantBit(masked)`通过二分查找法找到`masked`最高位的1,关于该算法的具体说明,可参考本文[TickMath对数计算部分](#整数部分)。简单而言,`mostSignificantBit(masked)`会返回一个数`n`,使得: $$ 2^n \leq masked < 2^{n+1} $$ 比如,如果`masked = 000101011`,`mostSignificantBit`将返回5,因为最高位的1在(从0开始)第5位:000`1`01011。 `compressed - int24(bitPos)`表示当前`wordPos`第一个`tick`,`compressed - int24(bitPos - BitMath.mostSignificantBit(masked))`表示该最高位`bitPos`对应的`tick`;`* tickSpacing`将恢复到原始`tick`值,因为保存位图时,`tick`需要先除以`tickSpacing`。 同样,如果向大于当前`tick`方向寻找第一个已初始化的`tick`时,方法和上述类似,只是`mostSignificantBit`需要换成`leastSignificantBit`,即从`tick`比特位开始(不含),往高位寻找第一个`bitPos`位为1的`tick`。 ```solidity else { // start from the word of the next tick, since the current tick state doesn't matter (int16 wordPos, uint8 bitPos) = position(compressed + 1); // all the 1s at or to the left of the bitPos uint256 mask = ~((1 << bitPos) - 1); uint256 masked = self[wordPos] & mask; // if there are no initialized ticks to the left of the current tick, return leftmost in the word initialized = masked != 0; // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick next = initialized ? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) * tickSpacing : (compressed + 1 + int24(type(uint8).max - bitPos)) * tickSpacing; } ``` ### Position.sol `Position.sol`管理头寸相关信息,包括以下方法: * [get](#get):获取头寸对象 * [update](#update1):更新头寸 一个头寸(Position)可由“所有者”`owner`、“区间低点”`tickLower`和“区间高点”`tickUpper`唯一确定,对于同一个交易对池子,每个用户可以创建多个不同价格区间的头寸,但只能创建一个相同价格区间的头寸;不同用户可以创建相同价格区间的头寸。 #### get 根据`owner`、`tickLower`和`tickUpper`返回一个头寸对象: ```solidity /// @notice Returns the Info struct of a position, given an owner and position boundaries /// @param self The mapping containing all user positions /// @param owner The address of the position owner /// @param tickLower The lower tick boundary of the position /// @param tickUpper The upper tick boundary of the position /// @return position The position info struct of the given owners' position function get( mapping(bytes32 => Info) storage self, address owner, int24 tickLower, int24 tickUpper ) internal view returns (Position.Info storage position) { position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))]; } ``` #### update 更新头寸的流动性和可取回代币,注意,该方法只会在`mint`和`burn`时被触发,`swap`并不会更新头寸信息。 ```solidity /// @notice Credits accumulated fees to a user's position /// @param self The individual position to update /// @param liquidityDelta The change in pool liquidity as a result of the position update /// @param feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries /// @param feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries function update( Info storage self, int128 liquidityDelta, uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128 ) internal { Info memory _self = self; uint128 liquidityNext; if (liquidityDelta == 0) { require(_self.liquidity > 0, 'NP'); // disallow pokes for 0 liquidity positions liquidityNext = _self.liquidity; } else { liquidityNext = LiquidityMath.addDelta(_self.liquidity, liquidityDelta); } ``` 更新流动性: * 如果是`mint`,则`liquidityDelta > 0` * 如果是`burn`,则`liquidityDelta < 0` ```solidity // calculate accumulated fees uint128 tokensOwed0 = uint128( FullMath.mulDiv( feeGrowthInside0X128 - _self.feeGrowthInside0LastX128, _self.liquidity, FixedPoint128.Q128 ) ); uint128 tokensOwed1 = uint128( FullMath.mulDiv( feeGrowthInside1X128 - _self.feeGrowthInside1LastX128, _self.liquidity, FixedPoint128.Q128 ) ); ``` 根据头寸区间自上一次更新后的每流动性手续费增长值,分别计算`token0`和`token1`的应收手续费。 ```solidity // update the position if (liquidityDelta != 0) self.liquidity = liquidityNext; self.feeGrowthInside0LastX128 = feeGrowthInside0X128; self.feeGrowthInside1LastX128 = feeGrowthInside1X128; if (tokensOwed0 > 0 || tokensOwed1 > 0) { // overflow is acceptable, have to withdraw before you hit type(uint128).max fees self.tokensOwed0 += tokensOwed0; self.tokensOwed1 += tokensOwed1; } } ``` 更新头寸流动性、本次每流动性手续费和可取回代币数。 ### SwapMath.sol #### computeSwapStep SwapMath只有一个方法,即`computeSwapStep`,计算单步交换的输入输出。 参数如下: * `sqrtRatioCurrentX96`:当前价格 * `sqrtRatioTargetX96`:目标价格 * `liquidity`:可用流动性 * `amountRemaining`:剩余输入代币 * `feePips`:手续费 返回值: * `sqrtRatioNextX96`:交换后价格 * `amountIn`:本次交换消耗的输入代币 * `amountOut`:本次交换消耗的输出代币 * `feeAmount`:交易手续费(含协议手续费) ```solidity 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; bool exactIn = amountRemaining >= 0; ``` 我们在[swap](#swap)章节提到,对于一个交换操作,根据`zeroForOne`和`exactIn`的值,有四种组合: |zeroForOne|exactInput|swap| |---|---|---| |true|true|输入固定数量`token0`,输出最大数量`token1`| |true|false|输入最小数量`token0`,输出固定数量`token1`| |false|true|输入固定数量`token1`,输出最大数量`token0`| |false|false|输入最小数量`token1`,输出固定数量`token0`| ```solidity if (exactIn) { uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6); amountIn = zeroForOne ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true) : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true); if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = sqrtRatioTargetX96; else sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput( sqrtRatioCurrentX96, liquidity, amountRemainingLessFee, zeroForOne ); } ``` 如果是输入固定数量,则交易手续费需要从输入代币中扣除。 因为`feePips`的单位是百分之一基点,即$\frac{1}{10^6}$,因此,按照如下公式扣除手续费: $$ amountRemaining \cdot (1 - \frac{feePips}{10^6}) $$ 使用[getAmount0Delta](#getAmount0Delta)或[getAmount1Delta](#getAmount1Delta),根据当前价格、目标价格和可用流动性计算所需的输入代币数量`amountIn`: * 如果从`token0`交换`token1`,则输入代币为`token0`,因此计算`amount0` * 如果从`token1`交换`token0`,则输入代币为`token1`,因此计算`amount1` 注意,方法的最后一个参数`roundUp`,当计算`amountIn`时需要设置`roundUp = true`,也就是合约只能多收钱,而不能少收,否则合约资金将出现损失;同样,当计算`amountOut`时设置`roundUp = false`,也就是合约可以少付钱,而不能多付。 * 如果可用代币数量大于`amountIn`,则表示该步交易可以完成,因此交换后的价格等于目标价格 * 否则,根据当前价格,可用流动性和可用代币`amountRemainingLessFee`,计算交换后价格,我们在[SqrtPriceMath.getNextSqrtPriceFromInput](#getNextSqrtPriceFromInput)会具体介绍计算方法 ```solidity 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 ); } ``` 如果不是输入固定数量,也就是输出固定数量,需要将`amountRemaining`作为输出代币数量使用: * 如果从`token0`交换`token1`,则输出代币为`token1`,因此计算`amount1` * 如果从`token1`交换`token0`,则输出代币为`token0`,因此计算`amount0` 首先根据当前价格、目标价格和可用流动性,计算可产生的输出代币数量`amountOut`,注意,这里与上面相反,如果是`token0`交换`token1`,需要计算`token1`的数量,需使用`SqrtPriceMath.getAmount1Delta`方法。 当表示固定输出代币时,传入的参数`amountRemaining`是负值: * 如果`amountOut`的绝对值小于应输出代币数量,则表示可完全交换,交换后价格等于目标价格 * 否则,根据可用输出`amountRemaining`,计算交换后价格 ```solidity bool max = sqrtRatioTargetX96 == sqrtRatioNextX96; ``` 如果交换后价格等于目标价格,则表示完全交换。 ```solidity // get the input/output amounts if (zeroForOne) { 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); } ``` 计算本次交换所需输入`amountIn`和所得输出`amountOut`: * 如果从`token0`交换`token1` - 所需输入`amountIn` - 如果完全交换,并且固定输入,则所需输入即为`amountIn` - 否则(非完全交换,或者固定输出),则使用`getAmount0Delta`,根据价格区间和流动性计算所需输入 - 所得输出`amountOut` - 如果完全交换,并且固定输出,则所得输出即为`amountOut` - 否则(非完全交换,或者固定输入),则使用`getAmount1Delta`,根据价格区间和流动性计算所得输出 * 如果从`token1`交换`token1` - 所需输入`amountIn` - 如果完全交换,并且固定输入,则所需输入即为`amountIn` - 否则(非完全交换,或者固定输出),则使用`getAmount1Delta`,根据价格区间和流动性计算所需输入 - 所得输出`amountOut` - 如果完全交换,并且固定输出,则所得输出即为`amountOut` - 否则(非完全交换,或者固定输入),则使用`getAmount0Delta`,根据价格区间和流动性计算所得输出 ```solidity // cap the output amount to not exceed the remaining output amount if (!exactIn && amountOut > uint256(-amountRemaining)) { amountOut = uint256(-amountRemaining); } ``` 确认所得输出没有超过指定输出。 ```solidity if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) { // we didn't reach the target, so take the remainder of the maximum input as fee feeAmount = uint256(amountRemaining) - amountIn; } else { feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips); } ``` 计算交易手续费(包括协议手续费): * 如果固定输入,并且没有达到目标价格,相当于是最后一次交换,则原始输入代币`amountRemaining`扣除本次交换所需输入`amountIn`,即为手续费; * 否则,需要根据`amountIn`计算手续费;注意,除了上述最后一次交换外,这里的`amountIn`和`amountOut`都是不包括手续费的,因此计算手续费需要除以`1e6 - feePips`,而非`1e6`。 ### SqrtPriceMath.sol #### getNextSqrtPriceFromAmount0RoundingUp 根据当前价格、`liquidity`和$\Delta{x}$,计算目标价格。 根据白皮书公式6.16: $$ \Delta{x} = \Delta{\frac{1}{\sqrt{P}}} \cdot L $$ 假设$\sqrt{P_a} > \sqrt{P_b}$,则$x_a < x_b$: $$ \Delta{x} = x_b - x_a $$ 如果已知$\sqrt{P_a}$计算$\sqrt{P_b}$,则: $$ \frac{1}{\sqrt{P_b}} = \frac{\Delta{x}}{L} + \frac{1}{\sqrt{P_a}} = \frac{1}{L} \cdot (\Delta{x} + \frac{L}{\sqrt{P_a}}) \tag{1.1} $$ $$ \sqrt{P_b} = \frac{L}{\Delta{x} + \frac{L}{\sqrt{P_a}}} \tag{1.2} $$ $$ {\sqrt{P_b}} = \frac{L \cdot \sqrt{P_a}}{L + \Delta{x} \cdot \sqrt{P_a}} \tag{1.3} $$ 如果已知$\sqrt{P_b}$计算$\sqrt{P_a}$,则: $$ \frac{1}{\sqrt{P_a}} = \frac{1}{\sqrt{P_b}} - \frac{\Delta{x}}{L} \tag{1.4} $$ $$ {\sqrt{P_a}} = \frac{L \cdot \sqrt{P_b}}{L - \Delta{x} \cdot \sqrt{P_b}} \tag{1.5} $$ ```solidity /// @notice Gets the next sqrt price given a delta of token0 /// @dev Always rounds up, because in the exact output case (increasing price) we need to move the price at least /// far enough to get the desired output amount, and in the exact input case (decreasing price) we need to move the /// price less in order to not send too much output. /// The most precise formula for this is liquidity * sqrtPX96 / (liquidity +- amount * sqrtPX96), /// if this is impossible because of overflow, we calculate liquidity / (liquidity / sqrtPX96 +- amount). /// @param sqrtPX96 The starting price, i.e. before accounting for the token0 delta /// @param liquidity The amount of usable liquidity /// @param amount How much of token0 to add or remove from virtual reserves /// @param add Whether to add or remove the amount of token0 /// @return The price after adding or removing amount, depending on add function getNextSqrtPriceFromAmount0RoundingUp( uint160 sqrtPX96, uint128 liquidity, uint256 amount, bool add ) internal pure returns (uint160) { // we short circuit amount == 0 because the result is otherwise not guaranteed to equal the input price if (amount == 0) return sqrtPX96; uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION; if (add) { uint256 product; if ((product = amount * sqrtPX96) / amount == sqrtPX96) { uint256 denominator = numerator1 + product; if (denominator >= numerator1) // always fits in 160 bits return uint160(FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator)); } return uint160(UnsafeMath.divRoundingUp(numerator1, (numerator1 / sqrtPX96).add(amount))); } else { uint256 product; // if the product overflows, we know the denominator underflows // in addition, we must check that the denominator does not underflow require((product = amount * sqrtPX96) / amount == sqrtPX96 && numerator1 > product); uint256 denominator = numerator1 - product; return FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator).toUint160(); } } ``` * 当`add = true`时,即已知$\sqrt{P_a}$计算$\sqrt{P_b}$ - 如果$\Delta{x} \cdot \sqrt{P_a}$没有发生溢出,则使用公式1.3计算$\sqrt{P_b}$,因为这种方式精度最高 - 否则,使用公式1.2计算$\sqrt{P_b}$ * 当`add = false`时,即已知$\sqrt{P_b}$,根据上述公式1.5计算$\sqrt{P_a}$ #### getNextSqrtPriceFromAmount1RoundingDown 根据当前价格、`liquidity`和$\Delta{y}$,计算目标价格。 根据白皮书公式6.13: $$ \Delta{\sqrt{P}} = \frac{\Delta{y}}{L} \tag{6.13} $$ 假设$\sqrt{P_a} > \sqrt{P_b}$,则$y_a > y_b$: $$ \Delta{y} = y_a - y_b $$ 如果已知$\sqrt{P_b}$计算$\sqrt{P_a}$,则: $$ \sqrt{P_a} = \sqrt{P_b} + \frac{\Delta{y}}{L} \tag{1.6} $$ 如果已知$\sqrt{P_a}$计算$\sqrt{P_b}$,则: $$ \sqrt{P_b} = \sqrt{P_a} - \frac{\Delta{y}}{L} \tag{1.7} $$ ```solidity /// @notice Gets the next sqrt price given a delta of token1 /// @dev Always rounds down, because in the exact output case (decreasing price) we need to move the price at least /// far enough to get the desired output amount, and in the exact input case (increasing price) we need to move the /// price less in order to not send too much output. /// The formula we compute is within <1 wei of the lossless version: sqrtPX96 +- amount / liquidity /// @param sqrtPX96 The starting price, i.e., before accounting for the token1 delta /// @param liquidity The amount of usable liquidity /// @param amount How much of token1 to add, or remove, from virtual reserves /// @param add Whether to add, or remove, the amount of token1 /// @return The price after adding or removing `amount` function getNextSqrtPriceFromAmount1RoundingDown( uint160 sqrtPX96, uint128 liquidity, uint256 amount, bool add ) internal pure returns (uint160) { // if we're adding (subtracting), rounding down requires rounding the quotient down (up) // in both cases, avoid a mulDiv for most inputs if (add) { uint256 quotient = ( amount <= type(uint160).max ? (amount << FixedPoint96.RESOLUTION) / liquidity : FullMath.mulDiv(amount, FixedPoint96.Q96, liquidity) ); return uint256(sqrtPX96).add(quotient).toUint160(); } else { uint256 quotient = ( amount <= type(uint160).max ? UnsafeMath.divRoundingUp(amount << FixedPoint96.RESOLUTION, liquidity) : FullMath.mulDivRoundingUp(amount, FixedPoint96.Q96, liquidity) ); require(sqrtPX96 > quotient); // always fits 160 bits return uint160(sqrtPX96 - quotient); } } ``` * 当`add = true`时,即按照公式1.6,根据$\sqrt{P_b}$计算$\sqrt{P_a}$ * 当`add = false`时,按照公式1.7,根据$\sqrt{P_a}$计算$\sqrt{P_b}$ #### getNextSqrtPriceFromInput 根据输入代币计算下一个价格,即添加`amountIn`数量的代币后的价格: ```solidity /// @notice Gets the next sqrt price given an input amount of token0 or token1 /// @dev Throws if price or liquidity are 0, or if the next price is out of bounds /// @param sqrtPX96 The starting price, i.e., before accounting for the input amount /// @param liquidity The amount of usable liquidity /// @param amountIn How much of token0, or token1, is being swapped in /// @param zeroForOne Whether the amount in is token0 or token1 /// @return sqrtQX96 The price after adding the input amount to token0 or token1 function getNextSqrtPriceFromInput( uint160 sqrtPX96, uint128 liquidity, uint256 amountIn, bool zeroForOne ) internal pure returns (uint160 sqrtQX96) { require(sqrtPX96 > 0); require(liquidity > 0); // round to make sure that we don't pass the target price return zeroForOne ? getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) : getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true); } ``` #### getNextSqrtPriceFromOutput 根据输出代币计算下一个价格,即移除`amountOut`数量的代币后的价格: ```solidity /// @notice Gets the next sqrt price given an output amount of token0 or token1 /// @dev Throws if price or liquidity are 0 or the next price is out of bounds /// @param sqrtPX96 The starting price before accounting for the output amount /// @param liquidity The amount of usable liquidity /// @param amountOut How much of token0, or token1, is being swapped out /// @param zeroForOne Whether the amount out is token0 or token1 /// @return sqrtQX96 The price after removing the output amount of token0 or token1 function getNextSqrtPriceFromOutput( uint160 sqrtPX96, uint128 liquidity, uint256 amountOut, bool zeroForOne ) internal pure returns (uint160 sqrtQX96) { require(sqrtPX96 > 0); require(liquidity > 0); // round to make sure that we pass the target price return zeroForOne ? getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false) : getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false); } ``` #### getAmount0Delta 该方法计算白皮书中的公式6.16: $$ \Delta{x} = \Delta{\frac{1}{\sqrt{P}}} \cdot L $$ 展开公式为: $$ amount0 = x_b - x_a = L \cdot (\frac{1}{\sqrt{P_b}} - \frac{1}{\sqrt{P_a}}) = L \cdot (\frac{\sqrt{P_a} - \sqrt{P_b}}{\sqrt{P_a} \cdot \sqrt{P_b}}) $$ ```solidity /// @notice Gets the amount0 delta between two prices /// @dev Calculates liquidity / sqrt(lower) - liquidity / sqrt(upper), /// i.e. liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower)) /// @param sqrtRatioAX96 A sqrt price /// @param sqrtRatioBX96 Another sqrt price /// @param liquidity The amount of usable liquidity /// @param roundUp Whether to round the amount up or down /// @return amount0 Amount of token0 required to cover a position of size liquidity between the two passed prices function getAmount0Delta( uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity, bool roundUp ) internal pure returns (uint256 amount0) { if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION; uint256 numerator2 = sqrtRatioBX96 - sqrtRatioAX96; require(sqrtRatioAX96 > 0); return roundUp ? UnsafeMath.divRoundingUp( FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96), sqrtRatioAX96 ) : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96; } ``` #### getAmount1Delta 同样,该方法计算白皮书公式6.7: $$ L = \frac{\Delta{Y}}{\Delta{\sqrt{P}}} $$ 展开公式为: $$ amount1 = y_b - y_a = L \cdot \Delta{\sqrt{P}} = L \cdot (\sqrt{P_b} - \sqrt{P_a}) $$ ```solidity /// @notice Gets the amount1 delta between two prices /// @dev Calculates liquidity * (sqrt(upper) - sqrt(lower)) /// @param sqrtRatioAX96 A sqrt price /// @param sqrtRatioBX96 Another sqrt price /// @param liquidity The amount of usable liquidity /// @param roundUp Whether to round the amount up, or down /// @return amount1 Amount of token1 required to cover a position of size liquidity between the two passed prices function getAmount1Delta( uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity, bool roundUp ) internal pure returns (uint256 amount1) { if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); return roundUp ? FullMath.mulDivRoundingUp(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96) : FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96); ``` ### Oracle.sol 请参考:[Uniswap v3 Oracle 预言机](/pcnFjAMCRtSDMQ0yVVlv4w)