Uniswap V3 数学图解

在这篇文章中,我们将通过使用UniswapPy Python包来讲解Uniswap V3 AMM协议背后的数学基础概念。

Uniswap V3 数学图解
一键发币: SUI | SOL | BNB | ETH | BASE | ARB | OP | POLYGON | AVAX | FTM | OK

2016年,以太坊创始人Vitalik Buterin在Reddit上提交了一篇帖子,提出了他的自动化做市商(AMM)的原始想法。一年后,Hayden Adams开始将其转化为一个功能产品,并创立了Uniswap,该产品于2018年11月在以太坊主网上线。

自动化做市商(AMM)协议是去中心化交易所(DEX)所使用的机制。这些DEX由各种交易对(例如ETH/USDC、ETH/WBTC等)表示的流动性池(LP)组成,作为AMM。这些LP中的交易活动通过智能合约进行管理,其核心概念被称为“恒定乘积交易(CPT)”公式(即xy = k)。

2021年5月,Uniswap推出了V3版本,作为解决“懒惰流动性问题”的升级,从而引入了集中流动性做市商(CLMM)协议。这一升级的核心思想是在活跃交易区间内集中流动性,这实际上加深了订单簿,使更多的流动性可用于交易;请参阅本文开头的图片。可以说,这是第一个通过AMM流动性频率分布来提高市场效率的协议。

在这篇文章中,我们将通过使用UniswapPy Python包来讲解Uniswap V3 AMM协议背后的数学基础概念,同时引导讨论。这对任何不熟悉Uniswap V3并希望首次接触AMM的人非常有用。

1、交换

交换是指某人带着一对资产中的一种数量Δy来到LP,并期望收到另一种资产的数量Δx。与Uniswap V2 CPT曲线相比,主要的区别在于我们限制了交换可以发生的范围在一个给定的价格区间内。这样做会营造出池中流动性比实际更多的假象,从而加深订单簿。因此,CPT曲线从原点向外推移,形成了所谓的虚拟流动性。一旦曲线调整完毕,交换在新曲线上的动态与我们在我们的Uniswap V2文章中描述的一样。一种直观的方式来看待Uniswap V3,就是将其视为一系列较小的Uniswap V2系统。首先,让我们设置我们的模拟LP:

eth = ERC20("ETH", "0x09")  
dai = ERC20("TKN", "0x111")  
  
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP",   
                                   address="0x011", version = 'V3',   
                                   tick_spacing = tick_spacing,   
                                   fee = fee)  
  
factory = UniswapFactory("ETH pool factory", "0x2")  
lp = factory.deploy(exchg_data)  
Join().apply(lp, user_nm, eth_amount, dai_amount, lwr_tick, upr_tick)  
lp.summary()  
  
Exchange ETH-TKN (LP)  
Real Reserves:   ETH = 1000.0000000000001, TKN = 1000000.0000000001  
Gross Liquidity: 31622.776601683796

Uniswap V3白皮书中的方程(6.13)和(6.15)分别给我们提供了:

其中

我们计算得到接收的Δx为

其中下一个价格Pₙₑₓₜ由Δy输入得出,包含费用(γ = 997/1000),给我们:

现在我们使用上述推导进行dy到dx的交换:

dy = 1000  
Q96 = 2**96  
sqrtp_cur = lp.slot0.sqrtPriceX96/Q96 # 将Q96转换为人类可读形式  
gamma = 997/1000   
  
x = lp.get_reserve(eth)  
y = lp.get_reserve(dai)  
L = lp.get_liquidity()  
  
sqrtp_next = sqrtp_cur + (gamma*dy) / (L)  
dx = L * (1/sqrtp_cur - 1/sqrtp_next)   
print(f'We receive {dx:.5f} ETH for {dy} DAI')  
  
We receive 0.99601 ETH for 1000 DAI

接下来,我们使用UniswapPy进行交换确认:

out = Swap().apply(lp, dai, user_nm, dy)  
lp.summary()  
  
