Uniswap V3:Swap

该文章已生成可运行项目,

在这里插入图片描述
作者:WongSSH

引言

本系列文章将带领读者从零实现 Uniswap V3 核心功能,深入解析其设计与实现。 主要参考了 Constructor | Uniswap V3 Core Contract Explained 系列教程、 Uniswap V3 Development BookPaco 博客中的相关内容。所有示例代码可在 clamm 代码库中找到,以便实践和探索。

​Swap

swap 环节是 Uniswap 最复杂的环节之一,在本节内,我们将 swap 分为多个环节依次介绍。在 Uniswap 的白皮书内,给出了以下流程图:
在这里插入图片描述
当用户的代币输入后,会在当前区间进行 swap 操作,然后检查是否完成了用户所有资金的兑换,如果没有,则寻找下一个价格区间继续进行兑换;如果已完成,则进行最终的代币转移。

computeSwapStep

我们先分析 computeSwapStep 函数,该函数的作用是在当前价格区间内进行代币的互换,该函数定义如下:


function computeSwapStep(
    uint160 sqrtRatioCurrentX96,
    uint160 sqrtRatioTargetX96,
    uint128 liquidity,
    int256 amountRemaining,
    uint24 feePips
) internal pure returns (uint160 sqrtRatioNextX96, uint256 amountIn, uint256 amountOut, uint256 feeAmount) {
    
}

此函数的参数为:

  1. sqrtRatioCurrentX96 当前的池子内的价格
  2. sqrtRatioCurrentX96 当前的价格区间的下一个价格,我们现在可以认为该参数用于锁定价格区间,保证 computeSwapStep 只在某一区间内进行 swap 操作
  3. liquidity 流动性,当前可用的流动性数量
  4. amountRemaining 需要兑换的代币数量
  5. feePips 手续费。Uniswap V3 使用 1e6 的精度保存手续费,即 1e6 = 100%,而手续费的最小精度为 0.01%。所以 feePips = 1 相当于 0.01% 的手续费。

返回值内的 sqrtRatioCurrentX96 代表当前 swap 结束后的价格,假如用户可以在当前区间完成所有兑换,则该价格就是兑换后价格;假如用户在当前区间无法完成所有代币兑换,则该价格会变成当前流动性区间的最大价格。而 amountInamountOut 则代表兑换的结果输出,在后文,我们会介绍为什么会有两个兑换结果的输出。而 feeAmount 则是当前兑换所需要的手续费。

由于 swap 内,大量涉及代币的顺序问题,在此处,我们可以认为以下几种说法是一致的:

  1. token0 / x x x 资产
  2. token1 / y y y 资产

computeSwapStep 中的第一步是先确定兑换的方向,即是使用 token0 兑换 token1 还是使用 token1 兑换 token0。我们可以利用 computeSwapStep 输入的 sqrtRatioCurrentX96sqrtRatioTargetX96 参数确定

我们可以推导出当 sqrtRatioCurrentX96 >= sqrtRatioTargetX96 时,应该为 token 0 -> token1 ,即:

bool zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96;

Uniswap V3 中,进行代币兑换有两种模式,一种是给定输入,要求将所有输入代币转化为输出代币,比如我们给定 1000 USDT 输入,要求 Uniswap V3 给定足够数量的 ETH 输出。另一种模式是给定输出,要求 Uniswap V3 基于我们的输出计算我的输入代币,比如给定我们需要兑换 1 ETH,要求 Uniswap V3 计算所吸引的 USDT 的数量。在实现上,给定输入还是给定输出取决于 amountRemaining 的正负情况。

amountRemaining >= 0 时, amountRemaining 等于输入代币的数量。

bool exactIn = amountRemaining >= 0;

接下来,我们可以分情况计算两种不同的模式。我们首先计算给定输入的情况,即 exactIn = true 的情况。我们首先需要知道当前区间所能接受的最大代币输入。即给定价格区间和流动性,计算当前流动性对应的代币数量。在上文介绍 _modifyPosition 时,我们已经介绍了 getAmount0DeltagetAmount1Delta 函数,这些函数刚好可以用来计算指定流动性下可以接受的最大代币数量。代码实现如下:

amountIn = zeroForOne
    ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true)
    : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true);

此处,需要注意 getAmount0Delta 要求第一个价格参数小于第二个价格参数,在此处,我们可以根据 zeroForOne 判断价格参数的大小。而且,注意 getAmount0Delta 计算是存在误差的,我们此处将 roundUp 设置为 true 使得用户承担误差。

接下来,我们要根据 amountIn 计算 swap 结束后的价格。此处也分为两种情况,第一种是在当前区间兑换没有全部完成,此时 swap 结束的价格就是当前价格区间的最大价格。我们可以根据 amountRemaining - fee >= amountIn 来判断。等同于我们支付了过多的输入代币,当前价格区间无法容纳。注意,我们在此处增加了手续费的计算。我们可以认为手续费是在用户的资金进入系统后立马扣除了,手续费部分不参与曲线上的计算。我们首先计算 amountRemaining - fee 的结果:

uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6);

然后,我们可以使用以下代码表示 amountRemainingLessFee >= amountIn 的情况,如下:

if (amountRemainingLessFee >= amountIn) {
    sqrtRatioNextX96 = sqrtRatioTargetX96;
} else {
    
}

另一种情况时,在当前区间,用户所有的输入都被耗尽,此时我们需要计算价格。在此处,我们需要使用 amountRemainingLessFee 作为参数,因为手续费是一个 AMM 曲线外逻辑。代码如下:

if (amountRemainingLessFee >= amountIn) {
    sqrtRatioNextX96 = sqrtRatioTargetX96;
} else {
    sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(
        sqrtRatioCurrentX96, liquidity, amountRemainingLessFee, zeroForOne
    );
}

此处我们直接调用了 getNextSqrtPriceFromInput 函数。该函数的具体计算原理是较为简单,我们可以使用上文给出的:

L = Δ x 1 P A − 1 P B L = \frac{\Delta_x}{\frac{1}{\sqrt{P_A}} - \frac{1}{\sqrt{P_B}}} L=PA 1PB 1Δx

本文章已经生成可运行项目
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值