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
      • Invitee
    • 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
    • Engagement control
    • 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 Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control 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
Invitee
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
4
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# 深入理解 Uniswap v3 合约代码 (二) ###### tags: `uniswap` `solidity` `logarithm` `uniswap-v3` `tick` `periphery` `contract` ## Uniswap-v3-periphery [Uniswap-v3-core](https://hackmd.io/TDPPCAIgRRqVDPwsSm6Kfw)合约定义的是基础方法,而Uniswap-v3-periphery合约才是我们平常直接交互的合约。 比如,众所周知Uniswap v3头寸是一个NFT,这个NFT就是在periphery合约中创建和管理的,在core合约中并没有任何NFT的概念。 ### NonfungiblePositionManager.sol 头寸管理合约,全局仅有一个,负责管理所有交易对的头寸,主要包括以下几个方法: * [createAndInitializePoolIfNecessary](#createAndInitializePoolIfNecessary):创建并初始化合约 * [mint](#mint):创建头寸 * [increaseLiquidity](#increaseLiquidity):添加流动性 * [decreaseLiquidity](#decreaseLiquidity):减少流动性 * [burn](#burn):销毁头寸 * [collect](#collect):取回代币 需要特别注意,该合约继承了`ERC721`,可以mint NFT。因为每个Uniswap v3的头寸(由`owner`、`tickLower`和`tickUpper`确定)是唯一的,因此非常适合用NFT表示。 #### createAndInitializePoolIfNecessary 我们在[Uniswap-v3-core](https://hackmd.io/TDPPCAIgRRqVDPwsSm6Kfw)中提到,一个交易对合约被创建后,需要初始化才能使用。 本方法就把这一系列操作合并成一个方法:创建并初始化交易对。 ```solidity /// @inheritdoc IPoolInitializer function createAndInitializePoolIfNecessary( address token0, address token1, uint24 fee, uint160 sqrtPriceX96 ) external payable override returns (address pool) { require(token0 < token1); pool = IUniswapV3Factory(factory).getPool(token0, token1, fee); if (pool == address(0)) { pool = IUniswapV3Factory(factory).createPool(token0, token1, fee); IUniswapV3Pool(pool).initialize(sqrtPriceX96); } else { (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0(); if (sqrtPriceX96Existing == 0) { IUniswapV3Pool(pool).initialize(sqrtPriceX96); } } } ``` 首先根据交易对代币(`token`和`token1`)和手续费`fee`获取`pool`对象: * 如果不存在,则调用Uniswap-v3-core工厂合约`createPool`创建该交易对并初始化 * 如果已存在,则根据额`slot0`判断是否已经初始化(价格),如果没有则调用Uniswap-v3-core的`initialize`方法进行初始化。 #### mint 创建新头寸,方法接受的参数如下: * `token0`:代币0 * `token1`:代币1 * `fee`:手续费等级(需符合工厂合约中定义的手续费等级) * `tickLower`:价格区间低点 * `tickUpper`:价格区间高点 * `amount0Desired`:希望存入的代币0数量 * `amount1Desired`:希望存入的代币1数量 * `amount0Min`:最少存入的`token0`数量(防止被frontrun) * `amount1Min`:最少存入的`token1`数量(防止被frontrun) * `recipient`:头寸接收者 * `deadline`:截止时间(超过该时间后请求无效)(防止重放攻击) 返回: * `tokenId`:每个头寸会分配一个唯一的`tokenId`,代表NFT * `liquidity`:头寸的流动性 * `amount0`:`token0`的数量 * `amount1`:`token1`的数量 ```solidity /// @inheritdoc INonfungiblePositionManager function mint(MintParams calldata params) external payable override checkDeadline(params.deadline) returns ( uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1 ) { IUniswapV3Pool pool; (liquidity, amount0, amount1, pool) = addLiquidity( AddLiquidityParams({ token0: params.token0, token1: params.token1, fee: params.fee, recipient: address(this), tickLower: params.tickLower, tickUpper: params.tickUpper, amount0Desired: params.amount0Desired, amount1Desired: params.amount1Desired, amount0Min: params.amount0Min, amount1Min: params.amount1Min }) ); ``` 首先通过[addLiquidity](#addLiquidity)方法完成流动性添加,获得实际得到的流动性`liquidity`,消耗的`amount0`、`amount1`,以及交易对`pool`。 ```solidity _mint(params.recipient, (tokenId = _nextId++)); ``` 通过`ERC721`合约的`_mint`方法,向接收者`recipient`铸造NFT,`tokenId`从1开始递增。 ```solidity bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); // idempotent set uint80 poolId = cachePoolKey( address(pool), PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee}) ); _positions[tokenId] = Position({ nonce: 0, operator: address(0), poolId: poolId, tickLower: params.tickLower, tickUpper: params.tickUpper, liquidity: liquidity, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128, tokensOwed0: 0, tokensOwed1: 0 }); emit IncreaseLiquidity(tokenId, liquidity, amount0, amount1); } ``` 最后,保存头寸信息到`_positions`中。 #### increaseLiquidity 为一个头寸添加流动性。需注意,可以修改头寸的代币数量,但是不能修改价格区间。 参数如下: * `tokenId`:创建头寸时返回的`tokenId`,即NFT的`tokenId` * `amount0Desired`:希望添加的`token0`数量 * `amount1Desired`:希望添加的`token1`数量 * `amount0Min`:最少添加的`token0`数量(防止被frontrun) * `amount1Min`:最少添加的`token1`数量(防止被frontrun) * `deadline`:截止时间(超过该时间后请求无效)(防止重放攻击) ```solidity /// @inheritdoc INonfungiblePositionManager function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable override checkDeadline(params.deadline) returns ( uint128 liquidity, uint256 amount0, uint256 amount1 ) { Position storage position = _positions[params.tokenId]; PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; IUniswapV3Pool pool; (liquidity, amount0, amount1, pool) = addLiquidity( AddLiquidityParams({ token0: poolKey.token0, token1: poolKey.token1, fee: poolKey.fee, tickLower: position.tickLower, tickUpper: position.tickUpper, amount0Desired: params.amount0Desired, amount1Desired: params.amount1Desired, amount0Min: params.amount0Min, amount1Min: params.amount1Min, recipient: address(this) }) ); ``` 首先根据`tokenId`获取头寸信息;与[mint](#mint)方法一样,这里调用[addLiquidity](#addLiquidity)添加流动性,返回添加成功的流动性`liquidity`,所消耗的`amount0`和`amount1`,以及交易对合约`pool`。 ```solidity bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper); // this is now updated to the current transaction (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); position.tokensOwed0 += uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 ) ); position.tokensOwed1 += uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 ) ); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; position.liquidity += liquidity; emit IncreaseLiquidity(params.tokenId, liquidity, amount0, amount1); } ``` 根据`pool`对象里的最新头寸信息,更新本合约的头寸状态,比如`token0`和`token1`的可取回代币数`tokensOwed0`和`tokensOwed1`,以及头寸当前流动性等。 > `tokensOwed0`, `tokensOwed1`, `feeGrowthInside0LastX128`,`feeGrowthInside1LastX128`和`liquidity`这几个数据在`pool.positions`方法都能够获取到,不知道这里为什么要再算一次。 #### decreaseLiquidity 移除流动性,可以移除部分或者所有流动性,移除后的代币将以待取回代币形式记录,需要再次调用[collect](#collect)方法取回代币。 参数如下: * `tokenId`:创建头寸时返回的`tokenId`,即NFT的`tokenId` * `liquidity`:希望移除的流动性数量 * `amount0Min`:最少移除的`token0`数量(防止被frontrun) * `amount1Min`:最少移除的`token1`数量(防止被frontrun) * `deadline`:截止时间(超过该时间请求无效)(防止重放攻击) ```solidity /// @inheritdoc INonfungiblePositionManager function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable override isAuthorizedForToken(params.tokenId) checkDeadline(params.deadline) returns (uint256 amount0, uint256 amount1) { ``` 注意,这里使用`isAuthorizedForToken` modifer: ```solidity modifier isAuthorizedForToken(uint256 tokenId) { require(_isApprovedOrOwner(msg.sender, tokenId), 'Not approved'); _; } ``` 确认当前用户具备操作该`tokenId`的权限,否则禁止移除。 ```solidity require(params.liquidity > 0); Position storage position = _positions[params.tokenId]; uint128 positionLiquidity = position.liquidity; require(positionLiquidity >= params.liquidity); ``` 确认头寸流动性大于等于待移除流动性。 ```solidity PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity); require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'); ``` 调用[Uniswap-v3-core](https://hackmd.io/TDPPCAIgRRqVDPwsSm6Kfw)的[burn](https://hackmd.io/TDPPCAIgRRqVDPwsSm6Kfw#burn)方法销毁流动性,返回该流动性对应的`token0`和`token1`的代币数量`amount0`和`amount1`,确认其符合`amount0Min`和`amount1Min`的限制。 ```solidity bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper); // this is now updated to the current transaction (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); position.tokensOwed0 += uint128(amount0) + uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, positionLiquidity, FixedPoint128.Q128 ) ); position.tokensOwed1 += uint128(amount1) + uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, positionLiquidity, FixedPoint128.Q128 ) ); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; // subtraction is safe because we checked positionLiquidity is gte params.liquidity position.liquidity = positionLiquidity - params.liquidity; emit DecreaseLiquidity(params.tokenId, params.liquidity, amount0, amount1); } ``` 与[increaseLiquidity](#increaseLiquidity)相同,此处计算头寸的待取回代币等信息。 #### burn 销毁头寸NFT。仅当该头寸的流动性为0,并且待取回代币数量都是0时,才能销毁NFT。 同样,调用该方法需要验证当前用户拥有`tokenId`的权限。 ```solidity /// @inheritdoc INonfungiblePositionManager function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) { Position storage position = _positions[tokenId]; require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, 'Not cleared'); delete _positions[tokenId]; _burn(tokenId); } ``` #### collect 取回待领取代币。 参数如下: * `tokenId`:创建头寸时返回的`tokenId`,即NFT的`tokenId` * `recipient`:代币接收者 * `amount0Max`:最多领取的`token0`代币数量 * `amount1Max`:最多领取的`token1`代币数量 ```solidity /// @inheritdoc INonfungiblePositionManager function collect(CollectParams calldata params) external payable override isAuthorizedForToken(params.tokenId) returns (uint256 amount0, uint256 amount1) { require(params.amount0Max > 0 || params.amount1Max > 0); // allow collecting to the nft position manager address with address 0 address recipient = params.recipient == address(0) ? address(this) : params.recipient; Position storage position = _positions[params.tokenId]; PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); (uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1); ``` 获取待取回代币数量。 ```solidity // trigger an update of the position fees owed and fee growth snapshots if it has any liquidity if (position.liquidity > 0) { pool.burn(position.tickLower, position.tickUpper, 0); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(PositionKey.compute(address(this), position.tickLower, position.tickUpper)); tokensOwed0 += uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 ) ); tokensOwed1 += uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 ) ); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; } ``` 如果该头寸含有流动性,则触发一次头寸状态的更新,这里使用`burn` 0流动性来触发。这是因为Uniswap-v3-core只在`mint`和`burn`时才更新头寸状态,而`collect`方法可能在`swap`之后被调用,可能会导致头寸状态不是最新的。 ```solidity // compute the arguments to give to the pool#collect method (uint128 amount0Collect, uint128 amount1Collect) = ( params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max, params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max ); // the actual amounts collected are returned (amount0, amount1) = pool.collect( recipient, position.tickLower, position.tickUpper, amount0Collect, amount1Collect ); // sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected // instead of the actual amount so we can burn the token (position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect); emit Collect(params.tokenId, recipient, amount0Collect, amount1Collect); } ``` 调用Uniswap-v3-core的`collect`方法取回代币,并更新头寸的待取回代币数量。 ### SwapRouter.sol 交换代币,包括以下几个方法: * [exactInputSingle](#exactInputSingle):单步交换,指定输入代币数量,尽可能多地获得输出代币 * [exactInput](#exactInput):多步交换,指定输入代币数量,尽可能多地获得输出代币 * [exactOutputSingle](#exactOutputSingle):单步交换,指定输出代币数量,尽可能少地提供输入代币 * [exactOutput](#exactOutput):多步交换,指定输出代币数量,尽可能少地提供输入代币 另外,该合约也实现了: * [uniswapV3SwapCallback](#uniswapV3SwapCallback):交换回调方法 * [exactInputInternal](#exactInputInternal):单步交换,内部方法,指定输入代币数量,尽可能多地获得输出代币 * [exactOutputInternal](#exactOutputInternal):单步交换,内部方法,指定输出代币数量,尽可能少地提供输入代币 #### exactInputSingle 单步交换,指定输入代币数量,尽可能多地获得输出代币。 参数如下: * `tokenIn`:输入代币地址 * `tokenOut`:输出代币地址 * `fee`:手续费等级 * `recipient`:输出代币接收者 * `deadline`:截止时间,超过该时间请求无效 * `amountIn`:输入的代币数量 * `amountOutMinimum`:最少收到的输出代币数量 * `sqrtPriceLimitX96`:(最高或最低)限制价格 返回: * `amountOut`:输出代币数量 ```solidity /// @inheritdoc ISwapRouter function exactInputSingle(ExactInputSingleParams calldata params) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { amountOut = exactInputInternal( params.amountIn, params.recipient, params.sqrtPriceLimitX96, SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender}) ); require(amountOut >= params.amountOutMinimum, 'Too little received'); } ``` 该方法实际上调用[exactInputInternal](#exactInputInternal),最后确认输出代币数量`amountOut`符合最小输出代币要求`amountOutMinimum`。 注意,`SwapCallbackData`中的`path`按照[Path.sol](#Pathsol)中定义的格式编码。 #### exactInput 多步交换,指定输入代币数量,尽可能多地获得输出代币。 参数如下: * `path`:交换路径,格式请参考:[Path.sol](#Pathsol) * `recipient`:输出代币收款人 * `deadline`:交易截止时间 * `amountIn`:输入代币数量 * `amountOutMinimum`:最少输出代币数量 返回: * `amountOut`:输出代币 ```solidity /// @inheritdoc ISwapRouter function exactInput(ExactInputParams memory params) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { address payer = msg.sender; // msg.sender pays for the first hop while (true) { bool hasMultiplePools = params.path.hasMultiplePools(); // the outputs of prior swaps become the inputs to subsequent ones params.amountIn = exactInputInternal( params.amountIn, hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies 0, SwapCallbackData({ path: params.path.getFirstPool(), // only the first pool in the path is necessary payer: payer }) ); // decide whether to continue or terminate if (hasMultiplePools) { payer = address(this); // at this point, the caller has paid params.path = params.path.skipToken(); } else { amountOut = params.amountIn; break; } } require(amountOut >= params.amountOutMinimum, 'Too little received'); } ``` 在多步交换中,需要按照交换路径,拆成多个单步交换,循环进行,直到路径结束。 如果是第一步交换,则`payer`为合约调用方,否则,`payer`为当前`SwapRouter`合约。 在循环中首先根据[hasMultiplePools](#hasMultiplePools)判断路径`path`中是否剩余2个及以上的池子。如果有,则中间交换步骤的收款地址设置为当前`SwapRouter`合约,否则设置为入口参数`recipient`。 每一步交换后,将当前交换路径`path`的前20+3个字节删除,即弹出(pop)最前面的token+fee信息,进入下一次交换,并将每一步交换的输出作为下一次交换的输入。 每一步交换调用[exactInputInternal](#exactInputInternal)进行。 多步交换后,确认最后的`amountOut`满足最小输出代币要求`amountOutMinimum`。 #### exactOutputSingle 单步交换,指定输出代币数量,尽可能少地提供输入代币。 参数如下: * `tokenIn`:输入代币地址 * `tokenOut`:输出代币地址 * `fee`:手续费等级 * `recipient`:输出代币收款人 * `deadline`:请求截止时间 * `amountOut`:输出代币数量 * `amountInMaximum`:最大输入代币数量 * `sqrtPriceLimitX96`:最大或最小代币价格 返回: * `amountIn`:实际输入代币数量 ```solidity /// @inheritdoc ISwapRouter function exactOutputSingle(ExactOutputSingleParams calldata params) external payable override checkDeadline(params.deadline) returns (uint256 amountIn) { // avoid an SLOAD by using the swap return data amountIn = exactOutputInternal( params.amountOut, params.recipient, params.sqrtPriceLimitX96, SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender}) ); require(amountIn <= params.amountInMaximum, 'Too much requested'); // has to be reset even though we don't use it in the single hop case amountInCached = DEFAULT_AMOUNT_IN_CACHED; } ``` 调用[exactOutputInternal](#exactOutputInternal)完成单步交换,并确认实际输入代币数量`amountIn`小于等于最大输入代币数量`amountInMaximum`。 #### exactOutput 多步交换,指定输出代币数量,尽可能少地提供输入代币。 参数如下: * `path`:交换路径,格式请参考:[Path.sol](#Path) * `recipient`:输出代币收款人 * `deadline`:请求截止时间 * `amountOut`:指定输出代币数量 * `amountInMaximum`:最大输入代币数量 ```solidity /// @inheritdoc ISwapRouter function exactOutput(ExactOutputParams calldata params) external payable override checkDeadline(params.deadline) returns (uint256 amountIn) { // it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output // swap, which happens first, and subsequent swaps are paid for within nested callback frames exactOutputInternal( params.amountOut, params.recipient, 0, SwapCallbackData({path: params.path, payer: msg.sender}) ); amountIn = amountInCached; require(amountIn <= params.amountInMaximum, 'Too much requested'); amountInCached = DEFAULT_AMOUNT_IN_CACHED; } ``` 调用[exactOutputInternal](#exactOutputInternal)完成交换,注意,该方法会在回调方法中继续完成下一步交换,因此不需要像[exactInput](#exactInput)使用循环交易。 最后确认实际输入代币数量`amountIn`小于等于最大输入代币数量`amountInMaximum`。 #### exactInputInternal 单步交换,内部方法,指定输入代币数量,尽可能多地获得输出代币。 ```solidity /// @dev Performs a single exact input swap function exactInputInternal( uint256 amountIn, address recipient, uint160 sqrtPriceLimitX96, SwapCallbackData memory data ) private returns (uint256 amountOut) { // allow swapping to the router address with address 0 if (recipient == address(0)) recipient = address(this); ``` 如果没有指定`recipient`,则默认为当前`SwapRouter`合约地址。这是因为在多步交换时,需要将中间代币保存在当前`SwapRouter`合约。 ```solidity (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); ``` 根据[decodeFirstPool](#decodeFirstPool)解析`path`中第一个池子的信息。 ```solidity bool zeroForOne = tokenIn < tokenOut; ``` 因为Uniswap v3池子`token0`地址小于`token1`,根据两个代币地址判断当前是否由`token0`交换到`token1`。注意,`tokenIn`可以是`token0`或`token1`。 ```solidity (int256 amount0, int256 amount1) = getPool(tokenIn, tokenOut, fee).swap( recipient, zeroForOne, amountIn.toInt256(), sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : sqrtPriceLimitX96, abi.encode(data) ); ``` 调用[swap](#swap)方法,获得完成本次交换所需的`amount0`和`amount1`。如果是从`token0`交换`token1`,则`amount1`是负数;反之,`amount0`是负数。 如果没有指定`sqrtPriceLimitX96`,则默认为最低或最高价格,因为在多步交换中,无法指定每一步的价格。 ```solidity return uint256(-(zeroForOne ? amount1 : amount0)); ``` 返回`amountOut`。 #### exactOutputInternal 单步交换,内部方法,指定输出代币数量,尽可能少地提供输入代币。 ```solidity /// @dev Performs a single exact output swap function exactOutputInternal( uint256 amountOut, address recipient, uint160 sqrtPriceLimitX96, SwapCallbackData memory data ) private returns (uint256 amountIn) { // allow swapping to the router address with address 0 if (recipient == address(0)) recipient = address(this); (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool(); bool zeroForOne = tokenIn < tokenOut; ``` 这部分代码与[exactInputInternal](#exactInputInternal)类似。 ```solidity (int256 amount0Delta, int256 amount1Delta) = getPool(tokenIn, tokenOut, fee).swap( recipient, zeroForOne, -amountOut.toInt256(), sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : sqrtPriceLimitX96, abi.encode(data) ); ``` 调用[Uniswap-v3-core](https://hackmd.io/TDPPCAIgRRqVDPwsSm6Kfw)的`swap`方法完成单步交换,注意,因为是指定输出代币数量,此处需要使用`-amountOut.toInt256()`。 返回的`amount0Delta`和`amount1Delta`为完成本次交换所需的`token0`数量和实际输出的`token1`数量。 ```solidity uint256 amountOutReceived; (amountIn, amountOutReceived) = zeroForOne ? (uint256(amount0Delta), uint256(-amount1Delta)) : (uint256(amount1Delta), uint256(-amount0Delta)); // it's technically possible to not receive the full output amount, // so if no price limit has been specified, require this possibility away if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut); } ``` #### uniswapV3SwapCallback swap的回调方法,实现`IUniswapV3SwapCallback.uniswapV3SwapCallback`接口。 参数如下: * `amount0Delta`:本次交换产生的`amount0`(对应代币为`token0`);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币 * `amount1Delta`:本次交换产生的`amount1`(对应代币为`token1`);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币 * `_data`:回调参数,这里为`SwapCallbackData`类型 ```solidity /// @inheritdoc IUniswapV3SwapCallback function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes calldata _data ) external override { require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData)); (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); ``` 解析回调参数`_data`,根据[decodeFirstPool](#decodeFirstPool)获得交易路径上的第一个交易对信息。 ```solidity (bool isExactInput, uint256 amountToPay) = amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta)) : (tokenOut < tokenIn, uint256(amount1Delta)); ``` 根据不同输入,有以下几种交易组合: |场景|说明|amount0Delta > 0|amount1Delta > 0|tokenIn < tokenOut|isExactInput|amountToPay| |---|---|---|---|---|---|---| |1|输入指定数量`token0`,输出尽可能多`token1`|true|false|true|true|amount0Delta| |2|输入尽可能少`token0`,输出指定数量`token1`|true|false|true|false|amount0Delta| |3|输入指定数量`token1`,输出尽可能多`token0`|false|true|false|true|amount1Delta| |4|输入尽可能少`token1`,输出指定数量`token0`|false|true|false|false|amount1Delta| ```solidity if (isExactInput) { pay(tokenIn, data.payer, msg.sender, amountToPay); } else { // either initiate the next swap or pay if (data.path.hasMultiplePools()) { data.path = data.path.skipToken(); exactOutputInternal(amountToPay, msg.sender, 0, data); } else { amountInCached = amountToPay; tokenIn = tokenOut; // swap in/out because exact output swaps are reversed pay(tokenIn, data.payer, msg.sender, amountToPay); } } ``` * 如果`isExactInput`,即指定输入代币的场景,上表中的场景1和场景3,则直接向`SwapRouter`合约转账`amount0Delta`(场景1)或`amount1Delta`(场景3)(都是正数)。 * 如果是指定输出代币的场景 - 如果是多步交换,则移除前23的字符(pop最前面的token+fee),将需要的输入作为下一步的输出,进入下一步交换 - 如果是单步交换(或最后一步),则`tokenIn`与`tokenOut`交换,并向`SwapRouter`合约转账 ### LiquidityManagement.sol #### uniswapV3MintCallback 添加流动性的回调方法。 参数如下: * `amount0Owed`:应转账的`token0`数量 * `amount1Owed`:应转账的`token1`数量 * `data`:在`mint`方法中传入的回调参数 ```solidity /// @inheritdoc IUniswapV3MintCallback function uniswapV3MintCallback( uint256 amount0Owed, uint256 amount1Owed, bytes calldata data ) external override { MintCallbackData memory decoded = abi.decode(data, (MintCallbackData)); CallbackValidation.verifyCallback(factory, decoded.poolKey); if (amount0Owed > 0) pay(decoded.poolKey.token0, decoded.payer, msg.sender, amount0Owed); if (amount1Owed > 0) pay(decoded.poolKey.token1, decoded.payer, msg.sender, amount1Owed); } ``` 首先反向解析回调参数`MintCallbackData`,并确认该方法是被指定的交易对合约调用,因为该方法是一个`external`方法,可以被外部调用,因此需要确认调用方。 最后,向调用方转入指定的代币数量。 #### addLiquidity 给已初始化的交易对(池子)添加流动性。 ```solidity /// @notice Add liquidity to an initialized pool function addLiquidity(AddLiquidityParams memory params) internal returns ( uint128 liquidity, 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)); ``` 根据`factory`、`token0`、`token1`和`fee`获取交易对`pool`。 ```solidity // compute the liquidity amount { (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, params.amount0Desired, params.amount1Desired ); } ``` 从`slot0`获取当前价格`sqrtPriceX96`,根据`tickLower`和`tickUpper`计算区间的最低价格`sqrtRatioAX96`和最高价格`sqrtRatioBX96`。 根据[getLiquidityForAmounts](#getLiquidityForAmounts)计算能够获得的最大流动性。 ```solidity (amount0, amount1) = pool.mint( params.recipient, params.tickLower, params.tickUpper, liquidity, abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender})) ); ``` 使用[Uniswap-v3-core](https://hackmd.io/TDPPCAIgRRqVDPwsSm6Kfw)的`mint`方法添加流动性,并返回实际消耗的`amount0`和`amount1`。 我们在Uniswap-v3-core的`mint`方法中提到,调用方需实现[uniswapV3MintCallback](#LiquidityManagement.uniswapV3MintCallback)接口。这里传入`MintCallbackData`作为回调参数,在`uniswapV3MintCallback`方法中可以反向解析出来,以便获取交易对和用户信息。 ```solidity require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'); ``` 最后,确认实际消耗的`amount0`和`amount1`满足`amount0Min`和`amount1Min`的最低要求。 ### LiquidityAmounts.sol #### getLiquidityForAmount0 根据`amount0`和价格区间计算流动性。 根据Uniswap-v3-core的`getAmount0Delta`中的公式: $$ 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}}) $$ 可得: $$ L = amount0 \cdot (\frac{\sqrt{P_a} \cdot \sqrt{P_b}}{\sqrt{P_a} - \sqrt{P_b}}) $$ ```solidity /// @notice Computes the amount of liquidity received for a given amount of token0 and price range /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)) /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary /// @param amount0 The amount0 being sent in /// @return liquidity The amount of returned liquidity function getLiquidityForAmount0( uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount0 ) internal pure returns (uint128 liquidity) { if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96); return toUint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96)); } ``` #### getLiquidityForAmount1 根据`amount1`和价格区间计算流动性。 根据Uniswap-v3-core的`getAmount1Delta`公式: $$ amount1 = y_b - y_a = L \cdot \Delta{\sqrt{P}} = L \cdot (\sqrt{P_b} - \sqrt{P_a}) $$ 可得: $$ L = \frac{amount1}{\sqrt{P_b} - \sqrt{P_a}} $$ ```solidity /// @notice Computes the amount of liquidity received for a given amount of token1 and price range /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary /// @param amount1 The amount1 being sent in /// @return liquidity The amount of returned liquidity function getLiquidityForAmount1( uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount1 ) internal pure returns (uint128 liquidity) { if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); return toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96)); } ``` #### getLiquidityForAmounts 根据当前价格,计算能够返回的最大流动性。 * 因为当$\sqrt{P}$增大时,需要消耗$x$,因此如果当前价格低于价格区间低点时,需要完全根据$x$即`amount0`计算流动性 * 反之,如果当前价格高于价格区间高点,需要根据$y$即`amount1`计算流动性 如下图所示: $$ p,...,\overbrace{p_a,...,p_b}^{amount0} $$ $$ \overbrace{p_a,...}^{amount1},p,\overbrace{...,p_b}^{amount0} $$ $$ \overbrace{p_a,...,p_b}^{amount1},...,p $$ 其中,$p$表示当前价格,$p_a$表示区间低点,$p_b$表示区间高点。 ```solidity /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current /// pool prices and the prices at the tick boundaries /// @param sqrtRatioX96 A sqrt price representing the current pool prices /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary /// @param amount0 The amount of token0 being sent in /// @param amount1 The amount of token1 being sent in /// @return liquidity The maximum amount of liquidity received function getLiquidityForAmounts( uint160 sqrtRatioX96, uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount0, uint256 amount1 ) internal pure returns (uint128 liquidity) { if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); if (sqrtRatioX96 <= sqrtRatioAX96) { liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0); } else if (sqrtRatioX96 < sqrtRatioBX96) { uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0); uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1); liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; } else { liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1); } } ``` ### Path.sol 在Uniswap v3 [SwapRouter](#SwapRoutersol)中,交易路径被编码为一个`bytes`类型字符串,其格式为: $$ \overbrace{token_0}^{20}\overbrace{fee_0}^{3}\overbrace{token_1}^{20}\overbrace{fee_1}^{3}\overbrace{token_2}^{20}... $$ 其中,$token_n$的长度为20个字节(bytes),$fee_n$的长度为3个字节,上述路径表示:从`token0`交换到`token1`,使用手续费等级为`fee0`的池子(`token0`、`token1`、`fee0`),继续交换到`token2`,使用手续费等级为`fee1`的池子(`token1`、`token2`、`fee1`)。 交易路径`path`示例如下: $$ 0x\overbrace{ca90cf0734d6ccf5ef52e9ec0a515921a67d6013}^{token0,20bytes}\overbrace{0001f4}^{fee,3bytes}\overbrace{68b3465833fb72a70ecdf485e0e4c7bd8665fc45}^{token1,20bytes} $$ #### hasMultiplePools 判断交易路径是否经过多个池子(2个及以上)。 ```solidity /// @notice Returns true iff the path contains two or more pools /// @param path The encoded swap path /// @return True if path contains two or more pools, otherwise false function hasMultiplePools(bytes memory path) internal pure returns (bool) { return path.length >= MULTIPLE_POOLS_MIN_LENGTH; } ``` 我们从上述路径编码可知,如果经过2个池子,至少包含3个代币,则路径长度至少需要$20+3+20+3+20=66$个字节。代码中`MULTIPLE_POOLS_MIN_LENGTH`即等于66。 #### numPools 计算路径中的池子数量。 算法为: $$ num = \frac{length - 20}{20 + 3} $$ ```solidity /// @notice Returns the number of pools in the path /// @param path The encoded swap path /// @return The number of pools in the path function numPools(bytes memory path) internal pure returns (uint256) { // Ignore the first token address. From then on every fee and token offset indicates a pool. return ((path.length - ADDR_SIZE) / NEXT_OFFSET); } ``` #### decodeFirstPool 解析第一个path的信息,包括`token0`,`token1`和`fee`。 分别返回字符串中0-19子串(`token0`,转`address`类型),20-22子串(`fee`,转`uint24`类型),和23-42子串(`token1`,转`address`类型)。请参考`BytesLib.sol`的[toAddress](#toAddress)和[toUint24](#toUint24)方法。 ```solidity /// @notice Decodes the first pool in path /// @param path The bytes encoded swap path /// @return tokenA The first token of the given pool /// @return tokenB The second token of the given pool /// @return fee The fee level of the pool function decodeFirstPool(bytes memory path) internal pure returns ( address tokenA, address tokenB, uint24 fee ) { tokenA = path.toAddress(0); fee = path.toUint24(ADDR_SIZE); tokenB = path.toAddress(NEXT_OFFSET); } ``` #### getFirstPool 返回第一个池子的路径,即返回前43(即20+3+20)个字符组成的子字符串。 ```solidity /// @notice Gets the segment corresponding to the first pool in the path /// @param path The bytes encoded swap path /// @return The segment containing all data necessary to target the first pool in the path function getFirstPool(bytes memory path) internal pure returns (bytes memory) { return path.slice(0, POP_OFFSET); } ``` #### skipToken 跳过当前路径上的第一个`token+fee`,即跳过前20+3个字符。 ```solidity /// @notice Skips a token + fee element from the buffer and returns the remainder /// @param path The swap path /// @return The remaining token + fee elements in the path function skipToken(bytes memory path) internal pure returns (bytes memory) { return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); } ``` ### BytesLib.sol #### toAddress 从字符串的指定序号起,读取一个地址(20个字符): ```solidity function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { require(_start + 20 >= _start, 'toAddress_overflow'); require(_bytes.length >= _start + 20, 'toAddress_outOfBounds'); address tempAddress; assembly { tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) } return tempAddress; } ``` 因为变量`_bytes`类型为`bytes`,根据[ABI定义](https://docs.soliditylang.org/en/develop/abi-spec.html),`bytes`的第一个32字节存储字符串的长度(length),因此需要先跳过前面32字节,即`add(_bytes, 0x20)`;`add(add(_bytes, 0x20), _start)`表示定位到字符串指定序号`_start`;`mload`读取从该序号起的32个字节,因为`address`类型只有20字节,因此需要`div 0x1000000000000000000000000`,即右移12字节。 假设`_strat = 0`,`_bytes`的分布如下图所示: $$ 0x\overbrace{0000000...2b}^{length,32bytes}\underbrace{\overbrace{ca90cf0734d6ccf5ef52e9ec0a515921a67d6013}^{address, 20 bytes}\overbrace{0001f468b3465833fb72a70e}^{div,12 bytes}}_{mload, 32bytes}cdf485e0e4c7bd8665fc45 $$ #### toUint24 从字符串的指定序号起,读取一个`uint24`(24位,即3个字符): ```solidity function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { require(_start + 3 >= _start, 'toUint24_overflow'); require(_bytes.length >= _start + 3, 'toUint24_outOfBounds'); uint24 tempUint; assembly { tempUint := mload(add(add(_bytes, 0x3), _start)) } return tempUint; } ``` 因为`_bytes`前32个字符表示字符串长度;`mload`读取32字节,可以确保从`_start`开始的3个字节在读取出来的32字节的最低位,赋值给类型为`uint24`的变量将只保留最低位的3个字节。 假设`_strat = 0`,`_bytes`的分布如下图所示: $$ 0x\overbrace{000000}^{0x3+\_start}\underbrace{0...2b\overbrace{ca90cf0734d6ccf5ef52e9ec0a515921a67d6013}^{address1,20bytes}\overbrace{0001f4}^{fee,3bytes}}_{mload,32bytes}\overbrace{68b3465833fb72a70ecdf485e0e4c7bd8665fc45}^{address2,20bytes} $$ ### OracleLibrary.sol 根据白皮书公式5.3-5.5,计算$t_1$至$t_2$时间内的几何平均价格如下: $$ \log_{1.0001}(P_{t_1,t_2}) = \frac{\sum^{t_2}_{i=t_1} \log_{1.0001}(P_i)}{t_2 - t_1} \tag{5.3} $$ $$ \log_{1.0001}(P_{t_1,t_2}) = \frac{a_{t_2} - a_{t_1}}{t_2 - t_1} \tag{5.4} $$ $$ P_{t_1,t_2} = 1.0001^{\frac{a_{t_2} - a_{t_1}}{t_2 - t_1}} \tag{5.5} $$ 本合约提供价格预言机相关方法,包括如下方法: * [consult](#consult):查询从一段时间前到现在的几何平均价格(以`tick`形式) * [getQuoteAtTick](#getQuoteAtTick):根据`tick`计算代币价格 #### consult 查询从一段时间前到现在的几何平均价格(以`tick`形式)。 参数如下: * `pool`: 交易对池子地址 * `period`:以秒计数的区间 返回: * `timeWeightedAverageTick`:时间加权平均价格 ```solidity /// @notice Fetches time-weighted average tick using Uniswap V3 oracle /// @param pool Address of Uniswap V3 pool that we want to observe /// @param period Number of seconds in the past to start calculating time-weighted average /// @return timeWeightedAverageTick The time-weighted average tick from (block.timestamp - period) to block.timestamp function consult(address pool, uint32 period) internal view returns (int24 timeWeightedAverageTick) { require(period != 0, 'BP'); uint32[] memory secondAgos = new uint32[](2); secondAgos[0] = period; secondAgos[1] = 0; ``` 构造两个监测点,第一个为`period`时间之前,第二个为现在。 ```solidity (int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondAgos); int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0]; ``` 根据`IUniswapV3Pool.observe`方法获取累积`tick`,即公式5.4中的$a_{t_2}$和$a_{t_1}$。 $$ tickCumulativesDelta = a_{t_2} - a_{t_1} $$ ```solidity timeWeightedAverageTick = int24(tickCumulativesDelta / period); ``` $$ timeWeightedAverageTick = \frac{tickCumulativesDelta}{t_2 - t_1} $$ ```solidity // Always round to negative infinity if (tickCumulativesDelta < 0 && (tickCumulativesDelta % period != 0)) timeWeightedAverageTick--; ``` 如果`tickCumulativesDelta`为负数,并且无法被`period`整除,则将平均价格-1。 #### getQuoteAtTick ```solidity /// @notice Given a tick and a token amount, calculates the amount of token received in exchange /// @param tick Tick value used to calculate the quote /// @param baseAmount Amount of token to be converted /// @param baseToken Address of an ERC20 token contract used as the baseAmount denomination /// @param quoteToken Address of an ERC20 token contract used as the quoteAmount denomination /// @return quoteAmount Amount of quoteToken received for baseAmount of baseToken function getQuoteAtTick( int24 tick, uint128 baseAmount, address baseToken, address quoteToken ) internal pure returns (uint256 quoteAmount) { uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself if (sqrtRatioX96 <= type(uint128).max) { uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; quoteAmount = baseToken < quoteToken ? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192) : FullMath.mulDiv(1 << 192, baseAmount, ratioX192); } else { uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); quoteAmount = baseToken < quoteToken ? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128) : FullMath.mulDiv(1 << 128, baseAmount, ratioX128); } } ``` 根据Uniswap-v3-core的`getSqrtRatioAtTick`方法计算`tick`对应的$\sqrt{P}$,即$\sqrt{\frac{token1}{token0}}$。 如果`baseToken < quoteToken`,则`baseToken`为`token0`,`quoteToken`为`token1`: $$ quoteAmount = baseAmount \cdot (\sqrt{P})^2 $$ 反之,`baseToken`为`token1`,`quoteToken`为`token0`: $$ quoteAmount = \frac{baseAmount}{(\sqrt{P})^2} $$

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