解析 Uniswap V2 的交换函数

本文深入解析了Uniswap V2的swap函数设计,包括其逻辑、闪贷功能、安全性检查以及如何通过代码实现K值平衡。文章通过代码片段和详细的解释帮助开发者理解该函数的运行机制。

Uniswap V2 的交换功能巧妙设计,但是许多开发者在第一次接触时会发现其逻辑与直觉相悖。本文将深入解释它是如何工作的。

以下是重现的代码:

uniswap v2 swap function

诚然,这是一段代码长壁,但让我们逐步解析。

  • 在第 170-171 行(用黄色框标出),该函数直接转移交易者在函数参数中请求的代币数量。函数内部没有地方进行代币转入。 扫描代码,看看能否找到代币转入的地方,答案是不存在。但是这并不意味着我们可以简单地调用 swap 方法,任意提取我们想要的所有代币!

  • 我们能够立即移除代币的原因是可以进行闪电贷。当然,第 182 行的 require 语句(橙色箭头)要求我们连同利息偿还闪电贷。

  • 在函数的顶端,有一个注释说明该函数应从另一个智能合约中调用,该合约实现了重要的安全检查。这意味着此函数特别缺少安全检查(红色下划线)。我们想要确定这些是什么。

  • 变量 _reserve0 和 _reserve1(蓝色下划线)在第 161、176-177 和 182 行被读取,但该函数没有写入这些变量

  • 第 182 行(橙色箭头)并没有严格检查 X × Y = K。它检查 balance1Adjusted × balance2Adjusted ≥ K。这是唯一一个执行“有趣”操作的 require 语句。 其他的 require 语句则检查值是否为零或是否试图将代币发送到其自身的合约地址。

  • balance0 和 balance1 直接从成对合约的实际余额中使用 ERC20 balanceOf 读取

  • 第 172 行(在黄色框下面)仅在数据非空时执行,否则不执行

通过这些观察,我们将一次研究来自这个函数的一个特性。

闪电借贷

用户不必使用 swap 函数进行代币交易,它可以仅仅作为闪电贷使用。

uniswap v2 flash borrowing

借贷合约只是请求它们希望借到的代币数量 (A),无需抵押,这些代币将被转移到合约中 (B)。

与函数调用一起提供的数据作为函数参数传入 (C),这将被传递到实现

IUniswapV2Callee 的函数中。函数 uniswapV2Call 必须偿还闪电贷加上费用,否则交易将 revert。

Swap 需要使用智能合约

如果没有使用闪电贷,那么传入的代币必须作为调用 swap 函数的一部分发送。

应该明确的是,只有智能合约能够与 swap 函数交互,因为 EOA 无法在没有另一个智能合约的帮助下同时发送传入的 ERC20 代币和调用 swap。

测量传入代币的数量

Uniswap V2 “测量”发送的代币数量的方法在第 176 和 177 行中进行,如下图黄色框标记所示。

swap measure reserves and balances

请记住,_reserve0 和 _reserve1 在此函数内部未更新。它们反映了在新的代币集合作为 swap 的一部分发送之前合约的余额。

对于每一个成对代币会发生两种情况之一:

  1. 池子中某一个代币的数量净增加。
  2. 池子中某一个代币的数量净减少(或没有变化)。

代码通过以下逻辑来确定发生了哪种情况:

currentContractbalanceX > _reserveX - _amountXOut

// 或者

currentContractBalanceX > previousContractBalanceX - _amountXOut

如果它测量到净减少,三元运算符返回零,否则将测量代币的净流入。

amountXIn = balanceX - (_reserveX - amountXOut)

因为第 162 行的 require 语句,_reserveX 总是大于 amountXOut。

An image of the require statement

以下是一些例子。

  • 假设我们之前的余额是 10,amountOut 为零,当前余额是 12。这意味着用户存入了 2 个代币。amountXIn 将为 2。

  • 假设我们之前的余额是 10,amountOut 为 7,当前余额是 3。amountXIn 将为 0。

  • 假设我们之前的余额是 10,amountOut 为 7,当前余额是 2。amountXIn 仍然为零,而不是 -1。池子确实有 8 个代币的净损失,但 amountXIn 不能为负。

  • 假设我们之前的余额是 10,amountOut 为 6。如果当前余额是 18,则用户“借入” 6 个代币但偿还了 8 个代币。