print(f'We receive {out:.5f} ETH for {dy} DAI')  
print(f'Confirm price: (1/sqrtp_next^2)={1/sqrtp_next**2:.8f} vs (actual price)={lp.get_price(dai):.8f}')   
  
Exchange ETH-TKN (LP)  
Real Reserves:   ETH = 999.0039930189602, TKN = 1001000.0000000001  
Gross Liquidity: 31622.776601683796   
  
We receive 0.99601 ETH for 1000 DAI  
Confirm price: (1/sqrtp_next^2)=0.00099801 vs (actual price)=0.00099801

由于Δx和Δy的动态不同,根据Uniswap V3白皮书中的方程(6.13)和(6.15),我们将重复上面的操作,以演示一次dx到dy的交换:

eth = ERC20("ETH", "0x09")  
dai = ERC20("TKN", "0x111")  
  
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP",   
                                   address="0x011", version = 'V3',   
                                   tick_spacing = tick_spacing,   
                                   fee = fee)  
  
factory = UniswapFactory("ETH pool factory", "0x2")  
lp = factory.deploy(exchg_data)  
Join().apply(lp, user_nm, eth_amount, dai_amount, lwr_tick, upr_tick)  
lp.summary()  
  
Exchange ETH-TKN (LP)  
Real Reserves:   ETH = 1000.0000000000001, TKN = 1000000.0000000001  
Gross Liquidity: 31622.776601683796

Uniswap V3白皮书中的方程(6.13)和(6.15)分别给出:

其中

我们计算接收到的Δy为:

其中下一个价格Pₙₑₓₜ由Δx输入得出,包含费用(γ = 997/1000),给我们:

现在我们使用上述推导进行dx到dy的交换:

dx = 1  
Q96 = 2**96  
sqrtp_cur = lp.slot0.sqrtPriceX96/Q96 # 将Q96转换为人类可读形式  
gamma = 997/1000   
  
x = lp.get_reserve(eth)  
y = lp.get_reserve(dai)  
L = lp.get_liquidity()  
  
sqrtp_next = 1/(1/sqrtp_cur + (gamma*dx)/(L))  
dy = L * (sqrtp_cur - sqrtp_next)  
  
print(f'We receive {dy:.5f} DAI for {dx} ETH')  
We receive 996.00698 DAI for 1 ETH

接下来,我们使用UniswapPy进行交换确认:

out = Swap().apply(lp, eth, user_nm, dx)  
lp.summary()  
  
print(f'We receive {out:.5f} DAI for {dx} ETH')  
print(f'Confirm price: (sqrtp_next^2)={sqrtp_next**2:.6f} vs (actual price)={lp.get_price(eth):.6f}')   
  
Exchange ETH-TKN (LP)  
Real Reserves:   ETH = 1001.0000000000001, TKN = 999003.9930189601  
Gross Liquidity: 31622.776601683796   
  
We receive 996.00698 DAI for 1 ETH  
Confirm price: (sqrtp_next^2)=998.008978 vs (actual price)=998.008978

3、双边取款

当流动性提供者希望从LP中提取资金时,他们必须同时提取x和y。这样做将减少池中的流动性ΔL,并将CPT曲线推向原点。再次像交换一样,我们必须考虑CLMM价格曲线的价格区间范围。假设上下限不变,这将导致撤资后价格没有变化。让我们再次设置我们的模拟LP:

eth = ERC20("ETH", "0x09")  
dai = ERC20("TKN", "0x111")  
  
exchg_data = UniswapExchangeData(tkn0 = eth, tkn1 = dai, symbol="LP",   
                                   address="0x011", version = 'V3',   
                                   tick_spacing = tick_spacing,   
                                   fee = fee)  
  
factory = UniswapFactory("ETH pool factory", "0x2")  
lp = factory.deploy(exchg_data)  
Join().apply(lp, user_nm, eth_amount, dai_amount, lwr_tick, upr_tick)  
lp.summary()  
  
Exchange ETH-TKN (LP)  
Real Reserves:   ETH = 1000.0000000000001, TKN = 1000000.0000000001  
Gross Liquidity: 31622.776601683796

