Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅析 Uniswap V3 #50

Open
DavidCai1111 opened this issue Dec 21, 2021 · 0 comments
Open

浅析 Uniswap V3 #50

DavidCai1111 opened this issue Dec 21, 2021 · 0 comments
Labels

Comments

@DavidCai1111
Copy link
Owner

DavidCai1111 commented Dec 21, 2021

参考

V2 的实现与问题

Uniswap V2 作为老牌的基于自动化做市的 DEX ,其核心公式十分简单优雅,即是:

x * y = k

x * y = k

我们假设 x 轴为 x 代币的数量,y 轴为 y 代币的数量。所以当我们使用 x 代币去交换 y 代币时,流动性池中 x,y 代币的数量将会从 A 点移至 B 点。由于双曲线的特性,所以所有做市商所注入的流动性,做市区间都为 (0, ∞)。正由于所有的流动性都被摊在这个一整个广阔的开区间内,所以就导致了一个问题,即每次交换时,实际流动性的资金利用率较低,最后提取收益时,也被摊薄得厉害。

x * y = k (2)

如上图例子所示,在 x,y 货币数量通过交换从 A 点移动至 B 点的过程中,区间内可用的流动性为黑色区域。而实际利用的流动性,仅有红色区域。

V3 的实现

所以为了解决上述问题,在 Uniswap V3 中,开始允许用户在提供流动性时,可以自定义该流动性所支持的价格区间,仅当交易价格处于指定的交易区间内时,提供的头寸(position)才会被激活。

但同时,又为了维持公式的一致性,所以 V3 中提出了“虚拟流动性”(x_virtual, y_virtual)的概念,即是:

(x + x_virtual) * (y + y_virtual) = L^2

注:由于后面的相关交易公式推导涉及到了开根号,所以为了方便计算,V3 使用了 L^2 来替代 k ,实质上两者是一样的(L^2 = k)。

(x + x_virtual) * (y + y_virtual) = L^2

如上图所示,当用户的头寸被激活进行交换时,V3 会注入虚拟流动性来保持公式的计算一致性,使曲线从橙色曲线拉抬成了青色曲线。但是, x_virtual, y_virtual 并不会参与真实的交易。

如此一来,理想状态下,由于每个用户对币价的预期不同,大家都会选择自认为流动性较大的区间做市,从而自高了头寸的资金利用率,获得更多的手续费收益。但是,在得到收益的机会变大的同时,其实风险也增加了。举个例子,某用户在一个 USDC/山寨币 交换池里,往 1 USDC 换 150 - 200 枚个山寨币的流动性区间,即(150, 200)中注入了流动性头寸进行做市。若此时,与用户期望的正好相反,USDC 对山寨币升值,变成 1 USDC 换 220 枚山寨币,根据曲线,此时价格点就会离开用户的流动性区间,这时,用户提供的头寸池内构成,就会全变成了山寨币。而相比 V2 ,由于流动性区间是 (0, ∞) ,所以用户的头寸池中仍会有 USDC,相当于所有人均摊了风险。

所以,V3 的改变,相当于是给用户提供了一种高风险高回报的收益模式。当然,如果用户对市场趋势判断的信心不足,愿意降低收益的同时降低风险,也可以将提供头寸的流动性区间手动设置为 (0, ∞) (V3 的前端 UI 中也是支持这么做的),如此一来,风险收益模型就和 V2 没有区别了。

V2 的交换公式

为了更方便的理解 V3 交换公式的推导,我们先来推导下更直观的 V2 公式。

交换的公式与其实现代码,其实是在解决如下题目:在一个流动性池中,有 X,Y 两种代币,已知 X 代币的数量为 x ,Y 代币的数量为 y ,现有用户提供了 Δx 个 X 代币,求能交换出多少 Y 代币(Δy)?

我们根据核心公式:

x * y = (x + Δx) * (y − Δy) = k

通过左右变换与带入,可得:

Δy = y − (xy / x + Δx) = Δxy / (x + Δx)

并且,由于 V2 每组代币对只有一个流动性池,且手续费在每个代币对的池子里都是固定的 0.3% (V3 是允许多种手续费的,细节后文会提到),故 V2 代码直接对交易数量进行抽成。

// https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    // ...

    uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
    uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));

    // ...
}

V3 的交换公式

v3 pool