结论:amount0In 和 amount1In 将在代币有净增益时反映其净增益,如果代币有净损失,则为零。

平衡 XY = K

现在我们知道用户发送了多少代币,来看一下如何强制执行 XY = K。

代码再次是

An image of code

Uniswap V2 每次 swap 收取 0.3% 的固定费用,这就是我们看到数字 1000 和 3 的原因,但让我们简化它,将其更改为 Uniswap V2 不收取费用的情况。这意味着我们可以去掉 .sub(amountXIn.mul(3)) 项,并且在第 180 到 181 行和第 182 行上的 1000 的平方都不会再乘。

新的代码将变为:

require(balance0 * balance1 >= reserve0 * reserve1, "K");

这表示

$$ \begin{align} X\text{new}Y\text{new} &\geq X\text{prev}Y\text{prev}\\ K\text{new}&\geq\text{prev} \end{align} $$

K 实际上并不恒定

说“K 保持不变”有点误导,尽管 AMM 公式有时被称作“恒定乘积公式”。

你可以这样想,如果有人向池子捐赠代币并改变了 K 的值,我们不希望阻止他们,因为他们让我们这些流动性提供者变得更富有,对吧?

Uniswap V2 并不阻止你“支付过多”,即在 swap 过程中转入过多的代币(这与稍后要到的一个安全检查有关)。

如果池子中出现净亏损,我们将感到不快,而这正是 require 语句所检查的内容。如果 K 增加,这意味着池子变得更大,作为流动性提供者,这正是我们想要的。

费用核算

但我们不仅希望 K 变得更大,我们希望它至少增加一个强制执行 0.3% 费用的数量。

具体来说,0.3% 费用适用于我们的交易规模,而不是池子的规模。它仅适用于进入的代币,而不适用于转出的代币。以下是一些例子:

  • 假设我们放入了 1000 的 token0 并移除了 1000 的 token1。我们需要为 token0 支付 3 的费用,而对于 token1 不需要支付费用。

  • 假设我们借入了 1000 的 token0 并没有借入 token1。我们需要将 1000 的 token0 还回去,并且我们需要为此支付 0.3% 的费用 — 3 的 token0。

请注意,如果我们闪电借贷其中一个代币,费用与用相同的金额 swap 该代币的费用是相同的。你为进入的代币支付费用,而不是转出的代币。但是如果你不放入代币,就无法借贷或交换。

请记住,reserve0 和 reserve1 代表旧余额,而 balance0 和 balance1 代表更新后的余额。

考虑到这一点,以下代码将是显而易见的。乘以 1000 和 3 的目的是简单地实现“分数”乘法,因为它会在最后抵消。

An image of fractional multiplication

该代码实现的公式是:

$$ \begin{align} (\text{new_balance}_0-0.003\times\text{amountIn}_0) &\times (\text{new_balance}_1 – 0.003\times\text{amountIn}_1) \\ \geq (\text{prev_balance}_0 &\times \text{prev_balance}_1) \end{align} $$

即,新余额必须增加进入代币的 0.3% 数量。在代码中,该公式通过将每个项乘以 1000 来完成,因为 Solidity 不支持浮点数,但数学公式表明代码想要实现的目标。

更新储备

交易完成后,“之前的余额”必须被当前余额替换。这发生在 swap() 中调用 _update() 函数时。

update reserves function call

_update() 函数

update reserves in _update function

这里有很多逻辑来处理 TWAP 预言机,但现在我们关心的是第 82 和 83 行,在这里储存变量 reserve0 和 reserve1 被更新以反映余额的变化。参数 _reserve0 和 _reserve1 用于更新预言机,但它们不会被存储。

安全检查

可能会出现两种错误情况:

  1. amountIn 没有被强制检查最优化,因此用户可能会为 swap 支付过多。
  2. AmountOut 没有灵活性,因为它被作为参数提供。如果 amountIn 相对于 amountOut 不够,则交易将 revert,并且 gas 将被浪费。

这些情况可能发生在有人抢跑交易(无论是有意还是无意)并在不利方向改变了池里资产的比率时。

通过 RareSkills 了解更多

本文是我们高级 Solidity Bootcamp 的一部分。请查看课程以了解更多信息。

最初发表于 2023 年 10 月 28 日

  • 原文链接: rareskills.io/post/unisw...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/