Uniswap V3白皮书中的方程(6.29)和(6.30)给出了双侧增量x和y,如下所示:

确定后,我们使用CPT撤资公式更新我们的价格曲线,公式如下:

现在,我们可以手动计算一次提款操作,使用以下推导:

dx = 1  
Q96 = 2**96  

sqrtp_pa = TickMath.getSqrtRatioAtTick(lwr_tick)/Q96  
sqrtp_pb = TickMath.getSqrtRatioAtTick(upr_tick)/Q96  
sqrtp_cur = lp.slot0.sqrtPriceX96/Q96  

dPx = (1/sqrtp_cur - 1/sqrtp_pb)    
dPy = (sqrtp_cur - sqrtp_pa)   
dLx = dx/(1/sqrtp_cur - 1/sqrtp_pb)  

dx = dLx*dPx  
dy = dLx*dPy  

new_x = (x-dx)  
new_y = (y-dy)   
new_L = L-dLx  

print(f'更新后的储备量为 {new_x:8f} ETH 和 {new_y:8f} DAI,更新后的流动性为 {new_L:8f}')  

更新后的储备量为 999.000000 ETH 和 999000.000000 DAI,更新后的流动性为 31591.153825

接下来,我们使用UniswapPy执行提款操作以确认结果:

RemoveLiquidity().apply(lp, eth, user_nm, dx, lwr_tick, upr_tick)  
lp.summary()  

ETH-TKN 交换池(LP)  
实际储备:ETH = 999.0000000000001,TKN = 999000.0000000001  
总流动性:31591.15382508211

4、双边存款

当流动性提供者希望将资金存入LP时,他们必须同时存入x和y。这样做会增加池中的流动性ΔL,并使CPT曲线从原点向外扩展。与交易类似,我们必须考虑价格带范围内的CLMM价格曲线。假设下限和上限未发生变化,这将导致存款后价格没有变化。让我们再次设置我们的模拟LP:

eth = ERC20("ETH", "0x09")  
dai = ERC20("TKN", "0x111")  

exchg_data = UniswapExchangeData(tkn0=eth, tkn1=dai, symbol="LP",  
                                  address="0x011", version='V3',  
                                  tick_spacing=tick_spacing,  
                                  fee=fee)  

factory = UniswapFactory("ETH 池工厂", "0x2")  
lp = factory.deploy(exchg_data)  
Join().apply(lp, user_nm, eth_amount, dai_amount, lwr_tick, upr_tick)  
lp.summary()  

ETH-TKN 交换池(LP)  
实际储备:ETH = 1000.0000000000001,TKN = 1000000.0000000001  
总流动性:31622.776601683796

Uniswap V3白皮书的第6.29和6.30节中提供了x和y的双向增量公式,如下所示:

确定后,我们使用CPT存款公式更新价格曲线,公式如下:

现在,我们可以手动计算一次存款操作,使用上述推导:

dx = 1  
Q96 = 2**96  

sqrtp_pa = TickMath.getSqrtRatioAtTick(lwr_tick)/Q96  
sqrtp_pb = TickMath.getSqrtRatioAtTick(upr_tick)/Q96  
sqrtp_cur = lp.slot0.sqrtPriceX96/Q96  

dPx = (1/sqrtp_cur - 1/sqrtp_pb)    
dPy = (sqrtp_cur - sqrtp_pa)   
dLx = dx/(1/sqrtp_cur - 1/sqrtp_pb)  

dx = dLx*dPx  
dy = dLx*dPy  

new_x = (x-dx)  
new_y = (y-dy)   
new_L = L-dLx  

print(f'更新后的储备量为 {new_x:8f} ETH 和 {new_y:8f} DAI,更新后的流动性为 {new_L:8f}')  

更新后的储备量为 999.000000 ETH 和 999000.000000 DAI,更新后的流动性为 31591.153825

接下来,我们使用UniswapPy执行存款操作以确认结果:

AddLiquidity().apply(lp, eth, user_nm, dx, lwr_tick, upr_tick)  
lp.summary()  

