adshao
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    8
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 深入理解 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)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully