uniswap
uniswap-v3
whitepaper
Uniswap v3是一个基于以太坊虚拟机(EVM)实现的无监管自动做市商(AMM)。与之前的版本相比,Uniswap v3提高了资金利用率,赋予流动性提供者更多控制能力,改进了价格预言机的准确性和便利性,同时增加了更灵活的手续费结构。
自动做市商(AMMs)是集中流动性,并基于算法将其开放给交易者的代理商。常值函数做市商(CFMMs)(Uniswap也是成员之一)作为AMM中的一个常见类别,已被广泛应用于去中心化金融场景,他们一般都在无需许可的区块链上以交易代币的智能合约的形式实现。
当前市场上的常值函数做市商大多存在资金利用率不高的问题。在Uniswap v1/v2使用的恒定乘积做市商公式中,对于给定价格,池子中仅部分资金参与做市。这显得十分低效,特别是当代币总是在特定价格附近交易时。
注:以稳定币为例,USDC/USDT的波动范围极小,而根据v2的公式,流动性提供者实际上会将资金分布在价格区间(0,
),即使这些价格几乎永远也无法使用到。因此在Uniswap v1/v2版本,资金利用效率较低,同时也导致交易滑点相对较高。
在此之前,Curve和YieldSpace等一些产品尝试解决这个资金利用率问题,他们通过建立池子,并使用不同的函数描述代币之间的关系。这要求池子里的所有流动性提供者都遵守同一个公式,而如果他们希望在不同的价格区间提供流动性,将导致流动性分裂。
在本文,我们将介绍Uniswap v3,一种新的自动做市商(AMM),它给予流动性提供者对资金被使用的价格区间更多控制权,并降低流动性分裂和gas消耗等问题的影响。该设计不依赖任何基于代币价格行为的共同假设。Uniswap v3仍然基于之前版本的常值函数曲线(即
集中流动性:流动性提供者(LP)将被赋予在任意价格区间集中流动性的能力。这将提高池子的资金利用率,并允许LP估算他们认可的价格曲线,同时又与池子里剩余资金一起提供高效聚合的流动性。我们将分别在第2节和第6节描述该特性及其实现。
灵活的手续费:交易手续费将不再限定在0.30%。相反,手续费等级在每个池子初始化时设置,每一个交易对包含多个等级(池子)。默认支持手续费等级为0.05%,0.30%和1%。可以通过UNI治理增加新的手续费等级。
注:UNI第9号提案申请引入新的手续费等级:0.01%,该提案已生效。0.01%的手续费适用于稳定币交易场景,使交易的滑点更小,这让Uniswap可以在稳定币交易领域直面Curve等市场龙头的竞争。
协议手续费治理:UNI治理可以灵活设置协议手续费对交易手续费的分成占比(参考6.2.2节)。
改进的价格预言机:Uniswap v3为用户提供了一种方式查询近期累计价格,从而避免了在计算TWAP(时间加权平均价格)的时间段开头和结尾手动记录累计价格。
流动性预言机:合约提供了一种时间加权平均流动性的预言机(参考5.3节)。
Uniswap v2 core合约被设计成不可升级的,因此Uniswap v3是在一组全新合约上实现。Uniswap v3合约同样是不可升级的,但允许一些参数被治理修改,我们将在第4节讨论。
Uniswap v3的设计思想是集中流动性:流动性限制在某个价格区间。
在之前的版本中,流动性被均匀分布在以下曲线:
其中,
注:比如稳定币交易对,大部分时候价格波动极小,如果像Uniswap v2一样将流动性分散到所有价格区间
,将导致资金利用率较低,因为大部分价流动性的价格区间永远不会被使用。
考虑到这个问题,允许LP将他们的流动性集中到更小的价格区间(而非
注:可以将一个v3池子的区间想象成一个v2池子的一部分。
特别地,一个头寸只需要持有足够的代币
当价格离开头寸区间时,该头寸的流动性将不再活跃,同时无法获得手续费。在该价格点上,流动性将完全只由一种代币组成,因为另一种代币都被耗尽。如果价格重新进入区间,流动性将再次变得活跃。
注:从图1可知,Uniswap常值函数池子的价格移动是以池子中两种代币余额的此消彼长来实现的,当价格(过高或过低)离开头寸区间,意味着其中一种代币被完全替换为另一种代币,因此此时区间中仅剩余一种代币。
流动性数量可以用
该曲线是公式2.1的变形,头寸只在自己的区间具有偿付能力(图2)。
注:下面我们来推导公式2.2:
由图2可知,v3区间流动性曲线(图中real reserves曲线)实际上是将v2流动性曲线(图中virtual reserves曲线)通过坐标平移而来。假设在
, 点的代币余额(reserves)(即坐标)分别为 , ,则需要将v2曲线沿横坐标平移: ,沿纵坐标平移: 。 已知v2的曲线为:
则平移后的v3曲线为:
, 点的价格分别为:
又因为:
因此,根据公式2.2.1和2.2.3可得:
根据公式2.2.2和2.2.4可得:
由于:
,因此: 将
, 代入公式2.2.0,可得:
只要流动性提供者觉得合适,他们可以自由地创建任意数量的头寸,每个头寸拥有自己的价格区间。通过这种方式,LP可以模拟价格空间中任意有分布需求的流动性(图3列举了部分例子)。此外,这种方式可以让市场决定流动性应该分配在什么地方。理智的LP们可以通过在当前价格附近的狭窄区间集中流动性来减少资金成本,并且通过添加或移除代币来移动价格,以使他们的流动性始终保持活跃。
在极小区间的头寸看起来非常像限价单,如果价格穿越区间,头寸将由完全为一种资产变成另一种资产(以及累计手续费)。区间订单与传统限价单有两点不同:
一个头寸的最小区间是有限制的。当价格正好位于头寸之内时,该限价单可能只有部分成交。
当头寸被穿越后,需要手动取回。否则,当价格再次回到区间时,头寸将自动反向交易。
注:如果价格反复穿越一个区间订单,头寸中的资产持仓将自动变化,从一种资产完全变成另一种资产,再反向变化,循环反复。而CEX的限价单在完全成交后,即使后期价格恢复,已成交的订单也不会回滚。
因此,如果需要实现像传统交易所一样的限价单效果,当价格穿越限价区间后,流动性提供者需要手动执行取回操作,才能完全获得另一种代币。或者可以使用第三方应用提供的自动取回功能,比如Gelato可以支持使用Uniswap v3的区间订单实现传统限价单的效果。
Uniswap v3实现了许多架构改动,其中一部分改动是为了实现集中流动性而必须引入的,而另一部分则是独立的功能改进。
在Uniswap v1和v2,每个交易对对应一个独立的流动性池子,并针对所有交易统一收取0.30%的手续费。虽然历史数据表明默认的手续费等级对于大部分代币都是合理的,但对于部分池子可能太高了(比如稳定币池子),而对于另一部分池子又太低了(比如高波动性或者冷门代币)。
Uniswap v3为每个交易对引入了多个池子,允许分别设置不同的交易手续费。所有池子都使用相同的工厂合约创建。默认允许创建三个手续费等级:0.05%,0.30%和1%。可以通过UNI治理添加更多手续费等级。
注:目前已经通过投票新增了一个0.01%的手续费等级。
3.2.1 非复利的手续费。之前版本的手续费收入会被作为流动性持续存入池子。这意味着即使没有主动存入,池子流动性也会随着时间而增长,并且可以复利地获取手续费收入。
在Uniswap v3,由于头寸的不可互换性,复利将变得不再可能。相反,手续费被独立保存,并且以支付手续费的代币形式持有(参考6.2.2)。
注:由于每个头寸的价格区间都不一样,因此v3的流动性不再像v2一样分布在所有价格区间,也就是说,v2流动性是可互换的,因此可以使用ERC-20代币表示。而v3流动性实际上是一个NFT(不可互换代币),使用ERC-721表示。
3.2.2 移除原生流动性代币。在Uniswap v1和v2,交易对池子合约本身是一个ERC-20合约,它的代币表示池子持有的流动性。虽然这种表示方式很方便,但它仍然与Uniswap v2的所倡导的理念有点不一致,即:任何不需要放在core合约的东西,都应该放到periphery合约,使用一个“标准”ERC-20的实现,阻止了后续创建ERC-20代币的优化版本。按理说,ERC-20代币实现应该放到periphery合约,再作为一个流动性头寸的封装放到core合约。
注:由于交易对合约的ERC-20实现在core合约中,并且是不可升级的,因此如果ERC-20的实现出现了bug,实际上会导致整个v2流动性受到影响。因此更好的方式是将ERC-20实现放到periphery合约中,而在core合约中仅存放一个wrapper引用,以便后续升级为新版本的ERC-20实现。
Uniswap v3引入的改动让可互换的流动性代币变成不可能。由于自定义流动性的特性,现在手续费以独立的代币被池子收集并持有,而不是自动复投为池子的流动性。
因此,v3的池子合约没有实现ERC-20标准。任何人都可以在periphery创建一种ERC-20代币合约,以便让流动性头寸变得更可互换,但这需要额外的逻辑来处理手续费收入的分发或再投资。或者,任何人都可以创建一个periphery合约,使用一种ERC-721 NFT代币表示个人流动性头寸(包括累计手续费)。
工厂合约拥有一个owner(所有者),该地址初始时被UNI代币持有者控制。owner没有权限暂停core合约的任何操作。
注:在ETH主网,factory合约地址为0x1F98431c8aD98523631AE4a59f267346ea31F984,owner是一个TimeLock合约,地址为0x1a9C8182C09F50C8318d769245beA52c32BE35BC
与Uniswap v2相同,Uniswap v3也有可以被UNI治理打开的协议手续费。在Uniswap v3,UNI治理可以更灵活地设置协议获取的交易手续费比例,可以将协议手续费设置为
注:Uniswap v2只能基于全局设置协议手续费,而Uniswap v3可以基于每个池子设置。
UNI治理可以添加额外的交易手续费等级。当添加一个手续费等级时,可以同时定义其对应的tickSpacing参数(参考6.1)。一旦手续费等级被添加进工厂合约,它就无法被移除(tickSpacing也无法被修改)。初始的手续费等级和tickSpacing分别为0.05%(tickSpacing为10,两个初始化tick之间约为0.10%),0.30%(tickSpacing为60,两个初始化tick之间约为0.60%),1%(tickSpacing为200,两个初始化tick之间约为2.02%)。
注:关于tick和tick spacing的概念,可以参考6.1节。
简单而言,每个tick(点)对应一个价格,为了聚合不同头寸的流动性,价格空间被划分为一个个可被初始化的tick,只有能被tickSpacing整除的tick才允许初始化;在tick内的交易机制与v2一样,当该tick的流动性被消耗以后,价格将进入下一个tick,并重复上述交易过程。因此tickSpacing越小意味着流动性越连续,交易滑点越小,但同时也带来了更大的gas消耗。
因此,每个手续费等级的tickSpacing是一个权衡值,但总体而言,越高的手续费等级,其tickSpacing越大。因为手续费越高,代表交易对的波动性越大,交易者能够承受的滑点也越大。
我们可以通过factory合约查看链上手续费配置:feeAmountTickSpacing,目前支持的feeAmount和tickSpacing分别为:
{100: 1, 500: 10, 3000: 60, 10000: 200}
。我们在6.1节会提到,两个相邻tick的最小价格误差为0.01%。
最后,UNI治理有权利将owner转移给其他地址。
Uniswap v2引入了时间加权平均价格(TWAP)预言机功能,Uniswap v3的TWAP包括三个重要改动。
其中最重要的改动是Uniswap v3无需预言机用户在外部记录历史累计价格。Uniswap v2要求用户在需要计算TWAP的区间的开始和结束阶段分别记录累计价格。Uniswap v3将累计检查点放到core合约,允许外部合约直接计算最近一段时间的链上TWAP,无需额外保存累计价格。
另一个改动是Uniswap v3不再使用累计价格之和计算算术平均数TWAP,而是通过记录
注:我们在《深入理解Uniswap v2白皮书》中提到,几何平均数相比算术平均数,受极端值的影响更小,并且无需为每种代币记录单独的累计价格,因为一个代币的几何平均数价格是另一个的倒数。
最后,除了价格累计数外,Uniswap v3还增加了一个流动性累计数,每秒累计
与Uniswap v2类似,Uniswap v3在每个区块开始记录累计价格,乘以自上一个区块到现在的时间(秒数)。
Uniswap v2的池子仅保存累计价格的最新值,该值由最近一个发生交易的区块更新。当在Uniswap v2计算平均价格时,需要由外部调用者负责提供提供累计价格的历史数据。如果有很多外部用户,每个用户都需要独立维护记录累计价格历史值的方法,或者使用一个共享方法减少成本。另外,无法保证每个有交互的区块都能影响累计价格。
在Uniswap v3,池子保存累计价格的一系列历史(如5.3节所述,也包括累计流动性)。在每个区块与池子第一次交互时,合约会自动记录累计价格,并且循环地使用新值覆盖数组中的最旧值,类似于一个环形缓冲区。虽然初始时数组仅分配一个检查点的空间,但是任何人都能够初始化额外的存储槽来扩展该数组,最多可达65,536个检查点。任何扩展该交易对检查点的人需要支付一次性的gas消耗来为数组初始化额外的存储槽。
注:扩展检查点空间的操作是一次性的,由发起操作的人支付。比如有人希望Uniswap v3的ETH-USDC交易对能够提供更多的历史价格检查点(检查点越多,意味着使用链上数据计算的预言机价格将越可信,因为攻击者要操纵这些价格所需的成本越高),以便通过链上可以获取预言机价格,他们就会调用ETH-USDC交易对的合约接口扩展检查点空间,并为此支付gas费用,因为该操作为交易对分配了额外的EVM存储槽空间。
交易对池子不仅向用户提供历史观测数据数组,还封装了一个便利函数用于在观测点周期内寻找任意时间点的累计价格。
Uniswap v2维护两个累计价格,一个是以token1表示的token0价格,另一个则是以token0表示的token1价格。用户可以计算任意时间段的时间加权算术平均数价格,通过将区间结尾的累计价格减去开始的累计价格,并除以区间的时间(秒数)得出。注意token0和token1的累计价格是分别跟踪的,因为两个算术平均数价格不是互为倒数关系。
Uniswap v3使用时间加权的几何平均数价格,避免了为两个代币分别维护累计价格。一组比例数的几何平均数是该比例倒数的几何平均数的倒数。
注:假设池子里以token1表示的token0价格为
,则以token0表示的token1价格为 。 token0的几何平均数价格为:
token1的几何平均数价格为:
因此,两种代币的几何平均数价格互为倒数,Uniswap v3合约中只需要保存一种代币的累计价格即可。
由于自定义流动性供应的实现机制(参考第6节),在Uniswap v3实现几何平均数比较简单。此外,累计数能够用更少的比特位表示,因为它只记录
注:为了以可接受的精度表示所有可能的价格,Uniswap v2使用224位比特的定点数表示价格。Uniswap v3仅需使用24位比特的有符号整数表示
,同时可以识别一个基点即0.01%的价格变动。 如前文所述,市场价格本身是一种随机布朗运动,理论上使用几何平均数更能准确跟踪平均价格,因为算术平均数更容易受到极端值的影响而产生偏差。
Uniswap v3记录当前tick序号的累计和(
注:为什么
能够识别的价格变化精度为0.01%(即1个基点)呢? 因为Uniswap v3使用int24(24位有符号整数)表示tick,假设当前tick为
,对应的价格为 ;下一个最近的tick为 ,对应的价格 ,其相对 的价格变化精度为:
任意时间段
注:这里我们回顾一下几何平均数的定义:
几何平均数 Geometric Mean:
可以看出
即为 至 时间段的几何平均价格。
为了计算这个值,你可以分别查看
除了每秒加权的累计数
这个计数可以被外部流动性挖矿合约使用,以便公平地分配奖励。如果一个外部合约希望以每秒
注:
表示:截止到 时刻,(池子)平均每份流动性持续的总时长(秒数)。
表示: 至 这个时间段,(池子)平均每份流动性持续的总时长(秒数)。
表示:在 至 时间段,平均每份流动性获得的奖励。 由于该头寸的活跃流动性为
,因此,其在 至 时间段获得的奖励为:
为了扩展这个公式,实现仅当流动性在头寸区间时才能获得奖励,Uniswap v3在每次tick被穿越时会保存一个基于该值计算后的检查点,我们将在第6.3节介绍。
链上合约可以使用该累计数,以使他们的预言机更健壮(比如用于评估哪个手续费等级的池子更适合被作为预言机数据源)。
本文剩余部分将介绍集中流动性供应的实现机制,同时简要介绍其在合约是如何实现的。
为了实现自定义流动性供应,可能的价格空间被离散的点(tick)划分。流动性提供者可以在任意两个(无需是临近的)tick定义的区间提供流动性。
每个区间可以被一对(有符号整数)tick序号(tick indices)定义:一个低点(
从概念上,每当价格
根据定义,两个相邻的tick之间的价格移动精度为0.01%(1个基点)。
注:见5.2节的公式推导。
由于6.2.1节中描述的技术原因,交易对池子实际上使用开根号价格
举个例子,
当流动性加入一个区间,如果其中一个或全部tick都没有被已存在的头寸用作边界点,该tick将被初始化。
不是每个tick都能被初始化。交易对池子在初始化时有一个参数tickSpacing(
任何时候价格穿越一个初始化的tick时,虚拟流动性将被加入或者移除。穿越一个初始化的tick所带来的gas消耗是固定的,与在该tick添加或移除虚拟流动性的头寸数量无关。
为了确保当价格穿越tick时,能够添加和移除正确数量的流动性;同时也为了确保当头寸在价格区间内时,能够正确获取对应比例的手续费收入,交易对池子需要一些记账工作。交易对合约使用存储变量来分别记录全局(每个池子)级别、每个tick级别和每个头寸级别的状态。
合约的全局状态包括7个与交换和流动性供应相关的存储变量。(它也有其他一些存储变量用于预言机,如第5节描述。)
在Uniswap v2,每个池子合约记录池子当前的代币余额:
注:Uniswap v3实际上只在一段价格区间内遵循常值函数。
交易对合约记录两个不同值:流动性liquidity(
反过来,两种代币的虚拟余额也可以使用这两个值计算得出:
使用
你可能注意到(基于代币虚拟余额的)流动性公式(即公式6.3)与Uniswap v2用于初始化流动性代币数量的公式类似(当还没有任何手续费收入时)。流动性可以被看作虚拟流动性代币。
同样,流动性也可以被看作token1的(无论是真实还是虚拟的)数量变化与价格
注:根据公式6.6,假设
和 时刻,对应的 和 分别为:
因此:
转换后即可得出公式6.7:
我们记录
全局状态记录当前tick序号为
注:根据公式6.2:
, 因此:
而i是整数,所以还需要(向下)取整。
每个交易对池子初始化时会设置一个不可修改的手续费(
注:默认的手续费值为500,3000,10000,分别表示的手续费为:500 x 0.0001% = 0.05%, 3000 x 0.0001% = 0.30%, 1000 x 0.0001% = 1%。
另一个变量为协议手续费
注:协议手续费开关无法在创建交易对的时候自动打开,只能由UNI治理针对具体池子单独执行手续费设置,并且可以针对不同池子分别设置协议手续费。
全局状态还记录两个值:feeGrowthGlobal0 (
最后,全局状态记录以每种代币表示的累计未被领取的协议手续费:protocolFees0 (
对于那些无法使价格变化超过一个tick(点)的小额交易,该合约像一个
假设
首先,feeGrowthGlobal1和protocolFees1将增加:
注:
是协议手续费占手续费的比例,因此协议手续费比例为: ,协议手续费收入为公式6.10。 剩余的手续费分给流动性提供者,即扣除协议手续费后的交易手续费,其比例为:
,交易手续费收入为公式6.9。
如果你用经过计算的虚拟余额(
注:因为在1个tick内,交易符合k常值函数,即:
因此可推出:
但是请注意,在v3,合约使用流动性(
同时可以推导出
注:根据公式6.5,假设
和 时刻,对应的 和 分别为:
因此:
转换后即可得公式6.16:
当使用一种代币交换另一种时,交易对合约可以先根据公式6.13或6.15计算新的开根号价格
对于任意交易,只要交易后的开根号价格
如果一个tick没有被用作流动性区间的边界点(即如果该tick没有被初始化),那么在交易过程中可以跳过这个tick。
为了更高效寻找下一个已初始化的tick,合约使用一个位图tickBitmap记录已初始化的tick。如果tick已被初始化,位图中对应于该tick序号的位置设置为1,否则为0。
当tick被一个新头寸用作边界点,并且该tick没有被任何其他流动性使用,那么它将被初始化,位图中对应的比特位置为1。当该点关联的流动性都被移除时,已初始化的tick将重新变成未初始化,位图中对应的比特位置为0。
为了记录每个tick被穿越时需要添加和移除的净流动性,以及在大于和小于该tick时所挣取的手续费,合约需要额外保存每个tick相关的信息。
合约保存一个映射表,每个tick序号对应以下7个变量:
每个tick记录
当tick没有流动性关联时,我们希望对其取消初始化。因为
feeGrowthOutside{0, 1}用于记录一个给定区间总共累计多少手续费。因为token0和token1收集手续费的公式相同,我们在本节剩余的公式中将忽略(token0和token1)下标。
根据当前价格是否在区间内,你可以使用一个公式计算每份流动性在tick
注: 首先回顾一下每个变量的含义,
是(每个流动性)全局累计手续费; 是在指定tick 之外(每个流动性)累计的手续费,需特别注意,该值随着当前tick 变化后会改变表示的方向。 当
时,
当
时,
我们可以使用上述函数计算任意两个tick(低点tick
注:根据上述推论,我们可以画出几个手续费的逻辑关系如下:
只有被至少一个头寸作为边界端点的tick才需要
注意,因为不同tick的
最后,合约同时为每个tick保存secondsOutside (
这三个变量于上文提到的手续费增长变量类似。但是不同于feeGrowthOutside{0, 1}跟踪feeGrowthGlobal{0, 1},secondsOutside跟踪seconds(也就是当前时间戳),secondsPerLiquidityOutside跟踪5.3节中描述的
比如,对于一个给定的tick,根据当前价格是否在区间内,
在
和
与
如6.2.3节描述,当在初始化的tick之间交易时,Uniswap v3可以像
为了记录在价格区间内时,该tick作为边界点的手续费收入(和持续时间),合约需要更新tick的状态。feeGrowthOutside{0, 1}和secondsOutside被更新到反映当前值,当与该tick关联的交易方向改变时,按照下述公式更新:
当一个tick被穿越后,如6.2.3节描述,交易将继续直到碰到下一个已初始化的tick。
合约记录一个映射表,从用户地址,头寸低点(左边界,一个tick序号,int24类型)和高点(右边界,一个tick序号,int24类型)到具体头寸信息的映射关系。每个头寸记录三个值:
liquidity(
liquidity(流动性)数量不代表从合约上次交互后的累计手续费,uncollected fees才用于表示未领取的手续费。为了计算未领取的手续费,需要在头寸保存额外信息,如feeGrowthInside0ast(
setPosition方法允许流动性提供者更新他们的头寸。
setPosition的两个参数:lowerTick和upperTick,与调用者msg.sender一起组成了头寸的信息。
该方法接受一个额外参数:liquidityDelta,用于指定用户希望添加或移除(负值)的虚拟流动性。
首先,该方法计算头寸的未领取手续费(
为了计算一个代币的未领取手续费,你需要知道自从上一次领取手续费后,该头寸对应的区间获得多少手续费
接着,合约将liquidityDelta加到头寸的liquidity(流动性)。在tick区间低点,它同时将liquidityDelta加到liquidityNet(注:tick从左到右,表示加入流动性);而在头寸的高点,则从liquidityNet减去liquidityDelta(注:tick从右到左,表示移除流动性)。如果池子当前价格在头寸区间内,合约也会将liquidity加到全局的globalLiquidity。
最后,根据销毁或铸造的流动性数量,池子将代币从用户转出(如果liquidityDelta是负值,则将代币转给用户)。
如果价格从当前价格(