V3 的流动性池中,由于用户可以单独指定提供流动性的价格区间,故上图的整体流动性池例子中,相比 V2 是一条平滑的横线(图一),V3 更多情况下,在不同的区间,深度都不同(图三),图中的完全正态分布只是特殊情况。

例如,以下是 MATIC/ETH (手续费为 0.3%)池的头寸分布,可以看出许多头寸都在比当前价格更高的位置,或许可理解为当前市场内的做市商未来看涨 MATIC:

MATIC/ETH Liquidty

所以,我们可知,在 V3 中,一个交易是可能横跨多个用户的不同头寸的,故实际执行的,是横跨多个头寸的聚合交易。

我们先将目光限定在交易只影响一个单独头寸的情况,已知代币 X,Y 的数量为 x , y , x * y = L^2 ,以及价格 P = y / x (即可以用 P 个 X 代币,交换到 1 个 Y 代币)。可得:

v3 L P

通过带入,我们又可得:

v3 x y

即:

v3 Δx Δy

这样一来,我们仅只需知道 L 与 P ,就可知道包含了虚拟流动性的 x 和 y,不用再关心其他变量。并且有一个好处是,在同一段头寸中 L 是不变的,在切换头寸区间池的瞬间,P 是不变的。

所以当我们在使用 Δx 枚 X 代币去交换 Y 代币时时,会先用上图第一行的公式,先计算出消耗完当前所在池流动性后,新的价格 P ,然后再使用第二行公式,计算出具体在池内可交换出的 Δy 。若第一个公式中的 ΔP 已经跨过了当前池的价格区间(意味着即使当前流动性区间池的流动性被消耗完毕,依然不足以消化掉所有的 Δx ),那么就进入下一个池,继续重复上述逻辑。直到能消耗完所有的 Δx ,此时累计的 Δy 即是可交换到的 Y 代币数量。

在 V3 代码实现中,用户提供的流动性头寸的价格区间的两头,被称为两个 Tick ,上述计算,是跨一个个 Tick 来进行。

既然 Tick 是用于表示价格区间中的某个具体价格的,理论上在 (0, ∞) 这个范围内,可以有无穷多个 Tick 点。但是显然,在 Solidity 编程中,一个无限膨胀的 storage 变量是昂贵且难以接受的。

所以 V3 中,为 Tick 提供了固定的可选值,即 1.0001^i ,所以 Tick 其实是一个等幂数列。这是基于,当价格很低,用户对细小的价格变化更敏感,反之,在价格很高时,用户很大概率并不在意汇率里小数点最后面几位的区别。每一个 Tick 之间,V3 还提供一个最小间隔 i 的限制(Tickspacing),例如当 Tickspacing 为 10 时,第一个可用 Tick 是 1.0001^1 的话,那么往大第二个可用 Tick 就是 1.0001^11 。

并且,目前 V3 中只设定了三个可选费率(更多费率可经由社区治理投票在未来给出),且为三个费率设定了固定 Tickspacing ,进一步规范化计算消耗:

费率 Tickspacing 建议的使用范围
0.05% 10 稳定币交易对
0.3% 60 适用大多数交易对
1% 200 波动极大的交易对

手续费提现

关于手续费提现的问题,在 V2 中处理的比较直观。在 V2 中,由于只存在一个总流动性池,当用户注入流动性时,合约会同时给予 ERC20 代币作为凭证,当用户提现时,合约根据所持代币所占比例,给予用户总手续费收益中的提成。

但是当使用 V3 版本的自定区间实现时,如果还使用 V2 的办法,就会遇到问题。每当一比交易穿过多个 Tick 时,包含着每个 Tick 上的各头寸都要作单独记录且按比例分配。这不仅会产生大量额外的 Gas 费。且这个费用会让交换代币的用户而不是提现者承担,也是不公平的。

所以 V3 的解决方案是,在做市商每一次提供区间头寸的时候,都会给与一个 ERC721 代币,即 NFT ,里面包含了价格区间以及提供的具体流动性数量。而当用户进行代币交换时,合约会维护一个全局的手续费收入并且追踪每个 Tick 参与收集到的手续费数量。在用户提现时,先获取到头寸所有包含的 Tick 收集的总费用以及总流动性,然后根据用户 NFT 中的流动性数量占比,给与用户收益。

最后

本文为个人学习 Uniswap V3 白皮书的学习笔记,若有不准确之处,欢迎指出。:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant