Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Uniswap V2:储备、兑换与流动性份额

Uniswap V2 是采用恒定乘积模型的自动做市商(AMM)。交易者与资金池兑换资产,流动性提供者向池中存入两种代币,获得代表池中份额的 LP 代币。

与 V1 的 ETH/代币池不同,V2 可以直接建立两种 ERC-20 代币的交易对。涉及原生 ETH 时,路由器通常先将其包装为 WETH。原始 V2 的费用、储备及路由规则与后续版本或分叉协议可能不同。

合约如何分工

合约职责
Factory创建交易对,并记录两种代币对应的 Pair 地址
Pair持有两种代币,记录储备,执行兑换、流动性铸造和赎回;自身也是 LP 代币合约
Router计算输入输出,沿调用者指定的兑换路径组织转账和 Pair 调用,并检查用户设定的限制

Factory 与 Pair 属于核心合约,Router 属于外围合约。用户通常通过 Router 交互,但 Pair 也可以被直接调用;直接调用时,需要自己处理转账顺序、最小输出和其他保护条件。

Pair 中的 reserve0、reserve1 是记录的储备,代币的 balanceOf(pair) 是实际余额。两者在正常结算后相符,但直接转账、代币增减余额等情况可以造成差异。兑换和流动性操作会根据实际余额识别收到的资产,并更新储备。

恒定乘积决定兑换数量

设池中 A、B 的储备分别为 x、y。忽略手续费时,兑换前后满足:

\[x \cdot y = k\]

这里的“恒定”描述一次不含手续费、也不增减流动性的兑换约束,不是池子生命周期中永远固定的数值。收取手续费或增减流动性都会改变储备乘积。

当 x、y 以完整代币数量表示时,y / x 是池内 A 的瞬时边际价格,单位为 B/A。若直接用合约中的原始整数储备计算,还需要调整两种代币的 decimals。这个价格也不等于一笔较大兑换的平均成交价格。

V2 对输入收取 0.3% 手续费。输入 Δx 个 A 时,用于报价的有效输入为 0.997 × Δx,输出 B 的数量为:

\[\Delta y = \frac{y \cdot 0.997\Delta x}{x + 0.997\Delta x}\]

例如,池中有 1000 A 和 1000 B,输入 100 A,输出约为 90.6611 B。成交后池中约有 1100 A 和 909.3389 B:全部输入都进入池子,手续费留在其中,因此实际储备乘积会增长。

Pair 最终检查的是扣除输入手续费后的余额乘积不低于原储备乘积。合约使用整数计算,输出需要向下取整,所以数学公式与实际结果可能有最小单位的差异。

输入越大,兑换过程中储备比例变化越大,平均成交价越偏离兑换前的边际价格,这称为价格影响。报价到成交之间还可能发生价格变化,用户通常用最小输出或最大输入限制可接受的滑点。

LP 代币代表池中份额

提供流动性时,需要区分首次建池和后续追加。以下公式中的数量都使用合约的原始整数单位,忽略取整之外的特殊代币行为。

首次存入 a 个 A 和 b 个 B 时,初始 LP 总量约为 sqrt(a × b)。原始 V2 将其中 MINIMUM_LIQUIDITY = 1000 个最小单位永久锁定在零地址,首次提供者实际获得:

\[L_{first} = \lfloor\sqrt{a b}\rfloor - 1000\]

这不是锁定 1000 个完整 LP 代币。首次存款必须足够大,使扣除锁定数量后的铸造量为正;首次储备比例也建立了池内初始价格,若明显偏离市场,会产生套利机会。

后续追加时,设现有储备为 x、y,铸造前 LP 总量为 S,实际存入 a、b,则新铸造数量为:

\[L_{added} = \min\left(\left\lfloor\frac{aS}{x}\right\rfloor,\left\lfloor\frac{bS}{y}\right\rfloor\right)\]

两种资产中较少的相对投入决定新增份额。Router 通常按池中比例选择转入金额;若直接向 Pair 不成比例地转入再铸造,多出的部分不会换来额外 LP 份额。

赎回时,持有者将 LP 代币交给 Pair 销毁,按其占总量的比例领取两种资产。赎回得到的是当时池中的资产组合,不保证与最初存入的两种数量相同。

手续费怎样进入 LP 收益

交易手续费留在池中,使每份 LP 对应的资产价值受益于交易活动。LP 通常在赎回份额时实现这部分收益,而不是每次兑换都收到一笔单独分红。

