# 深入理解 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} $$