ETH-TKN 交换池(LP)  
实际储备:ETH = 1001.0000000000001,TKN = 1001000.0000000001  
总流动性:31654.39937828548

5、单边提款

与同时提取x和y不同,这里我们推导如何仅提取其中一种资产(x或y),这就是所谓的单边提款。这是通过在一个自主程序中执行提款并随后对不想要的资产进行交换来实现的。这个操作在原始的Uniswap V3配对代码中不存在,是UniswapPy Python包的一项创新,称为WithdrawSwap。让我们再次设置我们的模拟LP:

eth = ERC20("ETH", "0x09")  
dai = ERC20("TKN", "0x111")  

exchg_data = UniswapExchangeData(tkn0=eth, tkn1=dai, symbol="LP",  
                                  address="0x011", version='V3',  
                                  tick_spacing=tick_spacing,  
                                  fee=fee)  

factory = UniswapFactory("ETH 池工厂", "0x2")  
lp = factory.deploy(exchg_data)  
Join().apply(lp, user_nm, eth_amount, dai_amount, lwr_tick, upr_tick)  
lp.summary()  

ETH-TKN 交换池(LP)  
实际储备:ETH = 1000.0000000000001,TKN = 1000000.0000000001  
总流动性:31622.776601683796

单边提款由提款和交换的总和组成,也称为WithdrawSwap。让我们回忆一下Uniswap V3白皮书中的双侧增量公式(6.29)和(6.30)。为了计算单边提款的数学公式,当我们考虑Δx和Δy时,得到的是单独的计算。使用双侧增量公式,首先让我们考虑设置Δy的WithdrawSwap:

为了求解ΔL,我们将Δx和Δy代入Δyₛ,经过一些代数运算,我们得到以下二次方程:

鉴于我们知道所需的提款金额Δyₛ,我们解上述二次方程以确定单边结算金额ΔL。现在我们可以使用UniswapPy执行单边提款:

WithdrawSwap().apply(lp, eth, user_nm, 1, lwr_tick, upr_tick)  
lp.summary()  

ETH-TKN 交换池(LP)  
实际储备:ETH = 999.0000000000001,TKN = 1000000.0000000001  
总流动性:31606.937511796757

接下来,让我们首先考虑设置Δx的WithdrawSwap。首先,让我们设置我们的池:

eth = ERC20("ETH", "0x09")  
dai = ERC20("TKN", "0x111")  

exchg_data = UniswapExchangeData(tkn0=eth, tkn1=dai, symbol="LP",  
                                  address="0x011", version='V3',  
                                  tick_spacing=tick_spacing,  
                                  fee=fee)  
factory = UniswapFactory("ETH 池工厂", "0x2")  
lp = factory.deploy(exchg_data)  
Join().apply(lp, user_nm, eth_amount, dai_amount, lwr_tick, upr_tick)  
lp.summary()  

ETH-TKN 交换池(LP)  
实际储备:ETH = 1000.0000000000001,TKN = 1000000.0000000001  
总流动性:31622.776601683796

类似于上面的方法,我们设置Δx如下:

为了求解ΔL,我们将Δx和Δy代入Δxₛ,经过一些代数运算,我们得到以下二次方程:

鉴于我们知道所需的提款金额Δxₛ,我们解上述二次方程以确定单边结算金额ΔL。

使用uniswappy执行单边提款

WithdrawSwap().apply(lp, dai, user_nm, 1000, lwr_tick, upr_tick)  
lp.summary()  

ETH-TKN 交换池(LP)  
实际储备:ETH = 1000.0000000000001,TKN = 999000.0  
总流动性:31606.937511796754

6、结束语

本文介绍了Uniswap V3协议背后的数学概念,使用了DeFiPy python套件,此演示背后的相关代码可在DeFiPy GitHub仓库中找到。还介绍了单边提款背后的数学原理,这些内容在Uniswap社区以及研究文献中尚未讨论过(据我所知)。这些自主操作非常有用,因为只需要其中一个资产对就可以完成此类交易。


原文链接:Uniswap V3 Math Tutorial using UniswapPy

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。
通过 NowPayments 打赏