<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>AdamShao</title>
        <link>https://paragraph.com/@adshao</link>
        <description>Coder</description>
        <lastBuildDate>Tue, 07 Apr 2026 21:27:29 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>AdamShao</title>
            <url>https://storage.googleapis.com/papyrus_images/0d5c541e9ca6a8998b20638ea6bfe41a8047627c14e7d3c6d2c572a4ad5220b2.jpg</url>
            <link>https://paragraph.com/@adshao</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Publications]]></title>
            <link>https://paragraph.com/@adshao/publications</link>
            <guid>PBv1pqdq4vSjIRliGu5V</guid>
            <pubDate>Sat, 09 Apr 2022 03:46:17 GMT</pubDate>
            <description><![CDATA[Uniswap深入理解 Uniswap v2 白皮书深入理解 Uniswap v2 合约代码深入理解 Uniswap v3 白皮书深入理解 Uniswap v3 合约代码（一）深入理解 Uniswap v3 合约代码（二）LinksHackMDGithubMirrorTwitter]]></description>
            <content:encoded><![CDATA[<h2 id="h-uniswap" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Uniswap</h2><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/@adshao/HkZwPZNf9">深入理解 Uniswap v2 白皮书</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/@adshao/rk7nI-EG9">深入理解 Uniswap v2 合约代码</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/d4GTJiyrQFigUp80IFb-gQ">深入理解 Uniswap v3 白皮书</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/TDPPCAIgRRqVDPwsSm6Kfw">深入理解 Uniswap v3 合约代码（一）</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/cTPg4x2TR4WthYEF8anLug">深入理解 Uniswap v3 合约代码（二）</a></p></li></ul><h2 id="h-links" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Links</h2><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/@adshao">HackMD</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/adshao">Github</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/adshao.eth">Mirror</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/AdamShao">Twitter</a></p></li></ul>]]></content:encoded>
            <author>adshao@newsletter.paragraph.com (AdamShao)</author>
        </item>
        <item>
            <title><![CDATA[深入理解 Uniswap v3 白皮书]]></title>
            <link>https://paragraph.com/@adshao/uniswap-v3</link>
            <guid>Oz0h0cXajoYUr9DuTvYG</guid>
            <pubDate>Sun, 13 Mar 2022 03:46:31 GMT</pubDate>
            <description><![CDATA[Github版本： https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v3-whitepaper/README.md概述Uniswap v3是一个基于以太坊虚拟机（EVM）实现的无监管自动做市商（AMM）。与之前的版本相比，Uniswap v3提高了资金利用率，赋予流动性提供者更多控制能力，改进了价格预言机的准确性和便利性，同时增加了更灵活的手续费结构。1 Introduction 介绍自动做市商（AMMs）是集中流动性，并基于算法将其开放给交易者的代理商。常值函数做市商（CFMMs）（Uniswap也是成员之一）作为AMM中的一个常见类别，已被广泛应用于去中心化金融场景，他们一般都在无需许可的区块链上以交易代币的智能合约的形式实现。 当前市场上的常值函数做市商大多存在资金利用率不高的问题。在Uniswap v1/v2使用的恒定乘积做市商公式中，对于给定价格，池子中仅部分资金参与做市。这显得十分低效，特别是当代币总是在特定价格附近交易时。注：以稳定币为例，USDC/USDT的波...]]></description>
            <content:encoded><![CDATA[<blockquote><p>Github版本：</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v3-whitepaper/README.md">https://github.com/adshao/publications/blob/master/uniswap/dive-into-uniswap-v3-whitepaper/README.md</a></p></blockquote><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">概述</h2><p>Uniswap v3是一个基于以太坊虚拟机（EVM）实现的无监管自动做市商（AMM）。与之前的版本相比，Uniswap v3提高了资金利用率，赋予流动性提供者更多控制能力，改进了价格预言机的准确性和便利性，同时增加了更灵活的手续费结构。</p><h2 id="h-1-introduction" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">1 Introduction 介绍</h2><p>自动做市商（AMMs）是集中流动性，并基于算法将其开放给交易者的代理商。常值函数做市商（CFMMs）（Uniswap也是成员之一）作为AMM中的一个常见类别，已被广泛应用于去中心化金融场景，他们一般都在无需许可的区块链上以交易代币的智能合约的形式实现。</p><p>当前市场上的常值函数做市商大多存在资金利用率不高的问题。在Uniswap v1/v2使用的恒定乘积做市商公式中，对于给定价格，池子中仅部分资金参与做市。这显得十分低效，特别是当代币总是在特定价格附近交易时。</p><blockquote><p>注：以稳定币为例，USDC/USDT的波动范围极小，而根据v2的公式，流动性提供者实际上会将资金分布在价格区间(0, 无穷大)，即使这些价格几乎永远也无法使用到。因此在Uniswap v1/v2版本，资金利用效率较低，同时也导致交易滑点相对较高。</p></blockquote><p>在此之前，Curve和YieldSpace等一些产品尝试解决这个资金利用率问题，他们通过建立池子，并使用不同的函数描述代币之间的关系。这要求池子里的所有流动性提供者都遵守同一个公式，而如果他们希望在不同的价格区间提供流动性，将导致流动性分裂。</p><p>在本文，我们将介绍Uniswap v3，一种新的自动做市商（AMM），它给予流动性提供者对资金被使用的价格区间更多控制权，并降低流动性分裂和gas消耗等问题的影响。该设计不依赖任何基于代币价格行为的共同假设。Uniswap v3仍然基于之前版本的常值函数曲线（即x * y = k），但提供许多重要的新特性：</p><ul><li><p><em>集中流动性</em>：流动性提供者（LP）将被赋予在任意价格区间集中流动性的能力。这将提高池子的资金利用率，并允许LP估算他们认可的价格曲线，同时又与池子里剩余资金一起提供高效聚合的流动性。我们将分别在第2节和第6节描述该特性及其实现。</p></li><li><p><em>灵活的手续费</em>：交易手续费将不再限定在0.30%。相反，手续费等级在每个池子初始化时设置，每一个交易对包含多个等级（池子）。默认支持手续费等级为0.05%，0.30%和1%。可以通过UNI治理增加新的手续费等级。</p><blockquote><p>注：UNI<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://app.uniswap.org/#/vote/2/9?chain=mainnet">第9号提案</a>申请引入新的手续费等级：0.01%，该提案已生效。0.01%的手续费适用于稳定币交易场景，使交易的滑点更小，这让Uniswap可以在稳定币交易领域直面Curve等市场龙头的竞争。</p></blockquote></li><li><p><em>协议手续费治理</em>：UNI治理可以灵活设置协议手续费对交易手续费的分成占比（参考6.2.2节）。</p></li><li><p><em>改进的价格预言机</em>：Uniswap v3为用户提供了一种方式查询近期累计价格，从而避免了在计算TWAP（时间加权平均价格）的时间段开头和结尾手动记录累计价格。</p></li><li><p><em>流动性预言机</em>：合约提供了一种时间加权平均流动性的预言机（参考5.3节）。</p></li></ul><p>Uniswap v2 core合约被设计成不可升级的，因此Uniswap v3是在一组全新合约上实现。Uniswap v3合约同样是不可升级的，但允许一些参数被治理修改，我们将在第4节讨论。</p><h2 id="h-2-concentrated-liquidity" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">2 Concentrated Liquidity 集中流动性</h2><p>Uniswap v3的设计思想是<em>集中流动性</em>：流动性限制在某个价格区间。</p><p>在之前的版本中，流动性被均匀分布在以下曲线：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/afd2e29990ecdd03207f7369c607944b8228940c3829b53bc3e66815a1a591d0.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>其中，x和y是两种代币X和Y的余额，k是一个常数。换句话说，之前版本被设计为给整个价格区间(0, 无穷大)提供流动性。这种方式容易实现，允许流动性被有效聚合，但也意味着池子中很多资产（流动性）永远不会被使用。</p><blockquote><p>注：比如稳定币交易对，大部分时候价格波动极小，如果像Uniswap v2一样将流动性分散到所有价格区间(0, 无穷大)，将导致资金利用率较低，因为大部分价流动性的价格区间永远不会被使用。</p></blockquote><p>考虑到这个问题，允许LP将他们的流动性集中到更小的价格区间（而非(0, 无穷大)）是合理的。我们将集中到一个有限区间的流动性称为“头寸”。一个头寸只需要维持足够的代币余额以支持该区间的交易即可，因此它与一个拥有更大代币余额（我们称为虚拟余额）的常值函数池子（在该价格区间）的运作方式很像。</p><blockquote><p>注：可以将一个v3池子的区间想象成一个v2池子的一部分。</p></blockquote><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2c67c1e0e2a1eebe2003a1ecb6fd27b133395846b57b2ab110d0816e8802cba0.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>特别地，一个头寸只需要持有足够的代币X以支持价格移动到其上限，因为当价格向上方移动时需要消耗X代币。同样，只需要持有足够的代币Y以支持价格移动到下限。图1描述了在价格区间[p_a, p_b]的头寸与当前价格p_c属于[p_a, p_b]的关系。x_{real}与y_{real}代表头寸的真实代币余额。</p><p>当价格离开头寸区间时，该头寸的流动性将不再活跃，同时无法获得手续费。在该价格点上，流动性将完全只由一种代币组成，因为另一种代币都被耗尽。如果价格重新进入区间，流动性将再次变得活跃。</p><blockquote><p>注：从图1可知，Uniswap常值函数池子的价格移动是以池子中两种代币余额的此消彼长来实现的，当价格（过高或过低）离开头寸区间，意味着其中一种代币被完全替换为另一种代币，因此此时区间中仅剩余一种代币。</p></blockquote><p>流动性数量可以用L衡量，其等价于根号k。头寸的真实代币余额可以用以下曲线表示：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6e51f64b46b066d0b6476eae202bd611a21e3fb3e1f4ae04d2063139a50d22eb.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>该曲线是公式2.1的变形，头寸只在自己的区间具有偿付能力（图2）。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6c872bd19b30a3200252c2e06353210c631dc1284a6b8e06523414a3ad5d3415.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>只要流动性提供者觉得合适，他们可以自由地创建任意数量的头寸，每个头寸拥有自己的价格区间。通过这种方式，LP可以模拟价格空间中任意有分布需求的流动性（图3列举了部分例子）。此外，这种方式可以让市场决定流动性应该分配在什么地方。理智的LP们可以通过在当前价格附近的狭窄区间集中流动性来减少资金成本，并且通过添加或移除代币来移动价格，以使他们的流动性始终保持活跃。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/16e9eeb485a5f95d092fff944dbf1e2641636e83babb52c64e8a1987254ca386.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h3 id="h-21-range-orders" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2.1 Range Orders 区间订单</h3><p>在极小区间的头寸看起来非常像限价单，如果价格穿越区间，头寸将由完全为一种资产变成另一种资产（以及累计手续费）。区间订单与传统限价单有两点不同：</p><ul><li><p>一个头寸的最小区间是有限制的。当价格正好位于头寸之内时，该限价单可能只有部分成交。</p></li><li><p>当头寸被穿越后，需要手动取回。否则，当价格再次回到区间时，头寸将自动反向交易。</p></li></ul><blockquote><p>注：如果价格反复穿越一个区间订单，头寸中的资产持仓将自动变化，从一种资产完全变成另一种资产，再反向变化，循环反复。而CEX的限价单在完全成交后，即使后期价格恢复，已成交的订单也不会回滚。</p><p>因此，如果需要实现像传统交易所一样的限价单效果，当价格穿越限价区间后，流动性提供者需要手动执行取回操作，才能完全获得另一种代币。或者可以使用第三方应用提供的自动取回功能，比如<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.gelato.network/">Gelato</a>可以支持使用Uniswap v3的区间订单实现传统限价单的效果。</p></blockquote><h2 id="h-3-architectural-changes" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3 Architectural Changes 架构变动</h2><p>Uniswap v3实现了许多架构改动，其中一部分改动是为了实现集中流动性而必须引入的，而另一部分则是独立的功能改进。</p><h3 id="h-31-multiple-pools-per-pair" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.1 Multiple Pools Per Pair 多池交易对</h3><p>在Uniswap v1和v2，每个交易对对应一个独立的流动性池子，并针对所有交易统一收取0.30%的手续费。虽然历史数据表明默认的手续费等级对于大部分代币都是合理的，但对于部分池子可能太高了（比如稳定币池子），而对于另一部分池子又太低了（比如高波动性或者冷门代币）。</p><p>Uniswap v3为每个交易对引入了多个池子，允许分别设置不同的交易手续费。所有池子都使用相同的工厂合约创建。默认允许创建三个手续费等级：0.05%，0.30%和1%。可以通过UNI治理添加更多手续费等级。</p><blockquote><p>注：目前已经通过投票新增了一个0.01%的手续费等级。</p></blockquote><h3 id="h-32-non-fungible-liquidity" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.2 Non-Fungible Liquidity 不可互换的流动性</h3><p><em>3.2.1 非复利的手续费</em>。之前版本的手续费收入会被作为流动性持续存入池子。这意味着即使没有主动存入，池子流动性也会随着时间而增长，并且可以复利地获取手续费收入。</p><p>在Uniswap v3，由于头寸的不可互换性，复利将变得不再可能。相反，手续费被独立保存，并且以支付手续费的代币形式持有（参考6.2.2）。</p><blockquote><p>注：由于每个头寸的价格区间都不一样，因此v3的流动性不再像v2一样分布在所有价格区间，也就是说，v2流动性是可互换的，因此可以使用ERC-20代币表示。而v3流动性实际上是一个NFT（不可互换代币），使用<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-721">ERC-721</a>表示。</p></blockquote><p><em>3.2.2 移除原生流动性代币</em>。在Uniswap v1和v2，交易对池子合约本身是一个ERC-20合约，它的代币表示池子持有的流动性。虽然这种表示方式很方便，但它仍然与Uniswap v2的所倡导的理念有点不一致，即：任何不需要放在core合约的东西，都应该放到periphery合约，使用一个“标准”ERC-20的实现，阻止了后续创建ERC-20代币的优化版本。按理说，ERC-20代币实现应该放到periphery合约，再作为一个流动性头寸的封装放到core合约。</p><blockquote><p>注：由于交易对合约的ERC-20实现在core合约中，并且是不可升级的，因此如果ERC-20的实现出现了bug，实际上会导致整个v2流动性受到影响。因此更好的方式是将ERC-20实现放到periphery合约中，而在core合约中仅存放一个wrapper引用，以便后续升级为新版本的ERC-20实现。</p></blockquote><p>Uniswap v3引入的改动让可互换的流动性代币变成不可能。由于自定义流动性的特性，现在手续费以独立的代币被池子收集并持有，而不是自动复投为池子的流动性。</p><p>因此，v3的池子合约没有实现ERC-20标准。任何人都可以在periphery创建一种ERC-20代币合约，以便让流动性头寸变得更可互换，但这需要额外的逻辑来处理手续费收入的分发或再投资。或者，任何人都可以创建一个periphery合约，使用一种ERC-721 NFT代币表示个人流动性头寸（包括累计手续费）。</p><h2 id="h-4-governance" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4 Governance 治理</h2><p>工厂合约拥有一个owner（所有者），该地址初始时被UNI代币持有者控制。owner没有权限暂停core合约的任何操作。</p><blockquote><p>注：在ETH主网，factory合约地址为<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x1f98431c8ad98523631ae4a59f267346ea31f984">0x1F98431c8aD98523631AE4a59f267346ea31F984</a>，owner是一个TimeLock合约，地址为<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x1a9c8182c09f50c8318d769245bea52c32be35bc">0x1a9C8182C09F50C8318d769245beA52c32BE35BC</a></p></blockquote><p>与Uniswap v2相同，Uniswap v3也有可以被UNI治理打开的协议手续费。在Uniswap v3，UNI治理可以更灵活地设置协议获取的交易手续费比例，可以将协议手续费设置为N分之一的交易手续费或者0，其中，4 &lt;= N &lt;= 10。该参数可以基于每个池子设置。</p><blockquote><p>注：Uniswap v2只能基于全局设置协议手续费，而Uniswap v3可以基于每个池子设置。</p></blockquote><p>UNI治理可以添加额外的交易手续费等级。当添加一个手续费等级时，可以同时定义其对应的tickSpacing参数（参考6.1）。一旦手续费等级被添加进工厂合约，它就无法被移除（tickSpacing也无法被修改）。初始的手续费等级和tickSpacing分别为0.05%（tickSpacing为10，两个初始化tick之间约为0.10%），0.30%（tickSpacing为60，两个初始化tick之间约为0.60%），1%（tickSpacing为200，两个初始化tick之间约为2.02%）。</p><blockquote><p>注：关于tick和tick spacing的概念，可以参考6.1节。</p><p>简单而言，每个tick（点）对应一个价格，为了聚合不同头寸的流动性，价格空间被划分为一个个可被初始化的tick，只有能被tickSpacing整除的tick才允许初始化；在tick内的交易机制与v2一样，当该tick的流动性被消耗以后，价格将进入下一个tick，并重复上述交易过程。因此tickSpacing越小意味着流动性越连续，交易滑点越小，但同时也带来了更大的gas消耗。</p><p>因此，每个手续费等级的tickSpacing是一个权衡值，但总体而言，越高的手续费等级，其tickSpacing越大。因为手续费越高，代表交易对的波动性越大，交易者能够承受的滑点也越大。</p><p>我们可以通过<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x1f98431c8ad98523631ae4a59f267346ea31f984#readContract">factory合约</a>查看链上手续费配置：feeAmountTickSpacing，目前支持的feeAmount和tickSpacing分别为：<code>{100: 1, 500: 10, 3000: 60, 10000: 200}</code>。</p><p>我们在6.1节会提到，两个相邻tick的最小价格误差为0.01%。</p></blockquote><p>最后，UNI治理有权利将owner转移给其他地址。</p><h2 id="h-5-oracle-upgrades" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5 Oracle Upgrades 预言机升级</h2><p>Uniswap v2引入了时间加权平均价格（TWAP）预言机功能，Uniswap v3的TWAP包括三个重要改动。</p><p>其中最重要的改动是Uniswap v3无需预言机用户在外部记录历史累计价格。Uniswap v2要求用户在需要计算TWAP的区间的开始和结束阶段分别记录累计价格。Uniswap v3将累计检查点放到core合约，允许外部合约直接计算最近一段时间的链上TWAP，无需额外保存累计价格。</p><p>另一个改动是Uniswap v3不再使用累计价格之和计算算术平均数TWAP，而是通过记录$log$价格之和计算几何平均数TWAP。</p><blockquote><p>注：我们在《深入理解Uniswap v2白皮书》中提到，几何平均数相比算术平均数，受极端值的影响更小，并且无需为每种代币记录单独的累计价格，因为一个代币的几何平均数价格是另一个的倒数。</p></blockquote><p>最后，除了价格累计数外，Uniswap v3还增加了一个流动性累计数，每秒累计L分之一（即流动性倒数）。累计流动性对于那些基于Uniswap v3实现流动性挖矿的外部合约很有用。它也可以被其他合约用于判断一个交易对的哪个池子具有最可信的TWAP。</p><h3 id="h-51-oracle-observations" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">5.1 Oracle Observations 预言机观测</h3><p>与Uniswap v2类似，Uniswap v3在每个区块开始记录累计价格，乘以自上一个区块到现在的时间（秒数）。</p><p>Uniswap v2的池子仅保存累计价格的最新值，该值由最近一个发生交易的区块更新。当在Uniswap v2计算平均价格时，需要由外部调用者负责提供提供累计价格的历史数据。如果有很多外部用户，每个用户都需要独立维护记录累计价格历史值的方法，或者使用一个共享方法减少成本。另外，无法保证每个有交互的区块都能影响累计价格。</p><p>在Uniswap v3，池子保存累计价格的一系列历史（如5.3节所述，也包括累计流动性）。在每个区块与池子第一次交互时，合约会自动记录累计价格，并且循环地使用新值覆盖数组中的最旧值，类似于一个环形缓冲区。虽然初始时数组仅分配一个检查点的空间，但是任何人都能够初始化额外的存储槽来扩展该数组，最多可达65,536个检查点。任何扩展该交易对检查点的人需要支付一次性的gas消耗来为数组初始化额外的存储槽。</p><blockquote><p>注：扩展检查点空间的操作是一次性的，由发起操作的人支付。比如有人希望Uniswap v3的ETH-USDC交易对能够提供更多的历史价格检查点（检查点越多，意味着使用链上数据计算的预言机价格将越可信，因为攻击者要操纵这些价格所需的成本越高），以便通过链上可以获取预言机价格，他们就会调用ETH-USDC交易对的合约接口扩展检查点空间，并为此支付gas费用，因为该操作为交易对分配了额外的EVM存储槽空间。</p></blockquote><p>交易对池子不仅向用户提供历史观测数据数组，还封装了一个便利函数用于在观测点周期内寻找任意时间点的累计价格。</p><h3 id="h-52-geometric-mean-price-oracle" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">5.2 Geometric Mean Price Oracle 几何平均数价格预言机</h3><p>Uniswap v2维护两个累计价格，一个是以token1表示的token0价格，另一个则是以token0表示的token1价格。用户可以计算任意时间段的时间加权算术平均数价格，通过将区间结尾的累计价格减去开始的累计价格，并除以区间的时间（秒数）得出。注意token0和token1的累计价格是分别跟踪的，因为两个算术平均数价格不是互为倒数关系。</p><p>Uniswap v3使用时间加权的几何平均数价格，避免了为两个代币分别维护累计价格。一组比例数的几何平均数是该比例倒数的几何平均数的倒数。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cc26019c70acae43a293ac7f0275286f62240b46c81181678c03a5cef753e67c.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>由于自定义流动性供应的实现机制（参考第6节），在Uniswap v3实现几何平均数比较简单。此外，累计数能够用更少的比特位表示，因为它只记录log{P}而不是P，log{P}可以用相同精度表示更大范围的价格。最后，理论证明时间加权的几何平均数价格更能反映真实的平均价格。</p><blockquote><p>注：为了以可接受的精度表示所有可能的价格，Uniswap v2使用224位比特的定点数表示价格。Uniswap v3仅需使用24位比特的有符号整数表示log_{1.0001}{P}，同时可以识别一个基点即0.01%的价格变动。</p><p>如前文所述，市场价格本身是一种随机布朗运动，理论上使用几何平均数更能准确跟踪平均价格，因为算术平均数更容易受到极端值的影响而产生偏差。</p></blockquote><p>Uniswap v3记录当前tick序号的累计和（log_{1.0001}{P}，即以1.0001为底的价格P对数，它可以识别1个基点即0.01%的价格变化），而不是记录累计价格$P$。任意时间点的累计数等价于该合约截止当前时间每秒对数价格（log_{1.0001}(P)）之和：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8681043296a41dab4206695f03740141675e6cae52d5b74f4ed77b60850ef464.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/541ef8abe2de09218672b70377c2f54f420c7fb380f594bc624399c906a77f9c.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>任意时间段t_1到t_2的几何平均价格（时间加权平均价格）$(p_{t_1,t_2})$为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/73492b6d951864329854d67afe22bf10e35bbff23b7b3f989be805a653cd21f3.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2a2225a36daba238fb972f6ad74fd2c5d073483ceb87dbf1a569e971c5ea3a81.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>为了计算这个值，你可以分别查看$t_1$和$t_2$时刻的累计价格，将后者减去前者，并除以时间差（秒数），最后计算$1.0001^x$得出时间加权几何平均价格：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2575e4a07c75e3fe1ca2297338989f9ed4b4fa792d1ffecfc691141b40883cc8.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h3 id="h-53-liquidity-oracle" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">5.3 Liquidity Oracle 流动性预言机</h3><p>除了每秒加权的累计数log_{1.0001}price，Uniswap v3在每个区块的开头还记录了一个每秒加权的流动性倒数L分之一（当前区间的虚拟流动性倒数）累计数：secondsPerLiquidityCumulative(s_{pl})。</p><p>这个计数可以被外部流动性挖矿合约使用，以便公平地分配奖励。如果一个外部合约希望以每秒$R$个代币的平均速率分配给合约中所有活跃的流动性，假设一个头寸从$t_0$到$t_1$的活跃流动性为$L$，则其在该时间段应该获得的奖励为：R L (s_{pl}(t_1) - s_{pl}(t_0))。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/22908732fa6511ac02c6fbc8c1016ad049ca4fa9a4e844443ecc1137dfb85df2.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>为了扩展这个公式，实现仅当流动性在头寸区间时才能获得奖励，Uniswap v3在每次tick被穿越时会保存一个基于该值计算后的检查点，我们将在第6.3节介绍。</p><p>链上合约可以使用该累计数，以使他们的预言机更健壮（比如用于评估哪个手续费等级的池子更适合被作为预言机数据源）。</p><h2 id="h-6-implementing-concentrated-liquidity" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">6 Implementing Concentrated Liquidity 实现集中流动性</h2><p>本文剩余部分将介绍集中流动性供应的实现机制，同时简要介绍其在合约是如何实现的。</p><h3 id="h-61-ticks-and-ranges" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">6.1 Ticks and Ranges 点和区间</h3><p>为了实现自定义流动性供应，可能的价格空间被离散的点（tick）划分。流动性提供者可以在任意两个（无需是临近的）tick定义的区间提供流动性。</p><p>每个区间可以被一对（有符号整数）tick序号（<em>tick indices</em>）定义：一个低点（$i_l$）和一个高点（$i_u$）。tick表示能够被合约虚拟流动性修改的价格。我们将假设价格总是以token1表示的token0的形式。token0和token1的赋值是任意的，不影响合约的逻辑（除了可能的舍入误差）。</p><p>从概念上，每当价格$p$等于1.0001的整数次方时就存在1个tick（点）。我们使用整数$i$表示tick（点），使得该点的价格可以表示为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fe0623ae9de1bf8cf00081a7fa527c6ae8906d57fbfd09b0006ad40ab1863588.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>根据定义，两个相邻的tick之间的价格移动精度为0.01%（1个基点）。</p><blockquote><p>注：见5.2节的公式推导。</p></blockquote><p>由于6.2.1节中描述的技术原因，交易对池子实际上使用开根号价格$\sqrt{price}$来跟踪tick（点），该值等于$\sqrt{1.0001}$的整数次方。可将上述等式转换为等价的开根号价格形式：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f243f18b3cf8bafcb4f4a6d1dc7e5f9106df861b6ca777590430cd2c0e425c11.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>举个例子，$\sqrt{p}(0)$（tick 0的开根号价格）等于1，$\sqrt{p}(0)$等于$\sqrt{1.0001} \approx 1.00005 $，$\sqrt{p}(-1)$等于$\frac{1}{\sqrt{1.0001}} \approx 0.99995$。</p><p>当流动性加入一个区间，如果其中一个或全部tick都没有被已存在的头寸用作边界点，该tick将被初始化。</p><p>不是每个tick都能被初始化。交易对池子在初始化时有一个参数tickSpacing($t_s$)；只有那些序号能够被tickSpacing整除的tick才能被初始化。比如，如果tickSpacing是2，则只有偶数tick (...-4, -2, 0, 2, 4...)能被初始化。小的tickSpacing允许更严格和更精确的区间，但可能导致每次交易消耗更多gas（因为每次交易穿越一个初始化的tick时，都需要给操作方带来gas消耗）。</p><p>任何时候价格穿越一个初始化的tick时，虚拟流动性将被加入或者移除。穿越一个初始化的tick所带来的gas消耗是固定的，与在该tick添加或移除虚拟流动性的头寸数量无关。</p><p>为了确保当价格穿越tick时，能够添加和移除正确数量的流动性；同时也为了确保当头寸在价格区间内时，能够正确获取对应比例的手续费收入，交易对池子需要一些记账工作。交易对合约使用存储变量来分别记录全局（每个池子）级别、每个tick级别和每个头寸级别的状态。</p><h3 id="h-62-global-state" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">6.2 Global State 全局状态</h3><p>合约的全局状态包括7个与交换和流动性供应相关的存储变量。（它也有其他一些存储变量用于预言机，如第5节描述。）</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ad5ced4ba1b34042b4e3638fb0405b2bb1d5dbe7c50203f44e706614b776225a.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h4 id="h-621-price-and-liquidity" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">6.2.1 Price and Liquidity 价格和流动性</h4><p>在Uniswap v2，每个池子合约记录池子当前的代币余额：$x$和$y$。而在Uniswap v3，合约可以被当作拥有虚拟余额，$x$和$y$值用于描述合约行为（在两个相邻的tick之间），就好像它仍遵循常值函数。</p><blockquote><p>注：Uniswap v3实际上只在一段价格区间内遵循常值函数。</p></blockquote><p>交易对合约记录两个不同值：流动性liquidity（$L$）和开根号价格sqrtPrice（$\sqrt{P}$），而不是虚拟余额。这两个值可根据虚拟余额计算如下：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e21ed854a5bca39623198c57378604f45e6581b3ee16dbfa21f83736353ff5e4.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>反过来，两种代币的虚拟余额也可以使用这两个值计算得出：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b888ba271650d8fe06f602b0b3fdb59846936b301f7b7adbc4594610860c984c.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>使用$L$和$\sqrt{P}$（而不是$x$和$y$）计算比较方便，因为一个时刻只有其中一个值会变化。当在一个tick内交易时，只有价格（即$\sqrt{P}$）发生变化；当穿越一个tick或者铸造/销毁流动性时，只有流动性（即$L$）发生变化。这避免了在记录虚拟余额时可能遇到的舍入误差问题。</p><p>你可能注意到（基于代币虚拟余额的）流动性公式（即公式6.3）与Uniswap v2用于初始化流动性代币数量的公式类似（当还没有任何手续费收入时）。流动性可以被看作虚拟流动性代币。</p><p>同样，流动性也可以被看作token1的（无论是真实还是虚拟的）数量变化与价格$\sqrt{P}$变化的比例：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c0d4d5a65243cf1c505c6df0fb1d6506c9eddd127e4e5d8fd43ee6e142186965.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2a5046c8231645c906566e10651d3a665e7a729dbbb0941c03a8d38e41287755.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>我们记录$\sqrt{P}$而不是$P$正式为了利用上述公式，如6.2.3节描述，这样也可以避免当计算交易时进行任何开根号运算。</p><p>全局状态记录当前tick序号为$tick(i_c)$，一个表示当前tick的有符号整数（更准确地说，是低于当前价格的最接近的tick）。这是一种优化策略（也是一种避免对数精度问题的方法），因为在任意时刻，你需要能够基于当前的开根号价格$sqrtPrice$计算出对应的tick。在任意时间点，以下等式总是成立：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e5cf072443beee4202776b4633bc0b763958b170f394b0d9457b24d41ff79528.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1c67ce85ca72b2f3d1b8fc4ed114149b5f623fd847b2179526e5b9837780c232.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h4 id="h-622-fees" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">6.2.2 Fees 手续费</h4><p>每个交易对池子初始化时会设置一个不可修改的手续费（$\gamma$），表示交易者需要支付的手续费，以百分之一基点为一个单位（0.0001%）。</p><blockquote><p>注：默认的手续费值为500，3000，10000，分别表示的手续费为：500 x 0.0001% = 0.05%, 3000 x 0.0001% = 0.30%, 1000 x 0.0001% = 1%。</p></blockquote><p>另一个变量为协议手续费$\phi$，初始时设置为0，但是可以通过UNI治理修改。该数字表示交易者支付手续费的部分比例将分给协议，而不是流动性提供者。$\phi$只允许被设置为以下几个合法值：0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9 或者 1/10。</p><blockquote><p>注：协议手续费开关无法在创建交易对的时候自动打开，只能由UNI治理针对具体池子单独执行手续费设置，并且可以针对不同池子分别设置协议手续费。</p></blockquote><p>全局状态还记录两个值：feeGrowthGlobal0 ($f_{g,0}$)和feeGrowthGlobal1 ($f_{g,1}$)。他们表示该合约到现在为止，每一份虚拟流动性（$L$）获取的手续费。你可以把他们理解为当合约第一次初始化时，每一份添加的非边界的流动性所获取的所有手续费。使用无符号定点数（128x128格式）表示。注意，在Uniswap v3，手续费是以原生代币形式收集，而不是流动性代币的形式（请参考3.2.1节）。</p><p>最后，全局状态记录以每种代币表示的累计未被领取的协议手续费：protocolFees0 ($f_{p,0}$)和protocolFees1 ($f_{p,1}$)。该变量以无符号uint128类型表示。累计协议手续费可以通过UNI治理领取，通过调用collectProtocol方法。</p><h4 id="h-623-swapping-within-a-single-tick-tick" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">6.2.3 Swapping Within a Single Tick 在一个Tick内交易</h4><p>对于那些无法使价格变化超过一个tick（点）的小额交易，该合约像一个 $x \cdot y = k$ 池子一样工作。</p><p>假设 $\gamma$ 是交易手续费，比如0.003，$y_{in}$ 是传入的token1代币数量。</p><p>首先，feeGrowthGlobal1和protocolFees1将增加：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7bbd35193a248202d1b43b4d506a15a6fb36d2e99a323a0a8e8aae3dbe486160.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>注：$\phi$是协议手续费占手续费的比例，因此协议手续费比例为：$\gamma \cdot \phi$，协议手续费收入为公式6.10。</p><p>剩余的手续费分给流动性提供者，即扣除协议手续费后的交易手续费，其比例为：$\gamma \cdot (1 - \phi)$，交易手续费收入为公式6.9。</p></blockquote><p>$\Delta y$是$y$的增加量（当手续费扣除后）。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/68084f78ffed563b300bbde8f7b62d416110004f2b4aac87c8b547a51efe220a.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>如果你用经过计算的虚拟余额（$x$和$y$）为token0和token1的数量，以下公式可以计算出交易后的token0的代币数量：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/de109344515784eda50a1e5a858462b85ef669f7e98315566be59aa274863c86.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6164f0c1290c12834ad0caf5435b97d7ce94191b32368f867cc14a1d10bb98c4.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>但是请注意，在v3，合约使用流动性（$L$）和开根号价格（$\sqrt{P}$）代替$x$和$y$。我们可以使用这两个值计算$x$和$y$，然后计算交易的成交价格。但是，对于给定的$L$，我们可以推导出简洁的等式描述$\Delta{\sqrt{P}}$和$\Delta{y}$的关系（可根据公式6.7推出）：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5ffc0dc977a0671c39ca4c4ae0d7e289352ccf2d20c049dd3cb0cc0e362ec074.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>同时可以推导出$\Delta{\frac{1}{\sqrt{P}}}$和$\Delta{x}$的关系：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2c7c7e67df9fc220e523b0595ba83d9e6d76841f3dc6e39008e054e49434035a.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4baf6e3f0f89dc93e4e0c30f2669f0c0c0f80d57e5b0e53073a2933aa3c2e2ff.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>当使用一种代币交换另一种时，交易对合约可以先根据公式6.13或6.15计算新的开根号价格$\sqrt{P}$，接着根据公式6.14或6.16计算需要转出的token0和token1代币数量。</p><p>对于任意交易，只要交易后的开根号价格$\sqrt{P}$没有进入下一个初始化的tick所在的价格，上述公式都可以正常工作。如果计算后的$\Delta{\sqrt{P}}$将使得$\sqrt{P}$进入下一个初始化的tick，合约将完成当前tick（仅占一部分交易），再继续进入下一个tick完成剩余的交易，参考6.3.1节。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f2c6a2066cffc312b77a4672c060dcea0976b1a5947d707df3cb5b54a793d9b5.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h4 id="h-624-initialized-tick-bitmap-tick" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">6.2.4 Initialized Tick Bitmap 初始化Tick的位图</h4><p>如果一个tick没有被用作流动性区间的边界点（即如果该tick没有被初始化），那么在交易过程中可以跳过这个tick。</p><p>为了更高效寻找下一个已初始化的tick，合约使用一个位图tickBitmap记录已初始化的tick。如果tick已被初始化，位图中对应于该tick序号的位置设置为1，否则为0。</p><p>当tick被一个新头寸用作边界点，并且该tick没有被任何其他流动性使用，那么它将被初始化，位图中对应的比特位置为1。当该点关联的流动性都被移除时，已初始化的tick将重新变成未初始化，位图中对应的比特位置为0。</p><h3 id="h-63-tick-indexed-state-tick" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">6.3 Tick-Indexed State Tick索引状态</h3><p>为了记录每个tick被穿越时需要添加和移除的净流动性，以及在大于和小于该tick时所挣取的手续费，合约需要额外保存每个tick相关的信息。</p><p>合约保存一个映射表，每个tick序号对应以下7个变量：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a2de50f8515034b4977d65003d8db53427bf593bbf70a1c5e43b233b8e7a9734.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>每个tick记录$\Delta{L}$，表示当该tick被完全穿越时需要加入和移除的总流动性数量。Tick只需要记录一个有符号整数：当交易促使tick值从左到右移动时，需要往该tick注入的流动性（反之，当tick值从右到左移动时，该值为负，表示移除流动性）。该值无需在每次价格穿越tick时更新（只需在使用该tick作为边界点的头寸更新时才更新）。</p><p>当tick没有流动性关联时，我们希望对其取消初始化。因为$\Delta{L}$是一个净值，还需要记录该tick关联的总流动性：liquidityGross。该值确保即使净流动性为0，我们仍能知道该tick是否被至少一个头寸关联，以此决定是否更新tick位图。</p><p>feeGrowthOutside{0, 1}用于记录一个给定区间总共累计多少手续费。因为token0和token1收集手续费的公式相同，我们在本节剩余的公式中将忽略（token0和token1）下标。</p><p>根据当前价格是否在区间内，你可以使用一个公式计算每份流动性在tick $i$之上（$f_a$）和之下（$f_b$）获取的手续费（根据当前tick序号$i_c$是否大于等于$i$）：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/91b36d26274f3185f33796086494f98b44161c2116c57d290316ce787850e6d5.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8d954917e147a5edb8b575533bb02bd99a76a6a9fe7788620333f7ad9bed0288.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>我们可以使用上述函数计算任意两个tick（低点tick $i_l$和高点tick $i_u$）区间内，每个流动性累计的全部手续费$f_r$：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2c375e19462fbb41c8e05fa25d88e41b79e54e8b1605aa6c9faf68cf9d216253.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/56d71f5c2df60457433847b07fdd8c51ca5bdb62f61eb4fe33489008a46a5fad.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>$f_o$需要在每次tick穿越时被更新。特别地，当tick被反方向穿越时，其对应的$f_o$（token0和token1）需要按照如下方式更新：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/77903fd1bf0cd3f8513c899b582bb5888b586d7e7a601fc1dd63ed416fcab0d8.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>只有被至少一个头寸作为边界端点的tick才需要$f_o$。因此，出于效率考虑，$f_o$不会被初始化（当tick被穿越时无需被更新），只有当使用该tick作为边界点创建头寸时才会初始化。当tick $i$的$f_o$初始化时，它的初始值被设置成当前所有的手续费都由小于该tick时收取：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/dbb43fb1a05c9f01ec746ba11acc33527a54278e66e16507c6f4cd31037aaf96.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>注意，因为不同tick的$f_0$值可以在不同时刻初始化，因此比较他们的$f_0$是无意义的，实际上无法保证$f_0$值不变。但这不会导致每个头寸的统计问题，如下文描述，所有的头寸只需要知道从上一次交互后，区间内的$g$值增长即可。</p><p>最后，合约同时为每个tick保存secondsOutside ($s_o$)，secondsPerLiquidityOutside和tickCumulativeOutside。这些变量不会被合约内部使用，而是帮助外部合约（如基于v3的流动性挖矿合约）更方便地获取合约信息。</p><p>这三个变量于上文提到的手续费增长变量类似。但是不同于feeGrowthOutside{0, 1}跟踪feeGrowthGlobal{0, 1}，secondsOutside跟踪seconds（也就是当前时间戳），secondsPerLiquidityOutside跟踪5.3节中描述的${1}/{L}$累计数（secondsPerLiquidityCumulative）；tickCumulativeOutside跟踪第5.2节中描述的$\log_{1.0001}P$累计数。</p><p>比如，对于一个给定的tick，根据当前价格是否在区间内，$s_a$与$s_b$分别为大于与小于tick $i$时持续的时长（秒数），$s_r$为区间内持续的秒数，其计算方式分别为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2cc0ae4bbc577cbd91faa591d3722fe95d03945ff3214c18850d6a21a985efe7.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>在$t_1$到$t_2$时间段内，头寸在价格区间内的持续时间可以通过记录$t_1$和$t_2$时间点的$s_r(i_l, i_u)$值，并将后者减去前者得到。</p><p>和$f_o$类似，对于不是头寸边界点的tick无需记录$s_o$。因此，只有使用该tick作为边界点的头寸创建时，才需要初始化$s_o$。为了方便，初始的默认值为截止到当前时间的秒数，并都发生在小于该tick的时候：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b2454968869ce36e9aa3caffe45bb5d19ba334711ae90bcedabdf6b9a6a2391d.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>与$f_o$值类似，比较不同tick的$t_o$值也是无意义的。仅当计算一个时间段（起始时间需在两个tick的$t_0$初始化之后）的指定价格区间的流动性持续时间时，$t_0$才是有意义的。</p><h4 id="h-631-crossing-a-tick-tick" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">6.3.1 Crossing a Tick 穿越一个Tick</h4><p>如6.2.3节描述，当在初始化的tick之间交易时，Uniswap v3可以像$k$常值函数一样工作。但是，当交易穿越一个已初始化的tick时，合约需要添加或移除流动性，以确保没有流动性提供者会破产。这意味着$\Delta{L}$是从tick中提取，并应用到全局$L$中。</p><p>为了记录在价格区间内时，该tick作为边界点的手续费收入（和持续时间），合约需要更新tick的状态。feeGrowthOutside{0, 1}和secondsOutside被更新到反映当前值，当与该tick关联的交易方向改变时，按照下述公式更新：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/671edc728217b86fc6509124cfd999f16ab2f9f325927c594480d4674f70aafb.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>当一个tick被穿越后，如6.2.3节描述，交易将继续直到碰到下一个已初始化的tick。</p><h3 id="h-64-position-indexed-state" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">6.4 Position-Indexed State 头寸索引状态</h3><p>合约记录一个映射表，从用户地址，头寸低点（左边界，一个tick序号，int24类型）和高点（右边界，一个tick序号，int24类型）到具体头寸信息的映射关系。每个头寸记录三个值：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1dbfd4a8ccce2b25d5c749c794f496fff0f27c36c2d4214f5056aac8e5e608d8.jpg" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>liquidity（$l$）表示上一次头寸更新时，该头寸所表示的虚拟流动性数量。特别地，liquidity可以被看作$\sqrt{x \cdot y}$，其中$x$和$y$分别表示在任意时刻该头寸进入价格区间时，由对应的虚拟token0和token1数量表示的加入池子的流动性。与Uniswap v2不同（每个流动性份额随时间增长），v3的流动性份额并不改变，因为手续费是单独累计；它总是等价于$\sqrt{x \cdot y}$，其中，$x$和$y$分别表示token0和token1的数量。</p><p>liquidity（流动性）数量不代表从合约上次交互后的累计手续费，uncollected fees才用于表示未领取的手续费。为了计算未领取的手续费，需要在头寸保存额外信息，如feeGrowthInside0ast（$f_{r,0}(t_o)$）和feeGrowthInside1Last($f_{r,1}(t_0)$)，如下文所述。</p><h4 id="h-641-setposition" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">6.4.1 setPosition 设置头寸</h4><p>setPosition方法允许流动性提供者更新他们的头寸。</p><p>setPosition的两个参数：lowerTick和upperTick，与调用者msg.sender一起组成了头寸的信息。</p><p>该方法接受一个额外参数：liquidityDelta，用于指定用户希望添加或移除（负值）的虚拟流动性。</p><p>首先，该方法计算头寸的未领取手续费（$f_u$）（分别以两种代币表示）。头寸所有者获取的手续费，将其减去用户添加或移除虚拟流动性，即为净收入。</p><p>为了计算一个代币的未领取手续费，你需要知道自从上一次领取手续费后，该头寸对应的区间获得多少手续费$f_r$（如6.3描述，使用区间$i_l$, $i_r$计算）。从$t_0$到$t_1$时间段，区间内每份流动性的的手续费增长为：$f_r(t_1) - f_r(t_0)$（其中，$f_r(t_0)$在头寸中以feeGrowthInside{0, 1}Last保存，$f_r(t_1)$能够从当前tick状态中计算）。将其乘以头寸的流动性，即为以token0表示的该头寸未领取的手续费：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7c95ecc4c3d117a59e1c060aada077f3d30584aa80f936e8b1db3ca768be26ba.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>接着，合约将liquidityDelta加到头寸的liquidity（流动性）。在tick区间低点，它同时将liquidityDelta加到liquidityNet（注：tick从左到右，表示加入流动性）；而在头寸的高点，则从liquidityNet减去liquidityDelta（注：tick从右到左，表示移除流动性）。如果池子当前价格在头寸区间内，合约也会将liquidity加到全局的globalLiquidity。</p><p>最后，根据销毁或铸造的流动性数量，池子将代币从用户转出（如果liquidityDelta是负值，则将代币转给用户）。</p><p>如果价格从当前价格（$P$）移动到高点或低点，需要存入的token0（$\Delta{X}$）和token1（$\Delta{Y}$）代币的数量可以被看作从头寸中卖出对应数量的代币。根据价格是否低于区间、在区间内或者高于区间，可以从6.14节和6.16节公式推出以下公式：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/54a9971ed5e0b8c3c4e502ee55a76a3f0136fcc5d7ad6573f9a0feb638b63a75.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">引用文献</h2><ul><li><p>[1] Hayden Adams, Noah Zinsmeister, and Dan Robinson. 2020. Uniswap v2 Core. Retrieved Feb 24, 2021 from <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://uniswap.org/whitepaper.pdf">https://uniswap.org/whitepaper.pdf</a></p></li><li><p>[2] Guillermo Angeris and Tarun Chitra. 2020. Improved Price Oracles: Constant Function Market Makers. In Proceedings of the 2nd ACM Conference on Advances in Financial Technologies (AFT ’20). Association for Computing Machinery, New York,NY,UnitedStates, 80–91. <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://doi.org/10.1145/3419614.3423251">https://doi.org/10.1145/3419614.3423251</a></p></li><li><p>[3] Michael Egorov. 2019. StableSwap - Efficient Mechanism for Stablecoin Liquidity. Retrieved Feb 24, 2021 from <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.curve.fi/stableswap-paper.pdf">https://www.curve.fi/stableswap-paper.pdf</a></p></li><li><p>[4] Allan Niemerg, Dan Robinson, and Lev Livnev. 2020. YieldSpace: An Automated Liquidity Provider for Fixed Yield Tokens. Retrieved Feb 24, 2021 from <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://yield.is/YieldSpace.pdf">https://yield.is/YieldSpace.pdf</a></p></li><li><p>[5] Abraham Othman. 2012. Automated Market Making: Theory and Practice. Ph.D. Dissertation. Carnegie Mellon University.</p></li></ul>]]></content:encoded>
            <author>adshao@newsletter.paragraph.com (AdamShao)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/4e3bd7bfc4a0597cd84990bdf019902c5ba598687d2c358ce115f4f21c33da6d.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[深入理解 Uniswap v2 合约代码]]></title>
            <link>https://paragraph.com/@adshao/uniswap-v2-2</link>
            <guid>gfwR4xJ4Gl9TXjkWNEEV</guid>
            <pubDate>Sun, 27 Feb 2022 03:48:56 GMT</pubDate>
            <description><![CDATA[上文介绍了《深入理解 Uniswap v2 白皮书》，今天我们来讲解Uniswap v2合约代码。本文不会逐行介绍合约代码，而是关注合约架构和重点方法，如果需要详细的代码说明，推荐阅读以太坊官方的Uniswap v2代码走读。合约架构Uniswap v2的合约主要分为两类：core合约和periphery合约。其中，core合约仅包含最基础的交易功能，核心代码仅200行左右，由于用户资金都存储在core合约里，因此需要保证core合约最简化，避免引入bug；periphery合约则针对用户使用场景提供多种封装方法，比如支持原生ETH交易（自动转为WETH），多路径交换（一个方法同时执行A→B→C交易）等，其底层调用的是core合约。我们在app.uniswap.org界面操作时用的就是periphery合约。uniswap v2 合约架构我们先介绍几个主要合约的功能：uniswap-v2-coreUniswapV2Factory：工厂合约，用于创建Pair合约（以及设置协议手续费接收地址）UniswapV2Pair：Pair（交易对）合约，定义和交易有关的几个最基础方法，如swa...]]></description>
            <content:encoded><![CDATA[<p>上文介绍了《<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/adshao.eth/qmzSfrOB8s6_-s1AsflYNqEkTynShdpBE0EliqjGC1U">深入理解 Uniswap v2 白皮书</a>》，今天我们来讲解Uniswap v2合约代码。</p><blockquote><p>本文不会逐行介绍合约代码，而是关注合约架构和重点方法，如果需要详细的代码说明，推荐阅读以太坊官方的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethereum.org/en/developers/tutorials/uniswap-v2-annotated-code/#introduction">Uniswap v2代码走读</a>。</p></blockquote><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约架构</h2><p>Uniswap v2的合约主要分为两类：core合约和periphery合约。其中，core合约仅包含最基础的交易功能，核心代码仅200行左右，由于用户资金都存储在core合约里，因此需要保证core合约最简化，避免引入bug；periphery合约则针对用户使用场景提供多种封装方法，比如支持原生ETH交易（自动转为WETH），多路径交换（一个方法同时执行A→B→C交易）等，其底层调用的是core合约。我们在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://app.uniswap.org/#/swap">app.uniswap.org</a>界面操作时用的就是periphery合约。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/91f3cc0738e8b379bffcacf3b437ecbe6786ec3441a72e1e24c15e859377b22b.png" alt="uniswap v2 合约架构" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">uniswap v2 合约架构</figcaption></figure><p>我们先介绍几个主要合约的功能：</p><ul><li><p>uniswap-v2-core</p><ul><li><p>UniswapV2Factory：工厂合约，用于创建Pair合约（以及设置协议手续费接收地址）</p></li><li><p>UniswapV2Pair：Pair（交易对）合约，定义和交易有关的几个最基础方法，如swap/mint/burn，价格预言机等功能，其本身是一个ERC20合约，继承UniswapV2ERC20</p></li><li><p>UniswapV2ERC20：实现ERC20标准方法</p></li></ul></li><li><p>uniswap-v2-periphery</p><ul><li><p>UniswapV2Router02：最新版的路由合约，相比UniswapV2Router01增加了对FeeOnTransfer代币的支持；实现Uniswap v2最常用的接口，比如添加/移除流动性，使用代币A交换代币B，使用ETH交换代币等</p></li><li><p>UniswapV1Router01：旧版本Router实现，与Router02类似，但不支持FeeOnTransferTokens，目前已不使用</p></li></ul></li></ul><h2 id="h-uniswap-v2-core" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">uniswap-v2-core</h2><p>代码地址：</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Uniswap/v2-core">https://github.com/Uniswap/v2-core</a></p><h3 id="h-uniswapv2factory" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">UniswapV2Factory</h3><p>在工厂合约中最重要的是createPair方法：</p><pre data-type="codeBlock" text="function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, &apos;UniswapV2: IDENTICAL_ADDRESSES&apos;);
    (address token0, address token1) = tokenA &lt; tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), &apos;UniswapV2: ZERO_ADDRESS&apos;);
    require(getPair[token0][token1] == address(0), &apos;UniswapV2: PAIR_EXISTS&apos;); // single check is sufficient
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair; // populate mapping in the reverse direction
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createPair</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> tokenA, <span class="hljs-keyword">address</span> tokenB</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span> pair</span>) </span>{
    <span class="hljs-built_in">require</span>(tokenA <span class="hljs-operator">!</span><span class="hljs-operator">=</span> tokenB, <span class="hljs-string">'UniswapV2: IDENTICAL_ADDRESSES'</span>);
    (<span class="hljs-keyword">address</span> token0, <span class="hljs-keyword">address</span> token1) <span class="hljs-operator">=</span> tokenA <span class="hljs-operator">&#x3C;</span> tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    <span class="hljs-built_in">require</span>(token0 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">'UniswapV2: ZERO_ADDRESS'</span>);
    <span class="hljs-built_in">require</span>(getPair[token0][token1] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">'UniswapV2: PAIR_EXISTS'</span>); <span class="hljs-comment">// single check is sufficient</span>
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> bytecode <span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(UniswapV2Pair).<span class="hljs-built_in">creationCode</span>;
    <span class="hljs-keyword">bytes32</span> salt <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(token0, token1));
    <span class="hljs-keyword">assembly</span> {
        pair <span class="hljs-operator">:=</span> <span class="hljs-built_in">create2</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">add</span>(bytecode, <span class="hljs-number">32</span>), <span class="hljs-built_in">mload</span>(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] <span class="hljs-operator">=</span> pair;
    getPair[token1][token0] <span class="hljs-operator">=</span> pair; <span class="hljs-comment">// populate mapping in the reverse direction</span>
    allPairs.<span class="hljs-built_in">push</span>(pair);
    <span class="hljs-keyword">emit</span> PairCreated(token0, token1, pair, allPairs.<span class="hljs-built_in">length</span>);
}
</code></pre><p>首先将token0 token1按照顺序排序，确保token0字面地址小于token1。接着使用assembly + create2创建合约。<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/develop/assembly.html#inline-assembly">assembly</a>可以在Solidity中使用<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/develop/yul.html#yul">Yul</a>语言直接操作EVM，是较底层的操作方法。我们在《深入理解 Uniswap v2 白皮书》中讲到，create2主要用于创建确定性的交易对合约地址，目的是根据两个代币地址直接计算pair地址，而无需调用链上合约查询。</p><p>CREATE2出自<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-1014">EIP-1014</a>，根据规范，这里能够影响最终生成地址的是用户自定义的salt值，只需要保证每次生成交易对合约时提供的salt值不同即可，对于同一个交易对的两种代币，其salt值应该一样；这里很容易想到应该使用交易对的两种代币地址，我们希望提供A/B地址的时候可以直接算出pair(A,B)，而两个地址又受顺序影响，因此在合约开始时先对两种代币进行排序，确保其按照从小到大的顺序生成salt值。</p><p>实际上在最新版的EMV中，已经直接支持给new方法传递salt参数，如下所示：</p><pre data-type="codeBlock" text="pair = new UniswapV2Pair{salt: salt}();
"><code><span class="hljs-attr">pair</span> = new UniswapV2Pair{salt: salt}()<span class="hljs-comment">;</span>
</code></pre><p>因为Uniswap v2合约在开发时还没有这个功能，所以使用assembly create2。</p><p>根据<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/develop/yul.html#yul">Yul规范</a>，create2的定义如下：</p><blockquote><p>create2(v, p, n, s)</p><p>create new contract with code mem[p…(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p…(p+n))) and send v wei and return the new address, where <code>0xff</code> is a 1 byte value, <code>this</code> is the current contract’s address as a 20 byte value and <code>s</code> is a big-endian 256-bit value; returns 0 on error</p></blockquote><p>源码中调用create2方法：</p><pre data-type="codeBlock" text="pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
"><code>pair := <span class="hljs-built_in">create2</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">add</span>(bytecode, <span class="hljs-number">32</span>), <span class="hljs-built_in">mload</span>(bytecode), salt)
</code></pre><p>因此，这几个参数含义如下：</p><ul><li><p>v=0：向新创建的pair合约中发送的ETH代币数量（单位wei）</p></li><li><p>p=add(bytecode, 32)：合约字节码的起始位置</p><ul><li><p>此处为什么要add 32呢？因为bytecode类型为bytes，根据<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/develop/abi-spec.html">ABI规范</a>，bytes为变长类型，在编码时前32个字节存储bytecode的长度，接着才是bytecode的真正内容，因此合约字节码的起始位置在bytecode+32字节</p></li></ul></li><li><p>n=mload(bytecode)：合约字节码总字节长度</p><ul><li><p>根据上述说明，bytecode前32个字节存储合约字节码的真正长度（以字节为单位），而mload的作用正是读出传入参数的前32个字节的值，因此mload(bytecode)就等于n</p></li></ul></li><li><p>s=salt：s为自定义传入的salt，即token0和token1合并编码</p></li></ul><h3 id="h-uniswapv2erc20" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">UniswapV2ERC20</h3><p>这个合约主要定义了UniswapV2的ERC20标准实现，代码比较简单。这里介绍下permit方法：</p><pre data-type="codeBlock" text="function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
    require(deadline &gt;= block.timestamp, &apos;UniswapV2: EXPIRED&apos;);
    bytes32 digest = keccak256(
        abi.encodePacked(
            &apos;\x19\x01&apos;,
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
        )
    );
    address recoveredAddress = ecrecover(digest, v, r, s);
    require(recoveredAddress != address(0) &amp;&amp; recoveredAddress == owner, &apos;UniswapV2: INVALID_SIGNATURE&apos;);
    _approve(owner, spender, value);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">permit</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> owner, <span class="hljs-keyword">address</span> spender, <span class="hljs-keyword">uint</span> value, <span class="hljs-keyword">uint</span> deadline, <span class="hljs-keyword">uint8</span> v, <span class="hljs-keyword">bytes32</span> r, <span class="hljs-keyword">bytes32</span> s</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-built_in">require</span>(deadline <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>, <span class="hljs-string">'UniswapV2: EXPIRED'</span>);
    <span class="hljs-keyword">bytes32</span> digest <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
        <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(
            <span class="hljs-string">'\x19\x01'</span>,
            DOMAIN_SEPARATOR,
            <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]<span class="hljs-operator">+</span><span class="hljs-operator">+</span>, deadline))
        )
    );
    <span class="hljs-keyword">address</span> recoveredAddress <span class="hljs-operator">=</span> <span class="hljs-built_in">ecrecover</span>(digest, v, r, s);
    <span class="hljs-built_in">require</span>(recoveredAddress <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>) <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> recoveredAddress <span class="hljs-operator">=</span><span class="hljs-operator">=</span> owner, <span class="hljs-string">'UniswapV2: INVALID_SIGNATURE'</span>);
    _approve(owner, spender, value);
}
</code></pre><p>permit方法实现的就是白皮书2.5节中介绍的“Meta transactions for pool shares 元交易”功能。<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-712">EIP-712</a>定义了离线签名的规范，即digest的格式定义，用户签名的内容是其（owner）授权（approve）某个合约（spender）可以在截止时间（deadline）之前花掉一定数量（value）的代币（Pair流动性代币），应用（periphery合约）拿着签名的原始信息和签名后生成的v, r, s，可以调用Pair合约的permit方法获得授权，permit方法使用ecrecover还原出签名地址为代币所有人，验证通过则批准授权。</p><h3 id="h-uniswapv2pair" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">UniswapV2Pair</h3><p>Pair合约主要实现了三个方法：mint（添加流动性）、burn（移除流动性）、swap（兑换）。</p><h4 id="h-mint" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">mint</h4><p>该方法实现添加流动性功能。</p><pre data-type="codeBlock" text="// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(token1).balanceOf(address(this));
    uint amount0 = balance0.sub(_reserve0);
    uint amount1 = balance1.sub(_reserve1);

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    if (_totalSupply == 0) {
        liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
        _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
    } else {
        liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
    }
    require(liquidity &gt; 0, &apos;UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED&apos;);
    _mint(to, liquidity);

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Mint(msg.sender, amount0, amount1);
}
"><code><span class="hljs-comment">// this low-level function should be called from a contract which performs important safety checks</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">lock</span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> liquidity</span>) </span>{
    (<span class="hljs-keyword">uint112</span> _reserve0, <span class="hljs-keyword">uint112</span> _reserve1,) <span class="hljs-operator">=</span> getReserves(); <span class="hljs-comment">// gas savings</span>
    <span class="hljs-keyword">uint</span> balance0 <span class="hljs-operator">=</span> IERC20(token0).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    <span class="hljs-keyword">uint</span> balance1 <span class="hljs-operator">=</span> IERC20(token1).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    <span class="hljs-keyword">uint</span> amount0 <span class="hljs-operator">=</span> balance0.sub(_reserve0);
    <span class="hljs-keyword">uint</span> amount1 <span class="hljs-operator">=</span> balance1.sub(_reserve1);

    <span class="hljs-keyword">bool</span> feeOn <span class="hljs-operator">=</span> _mintFee(_reserve0, _reserve1);
    <span class="hljs-keyword">uint</span> _totalSupply <span class="hljs-operator">=</span> totalSupply; <span class="hljs-comment">// gas savings, must be defined here since totalSupply can update in _mintFee</span>
    <span class="hljs-keyword">if</span> (_totalSupply <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        liquidity <span class="hljs-operator">=</span> Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
        _mint(<span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), MINIMUM_LIQUIDITY); <span class="hljs-comment">// permanently lock the first MINIMUM_LIQUIDITY tokens</span>
    } <span class="hljs-keyword">else</span> {
        liquidity <span class="hljs-operator">=</span> Math.<span class="hljs-built_in">min</span>(amount0.mul(_totalSupply) <span class="hljs-operator">/</span> _reserve0, amount1.mul(_totalSupply) <span class="hljs-operator">/</span> _reserve1);
    }
    <span class="hljs-built_in">require</span>(liquidity <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'</span>);
    _mint(to, liquidity);

    _update(balance0, balance1, _reserve0, _reserve1);
    <span class="hljs-keyword">if</span> (feeOn) kLast <span class="hljs-operator">=</span> <span class="hljs-keyword">uint</span>(reserve0).mul(reserve1); <span class="hljs-comment">// reserve0 and reserve1 are up-to-date</span>
    <span class="hljs-keyword">emit</span> Mint(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount0, amount1);
}
</code></pre><p>首先，getReserves() 获取两种代币的缓存余额。在白皮书中提到，保存缓存余额是为了防止攻击者操控价格预言机。此处还用于计算协议手续费，并通过当前余额与缓存余额相减获得转账的代币数量。</p><p>_mintFee用于计算协议手续费：</p><pre data-type="codeBlock" text="// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
    address feeTo = IUniswapV2Factory(factory).feeTo();
    feeOn = feeTo != address(0);
    uint _kLast = kLast; // gas savings
    if (feeOn) {
        if (_kLast != 0) {
            uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
            uint rootKLast = Math.sqrt(_kLast);
            if (rootK &gt; rootKLast) {
                uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                uint denominator = rootK.mul(5).add(rootKLast);
                uint liquidity = numerator / denominator;
                if (liquidity &gt; 0) _mint(feeTo, liquidity);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}
"><code><span class="hljs-comment">// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_mintFee</span>(<span class="hljs-params"><span class="hljs-keyword">uint112</span> _reserve0, <span class="hljs-keyword">uint112</span> _reserve1</span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> feeOn</span>) </span>{
    <span class="hljs-keyword">address</span> feeTo <span class="hljs-operator">=</span> IUniswapV2Factory(factory).feeTo();
    feeOn <span class="hljs-operator">=</span> feeTo <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">uint</span> _kLast <span class="hljs-operator">=</span> kLast; <span class="hljs-comment">// gas savings</span>
    <span class="hljs-keyword">if</span> (feeOn) {
        <span class="hljs-keyword">if</span> (_kLast <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">uint</span> rootK <span class="hljs-operator">=</span> Math.sqrt(<span class="hljs-keyword">uint</span>(_reserve0).mul(_reserve1));
            <span class="hljs-keyword">uint</span> rootKLast <span class="hljs-operator">=</span> Math.sqrt(_kLast);
            <span class="hljs-keyword">if</span> (rootK <span class="hljs-operator">></span> rootKLast) {
                <span class="hljs-keyword">uint</span> numerator <span class="hljs-operator">=</span> totalSupply.mul(rootK.sub(rootKLast));
                <span class="hljs-keyword">uint</span> denominator <span class="hljs-operator">=</span> rootK.mul(<span class="hljs-number">5</span>).add(rootKLast);
                <span class="hljs-keyword">uint</span> liquidity <span class="hljs-operator">=</span> numerator <span class="hljs-operator">/</span> denominator;
                <span class="hljs-keyword">if</span> (liquidity <span class="hljs-operator">></span> <span class="hljs-number">0</span>) _mint(feeTo, liquidity);
            }
        }
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (_kLast <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        kLast <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
    }
}
</code></pre><p>关于协议手续费的计算公式可以参考白皮书。</p><p>mint方法中判断，如果是首次提供该交易对的流动性，则根据根号xy生成流动性代币，并销毁其中的MINIMUM_LIQUIDITY（即1000wei）；否则根据转入的代币价值与当前流动性价值比例铸造流动性代币。</p><h4 id="h-burn" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">burn</h4><p>该方法实现移除流动性功能。</p><pre data-type="codeBlock" text="// this low-level function should be called from a contract which performs important safety checks
function burn(address to) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    address _token0 = token0;                                // gas savings
    address _token1 = token1;                                // gas savings
    uint balance0 = IERC20(_token0).balanceOf(address(this));
    uint balance1 = IERC20(_token1).balanceOf(address(this));
    uint liquidity = balanceOf[address(this)];

    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
    amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
    amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
    require(amount0 &gt; 0 &amp;&amp; amount1 &gt; 0, &apos;UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED&apos;);
    _burn(address(this), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));

    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Burn(msg.sender, amount0, amount1, to);
}
"><code><span class="hljs-comment">// this low-level function should be called from a contract which performs important safety checks</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">burn</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">lock</span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amount0, <span class="hljs-keyword">uint</span> amount1</span>) </span>{
    (<span class="hljs-keyword">uint112</span> _reserve0, <span class="hljs-keyword">uint112</span> _reserve1,) <span class="hljs-operator">=</span> getReserves(); <span class="hljs-comment">// gas savings</span>
    <span class="hljs-keyword">address</span> _token0 <span class="hljs-operator">=</span> token0;                                <span class="hljs-comment">// gas savings</span>
    <span class="hljs-keyword">address</span> _token1 <span class="hljs-operator">=</span> token1;                                <span class="hljs-comment">// gas savings</span>
    <span class="hljs-keyword">uint</span> balance0 <span class="hljs-operator">=</span> IERC20(_token0).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    <span class="hljs-keyword">uint</span> balance1 <span class="hljs-operator">=</span> IERC20(_token1).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    <span class="hljs-keyword">uint</span> liquidity <span class="hljs-operator">=</span> balanceOf[<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)];

    <span class="hljs-keyword">bool</span> feeOn <span class="hljs-operator">=</span> _mintFee(_reserve0, _reserve1);
    <span class="hljs-keyword">uint</span> _totalSupply <span class="hljs-operator">=</span> totalSupply; <span class="hljs-comment">// gas savings, must be defined here since totalSupply can update in _mintFee</span>
    amount0 <span class="hljs-operator">=</span> liquidity.mul(balance0) <span class="hljs-operator">/</span> _totalSupply; <span class="hljs-comment">// using balances ensures pro-rata distribution</span>
    amount1 <span class="hljs-operator">=</span> liquidity.mul(balance1) <span class="hljs-operator">/</span> _totalSupply; <span class="hljs-comment">// using balances ensures pro-rata distribution</span>
    <span class="hljs-built_in">require</span>(amount0 <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> amount1 <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'</span>);
    _burn(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
    balance0 <span class="hljs-operator">=</span> IERC20(_token0).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    balance1 <span class="hljs-operator">=</span> IERC20(_token1).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));

    _update(balance0, balance1, _reserve0, _reserve1);
    <span class="hljs-keyword">if</span> (feeOn) kLast <span class="hljs-operator">=</span> <span class="hljs-keyword">uint</span>(reserve0).mul(reserve1); <span class="hljs-comment">// reserve0 and reserve1 are up-to-date</span>
    <span class="hljs-keyword">emit</span> Burn(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount0, amount1, to);
}
</code></pre><p>与mint类似，burn方法也会先计算协议手续费。</p><blockquote><p>参考白皮书，为了节省交易手续费，Uniswap v2只在mint/burn流动性时收取累计的协议手续费。</p></blockquote><p>移除流动性后，根据销毁的流动性代币占总量的比例获得对应的两种代币。</p><h4 id="h-swap" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">swap</h4><p>该方法实现两种代币的交换（交易）功能。</p><pre data-type="codeBlock" text="// this low-level function should be called from a contract which performs important safety checks
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    require(amount0Out &gt; 0 || amount1Out &gt; 0, &apos;UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT&apos;);
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    require(amount0Out &lt; _reserve0 &amp;&amp; amount1Out &lt; _reserve1, &apos;UniswapV2: INSUFFICIENT_LIQUIDITY&apos;);

    uint balance0;
    uint balance1;
    { // scope for _token{0,1}, avoids stack too deep errors
    address _token0 = token0;
    address _token1 = token1;
    require(to != _token0 &amp;&amp; to != _token1, &apos;UniswapV2: INVALID_TO&apos;);
    if (amount0Out &gt; 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
    if (amount1Out &gt; 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
    if (data.length &gt; 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));
    }
    uint amount0In = balance0 &gt; _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
    uint amount1In = balance1 &gt; _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
    require(amount0In &gt; 0 || amount1In &gt; 0, &apos;UniswapV2: INSUFFICIENT_INPUT_AMOUNT&apos;);
    { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
    uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
    uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
    require(balance0Adjusted.mul(balance1Adjusted) &gt;= uint(_reserve0).mul(_reserve1).mul(1000**2), &apos;UniswapV2: K&apos;);
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
"><code><span class="hljs-comment">// this low-level function should be called from a contract which performs important safety checks</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">swap</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> amount0Out, <span class="hljs-keyword">uint</span> amount1Out, <span class="hljs-keyword">address</span> to, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> data</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">lock</span> </span>{
    <span class="hljs-built_in">require</span>(amount0Out <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> amount1Out <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'</span>);
    (<span class="hljs-keyword">uint112</span> _reserve0, <span class="hljs-keyword">uint112</span> _reserve1,) <span class="hljs-operator">=</span> getReserves(); <span class="hljs-comment">// gas savings</span>
    <span class="hljs-built_in">require</span>(amount0Out <span class="hljs-operator">&#x3C;</span> _reserve0 <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> amount1Out <span class="hljs-operator">&#x3C;</span> _reserve1, <span class="hljs-string">'UniswapV2: INSUFFICIENT_LIQUIDITY'</span>);

    <span class="hljs-keyword">uint</span> balance0;
    <span class="hljs-keyword">uint</span> balance1;
    { <span class="hljs-comment">// scope for _token{0,1}, avoids stack too deep errors</span>
    <span class="hljs-keyword">address</span> _token0 <span class="hljs-operator">=</span> token0;
    <span class="hljs-keyword">address</span> _token1 <span class="hljs-operator">=</span> token1;
    <span class="hljs-built_in">require</span>(to <span class="hljs-operator">!</span><span class="hljs-operator">=</span> _token0 <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> to <span class="hljs-operator">!</span><span class="hljs-operator">=</span> _token1, <span class="hljs-string">'UniswapV2: INVALID_TO'</span>);
    <span class="hljs-keyword">if</span> (amount0Out <span class="hljs-operator">></span> <span class="hljs-number">0</span>) _safeTransfer(_token0, to, amount0Out); <span class="hljs-comment">// optimistically transfer tokens</span>
    <span class="hljs-keyword">if</span> (amount1Out <span class="hljs-operator">></span> <span class="hljs-number">0</span>) _safeTransfer(_token1, to, amount1Out); <span class="hljs-comment">// optimistically transfer tokens</span>
    <span class="hljs-keyword">if</span> (data.<span class="hljs-built_in">length</span> <span class="hljs-operator">></span> <span class="hljs-number">0</span>) IUniswapV2Callee(to).uniswapV2Call(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount0Out, amount1Out, data);
    balance0 <span class="hljs-operator">=</span> IERC20(_token0).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    balance1 <span class="hljs-operator">=</span> IERC20(_token1).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    }
    <span class="hljs-keyword">uint</span> amount0In <span class="hljs-operator">=</span> balance0 <span class="hljs-operator">></span> _reserve0 <span class="hljs-operator">-</span> amount0Out ? balance0 <span class="hljs-operator">-</span> (_reserve0 <span class="hljs-operator">-</span> amount0Out) : <span class="hljs-number">0</span>;
    <span class="hljs-keyword">uint</span> amount1In <span class="hljs-operator">=</span> balance1 <span class="hljs-operator">></span> _reserve1 <span class="hljs-operator">-</span> amount1Out ? balance1 <span class="hljs-operator">-</span> (_reserve1 <span class="hljs-operator">-</span> amount1Out) : <span class="hljs-number">0</span>;
    <span class="hljs-built_in">require</span>(amount0In <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> amount1In <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'</span>);
    { <span class="hljs-comment">// scope for reserve{0,1}Adjusted, avoids stack too deep errors</span>
    <span class="hljs-keyword">uint</span> balance0Adjusted <span class="hljs-operator">=</span> balance0.mul(<span class="hljs-number">1000</span>).sub(amount0In.mul(<span class="hljs-number">3</span>));
    <span class="hljs-keyword">uint</span> balance1Adjusted <span class="hljs-operator">=</span> balance1.mul(<span class="hljs-number">1000</span>).sub(amount1In.mul(<span class="hljs-number">3</span>));
    <span class="hljs-built_in">require</span>(balance0Adjusted.mul(balance1Adjusted) <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-keyword">uint</span>(_reserve0).mul(_reserve1).mul(<span class="hljs-number">1000</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">2</span>), <span class="hljs-string">'UniswapV2: K'</span>);
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    <span class="hljs-keyword">emit</span> Swap(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount0In, amount1In, amount0Out, amount1Out, to);
}
</code></pre><p>为了兼容闪电贷功能，以及不依赖特定代币的transfer方法，整个swap方法并没有类似amountIn的参数，而是通过比较当前余额与缓存余额的差值来得出转入的代币数量。</p><p>由于在swap方法最后会检查余额（扣掉手续费后）符合k恒等式约束（参考白皮书公式），因此合约可以先将用户希望获得的代币转出，如果用户之前并没有向合约转入用于交易的代币，则相当于借币（即闪电贷）；如果使用闪电贷，则需要在自定义的uniswapV2Call方法中将借出的代币归还。</p><p>在swap方法最后会使用缓存余额更新价格预言机所需的累计价格，最后更新缓存余额为当前余额。</p><pre data-type="codeBlock" text="// update reserves and, on the first call per block, price accumulators
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
    require(balance0 &lt;= uint112(-1) &amp;&amp; balance1 &lt;= uint112(-1), &apos;UniswapV2: OVERFLOW&apos;);
    uint32 blockTimestamp = uint32(block.timestamp % 2**32);
    uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
    if (timeElapsed &gt; 0 &amp;&amp; _reserve0 != 0 &amp;&amp; _reserve1 != 0) {
        // * never overflows, and + overflow is desired
        price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
        price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
    }
    reserve0 = uint112(balance0);
    reserve1 = uint112(balance1);
    blockTimestampLast = blockTimestamp;
    emit Sync(reserve0, reserve1);
}
"><code><span class="hljs-comment">// update reserves and, on the first call per block, price accumulators</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_update</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> balance0, <span class="hljs-keyword">uint</span> balance1, <span class="hljs-keyword">uint112</span> _reserve0, <span class="hljs-keyword">uint112</span> _reserve1</span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> </span>{
    <span class="hljs-built_in">require</span>(balance0 <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> <span class="hljs-keyword">uint112</span>(<span class="hljs-number">-1</span>) <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> balance1 <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> <span class="hljs-keyword">uint112</span>(<span class="hljs-number">-1</span>), <span class="hljs-string">'UniswapV2: OVERFLOW'</span>);
    <span class="hljs-keyword">uint32</span> blockTimestamp <span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">%</span> <span class="hljs-number">2</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">32</span>);
    <span class="hljs-keyword">uint32</span> timeElapsed <span class="hljs-operator">=</span> blockTimestamp <span class="hljs-operator">-</span> blockTimestampLast; <span class="hljs-comment">// overflow is desired</span>
    <span class="hljs-keyword">if</span> (timeElapsed <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> _reserve0 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> _reserve1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-comment">// * never overflows, and + overflow is desired</span>
        price0CumulativeLast <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-keyword">uint</span>(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) <span class="hljs-operator">*</span> timeElapsed;
        price1CumulativeLast <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-keyword">uint</span>(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) <span class="hljs-operator">*</span> timeElapsed;
    }
    reserve0 <span class="hljs-operator">=</span> <span class="hljs-keyword">uint112</span>(balance0);
    reserve1 <span class="hljs-operator">=</span> <span class="hljs-keyword">uint112</span>(balance1);
    blockTimestampLast <span class="hljs-operator">=</span> blockTimestamp;
    <span class="hljs-keyword">emit</span> Sync(reserve0, reserve1);
}
</code></pre><p>注意，其中区块时间戳和累计价格都是溢出安全的。（具体推导过程请参考白皮书）</p><h2 id="h-uniswap-v2-periphery" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">uniswap-v2-periphery</h2><p>由于UniswapV2Router01在处理FeeOnTransferTokens时有bug，目前已不再使用。此处我们仅介绍最新版的UniswapV2Router02合约。</p><p>代码地址：</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Uniswap/v2-periphery">https://github.com/Uniswap/v2-periphery</a></p><h3 id="h-uniswapv2router02" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">UniswapV2Router02</h3><p>Router02封装了最常用的几个交易接口；为了满足原生ETH交易需求，大部分接口都支持ETH版本；同时，相比Router01，部分接口增加了FeeOnTrasnferTokens的支持。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f5669feca786ed064ac08fb90e8d025eb1c579ac465fd9b9305846c58e1dd2d9.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>我们将主要介绍ERC20版本的代码，因为ETH版本只是将ETH与WETH做转换，逻辑与ERC20一致。</p><p>在介绍具体ERC20方法前，我们先介绍Library合约中的几个常用方法，以及它们的数学公式推导。</p><h3 id="h-library" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Library</h3><p>代码地址：</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol">https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol</a></p><h4 id="h-pairfor" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">pairFor</h4><p>输入工厂地址和两个代币地址，计算这两个代币的交易对地址。</p><pre data-type="codeBlock" text="// calculates the CREATE2 address for a pair without making any external calls
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
    (address token0, address token1) = sortTokens(tokenA, tokenB);
    pair = address(uint(keccak256(abi.encodePacked(
            hex&apos;ff&apos;,
            factory,
            keccak256(abi.encodePacked(token0, token1)),
            hex&apos;96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f&apos; // init code hash
        ))));
}
"><code><span class="hljs-comment">// calculates the CREATE2 address for a pair without making any external calls</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pairFor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> factory, <span class="hljs-keyword">address</span> tokenA, <span class="hljs-keyword">address</span> tokenB</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span> pair</span>) </span>{
    (<span class="hljs-keyword">address</span> token0, <span class="hljs-keyword">address</span> token1) <span class="hljs-operator">=</span> sortTokens(tokenA, tokenB);
    pair <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-keyword">uint</span>(<span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(
            <span class="hljs-string">hex'ff'</span>,
            factory,
            <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(token0, token1)),
            <span class="hljs-string">hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'</span> <span class="hljs-comment">// init code hash</span>
        ))));
}
</code></pre><p>上文提到，由于使用CREATE2操作码，交易对地址可以直接根据规范算出，而无需调用链上合约进行查询。</p><blockquote><p>create2(v, p, n, s)</p><p>create new contract with code mem[p…(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p…(p+n))) and send v wei and return the new address, where <code>0xff</code> is a 1 byte value, <code>this</code> is the current contract’s address as a 20 byte value and <code>s</code> is a big-endian 256-bit value; returns 0 on error</p></blockquote><p>其中，新创建的pair合约的地址计算方法为：keccak256(0xff + this + salt + keccak256(mem[p…(p+n)))：</p><ul><li><p>this：工厂合约地址</p></li><li><p>salt：keccak256(abi.encodePacked(token0, token1))</p></li><li><p>keccak256(mem[p…(p+n))： 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f</p></li></ul><p>由于每个交易对都使用UniswapV2Pair合约创建，因此init code hash都是一样的。我们可以在UniswapV2Factory写一个Solidty方法计算hash：</p><pre data-type="codeBlock" text="function initCodeHash() external pure returns (bytes32) {
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 hash;
    assembly {
        hash := keccak256(add(bytecode, 32), mload(bytecode))
    }
    return hash;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initCodeHash</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bytes32</span></span>) </span>{
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> bytecode <span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(UniswapV2Pair).<span class="hljs-built_in">creationCode</span>;
    <span class="hljs-keyword">bytes32</span> hash;
    <span class="hljs-keyword">assembly</span> {
        hash <span class="hljs-operator">:=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">add</span>(bytecode, <span class="hljs-number">32</span>), <span class="hljs-built_in">mload</span>(bytecode))
    }
    <span class="hljs-keyword">return</span> hash;
}
</code></pre><h4 id="h-quote" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">quote</h4><p>quote方法将数量为amountA的代币A，按照合约中两种代币余额比例，换算成另一个代币B。此时不考虑手续费，因为仅是计价单位的换算。</p><pre data-type="codeBlock" text="// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
    require(amountA &gt; 0, &apos;UniswapV2Library: INSUFFICIENT_AMOUNT&apos;);
    require(reserveA &gt; 0 &amp;&amp; reserveB &gt; 0, &apos;UniswapV2Library: INSUFFICIENT_LIQUIDITY&apos;);
    amountB = amountA.mul(reserveB) / reserveA;
}
"><code><span class="hljs-comment">// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">quote</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> amountA, <span class="hljs-keyword">uint</span> reserveA, <span class="hljs-keyword">uint</span> reserveB</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amountB</span>) </span>{
    <span class="hljs-built_in">require</span>(amountA <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2Library: INSUFFICIENT_AMOUNT'</span>);
    <span class="hljs-built_in">require</span>(reserveA <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> reserveB <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2Library: INSUFFICIENT_LIQUIDITY'</span>);
    amountB <span class="hljs-operator">=</span> amountA.mul(reserveB) <span class="hljs-operator">/</span> reserveA;
}
</code></pre><h4 id="h-getamountout" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">getAmountOut</h4><p>该方法计算：输入一定数量（amountIn）代币A，根据池子中代币余额，能得到多少数量（amountOut）代币B。</p><pre data-type="codeBlock" text="// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
    require(amountIn &gt; 0, &apos;UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT&apos;);
    require(reserveIn &gt; 0 &amp;&amp; reserveOut &gt; 0, &apos;UniswapV2Library: INSUFFICIENT_LIQUIDITY&apos;);
    uint amountInWithFee = amountIn.mul(997);
    uint numerator = amountInWithFee.mul(reserveOut);
    uint denominator = reserveIn.mul(1000).add(amountInWithFee);
    amountOut = numerator / denominator;
}
"><code><span class="hljs-comment">// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAmountOut</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> amountIn, <span class="hljs-keyword">uint</span> reserveIn, <span class="hljs-keyword">uint</span> reserveOut</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amountOut</span>) </span>{
    <span class="hljs-built_in">require</span>(amountIn <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'</span>);
    <span class="hljs-built_in">require</span>(reserveIn <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> reserveOut <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2Library: INSUFFICIENT_LIQUIDITY'</span>);
    <span class="hljs-keyword">uint</span> amountInWithFee <span class="hljs-operator">=</span> amountIn.mul(<span class="hljs-number">997</span>);
    <span class="hljs-keyword">uint</span> numerator <span class="hljs-operator">=</span> amountInWithFee.mul(reserveOut);
    <span class="hljs-keyword">uint</span> denominator <span class="hljs-operator">=</span> reserveIn.mul(<span class="hljs-number">1000</span>).add(amountInWithFee);
    amountOut <span class="hljs-operator">=</span> numerator <span class="hljs-operator">/</span> denominator;
}
</code></pre><p>为了推导该方法的数学公式，我们需要先回顾白皮书以及core合约中对于swap交换后两种代币的约束：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1bdd5c9b30732c488a4e99ba5ff73de09120da0919ea85aaf351db1d3c3a0512.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>其中，x0, y0为交换前的两种代币余额，x1, y1为交换后的两种代币余额，xin为输入的代币A数量，因为只提供代币A，因此yin=0；yout为需要计算的代币B数量。</p><p>可推导数学公式如下：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8be48cc8ad2b0ade7b59384c345c40d371e2be5426541e1015466f5a8b6a4c10.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>由于Solidity不支持浮点数，因此可以换算成如下公式：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c5e7563ed924416f420cb43b74390dbf56dd50cf6427c12d7f4b77ecc9f5e3c2.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>可以看出，该计算结果即为getAmountOut方法中的amountOut，其中，</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8830294be7383ef105ab2550108b328897d9e0707d559b5bd7ffd49de13d2fbd.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h4 id="h-getamountin" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">getAmountIn</h4><p>该方法计算当希望获得一定数量（amountOut）的代币B时，应该输入多少数量（amoutnIn）的代币A。</p><pre data-type="codeBlock" text="// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
    require(amountOut &gt; 0, &apos;UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT&apos;);
    require(reserveIn &gt; 0 &amp;&amp; reserveOut &gt; 0, &apos;UniswapV2Library: INSUFFICIENT_LIQUIDITY&apos;);
    uint numerator = reserveIn.mul(amountOut).mul(1000);
    uint denominator = reserveOut.sub(amountOut).mul(997);
    amountIn = (numerator / denominator).add(1);
}
"><code><span class="hljs-comment">// given an output amount of an asset and pair reserves, returns a required input amount of the other asset</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAmountIn</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> amountOut, <span class="hljs-keyword">uint</span> reserveIn, <span class="hljs-keyword">uint</span> reserveOut</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amountIn</span>) </span>{
    <span class="hljs-built_in">require</span>(amountOut <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'</span>);
    <span class="hljs-built_in">require</span>(reserveIn <span class="hljs-operator">></span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> reserveOut <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">'UniswapV2Library: INSUFFICIENT_LIQUIDITY'</span>);
    <span class="hljs-keyword">uint</span> numerator <span class="hljs-operator">=</span> reserveIn.mul(amountOut).mul(<span class="hljs-number">1000</span>);
    <span class="hljs-keyword">uint</span> denominator <span class="hljs-operator">=</span> reserveOut.sub(amountOut).mul(<span class="hljs-number">997</span>);
    amountIn <span class="hljs-operator">=</span> (numerator <span class="hljs-operator">/</span> denominator).add(<span class="hljs-number">1</span>);
}
</code></pre><p>getAmountOut是已知xin，计算yout；相对应地，getAmountIn则是已知yout，计算xin。根据上述公式可以推导出：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/26654951d3a558d3af2efc3ddd7e875c8a299d2ee03df56cf43ed1754cf70184.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>计算结果即为合约中代码所示，注意最后有一个add(1)，这是为了防止amountIn为小数的情况，加1可以保证输入的数（amountIn）不小于理论的最小值。</p><h4 id="h-getamountsout" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">getAmountsOut</h4><p>该方法用于计算在使用多个交易对时，输入一定数量（amountIn）的第一种代币，最终能收到多少数量的最后一种代币（amounts）。amounts数组中的第一个元素表示amountIn，最后一个元素表示该目标代币对应的数量。该方法实际上是循环调用getAmountIn方法。</p><pre data-type="codeBlock" text="// performs chained getAmountOut calculations on any number of pairs
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
    require(path.length &gt;= 2, &apos;UniswapV2Library: INVALID_PATH&apos;);
    amounts = new uintUnsupported embed;
    amounts[0] = amountIn;
    for (uint i; i &lt; path.length - 1; i++) {
        (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
        amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
    }
}
"><code><span class="hljs-comment">// performs chained getAmountOut calculations on any number of pairs</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAmountsOut</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> factory, <span class="hljs-keyword">uint</span> amountIn, <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">memory</span> path</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span>[] <span class="hljs-keyword">memory</span> amounts</span>) </span>{
    <span class="hljs-built_in">require</span>(path.<span class="hljs-built_in">length</span> <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">2</span>, <span class="hljs-string">'UniswapV2Library: INVALID_PATH'</span>);
    amounts <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> uintUnsupported embed;
    amounts[<span class="hljs-number">0</span>] <span class="hljs-operator">=</span> amountIn;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint</span> i; i <span class="hljs-operator">&#x3C;</span> path.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        (<span class="hljs-keyword">uint</span> reserveIn, <span class="hljs-keyword">uint</span> reserveOut) <span class="hljs-operator">=</span> getReserves(factory, path[i], path[i <span class="hljs-operator">+</span> <span class="hljs-number">1</span>]);
        amounts[i <span class="hljs-operator">+</span> <span class="hljs-number">1</span>] <span class="hljs-operator">=</span> getAmountOut(amounts[i], reserveIn, reserveOut);
    }
}
</code></pre><h4 id="h-getamountsin" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">getAmountsIn</h4><p>与getAmountsOut相对，getAmountsIn用于计算当希望收到一定数量（amountOut）的目标代币，应该分别输入多少数量的中间代币。计算方法也是循环调用getAmountIn。</p><pre data-type="codeBlock" text="// performs chained getAmountIn calculations on any number of pairs
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
    require(path.length &gt;= 2, &apos;UniswapV2Library: INVALID_PATH&apos;);
    amounts = new uintUnsupported embed;
    amounts[amounts.length - 1] = amountOut;
    for (uint i = path.length - 1; i &gt; 0; i--) {
        (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
        amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
    }
}
"><code><span class="hljs-comment">// performs chained getAmountIn calculations on any number of pairs</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAmountsIn</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> factory, <span class="hljs-keyword">uint</span> amountOut, <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">memory</span> path</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span>[] <span class="hljs-keyword">memory</span> amounts</span>) </span>{
    <span class="hljs-built_in">require</span>(path.<span class="hljs-built_in">length</span> <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">2</span>, <span class="hljs-string">'UniswapV2Library: INVALID_PATH'</span>);
    amounts <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> uintUnsupported embed;
    amounts[amounts.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>] <span class="hljs-operator">=</span> amountOut;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint</span> i <span class="hljs-operator">=</span> path.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>; i <span class="hljs-operator">></span> <span class="hljs-number">0</span>; i<span class="hljs-operator">-</span><span class="hljs-operator">-</span>) {
        (<span class="hljs-keyword">uint</span> reserveIn, <span class="hljs-keyword">uint</span> reserveOut) <span class="hljs-operator">=</span> getReserves(factory, path[i <span class="hljs-operator">-</span> <span class="hljs-number">1</span>], path[i]);
        amounts[i <span class="hljs-operator">-</span> <span class="hljs-number">1</span>] <span class="hljs-operator">=</span> getAmountIn(amounts[i], reserveIn, reserveOut);
    }
}
</code></pre><h3 id="h-erc20-erc20" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">ERC20-ERC20</h3><h4 id="h-addliquidity" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">addLiquidity 添加流动性</h4><pre data-type="codeBlock" text="function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
    (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
    liquidity = IUniswapV2Pair(pair).mint(to);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addLiquidity</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> tokenA,
    <span class="hljs-keyword">address</span> tokenB,
    <span class="hljs-keyword">uint</span> amountADesired,
    <span class="hljs-keyword">uint</span> amountBDesired,
    <span class="hljs-keyword">uint</span> amountAMin,
    <span class="hljs-keyword">uint</span> amountBMin,
    <span class="hljs-keyword">address</span> to,
    <span class="hljs-keyword">uint</span> deadline
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">ensure</span>(<span class="hljs-params">deadline</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amountA, <span class="hljs-keyword">uint</span> amountB, <span class="hljs-keyword">uint</span> liquidity</span>) </span>{
    (amountA, amountB) <span class="hljs-operator">=</span> _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
    <span class="hljs-keyword">address</span> pair <span class="hljs-operator">=</span> UniswapV2Library.pairFor(factory, tokenA, tokenB);
    TransferHelper.safeTransferFrom(tokenA, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, pair, amountB);
    liquidity <span class="hljs-operator">=</span> IUniswapV2Pair(pair).mint(to);
}
</code></pre><p>由于Router02是直接与用户交互的，因此接口设计需要从用户使用场景考虑。addLiquidity提供了8个参数：</p><ul><li><p>address tokenA：代币A</p></li><li><p>address tokenB：代币B</p></li><li><p>uint amountADesired：希望存入的代币A数量</p></li><li><p>uint amountBDesired：希望存入的代币B数量</p></li><li><p>uint amountAMin：最少存入的代币A数量</p></li><li><p>uint amountBMin：最少存入的代币B数量</p></li><li><p>address to：流动性代币接收地址</p></li><li><p>uint deadline：请求失效时间</p></li></ul><p>用户提交交易后，该交易被矿工打包的时间是不确定的，因此提交时的代币价格与交易打包时的价格可能不同，通过amountMin可以控制价格的浮动范围，防止被矿工或机器人套利；同样，deadline可以确保该交易在超过指定时间后将失效。</p><p>在core合约中提到，如果用户提供流动性时的代币价格与实际价格有差距，则只会按照较低的汇率得到流动性代币，多余的代币将贡献给整个池子。_addLiquidity可以帮助计算最佳汇率。如果是首次添加流动性，则会先创建交易对合约；否则根据当前池子余额计算应该注入的最佳代币数量。</p><pre data-type="codeBlock" text="// **** ADD LIQUIDITY ****
function _addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
    // create the pair if it doesn&apos;t exist yet
    if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
        IUniswapV2Factory(factory).createPair(tokenA, tokenB);
    }
    (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
    if (reserveA == 0 &amp;&amp; reserveB == 0) {
        (amountA, amountB) = (amountADesired, amountBDesired);
    } else {
        uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
        if (amountBOptimal &lt;= amountBDesired) {
            require(amountBOptimal &gt;= amountBMin, &apos;UniswapV2Router: INSUFFICIENT_B_AMOUNT&apos;);
            (amountA, amountB) = (amountADesired, amountBOptimal);
        } else {
            uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
            assert(amountAOptimal &lt;= amountADesired);
            require(amountAOptimal &gt;= amountAMin, &apos;UniswapV2Router: INSUFFICIENT_A_AMOUNT&apos;);
            (amountA, amountB) = (amountAOptimal, amountBDesired);
        }
    }
}
"><code><span class="hljs-comment">// **** ADD LIQUIDITY ****</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_addLiquidity</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> tokenA,
    <span class="hljs-keyword">address</span> tokenB,
    <span class="hljs-keyword">uint</span> amountADesired,
    <span class="hljs-keyword">uint</span> amountBDesired,
    <span class="hljs-keyword">uint</span> amountAMin,
    <span class="hljs-keyword">uint</span> amountBMin
</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amountA, <span class="hljs-keyword">uint</span> amountB</span>) </span>{
    <span class="hljs-comment">// create the pair if it doesn't exist yet</span>
    <span class="hljs-keyword">if</span> (IUniswapV2Factory(factory).getPair(tokenA, tokenB) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
        IUniswapV2Factory(factory).createPair(tokenA, tokenB);
    }
    (<span class="hljs-keyword">uint</span> reserveA, <span class="hljs-keyword">uint</span> reserveB) <span class="hljs-operator">=</span> UniswapV2Library.getReserves(factory, tokenA, tokenB);
    <span class="hljs-keyword">if</span> (reserveA <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> reserveB <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        (amountA, amountB) <span class="hljs-operator">=</span> (amountADesired, amountBDesired);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">uint</span> amountBOptimal <span class="hljs-operator">=</span> UniswapV2Library.quote(amountADesired, reserveA, reserveB);
        <span class="hljs-keyword">if</span> (amountBOptimal <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> amountBDesired) {
            <span class="hljs-built_in">require</span>(amountBOptimal <span class="hljs-operator">></span><span class="hljs-operator">=</span> amountBMin, <span class="hljs-string">'UniswapV2Router: INSUFFICIENT_B_AMOUNT'</span>);
            (amountA, amountB) <span class="hljs-operator">=</span> (amountADesired, amountBOptimal);
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">uint</span> amountAOptimal <span class="hljs-operator">=</span> UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
            <span class="hljs-built_in">assert</span>(amountAOptimal <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> amountADesired);
            <span class="hljs-built_in">require</span>(amountAOptimal <span class="hljs-operator">></span><span class="hljs-operator">=</span> amountAMin, <span class="hljs-string">'UniswapV2Router: INSUFFICIENT_A_AMOUNT'</span>);
            (amountA, amountB) <span class="hljs-operator">=</span> (amountAOptimal, amountBDesired);
        }
    }
}
</code></pre><p>最后调用core合约mint方法铸造流动性代币。</p><h4 id="h-removeliquidity" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">removeLiquidity 移除流动性</h4><p>首先将流动性代币发送到pair合约，根据收到的流动性代币占全部代币比例，计算该流动性代表的两种代币数量。合约销毁流动性代币后，用户将收到对应比例的代币。如果低于用户设定的最低预期（amountAMin/amountBMin），则回滚交易。</p><pre data-type="codeBlock" text="// **** REMOVE LIQUIDITY ****
function removeLiquidity(
    address tokenA,
    address tokenB,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
    (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
    (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
    (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
    require(amountA &gt;= amountAMin, &apos;UniswapV2Router: INSUFFICIENT_A_AMOUNT&apos;);
    require(amountB &gt;= amountBMin, &apos;UniswapV2Router: INSUFFICIENT_B_AMOUNT&apos;);
}
"><code><span class="hljs-comment">// **** REMOVE LIQUIDITY ****</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">removeLiquidity</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> tokenA,
    <span class="hljs-keyword">address</span> tokenB,
    <span class="hljs-keyword">uint</span> liquidity,
    <span class="hljs-keyword">uint</span> amountAMin,
    <span class="hljs-keyword">uint</span> amountBMin,
    <span class="hljs-keyword">address</span> to,
    <span class="hljs-keyword">uint</span> deadline
</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">ensure</span>(<span class="hljs-params">deadline</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amountA, <span class="hljs-keyword">uint</span> amountB</span>) </span>{
    <span class="hljs-keyword">address</span> pair <span class="hljs-operator">=</span> UniswapV2Library.pairFor(factory, tokenA, tokenB);
    IUniswapV2Pair(pair).transferFrom(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, pair, liquidity); <span class="hljs-comment">// send liquidity to pair</span>
    (<span class="hljs-keyword">uint</span> amount0, <span class="hljs-keyword">uint</span> amount1) <span class="hljs-operator">=</span> IUniswapV2Pair(pair).burn(to);
    (<span class="hljs-keyword">address</span> token0,) <span class="hljs-operator">=</span> UniswapV2Library.sortTokens(tokenA, tokenB);
    (amountA, amountB) <span class="hljs-operator">=</span> tokenA <span class="hljs-operator">=</span><span class="hljs-operator">=</span> token0 ? (amount0, amount1) : (amount1, amount0);
    <span class="hljs-built_in">require</span>(amountA <span class="hljs-operator">></span><span class="hljs-operator">=</span> amountAMin, <span class="hljs-string">'UniswapV2Router: INSUFFICIENT_A_AMOUNT'</span>);
    <span class="hljs-built_in">require</span>(amountB <span class="hljs-operator">></span><span class="hljs-operator">=</span> amountBMin, <span class="hljs-string">'UniswapV2Router: INSUFFICIENT_B_AMOUNT'</span>);
}
</code></pre><h4 id="h-removeliquiditywithpermit" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">removeLiquidityWithPermit 使用签名移除流动性</h4><p>用户正常移除流动性时，需要两个操作：</p><ol><li><p>approve：授权Router合约花费自己的流动性代币</p></li><li><p>removeLiquidity：调用Router合约移除流动性</p></li></ol><p>除非第一次授权了最大限额的代币，否则每次移除流动性都需要两次交互，这意味着用户需要支付两次手续费。而使用removeLiquidityWithPermit方法，用户可以通过签名方式授权Router合约花费自己的代币，无需单独调用approve，只需要调用一次移除流动性方法即可完成操作，节省了gas费用。同时，由于离线签名不需要花费gas，因此可以每次签名仅授权一定额度的代币，提高安全性。</p><pre data-type="codeBlock" text="function removeLiquidityWithPermit(
    address tokenA,
    address tokenB,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline,
    bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    uint value = approveMax ? uint(-1) : liquidity;
    IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
    (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">removeLiquidityWithPermit</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> tokenA,
    <span class="hljs-keyword">address</span> tokenB,
    <span class="hljs-keyword">uint</span> liquidity,
    <span class="hljs-keyword">uint</span> amountAMin,
    <span class="hljs-keyword">uint</span> amountBMin,
    <span class="hljs-keyword">address</span> to,
    <span class="hljs-keyword">uint</span> deadline,
    <span class="hljs-keyword">bool</span> approveMax, <span class="hljs-keyword">uint8</span> v, <span class="hljs-keyword">bytes32</span> r, <span class="hljs-keyword">bytes32</span> s
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> amountA, <span class="hljs-keyword">uint</span> amountB</span>) </span>{
    <span class="hljs-keyword">address</span> pair <span class="hljs-operator">=</span> UniswapV2Library.pairFor(factory, tokenA, tokenB);
    <span class="hljs-keyword">uint</span> value <span class="hljs-operator">=</span> approveMax ? <span class="hljs-keyword">uint</span>(<span class="hljs-number">-1</span>) : liquidity;
    IUniswapV2Pair(pair).permit(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), value, deadline, v, r, s);
    (amountA, amountB) <span class="hljs-operator">=</span> removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
</code></pre><h4 id="h-swapexacttokensfortokens" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">swapExactTokensForTokens</h4><p>交易时的两个常见场景：</p><ol><li><p>使用指定数量的代币A（输入），尽可能兑换最多数量的代币B（输出）</p></li><li><p>获得指定数量的代币B（输出），尽可能使用最少数量的代币A（输入）</p></li></ol><p>本方法实现第一个场景，即根据指定的输入代币，获得最多的输出代币。</p><pre data-type="codeBlock" text="function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
    require(amounts[amounts.length - 1] &gt;= amountOutMin, &apos;UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT&apos;);
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">swapExactTokensForTokens</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint</span> amountIn,
    <span class="hljs-keyword">uint</span> amountOutMin,
    <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">calldata</span> path,
    <span class="hljs-keyword">address</span> to,
    <span class="hljs-keyword">uint</span> deadline
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">ensure</span>(<span class="hljs-params">deadline</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span>[] <span class="hljs-keyword">memory</span> amounts</span>) </span>{
    amounts <span class="hljs-operator">=</span> UniswapV2Library.getAmountsOut(factory, amountIn, path);
    <span class="hljs-built_in">require</span>(amounts[amounts.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>] <span class="hljs-operator">></span><span class="hljs-operator">=</span> amountOutMin, <span class="hljs-string">'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'</span>);
    TransferHelper.safeTransferFrom(
        path[<span class="hljs-number">0</span>], <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, UniswapV2Library.pairFor(factory, path[<span class="hljs-number">0</span>], path[<span class="hljs-number">1</span>]), amounts[<span class="hljs-number">0</span>]
    );
    _swap(amounts, path, to);
}
</code></pre><p>首先使用Library合约中的getAmountsOut方法，根据兑换路径计算每一次交易的输出代币数量，确认最后一次交易得到的数量（amounts[amounts.length - 1]）不小于预期最少输出（amountOutMin）；将代币发送到第一个交易对地址，开始执行整个兑换交易。</p><p>假设用户希望使用WETH兑换DYDX，链下计算的最佳兑换路径为WETH → USDC → DYDX，则amountIn为WETH数量，amountOutMin为希望获得最少DYDX数量，path为[WETH address, USDC address, DYDX address]，amounts为[amountIn, USDC amount, DYDX amount]。在_swap执行交易的过程中，每次中间交易获得的中间代币将被发送到下一个交易对地址，以此类推，直到最后一个交易完成，_to地址将收到最后一次交易的输出代币。</p><pre data-type="codeBlock" text="// requires the initial amount to have already been sent to the first pair
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
    for (uint i; i &lt; path.length - 1; i++) {
        (address input, address output) = (path[i], path[i + 1]);
        (address token0,) = UniswapV2Library.sortTokens(input, output);
        uint amountOut = amounts[i + 1];
        (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
        address to = i &lt; path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
        IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
            amount0Out, amount1Out, to, new bytes(0)
        );
    }
}
"><code><span class="hljs-comment">// requires the initial amount to have already been sent to the first pair</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_swap</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span>[] <span class="hljs-keyword">memory</span> amounts, <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">memory</span> path, <span class="hljs-keyword">address</span> _to</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint</span> i; i <span class="hljs-operator">&#x3C;</span> path.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        (<span class="hljs-keyword">address</span> input, <span class="hljs-keyword">address</span> output) <span class="hljs-operator">=</span> (path[i], path[i <span class="hljs-operator">+</span> <span class="hljs-number">1</span>]);
        (<span class="hljs-keyword">address</span> token0,) <span class="hljs-operator">=</span> UniswapV2Library.sortTokens(input, output);
        <span class="hljs-keyword">uint</span> amountOut <span class="hljs-operator">=</span> amounts[i <span class="hljs-operator">+</span> <span class="hljs-number">1</span>];
        (<span class="hljs-keyword">uint</span> amount0Out, <span class="hljs-keyword">uint</span> amount1Out) <span class="hljs-operator">=</span> input <span class="hljs-operator">=</span><span class="hljs-operator">=</span> token0 ? (<span class="hljs-keyword">uint</span>(<span class="hljs-number">0</span>), amountOut) : (amountOut, <span class="hljs-keyword">uint</span>(<span class="hljs-number">0</span>));
        <span class="hljs-keyword">address</span> to <span class="hljs-operator">=</span> i <span class="hljs-operator">&#x3C;</span> path.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> <span class="hljs-number">2</span> ? UniswapV2Library.pairFor(factory, output, path[i <span class="hljs-operator">+</span> <span class="hljs-number">2</span>]) : _to;
        IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
            amount0Out, amount1Out, to, <span class="hljs-keyword">new</span> <span class="hljs-keyword">bytes</span>(<span class="hljs-number">0</span>)
        );
    }
}
</code></pre><h4 id="h-swaptokensforexacttokens" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">swapTokensForExactTokens</h4><p>该方法实现交易的第二个场景，根据指定的输出代币，使用最少的输入代币完成兑换。</p><pre data-type="codeBlock" text="function swapTokensForExactTokens(
    uint amountOut,
    uint amountInMax,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
    require(amounts[0] &lt;= amountInMax, &apos;UniswapV2Router: EXCESSIVE_INPUT_AMOUNT&apos;);
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">swapTokensForExactTokens</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint</span> amountOut,
    <span class="hljs-keyword">uint</span> amountInMax,
    <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">calldata</span> path,
    <span class="hljs-keyword">address</span> to,
    <span class="hljs-keyword">uint</span> deadline
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">ensure</span>(<span class="hljs-params">deadline</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span>[] <span class="hljs-keyword">memory</span> amounts</span>) </span>{
    amounts <span class="hljs-operator">=</span> UniswapV2Library.getAmountsIn(factory, amountOut, path);
    <span class="hljs-built_in">require</span>(amounts[<span class="hljs-number">0</span>] <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> amountInMax, <span class="hljs-string">'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'</span>);
    TransferHelper.safeTransferFrom(
        path[<span class="hljs-number">0</span>], <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, UniswapV2Library.pairFor(factory, path[<span class="hljs-number">0</span>], path[<span class="hljs-number">1</span>]), amounts[<span class="hljs-number">0</span>]
    );
    _swap(amounts, path, to);
}
</code></pre><p>与上面类似，这里先使用Library的getAmountsIn方法反向计算每一次兑换所需的最少输入代币数量，确认计算得出的（扣除手续费后）第一个代币所需的最少代币数不大于用户愿意提供的最大代币数（amountInMax）；将代币发送到第一个交易对地址，调用_swap开始执行整个兑换交易。</p><h3 id="h-erc20-eth" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">ERC20-ETH</h3><h4 id="h-eth-support" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">ETH Support</h4><p>由于core合约只支持ERC20代币交易，为了支持ETH交易，periphery合约需要将ETH与WETH做转换；并为大部分方法提供了ETH版本。兑换主要涉及两种操作：</p><ol><li><p>地址转换：由于ETH没有合约地址，因此需要使用WETH合约的deposit和withdraw方法完成ETH与WETH的兑换</p></li><li><p>代币数量转换：ETH的代币需要通过msg.value获取，可根据该值计算对应的WETH数量，而后使用标准ERC20接口即可</p></li></ol><h4 id="h-feeontransfertokens" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">FeeOnTransferTokens</h4><p>由于某些代币会在转账（transfer）过程中收取手续费，转账数量与实际收到的数量有差异，因此无法直接通过计算得出中间兑换过程中所需的代币数量，此时应该通过balanceOf方法（而非transfer方法）判断实际收到的代币数量。Router02新增了对Inclusive Fee On Transfer Tokens的支持，更具体说明可以参考<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.uniswap.org/protocol/V2/reference/smart-contracts/common-errors#inclusive-fee-on-transfer-tokens">官方文档</a>。</p>]]></content:encoded>
            <author>adshao@newsletter.paragraph.com (AdamShao)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/63728c2305218719a543ecb78ac1cf55c92f6fe30523b91268a8c5b45c687323.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[深入理解 Uniswap v2 白皮书]]></title>
            <link>https://paragraph.com/@adshao/uniswap-v2</link>
            <guid>uYnOaNHODJzRWCvpKBsq</guid>
            <pubDate>Sun, 13 Feb 2022 04:40:13 GMT</pubDate>
            <description><![CDATA[前言本文作为《深入理解Uniswap》系列的第一篇，将从Uniswap v2白皮书入手，讲解Uniswap v2协议的设计思路和数学公式推导过程。 网络上讲解Uniswap的文章已经很多了，为什么要再写一遍呢？ 最初原因是为了记录我个人在学习Uniswap过程中的总结，这些总结不是简单的翻译，更多是对于白皮书知识点的延伸阅读、数学公式的推导以及合约代码的工程实现的学习思考，而这些在原版白皮书大多没有展开。 虽然目前Uniswap v3已经推出一段时间了，但是学习v2仍然是理解V3的基础；并且由于v3的License限制，其他EVM链AMM项目大多fork v2代码，因此深入学习Uniswap v2仍然很有必要。 此外，Uniswap作为DeFi的基础协议，无论是行业地位，还是理论基础及其工程实现，都是DeFi的经典范例，对于想要深入学习DeFi或者智能合约编程的同学，Uniswap v2是非常好的入门材料。 希望本文能够帮助你在理解Uniswap v2的过程中提供一点帮助。由于本人水平有限，文中难免出现错误，欢迎斧正。 下文将按照Uniswap v2 白皮书章节结构进行翻译，同时...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">前言</h2><p>本文作为《深入理解Uniswap》系列的第一篇，将从Uniswap v2白皮书入手，讲解Uniswap v2协议的设计思路和数学公式推导过程。</p><p>网络上讲解Uniswap的文章已经很多了，为什么要再写一遍呢？</p><p>最初原因是为了记录我个人在学习Uniswap过程中的总结，这些总结不是简单的翻译，更多是对于白皮书知识点的延伸阅读、数学公式的推导以及合约代码的工程实现的学习思考，而这些在原版白皮书大多没有展开。</p><p>虽然目前Uniswap v3已经推出一段时间了，但是学习v2仍然是理解V3的基础；并且由于<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://uniswap.org/blog/uniswap-v3#license">v3的License限制</a>，其他EVM链AMM项目大多fork v2代码，因此深入学习Uniswap v2仍然很有必要。</p><p>此外，Uniswap作为DeFi的基础协议，无论是行业地位，还是理论基础及其工程实现，都是DeFi的经典范例，对于想要深入学习DeFi或者智能合约编程的同学，Uniswap v2是非常好的入门材料。</p><p>希望本文能够帮助你在理解Uniswap v2的过程中提供一点帮助。由于本人水平有限，文中难免出现错误，欢迎斧正。</p><p>下文将按照<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://uniswap.org/whitepaper.pdf">Uniswap v2 白皮书</a>章节结构进行翻译，同时将重点知识的延伸阅读和数学公式推导过程以注释形式说明。</p><h1 id="h-uniswap-v2-core" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Uniswap v2 Core</h1><h2 id="h-1-introduction" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">1 Introduction 介绍</h2><p>Uniswap v1是一个以太坊链上智能合约系统，实现了基于x * y = k的AMM（自动做市）协议。每一个Uniswap v1交易对池子包含两种代币，在提供流动性的过程中保证两种代币余额的乘积无法减少。交易者为每次交易支付0.3%的手续费给流动性提供者。v1的合约不可升级。</p><p>Uniswap v2是基于同一个公式的新版实现，包含许多令人期待的新特性。其中最重要的一个特性是可以支持任意ERC20代币的交易对，而不是v1只支持ERC20与ETH的交易对。此外，v2提供了价格预言机功能，其原理是在每个区块开始时累计两种代币的相对价格。这将允许其他以太坊合约可以获取任意时间段内两种代币的时间加权平均价格；最后，v2还提供“闪电贷”功能，这将允许用户在链上自由借出并使用代币，只需在该交易的最后归还这些代币并支付一定手续费即可。</p><p>虽然v2的合约也是不可升级的，但是它支持在工厂合约中修改一个变量，以便允许Uniswap协议针对每笔交易收取0.05%的手续费（即0.3%的1/6）。该手续费默认关闭，但是可以在未来被打开，在打开后流动性提供者将只能获取0.25%手续费，而非0.3%</p><blockquote><p>注：因为其中0.05%分给协议。</p><p>关于0.05%协议手续费这个开关，后续引发了Sushiswap和Uniswap的流动性大战，Sushiswap fork Uniswap代码，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.sushi.com/products/yield-farming/the-sushibar#what-is-xsushi">将0.05%协议手续费分给SUSHI持有者</a>，一度要将Uniswap流动性抢走；并最终迫使Uniswap发行了自己的代币UNI。</p></blockquote><p>在第三节，我们将介绍Uniswap v2同时修复了Uniswap v1的一些小问题，同时重构了合约实现，通过最小化（持有流动性资金的）core合约逻辑，降低了Uniswap被攻击的风险，并使得系统更加容易升级。</p><p>本文讨论了core合约和用来初始化交易对合约的工厂合约的结构。实际上，使用Uniswap v2需要通过router（路由）合约调用交易对合约，它将帮助计算在交易和提供流动性时需要向交易对合约转账的代币数量。</p><h2 id="h-2-new-features" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">2 New features 新特性</h2><h3 id="h-21-erc-20-pairs-erc-20" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2.1 ERC-20 pairs ERC-20 交易对</h3><p>Uniswap v1使用ETH作为桥接代币，任何交易对都包含ETH作为其中一个代币。这使得路由更加简单，比如要想实现ABC和XYZ的交易，只需要分别使用ETH/ABC和ETH/XYZ交易对即可，这同时也减少了流动性分裂。</p><blockquote><p>注：由于交易对总是包含ETH，相比v2任意ERC-20代币组合，v1的交易对数量大大减少，并且流动性都被吸收到ETH这一侧。</p></blockquote><p>但是这样的规则给流动性提供者带来巨大成本。所有的流动性提供者都将面临ETH的风险敞口，并且在代币价格相对ETH价格波动时承受无常损失。</p><blockquote><p>注：由于x * y = k引入的滑点，流动性提供者在Uniswap做市时将承受无常损失，简单而言就是在代币价格单方面（上涨或下跌）波动时，做市者手中持有的代币总价值反而减少。关于无常损失的说明，可以参考<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://academy.binance.com/en/articles/impermanent-loss-explained">币安的这篇博客</a>。</p></blockquote><p>如果ABC和XYZ是两种关联的代币，比如都是USD稳定币，那么交易对ABC/XYZ的无常损失将小于ABC/ETH或XYZ/ETH。</p><blockquote><p>注：因为ABC与XYZ价格相对于ETH朝同一方向运动，ABC/XYZ价格波动较小，而ABC/ETH或XYZ/ETH价格波动较大。</p></blockquote><p>使用ETH作为强制的交易代币也会增加交易成本。相比直接使用ABC/XYZ交易对，他们将支付两倍的交易手续费，同时承受两倍的滑点。</p><blockquote><p>注：因为在v1，要想从ABC交易到XYZ，必须依次交易ABC/ETH和ETH/XYZ，因此手续费和滑点都需要两倍。</p></blockquote><p>Uniswap v2允许流动性提供者为任意两个ERC-20代币创建交易对合约。</p><p>交易对数量的激增将给寻找最优交易路径带来困难，但是路由问题可以在上层解决（比如通过链下或链上的路由器或聚合器）。</p><h3 id="h-22-price-oracle" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2.2 Price Oracle 价格预言机</h3><p>在时间点 <em>t</em> 由Uniswap提供的边际价格（不包含手续费）可以通过代币 <em>a</em> 和代币 <em>b</em> 的数量相除得出：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/217299e52edcd71a2d74defdebc4a6257c2e42a8d493319d1d637e3b8488cda0.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>当Uniswap提供的价格不正确时，套利者可以在Uniswap交易套利（通过足够数量代币以支付手续费），因此Uniswap提供的代币价格将跟随市场价格。这意味着Uniswap提供的代币价格可以作为一种近似的价格预言机。</p><blockquote><p>注：同一个代币在不同市场的价格差异提供了套利机会，驱使套利者维持Uniswap市场价格与其他市场（如中心化交易所或DEX）价格一致。</p></blockquote><p>然而，Uniswap v1无法提供安全的链上预言机，因为它的价格很容易被操控。假设其他合约使用当前ETH-DAI价格作为衍生品交易的基准价格。攻击者可以从ETH-DAI交易对买入ETH来操控价格，并触发衍生品合约的清算，接着再将ETH卖回以使价格回归正常。上述操作可以通过一个原子交易完成，或者被矿工通过排序同一区块中的不同的交易来实现。</p><blockquote><p>注：由于采样的价格是瞬时的，因此很容易通过买入卖出大额代币来操纵实时价格。 samczsun有<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://samczsun.com/taking-undercollateralized-loans-for-fun-and-for-profit/">一篇博客</a>介绍了这种攻击。</p></blockquote><p>Uniswap v2改进了预言机功能，通过在每个区块的第一笔交易前计算和记录价格来实现（等价于上一个区块的最后一笔交易之后）。操纵这个价格会比操纵区块中任意时间点的价格要困难。如果攻击者通过在区块的最后阶段提交一笔交易来操纵价格，其他套利者（发现价格差异后）可以在同一区块中提交另一笔交易来将价格恢复正常。矿工（或者支付了足够gas费用填充整个区块的攻击者）可以在区块的末尾操控价格，但是除非他们同时挖出了下一个区块，否则他们没有特殊的优势可以进行套利。</p><blockquote><p>注：由于价格预言机仅在每个区块记录一次，因此除非同一个人控制了两个区块的所有交易，否则他们将没有足够的套利优势。但是这从另一方面说明，Uniswap v2的预言机仍然是不够健壮的。我们在v3可以看到这方面的改进。</p></blockquote><p>Uniswap v2通过在每个区块第一笔交易前记录累计价格实现预言机。每个价格会以时间权重记录（基于当前区块与上一次更新价格的区块的时间差）。这意味着在任意时间点，该累计价格将是此合约历史上每秒的现货价格之和。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9e1d8cea4ffc81debf1d383673d5f5c757e2c212f0870d0d60b0ced347a2e54d.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>为了估算在t1到t2时间段内的时间加权平均价格（TWAP），外部调用者可以分别记录t1和t2的累计价格，将t2价格减去t1价格，并除以t2-t1的时间差（需注意，合约本身不存储历史的累计价格，因此需要调用者在区间开始时调用合约，读取并保存当前的价格）。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a31b355f79128cb00fb4c7940ee6697699215e49eadeb3b25ef514d8f2960350.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>预言机的用户可以自行选择区间的开始和结束。选择一个更长的区间，意味着攻击者将花费更高的代价来操控该区间的时间加权平均价格，虽然这将导致该平均价格与实时价格相差较大。</p><blockquote><p>注：公式(3)比较容易理解，这里就不展开。但是需注意，由于合约仅记录当前的累计价格，因此如果需要计算区间的平均价格，外部应用要自己记录并保存历史价格，合约本身不保存历史数据。</p><p>Uniswap v2的TWAP计算方式实际上使用的是（加权）算数平均数（Arithmetic Mean），这里我们需要了解几种平均数的概念和应用场景。</p><p>在数学上有一个<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://en.wikipedia.org/wiki/Pythagorean_means">毕达哥拉斯平均</a>的概念，指的是三种经典平均数，分别是：算数平均数、几何平均数和调和平均数。</p></blockquote><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a596adf165195aa9a330ec31fb05f5261f189bc1ccea7e17cca8fb17b213bad5.png" alt="毕达哥拉斯平均" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">毕达哥拉斯平均</figcaption></figure><blockquote><p>其中，算术平均数是最常见的一种平均数，其优点是计算简单，缺点是容易受到极端数据的影响，导致均值误差；几何平均数相比算术平均数，更适用于在金融市场场景，因为金融市场价格本身是一种<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://en.wikipedia.org/wiki/Brownian_motion">布朗运动</a>；调和平均数更易受到极小值的影响，一般应用于计算平均速率等场景。</p><p>从应用场景上，Uniswap价格均值应该使用几何平均数更合适，均值的误差更小，但由于几何平均数在以太坊合约上实现难度较大，所以Uniswap v2版本采用算数平均数；但是Uniswap v3则使用几何平均数计算价格预言机。</p><p>在3.4节，Uniswap v2使用几何平均数计算初始流动性代币数量。</p></blockquote><p>一个难题：我们应该计算以B代币计价的A代币价格，还是以A代币计价的B代币价格？虽然在现货价格上，以B代币计价的A代币价格（B/A）与以A代币计价的B代币价格（A/B）总是互为倒数，但在计算某个时间区间的算数平均数时，二者却不是互为倒数关系。比如，假设在区块1的价格为100 USD/ETH（B为USD，A为ETH），区块2的价格为300 USD/ETH，则其平均价格为200 USD/ETH，但ETH/USD的平均价格却是1/150 ETH/USD。因为合约无法知道交易对中哪一个代币将被用户用作计价单位，因此Uniswap v2同时记录了两个代币的价格。</p><blockquote><p>注：两种代币计价的均值计算如下：</p></blockquote><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cf605aff3ca53f3d4f6b34e14e3ebc89f799de4764a4391ba17b451011139de8.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>另一个难题是用户可以不通过交易而直接向交易对合约发送代币（这将改变代币余额并影响价格），此时将无法触发预言机价格更新。</p><blockquote><p>注：因为预言机价格需要在区块的第一笔交易之前更新，因此如果不交易，将绕开预言机更新。</p></blockquote><p>如果合约只是简单地检查它的余额，并使用当前余额计算价格来更新预言机，那么攻击者可以在区块的第一笔交易之前，立即向合约发送代币来操控预言机价格。如果上一笔交易是在X秒之前的某个区块，合约将错误的使用（被操纵后的）新价格乘以X来累计，即使并没有人使用该价格交易过。</p><blockquote><p>注：假设在上一个区块最后一笔交易后，交易对合约中两个代币A、B的余额分别为100、200，以A计价的B价格为200/100=2，在X秒后，下一个区块第一笔交易发生之前，应该累计的价格是<em>2X</em>，但是如果在第一笔交易发生之前，攻击者向合约发送了100个B，此时价格为200/200=1，合约将错误地以<em>1X</em>累计。</p></blockquote><p>为了防止这个问题，core合约在每次交互后缓存了两种代币余额，并且使用缓存余额（而非实时余额）更新预言机价格。除了防止预言机价格被操控外，这个改动也带来了合约架构的重新设计，我们将在3.2节进行说明。</p><h4 id="h-221-precision" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">2.2.1 Precision 精度</h4><p>因为Solidity原生不支持非整数数据类型，Uniswap v2使用了简单的二进制定点制进行编码和操作价格。确切地说，任意时间的价格都被保存为UQ112.112格式的数据，它表示在小数点的左右两边都有112位比特表示精度，无符号（注：非负数）。这个格式能表示的范围为[0, (2的112次方)-1]，精度为(2的112次方)分之一。</p><blockquote><p>注：UQ112.112的理论最大值为：2的112次方-(2的112次方)分之一，但由于Uniswap v2使用两个uint112相除得到UQ112.112，因此其最大值为(2的112次方)-1。</p></blockquote><p>选择UQ112.112格式是出于（Solidty合约）编程实践的考虑，因为这些格式的数字能够使用一个uint224类型（占用224位比特，28个字节）的变量表示，在一个256位（比特）的存储槽（注：EVM中一个Storage Slot是256位）中正好剩余32位可用。而对于缓存的代币余额变量，每一个代币余额可以使用一个uint112类型（112比特位，14个字节）的变量，（在声明时）也正好在256位的存储槽中剩余32位可用。这些剩余空间可用于上述的累计运算使用。具体来说，代币余额与最近一个有交易区块的时间戳一起保存，该时间戳针对2的32次方取模，以确保可以使用32位表示。此外，虽然在任意时间点的价格（使用UQ112.112格式的数字）一定符合224位，但是一段时间的累计价格却不是这样。在存储槽末尾的多余32位空间将用于存储由于重复累计价格导致的溢出数据。这样的设计意味着价格预言机仅仅在每个区块的第一笔交易增加了3个SSTORE操作（当前消耗15,000 gas）。</p><blockquote><p>注：这里我们可以看到Uniswap在设计上是非常小心的，价格预言机虽然是一个很有用的功能，但是却不能因为过度设计而给用户增加成本，因此如何在确保对用户影响最小的同时把功能实现，就成为设计的核心所在。为了避免每次交易都更新预言机给用户带来额外交易成本，Uniswap v2才设计成只在每个区块的第一笔交易之前更新。</p><p>每个代币余额使用uint112表示，时间戳使用32位表示，总共112+112+32=256位，正好占用一个storage slot。更少的storage slot意味着交互时需要花费的gas更小，有利于减少用户操作成本。累计价格则采用256位表示。</p></blockquote><p>这个设计最主要的缺点是32位无法确保时间戳永不溢出。事实上，Unix时间戳溢出32位（可表示的最大值）将发生在02/07/2106。为了确保系统能够在该时间后正常工作，同时在每一轮32位溢出后（(2的32次方)-1秒）也能正常工作，预言机需要至少在每一轮（大约136年）被调用查询一次。因为累计计算的核心方法是溢出安全的，这意味着即使交易跨越了时间戳溢出的时间点，它也是可以被正常累计的，只要预言机使用了正确的溢出算法来检查时间间隔。</p><blockquote><p>注：这里我们主要关注在时间戳溢出边界，使用公式（3）是否能够正确算出预言机价格的平均数。假设在2106年2月7日附近，t1,t2,t3分别表示三个连续的区块时间，其中t1未发生时间戳溢出（差1秒），而t2,t3则发生溢出，可以算出即使在溢出后，t2-t1仍然可以计算出正确的时间差（3秒）；同理可以计算即使当累计价格at3发生溢出后，只要调用者保存了at1的值，即可计算出二者正确的差值。pt1,t3为t1到t3时间区间的平均价格，按照公式（3）可推出如下计算：</p></blockquote><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/448955f8d34650aa68e40529e2836517d62633ab77b2375c336905d907b0fa31.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><h3 id="h-23-flash-swaps" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2.3 Flash Swaps 闪电贷</h3><p>在Uniswap v1，用户如果想使用XYZ购买ABC，则需要先将XYZ发送到合约才能收到ABC。这将给那些希望使用ABC购买XYZ的用户带来不便。比如，当Uniswap与其他合约出现套利机会时，用户可能希望使用ABC在别的合约购买XYZ；或者用户希望通过卖出抵押物来释放他们在Maker或Compound的头寸，以此偿还Uniswap的借款。</p><p>Uniswap v2增加了一个新特性，允许用户在支付费用前先收到并使用代币，只要他们在同一个交易中完成支付。swap方法会在转出代币和检查k值两个步骤之间，调用一个可选的用户指定的回调合约。一旦回调完成，Uniswap合约会检查当前代币余额，并且确认其满足k值条件（在扣除手续费后）。如果当前合约没有足够的余额，整个交易将被回滚。</p><p>用户可以只归还原始代币，而不需要执行交易操作。这个功能将使得任何人可以闪电借出Uniswap池子中的任意数量的代币（闪电贷手续费与交易手续费一致，都是0.30%）。</p><blockquote><p>注：闪电贷在DeFi领域非常实用，对于TVL较高的协议，协议可以通过闪电贷获取手续费收入。比如dYdX和Aave，都推出了闪电贷功能。Uniswap v2合约中的闪电贷与交易功能实际上使用同一个swap方法。</p></blockquote><h3 id="h-24-protocol-fee" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2.4 Protocol fee 协议手续费</h3><p>Uniswap v2包含一个0.05%的协议手续费开关。如果打开，该手续费将被发送到合约中的feeTo地址。</p><p>默认情况下没有设置feeTo地址，因此不收取协议手续费。预定义的feeToSetter地址可以调用Uniswap v2工厂合约中的setFeeTo方法来修改feeTo地址。feeToSetter也可以调用setFeeToSetter修改合约中feeToSetter地址。</p><p>如果feeTo地址被设置了，协议将开始收取5个基点（0.05%）的手续费，也就是流动性提供者收取的30个基点（0.30%）手续费中的1/6将分配给协议。这意味着交易者将继续为每一笔交易支付0.30%的交易手续费，83.3%（5/6）的手续费（整笔交易的0.25%）将分配给流动性提供者，剩余的16.6%（手续费的1/6，整笔交易的0.05%）将分配给feeTo地址。</p><p>如果在每笔交易时收取0.05%的手续费，将带来额外的gas消耗。为了避免这个问题，累计的手续费只在提供或销毁流动性时收取。合约计算累计手续费，并且在流动性代币铸造或销毁的时候，为手续费受益者（feeTo地址）铸造新的流动性代币。</p><p>总累计手续费可以通过计算从上次收取手续费后，以根号k（也就是根号xy）计价的增长量。可计算从t1到t2的累计手续费，与t2时刻的流动性的百分比如下：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/db188644bff93ad2b48afa6a9feb4fb08690db52989cf06baafcf2656668f35e.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>注：这里不太好理解，为什么手续费是以根号xy的形式给出的呢？如果看完白皮书【3.4 初始化流动性代币供应】就明白了。第一次流动性铸造的代币数量是以根号xy算出的，在t1,t2不同时刻，（不考虑mint/burn流动性时）其流动性代币数量始终等于根号x1y1与根号x2y2，其增长部分即为手续费，因此公式（4）可按照如下推导得出：</p></blockquote><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2016803defa0f1ab48bf204042d75385b0bfafd3c5dc3fb5a2d679aef73ecbb8.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>那么当mint/burn流动性时，如何计算手续费呢？实际上在每次mint/burn流动性之前，都会先结算未领取的累计手续费，所以在mint/burn之后，可以重新按照上面的公式计算手续费。因此上述公式只需关注仅当发生swap交易时，累计手续费如何计算这一问题。</p></blockquote><p>如果协议手续费在t1之前被激活，那么在t1,t2时段，feeTo地址应该收取1/6的手续费作为协议手续费。因此，我们需要为feeTo地址铸造新的流动性代币以代表该时段手续费，这里等于1/6 * f1,2。</p><p>假设协议手续费对应的流动性代币数量为sm，s1为t1时刻的流动性代币数量，则有以下等式：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b44abd56dc4c9b91e9428ecb531bd0906579a45fec27cee9378d5476307e095d.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>使用公式（4）替换f1,2，经过计算可以得出sm为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/844f2ed4593d9ddab060cb795c20027e77f838122906c639c4d9f47d37b36052.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>使用1/6替换其中的比例部分，可得：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b2cdaf1ab949c09fc60b47281ddcb498f5bd311a8930de3e1ad038ba080e0bb6.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>假设初始流动性提供者存入100 DAI和1 ETH，获得10个流动性代币。一段时间后（假设没有其他流动性提供者），当feeTo希望取出协议手续费时，两种代币余额分别为96 DAI和1.5 ETH。分别代入公式（7）可得：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8e3bafabf8e866962061bb36d0145f4d5bf1cc5b410179271ce30574fb04a33d.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>注：当没有mint/burn流动性时，只是单纯swap，池子的k值是不断变大的，原因就在于手续费沉淀，因为此时流动性代币总量（shares）不变，但交易对池子中两种代币余额不断增加。如上所述，k1 = 100 DAI x 1 ETH = 100，k2 = 96 DAI * 1.5 ETH = 144，k2 &gt; k1。</p></blockquote><h3 id="h-25-meta-transactions-for-pool-shares-yuan" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2.5 Meta transactions for pool shares 元交易</h3><p>Uniswap v2的池子份额（即流动性代币）天然支持元交易。这意味着用户可以通过签名授权第三方转移其持有的流动性代币，而无需通过他们自己发起链上交易。任何人都可以通过调用<em>permit</em>方法来代替该用户提交签名，支付gas费用，并且可以在同一交易中执行其他操作。</p><blockquote><p>注：这里的元交易实际上指的是通过离线签名方式，由第三方代替用户发起链上交易。在某些场景下很实用，比如用户的钱包没有ETH，可以由第三方代付gas。</p><p>在Uniswap v2 core合约中的签名功能是授权转账流动性代币；这个签名是在外围的router合约中使用，因为v2将合约分为core（最核心的swap/mint/burn功能）和periphery（外围应用）合约，而应用一般直接调用periphery合约，通过签名方式可以减少用户与core合约的链上交互，只需使用离线签名与periphery合约交互一次即可移除流动性。签名也便于其他合约与core合约集成。</p><p>这里涉及两个EIP，分别是<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-712">EIP-712</a>与<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-2612">EIP-2612</a>，我们在另外的文章再具体说明这两个EIP。简单而言，EIP-712定义了针对结构数据的签名方式，在以前只能针对一串hash签名，实际上我们并不知道签名的内容是什么，容易引发安全问题，比如误将代币授权给恶意合约；通过EIP-712，我们可以在签名时检查具体的签名内容，如授权转账的额度，截止时间等信息。（但从实际使用上，大部分用户仍然并不知道自己签名会带来什么影响）。EIP-2612则是关于使用EIP-712的permit方法的Solidity编码规范，该提案是在Uniswap v2以后才出来的，目前仍处于Review阶段。</p></blockquote><h2 id="h-3-other-changes" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3 Other changes 其他改动</h2><h3 id="h-31-solidity" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.1 Solidity</h3><p>Uniswap v1使用Vyper语言实现，这是一个类Python的智能合约语言。Uniswap v2使用更流行的Solidity语言实现，因为v2依赖一些（在开发时）Vyper语言还不具有的能力，比如解析非标准ERC-20代币的返回值，通过内联的assembly语法访问一些新操作码，如chainid。</p><h3 id="h-32-contract-re-architecture" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.2 Contract re-architecture 合约重构</h3><p>Uniswap v2的一个设计重点在于最小化core交易对合约的对外接口范围和复杂度（core合约存放流动性提供者的代币资产）。在core合约上发现的任何bug都可能是灾难性的，因为这可能会导致数百万美元的流动性资产被盗走或冻结。</p><p>在评估core合约的安全性时，最重要的问题是它是否能保护流动性提供者的资产不被盗走或冻结。任何增强或保护交易者的功能特性，而不是允许池子里资产交换这种最基本的功能，都应该被抽取放到router（路由）合约。</p><blockquote><p>core合约仅保留最基础最重要的功能，以保证安全性，因为所有流动性资产将存放在core合约中。代码越少，改动越小，出现bug的概率也越小。实际上core合约的核心代码只有200行左右。</p></blockquote><p>事实上，甚至部分swap功能的代码也可以被提到router合约中。如前所述，Uniswap v2保存每种代币最后的余额记录（为了防止攻击者操纵预言机机制）。新的架构在此基础上针对Uniswap v1做了进一步简化。</p><p>在Uniswap v2，卖方在执行swap方法前，会发送代币到core合约。合约将通过比较缓存余额和当前余额来判断收到多少代币。这意味着core合约无法知道交易者是通过什么方式发送代币。事实上，他可以通过离线签名的元交易方式，或者其他未来授权ERC-20代币转移的机制，而不只是transferFrom方法。</p><h4 id="h-32-1-adjustment-for-fee" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">3.2 1 Adjustment for fee 手续费调整</h4><p>Uniswap v1的交易手续费是通过减少存入合约的代币数量来实现，在比较k恒等式之前，需要先减去0.3%的交易手续费。合约隐式约束如下：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e14c6465ed2cf264463dfbcc9f4f1d20003475595bb521d8899f1ac4bd497a9f.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>注：扣除手续费以后的两种代币余额，符合k恒等式。</p></blockquote><p>通过闪电贷功能，Uniswap v2引入了一种可能性，即xin和yin可能同时不为0（当一个用户希望通过归还借出的代币，而不是做交易时）。为了处理这种情况下的手续费问题，合约强制要求如下约束：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ea131319cc8d00696c84094dafdf1f0dd0d52b74595da836ea265fe3655ec517.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>注：Uniswap的swap方法可以同时支持闪电贷和交易功能，当通过闪电贷同时借出x和y两种代币时，需要分别对x和y收取0.3%的手续费，因此需要先扣除手续费，再保证余额满足k值约束。</p></blockquote><p>为了简化链上计算，我们可以为公式（10）两边同时乘以1,000,000，得出：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/08b452d3aeb44fdb202b425d6d558d0bd9e9737afe03bb6a0b16b2e1e280daa0.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>因为Solidity不支持浮点数，因此通过同步放大来简化计算。</p></blockquote><h4 id="h-322-sync-skim" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">3.2.2 sync() 和 skim()</h4><p>为了防止某些可以修改交易对合约余额的定制代币，同时也为了更优雅地解决那些总量超过2的112次方的代币，Uniswap v2提供了两个方法：sync()和skim()。</p><p>当某种代币异步通缩时，sync()可以作为一种恢复手段。在这种场景下，交易将获得次优的兑换率，如果没有流动性提供者愿意纠正这种状态，交易对将难以继续工作。sync()方法可以将合约中缓存的代币余额设置为当前实际余额，以帮助系统从这种状态中恢复。</p><p>当发送大量代币导致交易对的代币余额溢出（超过uint112最大值）时，交易将失败，skim()可以作为这种情况的恢复手段。skim()允许任意用户取出多余的代币（代币实际余额与(2的112次方)-1的差值）。</p><blockquote><p>简单来说，由于某些（非Uniswap导致的）外部因素，交易对合约中的缓存余额与实际余额可能出现算法外的不一致问题。sync()方法可以更新缓存余额到实际余额，skim()方法可以更新实际余额到缓存余额，从而保证系统继续运行。任何人都可以执行这两个方法。Alpha Leak：如果有人误将交易对中的代币转入合约，任何人都可以取出这些代币。</p></blockquote><h3 id="h-33-handling-non-standard-and-unusual-tokens" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.3 Handling non-standard and unusual tokens 处理非标准和罕见代币</h3><p>ERC-20标准要求transfer()和transferFrom()返回一个布尔值表示该请求是否成功。然而某些代币在实现这两个（或其中一个）方法时并没有返回值，比如USDT和BNB。Uniswap v1在解析无返回值的方法时，将其当作失败处理，因此将回滚交易，从而导致交易失败。</p><blockquote><p>扩展阅读： <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-20">EIP-20: ERC-20代币标准</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code">USDT合约地址</a> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0xB8c77482e45F1F44dE1745F52C74426C631bDD52#code">BNB合约地址</a></p></blockquote><p>Uniswap v2针对非标准ERC-20代币的实现，则使用不一样的处理方法。当transfer()方法没有返回值时，Uniswap v2认为它表示执行成功，而非失败。这个改动不会影响任何实现标准ERC-20的代币（因为他们的transfer()方法有返回值）。</p><p>同样，Uniswap v1假设transfer()和transferFrom()不能触发重入交易对合约的方法。这种假设会和某些ERC-20代币冲突，包括那些支持ERC-777标准hooks的代币。为了完全支持这些代币，Uniswap v2引入了”lock”机制用来解决所有公开修改状态方法的重入问题。这也可以防止在闪电贷中用户自定义回调的重入问题。</p><blockquote><p>注：lock实际上是一个Solidity modifer，通过一个unlock变量控制同步锁。</p></blockquote><h3 id="h-34-initialization-of-liquidity-token-supply" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.4 Initialization of liquidity token supply 初始化流动性代币供应</h3><p>当一个新的流动性提供者将代币存入一个已存在的Uniswap交易对，新铸造的流动性代币数量可根据当前代币数量计算：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0a32bbbc39f6f642bd9ffb7065f4b438fa479d8e7fd94e1d250734474b5a3a7d.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><blockquote><p>注：因为流动性代币本身是一种ERC-20代币，持有流动性代币数量即表示占有该池子代币的份额（shares）。因此对于已存在的交易对，即已经有该交易对的流动性代币存在，那么存入的代币价值与总价值的比例，与其得到的流动性代币数量与总数量的比例应相等：</p></blockquote><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b57da56a998e8fbe61b1ba14c83b272b86d2bc0b64374806f40ca7fe63dfea81.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>但如果他们是第一个流动性提供者呢？在这种情况下，Xstarting是0，因此上述公式无法适用。</p><p>Uniswap v1将首次流动性代币数量等同于存入的ETH数量（以wei为单位）。这有一定的合理性，因为如果首次流动性是以正确的价格存入的，那么1个流动性份额（如ETH是一种有18位小数的代币）将代表大约2ETH的价值。</p><blockquote><p>注：因为Uniswap v1/v2提供流动性时需要注入两边等值的代币，如果份额等同于ETH数量，则1份额表示需要存入1ETH，而在价格正确时，另一个代币的价值也同样是1ETH，因此1个流动性份额的流动性总价值是2ETH。</p></blockquote><p>然而，这意味着流动性份额的价值需要依赖首次注入流动性时的价格比例，而这个价格是可以被认为控制的，我们无法保证首次注入流动性时的两种代币的比例能够正确反映真实价格。此外，由于Uniswap v2支持任意代币的交易对，因此将有更多的交易对不包含ETH。</p><p>与v1不同，Uniswap v2规定首次铸造流动性代币的数量等于存入的两种代币数量的几何平均数：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/be195b9ecaf4d1d4a82bc9dd9eecff72b5484f754fdd3200b00f572588ea4ae7.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>该公式确保在任意时刻，流动性份额的价值与其存入代币的价格比例无关。比如，假设当前1 ABC的价格是100 XYZ，如果首次存入2 ABC和200 XYZ（对应的比例为1:100），则流动性提供者将收到根号(2*200)=20个流动性代币。这些代币价值2 ABC和200 XYZ，以及对应的累计手续费。</p><p>如果首次存入2 ABC和800 XYZ（对应比例1:400），则流动性提供者将收到根号(2*800)=40个流动性代币。</p><p>以上公式确保1个流动性份额（代币）的价值将不少于池子中两种代币余额的几何平均数。然后，1个流动性代币的价值将可能随着时间持续增长，比如通过累计交易手续费，或者通过其他人“捐赠”代币到池子里。</p><p>理论上可能存在这种情况，最小的流动性代币单位（1的18次方分之一，即1 wei）的价值太高，以至于无法让其他（小）流动性提供者加入。</p><p>为了解决这个问题，Uniswap v2销毁首次铸造10的15次方分之一（最小代币单位的1000倍）流动性代币。这个损耗对于大部分交易对而言都是微不足道的。但是这将极大提到首次铸币攻击的代价。为了将每个流动性代币价格提高到100美元，攻击者需要捐赠10万美元的代币到池子中，这些代币将被作为流动性而永久锁定。</p><blockquote><p>注：首次铸币攻击是指攻击者在第一次添加流动性时存入最小单位（10的-18次方，即1 wei）的流动性，比如1 wei ABC和1 wei XYZ，此时将铸造1 wei 流动性代币（根号1）；同时，攻击者在同一个交易中继续向池子转入（非铸造）100万个ABC和100万个XYZ，接着调用 sync()方法更新缓存余额，此时1 wei的流动性代币价值100万+(10的-18次方)ABC和100万+(10的-18次方)XYZ，其他流动性参与者要想添加流动性，需要等价的大量代币，其价格可能高到大部分人无法参与。</p><p>通过销毁首次铸造的1000wei代币，攻击者如果想将每个代币价格提高到100美元，则至少需要铸造1000+1=1001个流动性代币，总价值1001*100=10万零100美元，其中10万美元将被永久销毁，这可以极大提高攻击者成本。</p></blockquote><h3 id="h-35-wrapping-eth-weth" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.5 Wrapping ETH - WETH</h3><p>使用以太坊原生代币ETH进行交易的接口，与使用ERC-20代币的接口是不同的。因此，许多以太坊协议并不支持ETH，而使用一种符合ERC-20标准的代币封装ETH，即WETH。</p><p>Uniswap v1是一个例外。因为每一个Uniswap v1交易对都使用ETH作为其中一种交易代币，因此直接支持ETH交易是合理的，并且能够更省gas。</p><p>由于Uniswap v2支持任意ERC-20交易对，因此没有必要支持原生ETH交易。增加这种支持将使core合约代码量翻倍，并且将使流动性分裂为ETH和WETH交易对。原生ETH需要先封装为WETH才能在Uniswap v2交易。</p><blockquote><p>注：事实上，Uniswap v2只是core合约不支持原生ETH，periphery合约仍然支持原生ETH交易，合约会自动将ETH转为WETH，然后再调用core合约进行交易。这里也反映出Uniswap一直倡导的开发原则，保持core合约最简化，应用和用户体验的逻辑依靠periphery合约解决。</p></blockquote><h3 id="h-36-deterministic-pair-address" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.6 Deterministic pair address 确定的交易对地址</h3><p>与Uniswap v1一样，所有Uniswap v2交易对合约都由一个统一的工厂合约初始化生成。在Uniswap v1，这些合约使用CREATE操作码创建，这意味着这些合约的地址依赖于合约生成的顺序。Uniswap v2使用以太坊新的CREATE2操作码生成具有确定地址的交易对合约。这意味着交易对合约的地址是可以通过链下计算的，而无需查询链上状态。</p><blockquote><p>注：关于CREATE与CREATE2的用法，可以参考<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/develop/yul.html#yul-object">官方文档</a>。 CREATE2源自<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-1014">EIP-1014</a>。</p><p>如果交易对的地址是不确定的，就意味着用户希望使用ABC交易XYZ时，需要调用链上合约接口查询ABC/XYZ交易对地址才能进行交易，同时，合约也需要存储交易对代币与交易地址的映射关系；如果使用确定性地址，前端页面或者应用只需要按照规定算法即可算出交易对合约地址，避免了链上查询。</p></blockquote><h3 id="h-37-maximum-token-balance" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">3.7 Maximum token balance 最大代币余额</h3><p>为了更有效地实现预言机功能，Uniswap v2只支持缓存代币余额的最大值为(2的112次方)-1。该数字已经大到可以支持代币总量超过千万亿的18位小数代币。</p><p>如果任意一种代币余额超过最大值，swap方法的调用将会失败（由于_update()方法的检查导致）。为了从这种状况中恢复，任何人都可以调用skim()方法来从池子中移除多余的代币。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">引用文献</h2><p>[1] Hayden Adams. 2018. url: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hackmd.io/@477aQ9OrQTCbVR3fq1Qzxg/HJ9jLsfTz?type=view">https://hackmd.io/@477aQ9OrQTCbVR3fq1Qzxg/HJ9jLsfTz?type=view</a>. [2] Guillermo Angeris et al. An analysis of Uniswap markets. 2019. arXiv: 1911.03380 [q-fin.TR]. [3] samczsun. Taking undercollateralized loans for fun and for profit. Sept. 2019. url: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://samczsun.com/taking-undercollateralized-loans-for-fun-and-for-profit/">https://samczsun.com/taking-undercollateralized-loans-for-fun-and-for-profit/</a>. [4] Fabian Vogelsteller and Vitalik Buterin. Nov. 2015. url: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-20">https://eips.ethereum.org/EIPS/eip-20</a>. [5] Jordi Baylina Jacques Dafflon and Thomas Shababi. EIP 777: ERC777 Token Standard. Nov. 2017. url: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-777">https://eips.ethereum.org/EIPS/eip-777</a>. [6] Radar. WTF is WETH? url: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://weth.io/">https://weth.io/</a>. [7] <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://Uniswap.info">Uniswap.info</a>. Wrapped Ether (WETH). url: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://uniswap.info/token/0xc02aaa39b223fe8d0a0e5c4f27e">https://uniswap.info/token/0xc02aaa39b223fe8d0a0e5c4f27e</a> [8] Vitalik Buterin. EIP 1014: Skinny CREATE2. Apr. 2018. url: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-1014">https://eips.ethereum.org/EIPS/eip-1014</a>.</p>]]></content:encoded>
            <author>adshao@newsletter.paragraph.com (AdamShao)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/e3f9bbcd11540f2357c9ed486eab2d9073a936be77706b27c27a6022df1e928d.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>