9

uniswap - V3源代码导读

 3 years ago
source link: https://learnblockchain.cn/article/2371
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

理解了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的管理的基础上实现。

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 16小时前
  • 阅读 ( 28 )
  • 学分 ( 0 )
  • 分类:Uniswap

Recommend

  • 68

    10.8导读 - 让央行和华尔街闻风丧胆?盘点全球最牛“金融黑帮” 作者: 陈沁 2017-10-07 16:28 字数 6,190 阅读需 16分钟

  • 83

    Java并发思考该篇作为本文集的导读目录,将现有9篇关于Java并发的文章的核心内容整理并列出。这9篇文章大体上将Java并发的体系简述了一遍,对我个人是一次知识上的梳理,对大家也希望是一种阅读上的一种帮助。祝各位阅读愉快。 1.《多线程安全性:每个人都在谈,但...

  • 106
    • 微信 mp.weixin.qq.com 6 years ago
    • Cache

    FFmpeg代码导读——基础篇

  • 62

  • 56
    • lambeta.com 5 years ago
    • Cache

    架构整洁之道导读(一)

    我是《架构整洁之道》( Clean Architecture ) 中文版的技术审校者,在审校的过程当中略有感悟,所以希望通过撰写导读的方式分享给大家。 书名的由来 《架构整洁之道》是 Clean Architecture 的...

  • 8
    • learnblockchain.cn 3 years ago
    • Cache

    L2 - zkSync源代码导读

    zkSync通过zk Rollup协议,实现了L2的转账。zkSync项目非常完整,是学习L2非常好的参考项目。zkSync采用Plonk零知识证明算法向L1证明状态的正确性。Plonk算法是Universal的零知识证明算法,只需要一次可信设置。zkSync电路设计采用Chunk设...

  • 7
    • learnblockchain.cn 3 years ago
    • Cache

    uniswap - V3技术白皮书导读

    uniswap - V3技术白皮书导读 | 登链社区 | 深入浅出区块链技术uniswap - V3技术白皮书导读 uniswap V3...

  • 6
    • learnblockchain.cn 3 years ago
    • Cache

    mina - 源代码导读(基础篇)

    mina - 源代码导读(基础篇) | 登链社区 | 深入浅出区块链技术 Mina链上的区块数据永远“压缩”在22k。Mina是一种新的区块链形式,解决了一般链式区块链数据爆炸的问题。Mina采用PoS的共识机制,“最长/最重”链确认为主链。为了压缩...

  • 5

    什么是 Uniswap? 史上最全新手导读 达瓴智库 原创 2021-12-17 02:13 热度 469872 分享 微信扫一扫:分享 ...

  • 4

    理解Halo2,可以从两部分着手:1/ 电路构建 2/ 证明系统。从开发者的角度看,电路构建是接口。如何通过Halo2构建建电路,这些电路在Halo2的内部如何表示是理解电路构建的关键。本文就从源代码的角度深入浅出讲解Halo2的电路构建。 在深入理解Halo2电路...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK