针对 uniswapv2 白皮书精校翻译
制作不易,转载此篇请注明出处
附加个人注释部分,全部由 markdown 引用插入文档,保持与原文的差异。
whitepaper 原 pdf 经过 minerU 转换
Uniswap v2 Core
Hayden Adams | Noah Zinsmeister | Dan Robinson |
hayden@uniswap.org | noah@uniswap.org | dan@paradigm.xyz |
Abstract
This technical whitepaper explains some of the design decisions behind the Uniswap v2 core contracts. It covers the contracts' new features—including arbitrary pairs between ERC20s, a hardened price oracle that allows other contracts to estimate the time-weighted average price over a given interval, "flash swaps" that allow traders to receive assets and use them elsewhere before paying for them later in the transaction, and a protocol fee that can be turned on in the future. It also re-architects the contracts to reduce their attack surface. This whitepaper describes the mechanics of Uniswap v2's "core" contracts including the pair contract that stores liquidity providers' funds—and the factory contract used to instantiate pair contracts.
摘要
本技术白皮书解释了 Uniswap v2 核心合约背后的一些设计决策。它涵盖了合约的新特性——包括任意 ERC20 代币对、一个强化的价格预言机(允许其他合约估计给定时间间隔内的时间加权平均价格)、"闪电兑换"(允许交易者在同一笔交易中先接收资产并在其他地方使用,然后再支付)、以及未来可以开启的协议费用。它还对合约进行了重新架构,以减少合约受攻击面。本白皮书描述了 Uniswap v2"核心"合约的机制,包括存储流动性提供者资金的交易对合约,以及用于实例化交易对合约的工厂合约。
1 Introduction (介绍)
Uniswap v1 is an on-chain system of smart contracts on the Ethereum blockchain, implementing an automated liquidity protocol based on a "constant product formula" 1. Each Uniswap v1 pair stores pooled reserves of two assets, and provides liquidity for those two assets, maintaining the invariant that the product of the reserves cannot decrease. Traders pay a 30-basis-point fee on trades, which goes to liquidity providers. The contracts are non-upgradeable.
Uniswap v1 是以太坊区块链上的一套链上智能合约系统,它基于"恒定乘积公式"1 实现了一种自动化的流动性协议。每个 Uniswap v1 交易对都存储着两种资产的汇集储备,并为这两种资产提供流动性,维持储备金乘积不能减少的不变性。交易者在交易时支付 30 个基点的费用,这笔费用会分配给流动性提供者。这些合约是不可升级的。
基点:金融学术语,万分之一为 1 基点。30 基点即千分之三
Uniswap v2 is a new implementation based on the same formula, with several new highlydesirable features. Most significantly, it enables the creation of arbitrary ERC20/ERC20 pairs, rather than supporting only pairs between ERC20 and ETH. It also provides a hardened price oracle that accumulates the relative price of the two assets at the beginning of each block. This allows other contracts on Ethereum to estimate the time-weighted average price for the two assets over arbitrary intervals. Finally, it enables "flash swaps" where users can receive assets freely and use them elsewhere on the chain, only paying for (or returning) those assets at the end of the transaction.
Uniswap v2 是基于相同公式的新实现,具有几个非常理想的新特性。最重要的是,它能够创建任意 ERC20/ERC20 交易对,而不仅仅是支持 ERC20 和 ETH 之间的交易对。它还提供了一个强化的价格预言机,该预言机在每个区块的开始累积两种资产的相对价格。这使得以太坊上的其他合约能够估算任意时间间隔内这两种资产的时间加权平均价格。最后,它启用了“闪电兑换”,用户可以免费接收资产并在链上的其他地方使用它们,只需在交易结束时支付(或归还)这些资产。
While the contract is not generally upgradeable, there is a private key that has the ability to update a variable on the factory contract to turn on an on-chain 5-basis-point fee on trades. This fee will initially be turned off, but could be turned on in the future, after which liquidity providers would earn 25 basis points on every trade, rather than 30 basis points.
虽然该合约通常是不可升级的,但存在一个私钥,该私钥能够更新工厂合约上的一个变量以开启收取链上 5 个基点的交易费用。此费用最初将处于关闭状态,但未来可能会开启,之后流动性提供者将在每笔交易中赚取 25 个基点,而不是 30 个基点。
As discussed in section 3, Uniswap v2 also fixes some minor issues with Uniswap v1, as well as rearchitecting the implementation, reducing Uniswap's attack surface and making the system more easily upgradeable by minimizing the logic in the "core" contract that holds liquidity providers' funds.
正如第 3 节所讨论的,Uniswap v2 还修复了 Uniswap v1 的一些小问题,并重新设计了架构实现,从而减少了 Uniswap 的受攻击面,并通过最小化存储流动性提供者资金的“核心”合约中的逻辑,使系统更容易升级。
This paper describes the mechanics of that core contract, as well as the factory contract used to instantiate those contracts. Actually using Uniswap v2 will require calling the pair contract through a "router" contract that computes the trade or deposit amount and transfers funds to the pair contract.
本文描述了该核心合约以及用于实例化这些合约的工厂合约的机制。实际上使用 Uniswap v2 需要通过一个“路由器”合约来调用交易对合约,该路由器合约计算交易或存款金额并将资金转移到交易对合约。
2 New features (新特性)
2.1 ERC-20 pairs (ERC-20 币对)
Uniswap v1 used ETH as a bridge currency. Every pair included ETH as one of its assets. This makes routing simpler—every trade between ABC and XYZ goes through the ETH/ABC pair and the ETH/XYZ pair—and reduces fragmentation of liquidity.
Uniswap v1 使用 ETH 作为桥接货币。每个交易对都包含 ETH 作为其资产之一。这使得路由更简单——ABC 和 XYZ 之间的每笔交易都通过 ETH/ABC 交易对和 ETH/XYZ 交易对进行——并减少了流动性的分散。
However, this rule imposes significant costs on liquidity providers. All liquidity providers have exposure to ETH, and suffer impermanent loss based on changes in the prices of other assets relative to ETH. When two assets ABC and XYZ are correlated—for example, if they are both USD stablecoins—liquidity providers on a Uniswap pair ABC/XYZ would generally be subject to less impermanent loss than the ABC/ETH or XYZ/ETH pairs.
然而,这条规则给流动性提供者带来了显著的成本。所有流动性提供者都暴露于 ETH 的风险之下,并会因其他资产相对于 ETH 的价格变动而遭受无常损失。当两种资产 ABC 和 XYZ 相关联时——例如,如果它们都是美元稳定币——与 ABC/ETH 或 XYZ/ETH 交易对相比,Uniswap 上 ABC/XYZ 交易对的流动性提供者通常会遭受更少的无常损失。
Using ETH as a mandatory bridge currency also imposes costs on traders. Traders have to pay twice as much in fees as they would on a direct ABC/XYZ pair, and they suffer slippage twice.
使用 ETH 作为强制性的桥接货币也给交易者带来了成本。与直接的 ABC/XYZ 交易对相比,交易者需要支付双倍的费用,并且遭受双倍的滑点。
滑点:指在进行交易时,客户下达的指定交易价格与实际成交价格存在偏差的一种现象。相关链接
双倍费用:进行一次 ABC->ETH,收取一次交易费用,发生一次滑点。进行一次 ETH->XYZ,收取一次交易费用,发生一次滑点。
Uniswap v2 allows liquidity providers to create pair contracts for any two ERC-20s.
Uniswap v2 允许流动性提供者为任意两种 ERC-20 代币创建交易对合约。
A proliferation of pairs between arbitrary ERC-20s could make it somewhat more difficult to find the best path to trade a particular pair, but routing can be handled at a higher layer (either off-chain or through an on-chain router or aggregator).
任意 ERC-20 代币之间大量交易对的出现可能会使得寻找特定交易对的最佳交易路径变得更加困难,但路由可以在更高的层级处理(无论是链下还是通过链上路由器或聚合器)。
2.2 Price oracle (价格预言机)
The marginal price offered by Uniswap (not including fees) at time \(t\) can be computed by dividing the reserves of asset \(a\) by the reserves of asset \(b\) .
在时间 \(t\),Uniswap 提供的边际价格(不包括费用)可以通过将资产 \(a\) 的储备量除以资产 \(b\) 的储备量来计算。
$$ p_t = \frac{r_t^a}{r_t^b} $$
Since arbitrageurs will trade with Uniswap if this price is incorrect (by a sufficient amount to make up for the fee), the price offered by Uniswap tends to track the relative market price of the assets, as shown by Angeris et al 2. This means it can be used as an approximate price oracle.
由于套利者会在 Uniswap 提供的价格不正确(达到足以弥补费用的程度)时与 Uniswap 进行交易,因此 Uniswap 提供的价格往往会跟踪资产的相对市场价格,正如 Angeris et al 2 所展示的那样。这意味着它可以被用作一个近似的价格预言机。
当 uniswap 的价格与市场真实价格差距较大时,如 1 ETH swap 1 USDT,套利者利用价格差距买入 ETH,将 ETH 提出赚取市场差价。这同时改变了 uniswap 中的价格比值,如更新后变为 1 ETH swap 100 USDT。随着时间变化,此类交易次数增多,相对套利空间减少,此比率越来越接近市场价值。
However, Uniswap v1 is not safe to use as an on-chain price oracle, because it is very easy to manipulate. Suppose some other contract uses the current ETH-DAI price to settle a derivative. An attacker who wishes to manipulate the measured price can buy ETH from the ETH-DAI pair, trigger settlement on the derivative contract (causing it to settle based on the inflated price), and then sell ETH back to the pair to trade it back to the true price. 1 This might even be done as an atomic transaction, or by a miner who controls the ordering of transactions within a block.
然而,Uniswap v1 作为链上价格预言机并不安全,因为它非常容易被操纵。假设某个其他合约使用当前的 ETH-DAI 价格来结算衍生品。一个希望操纵衡量价格的攻击者可以从 ETH-DAI 交易对中购买 ETH,触发衍生品合约的结算(导致其基于膨胀的价格结算),然后将 ETH 卖回该交易对以将其价格恢复到真实水平。 1这甚至可以作为原子交易完成,或者由控制区块内交易顺序的矿工完成。
假设场景: 有一个名为 "衍生品合约" 的 DeFi 合约,它使用 Uniswap v1 的 ETH-DAI 交易对的价格作为结算依据。 当前 Uniswap v1 的 ETH-DAI 交易对中,1 ETH 的价格为 1000 DAI。 攻击者拥有大量 DAI 和 ETH。 攻击步骤: 价格操纵:攻击者使用大量 DAI 在 Uniswap v1 的 ETH-DAI 交易对中购买 ETH。 由于 Uniswap v1 的价格机制,这会导致交易对中的 ETH 数量减少,DAI 数量增加,从而推高 ETH 的价格。 假设攻击者通过大量购买,将 ETH 的价格暂时推高到 1200 DAI。 触发结算:此时,衍生品合约检测到 Uniswap v1 提供的 ETH 价格为 1200 DAI,并基于这个膨胀的价格进行结算。 假设这个衍生品合约是多单合约,那么价格越高,多单合约的获利也就越高。 恢复价格:结算完成后,攻击者立即将之前购买的 ETH 卖回 Uniswap v1 的 ETH-DAI 交易对。 这会导致交易对中的 ETH 数量增加,DAI 数量减少,从而将 ETH 的价格恢复到接近 1000 DAI 的真实水平。 获利:攻击者通过在价格被操纵的高点触发衍生品合约的结算,从而获得了超额利润。 并且因为,攻击者最后恢复了价格,所以攻击者所持有的ETH或者DAI的数量并没有明显的减少。 关键点: 整个攻击过程可以在一次原子交易中完成,使得攻击难以被追踪和阻止。 攻击者利用了 Uniswap v1 价格机制的弱点,即价格容易受到交易量影响。 这种攻击对依赖 Uniswap v1 价格的 DeFi 应用造成了严重威胁。
Uniswap v2 improves this oracle functionality by measuring and recording the price before the first trade of each block (or equivalently, after the last trade of the previous block). This price is more difficult to manipulate than prices during a block. If the attacker submits a transaction that attempts to manipulate the price at the end of a block, some other arbitrageur may be able to submit another transaction to trade back immediately afterward in the same block. A miner (or an attacker who uses enough gas to fill an entire block) could manipulate the price at the end of a block, but unless they mine the next block as well, they may not have a particular advantage in arbitraging the trade back.
Uniswap v2 通过在每个区块的第一笔交易之前(或等效地,在上一个区块的最后一笔交易之后)测量和记录价格来改进此预言机功能。与区块内的价格相比,此价格更难被操纵。如果攻击者提交一笔试图在区块末尾操纵价格的交易,一些其他的套利者可能会立即在同一区块内提交另一笔交易以恢复价格。矿工(或使用足够 Gas 来填满整个区块的攻击者)可以在区块末尾操纵价格,但除非他们也挖掘下一个区块,否则他们在套利交易以恢复价格方面可能没有特别的优势。
“区块最后一笔交易”指的是什么?
一个区块包含多笔交易(可能是转账、合约调用等),矿工/验证者会按一定顺序打包这些交易。最后一笔交易是指这个区块中最后被执行的那一笔(无论是否是 Uniswap 交易)。
例如:
交易 1: Alice 转账给 Bob
交易 2: Uniswap 交易(攻击者操纵价格)
交易 3: (无)→ 交易 2 就是最后一笔。
为什么可以操纵最后一笔交易?
关键在于矿工/验证者对交易顺序的控制权:
矿工决定交易顺序:矿工可以自由选择将哪笔交易放在区块的末尾(比如通过 Gas 费竞价或恶意排序)。
如果攻击者是矿工: 攻击者将自己的操纵交易(例如在 Uniswap 上虚假拉高价格)放在区块末尾。 由于这是最后一笔交易,同一区块内没有后续交易可以立即对冲或套利(比如其他套利者无法在同一个区块内提交反向交易)。 此时,被操纵的价格会被暂时记录(直到下一个区块)。
如果攻击者不是矿工: 攻击者可以尝试用高 Gas 费贿赂矿工,让自己的交易成为最后一笔。 但普通用户很难保证交易一定在末尾,因为矿工可能优先打包其他高 Gas 交易。
为什么操纵“非末尾”交易更难? 如果攻击者的交易在区块中间(非末尾),其他套利者可以在同一区块内紧跟一笔反向交易,迅速抵消操纵效果。 例如:
交易 1: 攻击者拉高价格
交易 2: 套利者发现机会,卖出代币恢复价格
结果:价格未被成功操纵。 但在区块末尾,套利者没有机会在同一区块内反应,必须等到下一个区块,此时攻击可能已生效(例如触发其他合约的清算或预言机喂价)。
为什么矿工或攻击者在不挖掘下一区块的时候没有特别的优势?
当尝试操纵价格后且下一区块不属于上一区块矿工或攻击者挖掘时。所有用户均同时看见此验证过的区块,并且能够作出决策的起跑线一致(区块被验证有效的瞬间)
若无区块挖掘权利,此矿工或攻击者将与其他人竞争交易优先权(通过支付高 gas 费),且尝试提高价格的支出成本存在损失风险。
Specifically, Uniswap v2 accumulates this price, by keeping track of the cumulative sum of prices at the beginning of each block in which someone interacts with the contract. Each price is weighted by the amount of time that has passed since the last block in which it was updated, according to the block timestamp.2 This means that the accumulator value at any given time (after being updated) should be the sum of the spot price at each second in the history of the contract.
具体来说,Uniswap v2 通过追踪每个有人与合约交互的区块开始时的累积价格总和来累积这个价格。每个价格都根据自上次更新的区块以来经过的时间(根据区块时间戳)进行加权2。这意味着在任何给定时间(更新后),累积器的值应该是合约历史上每秒钟现货价格的总和。
$$ a_t = \sum_{i=1}^{t} p_i $$
To estimate the time-weighted average price from time \(t_1\) to \(t_2\), an external caller can checkpoint the accumulator's value at \(t_1\) and then again at \(t_2\), subtract the first value from the second, and divide by the number of seconds elapsed. (Note that the contract itself does not store historical values for this accumulator—the caller has to call the contract at the beginning of the period to read and store this value.)
要估计从时间 \(t_1\) to \(t_2\) 的时间加权平均价格,外部调用者可以在\(t_1\) 时刻检查累加器的值,然后在\(t_2\)时刻再次检查,将第一个值从第二个值中减去,然后除以经过的秒数。(请注意,合约本身不存储此累加器的历史值——调用者必须在期间开始时调用合约以读取和存储此值。)
$$ p_{t_1,t_2} = \frac{\sum_{i=t_1}^{t_2} p_i}{t_2 - t_1} = \frac{\sum_{i=1}^{t_2} p_i - \sum_{i=1}^{t_1} p_i}{t_2 - t_1} = \frac{a_{t_2} - a_{t_1}}{t_2 - t_1} $$
Users of the oracle can choose when to start and end this period. Choosing a longer period makes it more expensive for an attacker to manipulate the TWAP, although it results in a less up-to-date price.
预言机的用户可以选择时间范围的起始位置和结束位置。选择较长的时间区间会增加攻击者操纵 TWAP 的成本,尽管这会导致用户获取到不太实时的价格。
时间加权平均价格 (TWAP) 的时间间隔过长可能会导致几个问题,尤其是在波动性较大的市场中。以下是一些潜在的结果:
- 价格滞后:
- 时间间隔越长,TWAP 对当前市场价格的反应就越慢。
- 如果市场价格快速变化,TWAP 可能无法及时反映这些变化,从而导致交易者做出基于过时信息的决策。
- 滑点增加:
- 在价格快速变化的市场中,使用滞后的 TWAP 进行交易可能会导致滑点增加。
- 滑点是指预期交易价格与实际执行价格之间的差异。
- 如果 TWAP 没有及时反映当前市场价格,交易者可能会以比预期更差的价格成交。
- 套利机会:
- 滞后的 TWAP 可能会产生套利机会。
- 如果 TWAP 没有及时反映当前市场价格,套利者可能会利用价格差异在其他交易所或市场中进行获利。
- 预言机操纵:
- 在极端情况下,如果时间间隔过长,攻击者可能会更容易操纵 TWAP。
- 攻击者可以通过在较长时间内进行少量交易来缓慢地影响 TWAP,而不会引起注意。
- 对瞬时价格波动的敏感度降低:
- TWAP 的设计初衷是为了降低单笔交易对价格的影响,但是时间间隔过长,会降低价格对于短时剧烈波动的敏感度。
- 在一些希望对价格波动快速做出反应的协议中,这样的特性并不好。 时间间隔过长会降低 TWAP 的响应能力,使其对当前市场价格的反应滞后。这可能会导致滑点增加、套利机会和预言机操纵等问题。因此,在选择 TWAP 的时间间隔时,需要权衡准确性和响应能力。
One complication: should we measure the price of asset A in terms of asset B, or the price of asset B in terms of asset A? While the spot price of A in terms of B is always the reciprocal of the spot price of B in terms of A, the mean price of asset A in terms of asset B over a particular period of time is not equal to the reciprocal of the mean price of asset B in terms of asset A.3 For example, if the USD/ETH price is 100 in block 1 and 300 in block 2, the average USD/ETH price will be 200 USD/ETH, but the average ETH/USD price will be 1/150 ETH/USD. Since the contract cannot know which of the two assets users would want to use as the unit of account, Uniswap v2 tracks both prices.
一个复杂的问题是:我们应该衡量资产 A 相对于资产 B 的价格,还是资产 B 相对于资产 A 的价格?虽然 A 相对于 B 的即时价格始终是 B 相对于 A 的即时价格的倒数,但在特定时间段内,资产 A 相对于资产 B 的平均价格不等于资产 B 相对于资产 A 的平均价格的倒数3。例如,如果 USD/ETH 的价格在区块 1 中是 100,在区块 2 中是 300,那么平均 USD/ETH 价格将是 200 USD/ETH,但平均 ETH/USD 价格将是 1/150 ETH/USD。由于合约无法知道用户希望将两种资产中的哪一种用作记账单位,因此 Uniswap v2 会跟踪两种价格。
1. 即时价格的倒数关系 这句话首先明确了,在任何一个瞬间,资产 A 相对于资产 B 的价格,和资产 B 相对于资产 A 的价格,是互为倒数的关系。 例如,如果 1 ETH = 200 USD,那么 1 USD = 1/200 ETH。 2. 平均价格的差异 但是,当计算一段时间内的平均价格时,这个倒数关系就不成立了。 原因在于,平均价格的计算是基于价格本身的平均,而不是基于价格的倒数。 3. 示例分析:USD/ETH 和 ETH/USD USD/ETH 的平均价格: 区块 1:1 ETH = 100 USD 区块 2:1 ETH = 300 USD 平均 USD/ETH:(100 + 300) / 2 = 200 USD/ETH ETH/USD 的平均价格: 区块 1:1 USD = 1/100 ETH 区块 2:1 USD = 1/300 ETH 平均 ETH/USD:((1/100) + (1/300)) / 2 为了计算这个平均值,我们需要先计算括号内的算式,然后再除以 2。 (1/100 + 1/300) = (3/300 + 1/300) = 4/300 = 1/75 (1/75)/2 = 1/150 所以平均 ETH/USD 价格是 1/150 ETH/USD 4. 为什么会出现 1/150? 1/150 是通过计算 ETH/USD 价格在两个区块中的平均值得到的。 由于我们是对分数进行平均,而不是直接对倒数进行平均,所以结果与 1/200 不同。 5. Uniswap v2 的解决方案 因为用户可能需要不同方向的平均价格,Uniswap v2 同时跟踪两种价格,以确保提供全面的信息。
Another complication is that it is possible for someone to send assets to the pair contract—and thus change its balances and marginal price—without interacting with it, and thus without triggering an oracle update. If the contract simply checked its own balances and updated the oracle based on the current price, an attacker could manipulate the oracle by sending an asset to the contract immediately before calling it for the first time in a block. If the last trade was in a block whose timestamp was X seconds ago, the contract would incorrectly multiply the new price by X before accumulating it, even though nobody has had an opportunity to trade at that price. To prevent this, the core contract caches its reserves after each interaction, and updates the oracle using the price derived from the cached reserves rather than the current reserves. In addition to protecting the oracle from manipulation, this change enables the contract re-architecture described below in section 3.2.
另一个复杂的问题是,有人可能在不与交易对合约交互的情况下向此交易对合约发送资产,从而改变其余额和边际价格(因此也不会触发预言机更新)。如果合约只是检查自身的余额并根据当前价格更新预言机,攻击者可以通过在一个区块中首次调用合约之前立即向合约发送资产来操纵预言机。如果上次交易发生在 X 秒前的区块中,即使没有人有机会以该价格进行交易,合约也会错误地将新价格乘以 X 然后累积它。为了防止这种情况,核心合约在每次交互后缓存其资产储备值,并使用从缓存的储备而不是当前储备中得出的价格更新预言机。除了保护预言机免受操纵外,此更改还实现了下文 3.2 节中描述的合约重新架构。
此处简单的翻译即为:uniswap 不基于当前合约中的资产量计算价格,而是每次通过所有触发交易的事件记录并更新储备量。屏蔽了私下转入的资产对预言机的影响
2.2.1 Precision (精度)
Because Solidity does not have first-class support for non-integer numeric data types, the Uniswap v2 uses a simple binary fixed point format to encode and manipulate prices. Specifically, prices at a given moment are stored as UQ112.112 numbers, meaning that 112 fractional bits of precision are specified on either side of the decimal point, with no sign. These numbers have a range of \([0,2^{112}-1]\)4 and a precision of \(\frac{1}{2^{112}}\).
由于 Solidity 本身不支持非整数数值数据类型,Uniswap v2 使用一种简单的二进制定点格式来编码和操作价格。具体来说,某一时刻的价格存储为 UQ112.112 格式的数字,这意味着在小数点两侧都指定了 112 位的小数精度,并且没有符号。这些数字的范围是 \([0,2^{112}-1]\)4,精度为 \(\frac{1}{2^{112}}\).
The UQ112.112 format was chosen for a pragmatic reason — because these numbers can be stored in a uint224, this leaves 32 bits of a 256 bit storage slot free. It also happens that the reserves, each stored in a uint112, also leave 32 bits free in a (packed) 256 bit storage slot. These free spaces are used for the accumulation process described above. Specifically, the reserves are stored alongside the timestamp of the most recent block with at least one trade, modded with \(2^{32}\) so that it fits into 32 bits. Additionally, although the price at any given moment (stored as a UQ112.112 number) is guaranteed to fit in 224 bits, the accumulation of this price over an interval is not. The extra 32 bits on the end of the storage slots for the accumulated price of A/B and B/A are used to store overflow bits resulting from repeated summations of prices. This design means that the price oracle only adds an additional three SSTORE operations (a current cost of about 15,000 gas) to the first trade in each block.
选择 UQ112.112 格式是出于务实的考虑——因为这些数字可以存储在 uint224 中,这使得一个 256 位的存储槽空闲了 32 位。碰巧的是,储备金(每个都存储在 uint112 中)也在一个(紧凑的)256 位存储槽中留出了 32 位空闲空间。这些空闲空间用于上述的累积过程。具体来说,储备金与最近一次至少发生一笔交易的区块的时间戳一起存储,该时间戳会进行模 \(2^{32}\)运算,以截取此时间戳的末尾 32 位。此外,虽然任何给定时刻的价格(存储为 UQ112.112 数字)保证可以容纳在 224 位中,但该价格在一段时间内的累积却不能。存储槽末尾用于存储 A/B 和 B/A 累积价格的额外 32 位用于存储重复的价格求和产生的溢出位。这种设计意味着价格预言机在每个区块的第一笔交易中仅额外增加三个 SSTORE 操作(目前成本约为 15,000 gas)。
1.
UQ112.112
格式的用途
- 定义:
UQ112.112
是一种 224 位无符号定点数格式,其中:
- 前 112 位 表示整数部分。
- 后 112 位 表示小数部分。
- 作用:用于精确存储价格(如
reserve1/reserve0
),避免浮点数精度丢失。
2. 存储布局优化
Uniswap v2 的存储槽(256 位)被 紧凑排列 以节省 Gas 成本:
存储槽 1:储备量 + 时间戳
字段 位数 说明 reserve0
112 位 代币 0 的储备量(如 USDC)。 reserve1
112 位 代币 1 的储备量(如 ETH)。 blockTimestampLast
32 位 最后一次交易的时间戳(取模 2^32
,仅保留后 32 位)。
- 空闲位:
reserve0 + reserve1 + blockTimestampLast = 112 + 112 + 32 = 256位
,刚好填满一个存储槽。存储槽 2:累积价格
字段 位数 说明 price0Cumulative
224 位 代币 0 的累积价格( UQ112.112
格式)。price1Cumulative
224 位 代币 1 的累积价格( UQ112.112
格式)。溢出位 32 位 用于处理累积价格的溢出(见下文)。
- 空闲位:
price0Cumulative + price1Cumulative = 224 + 224 = 448位
,但一个存储槽仅 256 位,因此实际分为两个存储槽,剩余 32 位用于溢出处理。
3. 为什么选择
UQ112.112
?
- Gas 效率:
- 每次交易更新价格时,只需修改 一个存储槽(储备量+时间戳)和 部分累积价格槽。
- 若使用更大的数据类型(如
uint256
),会占用更多存储槽,增加 Gas 成本。- 精度足够:
- 112 位小数部分可满足大多数代币价格的精度需求(如 ETH/USDC 价格通常小于
2^112
)。- 溢出处理:
- 累积价格可能随时间溢出
UQ112.112
的 224 位范围,因此额外 32 位用于记录溢出次数(类似“进位”)。
4. 时间戳的截断(模
2^32
)
- 原因:
- 区块时间戳本身是
uint256
,但仅需记录相对时间差(ΔT
)。- 截断为 32 位后,可覆盖 约 136 年 的时间范围(
2^32秒 ≈ 136年
),完全够用。- 节省空间:
- 避免占用额外存储槽,与储备量共享同一槽位。
5. Gas 成本优化
- 每次更新的操作:
- 更新
reserve0
和reserve1
(同一存储槽)。- 更新
price0Cumulative
和price1Cumulative
(需两个存储槽)。- 更新
blockTimestampLast
(与储备量共享存储槽)。- 总成本:约 15,000 Gas(主要来自 3 次
SSTORE
操作)。
6. 示例说明
假设在区块时间戳
t=1000
时:
- 储备量:
reserve0 = 1000 USDC
,reserve1 = 1 ETH
→ 价格price0 = 1/1000 = 0.001
。- 累积价格更新:
- 若
blockTimestampLast = 500
,则ΔT = 1000 - 500 = 500秒
。price0Cumulative += 0.001 * 500 = 0.5
(以UQ112.112
格式存储)。
7. 设计优势总结
- 紧凑存储:充分利用每个存储槽的 256 位,减少 Gas 消耗。
- 抗溢出:通过额外 32 位处理累积价格的溢出。
- 低延迟更新:仅在区块首笔交易时更新累积价格,避免频繁计算。
- 兼容性:与 EVM 的存储模型完美匹配,无需复杂升级。
The primary downside is that 32 bits isn't quite enough to store timestamp values that will reasonably never overflow. In fact, the date when the Unix timestamp overflows a uint32 is 02/07/2106. To ensure that this system continues to function properly after this date, and every multiple of \(2^{32}-1\) seconds thereafter, oracles are simply required to check prices at least once per interval (approximately 136 years). This is because the core method of accumulation (and modding of timestamp), is actually overflow-safe, meaning that trades across overflow intervals can be appropriately accounted for given that oracles are using the proper (simple) overflow arithmetic to compute deltas.
主要的缺点是 32 位不足以存储合理范围内永远不会溢出的时间戳值。事实上,Unix 时间戳溢出 uint32 的日期是 2106 年 2 月 7 日。为了确保该系统在此日期之后以及此后每隔 \(2^{32}-1\)秒的倍数都能正常运行,预言机只需要至少每隔一段时间(大约 136 年)检查一次价格即可。这是因为累积(以及时间戳的模运算)的核心方法实际上是溢出安全的,这意味着只要预言机使用正确的(简单的)溢出算术来计算增量,就可以适当地计算跨越溢出间隔的交易。
_update() function . 时间计算式 : \( 2^{32}_{s} / ( {60}_{s/m} * {60}_{m/h} * {24}_{h/d} * {365}_{d/y} ) \)≈ 136.192519
2.3 Flash Swaps (闪电互换)
In Uniswap v1, a user purchasing ABC with XYZ needs to send the XYZ to the contract before they could receive the ABC. This is inconvenient if that user needs the ABC they are buying in order to obtain the XYZ they are paying with. For example, the user might be using that ABC to purchase XYZ in some other contract in order to arbitrage a price difference from Uniswap, or they could be unwinding a position on Maker or Compound by selling the collateral to repay Uniswap.
在 Uniswap v1 中,用户用 XYZ 购买 ABC 需要先将 XYZ 发送到合约,然后才能收到 ABC。如果用户需要他们购买的 ABC 来获得他们用于支付的 XYZ,这会很不方便。例如,用户可能正在使用该 ABC 在某个其他合约中购买 XYZ,以赚取此合约与 Uniswap 的差价,或者他们可以通过出售抵押品来偿还 Uniswap 来平仓 Maker 或 Compound 的头寸。
头寸: 投资者拥有或借用的资金数量
Uniswap v2 adds a new feature that allows a user to receive and use an asset before paying for it, as long as they make the payment within the same atomic transaction. The swap function makes a call to an optional user-specified callback contract in between transferring out the tokens requested by the user and enforcing the invariant. Once the callback is complete, the contract checks the new balances and confirms that the invariant is satisfied (after adjusting for fees on the amounts paid in). If the contract does not have sufficient funds, it reverts the entire transaction.
Uniswap v2 增加了一项新功能,允许用户在付款之前接收和使用资产,只要他们在同一个原子交易中完成付款即可。swap 函数在转出用户请求的代币和强制执行不变量之间,调用一个可选的用户指定的回调合约。回调完成后,合约检查新的余额,并确认不变量得到满足(在根据支付金额调整费用后)。如果合约没有足够的资金,它将回滚整个交易。
A user can also repay the Uniswap pool using the same token, rather than completing the swap. This is effectively the same as letting anyone flash-borrow any of assets stored in a Uniswap pool (for the same \(0.30\%\) fee as Uniswap charges for trading).5
用户也可以使用相同的代币偿还 Uniswap 池,而不是完成兑换。这实际上等同于允许任何人闪电借用存储在 Uniswap 池中的任何资产(费用与 Uniswap 收取的交易费用相同,为 0.30%)5。
此借贷是一个临时资金周转的工具,起到的最大作用即加速资金的流转。
有意思的点是,与传统金融中过桥协议类似
1.两者中传统过桥协议需要抵押物,存在一定无法偿还的风险。而闪电互换无需抵押,依赖于区块链的回滚机制,除了协议漏洞外无偿还风险。
2.两者中传统过桥协议资金量大,面向企业。而闪电互换面向所有用户,常为小资金用户。
2.4 Protocol fee
Uniswap v2 includes a \(0.05\%\) protocol fee that can be turned on and off. If turned on, this fee would be sent to a feeTo address specified in the factory contract.
Uniswap v2 包含一个可以开启和关闭的 \(0.05\%\) 协议费用。如果开启,此费用将发送到工厂合约中指定的 feeTo 地址。
Initially, feeTo is not set, and no fee is collected. A pre-specified address—feeToSetter—can call the setFeeTo function on the Uniswap v2 factory contract, setting feeTo to a different value. feeToSetter can also call the setFeeToSetter to change the feeToSetter address itself.
初始化时,feeTo 未设置,不收取任何费用。一个预先指定的地址——feeToSetter——可以调用 Uniswap v2 工厂合约上的 setFeeTo 函数,将 feeTo 设置为不同的值。feeToSetter 还可以调用 setFeeToSetter 来更改 feeToSetter 地址本身。
If the feeTo address is set, the protocol will begin charging a 5-basis-point fee, which is taken as a \(\frac{1}{6}\) cut of the 30-basis-point fees earned by liquidity providers. That is, traders will continue to pay a \(0.30\%\) fee on all trades; \(83.\overline{3}\%\) of that fee ( \(0.25\%\) of the amount traded) will go to liquidity providers, and \(16.\overline{6}\%\) of that fee ( \(0.05\%\) of the amount traded) will go to the feeTo address.
如果 feeTo 地址已设置,协议将开始收取 5 个基点的费用,这相当于流动性提供者赚取的 30 个基点费用的 \(\frac{1}{6}\)。也就是说,交易者将继续为所有交易支付 \(0.30\%\)的费用;该费用的 \(83.\overline{3}\%\)(交易金额的 0.25%)将归流动性提供者所有,而该费用的 \(16.\overline{6}\%\)(交易金额的 0.05%)将归 feeTo 地址所有。
Collecting this \(0.05\%\) fee at the time of the trade would impose an additional gas cost on every trade. To avoid this, accumulated fees are collected only when liquidity is deposited or withdrawn. The contract computes the accumulated fees, and mints new liquidity tokens to the fee beneficiary, immediately before any tokens are minted or burned.
在交易时收取这 \(0.05\%\) 的费用会在每笔交易中增加额外的 Gas 成本。为了避免这种情况,累积的费用仅在存入或提取流动性时收取。合约在铸造或销毁任何代币之前,计算累积的费用,并向费用受益人铸造新的流动性代币。
The total collected fees can be computed by measuring the growth in \(\sqrt{k}\) (that is, \(\sqrt{x \cdot y}\)) since the last time fees were collected.6 This formula gives you the accumulated fees between \(t_1\) and \(t_2\) as a percentage of the liquidity in the pool at \(t_2\):
可以通过测量自上次收取费用以来\(\sqrt{k}\) (即 \(\sqrt{x \cdot y}\))的增长来计算收取的总费用6。此公式给出了在 \(t_1\)和 \(t_2\)之间累积的费用占 \(t_2\)时刻池中流动性的百分比:
$$ f_{1,2} = 1 - \frac{\sqrt{k_1}}{\sqrt{k_2}} $$
If the fee was activated before \(t_1\), the feeTo address should capture \(\frac{1}{6}\) of fees that were accumulated between \(t_1\) and \(t_2\). Therefore, we want to mint new liquidity tokens to the feeTo address that represent \( \phi \cdot f_{1,2} \) of the pool, where \(\phi\) is \(\frac{1}{6}\).
如果费用在 \(t_1\)之前被激活,则 feeTo 地址应获得\(\frac{1}{6}\)的,在\(t_1\)和\(t_2\)之间产生的费率。 因此,我们希望向 feeTo 地址铸造代表池中 \( \phi \cdot f_{1,2} \) 份额的新流动性代币,其中\(\phi\) 是 \(\frac{1}{6}\)。
That is, we want to choose \(s_m\) to satisfy the following relationship, where \(s_1\) is the total quantity of outstanding shares at time \(t_1\):
也就是说,我们希望选择 \(s_m\)来满足以下关系,其中\(s_1\)是在 \(t_1\)时刻流通的份额总数:
$$ \frac{s_m}{s_m + s_1} = \phi \cdot f_{1,2} $$
After some manipulation, including substituting \(1 - \frac{\sqrt{k_1}}{\sqrt{k_2}}\) for \(f_{1,2}\) and solving for \(s_m\), we can rewrite this as:
在进行一些操作后,包括代入\(1 - \frac{\sqrt{k_1}}{\sqrt{k_2}}\) 作为\(f_{1,2}\) ,并且求解\(s_m\),我们可以重写此公式为:
$$ s_m = \frac{\sqrt{k_2} - \sqrt{k_1}}{(\frac{1}{\phi} - 1) \cdot \sqrt{k_2} + \sqrt{k_1}} \cdot s_1 $$
Setting \(\phi\) to \(\frac{1}{6}\) gives us the following formula:
将\(\phi\) 设定为\(\frac{1}{6}\) 给我们带来如下的公式:
$$ s_m = \frac{\sqrt{k_2} - \sqrt{k_1}}{5 \cdot \sqrt{k_2} + \sqrt{k_1}} \cdot s_1 $$
Suppose the initial depositor puts 100 DAI and 1 ETH into a pair, receiving 10 shares. Some time later (without any other depositor having participated in that pair), they attempt to withdraw it, at a time when the pair has 96 DAI and 1.5 ETH. Plugging those values into the above formula gives us the following:
假设初始存款人向一个交易对投入 100 DAI 和 1 ETH,并获得 10 份份额。一段时间后(没有其他存款人参与该交易对),他们在交易对拥有 96 DAI 和 1.5 ETH 时尝试提取。将这些值代入上述公式,我们得到以下结果:
$$ s_m = \frac{\sqrt{1.5 \cdot 96} - \sqrt{1 \cdot 100}}{5 \cdot \sqrt{1.5 \cdot 96} + \sqrt{1 \cdot 100}} \cdot 10 \approx 0.0286 $$
2.5 Meta transactions for pool shares (流动池份额中的元交易)
Pool shares minted by Uniswap v2 pairs natively support meta transactions. This means users can authorize a transfer of their pool shares with a signature7, rather than an on-chain transaction from their address. Anyone can submit this signature on the user's behalf by calling the permit function, paying gas fees and possibly performing other actions in the same transaction.
Uniswap v2 交易对铸造的池份额原生支持元交易。这意味着用户可以通过签名7授权转移他们的池份额,而无需从他们的地址发起链上交易。任何人都可以通过调用 permit 函数代表用户提交此签名,支付 Gas 费用,并可能在同一笔交易中执行其他操作。
3 Other changes (其他变化)
3.1 Solidity
Uniswap v1 is implemented in Vyper, a Python-like smart contract language. Uniswap v2 is implemented in the more widely-used Solidity, since it requires some capabilities that were not yet available in Vyper (such as the ability to interpret the return values of non-standard ERC-20 tokens, as well as access to new opcodes such as chainid via inline assembly) at the time it was being developed.
Uniswap v1 使用 Vyper(一种类似 Python 的智能合约语言)实现。Uniswap v2 则使用更广泛使用的 Solidity 实现,因为它在开发时需要一些 Vyper 尚未提供的功能(例如解释非标准 ERC-20 代币返回值的能力,以及通过内联汇编访问诸如 chainid 等新操作码的能力)。
3.2 Contract re-architecture (合约重构)
One design priority for Uniswap v2 is to minimize the surface area and complexity of the core pair contract—the contract that stores liquidity providers' assets. Any bugs in this contract could be disastrous, since millions of dollars of liquidity might be stolen or frozen.
Uniswap v2 的一个设计重点是最小化核心交易对合约(存储流动性提供者的资产的合约)的表面积(即合约暴露给调用者的部分)和复杂性。此合约中的任何错误都可能是灾难性的,因为数百万美元的流动性可能会被盗或冻结。
When evaluating the security of this core contract, the most important question is whether it protects liquidity providers from having their assets stolen or locked. Any feature that is meant to support or protect traders—other than the basic functionality of allowing one asset in the pool to be swapped for another—can be handled in a "router" contract.
在评估此核心合约的安全性时,最重要的问题是它是否保护流动性提供者免于资产被盗或锁定。任何旨在支持或保护交易者的功能——除了允许池中的一种资产兑换为另一种资产的基本功能之外——都可以在“路由器”合约中处理。
In fact, even part of the swap functionality can be pulled out into the router contract. As mentioned above, Uniswap v2 stores the last recorded balance of each asset (in order to prevent a particular manipulative exploit of the oracle mechanism). The new architecture takes advantage of this to further simplify the Uniswap v1 contract.
事实上,甚至是部分兑换功能也可以提取到路由器合约中。如上所述,Uniswap v2 存储了每种资产的最后记录余额(以防止特定的预言机机制的操纵性利用)。新的架构利用这一点进一步简化了 Uniswap v1 合约。
In Uniswap v2, the seller sends the asset to the core contract before calling the swap function. Then, the contract measures how much of the asset it has received, by comparing the last recorded balance to its current balance. This means the core contract is agnostic to the way in which the trader transfers the asset. Instead of transferFrom, it could be a meta transaction, or any other future mechanism for authorizing the transfer of ERC-20s.
在 Uniswap v2 中,卖方在调用 swap 函数之前将资产发送到核心合约。然后,合约通过比较最后记录的余额和当前余额来衡量它收到了多少资产。这意味着核心合约与交易者转移资产的方式无关。它可以是元交易,也可以是任何其他未来授权 ERC-20 转移的机制,而不是 transferFrom。
3.2.1 Adjustment for fee (费率调整)
Uniswap v1's trading fee is applied by reducing the amount paid into the contract by \(0.3\%\) before enforcing the constant-product invariant. The contract implicitly enforces the following formula:
Uniswap v1 的交易费用是通过在执行 恒定乘积不变量计算之前划扣掉用户支付到合约的总资产量的\(0.3\%\) 实现的。合约隐式地强制执行以下公式:
$$ (x_1 - 0.003 \cdot x_{in}) \cdot y_1 \gt = x_0 \cdot y_0 $$
With flash swaps, Uniswap v2 introduces the possibility that \(x_{in}\) and \(y_{in}\) might both be non-zero (when a user wants to pay the pair back using the same asset, rather than swapping). To handle such cases while properly applying fees, the contract is written to enforce the following invariant8:
借助闪电互换,Uniswap v2 引入了 \( x_{in}\)和 \(y_{in}\)可能都非零的情况(当用户想要使用相同的资产偿还交易对,而不是进行兑换时)。为了在正确应用费用的同时处理这种情况,合约被编写为强制执行以下不变量8:
$$ (x_1 - 0.003 \cdot x_{in}) \cdot (y_1 - 0.003 \cdot y_{in}) \gt = x_0 \cdot y_0 $$
To simplify this calculation on-chain, we can multiply each side of the inequality by 1,000,000:
为简化这个在链上的计算,我们可以在不等式两侧同乘 1,000,000:
$$ (1000 \cdot x_1 - 3 \cdot x_{in}) \cdot (1000 \cdot y_1 - 3 \cdot y_{in}) \gt = 1000000 \cdot x_0 \cdot y_0 $$
3.2.2 sync() and skim()
To protect against bespoke token implementations that can update the pair contract's balance, and to more gracefully handle tokens whose total supply can be greater than \(2^{112}\), Uniswap v2 has two bail-out functions: sync() and skim().
为了防范可能更新交易对合约余额的定制代币实现,并更优雅地处理总供应量可能大于 \(2^{112}\)的代币,Uniswap v2 提供了两个应急函数:sync() 和 skim()。
sync() functions as a recovery mechanism in the case that a token asynchronously deflates the balance of a pair. In this case, trades will receive sub-optimal rates, and if no liquidity provider is willing to rectify the situation, the pair is stuck. sync() exists to set the reserves of the contract to the current balances, providing a somewhat graceful recovery from this situation.
sync() 函数作为一种恢复机制,用于处理代币异步通缩交易对余额的情况。在这种情况下,交易将获得次优的价格,如果没有任何流动性提供者愿意纠正这种情况,交易对就会陷入困境。sync() 的存在是为了将合约的储备设置为当前的余额,从而为此情况提供某种程度的优雅恢复。
此机制触发将一定程度上打破币对供给平衡(前提是有用户直接向合约转入代币而不调用合约),通过尝试增加套利空间而使交易继续进行
skim() functions as a recovery mechanism in case enough tokens are sent to an pair to overflow the two uint112 storage slots for reserves, which could otherwise cause trades to fail. skim() allows a user to withdraw the difference between the current balance of the pair and \(2^{112}-1\) to the caller, if that difference is greater than 0.
skim() 函数作为一种恢复机制,用于处理发送到交易对的代币数量过多,导致超出用于存储储备的两个 uint112 存储槽的情况,否则可能导致交易失败。如果当前交易对余额与 \(2^{112}-1\) 之间的差值大于 0,skim() 允许用户将该差值提取给调用者。
一般正常的合约不会提供超过一千万亿额度的代币,此处主要处理潜在的非正常情况
3.3 Handling non-standard and unusual tokens (处理非标准及不常见代币)
The ERC-20 standard requires that transfer() and transferFrom() return a boolean indicating the success or failure of the call 4. The implementations of one or both of these functions on some tokens—including popular ones like Tether (USDT) and Binance Coin (BNB)—instead have no return value. Uniswap v1 interprets the missing return value of these improperly defined functions as false—that is, as an indication that the transfer was not successful—and reverts the transaction, causing the attempted transfer to fail.
ERC-20 标准要求 transfer() 和 transferFrom() 返回一个布尔值,指示调用成功或失败 4。然而,一些代币(包括流行的 Tether (USDT) 和 Binance Coin (BNB))上这两个函数中的一个或两个的实现都没有返回值。Uniswap v1 将这些不正确定义的函数缺失的返回值解释为 false——即表示转账不成功——并回滚交易,导致尝试的转账失败。
Uniswap v2 handles non-standard implementations differently. Specifically, if a transfer() call9 has no return value, Uniswap v2 interprets it as a success rather than as a failure. This change should not affect any ERC-20 tokens that conform to the standard (because in those tokens, transfer() always has a return value).
Uniswap v2 以不同的方式处理非标准实现。具体来说,如果 transfer() 调用9 没有返回值,Uniswap v2 会将其解释为成功而不是失败。此更改不应影响任何符合标准的 ERC-20 代币(因为在这些代币中,transfer() 始终具有返回值)。
Uniswap v1 also makes the assumption that calls to transfer() and transferFrom() cannot trigger a reentrant call to the Uniswap pair contract. This assumption is violated by certain ERC-20 tokens, including ones that support ERC-777's "hooks" 5. To fully support such tokens, Uniswap v2 includes a "lock" that directly prevents reentrancy to all public state-changing functions. This also protects against reentrancy from the user-specified callback in a flash swap, as described in section 2.3.
Uniswap v1 还假设对 transfer() 和 transferFrom() 的调用不会触发对 Uniswap 交易对合约的重入调用。某些 ERC-20 代币违反了此假设,包括支持 ERC-777“钩子”的代币5。为了完全支持此类代币,Uniswap v2 包含一个“锁”,该锁直接阻止对所有公共状态更改函数的重入。正如 2.3 节所述,这还可以防止闪电互换中用户指定的回调函数的重入。
3.4 Initialization of liquidity token supply (流动性代币的初始化补充)
When a new liquidity provider deposits tokens into an existing Uniswap pair, the number of liquidity tokens minted is computed based on the existing quantity of tokens:
当一个新的流动性提供者向现有的 Uniswap 交易对存入代币时,所铸造的流动性代币数量是基于现有的代币数量计算的:
$$ s_{minted} = \frac{x_{deposited}}{x_{starting}} \cdot s_{starting} $$
But what if they are the first depositor? In that case, \(x_{starting}\) is \(0\), so this formula will not work.
但如果他们是第一个存款人呢?在这种情况下,\(x_{starting}\) 为 \(0\),因此该公式将不起作用。
Uniswap v1 sets the initial share supply to be equal to the amount of ETH deposited (in wei). This was a somewhat reasonable value, because if the initial liquidity was deposited at the correct price, then 1 liquidity pool share (which, like ETH, is an 18-decimal token) would be worth approximately 2 ETH.
Uniswap v1 将初始份额供应量设置为等于存入的 ETH 数量(以 wei 为单位)。这是一个相对合理的值,因为如果初始流动性以正确的价格存入,那么 1 份流动性池份额(与 ETH 一样,是 18 位小数的代币)的价值约为 2 ETH。
例如:初始化创建者向 ETH-DAI 币对存入 1ETH+100DAI(至少以创建者的价值设定以及接受程度,100DAI 值 1ETH)。那么按照合约规则创建者获取到 1 LP-Token。这 1 个 Token 值 1ETH+100DAI,按照价值等比转换的算法,值 2ETH。至于按常规市场视角来说,这个比例不合理,那么就会有潜在的交易者从中套利,开始使得这个价值比例趋于市场价格。初始创建者将承担潜在的损失 ETH 或 DAI 的风险。
一个潜在的有意思的设计在于:想在创建之初吸引到足够多的交易者进入或参与到此币对的互换中来时,创建者可以投入大量的资金创造这种代币兑换的不平衡。尽管这将在前期带来一定量的亏损,不过在后期,尤其是创建者是其中某个代币发行方时,将会获得大量用户并使得货币价格得到一定的保障(毕竟在不爆雷或者出现重大漏洞的情况下,每个人的交易时间,获取到的信息和对代币当下的心理价位不同)。
However, this meant that the value of a liquidity pool share was dependent on the ratio at which liquidity was initially deposited, which was fairly arbitrary, especially since there was no guarantee that that ratio reflected the true price. Additionally, Uniswap v2 supports arbitrary pairs, so many pairs will not include ETH at all.
然而,这意味着流动性池份额的价值取决于初始存入流动性的比例,而这个比例相当随意,特别是因为不能保证该比例反映真实价格。此外,Uniswap v2 支持任意交易对,因此许多交易对根本不会包含 ETH。
Instead, Uniswap v2 initially mints shares equal to the geometric mean of the amounts deposited:
相反,Uniswap v2 最初铸造的份额等于存入金额的几何平均值:
$$ s_{minted} = \sqrt{x_{deposited} \cdot y_{deposited}} $$
This formula ensures that the value of a liquidity pool share at any time is essentially independent of the ratio at which liquidity was initially deposited. For example, suppose that the price of 1 ABC is currently 100 XYZ. If the initial deposit had been 2 ABC and 200 XYZ (a ratio of 1:100), the depositor would have received \(\sqrt{2 \cdot 200} = 20\) shares. Those shares should now still be worth 2 ABC and 200 XYZ, plus accumulated fees.
此公式确保任何时候流动性池份额的价值基本上独立于初始存入流动性的比例。例如,假设 1 ABC 的价格当前为 100 XYZ。如果初始存款为 2 ABC 和 200 XYZ(比例为 1:100),存款人将收到 \(\sqrt{2 \cdot 200} = 20\) 份份额。这些份额现在仍然值 2 ABC 和 200 XYZ,加上累积的费用。
If the initial deposit had been 2 ABC and 800 XYZ (a ratio of 1:400), the depositor would have received \(\sqrt{2 \cdot 800} = 40\) pool shares.10
如果初始存款为 2 ABC 和 800 XYZ(比例为 1:400),存款人将收到 \(\sqrt{2 \cdot 800} = 40\) 份池份额10。
The above formula ensures that a liquidity pool share will never be worth less than the geometric mean of the reserves in that pool. However, it is possible for the value of a liquidity pool share to grow over time, either by accumulating trading fees or through "donations" to the liquidity pool. In theory, this could result in a situation where the value of the minimum quantity of liquidity pool shares (1e-18 pool shares) is worth so much that it becomes infeasible for small liquidity providers to provide any liquidity.
上述公式确保流动性池份额的价值永远不会低于该池中储备的几何平均值。然而,流动性池份额的价值可能会随着时间的推移而增长,无论是通过累积交易费用还是通过向流动性池“捐赠”。理论上,这可能会导致最小数量的流动性池份额(1e-18 份池份额)的价值变得非常高,以至于小额流动性提供者无法提供任何流动性。
To mitigate this, Uniswap v2 burns the first 1e-15 (0.000000000000001) pool shares that are minted (1000 times the minimum quantity of pool shares), sending them to the zero address instead of to the minter. This should be a negligible cost for almost any token pair.11 But it dramatically increases the cost of the above attack. In order to raise the value of a liquidity pool share to \($100\), the attacker would need to donate $100,000 to the pool, which would be permanently locked up as liquidity.
为了缓解这种情况,Uniswap v2 销毁了最初铸造的 1e-15 (0.000000000000001) 份池份额(是最小池份额数量的 1000 倍),将其发送到零地址而不是铸造者。对于几乎任何代币对来说,这都应该是一个可以忽略不计的成本11。但它大大增加了上述攻击的成本。为了将流动性池份额的价值提高到 \($100\),攻击者需要向池中捐赠 $100,000,这将作为流动性永久锁定。
背景知识
- 流动性池份额(LP Shares)
- 当用户向 Uniswap 的流动性池存入代币时,会获得对应的流动性池份额(LP Token),代表他们在池中的所有权。
- 池份额的价值由池中代币储备的几何平均值决定(即公式 \( \sqrt{reserveA \cdot reserveB} \)),确保其价值不会低于该几何平均值。
- 最小流动性份额(1e-18)
- Uniswap v2 中,最小的流动性份额单位是 \( 10^{-18} \)(即 1 wei 的 LP Token)。这是技术限制下的最小可分割单位。
小额流动性提供者可能被挤出
- 如果流动性池份额的价值因交易费用累积或人为“捐赠”而变得非常高,那么:
- 例如,假设 1 份 LP Token 的价值涨到$100,那么最小的 \( 10^{-18} \) 份额价值就是 \( 10^{-16} \) 美元(几乎可以忽略不计)。
- 但问题在于:用户无法存入比最小份额更小的流动性。如果池份额价值极高,用户需要存入大量代币才能获得哪怕 \( 10^{-18} \) 的份额,这会让小额流动性提供者无法参与。
Uniswap v2 的解决方案:销毁初始流动性
为了缓解这一问题,Uniswap v2 在创建池子时:
- 初始铸造时销毁一部分份额
- 在池子创建时,会先铸造 \( 10^{-15} \)(即 1000 倍于最小份额)的流动性,但不分配给任何人,而是直接发送到零地址销毁。
- 这样,池中始终有至少 \( 10^{-15} \) 的流动性被永久锁定,无法被赎回。
- 攻击成本大幅提高
- 假设攻击者想通过“捐赠”代币(比如向池中无偿转入代币)来抬高 LP 份额的价值:
- 如果目标是让 1 份 LP Token 的价值达到$100,那么攻击者需要让池的几何平均值达到 \( $100 \)。
- 由于初始有 \( 10^{-15} \) 的流动性被销毁,攻击者需要让这部分被销毁的份额也值$100,因此需要让整个池子的价值大幅提高。
- 攻击者需要锁定的资金量,会根据池子中总的流动性变化,池子流动性越大,需要的资金越多。
- 实际上,Uniswap 文档中的例子是$100,000,可能是基于更合理的假设,但核心逻辑一致:攻击者需要锁定巨额资金。
- 实际效果
- 销毁初始流动性后,攻击者需要付出极高的成本才能操纵 LP 份额的价值,而小额流动性提供者仍能参与(因为最小份额的价值不会轻易被抬高到无法接受的程度)。
为什么初始销毁 \( 10^{-15} \)?
- 这是一个权衡后的值:
- 足够小:对普通用户来说,销毁 \( 10^{-15} \) 的份额成本可以忽略不计(几乎不值钱)。
- 足够大:能显著提高攻击者的成本(需要锁定更多资金)。
- 1e-15 的选择,也是为了在技术限制和安全性之间取得平衡。
Uniswap v2 通过销毁初始流动性(“最小化初始流动性”),实现了以下目标:
- 防止攻击者通过少量“捐赠”抬高 LP 份额价值,挤兑小额流动性提供者。
- 确保攻击者需付出极高成本(如永久锁定大量资金)才能操纵池子。
- 对正常用户几乎无成本影响,同时维护了池的长期可用性。
这是一种巧妙的经济安全设计,通过牺牲极小的初始流动性,换取了系统的健壮性和公平性。
3.5 Wrapping ETH (封装 ETH)
The interface for transacting with Ethereum's native asset, ETH, is different from the standard interface for interacting with ERC-20 tokens. As a result, many other protocols on Ethereum do not support ETH, instead using a canonical "wrapped ETH" token, WETH 6.
与以太坊原生资产 ETH 进行交易的接口不同于与 ERC-20 代币交互的标准接口。因此,以太坊上的许多其他协议不支持 ETH,而是使用规范的“封装 ETH”代币,WETH 6。
Uniswap v1 is an exception. Since every Uniswap v1 pair included ETH as one asset, it made sense to handle ETH directly, which was slightly more gas-efficient.
Uniswap v1 是一个例外。由于每个 Uniswap v1 交易对都包含 ETH 作为一种资产,因此直接处理 ETH 是有意义的,这样 Gas 效率会略高。
Since Uniswap v2 supports arbitrary ERC-20 pairs, it now no longer makes sense to support unwrapped ETH. Adding such support would double the size of the core codebase, and risks fragmentation of liquidity between ETH and WETH pairs12. Native ETH needs to be wrapped into WETH before it can be traded on Uniswap v2.
由于 Uniswap v2 支持任意 ERC-20 交易对,因此现在不再支持未封装的 ETH。添加此类支持将使核心代码库的大小翻倍,并存在 ETH 和 WETH 交易对之间流动性分散的风险12。原生 ETH 需要封装成 WETH 才能在 Uniswap v2 上进行交易。
3.6 Deterministic pair addresses ( 确定性的交易对地址)
As in Uniswap v1, all Uniswap v2 pair contracts are instantiated by a single factory contract. In Uniswap v1, these pair contracts were created using the CREATE opcode, which meant that the address of such a contract depended on the order in which that pair was created. Uniswap v2 uses Ethereum's new CREATE2 opcode 8 to generate a pair contract with a deterministic address. This means that it is possible to calculate a pair's address (if it exists) off-chain, without having to look at the chain state.
与 Uniswap v1 一样,所有 Uniswap v2 交易对合约都由单个工厂合约实例化。在 Uniswap v1 中,这些交易对合约是使用 CREATE 操作码创建的,这意味着此类合约的地址取决于创建该交易对的顺序。Uniswap v2 使用以太坊新的 CREATE2 操作码 8 生成具有确定性地址的交易对合约。这意味着可以在链下计算交易对的地址(如果存在),而无需查看链状态。
3.7 Maximum token balance (最大代币余额)
In order to efficiently implement the oracle mechanism, Uniswap v2 only support reserve balances of up to \(2^{112}-1\). This number is high enough to support 18-decimal-place tokens with a totalSupply over 1 quadrillion.
为了高效地实现预言机机制,Uniswap v2 仅支持最高为 \(2^{112}-1\) 的储备余额。这个数字足够大,可以支持总供应量超过 1 千万亿的 18 位小数代币。
If either reserve balance does go above \(2^{112}-1\), any call to the swap function will begin to fail (due to a check in the _update() function). To recover from this situation, any user can call the skim() function to remove excess assets from the liquidity pool.
如果任何一个储备余额超过 \(2^{112}-1\),对 swap 函数的任何调用都将开始失败(由于 _update() 函数中的检查)。为了从这种情况中恢复,任何用户都可以调用 skim() 函数从流动性池中移除多余的资产。
References
4 Disclaimer
This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment recommendations. This paper reflects current opinions of the authors and is not made on behalf of Paradigm or its affiliates and does not necessarily reflect the opinions of Paradigm, its affiliates or individuals associated with Paradigm. The opinions reflected herein are subject to change without being updated.
这份文件仅供一般信息参考。它不构成投资建议,也不构成购买或出售任何投资的推荐或招揽,不应用于评估任何投资决策的优点。不应将其作为会计、法律或税务建议或投资推荐的依据。这份文件反映了作者当前的观点,并非代表 Paradigm 或其关联公司发表,也不一定反映 Paradigm、其关联公司或与 Paradigm 相关联的个人的观点。此处反映的观点可能会发生变化,恕不另行更新。
footnode (脚注)
-
For a real-world example of how using Uniswap v1 as an oracle can make a contract vulnerable to such an attack, see 3.对于现实世界中使用 uniswap v1 作为预言机能让合约本身易受到攻击的例子,可见3。 ↩ ↩2
-
Since miners have some freedom to set the block timestamp, users of the oracle should be aware that these values may not correspond precisely to real-world times.由于矿工本身对于设置区块时间戳有一定的自由度,使用预言机的用户应该清楚获取到的时间戳不一定精准地与现实世界同步。 ↩ ↩2
-
The arithmetic mean price of asset A in terms of asset B over a given period is equal to the reciprocal of the harmonic mean price of asset B in terms of asset A over that period. If the contract measured the geometric mean price, then the prices would be the reciprocals of each other. However, the geometric mean TWAP is less commonly used, and is difficult to compute on Ethereum. 给定一段时间内,资产 A 相对于资产 B 的算术平均价格等于该段时间内资产 B 相对于资产 A 的调和平均价格的倒数。如果合约测量几何平均价格,则价格将互为倒数。然而,几何平均 TWAP 并不常用,并且在以太坊上难以计算。 ↩ ↩2
-
The theoretical upper bound of \(2^{112} - (1/2^{112})\) does not apply in this setting, as UQ112.112 numbers in Uniswap are always generated from the ratio of two uint112s. The largest such ratio is \(\frac{2^{112}-1}{1} = 2^{112}-1\) \(2^{112} - (1/2^{112})\) 的理论上限不适用于此设置,因为 Uniswap 中的 UQ112.112 数字始终由两个 uint112 的比率生成。这种最大比率是 \(\frac{2^{112}-1}{1} = 2^{112}-1\) ↩ ↩2
-
Because Uniswap charges fees on input amounts, the fee relative to the withdrawn amount is actually slightly higher: \(\frac{1}{1-0.003} - 1 = \frac{3}{997} \approx 0.3009203\%\) 由于 Uniswap 对输入金额收取费用,因此相对于提取金额的费用实际上略高:\(\frac{1}{1-0.003} - 1 = \frac{3}{997} \approx 0.3009203\%\) ↩ ↩2
-
We can use this invariant, which does not account for liquidity tokens that were minted or burned, because we know that fees are collected every time liquidity is deposited or withdrawn.
-
The signed message conforms to the EIP-712 standard, the same one used by meta transactions for tokens like CHAI and DAI. 签名消息符合 EIP-712 标准,与 CHAI 和 DAI 等代币的元交易使用的标准相同。 ↩ ↩2
-
Note that using the new architecture, \(x_{in}\) is not provided by the user; instead, it is calculated by measuring the contract's balance after the callback, \(x_1\), and subtracting \((x_0 - x_{out})\) from it. This logic does not distinguish between assets sent into the contract before it is called and assets sent into the contract during the callback. \(y_{in}\) is computed in the same way, based on \(y_0\), \(y_1\), and \(y_{out}\).
请注意,使用新架构,\(x_{in}\) 不是由用户提供的;相反,它是通过测量回调后合约的余额 \(x_1\),并从中减去 \((x_0 - x_{out})\) 来计算的。此逻辑不区分在调用合约之前发送到合约的资产和在回调期间发送到合约的资产。\(y_{in}\) 以相同的方式计算,基于\(y_0\)、\(y_1\) 和 \(y_{out}\)。 ↩ ↩2
-
As described above in section 3.2, Uniswap v2 core does not use transferFrom(). 如上文 3.2 节所述,Uniswap v2 核心不使用 transferFrom()。 ↩ ↩2
-
This also reduces the likelihood of rounding errors, since the number of bits in the quantity of shares will be approximately the mean of the number of bits in the quantity of asset X in the reserves, and the number of bits in the quantity of asset Y in the reserves: \(\log_2 \sqrt{x \cdot y} = \frac{\log_2 x + \log_2 y}{2}\) 这也降低了舍入误差的可能性,因为份额数量的位数将大约是储备中资产 X 数量的位数和储备中资产 Y 数量的位数的平均值:\(\log_2 \sqrt{x \cdot y} = \frac{\log_2 x + \log_2 y}{2}\) ↩ ↩2
-
In theory, there are some cases where this burn could be non-negligible, such as pairs between high-value zero-decimal tokens. However, these pairs are a poor fit for Uniswap anyway, since rounding errors would make trading infeasible. 理论上,在某些情况下,这种销毁可能是不可忽略的,例如在高价值零小数代币之间的交易对。然而,这些交易对无论如何都不适合 Uniswap,因为舍入误差会使交易变得不可行。 ↩ ↩2
-
As of this writing, one of the highest-liquidity pairs on Uniswap v1 is the pair between ETH and WETH7. 截至撰写本文时,Uniswap v1 上流动性最高的交易对之一是 ETH 和 WETH 之间的交易对7。 ↩ ↩2