来源:Gryphsis Academy
本篇研报是对 UniswapV2 协议的工作原理、项目构成、源码分析等部分进行详细解读。 工作原理主要是涉及自动做市商( AMM ),本篇研报会描述流动性池的创建和管理过程,以及如何通过提供流动性赚取手续费;在项目构成方面,我们主要概述 UniswapV2 的架构,包括主要合约(如工厂合约、交换合约)及其功能;在源码分析部分我们分析 UniswapV2 的智能合约源码,解释关键函数和数据结构的设计理念。
1.协议简介
UniswapV2 是一种去中心化交易协议,基于以太坊区块链,允许用户无需信任中介即可进行加密货币的交易。与传统的中心化交易所不同,UniswapV2 采用了自动做市商( AMM )模型,通过智能合约来管理交易和流动性池,从而实现完全的去中心化。
UniswapV2 的核心在于其恒定乘积公式( x * y = k ),其中 x 和 y 分别代表流动性池中两种不同资产的数量,k 是一个常数。这个公式确保了在每次交易后,池中的资产比例会重新平衡,从而为用户提供流动性。这种设计使得交易过程透明且公平,用户可以随时添加或移除流动性,并通过交易手续费赚取收益。
UniswapV2 协议由多个智能合约构成,其中最主要的是工厂合约和交换合约。工厂合约负责创建和管理流动性池,而每个交换合约则对应一个特定的交易对(例如 ETH/DAI)。
此外,UniswapV2 还引入了路由器合约和库函数,以提高交易效率和安全性。与其前身 UniswapV1 相比,UniswapV2 带来了几项重要改进。首先是闪电交换功能,允许用户在单个交易中借入资产,只要在交易结束前归还即可。其次是价格预言机,通过累积价格时间加权平均值( TWAP )来提供更可靠的价格数据。
此外,UniswapV2 支持任意 ERC-20 代币的直接交易,而无需通过以太坊作为中介。UniswapV2 的成功不仅在于其技术创新,还在于其开放性和社区驱动的开发模式。任何人都可以自由使用和扩展 Uniswap 协议,这为去中心化金融( DeFi )生态系统带来了无限的可能性。许多其他 DeFi 项目,如借贷平台和稳定币协议,都建立在 Uniswap 的基础之上,形成了一个繁荣的生态系统。
总的来说,UniswapV2 通过其创新的协议设计和去中心化的运营模式,彻底改变了加密货币交易的方式,成为 DeFi 领域的重要基石。随着技术的不断发展和社区的持续创新,UniswapV2 的影响力将进一步扩大,为全球用户带来更多的金融自由和机会。
2.协议特性
2.1 ERC-20 Pairs
Uniswapv1 会使用以太坊 (ETH) 作为过渡货币,即如果用户想进行 TokenA 和 TokenB 交换,需要先使用 TokenA 来换取 ETH ,之后再使用 ETH 来换取 TokenB ,尽管这种措施减少了流动性的分散,但它对流动性提供者造成了极大的成本压力。每个流动性提供者都必须具备与 ETH 交换的接口,同时,流动性提供者所持有资产的价值随着ETH的价格波动而波动,可能会导致严重的损失。
当两个资产 ABC 和 XYZ 相关联时(例如,它们都是美元稳定币),Uniswap 上的流动性提供者在 ABC / XYZ对中通常会承受较小的永久损失,相较于 ABC/ETH 或 XYZ/ETH 对。此外,使用 ETH 作为强制过渡货币会增加交易者的成本。交易者需要支付的费用是直接购买 ABC/XYZ 对费用的两倍,并且还会遭受两次滑点。
在 UniswapV2 当中,允许流动性提供者为任意两个 ERC-20 代币创建交易对合约。虽然任意 ERC-20 代币之间的交易对数量激增,可能会使找到特定资产交易路径变得更加复杂,但这一问题可以通过更高级别的路由来解决(通过链外或链上路由器或聚合器)。
2.2 价格预言机
UniswapV1的价格计算方式是在 t 时刻,Uniswap 所提供的边际价格(不包含手续费)可以通过用资产 a 的储备量除以资产 b 的储备量来获得。具体的计算公式如下:
然而,UniswapV1 用作链上价格预言机并不安全,因为它非常容易被操纵。操纵者会在一个区块的开头大量卖出某种资产 A 以影响价格,然后在该区块中间根据波动的价格执行其他合约操作(非 Uniswap 交易对合约),最后在区块结束时买回相同数量的资产 A,使价格恢复正常水平。Uniswap V2 引入了价格累计机制,允许第三方使用某一区间的平均价格,从而大大增加了价格操纵的难度,并使得操纵行为无实质收益。
具体来说,Uniswap v2 通过在每个区块开始时记录价格的累积和来累积这个价格,其中有人与合约交互。每个价格根据自上一个区块更新以来所经过的时间进行加权,根据区块时间戳。这意味着,在任何给定时间(更新后),累积器的值应该是合约历史中每一秒的现货价格的总和。
为了估算从时间 t₁ 到 t₂ 的时间加权平均价格,外部调用者可以在 t₁ 检查累积器的值,然后在 t₂ 再次检查,减去第一个值,然后除以经过的秒数。(注意,合约本身不存储这个累积器的历史值——调用者必须在周期开始时调用合约以读取和存储这个值。)
预言机的用户可以选择何时开始和结束这个周期。选择一个更长的周期会使攻击者操纵时间加权平均价格(TWAP)的成本更高,尽管这会导致价格的最新程度较低。由于采用了平均值,A/B 和 B/A 在某一区间内的平均值不再是倒数关系,因此 Uniswap V2 提供了这两种价格。
2.3 价格计算精度
由于 Solidity 不支持非整数的数值数据类型,UniswapV2 采用 UQ112.112 数据格式来提升价格计算的精度,同时使用 uint112 来存储交易对中的资产数量,并用 32 位来记录当前区块的创建时间。这种时间记录方式在 100 年后,也就是 2106 年 2 月 7 日,会导致 Unix 时间戳溢出。这个是因为 Unix 时间戳是以自 1970 年 1 月 1 日(称为 Unix 纪元)以来经过的秒数来表示时间的,而 uint32 能表示的范围是从0到 2^{32} - 1 秒,即约 136 年。
为了确保系统在 2106 年 2 月 7 日及以后仍能正常工作,UniswapV2 的设计中要求预言机每隔一个溢出周期(约 136 年)至少检查一次价格。这是因为在这种设计下,即使时间戳溢出,累积价格的方法仍然是溢出安全的,即使交易跨越溢出间隔也可以正确计算价格变化。通过这种方法,确保系统在长时间内保持准确和可靠。
2.4 闪电兑换
闪电兑换是一种在区块链平台上进行的即时加密货币交换,用户可以在无需等待多个区块确认的情况下快速完成不同加密货币之间的交易。这种交易通常由智能合约自动执行,确保交易双方同时履行交换承诺,从而降低交易风险并提高效率。简答来说,闪电兑换就是先欠再还进行交易,通过闪电兑换可以实现零成本套利。
我们通过一个实际例子来解释一下闪电兑换,假定 UniswapV2 中存在一个 A/B 的交易对,我们可以先借 A ,从而从 Uniswap 当中获取 B ,之后再使用 B 从其他的去中心化交易所上获取 A0,之后归还 Uniswap 等量的 A 即可。通过上面这个流程,我们就 0 成本的完成了 A0-A 之间的价差套利。我们并不需要任何本金,只需要支付 gas 费即可。如果发现链上其他的 dex 之间有价差,也可以对其进行类似的操作进行零成本的套利。
2.5 Protocol Fee
UniswapV2 引入了一个可以开启和关闭的 0.05% 协议费。如果开启,这个费用将发送到 factory 合约中指定的 feeTo 地址。最初,feeTo 是未设置的,因此不收取费用。一个预先指定的地址 feeToSetter 可以调用 UniswapV2 factory 合约中的 setFeeTo 函数,设置 feeTo 为不同的值。feeToSetter 还可以调用 setFeeToSetter 来更改 feeToSetter 地址本身。
如果设置了 feeTo 地址,协议将开始收取 5 个基点的费用,这是流动性提供者赚取的 30 个基点费用的 \frac{1}{6} 。也就是说,交易者将继续支付 0.30% 的所有交易费用,其中 83.3%(即 0.25%)将支付给流动性提供者,16.6%(即 0.05%)将支付给 feeTo 地址。在每次交易时收取这 0.05% 的费用会增加额外的 gas 成本。为了避免这种情况,累计费用仅在存入或提取流动性时收取。合约会计算累计费用,并在代币铸造或销毁之前立即将新的流动性代币铸造给费用受益人。
累计费用可以通过测量√k 的增长来计算(即 √(x·y) ),自上次收取费用以来。这公式给出了在 t₁ 和 t₂ 之间,作为 t₂ 时流动池中流动性的百分比的累计费用:
如果费用在 t₁ 之前被激活,feeTo 地址应当捕获 t₁ 到 t₂ 之间累计费用到 ⅙。因此,我们想要铸造新的流动性代币到 feeTo 地址,代表 Φ·f₁,₂ 的池子, 其中Φ=⅙ 。
也就是说,我们希望选择 sₘ 来满足以下关系,其中 s₁ 是时间 t₁ 时的流通股的总量:
经过一些运算,包括用 1-
来代替 f₁,₂ 并求解 sₘ , 我们可以将其改写为:
将 Φ 设置为 ⅙ ,我们会得到以下公式
下面,我们通过一个具体的例子来解释一下。假设初始存款人将 100 DAI 和 1 ETH 存入一个配对中,获得 10 股。一段时间后(没有其他存款人参与该配对),他们尝试提取资金,此时该配对中有 96 DAI 和 1.5 ETH。将这些值代入上述公式,我们得到如下结果:
2.6 交易费用计算
UniswapV1 交易费用的计算是公式是
这个公式意味着只是先减少支付数量后再执行恒定乘积公式;在 UniswapV2 的版本中,因为 Flash Swaps 机制的存在,交易费用的计算公式调整为
2.7 sync ()和skim()
sync() 用于将合约中暂存的资产数量更新为合约的当前实际值,主要用于处理那些比例失衡且没有流动性提供者的情况。skim() 则用于处理合约内某种资产数量超过 uint112 最大值的情况,允许用户提取超出 uint112 最大值的那部分资产。
2.8 处理非标准和不常用代币
标准的 ERC20 代币合约在转移代币后需要返回一个布尔值来表示转移是否成功,但并非所有代币都会这么做。有些代币没有返回值。在 UniswapV1 中,对于没有返回值的代币转移默认视为失败,整个交易会被重置。而在 Uniswap V2 中,没有返回值的代币转移被视为成功。
此外,UniswapV1 假设代币转移不会触发交易对的重入,但支持 ERC777 hooks 的一些 ERC20 代币打破了这一假设。为了支持这些代币,UniswapV2 在所有公共状态变量修改函数中增加了防重入锁功能,同时也阻止了 Flash Swaps 中用户自定义回调的重入。
2.9 流动性初始化设置
如果用户为一个已经存在的交易对 A/B 提供流动性,那么根据当前 A 和 B 的比例就可以计算出来要提供多少比例的 A 和 B 。但是在一个交易对初始化的时候,没有可以参考的比例,拿这个时候应该怎么处理呢?在 UniswapV1 的版本中,当新的流动性提供者将代币存入现有的 Uniswap 代币对时,将根据现有代币数量计算所铸造的流动性代币数量。具体的计算公式如下:
对于第一个提供流动性的人来说,公式当中的是Xstarting是 0。在面对这种情况,UniswapV1 采用的方法是初始流动性的数值直接等于初始提供的 ETH 数量,这种初始流动性的提供的问题在于交易对的价值完全由初始流动性的比例所决定的,但是问题在于,没有任何机制去担保这个比例就是符合真实价值的。在 UniswapV2 当中,流动性的初始化可以用如下的公式
这个公式的意思是 Sminted 这是你将获得的流动性代币的数量, Xdeposited 这是你存入的第一种代币的数量.例如,如果你存入的是 ETH,那 Xdeposited 就是你存入的 ETH 数量。 Ydeposited 这是你存入的第二种代币的数量。
例如,如果你存入的是 DAI,那 Ydeposited 就是你存入的 DAI 数量。这个公式可以保证流动资金池中的份额永远不会低于该池中的几何平均值,但是这个公式的值也会随着池子当中代币数量的变化而变化,为了减弱资金池中代币数量变化带来的影响,UniswapV2 销毁了最初的 1e- ¹⁵ 流动性,这个数值是最小流动性 1e-¹⁸ 的 1000 倍。尽管对于任何交易对来说这都微不足道,但却显著增加了利用这一机制获利的攻击者的成本。
2.10 WETH
由于交易以太坊原生货币 ETH 的接口与交易 ERC20 代币的接口不同,许多协议不直接支持 ETH,而是使用一种替代品 WETH(包装后的 ETH 代币)。UniswapV1 是个例外,因为它的交易对中直接包含了 ETH,允许用户直接使用 ETH 进行交易。然而,Uniswap V2 设计为支持任意 ERC20 代币之间的交易对,直接支持 ETH 会使系统复杂化且增加风险。
因此,UniswapV2 中不直接支持 ETH,用户在使用交易对前必须先将 ETH 转换成 WETH。实际上,UniswapV2 内部自动将用户提供的 ETH 转换为 WETH,这样简化了用户的操作,让他们无需手动转换 ETH 为 WETH。虽然对任何交易对来说这种转换是微不足道的,但它有效地提高了系统的安全性和操作简便性。
2.11 确定性交易对地址
无论是 UniswapV1 还是 UniswapV2,所有交易对都是通过单一的工厂(factory)合约创建的。在 UniswapV1 中,使用的是 create 操作码,交易对合约的地址会受到创建顺序的影响。而在 UniswapV2 中,采用了新的操作码 create2 ,这种方法生成的地址是确定的。这意味着可以在链下提前计算出交易对的地址,而不需要查询链上的状态。
2.12 最大代币数量
为了高效地实现预言机功能,Uniswap V2 采用 uint112 来保存代币数量,这意味着其最大支持的代币数量为 2¹¹² - 1 。对于精度为 18 的代币来说,这个数值是足够的,大约为 5192296858534828( 5.19e¹⁵)枚,即 5.19 千万亿枚。如果合约中的记录值超过了这个限制,交易将会失败并重置。正如之前提到的,任何人都可以使用 skim() 函数来恢复,通过移除流动性池中多余的资产来解决这一问题。
3. UniswapV2原理解析
Uniswap 是一种自动化流动性协议,由恒定的产品公式提供支持,并在以太坊区块链上的不可升级智能合约系统中实现。它消除了对可信中介机构的需求,优先考虑去中心化、抗审查和安全性。每个 Uniswap 智能合约或货币对都管理着一个由两个 ERC-20 代币储备组成的流动性池。任何人都可以通过存入每个标的代币的等值值来换取池代币,从而成为资金池的流动性提供者 (LP)。这些代币跟踪总储备中按比例分配的 LP 份额,并且可以随时赎回标的资产。
首先先介绍 Uniswap 的自动做市商机制,下面展示了 UniswapV2 自动做市商(AMM)模型的函数图像,是基于如下公式的:
其中,x 代表 Token A 的数量,y 代表 Token B 的数量,k 是一个常数,表示池中两种代币数量的乘积保持不变。交易者在 Uniswap 上进行交易时,通过向池中添加或移除代币,改变了池中代币的数量。根据恒定乘积公式,另一种代币的数量会相应变化,以保持乘积 k 不变。这种变化决定了交易的价格。
例如,如果交易者希望用 Token A 交换 Token B,他们需要向池中增加一定数量的 Token A,这会导致池中的 Token B 数量减少,从而改变价格。交易者的操作会沿着这条曲线移动,改变代币的数量和价格。曲线上任意一点都满足恒定乘积关系。
UniswapV2 的工作原理大概可以分成三个部分,流动性提供者( Liquidity Provider),Uniswap 池(Uniswap Pool),交易者(Trader)。流动性提供者的作用是流动性提供者将两种代币(例如,Token A 和 Token B)存入 Uniswap 池中。
图中显示的例子中,流动性提供者存入了 10 个 Token A 和 1 个 Token B;Uniswap 池中储备了各种代币,例如图中显示的 100 个 Token A 和 10 个 Token B。
池中的流动性份额由流动性代币表示,图中显示共有 12 个流动性代币;交易者交易者可以向池中提交代币并交换他们需要的另一种代币。例如,交易者可以存入 10 个 Token A,并支付 0.3% 的手续费,从池中获取 1 个 Token B。
我们先看流动性提供者 (LP) 是如何提供流通性的。如下图所示,流动性提供者将代币存入 Uniswap 池中,增加流动性。
例如,在图中,流动性提供者存入了 3 个 Token A 和 1 个 Token B 。当流动性提供者存入代币后,他们会收到代表其流动性份额的池代币( Pool Tokens )。在图中,流动性提供者获得了 12.4 个池代币。池中的代币储备会增加,例如,图中池中的代币储备变为 1210 个 Token A 和 399 个 Token B 。更多的流动性有助于降低价格滑点,使交易更加稳定。
Uniswap 使用的恒定乘积公式 x · y = k 来确定价格曲线。增加的流动性扩展了低滑点区域,提升了交易的价格稳定性。流动性提供者通过存入代币来增加池中的流动性,并获得相应的流动性代币作为回报。这不仅帮助了交易者获得更稳定的价格,也为流动性提供者带来了交易手续费的收益。
接下来,我们从交易者的视角去看,交易者是如何换取代币以及交易行为会对 Uniswap Pool 产生什么影响。
如下图所示,交易者希望在 Uniswap 上交换代币。例如,在图中,交易者打算交换3个 Token A 。交易者输入 3 个 Token A ,并支付 0.3% 的手续费。最终,交易者将获得约 0.997 个 Token B 作为输出。交易会改变池中的储备平衡,从而导致新的价格。在交易前,池中有 1200 个 Token A 和 400 个 Token B 。
根据恒定乘积公式 x·y = k 交易后的池中将有大约 1203.009 个 Token A 和大约 399.003 个 Token B 。Uniswap 使用 x·y = k 的恒定乘积公式来定义价格曲线。随着交易者的交换操作,池中的代币数量变化,价格曲线也随之调整,确定新的价格。
4. 源码分析
4.1 核心操作流程图解析
在这一节,我们会介绍在 UniswapV2 当中最常用的三个操作,即添加流动性,撤除流动性,交换代币。我们会通过流程图来分析他们调用的合约以及调用的函数,来更加深刻的理解 UniswapV2 的源码。
4.1.1 添加流动性
用户在添加流动性的时候,用户首先调用 UniswapV2Router.sol 合约,提供 Token A 和 Token B 的数量,UniswapV2Router.sol 合约的 addLiquidity 函数接收用户的请求并进行处理。
addLiquidity 函数进一步调用 UniswapV2Pair.sol 合约,在 UniswapV2Pair.sol 合约中,调用 mint 函数执行实际的流动性添加操作,mint 函数根据用户提供的 Token A 和 Token B 的数量,计算应铸造的流动性代币(LP 代币)的数量,并将这些 LP 代币分配给用户,流动性添加操作完成后,mint 函数调用 _update 函数更新储备量。
4.1.2 交换代币
用户想要交换代币的时候,首先调用 UniswapV2Router.sol 合约,提供输入代币数量和最小输出代币数量。
然后 UniswapV2Router 合约的 swapExactTokensForTokens 函数接收用户的请求并进行处理,swapExactTokensForTokens 函数进一步调用 UniswapV2Pair.sol 合约,在 UniswapV2Pair.sol 合约中,调用 swap 函数执行实际的代币交换, swap 函数根据输入代币数量和储备量,计算应输出的代币数量,并执行交换,交换完成后,swap 函数调用 _update 函数更新储备量和累计手续费。具体的流程如下所示:
4.1.3 撤除流动性
用户首先调用 UniswapV2Router.sol 合约,提供要撤出 LP 代币的数量,UniswapV2Router.sol 合约的 removeLiquidity 函数接收用户的请求并进行处理,removeLiquidity 函数进一步调用 UniswapV2Pair.sol 合约,在 UniswapV2Pair.sol 合约中,调用 burn 函数执行实际的流动性撤出操作,burn 函数根据提供的 LP 代币数量,计算应返还的 Token A 和 Token B 数量,并将这些代币返还给用户。
4.2 Core 合约
UniswapV2 Core 合约是去中心化交易平台 Uniswap 的核心部分,负责实现其自动化做市商( AMM )功能。与传统订单簿不同,Uniswap 通过流动性池和恒定乘积公式 x·y = k 来实现交易。流动性提供者将两种代币存入池中,获得流动性代币作为凭证。用户在进行交易时,合约根据池中代币数量和恒定乘积公式计算交易价格。UniswapV2 引入了多项改进,包括 ERC20 pairs 直接交易,价格预言机的改进, 闪电贷以及协议费的调整。core 合约当中的核心组件包括下面三个文件:
UniswapV2Pair.sol :管理每个交易对的流动性池,处理代币交换、流动性添加和移除
UniswapV2Factory.sol :负责创建和管理交易对
UniswapV2ERC20.sol :流动性代币的标准实现,代表流动性提供者的份额
4.2.1 UniswapV2 Factory.sol
UniswapV2Factory 合约的作用是负责创建和管理交易对(流动性池)。该合约允许用户创建新的交易对,并记录所有创建的交易对。此外,它还管理交易费接收地址和设置者地址。UniswapV2Factory.sol 有五个函数,分别来看一下
constructor 函数:构造函数,用于初始化 UniswapV2Factory 合约。输入是交易费设置者地址 _feeToSetter , 输出是无。
allPairsLength 函数:返回所有创建的交易对的数量。 输入是无, 输出是所有交易对数量的 unit。
createPair 函数: 创建新的交易对。输入是 tokenA 和 tokenB 的两个代币地址,输出是创建的交易对地址 pair。
setFeeTo 函数: 设置交易费接受地址。 输入是新的交易费接受地址 _feeTo, 输出是无。
setFeeToSetter 函数: 设置新的交易费设置者地址。输入是新的交易费设置者地址 _feeToSetter, 输出是无。
具体的代码解析如下:
createPair 函数
createPair 函数的作用是创建一个以 TokenA 和 TokenB 的交易对,在前端输入 TokenA 和 TokenB 之后,会先检查 TokenA 和 TokenB 是否是同一个币种,之后会对 TokenA 和 TokenB 做一个简单的排序,之后是检查 Token0 的地址,要求 Token0 的地址不能是 0;
之后是通过 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS') ; 来检查这个代币对是否存在,只有存在了,才可以进行下去;后面通过 creationCode 来获取 UniswapV2Pair 合约的创建字节码;
之后使用 token0 和 token1 的哈希值作为盐值,确保每个代币对的地址是唯一的,因为如果代币对的地址不唯一,那么交易者添加流动性可能会添加到错误的池子当中;之后使用内联汇编的 create2 指令创建合约,保证合约地址的唯一性和可预测性;
之后便是初始化新创建的代币对合约,然后更新映射表,记录这个代币对合约的地址,然后将新创建的代币对合约地址添加到所有代币对的列表中,最后是触发 PairCreated 事件,通知外部有新的代币对创建。
4.2.2 UniswapV2 ERC20.sol
UniswapV2ERC20.sol 的主要功能是实现 ERC-20 代币,它实现了 ERC20 标准的代币功能,专门用于 UniswapV2 流动性池。合约包含铸造( mint )、销毁( burn )、批准( approve )和转移( transfer )等基本操作。此外,它还支持 permit 功能,允许使用签名来批准代币转移。我们来逐个看他包含的函数:
constructor 函数:初始化合约,设置 DOMAIN_SEPARATOR 用于 permit 功能。输入无,输出无。
_mint 函数:铸造新的代币,输入是接收地址“ to ”,和铸造数量“ value ”,输出无
_burn 函数: 销毁代币,输入是销毁地址 from 和销毁数量 value,输出无。
_approve 函数: 批准代币转移,所有者地址 owner,批准地址 spender 和批准数量 value,输出无。
_transfer 函数:转移代币, 输入是转出地址 from,接收地址 to 和转移数量 value, 输出无。
approve 函数:公开的批准函数,作用是调用 _approve 函数,输入是approve 函数公开的批准函数, 输出是返回布尔值 true 表示操作成功。
transfer 函数:作用是调用 _transfer 函数,输入是接受地址 to 和转移数量 value, 输出是返回布尔值 true 表示操作成功
transferFrom 函数:公开的授权转移函数。输入是转出地址 from,接收地址 to 和 转移数量 value,输出是 返回布尔值 true 表示操作成功
permit 函数: 使用签名来批准代币转移,验证签名并调用 _approve 函数,输入是所有者地址 owner,批准地址 spender,批准数量 value,截止时间 deadline,签名参数 v、r、s,输出无。
官方的源码解析如下所示:
4.2.3 UniswapV2 Pair.sol
UniswapV2Pair 即交易对合约,实现了 Uniswap v2 的核心功能,即管理和操作每个交易对的流动性池。该合约负责处理代币的交换、流动性的添加和移除,以及价格的累积计算。它确保在每次交易后,交易对的储备和价格信息得到更新,并触发相应的事件通知。UniswapV2Pair.sol 中有 11 个函数,具体入下面表格所示:
官方的 UniswapV2Pair.sol 的代码和注释如下:
UniswapV2Pair 是继承 IUniswapV2Pair, UniswapV2ERC20,首先看看 IUniswapV2Pair 的源码,看看 IUniswapV2Pair 如何定义接口:
之后定义了全局变量和修饰器
上面的 MINIMUM_LIQUIDITY 是一个常量,它设定了流动性池中必须保留的最小流动性代币数量,以确保流动性提供者在任何时候都至少保留一定量的代币,从而避免流动性枯竭,数值是 10 的3次方,在提供初始流动性时会被燃烧掉; SELECTOR 存储的是代币转账函数的 ABI(应用程序二进制接口)选择器,它用于在智能合约中准确地识别和调用其他合约的 transfer 函数,确保在执行代币转移时使用正确的函数签名;
factory 用于存储交易对合约的Uniswap V2工厂合约的地址,Token0,Token1 用于存储代币地址,reserve0, reserve1 和 blockTimestampLast 这三个状态变量记录了最新的恒定乘积中两种资产的数量和交易时的区块(创建)时间;
而 price0CumulativeLast 和 price1CumulativeLast 变量用于记录交易对中两种价格的累计值,kLast 用于跟踪 UniswapV2 交易对中两种代币储备量乘积的最近状态,作为一个关键参数来维持流动性池的价格稳定性和计算交易费用,主要用于团队手续费的计算。
下面这一段修饰器是提供了一种锁机制来防止重入攻击,具体代码解析如下:
上面的代码当中的 _; 表示被修饰的函数体,这段代码的大致逻辑是:定义了一个 lock 修饰符,它通过改变 unlocked 变量的状态来确保在执行被修饰的函数期间合约不会被重新进入,从而防止重入攻击和竞态条件。
下面的 getReserves 函数的作用是提供一种方式来公开查询并返回 UniswapV2 交易对合约当前的两种代币储备量和最后更新时间戳的信息。
_safeTransfer 函数的作用是在智能合约内部执行代币的转移操作,并检查转移是否成功,如果失败则抛出异常,确保了合约的安全性和代币转移的可靠性,下面是这一段代码的详细注释:
下面的构造函数只是简单的用于初始化 factory:
initialize 函数的作用是设置交易对合约所涉及的两种代币的地址,且只能由部署交易对的工厂合约(factory) 来调用,确保交易对的初始化过程是安全和受控的。
_update 函数的主要作用是确保交易对合约的储备量和价格累积器能够反映最新的状态, 具体实现的方法是通过比较当前区块的时间戳和上次更新的时间戳。_update 函数的四个输入参数分别是:balance0 和 balance1 ,表示交易对中两种代币当前的余额;_reserve0 和 _reserve1 ,表示函数调用前两种代币的储备量。我们接下来用bullet point的方式来讲解一下 _update 函数是如何实现的:
检查余额值是否可能导致溢出:通过 require语句确保传入的 balance0 和 balance1 不会超过 uint112 的最大值,这是因为 reserve0 和 reserve1 在存储时使用 uint112 类型,需要保证数据类型转换的安全性。
记录当前区块时间:获取当前区块的时间戳,并将其与 blockTimestampLast 进行模2^32运算,得到 blockTimestamp 。这个操作是因为以太坊的区块时间戳是32位的,而且我们只关心在一个区块内的时间差,而不是绝对时间。
计算时间差:计算当前区块时间与上次更新时间的差值 timeElapsed 。如果 timeElapsed 为0,表示这是同一区块内的连续调用,因此不会更新价格累积值。
价格累积更新:如果时间差大于0,并且储备量不为0,使用固定点数学库 UQ112x112 来计算价格比例,并更新 price0CumulativeLast 和 price1CumulativeLast 。这里的“never overflows”意味着由于时间间隔 timeElapsed 是 uint32 类型,与价格累积值(uint224)相乘不会导致溢出。“+ overflow is desired”指的是价格累积值允许溢出,因为价格计算使用的是变化量(delta)而不是绝对值,即使有溢出,计算平均价格时使用的变化量仍然是准确的。
更新储备量:将新的余额赋值给 reserve0 和 reserve1 ,更新流动性池的储备量。
更新时间戳:将当前区块时间戳赋值给 blockTimestampLast ,为下一次更新做准备。
触发同步事件:通过 emit 关键字发出 Sync 事件,告知外部监听者储备量已经更新。
这种设计允许 UniswapV2 在处理大量交易时保持价格的连续性和准确性,即使在区块时间戳或价格累积值可能溢出的情况下,依然能够通过变化量来准确计算出平均交易价格。这是通过巧妙地利用固定点数学和时间差分来实现的。
在 UniswapV2 中,用户每笔交易会被收取0.3%的手续费。这笔手续费中的六分之一将分配给开发团队,而剩下的六分之五将作为奖励给予流动性提供者。然而,如果每次交易都计算一次手续费,这将不可避免地增加用户的 Gas 费用。
因此,在UniswapV2中,手续费会被累积起来,只有在流动性发生变化时才会对手续费进行分配。_mintFee函数首先检查是否开启了交易费用,并确定费用接收地址。如果交易费用未开启,且之前有铸造过费用(_kLast不为0),则重置 kLast 值。这种费用铸造机制是 UniswapV2 的一部分,用于为流动性提供者提供额外的激励;如果交易费用开启,则根据下面的公式来计算手续费的值,
Sₘ 表示应该铸造的手续费流动性代币数量,k₁表示上一个流动性事件后的储备的乘积k,k₂ 表示当前的储备乘积k,S₁表示上一个流动性事件后的总流动性代币供应量。
4.3 Periphery 合约
UniswapV2 周边合约的主要作用是作为外部账户与核心合约之间的桥梁,包含接口定义、工具类库、Router 和示例实现四部分内容。
4.3.1 Libraries
Libraries 文件夹下面包含了四个文件
我们接下来分别对这四个sol文件进行详细的解析
SafeMath.sol
SafeMath.sol 用于执行溢出安全的数学运算,这种安全数学运算对于避免整数溢出和下溢错误非常重要,特别是在区块链和智能合约开发中。主要是包含三个函数。
add 函数:用于安全地执行两个无符号整数的加法运算
sub 函数:用于安全地执行两个无符号整数的减法运算
mul函数:用于安全地执行两个无符号整数的乘法运算
具体的代码注释如下
UniswapV2Library.sol
UniswapV2Library.sol 提供了一些实用函数,用于与 Uniswap v2 交换对 (pairs) 进行交互和操作。
这些函数主要用于计算交易路径、获取储备、计算价格和执行链式计算。该库使用了一个名为 SafeMath 的库来确保数学运算的安全性,避免整数溢出和下溢。 UniswapV2Library.sol 这个文件包含了八个函数:
sortTokens 函数:返回按地址排序的两个代币地址。输入是两个代币地址 tokenA 和 tokenB。输出是排序后的代币地址 token0 和 token1。
pairFor 函数:计算出给定工厂地址和两个代币地址的对的地址,而无需进行外部调用。输入是工厂地址 factory, 两个代币地址 tokenA 和 tokenB; 输出是该对的地址 pair
getReserves 函数:获取并排序一个对的储备。输入是工厂地址 factory,两个代币地址 tokenA 和 tokenB; 输出是两个代币的储备量 reserveA 和 reserve B
quote函数:根据给定资产数量和对的储备,返回另一个资产的等价数量。输入:资产数量 amountA ,储备 reserveA 和 reserveB 。输出是另外一个资产的数量 amountB。
getAmountOut函数: 根据输入资产数量和对的储备,返回另一个资产的最大输出数量。 输入是输入资产数量 amountIn,储备 reserveIn 和 reserveOut; 输出是 输出资产的数量 amountOut。
getAmountIn函数:根据输出资产数量和对的储备,返回所需输入的另一个资产的数量。输入是输出资产数量 amountOut,储备 reserveIn 和 reserveOut。
getAmountsOut函数:在任意数量的对上执行链式 getAmountOut 计算,输入是工厂地址 factory ,输入资产数量 amountIn ,路径 path;输出是每个路径节点的资产数量数组 amounts。
getAmountsIn函数:在任意数量的对上执行链式 getAmountIn 计算,输入是工厂地址 factory,输出资产数量 amountOut,路径 path ; 输出是每个路径节点的资产数量数组 amounts
UniswapV2Libray 源码详细注释如下:
UniswapV2OracleLibrary.sol
UniswapV2OracleLibrary.sol 文件,提供了一些辅助方法,用于与预言机计算平均价格相关的操作。该库包括获取当前区块时间戳和计算累积价格的方法,帮助节省gas费用和避免频繁的同步调用。包含了两个函数,具体如下:
UniswapV2OracleLibrary.sol 官方的源码详细注释如下:
UniswapV2LiquidityMathLibrary.sol
UniswapV2LiquidityMathLibrary.sol 的官方源码如下
computeProfitMaximizingTrade 函数:计算实现利润最大化的交易方向和大小。
getReservesAfterArbitrage 函数:在观察到的“真实价格”下,通过套利交易后获取流动性池的储备量。
computeLiquidityValue 函数:计算流动性价值,给定流动性池的所有参数。
getLiquidityValue 函数:获取当前所有参数,并计算流动性金额的价值。
getLiquidityValueAfterArbitrageToPrice 函数:给定两种代币和它们的“真实价格”,以及流动性金额,返回流动性的价值。
UniswapV2Router02.sol
我们先看看 UniswapV2Router02 的源码,这一部分的代码可以大致分成六个大的部分,构造函数和修饰符, 接受ETH的函数, 添加流动性,移除流动性, 代币交换, 库函数。
构造函数和修饰符
constructor(address _factory, address _WETH): 初始化工厂合约地址和WETH合约地址。
modifier ensure(uint deadline): 确保交易在截止时间之前完成。
接收ETH的函数
添加流动性
_addLiquidity: 内部函数,用于添加流动性,根据现有储备量计算最优的代币数量。
addLiquidity: 添加两种ERC-20代币的流动性。
addLiquidityETH: 添加ERC-20代币和ETH的流动性。
移除流动性
removeLiquidity: 移除两种ERC-20代币的流动性。
removeLiquidityETH: 移除ERC-20代币和ETH的流动性。
removeLiquidityWithPermit: 带许可的移除两种ERC-20代币的流动性。
removeLiquidityETHWithPermit: 带许可的移除ERC-20代币和ETH的流动性。
removeLiquidityETHSupportingFeeOnTransferTokens: 移除ERC-20代币和ETH的流动性,支持手续费代币。
removeLiquidityETHWithPermitSupportingFeeOnTransferTokens: 带许可的移除ERC-20代币和ETH的流动性,支持手续费代币。
代币交换
_swap: 内部函数,执行代币交换逻辑。
swapExactTokensForTokens: 使用确切数量的代币交换另一种代币。
swapTokensForExactTokens: 使用代币交换确切数量的另一种代币。
swapExactETHForTokens: 使用确切数量的ETH交换代币。
swapTokensForExactETH: 使用代币交换确切数量的ETH。
swapExactTokensForETH: 使用确切数量的代币交换ETH。
swapETHForExactTokens: 使用ETH交换确切数量的代币。
swapExactTokensForTokensSupportingFeeOnTransferTokens: 使用确切数量的代币交换另一种代币,支持手续费代币。
swapExactETHForTokensSupportingFeeOnTransferTokens: 使用确切数量的ETH交换代币,支持手续费代币。
swapExactTokensForETHSupportingFeeOnTransferTokens: 使用确切数量的代币交换ETH,支持手续费代币。
库函数
quote: 根据储备量计算给定数量代币A对应的代币B数量。
getAmountOut: 计算给定输入数量和储备量的情况下可以获得的输出数量。
getAmountIn: 计算给定输出数量和储备量的情况下需要的输入数量。
getAmountsOut: 根据路径和输入数量计算输出数量。
getAmountsIn: 根据路径和输出数量计算输入数量。
具体我们下面逐个函数的去看,UniswapV2Router02.sol 的合约通过下面这一段代码继承自 IUniswapV2Router02,
UniswapV2Router02 合约实现了 IUniswapV2Router02 接口,提供了以下关键功能:
添加流动性:允许用户向流动性池中添加两种代币,以换取流动性提供者代币。
移除流动性:允许流动性提供者将他们的流动性提供者代币兑换回两种代币。
交易:允许用户在不同代币之间进行交易,支持直接交易和通过多个交易对的路径交易。
报价计算:提供了一系列函数,用于计算给定输入或输出量时的交易细节。
ensure 修饰器用于检查当前区块时间是否超过最迟交易时间,确保用户指定的交易不会因超时而失败,有助于提高交易的安全性和可靠性。
构造函数初始化了工程合约地址和 weth 合约地址,这两个地址在整个合约的生命周期中都是不变的。
receive 函数通常用于允许合约直接接收以太币,而不是通过函数调用。在这个特定的例子中,receive 函数通过 assert 语句确保只有 WETH 合约可以向其发送以太币。
_addLiquidity 函数的目的是计算在添加流动性时用户需要存入的两种代币的最优数量。这个函数的接受有六个参数,tokenA 和 tokenB 是要添加流动性的两种代币的地址,amountADesired 和 amountBDesired 是用户希望添加的两种代币的初始数量,amountAMin 和 amountBMin 是用户能接受的最小添加数量,用于防止滑点过低,函数返回两个参数:amountA 和 amountB,即实际添加的两种代币的数量。
这个计算考虑了流动性池中已有的代币储备量,以确保用户添加的流动性是平衡的。如果流动性池是新的,没有现有的储备,用户可以直接添加他们希望的数量。如果池中已经有储备,函数将计算基于当前比例的最优添加数量,确保流动性添加后池中的代币比例保持不变。
addLiquidity 的公共函数,用于向指定的两种代币( tokenA 和 tokenB )的流动性池添加流动性
addLiquidityETH 函数允许用户向 UniswapV2 流动性池添加非以太币代币和以太币,以换取相应的流动性代币,这个函数有六个接受的参数,token 是要添加流动性的非以太币代币的地址,amountTokenDesired 是用户想要添加的非以太币代币的数量,amountTokenMin 是用户能接受的最小非以太币添加数量,用于防止滑点过低,amountETHMin 是以太币的最小添加数量,同样用于防止滑点过低,to是接收新铸造的流动性代币的地址,deadline是交易的截止时间,用于防止交易超时。
函数返回三个参数:amountToken 和 amountETH 是实际添加到流动性池的非以太币和以太币的数量,liquidity 是新铸造的流动性代币的数量。
removeLiquidity 函数允许用户从流动性池中移除他们之前添加的流动性,并按照比例获得两种代币。burn 函数是 IUniswapV2Pair 流动性池合约中的一个底层函数,用于执行实际的流动性销毁和代币分配操作。
removeLiquidityETH 函数允许用户从 UniswapV2 流动性池中移除他们之前与以太币一起添加的流动性,并分别获得相应的非以太币和以太币。函数首先调用 removeLiquidity ,之后通过 safeTransfer 提取对应的 token ,然后将燃烧流动性提取的 WETH 换成 ETH ,之后将兑换的 ETH 转给接受者。
removeLiquidityWithPermit 函数使用户能够通过 ECDSA 签名进行授权,从而允许合约代表用户移除流动性,而无需用户先前通过 approve 函数进行授权。
removeLiquidityETHWithPermit 函数结合了 removeLiquidityETH 和 permit 授权机制,允许用户通过签名一次性授权合约移除流动性,而不需要使用标准的 approve 模式。这在提供更好的用户体验和安全性的同时,也减少了交易的 gas 成本。
removeLiquidityETHSupportingFeeOnTransferTokens 函数允许用户从 UniswapV2 流动性池中移除他们之前添加的与特定代币和以太币的流动性,同时考虑到某些代币在转账时可能会收取费用。
函数首先调用 removeLiquidity 来执行流动性的移除,然后处理代币的转移,确保用户获得他们应得的代币数量,最后将 WETH 转换回以太币并转移给用户。整个过程需要在用户指定的交易截止时间之前完成。
removeLiquidityETHWithPermitSupportingFeeOnTransferTokens 函数结合了 removeLiquidityETHSupportingFeeOnTransferTokens 和 permit 授权机制,允许用户通过签名一次性授权合约移除流动性,同时处理可能在转账时收取费用的代币。
这种方法提供了一种无需使用 approve 模式即可授权合约操作用户资产的方式,从而减少了 gas 成本并提高了用户体验。
_swap 函数是一个内部函数,用于执行一系列代币交换操作。它按照指定的路径和数量,从一个代币交换到另一个代币,直到达到最终的代币。这个函数是流动性池交互的核心部分,用于实现代币的转换和流动性池的更新。
swapExactTokensForTokens 函数是 UniswapV2 Router 的一个关键功能,允许用户以一个确切的输入量来交换至少一个最小输出量的代币。这个函数首先计算出整个交换路径的输出量,然后确保最终的输出量满足用户的最小要求,接着安全地从用户转移代币到流动性池,并执行交换操作。
swapTokensForExactTokens 函数允许用户指定他们希望获得的代币数量,并提供不超过最大值的代币以进行交换。这个函数首先计算出为了获得 amountOut 所需的最大输入量,然后确保这个输入量不超过用户指定的 amountInMax 。
swapExactETHForTokens 函数允许用户以确切数量的 ETH 交换至少一定数量的另一种代币。这个函数首先验证交换路径是否有效,然后计算所需的 WETH 数量,将ETH 存入 WETH 合约,然后执行交换操作,并将交换得到的代币发送到用户指定的地址。整个过程需要在用户指定的交易截止时间之前完成。
swapExactETHForTokens 函数的作用是用于卖出制定数量的 ETH 换取其他 Token 。首先进行路径有效检查,确保 path 数组的第一个元素是 WETH 地址,因为 Uniswap 交易对为 ERC20/ERC20 交易对;
接下来是计算输出金额,使用 UniswapV2Library.getAmountsOut 函数,根据用户发送的 ETH 数量 msg.value 和代币路径 path ,计算出用户能够获得的每种代币的数量,并将结果存储在 amounts 数组中;接下来是做最小输出验证,函数检查 amounts 数组中最后一个元素(即目标代币的数量)是否满足用户设定的最小输出数量 amountOutMin 。
如果不满足,将抛出一个错误;接下来是调用 WETH 代币合约的 deposit 函数,将 msg.value 的 ETH 存入 WETH 合约,断言 WETH 合约向指定的交易对合约地址转移 amounts[0] 数量的 WETH,如果失败则交易回滚,最后是调用内部函数 _swap ,执行实际的代币交换过程。
swapTokensForExactETH 是卖出其他 Token 换取一定数量 ETH 的过程,函数接收五个参数,包括期望获得的 ETH 数量 amountOut ,用户愿意提供的最大代币数量 amountInMax ,代币交换路径 path ,接收 ETH 的地址 to ,以及交易的截止时间 deadline;
首先是路径有效性检查,确保交换路径的最后一个元素是 WETH 地址,如果不是则抛出异常;接下来是输入金额计算, 使用 UniswapV2Library.getAmountsIn 函数计算为了获得 amountOut 数量的 ETH ,用户需要提供的代币数量;确保计算出的需要提供的代币数量不超过用户设定的最大数量,如果不是则抛出异常;使用 TransferHelper 的 safeTransferFrom 函数,从 msg.sender 地址安全转移代币到流动性池;
接下来调用内部函数 _swap ,执行实际的代币交换过程;调用 WETH 合约的 withdraw 函数,将交换得到的 WETH 转换回 ETH;最后使用 TransferHelper 的 safeTransferETH 函数,将获得的 ETH 安全转移到用户指定的地址。
swapExactTokensForETH 函数的作用是用于以固定数量的代币交换至少 amountOutMin 数量的 ETH,函数接收五个参数,包括用户愿意提供的代币数量 amountIn ,期望获得的最小 ETH 数量 amountOutMin ,代币交换路径 path ,接收 ETH 的地址 to ,以及交易的截止时间 deadline;函数的实现逻辑和 swapTokensForExactETH 的逻辑很像。
swapETHForExactTokens 的函数,它允许用户使用 ETH 交换固定数量的代币,实现逻辑和上面的 swapExactTokensForETH,swapTokensForExactETH 类似。
_swapSupportingFeeOnTransferTokens 函数支持交易费用的代币交换的逻辑,通过遍历代币路径,计算每个流动性池中需要交换的代币数量,并执行交换操作。
swapExactTokensForTokensSupportingFeeOnTransferTokens 固定数量的代币精确兑换至少期望数量的另一种代币,同时处理了交易费用代币的情况。
swapExactETHForTokensSupportingFeeOnTransferTokens 实现了用特定数量的 ETH 去换取其他 Token
swapExactTokensForETHSupportingFeeOnTransferTokens 函数实现了用户能够以固定数量的代币精确兑换至少期望数量的 ETH,同时处理了交易费用代币的情况,并确保了 ETH 的准确转移。
quote 函数实现了根据用户指定的代币数量和流动性池中两种代币的储备量,计算并返回用户能够交换得到的另一种代币数量的功能。
getAmountOut 函数和 getAmountsOut 函数的计算原理都是恒定乘积算法。定义函数 getAmountOut ,用于计算给定输入金额和储备量后,用户可以得到的输出金额;定义函数 getAmountsOut ,用于计算给定输入金额和交换路径后,用户可以得到的一系列输出金额。
getAmountIn 和 getAmountsIn 的计算原理是根据恒定乘积算法,实现了制定买进资产数量的情况下,计算卖出资产的数量。
4.3.2 UniswapV2 Migrator.sol
UniswapV2Migrator.sol 用于将流动性从 Uniswap v1 迁移到 Uniswap v2。该合约包括接收 ETH 的能力,并使用 Uniswap v1 和 v2 的路由器和交换合约进行迁移操作。它确保用户可以安全地将他们在 Uniswap v1 中的流动性转移到 Uniswap v2 中。具体有下面三个函数,每个函数的具体功能如下:
constructor 函数:构造函数,用于初始化 UniswapV2Migrator 合约。输入是 v1 工厂地址 _factoryV1 和 v2 路由器地址 _router; 输出无
receive 函数:接收 ETH,允许合约接收来自任何 v1 交换和路由器的 ETH。输入无,输出无。
migrate 函数:输入是代币地址 token、最小代币数量 amountTokenMin、最小 ETH 数量 amountETHMin、接收地址 to、截止时间 deadline;输出是无
UniswapV2Migrator 的官方源码详细解析如下:
4.3.3 Interfaces 文件夹
interfaces 文件夹包含了用于与 UniswapV1 和 V2 交换、路由器、工厂、以及 WETH 和 ERC20 合约进行交互的接口定义。主要功能包括管理和迁移流动性、处理代币交易、添加和移除流动性、以及包装和解包 ETH。这些接口确保合约间的标准化交互。我们具体来看每个文件的内容。
IUniswapV1Exchange.sol
这个文件定义了一个接口 IUniswapV1Exchange,用于与 UniswapV1 交换合约进行交互。交换合约主要负责处理代币与 ETH 之间的交易和流动性管理。包含如下四个函数:
balanceOf 函数:返回某地址在交换合约中的余额。
transferFrom 函数:从一个地址转移代币到另一个地址。
tokenToEthSwapInput 函数:用于将代币交换为以太币。
ethToTokenSwapInput 函数:将以太币交换为代币。
IUniswapV1Factory.sol
IUniswapV1Factory.sol 它用于与 UniswapV1 的工厂合约进行交互。具体的代码解析如下:
IERC20.sol
IERC20.sol 定义了一个名为 IERC20 的接口,它遵循了以太坊代币标准( ERC-20 )