原始 V2 还支持可选的协议费用。启用时,协议通过在流动性事件中向 feeTo 铸造 LP 份额,分享约六分之一的手续费收益,相当于成交额的约 0.05%。这从原有 0.3% 中分出,不是再额外收取 0.05%。

是否开启协议费用,要检查对应 Factory 的 feeTo 配置。计算追加和赎回份额时,也要考虑协议费用铸造可能先改变 LP 总量,不能把所有部署都视为同一种费用状态。

价格累计值与 TWAP

V2 提供两种方向的价格累计值,供外部合约计算时间加权平均价格(TWAP)。每段时间的贡献是该段价格乘以持续时间,而不是简单地把价格相加:

\[C(t_2)-C(t_1)=\sum_i p_i\Delta t_i\]

\[TWAP=\frac{C(t_2)-C(t_1)}{t_2-t_1}\]

累计值在储备更新时按已过去的时间更新,使用更新前的储备比例。它不是每到一个新区块就自动执行一次;若当前时间还没有储备更新,读取方可以根据已有储备和时间补算当前累计值。

应用需要记录两个时间点的观测,选择窗口并处理精度,Pair 不会直接返回任意窗口的 TWAP。两种方向应分别计算:A/B 的平均价格通常不等于 B/A 平均价格的倒数。

TWAP 让操纵通常需要影响一段时间内的价格,较难依靠单个瞬时价格误导应用。但它没有消除操纵风险,窗口、流动性、观测方式和市场条件都会影响安全性,不能直接把池内价格视为可信的外部报价。

闪电兑换如何结算

闪电兑换允许接收方先拿到输出资产,在同一笔交易中使用,再支付输入资产。它与普通兑换使用同一个 swap(amount0Out, amount1Out, to, data) 入口;参数指定输出数量,输入由最终余额推算。

当 data 非空时,Pair 在转出资产后调用接收合约的 uniswapV2Call()。接收合约可以进行套利或其他操作,然后在回调结束前将应付代币转入 Pair。Pair 最后检查余额和手续费约束,不满足则整笔交易回滚。

归还同一种代币时,若取出 q,所需总归还量至少约为 q / 0.997,还需向上取整;支付另一种代币时,按池中储备和手续费计算输入。不存在“只归还原数量就完成借款”的通用规则。

不需要再次调用同一个 Pair 的 swap() 来还款,这还会受到 Pair 重入锁的阻止。回调合约应验证调用者确实是预期 Factory 创建的 Pair,避免被伪造回调诱导转账。

无常损失是相对持有的差额

当外部相对价格变化时,套利会推动池内储备比例调整。LP 赎回时的资产组合与单纯持有最初两种资产不同,因此需要比较这两种策略在同一价格下的价值。

设 A 相对 B 的价格从 p₀ 变为 p₁,r = p₁ / p₀。对初始等值投入的恒定乘积池,忽略手续费,假设套利使池内价格跟随外部价格,LP 相对持有的收益差额为:

\[\frac{V_{LP}}{V_{hold}}-1=\frac{2\sqrt{r}}{1+r}-1\]

当价格变为原来的 4 倍时,结果为 -20%,表示 LP 价值比持有策略低 20%。它不表示 LP 的资产绝对价值一定减少了 20%;若把“损失幅度”写成正数,对应的是 1 - 2sqrt(r)/(1+r)。

手续费收入可以抵消部分差额,但不能保证覆盖。价格回到初始比例时,上述理想模型中的差额会消失;如果已经退出或价格没有回来,不能凭“无常”这个名称认为损失一定会恢复。

使用时的边界

Pair 的重入锁保护其关键入口,不是所有外围合约和代币的整体安全保证。对 transfer() 不返回布尔值的兼容,也不意味着任意带转账税、重基准或恶意逻辑的代币都能正常兑换。

sync() 将记录的储备更新为实际余额,skim(to) 转出超过记录储备的余额。这是公开的余额协调函数,不是管理员专用的救援入口;直接向 Pair 转入资产,并不会自动建立属于发送者的可赎回存款。

V2 可以用于兑换、提供流动性、套利和价格观测,但这些用途各有条件。理解储备与余额、成交价与边际价、LP 份额与原始存款之间的区别,比把“恒定乘积”当作一个固定不变的等式更重要。

参考资料