uniswap – V3源代码导读

uniswap V3的核心是在一定区间提供流动性。相对V2,代码复杂度增加不少。整个代码主要分为两部分:核心逻辑和辅助功能。核心逻辑又分为两部分:交易池以及Position的管理和Swap功能逻辑。交易池中的每个Position设计并实现成ERC721的Token。Swap核心逻辑在Tick以及Position的管理的基础上实现。

理解了uniswap V3的技术白皮书,看对应的源代码相对轻松。uniswap V3的逻辑复杂一些,代码写的还是比较清晰。强烈建议,先理解uniswap V3的技术白皮书,再查看源代码: [uniswap - V3技术白皮书导读](https://learnblockchain.cn/article/2357) uniswap V3的智能合约的代码链接如下: https://github.com/Uniswap/uniswap-v3-core https://github.com/Uniswap/uniswap-v3-periphery ## 1 总体框架 和V2的代码逻辑一致,整个功能分成两部分:核心功能(core)和辅助功能(periphery)。两个部分的关系如下: ![](https://img.learnblockchain.cn/2021/04/13/16182826144859.jpg) 辅助功能也分为两个部分:交易池(Position)管理和swap路由管理。NonfungiblePositionManager负责交易池的创建以及流动性的添加删除。SwapRouter是swap路由的管理。UniswapV3Factory是交易池(UniswapV3Pool)统一创建的接口。UniswapV3Pool由UniswapV3PoolDeployer统一部署。UniswapV3Pool是核心逻辑,管理了Tick和Position,实现流动性管理以及一个交易池中swap功能实现。每个Pool中的Position都做成了ERC721的Token。也就是说,每个Position都有独立的ERC721的Token ID。 ## 2 创建交易池(Pool) NonfungiblePositionManager负责交易池的创建以及流通性的添加/删除。先介绍一些全局变量的定义: ``` /// @dev IDs of pools assigned by this contract mapping(address => uint80) private _poolIds; /// @dev Pool keys by pool ID, to save on SSTOREs for position data mapping(uint80 => PoolAddress.PoolKey) private _poolIdToPoolKey; /// @dev The token ID position data mapping(uint256 => Position) private _positions; /// @dev The ID of the next token that will be minted. Skips 0 uint176 private _nextId = 1; /// @dev The ID of the next pool that is used for the first time. Skips 0 uint80 private _nextPoolId = 1; ``` 每一个Pool都有一个唯一编号,编号从1开始(_nextPoolId)。_poolIds记录所有交易池的地址和编号的对应关系。每个交易池的关键信息由PoolKey表示(定义在libraries/PoolAddress.sol): ``` struct PoolKey { address token0; address token1; uint24 fee; } ``` 每个交易池由交易池的两个Token以及收取的费用唯一标示。_poolIdToPoolKey记录交易池编号和PoolKey的对应关系。 所有交易池中的Position都归总管理,并赋予一个全局唯一的编号(_nextId),从1开始。 每个Position由创建地址以及边界唯一确定: ``` function compute( address owner, int24 tickLower, int24 tickUpper ) internal pure returns (bytes32) { return keccak256(abi.encodePacked(owner, tickLower, tickUpper)); } ``` 接着看看NonfungiblePositionManager的构造函数: ``` constructor( address _factory, address _WETH9, address _tokenDescriptor_ ) ERC721Permit('Uniswap V3 Positions NFT-V1', 'UNI-V3-POS', '1') PeripheryImmutableState(_factory, _WETH9) { _tokenDescriptor = _tokenDescriptor_; } ``` `_factory`是核心功能(core)中的`UniswapV3Factory`的地址。`_WETH9`是ETH智能合约的地址。`_tokenDescriptor` 是ERC721描述信息的接口地址。 通过createAndInitializePoolIfNecessary函数创建一个交易池: ``` function createAndInitializePoolIfNecessary( address tokenA, address tokenB, uint24 fee, uint160 sqrtPriceX96 ) external payable override returns (address pool) { ``` 逻辑比较简单,通过UniswapV3Factory查看是否已经存在对应的交易池,如果没有,创建交易池,如果有了但是还没有初始化,初始化交易池。深入查看两个函数:createPool和每个交易池的initialize函数。 * createPool 核心逻辑是调用UniswapV3PoolDeployer的deploy函数创建UniswapV3Pool智能合约并设置两个token信息,交易费用信息和tick的步长信息: `pool = deploy(address(this), token0, token1, fee, tickSpacing);` 接着查看deploy函数,创建UniswapV3Pool智能合约。注意每个交易池的地址的设置,是token0/token1/fee的编码后的结果。也就是说,每个交易池有唯一的地址,并且和PoolKey信息保持一致。通过这种方法,从PoolKey信息可以反推出交易池的地址。 ``` 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; } ``` * initialize 每个交易池的initialize函数初始化交易池的参数和状态。所有交易池的参数和状态用一个数据结构Slot0来记录: ``` struct Slot0 { // the current price uint160 sqrtPriceX96; // the current tick int24 tick; // the most-recently updated index of the observations array uint16 observationIndex; // the current maximum number of observations that are being stored uint16 observationCardinality; // the next maximum number of observations to store, triggered in observations.write uint16 observationCardinalityNext; // the current protocol fee as a percentage of the swap fee taken on withdrawal // represented as an integer denominator (1/x)% uint8 feeProtocol; // whether the pool is locked bool unlocked; } /// @inheritdoc IUniswapV3PoolState Slot0 public override slot0; ``` 注意的是,在初始化的时候,初始化了交易价格。这样可以把所有流动性的添加逻辑统一。 ## 3 添加流动性 `NonfungiblePositionManager`的mint函数实现初始的流动性的添加。`increaseLiquidity`函数实现了流动性的增加。这两个函数的逻辑基本一致,都是通过调用`addLiquidity`函数实现。mint需要额外创建ERC721的token。 `addLiquidity`实现在`LiquidityManagement.sol`: ``` struct AddLiquidityParams { address token0; address token1; uint24 fee; address recipient; int24 tickLower; int24 tickUpper; uint128 amount; uint256 amount0Max; uint256 amount1Max; } /// @notice Add liquidity to an initialized pool function addLiquidity(AddLiquidityParams memory params) internal returns ( uint256 amount0, uint256 amount1, IUniswapV3Pool pool ) ``` 先通过交易池的核心信息计算出对应创建的交易池的地址: ``` PoolAddress.PoolKey memory poolKey = PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee}); pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); ``` 流动性添加的核心逻辑由交易池的mint函数实现。mint函数又是由两个子函数实现:_modifyPosition和_updatePosition。 * **_updatePosition** 为了便于计算,流动性的状态更新是通过流动性(position)边界上的Tick的`liquidityNet`来表示: ``` function _updatePosition( address owner, int24 tickLower, int24 tickUpper, int128 liquidityDelta, int24 tick ) private returns (Position.Info storage position) { ``` `_updatePosition`主要就是更新`Poisition`对应边界的Tick信息: ``` flippedLower = ticks.update( tickLower, tick, liquidityDelta, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128, false, maxLiquidityPerTick ); flippedUpper = ticks.update( tickUpper, tick, liquidityDelta, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128, true, maxLiquidityPerTick ); ``` * **_modifyPosition** 除了更新Tick信息外,`_modifyPosition`需要计算在当前价格情况下一定流动性对应资金金额。当前的价格存在`_slot0.tick`中,所以大体的逻辑如下: ``` if (_slot0.tick ... } else if (_slot0.tick ... liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta); } else { ... } ``` 具体的计算公式可以查看技术白皮书的6.29和6.30公式。值得注意的是,在添加流动性时,如果添加的流动性包括当前的价格,当前的流动性需要更新。也就是上述代码的liquidity的更新。每个交易池中的liquidity保存了当前价格对应的流动性总和。 交易池的mint函数只是实现了当前价格下添加对应流动性的两种Token的金额的计算。代币的转账通过uniswapV3MintCallback函数实现。 ## 4 删除流动性 删除流动性的逻辑,和添加流动性的逻辑调用关系类似,调用交易池的burn函数。burn函数的核心也是调用`_modifyPosition`函数实现流动性的调整。`_modifyPosition`函数实现了正负流动性的调整。 在删除完流动性后,每个流动性对应需要取回的资金金额暂时存储在`tokensOwed0`和`tokensOwed1`变量: ``` position.tokensOwed0 += uint128(amount0) + uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 ) ); position.tokensOwed1 += uint128(amount1) + uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 ) ); ``` 如果某个流动性为0,并且所有的手续费已经收取,可以通过`NonfungiblePositionManager`的burn函数删除该流动性对应的ERC721的Token 。 ## 5 Swap流程 swap的逻辑实现在SwapRouter.sol,实现了多条路径互连swap逻辑。总共有两套函数: * exactInputSingle/exactInput * exactOutputSingle/exactOutput `exactInputSingle`和`exactOutputSingle`是单交易池的swap函数,一个是从指定swap的输入金额,换取一定的输出,一个是指定swap的输出金额,反推需要多少输入金额。 无论是`exactInputSingle`,还是`exactOutputSingle`,最终都是调用交易池的swap函数: ``` function swap( address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) external override noDelegateCall returns (int256 amount0, int256 amount1) { ``` `recipient`是发起swap的发送地址,`zeroForOne`的意思是,是否是Token0转换为Token1,`amountSpecified`是需要转换的金额,`sqrtPriceLimitX96`是价格上限。 exactInput还是exactOutput通过传入的金额正负进行区分: `bool exactInput = amountSpecified > 0;` 整个函数的主体由一个while循环组成。也就是说,swap过程分解成多个小步骤,一点点的调整当前的Tick,直到满足所有的交易量: while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) { * 计算下一个可能的Tick,并更新价格 ``` (step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord( state.tick, tickSpacing, zeroForOne ); step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext); ``` * 计算swap的Token0/Token1以及交易费用 ``` (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep( state.sqrtPriceX96, (zeroForOne ? step.sqrtPriceNextX96 sqrtPriceLimitX96) ? sqrtPriceLimitX96 : step.sqrtPriceNextX96, state.liquidity, state.amountSpecifiedRemaining, fee ); ``` 在一个价格范围内的Token0/Token1量的变化,可以通过`getAmount0Delta`/`getAmount1Delta`函数(SqrtPriceMath.sol)计算,也就是6.14/6.16的公式。 * 计算费用 ``` if (cache.feeProtocol > 0) { uint256 delta = step.feeAmount / cache.feeProtocol; step.feeAmount -= delta; state.protocolFee += uint128(delta); } if (state.liquidity > 0) state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity); ``` * 更新Tick信息 ``` int128 liquidityNet = ticks.cross( step.tickNext, (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128), (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128) ); ``` 在swap完成后,结合IUniswapV3SwapCallback接口实现Swap的两种代币转账: ``` if (zeroForOne) { if (amount1 uint256 balance0Before = balance0(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance0Before.add(uint256(amount0)) } else { if (amount0 uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance1Before.add(uint256(amount1)) } ``` 多条路径的swap(exactInput/exactOutput)是在exactInputSingle/exactOutputSingle的基本上构建而成。 ## 6 提取交易费 NonfungiblePositionManager提供了collect函数提取手续费。每个Position中记录在流动性不变的情况下的一定时间内的费用增长率(feeGrowthInside)。在每个Position更新流动性时会更新一次增长率。如果不更新流动性,在提取交易费时,先调用交易池的burn函数更新一下增长率,并主动计算出可以收取的手续费: `pool.burn(position.tickLower, position.tickUpper, 0);` 再调用交易池的collect函数,完成交易费的收取。 `(amount0, amount1) = pool.collect(recipient, position.tickLower, position.tickUpper, amount0Max, amount1Max);` ## **总结**: uniswap V3的核心是在一定区间提供流动性。相对V2,代码复杂度增加不少。整个代码主要分为两部分:核心逻辑和辅助功能。核心逻辑又分为两部分:交易池以及Position的管理和Swap功能逻辑。交易池中的每个Position设计并实现成ERC721的Token。Swap核心逻辑在Tick以及Position的管理的基础上实现。 ![](https://img.learnblockchain.cn/2020/08/13/15973018578548.jpg!/scale/20)

理解了uniswap V3的技术白皮书,看对应的源代码相对轻松。uniswap V3的逻辑复杂一些,代码写的还是比较清晰。强烈建议,先理解uniswap V3的技术白皮书,再查看源代码:

uniswap - V3技术白皮书导读

uniswap V3的智能合约的代码链接如下:

https://github.com/Uniswap/uniswap-v3-core

https://github.com/Uniswap/uniswap-v3-periphery

1 总体框架

和V2的代码逻辑一致,整个功能分成两部分:核心功能(core)和辅助功能(periphery)。两个部分的关系如下:

辅助功能也分为两个部分:交易池(Position)管理和swap路由管理。NonfungiblePositionManager负责交易池的创建以及流动性的添加删除。SwapRouter是swap路由的管理。UniswapV3Factory是交易池(UniswapV3Pool)统一创建的接口。UniswapV3Pool由UniswapV3PoolDeployer统一部署。UniswapV3Pool是核心逻辑,管理了Tick和Position,实现流动性管理以及一个交易池中swap功能实现。每个Pool中的Position都做成了ERC721的Token。也就是说,每个Position都有独立的ERC721的Token ID。

2 创建交易池(Pool)

NonfungiblePositionManager负责交易池的创建以及流通性的添加/删除。先介绍一些全局变量的定义:

/// @dev IDs of pools assigned by this contract
mapping(address => uint80) private _poolIds;

/// @dev Pool keys by pool ID, to save on SSTOREs for position data
mapping(uint80 => PoolAddress.PoolKey) private _poolIdToPoolKey;

/// @dev The token ID position data
mapping(uint256 => Position) private _positions;

/// @dev The ID of the next token that will be minted. Skips 0
uint176 private _nextId = 1;

/// @dev The ID of the next pool that is used for the first time. Skips 0
uint80 private _nextPoolId = 1;

每一个Pool都有一个唯一编号,编号从1开始(_nextPoolId)。_poolIds记录所有交易池的地址和编号的对应关系。每个交易池的关键信息由PoolKey表示(定义在libraries/PoolAddress.sol):

struct PoolKey {
     address token0;
     address token1;
     uint24 fee;
}

每个交易池由交易池的两个Token以及收取的费用唯一标示。_poolIdToPoolKey记录交易池编号和PoolKey的对应关系。

所有交易池中的Position都归总管理,并赋予一个全局唯一的编号(_nextId),从1开始。 每个Position由创建地址以及边界唯一确定:

function compute(
     address owner,
     int24 tickLower,
     int24 tickUpper
) internal pure returns (bytes32) {
     return keccak256(abi.encodePacked(owner, tickLower, tickUpper));
}

接着看看NonfungiblePositionManager的构造函数:

constructor(
     address _factory,
     address _WETH9,
     address _tokenDescriptor_
 ) ERC721Permit('Uniswap V3 Positions NFT-V1', 'UNI-V3-POS', '1') PeripheryImmutableState(_factory, _WETH9) {
     _tokenDescriptor = _tokenDescriptor_;
 }

_factory是核心功能(core)中的UniswapV3Factory的地址。_WETH9是ETH智能合约的地址。_tokenDescriptor 是ERC721描述信息的接口地址。

通过createAndInitializePoolIfNecessary函数创建一个交易池:

function createAndInitializePoolIfNecessary(
     address tokenA,
     address tokenB,
     uint24 fee,
     uint160 sqrtPriceX96
 ) external payable override returns (address pool) {

逻辑比较简单,通过UniswapV3Factory查看是否已经存在对应的交易池,如果没有,创建交易池,如果有了但是还没有初始化,初始化交易池。深入查看两个函数:createPool和每个交易池的initialize函数。

  • createPool

    核心逻辑是调用UniswapV3PoolDeployer的deploy函数创建UniswapV3Pool智能合约并设置两个token信息,交易费用信息和tick的步长信息:

pool = deploy(address(this), token0, token1, fee, tickSpacing);

接着查看deploy函数,创建UniswapV3Pool智能合约。注意每个交易池的地址的设置,是token0/token1/fee的编码后的结果。也就是说,每个交易池有唯一的地址,并且和PoolKey信息保持一致。通过这种方法,从PoolKey信息可以反推出交易池的地址。

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;
 }
  • initialize

每个交易池的initialize函数初始化交易池的参数和状态。所有交易池的参数和状态用一个数据结构Slot0来记录:

struct Slot0 {
 // the current price
 uint160 sqrtPriceX96;
 // the current tick
 int24 tick;
 // the most-recently updated index of the observations array
 uint16 observationIndex;
 // the current maximum number of observations that are being stored
 uint16 observationCardinality;
 // the next maximum number of observations to store, triggered in observations.write
 uint16 observationCardinalityNext;
 // the current protocol fee as a percentage of the swap fee taken on withdrawal
 // represented as an integer denominator (1/x)%
 uint8 feeProtocol;
 // whether the pool is locked
 bool unlocked;
 }
 /// @inheritdoc IUniswapV3PoolState
 Slot0 public override slot0;

注意的是,在初始化的时候,初始化了交易价格。这样可以把所有流动性的添加逻辑统一。

3 添加流动性

NonfungiblePositionManager的mint函数实现初始的流动性的添加。increaseLiquidity函数实现了流动性的增加。这两个函数的逻辑基本一致,都是通过调用addLiquidity函数实现。mint需要额外创建ERC721的token。

addLiquidity实现在LiquidityManagement.sol

struct AddLiquidityParams {
 address token0;
 address token1;
 uint24 fee;
 address recipient;
 int24 tickLower;
 int24 tickUpper;
 uint128 amount;
 uint256 amount0Max;
 uint256 amount1Max;
 }

 /// @notice Add liquidity to an initialized pool
 function addLiquidity(AddLiquidityParams memory params)
 internal
 returns (
 uint256 amount0,
 uint256 amount1,
 IUniswapV3Pool pool
 )

先通过交易池的核心信息计算出对应创建的交易池的地址:

PoolAddress.PoolKey memory poolKey =
 PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});

pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

流动性添加的核心逻辑由交易池的mint函数实现。mint函数又是由两个子函数实现:_modifyPosition和_updatePosition。

  • _updatePosition

    为了便于计算,流动性的状态更新是通过流动性(position)边界上的Tick的liquidityNet来表示:

function _updatePosition( 
 address owner,
 int24 tickLower,
 int24 tickUpper,
 int128 liquidityDelta,
 int24 tick
 ) private returns (Position.Info storage position) {

_updatePosition主要就是更新Poisition对应边界的Tick信息:

flippedLower = ticks.update(
 tickLower,
 tick,
 liquidityDelta,
 _feeGrowthGlobal0X128,
 _feeGrowthGlobal1X128, 
 false,
 maxLiquidityPerTick
 );
 flippedUpper = ticks.update(
 tickUpper,
 tick, 
 liquidityDelta,
 _feeGrowthGlobal0X128,
 _feeGrowthGlobal1X128,
 true, 
 maxLiquidityPerTick 
 );
  • _modifyPosition

除了更新Tick信息外,_modifyPosition需要计算在当前价格情况下一定流动性对应资金金额。当前的价格存在_slot0.tick中,所以大体的逻辑如下:

if (_slot0.tick 
...
} else if (_slot0.tick 
...
liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
} else {
...
}

具体的计算公式可以查看技术白皮书的6.29和6.30公式。值得注意的是,在添加流动性时,如果添加的流动性包括当前的价格,当前的流动性需要更新。也就是上述代码的liquidity的更新。每个交易池中的liquidity保存了当前价格对应的流动性总和。

交易池的mint函数只是实现了当前价格下添加对应流动性的两种Token的金额的计算。代币的转账通过uniswapV3MintCallback函数实现。

4 删除流动性

删除流动性的逻辑,和添加流动性的逻辑调用关系类似,调用交易池的burn函数。burn函数的核心也是调用_modifyPosition函数实现流动性的调整。_modifyPosition函数实现了正负流动性的调整。

在删除完流动性后,每个流动性对应需要取回的资金金额暂时存储在tokensOwed0tokensOwed1变量:

position.tokensOwed0 +=
 uint128(amount0) +
 uint128(
 FullMath.mulDiv(
 feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
 position.liquidity,
 FixedPoint128.Q128
 )
 );
 position.tokensOwed1 +=
 uint128(amount1) +
 uint128(
 FullMath.mulDiv(
 feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
 position.liquidity,
 FixedPoint128.Q128
 )
 );

如果某个流动性为0,并且所有的手续费已经收取,可以通过NonfungiblePositionManager的burn函数删除该流动性对应的ERC721的Token 。

5 Swap流程

swap的逻辑实现在SwapRouter.sol,实现了多条路径互连swap逻辑。总共有两套函数:

  • exactInputSingle/exactInput
  • exactOutputSingle/exactOutput

exactInputSingleexactOutputSingle是单交易池的swap函数,一个是从指定swap的输入金额,换取一定的输出,一个是指定swap的输出金额,反推需要多少输入金额。

无论是exactInputSingle,还是exactOutputSingle,最终都是调用交易池的swap函数:

function swap(
 address recipient,
 bool zeroForOne,
 int256 amountSpecified,
 uint160 sqrtPriceLimitX96,
 bytes calldata data
 ) external override noDelegateCall returns (int256 amount0, int256 amount1) {

recipient是发起swap的发送地址,zeroForOne的意思是,是否是Token0转换为Token1,amountSpecified是需要转换的金额,sqrtPriceLimitX96是价格上限。

exactInput还是exactOutput通过传入的金额正负进行区分:

bool exactInput = amountSpecified > 0;

整个函数的主体由一个while循环组成。也就是说,swap过程分解成多个小步骤,一点点的调整当前的Tick,直到满足所有的交易量:

while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {

  • 计算下一个可能的Tick,并更新价格
(step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
     state.tick,
     tickSpacing,
     zeroForOne
    );
    step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
  • 计算swap的Token0/Token1以及交易费用
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
     state.sqrtPriceX96,
     (zeroForOne ? step.sqrtPriceNextX96  sqrtPriceLimitX96)
     ? sqrtPriceLimitX96
     : step.sqrtPriceNextX96,
     state.liquidity,
     state.amountSpecifiedRemaining,
     fee
    );

在一个价格范围内的Token0/Token1量的变化,可以通过getAmount0Delta/getAmount1Delta函数(SqrtPriceMath.sol)计算,也就是6.14/6.16的公式。

  • 计算费用
if (cache.feeProtocol > 0) {
     uint256 delta = step.feeAmount / cache.feeProtocol;
     step.feeAmount -= delta;
     state.protocolFee += uint128(delta);
    }

    if (state.liquidity > 0)
     state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);
  • 更新Tick信息
int128 liquidityNet =
     ticks.cross(
     step.tickNext,
     (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
     (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128)
    );

在swap完成后,结合IUniswapV3SwapCallback接口实现Swap的两种代币转账:

if (zeroForOne) {
 if (amount1 

 uint256 balance0Before = balance0();
 IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
 require(balance0Before.add(uint256(amount0)) 
 } else {
 if (amount0 

 uint256 balance1Before = balance1();
 IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
 require(balance1Before.add(uint256(amount1)) 
 }

多条路径的swap(exactInput/exactOutput)是在exactInputSingle/exactOutputSingle的基本上构建而成。

6 提取交易费

NonfungiblePositionManager提供了collect函数提取手续费。每个Position中记录在流动性不变的情况下的一定时间内的费用增长率(feeGrowthInside)。在每个Position更新流动性时会更新一次增长率。如果不更新流动性,在提取交易费时,先调用交易池的burn函数更新一下增长率,并主动计算出可以收取的手续费:

pool.burn(position.tickLower, position.tickUpper, 0);

再调用交易池的collect函数,完成交易费的收取。

(amount0, amount1) = pool.collect(recipient, position.tickLower, position.tickUpper, amount0Max, amount1Max);

总结

uniswap V3的核心是在一定区间提供流动性。相对V2,代码复杂度增加不少。整个代码主要分为两部分:核心逻辑和辅助功能。核心逻辑又分为两部分:交易池以及Position的管理和Swap功能逻辑。交易池中的每个Position设计并实现成ERC721的Token。Swap核心逻辑在Tick以及Position的管理的基础上实现。

区块链技术网。

  • 发表于 2021-04-13 11:10
  • 阅读 ( 1842 )
  • 学分 ( 45 )
  • 分类:Uniswap

评论