im钱包下载地址
数字资产服务平台

im钱包下载地址是全球著名的数字资产交易平台之一,主要面向全球用户提供比特币、莱特币、以太币等数字资产的币币和衍生品交易服务。

比特派钱包下载中文版苹果|uniwap

时间:2024-03-17 07:28:57

DeFi工具—UNISWAP使用教程 - 知乎

DeFi工具—UNISWAP使用教程 - 知乎首发于DeFi挖矿教程大全切换模式写文章登录/注册DeFi工具—UNISWAP使用教程派派什么是Uniswap?Uniswap是 2018 年 11 月发布在以太坊主网上的去中心化交易所协议,它的创建者是 Hayden Adams。当前Uniswap在DeFi Pluses上拍名第11位,较之前有下降。不同于其它挂单式去中心化交易所,Uniswap采用了全新的AMM自动做市商算法来执行交易的设计。相比于其他交易所,Uniswap 交易所的智能合约设计能够大幅减少 gas 的用量。如何理解Uniswap的自动做市机制?传统的交易所一般是提供一个挂单平台,通过程序(撮合引擎)撮合买卖双方达成交易。它是一个自由买卖的市场,具有买卖意愿的人们自行挂出“买单”和“卖单”,通过交易所“中介”实现双方订单的成交。传统交易所有以下特点:市场上必须要有用户进行挂单,要有一定量的订单(市场深度)。订单必须重叠才能成交,即买价高于或等于卖价。需要将资产存储在交易所。而Uniswap的模型却完全不同。简单来讲,Uniswap的撮合机制基于一个最基本的公式:K=X*Y。其中,X代表一种资产,Y代表与X等值的另一种资产, 而K则是它俩相乘后的一个函数。当K值不变的情况下,X与Y的值就成反比,即X增加,Y就会减小(反之亦然)。而因为资产Y的较少会导致相对市场的溢价,则会吸引用户来进行搬砖套利,从而抹平价差,使系统重归平衡。下面我们就以ETH与MakerDAO的算法稳定币DAI为例,来为大家解释下这个模型(不考虑手续费)。假设当前ETH价格为150USD,DAI的价格与美金挂钩,等于1USD。现在往系统里注入100ETH与15,000 DAI,则K=100*15,000=1,500,000。当用户将2个ETH兑换成DAI后,ETH总量减少为98,则此时DAI的总量=1,500,000/98=15,306。而因此增加的306个DAI就等于2个ETH的价格,即1ETH=153USD。此时,ETH就会相对市场有溢价,会吸引用户来进行搬砖套利。用户将自己的ETH卖给系统,补充资金池里的ETH,使系统再次回到最初的稳定状态。Uniswap有什么不足之处?在挂单机制下,如果价格达不到挂单者的设置价格,系统是不会成交的。但是在Uniswap的模式下,用户要注意当前资金池的流动性,以免滑点太多,造成资产损失。这里给大家进行了一个简单计算,从表中我们可以看出,随着系统内ETH数量的增加,用户购买单个DAI相对于ETH的成本就会增高。当数量为10ETH时,点差可以达到9.89%。而前段时间UMA上线Uniswap,价格从0.26一路走高到1.2美金的事情也印证了这一点。针对这一点,Balancer做了进一步的修改。但是总体来看,这类AMM机制的DEXes难免会因为流动性问题而出现滑点。不过,对于初创企业来说,此类的DEXes是它们为Token定价并进行销售的福音,Uniswap为他们省去了一大笔要支付给交易所跟做市商的钱。如何在比特派里使用Uniswap?第一步,打开比特派并找到Uniswap安卓版本可以在发现页下的推荐/热门DApp里找到Uniswap。iOS版请更新到bitpie PRO 版本,在发现页的搜索框里输入:https://uniswap.bitpie.com/* Uniswap因为使用的是新加坡域名,所以经常会遇到加载缓慢或者访问出错的情况,而比特派则对此做了优化,当前使用比特派访问Uniswap的体验十分顺畅。第二步,进入DApp主页面,选择要使用的功能图为改版后的Uniswap页面,个人感觉比之前更简洁了。第三步,选择兑换,输入兑换金额及币种,选择浮点后进行兑换这里以ETH兑换DAI为例,并选择接受增加最多0.5%的偏差。设定完毕后,点击“兑换”。待订单确认后,你的钱包地址里就会收到DAI了。从主页面我们可以看到,除了兑换还有“发送”与“资金池”功能。这两个功能都属于V2版本,因为使用时系统会自动切换到V2下。其中,发送与兑换功能大同小异,只是增加了一个收款地址,让你在兑换完成后可以将币发到自己/他人的指定地址,或者ENS域名。而资金池功能,是允许用户通过为系统注入ETH增加系统流动性而获取收益的。下图则展示了如何使用资金池功能为系统注入流动性。以上就是对Uniswap的简介及使用体验了。DeFi市场最近因为Compound的借贷挖矿又火了一把,引来的市场的广泛关注,这对DeFi市场来说是一件好事。同时,DeFi产品因其自身具有去中心化、抗审查、公开透明等特性也正逐渐受到市场的青睐。比特派也会持续关注DeFi生态,为大家带来更多的DeFi产品介绍及体验。本文仅为第三个工具在钱包端的操作教程,不构成任何投资建议。投资有风险,请您谨慎评估。安全、强大钱包就用比特派发布于 2020-09-02 17:08交易所智能合约​赞同 24​​3 条评论​分享​喜欢​收藏​申请转载​文章被以下专栏收录DeFi挖矿教程大全持续更新各类 DeFi 工具使用,微博@比特

Uniswap Wallet on iOS and Android

Uniswap Wallet on iOS and Android

Uniswap in your pocket.A wallet built for swappingLearn moreJoin the waitlist for the Uniswap ExtensionJoin the waitlist for the Uniswap ExtensionJoin the waitlist for the Uniswap ExtensionClaim your uni.eth username in the Uniswap mobile app to join the waitlist for the Uniswap Extension.Claim your uni.eth username in the Uniswap mobile app to join the waitlist for the Uniswap Extension.Claim your uni.eth username in the Uniswap mobile app to join the waitlist for the Uniswap Extension.Learn MoreSwap on multiple chains.Supports Ethereum, Optimism, Polygon, Arbitrum, Base, and BNBOne simple wallet.All your NFTs, tokens, and wallets in one placeSafe and secure by design.Built by the most trusted team in DeFi.Swap on multiple chains.Supports Ethereum, Optimism, Polygon, Arbitrum, Base, and BNBOne simple wallet.All your NFTs, tokens, and wallets in one placeSafe and secure by design.Built by the most trusted team in DeFi.Swap on multiple chains.Supports Ethereum, Optimism, Polygon, Arbitrum, Base, and BNBOne simple wallet.All your NFTs, tokens, and wallets in one placeSafe and secure.Built by the most trusted team in DeFi.ResourcesDownload SupportOther ProductsSwap TokensExplore NFTsLearn moreBlogDiscordTwitter© 2024 – Uniswap Privacy PolicyTrademark PolicyResourcesDownload SupportOther ProductsSwap TokensExplore NFTsLearn moreBlogDiscordTwitter© 2024 – Uniswap Privacy PolicyTrademark PolicyResourcesDownload SupportOther ProductsSwap TokensExplore NFTsLearn moreBlogDiscordTwitter© 2024 – Uniswap Privacy PolicyTrademark Policy

什么是Uniswap? 史上最全新手导读 - 知乎

什么是Uniswap? 史上最全新手导读 - 知乎切换模式写文章登录/注册什么是Uniswap? 史上最全新手导读达瓴智库Uniswap是一个建立在以太坊上的主流分布式加密交易平台。绝大部分的加密资产交易发生在中心化交易平台,如Coinbase和Binance。这些平台受约束于单一中心机构(运转交易公司),用户需按其要求放入资金,并用传统的订单簿系统来促进交易。订货单交易是买入和卖出订单以其各自的金额呈现在一张表单中。一种资产未完成的买卖订单量被称为“市场深度”。为了在这个系统中促成成功的交易,订单中的一个买单需要匹配相对应的一个卖单,反之亦然。 例如,如果你想在中心化交易平台中以33000美元的价位卖出一个BTC,你需要等到一个买家出现,他希望以这个价格去购买同等或更高价格的BTC。这种系统的主要问题是流动性,在该种情况下,流动性指的是在任何给定时间内订单簿上的订单深度和数量。如果流动性比较低,意味着交易者无法执行他们买卖的订单。从另个角度思考流动性,想象你在一个集市上有一个自己的小摊,如果这个集市很繁忙,不断有人买卖商品,则这个就可以被认为是一个“流动性”。如果这个市场安静并且只有少量交易,则可被认为是个“狭窄的市场”。一、Uniswap是什么?Uniswap 是一种完全不同的交易平台,完全的分布式意味着它自己并不被单一实体所有并且运营,而是运用了名为自动流动性协议的一个全新交易模式。Uniswap平台于2018年建立在全球市值第二大的加密资产项目以太坊区块链之上,这使得它与所有ERC-20Token和基础设施(如MetaMask和MyEtherWallet等钱包服务)兼容。Uniswap也是一个完全开源的项目,意味着任何人都能复制代码去创造他们自己的分布式交易平台。它甚至允许用户免费上新Token。一般中心化交易所都是受利益驱动,并对上新Token收取高额费用。所以这是一个显著的差异。因为Uniswap是一个分布式交易平台(DEX),这也意味着用户任何时候都保持对他们基金的控制,而不是像中心化交易平台那样,要求交易者放弃控制自己的私钥,因此订单可以登录一个内部数据库,而不是在区块链上执行,两者比较起来,中心化交易平台更耗时和成本更高。通过保留对私钥的控制,它消除了交易平台遭到黑客攻击而损失资产的风险。根据2月初的数据,Uniswap是第四大分布式金融(DeFi)平台,在其协议上锁定了价值超过30亿美元的加密资产。二、Uniswap如何工作Uniswap运行在两个智能合约上; 一个“交换”合同和一个“工厂”合同。 这是一种自动计算机程序,当满足某些条件时,它被设计来执行特定的功能。 在这种情况下,工厂智能合约用于向平台添加新的Token,而交换合约为所有Token交换或“交易”提供便利。 任何基于ERC-20的Token都可以在更新的Uniswap v.2平台上与另一个Token交换。 三、自动化的流动性协议Uniswap用自动化流动性协议来解决中心化交易平台的流动性问题。这是通过激励人们在交易平台进行交易,从而成为流动性提供者(LPs)来实现的:Uniswap用户将他们的资金聚集在一起,来创建一个基金,用于执行平台上发生的所有交易。每一个登记的Token都有他们自己的用户可以贡献的基金pool,并且每个Token的价格用由电脑通过数学算法计算出来(在下面解释:Token价格如何决定的)。有了这个系统,买方或卖方不必等待对方出现来完成交易。相反,他们可以立即以已知价格执行任何交易,前提是特定pool中有足够的流动性为其提供便利。作为提供资金的交换,每个参与者都会收到一个Token,代表他们对投资pool的贡献。例如,如果你向一个总计持有10万美元的流动性pool贡献了1万美元,你将获得该流动性pool10%的Token。这个Token可以兑换一部分交易费用。Uniswap对用户在平台上进行的每笔交易收取0.30%的固定费用,并自动将其发送到一个流动性储备中。每当流动性提供者决定退出时,他们就会从准备金中获得相对于其在pool中持股金额的一部分费用。他们收到的Token记录了他们相对应的权益然后被销毁。在Uniswap v.2升级后,引入了一种新的协议费用,它可以通过社区投票来开启或关闭,基本上是将每0.30%的交易费用的0.05%发送给Uniswap基金,为未来的发展提供资金。目前,这一收费选项已被关闭,但一旦开启,这意味着参与者将开始收取pool交易费用的0.25%。四、Token的价格是如何决定的这个系统中另一个重要的原理是每个Token的价格是如何决定的。不同于最高买家和最低卖家决定每种资产的价格的订单系统,Uniswap使用的是自动做市商系统。这个替代的方法是基于供需的一个长期数学公式来调整资产价格。它的工作原理是根据每个资金pool中Token数量的比例来增加和减少Token的价格。这是一个非常重要的点,即每当有人向Uniswap添加一个新的ERC-20Token时,这个人就必须添加一定量的选定的ERC-20Token和等量的另一种ERC-20Token,以启动流动pool。每个Token价格的计算公式是x*y=k,TokenA的数量是X,TokenB的数量是Y,K是常数,是一个不变的数字。例如,Bob 想在Uniswap LINK/ETH资金pool中交易chainlink (LINK)Token, Bob将大量的LINK Token投入资金pool中,从而增加了资金pool中LINK相对ETH的比例。由于K值必须保持不变,这意味着ETH的成本增加,而资金pool中LINK Token的成本降低。因此,Bob LINKToken放入越多,他获取另外的Token就越少。流动性pool的规模也决定了Token价格在交易期间的变化幅度。在一个流动性pool中,资金越多,就越容易实施大规模的操作,而不语导致价格大幅下降。五、套利套利交易者是Uniswap生态系统中一个不可或缺的组成部分。这些交易员擅长发现多个交易平台之间的价格差异,并利用它们来获取利润。例如,如果BTC在Kraken上的交易价格是3.55万美元,Binance的价格是3.45万美元,你可以在Binance上购买BTC,然后在Kraken上出售,从而轻松获利。如果交易量大,就有可能以相对低的风险获得可观的利润。套利交易者在Uniswap上所做的是找到高于或低于其平均价格的Token交易——这是由于大量交易在pool中造成失衡并降低或提高价格的结果——并相应地买卖它们。六、如何使用Uniswap使用Uniswap相对简单,但是,你需要确保你已经有了支持ERC-20的钱包设置,如MetaMask、WalletConnect、Coinbase钱包、Portis或Fortmatic。一旦你有了这样一个钱包,你就需要添加ETH,以便在Uniswap上进行交易并支付GAS费(以太坊交易费用名称)。GAS费在价格方面的变化依赖于正在使用的网络的数量。在通过以太坊区块链进行支付时,大多数ERC-20兼容的钱包服务提供三种选择:慢速、中速或快速。慢速是最便宜的选择,快速是最贵的选择,而中速是介于中间。你交易快慢是靠以太坊网络节点维护者处理交易的速度决定的。翻译:Miki丨达瓴智库校对:Jane丨达瓴智库排版:炯炯丨达瓴智库审核:Amber丨达瓴智库原文:Ollie Leech, P. (2021, Feb 4). What Is Uniswap? A Complete Beginner’s Guide. CoinDesk. https://www.coindesk.com/business/2021/02/04/what-is-uniswap-a-complete-beginners-guide/发布于 2021-12-16 20:10区块链(Blockchain)去中心化金融(DeFi)​赞同 26​​3 条评论​分享​喜欢​收藏​申请

新手指南 | Uniswap 是什么? | 登链社区 | 区块链技术社区

新手指南 | Uniswap 是什么? | 登链社区 | 区块链技术社区

文章

问答

讲堂

专栏

集市

更多

提问

发表文章

活动

文档

招聘

发现

Toggle navigation

首页 (current)

文章

问答

讲堂

专栏

活动

招聘

文档

集市

搜索

登录/注册

新手指南 | Uniswap 是什么?

Bisade Asolo

更新于 2019-12-23 10:55

阅读 14974

Uniswap 是一个基于以太坊的协议,旨在实现 ETH 和 ERC20 代币数字资产之间的自动兑换。Uniswap 是一个完全的链上协议,个人只要安装了 MetaMask 就可以使用该协议。Uniswap 也可以被认为是一个 DeFi(去中心化金融)项目,因为它寻求利用其去中心化协议,消除参与数字资产交易流程的中间人。

Uniswap 是一个基于以太坊的协议,旨在实现 ETH 和 ERC20 代币数字资产之间的自动兑换。Uniswap 是一个完全的链上协议,个人只要安装了 MetaMask 就可以使用该协议。Uniswap 也可以被认为是一个 [DeFi(去中心化金融)](https://www.mycryptopedia.com/what-are-decentralized-finance-defi-applications/ "DeFi(去中心化金融)")项目,因为它寻求利用其去中心化协议,消除参与数字资产交易流程的中间人。

Uniswap 由两种类型的[智能合约](https://www.mycryptopedia.com/3-ethereum-smart-contract-use-cases/ "智能合约")组成:

- 一个交易合约 (exchange contract)

- 一个工厂合约 (factory contract)

这些合约都由 Vyper 智能合约编程语言所编写,而且它们是 Uniswap 协议功能的核心。**一个交易合约只支持一种 ERC20 代币,而且每个交易合约都储备了 ETH 和所支持的 ERC20 代币**。这意味着在一个特定交易合约上执行的交易,是基于合约中存储的 ETH 和 ERC20 代币的相对供应量。在交易合约中执行的交易,也可以是通过将 ETH 作为中介来实现 ERC20 与 ERC20 之间的直接交易。

**工厂合约**可用于部署一个新的交易合约,因此,**尚未拥有交易合约的 ERC20 代币都可以通过工厂合约来创建一个交易合约。**createExchange() 函数允许任何以太坊用户利用工厂合约部署一个交易合约。还需要注意的是,工厂合约被用作 Uniswap 交易合约的注册表,这意味着工厂合约可用于查询被添加进系统中的**所有代币和交易地址**。当一个交易合约发布时,工厂合约不会对代币进行检查(除了需要检查每个交易合约只支持一种代币的约束意外),因此,**用户应该只与他们完全信任的交易合约进行交互。**

## Uniswap 的流动性

Uniswap 协议的设计架构不同于传统数字资产交易所中的模型。大多数传统交易所会维护一个订单簿并用于匹配特定资产的买家和卖家。而 Uniswap 利用**流动性储备 (liquidity reserves) **来在协议中实现数字资产的交易。

交易合约的流动性储备由网络中的流动性提供者 (liquidity providers) 提供。这些流动性提供者将等价的 ETH 和 ERC20 代币存入特定的 ERC20 代币交易合约之中。**第一个向某个交易合约提供流动性的流动性提供者将首先设定 ETH 和该交易合约中相应的 ERC20 代币之间的兑换率**。流动性提供者通过存入他们认为等价的 ETH 和该合约支持的 ERC20 代币来设定这个兑换率的。**如果流动性提供者设定的兑换率与更广泛的加密货币交易市场不一致,那么套利的交易者会把 ETH 和 ERC20 代币间的兑换率引向市场上所认为的正确兑换率上。因此,所有后续的流动性提供者将会使用自己存入资产时的兑换率来存入流动性。**

Uniswap 还使用所谓的**“流动性代币” (liquidity tokens)**,这些代币本身符合 ERC20 标准。这些代币可以视为流动性提供者对交易合约贡献的一种表示。Uniswap 限制每个交易合约只能支持一种 ERC20 代币的背后原理是为了**鼓励流动性提供者将他们的流动性资产集中到单一的储备中**。Uniswap 通过铸造流动性代币来追踪每个流动性提供者贡献给总储备的相对比例。**流动性提供者能够在他们选择的时间对他们的流动性代币进行销毁,从而他们可以从交易合约中取出他们的 ETH 和 ERC20 代币的比例份额。**

流动性提供者可以选择出售或者转让其流动性代币,而不必从交易合约中移除流动性。然而,Uniswap的流动性代币被严格地指定于一个交易合约。**与 Uniswap 协议相关联的单一原生数字资产是不存在的**。流动性提供者还可以通过调用 addLiquidity() 函数往交易合约存入流动性。**作为提供流动性的奖励,流动性提供者在交易执行时会获得一定比例的交易手续费。**

## 在 Uniswap 进行交易:ETH ⇄ ERC20 交易

**能在 Uniswap 协议中执行的其中一类交易是将 ETH 与任何特定的 ERC20 代币进行兑换。**如前文所述,**ETH 和某种 ERC20 代币之间的兑换率基于交易合约中各资产流动性池的相对大小。**兑换率是建立这个在 Uniswap 的不变式之上:**ETH pool * token pool = invariant (不变量)**。在执行 Uniswap 协议上的任何交易期间,invariant (不变量)会保持不变。此外,**只有当交易合约中的流动性增加或减少时,不变量才会发生变化。**

**例子 ETH ⇄ BAT:**Bob 希望发起一笔交易,用于把 1 ETH 兑换为 ERC20 代币 BAT。Bob 将使用一个 Uniswap 协议上现有的交易合约来执行此交易。流动性提供者已将一定数量的 ETH 和 BAT 存入该交易合约,就本例而言,假设为 10 ETH 和 500 BAT。底层的不变式被设置为:ETH pool * BAT pool = invariant (不变量),也即:

- ETH pool = 10

- BAT pool = 500

- Invariant = 10 * 500 = 5,000

Bob 通过发送 1 ETH 到交易合约的 ETH 池中来发起他交易,在此基础上扣除 0.3% 作为给流动性提供者的费用。**剩下的 0.997 ETH 被添加到 ETH 池子中。然后,用不变量除以新的 ETH 总量以确定 BAT 池子的最新大小**。然后,剩余的 BAT 代币会被发送给买家,在本例中买家为 Bob。也即:

- Bob 发送:1 ETH

- 手续费:0.003 ETH

- ETH pool = 10 + (1 – 0.003) = 10.997

- BAT pool = 5000/10.997 = 454.67

- Bob 收到:500 – 454.67 = 45.33 BAT

**支付给流动性提供者的手续费在先前 Bob 发起交易时被扣除,现在这笔手续费又被加入到流动性池子中 (这意味着不变量会增加)。**这起到了向流动性提供者支付费用的作用,**当这些提供者从市场中移除他们的流动性贡献时,他们就可以获取这些费用。**由于这笔费用在价格计算后重新被加入,**随着在交易合约上的每笔交易被执行,不变量逐渐增大,**使得把流动性存入一个交易合约的行为对于流动性提供者来说是一种有利可图的行为。此时:

- ETH pool = 10.997 + 0.003 = 11

- BAT pool = 454.67

- 新的不变量 (invariant) = 5,001.37

在这次交易中,Bob 获取 BAT 的兑换率为 45.33 BAT/ETH。

- 输入:1 ETH

- 输出:45.33 BAT

- 兑换率 = 45.33 BAT/ETH

## 在 Uniswap 进行交易:ERC20 ⇄ ERC20 交易

**能在 Uniswap 协议中执行的另一类交易是将某种 ERC20 代币与另一种 ERC20 代币进行兑换。**由于 ETH 用作所有 ERC20 代币的公共交易对,Uniswap 将 ETH 用作 ERC20 和 ERC20 之间直接交易的**中介资产**。比如,Uniswap 可以在某个交易合约中将 BAT 代币兑换成 ETH,然后在另一个交易合约中将这部分 ETH 兑换成 OMG,所有这些都是在一笔交易种完成的,从而使得任何两种 ERC20 代币之间得兑换成为可能。。

这个公式的作用非常类似于一个普通的市场,原因是你购买的代币越多时,为了购买多一个单元的代币,你所要支付的边际兑换率就会越高。

## 交易费用构成

在协议上执行交易的 Uniswap 费用的构成如下:

- ETH 兑换为 ERC20:支付 0.3% 的 ETH。

- ERC20 兑换为 ETH:支付 0.3% 的 ERC20 代币。

- ERC20 兑换为 ERC20:支付 0.3% 的 ERC20 代币 (针对将某种 ERC20 代币兑换为 ETH) 和 0.3% 的 ETH (针对将 ETH 再兑换为另一种 ERC20 代币)。

## 代币兑换率

Uniswap 采用的用于确定代币间兑换率的不变式取自于 Vitalik Buterin 在2018年3月发表的[一篇文章](https://ethresear.ch/t/improving-front-running-resistance-of-x-y-k-market-makers/1281)。在该文章中,ERC20 代币的兑换率按照一下公式计算:

x * y = k

k 是一个不变的常量,x和y表示在特定的交易中可用的 ETH 和 ERC20 代币的数量,在 Uniswap 的情况下,x 和 y 就是在特定交易合约中可用的 ETH 和 ERC20 代币的数量。**利用这个公式,代币的兑换率将始终是上述公式产生的曲线上的某个点**。见下图。

x*y=k 公式是 Uniswap 协议中不可分割的一部分,而且 Vitalik Buterin 使用下面所示的图,以如下方式进行描述:

![null](https://img.learnblockchain.cn/2020/02/08_/723842887.png)

根据该公式,一个合约 (在本例中为 Uniswap 的交易合约) 将会持有x个**代币A**和y个**代币B**。**这个合约将维持不变量使得 x * y = k 始终成立**。任何个人都可以通过有效地改变做市商在 x * y 曲线上的头寸 (position) 来买卖该合约中的代币。将做市商的头寸向右移动意味着,一笔交易向右移动的金额就是交易员必须输入的**代币A**的量 (即通过输入代币A来兑换代币B)。相应地,做市商的头寸向下移动的量是交易者应该买入多少**代币B**。

假设代币B是 ETH,代币A是任何特定的 ERC20 代币,我们可以看到它们是如何利用公式x * y = k进行运转的。如果 Alice 决定购买一大批特定的 ERC20 代币,这将使得交易合约中的 ERC20 代币的短缺以及 ETH 的增加。Alice 的购买结果是把兑换率移动到 x * y = k 曲线上的一个不同的点。也即红点将会向左移动,这意味着购买更多 ERC20 代币会变得更加昂贵。ETH 和 ERC20 代币的相对供应量可以被认为是 ERC20 代币供需的反映,这最终会决定兑换率价值。

如前文所述,流动性往交易合约的首次存入并非由兑换率所决定。相反,流动性提供者将会存入一定数量的 ETH 和 ERC20 代币,这反映了他们心中 ETH 和 ERC20 代币间的兑换率值。**如果 ETH 和 ERC20 代币被认为是太便宜或者太贵,那么套利交易者就会受到激励去把价格带到市场认为正确的价格上。**

## Uniswap 和套利交易

值得主要的是,虽然 Uniswap 是一个去中心化的链上数字资产交易所,但是它不是为了替代中心化交易所而存在的。当 Uniswap 上的交易机制出现偏差时(笔者注:这里的意思是 Uniswap 上兑换率偏高或偏低),那么必须存在一个纠正错误的机制。这个机制以套利交易的形式存在。

套利交易 (arbitrage trading) 可以被很好地理解为交易者利用两个市场之间存在的价格差进行交易获利的策略。在加密货币的情况下,这种价格差可以在[加密货币交易所](https://www.mycryptopedia.com/cryptocurrency-exchanges-complete-beginners-guide/ "加密货币交易所")之间的数字资产价格差异中找到。如果交易者发现了套利的机会,那么他们会在一个交易所中购买数字资产,然后在另一个加密货币交易所中出售。**套利交易对于 Uniswap 的运作至关重要,这是因为交易者可以利用其他加密货币交易所的兑换率来纠正 Uniswap 上可能出现的价格偏差。**

## Uniswap 和 Gas

使用 Uniswap 协议进行数字资产兑换的其中一个优点是它十分节约 gas。**在 Uniswap 上进行交易时所产生的 gas 成本相对来说比其他去中心化交易所更加便宜。**正如下面展示的 gas 基准:ETH 至 ERC20,ERC20 至 ETH 以及 ERC20 至 ERC20 的转换明显比 Bancor,EtherDelta 等交易所更加便宜。

![null](https://img.learnblockchain.cn/2020/02/08_/422127116.jpeg)

节约 gas 只是 Uniswap 协议的其中一个优点,更多的**优点**包括:

- Uniswap 是去中心化的,因此它的运作不依赖于第三方。此外,希望连接到该协议的任何人都可以自由访问 Uniswap。

- 与其他数字资产交易所相比,在 Uniswap 上进行交易的成本相对较低。

- Uniswap 允许任何用户为任何特定的 ERC20 代币创建交易合约。

然而,Uniswap 确实有它的**局限性**:

- Uniswap 确实依靠套利交易来控制协议中代币的交易价格。这意味着 Uniswap 依靠其他数字资产交易所的存在来保持兑换率的平衡。

- Uniswap 仍处于试验阶段,仍需要对协议进行更多的改进以了解它在促进数字资产交易方面的有效性。

## 结论

总之,Uniswap 代表了以太坊生态内支持数字资产交易的重要一步,正因它使得资产交易得的流程变得更加有效。Uniswap 仍然处于初期阶段,但是,看着这一创新的协议在未来如何发展将是一件有趣的事情。

Uniswap 是一个基于以太坊的协议,旨在实现 ETH 和 ERC20 代币数字资产之间的自动兑换。Uniswap 是一个完全的链上协议,个人只要安装了 MetaMask 就可以使用该协议。Uniswap 也可以被认为是一个 DeFi(去中心化金融)项目,因为它寻求利用其去中心化协议,消除参与数字资产交易流程的中间人。

Uniswap 由两种类型的智能合约组成:

一个交易合约 (exchange contract)

一个工厂合约 (factory contract)

这些合约都由 Vyper 智能合约编程语言所编写,而且它们是 Uniswap 协议功能的核心。一个交易合约只支持一种 ERC20 代币,而且每个交易合约都储备了 ETH 和所支持的 ERC20 代币。这意味着在一个特定交易合约上执行的交易,是基于合约中存储的 ETH 和 ERC20 代币的相对供应量。在交易合约中执行的交易,也可以是通过将 ETH 作为中介来实现 ERC20 与 ERC20 之间的直接交易。

工厂合约可用于部署一个新的交易合约,因此,尚未拥有交易合约的 ERC20 代币都可以通过工厂合约来创建一个交易合约。createExchange() 函数允许任何以太坊用户利用工厂合约部署一个交易合约。还需要注意的是,工厂合约被用作 Uniswap 交易合约的注册表,这意味着工厂合约可用于查询被添加进系统中的所有代币和交易地址。当一个交易合约发布时,工厂合约不会对代币进行检查(除了需要检查每个交易合约只支持一种代币的约束意外),因此,用户应该只与他们完全信任的交易合约进行交互。

Uniswap 的流动性

Uniswap 协议的设计架构不同于传统数字资产交易所中的模型。大多数传统交易所会维护一个订单簿并用于匹配特定资产的买家和卖家。而 Uniswap 利用流动性储备 (liquidity reserves) 来在协议中实现数字资产的交易。

交易合约的流动性储备由网络中的流动性提供者 (liquidity providers) 提供。这些流动性提供者将等价的 ETH 和 ERC20 代币存入特定的 ERC20 代币交易合约之中。第一个向某个交易合约提供流动性的流动性提供者将首先设定 ETH 和该交易合约中相应的 ERC20 代币之间的兑换率。流动性提供者通过存入他们认为等价的 ETH 和该合约支持的 ERC20 代币来设定这个兑换率的。如果流动性提供者设定的兑换率与更广泛的加密货币交易市场不一致,那么套利的交易者会把 ETH 和 ERC20 代币间的兑换率引向市场上所认为的正确兑换率上。因此,所有后续的流动性提供者将会使用自己存入资产时的兑换率来存入流动性。

Uniswap 还使用所谓的“流动性代币” (liquidity tokens),这些代币本身符合 ERC20 标准。这些代币可以视为流动性提供者对交易合约贡献的一种表示。Uniswap 限制每个交易合约只能支持一种 ERC20 代币的背后原理是为了鼓励流动性提供者将他们的流动性资产集中到单一的储备中。Uniswap 通过铸造流动性代币来追踪每个流动性提供者贡献给总储备的相对比例。流动性提供者能够在他们选择的时间对他们的流动性代币进行销毁,从而他们可以从交易合约中取出他们的 ETH 和 ERC20 代币的比例份额。

流动性提供者可以选择出售或者转让其流动性代币,而不必从交易合约中移除流动性。然而,Uniswap的流动性代币被严格地指定于一个交易合约。与 Uniswap 协议相关联的单一原生数字资产是不存在的。流动性提供者还可以通过调用 addLiquidity() 函数往交易合约存入流动性。作为提供流动性的奖励,流动性提供者在交易执行时会获得一定比例的交易手续费。

在 Uniswap 进行交易:ETH ⇄ ERC20 交易

能在 Uniswap 协议中执行的其中一类交易是将 ETH 与任何特定的 ERC20 代币进行兑换。如前文所述,ETH 和某种 ERC20 代币之间的兑换率基于交易合约中各资产流动性池的相对大小。兑换率是建立这个在 Uniswap 的不变式之上:*ETH pool token pool = invariant (不变量)。在执行 Uniswap 协议上的任何交易期间,invariant (不变量)会保持不变。此外,只有当交易合约中的流动性增加或减少时,不变量才会发生变化。**

例子 ETH ⇄ BAT:Bob 希望发起一笔交易,用于把 1 ETH 兑换为 ERC20 代币 BAT。Bob 将使用一个 Uniswap 协议上现有的交易合约来执行此交易。流动性提供者已将一定数量的 ETH 和 BAT 存入该交易合约,就本例而言,假设为 10 ETH 和 500 BAT。底层的不变式被设置为:ETH pool * BAT pool = invariant (不变量),也即:

ETH pool = 10

BAT pool = 500

Invariant = 10 * 500 = 5,000

Bob 通过发送 1 ETH 到交易合约的 ETH 池中来发起他交易,在此基础上扣除 0.3% 作为给流动性提供者的费用。剩下的 0.997 ETH 被添加到 ETH 池子中。然后,用不变量除以新的 ETH 总量以确定 BAT 池子的最新大小。然后,剩余的 BAT 代币会被发送给买家,在本例中买家为 Bob。也即:

Bob 发送:1 ETH

手续费:0.003 ETH

ETH pool = 10 + (1 – 0.003) = 10.997

BAT pool = 5000/10.997 = 454.67

Bob 收到:500 – 454.67 = 45.33 BAT

支付给流动性提供者的手续费在先前 Bob 发起交易时被扣除,现在这笔手续费又被加入到流动性池子中 (这意味着不变量会增加)。这起到了向流动性提供者支付费用的作用,当这些提供者从市场中移除他们的流动性贡献时,他们就可以获取这些费用。由于这笔费用在价格计算后重新被加入,随着在交易合约上的每笔交易被执行,不变量逐渐增大,使得把流动性存入一个交易合约的行为对于流动性提供者来说是一种有利可图的行为。此时:

ETH pool = 10.997 + 0.003 = 11

BAT pool = 454.67

新的不变量 (invariant) = 5,001.37

在这次交易中,Bob 获取 BAT 的兑换率为 45.33 BAT/ETH。

输入:1 ETH

输出:45.33 BAT

兑换率 = 45.33 BAT/ETH

在 Uniswap 进行交易:ERC20 ⇄ ERC20 交易

能在 Uniswap 协议中执行的另一类交易是将某种 ERC20 代币与另一种 ERC20 代币进行兑换。由于 ETH 用作所有 ERC20 代币的公共交易对,Uniswap 将 ETH 用作 ERC20 和 ERC20 之间直接交易的中介资产。比如,Uniswap 可以在某个交易合约中将 BAT 代币兑换成 ETH,然后在另一个交易合约中将这部分 ETH 兑换成 OMG,所有这些都是在一笔交易种完成的,从而使得任何两种 ERC20 代币之间得兑换成为可能。。

这个公式的作用非常类似于一个普通的市场,原因是你购买的代币越多时,为了购买多一个单元的代币,你所要支付的边际兑换率就会越高。

交易费用构成

在协议上执行交易的 Uniswap 费用的构成如下:

ETH 兑换为 ERC20:支付 0.3% 的 ETH。

ERC20 兑换为 ETH:支付 0.3% 的 ERC20 代币。

ERC20 兑换为 ERC20:支付 0.3% 的 ERC20 代币 (针对将某种 ERC20 代币兑换为 ETH) 和 0.3% 的 ETH (针对将 ETH 再兑换为另一种 ERC20 代币)。

代币兑换率

Uniswap 采用的用于确定代币间兑换率的不变式取自于 Vitalik Buterin 在2018年3月发表的一篇文章。在该文章中,ERC20 代币的兑换率按照一下公式计算:

x * y = k

k 是一个不变的常量,x和y表示在特定的交易中可用的 ETH 和 ERC20 代币的数量,在 Uniswap 的情况下,x 和 y 就是在特定交易合约中可用的 ETH 和 ERC20 代币的数量。利用这个公式,代币的兑换率将始终是上述公式产生的曲线上的某个点。见下图。

x*y=k 公式是 Uniswap 协议中不可分割的一部分,而且 Vitalik Buterin 使用下面所示的图,以如下方式进行描述:

根据该公式,一个合约 (在本例中为 Uniswap 的交易合约) 将会持有x个代币A和y个代币B。这个合约将维持不变量使得 x * y = k 始终成立*。任何个人都可以通过有效地改变做市商在 x y 曲线上的头寸 (position) 来买卖该合约中的代币。将做市商的头寸向右移动意味着,一笔交易向右移动的金额就是交易员必须输入的代币A的量 (即通过输入代币A来兑换代币B)。相应地,做市商的头寸向下移动的量是交易者应该买入多少代币B**。

假设代币B是 ETH,代币A是任何特定的 ERC20 代币,我们可以看到它们是如何利用公式x y = k进行运转的。如果 Alice 决定购买一大批特定的 ERC20 代币,这将使得交易合约中的 ERC20 代币的短缺以及 ETH 的增加。Alice 的购买结果是把兑换率移动到 x y = k 曲线上的一个不同的点。也即红点将会向左移动,这意味着购买更多 ERC20 代币会变得更加昂贵。ETH 和 ERC20 代币的相对供应量可以被认为是 ERC20 代币供需的反映,这最终会决定兑换率价值。

如前文所述,流动性往交易合约的首次存入并非由兑换率所决定。相反,流动性提供者将会存入一定数量的 ETH 和 ERC20 代币,这反映了他们心中 ETH 和 ERC20 代币间的兑换率值。如果 ETH 和 ERC20 代币被认为是太便宜或者太贵,那么套利交易者就会受到激励去把价格带到市场认为正确的价格上。

Uniswap 和套利交易

值得主要的是,虽然 Uniswap 是一个去中心化的链上数字资产交易所,但是它不是为了替代中心化交易所而存在的。当 Uniswap 上的交易机制出现偏差时(笔者注:这里的意思是 Uniswap 上兑换率偏高或偏低),那么必须存在一个纠正错误的机制。这个机制以套利交易的形式存在。

套利交易 (arbitrage trading) 可以被很好地理解为交易者利用两个市场之间存在的价格差进行交易获利的策略。在加密货币的情况下,这种价格差可以在加密货币交易所之间的数字资产价格差异中找到。如果交易者发现了套利的机会,那么他们会在一个交易所中购买数字资产,然后在另一个加密货币交易所中出售。套利交易对于 Uniswap 的运作至关重要,这是因为交易者可以利用其他加密货币交易所的兑换率来纠正 Uniswap 上可能出现的价格偏差。

Uniswap 和 Gas

使用 Uniswap 协议进行数字资产兑换的其中一个优点是它十分节约 gas。在 Uniswap 上进行交易时所产生的 gas 成本相对来说比其他去中心化交易所更加便宜。正如下面展示的 gas 基准:ETH 至 ERC20,ERC20 至 ETH 以及 ERC20 至 ERC20 的转换明显比 Bancor,EtherDelta 等交易所更加便宜。

节约 gas 只是 Uniswap 协议的其中一个优点,更多的优点包括:

Uniswap 是去中心化的,因此它的运作不依赖于第三方。此外,希望连接到该协议的任何人都可以自由访问 Uniswap。

与其他数字资产交易所相比,在 Uniswap 上进行交易的成本相对较低。

Uniswap 允许任何用户为任何特定的 ERC20 代币创建交易合约。

然而,Uniswap 确实有它的局限性:

Uniswap 确实依靠套利交易来控制协议中代币的交易价格。这意味着 Uniswap 依靠其他数字资产交易所的存在来保持兑换率的平衡。

Uniswap 仍处于试验阶段,仍需要对协议进行更多的改进以了解它在促进数字资产交易方面的有效性。

结论

总之,Uniswap 代表了以太坊生态内支持数字资产交易的重要一步,正因它使得资产交易得的流程变得更加有效。Uniswap 仍然处于初期阶段,但是,看着这一创新的协议在未来如何发展将是一件有趣的事情。

学分: 31

分类: Uniswap

标签:

Uniswap 

点赞 5

收藏 7

分享

Twitter分享

微信扫码分享

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

你可能感兴趣的文章

UniswapV3 部署 - - Foundry Edition

240 浏览

uniswap v2

315 浏览

#DEFI# 从 BitShares 到 Uniswap:Dex 发展亲历者的去中心化交易所之旅

554 浏览

对比 Uniswap,一种新的去中心化交易所的流动性算法

976 浏览

以太坊合并一年后的MEV格局

961 浏览

EIP-1153 除了赋能 Uniswap-v4 还能做什么

906 浏览

相关问题

有没有已经部署的合约去查询Uniswap V3 NFT仓位的价值

1 回答

一般在编写套利合约中通过什么能直接读取给定交易对uniswap v2中的价格?

4 回答

uniswap 多跳路由寻找相关

1 回答

Uniswap中的LP token的数值是如何计算出来的(需要详细计算过程)

2 回答

如何获取 Uniswap 代币价格,v2 v3 是否不同

1 回答

复制以太坊钱包地址在 Uniswap 上发生交易的机器人

2 回答

3 条评论

请先 登录 后评论

Bisade Asolo

关注

贡献值: 30

学分: 48

江湖只有他的大名,没有他的介绍。

文章目录

关于

关于我们

社区公约

学分规则

Github

伙伴们

DeCert

ChainTool

GCC

合作

广告投放

发布课程

联系我们

友情链接

关注社区

Discord

Twitter

Youtube

B 站

公众号

关注不错过动态

微信群

加入技术圈子

©2024 登链社区 版权所有 |

Powered By Tipask3.5|

粤公网安备 44049102496617号

粤ICP备17140514号

粤B2-20230927

增值电信业务经营许可证

×

发送私信

请将文档链接发给晓娜,我们会尽快安排上架,感谢您的推荐!

发给:

内容:

取消

发送

×

举报此文章

垃圾广告信息:

广告、推广、测试等内容

违规内容:

色情、暴力、血腥、敏感信息等内容

不友善内容:

人身攻击、挑衅辱骂、恶意行为

其他原因:

请补充说明

举报原因:

取消

举报

×

如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!

uniswap - V3技术白皮书导读 - 知乎

uniswap - V3技术白皮书导读 - 知乎切换模式写文章登录/注册uniswap - V3技术白皮书导读Star.Liuniswap V3一公布就引发广泛关注。相对V2来说,逻辑和代码都复杂一些。V3的核心是通过盘口区间提供流动性(集中式流动性),解决LP提供流动性时的资金利用率的问题。什么是资金利用率?V3如何推导区间流动性的计算公式?如何理解流动性?如何计算swap费用?本文先从V3技术白皮书详细分析开始。先给出一些uniswap官方有关V3的资料:V3官方介绍https://uniswap.org/blog/uniswap-v3/技术白皮书https://uniswap.org/whitepaper-v3.pdf智能合约代码https://github.com/Uniswap/uniswap-v3-corehttps://github.com/Uniswap/uniswap-v3-periphery推荐先认真查看技术白皮书,再看智能合约代码,最后再对照官方介绍。理解了技术白皮书,代码非常容易理解。在技术白皮书的第一章总结了uniswap V3的技术特点,最核心的概念是"Concentrated Liquidity" (集中式流动性)。1 资金利用率先看看uniswap V2的资金利用率:上图为资金池中的x/y的量变化曲线。资金池中的当前价格在c点,并且假设会在a价格点和b价格点之间波动。从c点向a点滑动,消耗最大y_real,从c点向b点滑动,消耗最大为x_real。也就是说,当前价格c点,在a点和b点之间震荡的话,最大只需要消耗x_real和y_real。理论上只要提供x_real和y_real就足够了。而事实上,如上图所示,在价格c点,分别提供了大于x_real和y_real的x和y。明显可以看出,x-x_real和y-y_real的资金在这种情况下是永远用不上的,也就称为闲置资金。在这种情况下,资金利用率为x_real/x或者y_real/y。如果价格波动非常小的话,资金利用率是非常低的。uniswap V3就是尝试解决资金利用率低的问题。想法比较简单,所有资金可以只添加到某个区间,只添加到有可能价格波动到的区间范围。如何在某个区间添加流动性并提供swap功能是uniswap V3的重点。先从Virtual Reserves说起。2 虚拟资金池(Virtual Reserves)uniswap的交易采用的乘积固定模型(x*y=k)。所谓的虚拟资金池(Virtual Reserves),是指还在乘积固定曲线上,只提供某个区间流动性的资金池:图中的墨绿色的曲线就是虚拟资金池满足的乘积固定曲线。事实上需要的资金的曲线如图中的橘黄色。橘黄色的曲线公式如下图:你可以想象成虚拟资金曲线在x/y轴进行平移,使得a/b点和x/y轴重合。也就是用一定量的资金就能达到“虚拟”的交易曲线的效果。如何计算在某个区间提供虚拟资金池,先要从深入理解流动性开始。3 流动性(Liquidity - L)乘积固定的交易模型,满足资金池中的两种代币金额满足:x*y = K。如果设定K=L^2的话,x*y = L^2。L就是我们说的流动性。由乘积固定的交易模型得出如下的公式:在已知L和sqrt(P)的情况下,也能推导出资金需求量x和y。通过公式6.6,在流动性不变的情况下(不添加删除流动性),流动性可以看成是单位“价格波动”的y资金量的变化。”价格波动“打上引号是因为事实上是sqrt(P)的变化。这个是uniswap V3核心公式(6.7),用相对值(资金和价格相对值)来计算流动性。所谓的流动性,就是单位“价格变化”的资金量。在一定的交易量的情况下,如果流动性好,价格变化就小,流动性不够的话,价格波动就大。特别注意的是,一个区间上的流动性和V2的普适的流动性不同。一个区间上的流动性,重点在“区间上”。不同区间的流动性没有可比性。V2的流动性和区间上的流动性的区别如下图:V2的流动性是“普适”的,在所有价格点上流动性相同。V3的流动性是由一系列不同区间上的流动性组成。相对来说,在当前价格左右的流动性比较高。流动性提供者LP只有提供了可供交易的流动性才能获取交易费。为了获取更多的交易费,为了提高资金的利用率,流动性提供者会将资金提供在合理的价格波动范围内。也就是说,在某个区间swap交易产生的手续费,只有该区间流动性提供者才能获取手续费。为了计算每个区间获取的手续费,引入了Tick的概念和计算方法。4 Tick在区间上提供流动性,带来很多复杂的情况:区间和区间的重叠覆盖。因为不同区间的流动性没有可比性,某个交易费并不能混入到流动资金中,等删除流动性时候一并提取。为了解决这个问题,uniswap V3引入了Tick的概念。交易费用实时结算并单独记录,并不混入流动资金中。虽然整个区间和区间的流动性没有可比性,但是,在具体的某个价格点上(一个价格片上),流动性是可比较的。uniswap V3将整个价格范围(负无穷到正无穷)分成一个个的Tick(价格点):后一个价格点的价格是前一个价格点价格基础上浮动万分之一。每个Tick也有一个唯一的序号。区间(Position)可以由两个Tick表示。逻辑上交易手续费可以一个个的Tick计算,并在每一个Tick上根据流动性的占比进行交易分配。先看看一个Tick范围的swap的计算。5Tick内的SWAP假设有一个很小的量的y,需要swap为x。通过6.13的公式,可以计算出因为y的变化导致的价格变化。再利用6.15的公式可以算出换取的x的量。uniswap V3针对同样的交易对设置了不同的交易费:0.05%,0.3%以及1% (还可以添加其他费率)。如果是从Y换取X,则在换取之前先扣除手续费再进行上述的交换。在同一交易池中只支持一种费率。也就是说,在一个交易池中支持不同的价格区间,但是都是同样的费率。如果需要添加同样交易对的不同交易费的交易池,必须创建新的交易池。接下来,深入讲解一下 添加/删除流动性以及交易费用的计算逻辑。6 添加/删除流动性V3的添加/删除流动性是当前价格情况下在某个区间添加或者删除流动性。所有的流动性添加/删除流动性采用如下的公式:注意,价格变化并不是指区间的大小,而是在某个区间上提供流动性,相对当前价格,“需要相应资金变化”对应的价格变化。分为三种情况,想要添加的流动性区间和当前价格的关系。当前价格处于流动性价格区间 (il <= ic < iu)因为价格在区间范围内,如果价格滑动到il,则需要提供y资金。如果价格滑动到iu,则需要提供相应的x资金。所以,对于delta_Y来说的,价格变化为sqrt(P) - sqrt(p(il));对于delta_X来说,价格变化为1/sqrt(P) - 1/sqrt(p(iu))。当前价格低于流动性价格区间因为当前价格远低于il,即使从当前价格向iu滑动,也只需要x的资金,不需要y的资金。所以,在这种情况下,delta_Y = 0。因为从il滑向iu需要整个区间的x的资金,对于delta_X来说,价格变化为1/sqrt(il) - 1/sqrt(p(iu))。当前价格高于流动性价格区间和第一种情况类似,不重复分析了。7 Swap交易费用uniswap最复杂的逻辑是计算交易费用并分配。在添加和删除流动性之前需要将相应的交易费用提取。Tick上的总流动性 在某个Tick上可以存在多个区间。在计算交易费用时,需要平摊这些费用给所有在这个Tick上多个区间的总的流动性。在每个区间的边界的Tick上记录下delta_L(所有以这个Tick为边界的区间的流动性总和)。 存在一个全局状态:liquidity,保存当前价格对应Tick的流动性总和。当价格波动,穿过某个Tick时,会进行流动性的增加或者减少(取决于价格波动方向)。举例来说,价格从左到右穿过区间,当穿过区间的第一个Tick时,流动性需要增加,穿出最后一个Tick时,流动性需要减少,中间的Tick都没有流动性的增加和减少(delta_L为0)。 区间(Position)上的交易费用率 计算一个区间上的交易费用率,采用总的费用率减去区间外的费用率的方法。在一个区间的边界Tick上记录feeGrowthOutside。所谓的feeGrowthOutside,就是“另外”一个方向上总的费用率。另外的一个方向是相对穿过当前Tick的方向而言。当价格从左到右穿过一个Tick,feeGrowthOutside指的是Tick左边所有区间的费用率。简单的说,就是价格要去方向的相反方向所有区间的费用率。feeGrowthOutside用fo表示。因为fo是一个Tick的两个方向的总的费用率,两个方向的费用率的总和肯定是等于fg(全局的费用率)。所以当穿过一个Tick时,这个Tick上的fo要进行翻转:当一个区间创建时,区间边界上Tick的fo需要初始化:如果当前的价格大于Tick的价格时,因为即使当前价格在设置的区间内,但是之前费用也不会分到,所以,可以简单的假想为所有的费用发生在Tick价格之下,也就是fo=fg。如果Tick的价格大于当前价格,价格还没有穿过Tick,因为假设了之前所有发生的费用发生在Tick价格之下,Tick之上是没有费用的,所以fo=0。在理解了这些逻辑的基础上,在swap的过程中,随着价格的波动,一个区间上,超过最高Tick的费用率以及低于最低Tick的费用率可以用如下的方式计算:以低于最低Tick的费用率的计算为例,如果ic>=i (当前的价格是高于最低Tick的),低于Tick的所有的费用率就是fo(定义如此)。如果icUniswap-v2 合约概览 | ethereum.orgwap-v2 合约概览 | ethereum.org跳转至主要内容学习用法构建参与研究搜索​​​​语言 ZH帮助更新此页面本页面有新版本,但现在只有英文版。请帮助我们翻译最新版本。翻译页面没有错误!此页面未翻译,因此特意以英文显示。不再显示Uniswap-v2 合约概览solidity中级Ori Pomerantz 2021年5月1日81 分钟阅读 minute read在本页面介绍Uniswap 是做什么的?为什么选择 v2? 而不是 v3?核心合约与外围合约数据和控制流程兑换增加流动资金撤回流动资金核心合约UniswapV2Pair.solUniswapV2Factory.solUniswapV2ERC20.sol外围合约UniswapV2Router01.solUniswapV2Router02.solUniswapV2Migrator.sol程序库数学定点小数 (UQ112x112)UniswapV2Library转账帮助结论介绍Uniswap v2(opens in a new tab) 可以在任何两个 ERC-20 代币之间创建一个兑换市场。 在本文中,我们将深入探讨实现此协议的合约的源代码,了解为何要如此编写协议。Uniswap 是做什么的?一般来说有两类用户:流动资金提供者和交易者。流动性提供者为资金池提供两种可以兑换的代币(称为 Token0 和 Token1)。 作为回报,他们会收到第三种叫做流动性代币的代币,代表他们对资金池的部分所有权。交易者将一种代币发送到资金池,并从资金池中接收流动性提供者提供的另一种代币(例如,发送 Token0 并获得 Token1)。 兑换汇率由资金池中 Token0 和 Token1 的相对数量决定。 此外,资金池将收取汇率的一小部分作为流动性资金池的奖励。当流动性提供者想要收回他们的代币资产时,他们可以销毁资金池代币并收回他们的代币,其中包括属于他们的奖励。点击此处查看更完整的描述(opens in a new tab)。为什么选择 v2? 而不是 v3?Uniswap v3(opens in a new tab) 是 v2 的升级,远比 v2 复杂得多。 比较容易的方法是先学习 v2,然后再学习 v3。核心合约与外围合约Uniswap v2 可以分为两个部分,一个为核心部分,另一个为外围部分。 核心合约存放着资产,因而必须确保安全,这种分法就使核心合约更加简洁且更便于审核。 而所有交易者需要的其它功能可以通过外围合约提供。数据和控制流程执行 Uniswap 的三个主要操作时,会出现以下数据和控制流程:兑换不同代币将资金添加到市场中提供流动性,并获得兑换中奖励的流动池 ERC-20 代币消耗流动池 ERC-20 代币并收回交易所允许交易者兑换的 ERC-20 代币兑换这是交易者最常用的流程:调用者向外围帐户提供兑换额度。调用外围合约中的一个兑换函数。外围合约有多种兑换函数,调用哪一个取决于是否涉及以太币、交易者是指定了存入的代币金额还是提取的代币金额等。 每个兑换函数都接受一个 path,即要执行的一系列兑换。在外围合约 (UniswapV2Router02.sol) 中确定兑换路径中,每次兑换所需交易的代币数额。沿路径迭代。 对于路径上的每次兑换,首先发送输入代币,然后调用交易所的 swap 函数。 在大多数情况下,代币输出的目的地址是路径中下一个配对交易。 在最后一个交易所中,该地址是交易者提供的地址。在核心合约 (UniswapV2Pair.sol) 中验证核心合约没有被欺骗,可在兑换后保持足够的流动资金。检查除了现有的储备金额外,还有多少额外的代币。 此数额是我们收到的要用于兑换的输入代币数量。将输出代币发送到目的地址。调用 _update 来更新储备金额回到外围合约 (UniswapV2Router02.sol)执行所需的必要清理工作(例如,消耗包装以太币代币以返回以太币给交易者)增加流动资金调用者向外围帐户提交准备加入流动资金池的资金额度。调用外围合约的其中一个 addLiquidity 函数。在外围合约 (UniswapV2Router02.sol) 中必要时创建一个新的配对交易如果有现有的币对交易所,请计算要增加的代币金额。 该金额对于两种代币应该是相同的,因此新代币对现有代币的比率是相同的。检查金额是否可接受(调用者可以指定一个最低金额,低于此金额他们就不增加流动性)调用核心合约。在核心合约 (UniswapV2Pair.sol) 中生成流动池代币并将其发送给调用者调用 _update 来更新储备金额撤回流动资金调用者向外围帐户提供一个流动池代币的额度,作为兑换底层代币所需的消耗。调用外围合约的其中一个 removeLiquidity 函数。在外围合约 (UniswapV2Router02.sol) 中将流动池代币发送到该配对交易在核心合约 (UniswapV2Pair.sol) 中向目的地址发送底层代币,金额与销毁的代币成比例。 例如,如果资金池里有 1000 个 A 代币,500 个 B 代币和 90 个流动性代币,而我们收到请求销毁 9 个流动性代币,那么,我们将销毁 10% 的流动性代币,然后将返还用户 100 个 A 代币和 50 个 B 代币。销毁流动性代币调用_update来更新储备金额核心合约这些是持有流动资金的安全合约。UniswapV2Pair.sol本合约(opens in a new tab)实现用于交易代币的实际资金池。 这是 Uniswap 的核心功能。1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';显示全部 复制这些都是该合约需要知道的接口,因为该合约实现了它们(IUniswapV2Pair 和 UniswapV2ERC20),或因为该合约调用了实现它们的合约。1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 { 复制此合约继承自 UniswapV2ERC20,为流动池代币提供 ERC-20 代币功能。1 using SafeMath for uint; 复制SafeMath 库(opens in a new tab)用于避免整数上溢和下溢。 这很重要,否则最终可能会出现这样的情况:本该是 -1 的值,结果却成了 2^256-1。1 using UQ112x112 for uint224; 复制流动池合约中的许多计算都需要分数。 但是,以太坊虚拟机本身不支持分数。 Uniswap 找到的解决方案是使用 224 位数值,整数部分为 112 位,小数部分为 112 位。 因此,1.0 用 2^112 表示,1.5 用 2^112 + 2^111 表示,以此类推。关于这个函数库的更详细内容在文档的稍后部分。变量1 uint public constant MINIMUM_LIQUIDITY = 10**3; 复制为了避免分母为零的情况,始终存在最低数量的流动性代币(但为帐户零所拥有)。 该数字,即 MINIMUM_LIQUIDITY,为 1000。1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 复制这是 ERC-20 传输函数的应用程序二进制接口选择程序。 它用于在两个代币帐户中转移 ERC-20 代币。1 address public factory; 复制这就是由工厂合约创造的资金池地址。 每个资金池都是两种 ERC-20 代币之间的交易所,工厂是连接所有这些资金池的中心点。1 address public token0;2 address public token1; 复制这两个地址是该资金池可以交易的两种 ERC-20 代币的合约地址。1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReserves 复制每个代币类型都有储备的资源库。 我们假定两者代表相同数量的值,因此每个 token0 的价值都等同于 reserve1/reserve0 token1。1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 复制发生兑换的最后一个区块的时间戳,用来追踪一段时间内的汇率。以太坊合约中燃料消耗量最大的一项是存储,这种燃料消耗从一次合约调用持续到下一次调用。 每个存储单元长度为 256 位。 因此,reserve0、reserve1 和 blockTimestampLast 三个变量的分配方式让单个存储值可以包含全部这三个变量 (112+112+32=256)。1 uint public price0CumulativeLast;2 uint public price1CumulativeLast; 复制这些变量存放每种代币的累计成本(每种代币在另一种代币的基础上计算)。 可以用来计算一段时间内的平均汇率。1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event 复制币对交易所决定 token0 和 token1 之间汇率的方式是在交易中保持两种储备金的乘数恒定不变。 即 kLast 这个值。 当流动性提供者存入或提取代币时,该乘数就会变化,由于兑换市场的费用为 0.3%,它会略有增加。下面是一个示例。 请注意,为了简单起见,表格中的数字仅保留了小数点后三位,我们忽略了 0.3% 交易费,因此数字并不准确。事件reserve0reserve1reserve0 * reserve1平均汇率 (token1 / token0)初始设置1,000.0001,000.0001,000,000交易者 A 用 50 个 token0 兑换 47.619 个 token11,050.000952.3811,000,0000.952交易者 B 用 10 个 token0 兑换 8.984 个 token11,060.000943.3961,000,0000.898交易者 C 用 40 个 token0 兑换 34.305 个 token11,100.000909.0901,000,0000.858交易者 D 用 100 个 token1 兑换 109.01 个 token0990.9901,009.0901,000,0000.917交易者 E 用 10 个 token0 兑换 10.079 个 token11,000.990999.0101,000,0001.008由于交易者提供了更多 token0,token1 的相对价值增加了,反之亦然,这取决于供求。锁定1 uint private unlocked = 1; 复制有一类基于重入攻击(opens in a new tab)的安全漏洞。 Uniswap 需要转让不同数值的 ERC-20 代币,这意味着调用的 ERC-20 合约可能会导致调用合约的 Uniswap 市场遭受攻击。 通过在合约中使用 unlocked 变量,我们可以防止函数在运行时被调用(同一笔交易中)。1 modifier lock() { 复制此函数是一个修改器(opens in a new tab),它对正常函数进行包装数,以便以某种方式改变其行为。1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0; 复制如果 unlocked 变量值为 1,将其设置为 0。 如果已经是 0,则撤销调用,返回失败。1 _; 复制在修饰符中,_; 是原始函数调用(含所有参数)。 此处,这意味着仅在 unlocked 变量值为 1 时调用函数,该函数调用才有效;而当函数运行时,unlocked 值为 0。1 unlocked = 1;2 } 复制当主函数返回后,释放锁定。其他 函数1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 } 复制此函数返回给调用者当前的兑换状态。 请注意,Solidity 函数可以返回多个值(opens in a new tab)。1 function _safeTransfer(address token, address to, uint value) private {2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); 复制此内部函数可以从交易所转账一定数额的 ERC20 代币给其他帐户。 SELECTOR 指定我们调用的函数是 transfer(address,uint)(参见上面的定义)。为了避免必须为代币函数导入接口,我们需要使用其中一个应用程序二进制接口函数(opens in a new tab)来“手动”创建调用。1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 } 复制ERC-20 的转移调用有两种方式可能失败:回滚 如果对外部合约的调用回滚,则布尔返回值为 false正常结束但报告失败。 在这种情况下,返回值的缓冲为非零长度,将其解码为布尔值时,其值为 false一旦出现这两种情况,转移调用就会回退。事件1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); 复制当流动资金提供者存入流动资金 (Mint) 或提取流动资金 (Burn) 时,会发出这两个事件。 在这两种情况下,存入或提取的 token0 和 token1 金额是事件的一部分,以及调用合约的帐户身份 (Sender) 也是事件的一部分。 在提取资金时,事件中还包括获得代币的目的地址 (to),这个地址可能与发送人不同。1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 ); 复制当交易者用一种代币交换另一种代币时,会激发此事件。 同样,代币发送者和兑换后代币的存入目的帐户可能不一样。 每种代币都可以发送到交易所,或者从交易所接收。1 event Sync(uint112 reserve0, uint112 reserve1); 复制最后,无论出于何种原因,每次存入或提取代币时都会触发 Sync 事件,以提供最新的储备金信息(从而提供汇率)。设置函数这些函数应在建立新的配对交易时调用。1 constructor() public {2 factory = msg.sender;3 } 复制构造函数确保我们能够跟踪产生配对的工厂合约的地址。 initialize 函数和工厂交易费(如果有)需要此信息1 // called once by the factory at time of deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 } 复制这个函数允许工厂(而且只允许工厂)指定配对中进行兑换的两种 ERC-20 代币。内部更新函数_update1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { 复制每次存入或提取代币时,会调用此函数。1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); 复制如果 balance0 或 balance1 (uint256) 大于 uint112(-1) (=2^112-1)(因此当转换为 uint112 时会溢出并返回 0),拒绝继续执行 _update 以防止溢出。 一般的代币可以细分成 10^18 个单元,这意味在每个交易所,每种代币的限额为 5.1*10^15 个。 迄今为止,这并不是一个问题。1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { 复制如果流逝的时间值不是零,这意味着本交易是此区块上的第一笔兑换交易。 在这种情况下,我们需要更新累积成本值。1 // * never overflows, and + overflow is desired2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 } 复制每个累积成本值都用最新成本值(另一个代币的储备金额/本代币的储备金额)与以秒为单位的流逝时间的乘积加以更新。 要获得平均兑换价格,需要读取两个时间点的累积价格并除以两个时间点之间的时间差。 例如,假设下面这些事件序列:事件reserve0reserve1时间戳边际汇率 (reserve1 / reserve0)price0CumulativeLast初始设置1,000.0001,000.0005,0001.0000交易者 A 存入 50 个代币 0 获得 47.619 个代币 11,050.000952.3815,0200.90720交易者 B 存入 10 个代币 0 获得 8.984 个代币 11,060.000943.3965,0300.8920+10*0.907 = 29.07交易者 C 存入 40 个代币 0 获得 34.305 个代币 11,100.000909.0905,1000.82629.07+70*0.890 = 91.37交易者 D 存入 100 个代币 0 获得 109.01 个代币 1990.9901,009.0905,1101.01891.37+10*0.826 = 99.63交易者 E 存入 10 个代币 0 获得 10.079 个代币 11,000.990999.0105,1500.99899.63+40*1.1018 = 143.702比如说我们想要计算时间戳 5,030 到 5,150 之间代币 0 的平均价格。 price0Cumulative 的差值为 143.702-29.07=114.632。 此为两分钟(120 秒)间的平均值。 因此,平均价格为 114.632/120 = 0.955。此价格计算是我们需要知道原有资金储备规模的原因。1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 } 复制最后,更新全局变量并发布一个 Sync 事件。_mintFee1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { 复制在 Uniswap 2.0 的合约中规定交易者为使用兑换市场支付 0.30% 的费用。 这笔费用的大部分(交易的 0.25%)支付给流动性提供者。 余下的 0.05% 可以支付给流动性提供者或支付给工厂指定的地址作为协议费,用于支付 Uniswap 团队的开发费用。为了减少计算次数(因此减少燃料费用),仅在向资金池中增加或减少流动性时才计算该费用,而不是在每次兑换交易时都计算。1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0); 复制读取工厂的费用支付地址。 如果返回值为零,则代表没有协议费,也不需要计算这笔费用。1 uint _kLast = kLast; // gas savings 复制kLast 状态变量位于内存中,所以在合约的不同调用中都有一个值。 虽然易失性内存每次在函数调用合约结束后都会清空,但由于访问存储的费用比访问内存高得多,所以我们使用内部变量,以降低燃料费用。1 if (feeOn) {2 if (_kLast != 0) { 复制流动资金提供者仅仅因为提供流动性代币而得到所属的费用。 但是协议费用要求铸造新的流动性代币,并提供给 feeTo 地址。1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) { 复制如果有新的流动性变化需要收取协议费。 你可以在本文后面部分看到平方根函数。1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator; 复制这种复杂的费用计算方法在白皮书(opens in a new tab)第 5 页中作了解释。 从计算 kLast 的时间到当前为止,流动性没有增加或减少(因为每次计算都是在流动性增加或减少并发生实际变化之前进行),所以 reserve0 * reserve1 的任何变化一定是从交易费用中产生(如果没有交易费,reserve0 * reserve1 值为常量)。1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 } 复制使用 UniswapV2ERC20._mint 函数产生更多的流动池代币并发送到 feeTo 地址。1 } else if (_kLast != 0) {2 kLast = 0;3 }4 } 复制如果不需收费则将 klast 设为 0(如果 klast 不为 0)。 编写该合约时,有一个燃料返还功能(opens in a new tab),用于鼓励合约将其不需要的存储释放,从而减少以太坊上状态的整体存储大小。 此段代码在可行时返还。外部可访问函数请注意,虽然任何交易或合约都可以调用这些函数,但这些函数在设计上是从外围合约调用。 如果直接调用,您无法欺骗币对交易所,但可能因为错误而丢失价值。铸币1 // this low-level function should be called from a contract which performs important safety checks2 function mint(address to) external lock returns (uint liquidity) { 复制当流动资金提供者为资金池增加流动资金时,将会调用此函数。 它铸造额外的流动性代币作为奖励。 应从外围合约中调用该函数,在同一笔交易中增加流动性后外围合约就调用该函数(因此其他人都不能在合法所有者之前提交要求新增加流动性的交易)。1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings 复制这是 Solidity 函数中读取多个返回值的方式。 我们丢弃了最后返回的值区块时间戳,因为不需要它。1 uint balance0 = IERC20(token0).balanceOf(address(this));2 uint balance1 = IERC20(token1).balanceOf(address(this));3 uint amount0 = balance0.sub(_reserve0);4 uint amount1 = balance1.sub(_reserve1); 复制获取当前余额并查看每个代币类型中添加的数量。1 bool feeOn = _mintFee(_reserve0, _reserve1); 复制如果有协议费用的话,计算需要收取的费用,并相应地产生流动池代币。 因为输入 _mintFee 函数的参数是原有的储备金数值,相应费用仅依据费用导致的资金池变化来精确计算。1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens 复制如果这是第一笔存款,会创建数量为 MINIMUM_LIQUIDITY 的代币并将它们发送到地址 0 进行锁定。 这些代币永远无法赎回,也就是说资金池永远不会完全变空(避免某些情况下出现分母为零错误)。 MINIMUM_LIQUIDITY 的值是 1000,因为考虑到大多数 ERC-20 细分成 1 个代币的 10^-18 个单位,而以太币则被分为 wei,为 1 个代币价值的 10^-15。 成本不高。在首次存入时,我们不知道两种代币的相对价值,所以假定两种代币都具有相同的价值,只需要两者数量的乘积并取一下平方根。我们可以相信这一点,因为提供同等价值、避免套利符合存款人的利益。 比方说,这两种代币的价值是相同的,但我们的存款人存入的 Token1 是 Token0 的四倍。 交易者可以利用币对交易所认为 Token0 的价值更高这种情况,减少其价值。事件reserve0reserve1reserve0 * reserve1流动池价值 (reserve0 + reserve1)初始设置83225640交易者存入 8 个 Token0 代币,获得 16 个 Token1 代币161625632正如您可以看到的,交易者额外获得了 8 个代币,这是由于流动池价值下降造成的,损害了拥有流动池的存款人。1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); 复制对于随后每次存入,我们已经知道两种资产之间的汇率。我们期望流动性提供者提供等值的两种代币。 如果他们没有,我们根据他们提供的较低价值代币来支付他们的流动池代币以做惩罚。无论是最初存入还是后续存入,流动性代币的数量均等于 reserve0*reserve1 变化的平方根,而流动性代币的价值不变(除非存入的资金为不等值的代币类型,那么就会分派“罚金”)。 下面是另一个示例,两种代币具有相同价值,进行了三次良性存入和一次不良存入(即只存入一种类型的代币,所以不会产生任何流动性代币)。事件reserve0reserve1reserve0 * reserve1流动池价值 (reserve0 + reserve1)存入资金而产生的流动池代币流动池代币总值每个流动池代币的值初始设置8.0008.0006416.000882.000每种代币存入 4 个12.00012.00014424.0004122.000每种代币存入 2 个14.00014.00019628.0002142.000不等值的存款18.00014.00025232.000014~2.286套利后~15.874~15.874252~31.748014~2.2671 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity); 复制使用 UniswapV2ERC20._mint 函数产生更多流动池代币并发送到正确的帐户地址。12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date4 emit Mint(msg.sender, amount0, amount1);5 } 复制更新相应的状态变量(reserve0、reserve1,必要时还包含 kLast)并激发相应事件。销毁1 // this low-level function should be called from a contract which performs important safety checks2 function burn(address to) external lock returns (uint amount0, uint amount1) { 复制当流动资金被提取且相应的流动池代币需要被销毁时,将调用此函数。 还需要从外围帐户调用。1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2 address _token0 = token0; // gas savings3 address _token1 = token1; // gas savings4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)]; 复制外围合约在调用函数之前,首先将要销毁的流动资金转到本合约中。 这样,我们知道有多少流动资金需要销毁,并可以确保它被销毁。1 bool feeOn = _mintFee(_reserve0, _reserve1);2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); 复制流动资金提供者获得等值数量的两种代币。 这样不会改变兑换汇率。1 _burn(address(this), liquidity);2 _safeTransfer(_token0, to, amount0);3 _safeTransfer(_token1, to, amount1);4 balance0 = IERC20(_token0).balanceOf(address(this));5 balance1 = IERC20(_token1).balanceOf(address(this));67 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date9 emit Burn(msg.sender, amount0, amount1, to);10 }11显示全部 复制burn 函数的其余部分是上述 mint 函数的镜像。兑换1 // this low-level function should be called from a contract which performs important safety checks2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { 复制此函数也应该从外围合约调用。1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errors 复制本地变量可以存储在内存中,或者如果变量数目不太多,直接存储进堆栈。 如果我们可以限制变量数量,那么建议使用堆栈以减少燃料消耗。 欲了解更多详情,请参阅以太坊黄皮书(以前的以太坊规范)(opens in a new tab)第 26 页上的“方程式 298”。1 address _token0 = token0;2 address _token1 = token1;3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens 复制这种转移应该是会成功的,因为在转移之前我们确信所有条件都得到满足。 在以太坊中这样操作是可以的,原因在于如果在后面的调用中条件没有得到满足,我们可以回滚操作和造成的所有变化。1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); 复制如果收到请求,则通知接收者要进行兑换。1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 } 复制获取当前余额。 外围合约在调用交换函数之前,需要向合约发送要兑换的代币。 这让合约可以方便检查它有没有受到欺骗,这是在核心合约中必须进行的检查(因为除外围合约之外的其他实体也可以调用该函数)。1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); 复制这是一项健全性检查,确保我们不会因兑换而损失代币。 在任何情况下兑换都不应减少 reserve0*reserve1。 这也是我们确保为兑换发送 0.3% 费用的方式;在对 K 值进行完整性检查之前,我们将两个余额乘以 1000 减去 3 倍的金额,这意味着在将其 K 值与当前准备金 K 值进行比较之前,从余额中扣除 0.3% (3/1000 = 0.003 = 0.3%)。1 }23 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 } 复制更新 reserve0 和 reserve1 的值,并在必要时更新价格累积值和时间戳并激发相应事件。同步或提取实际余额有可能与配对交易所认为的储备金余额没有同步。 没有合约的认同,就无法撤回代币,但存款却不同。 帐户可以将代币转移到交易所,而无需调用 mint 或 swap。在这种情况下,有两种解决办法:sync,将储备金更新为当前余额skim,撤回额外的金额。 请注意任何帐户都可以调用 skim 函数,因为无法知道是谁存入的代币。 此信息是在一个事件中发布的,但这些事件无法从区块链中访问。1 // force balances to match reserves2 function skim(address to) external lock {3 address _token0 = token0; // gas savings4 address _token1 = token1; // gas savings5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }891011 // force reserves to match balances12 function sync() external lock {13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);14 }15}显示全部 复制UniswapV2Factory.sol此合约(opens in a new tab)创建币对交易所。1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter; 复制这些状态变量是执行协议费用所必需的(请见白皮书(opens in a new tab)的第 5 页)。 feeTo 地址用于累积流动性代币以收取协议费,而 feeToSetter 地址可用于将 feeTo 更改为不同地址。1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs; 复制这些变量用以跟踪配对,即两种代币之间的兑换。第一个变量 getPair 是一个映射,它根据兑换的两种 ERC-20 代币来识别币对交易所合约。 ERC-20 代币通过实现它们的合约的地址来识别,因此关键字和值都是地址。 为了获取币对交易所地址,以便能够将 tokenA 兑换成 tokenB,可以使用 getPair [](或其他方式)。第二个变量 allPairs 是一个数组,其中包括该工厂创建的所有币对交易所的地址。 在以太坊中,无法迭代映射内容,或获取所有关键字的列表,所以,该变量是了解此工厂管理哪些交易所的唯一方式。注意:不能迭代所有映射关键字的原因是合约数据存储费用昂贵,所以我们越少用存储越好,且越少改变 越好。 可以创建支持迭代的映射(opens in a new tab),但它们需要额外存储关键字列表。 但在大多数应用程序中并不需要。1 event PairCreated(address indexed token0, address indexed token1, address pair, uint); 复制当新的配对交易创建时,将激发此事件。 它包括代币地址、币对交易所地址以及工厂管理的交易所总数。1 constructor(address _feeToSetter) public {2 feeToSetter = _feeToSetter;3 } 复制构造函数做的唯一事情是指定 feeToSetter。 工厂开始时没有费用,只有 feeSetter 可以改变这种情况。1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 } 复制此函数返回交易配对的数量。1 function createPair(address tokenA, address tokenB) external returns (address pair) { 复制这是工厂的主要函数,可以在两个 ERC-20 代币之间创建配对交易。 注意,任何人都可以调用此函数。 不需要 Uniswap 许可就能创建新的币对交易所。1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 复制我们希望新交易所的地址是可以确定的,这样就可以在链下提前计算(这对于二层网络交易来说比较有用)。 为此,无论收到代币地址的顺序如何,我们需要代币地址始终按顺序排列,因此我们在此处对它们排序。1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient 复制大流动资金池优于小流动资金池,因为其价格比较稳定。 我们不希望每一对代币有多个流动性池。 如果已经有一个交易所,则无需为相同代币对创建另一个交易所。1 bytes memory bytecode = type(UniswapV2Pair).creationCode; 复制要创建新合约,我们需要使用创建它的代码(包括构造函数和写入用于存储实际合约以太坊虚拟机字节码的代码)。 在 Solidity 语言中,通常只需使用 addr = new () 的格式语句,然后编译器就可以完成所有的工作,不过为了获取一个确定的合约地址,需要使用 CREATE2 操作码(opens in a new tab)。 在编写这个代码时,Solidity 还不支持操作码,因此需要手动获取该代码。 目前这已经不再是问题,因为 Solidity 现已支持 CREATE2(opens in a new tab)。1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));2 assembly {3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)4 } 复制当 Solidity 不支持操作码时,我们可以通过内联汇编(opens in a new tab)来调用。1 IUniswapV2Pair(pair).initialize(token0, token1); 复制调用 initialize 函数来告诉新兑换交易可以兑换哪两种代币。1 getPair[token0][token1] = pair;2 getPair[token1][token0] = pair; // populate mapping in the reverse direction3 allPairs.push(pair);4 emit PairCreated(token0, token1, pair, allPairs.length);5 } 复制在状态变量中保存新的配对信息,并激发一个事件来告知外界新的配对交易合约已生成。1 function setFeeTo(address _feeTo) external {2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');3 feeTo = _feeTo;4 }56 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}显示全部 复制这两个函数允许 feeSetter 管理费用接收人(如果有)并将 feeSetter 更改为新地址。UniswapV2ERC20.sol本合约(opens in a new tab)实现 ERC-20 流动性代币。 它与 OpenZeppelin ERC-20 合约相似,因此这里仅解释不同的部分,即 permit 的功能。以太坊上的交易需要消耗以太币 (ETH),相当于实际货币。 如果你有 ERC-20 代币但没有以太币,就无法发送交易,因而不能用代币做任何事情。 避免该问题的一个解决方案是元交易(opens in a new tab)。 代币的所有者签署一个交易,允许其他人从链上提取代币,并通过网络发送给接收人。 接收人拥有以太币,可以代表所有者提交许可。1 bytes32 public DOMAIN_SEPARATOR;2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 复制此哈希值是这种交易类型的标识(opens in a new tab)。 在这里,我们仅支持带有这些参数的 Permit。1 mapping(address => uint) public nonces; 复制接收人无法伪造数字签名。 但是,可以将同一笔交易发送两次(这是一种重放攻击(opens in a new tab))。 为防止发生这种情况,我们使用了随机数(opens in a new tab)。 如果新 Permit 的随机数不是上次使用的随机数加一,我们认为它无效。1 constructor() public {2 uint chainId;3 assembly {4 chainId := chainid5 } 复制这是获取链标识符(opens in a new tab)的代码。 它使用一种名为 Yul(opens in a new tab) 的以太坊虚拟机汇编语言。 请注意,在当前版本 Yul 中,必须使用 chainid(),而非 chainid。1 DOMAIN_SEPARATOR = keccak256(2 abi.encode(3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),4 keccak256(bytes(name)),5 keccak256(bytes('1')),6 chainId,7 address(this)8 )9 );10 }显示全部 复制计算 EIP-712 的域分隔符(opens in a new tab)。1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { 复制这是实现批准功能的函数。 它接收相关字段作为参数,并将三个标量值(v、r 和 s)作为签名(opens in a new tab)。1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); 复制截止日期后请勿接受交易。1 bytes32 digest = keccak256(2 abi.encodePacked(3 '\x19\x01',4 DOMAIN_SEPARATOR,5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))6 )7 ); 复制abi.encodePacked(...) 是我们预计将收到的信息。 我们知道随机数应该是什么,所以不需要将它作为一个参数。以太坊签名算法预计获得 256 位用于签名,所以我们使用 keccak256 哈希函数。1 address recoveredAddress = ecrecover(digest, v, r, s); 复制从摘要和签名中,我们可以用 ecrecover(opens in a new tab) 函数计算出签名的地址。1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');2 _approve(owner, spender, value);3 }4 复制如果一切正常,则将其视为 ERC-20 批准(opens in a new tab)。外围合约外围合约是用于 Uniswap 的 API(应用程序接口)。 它们可用于其他合约或去中心化应用程序进行的外部调用。 你可以直接调用核心合约但更为复杂,如果你出错,则可能会损失价值。 核心合约只包含确保它们不会遭受欺骗的测试,不会对其他调用者进行健全性检查。 它们在外围,因此可以根据需要进行更新。UniswapV2Router01.sol本合约(opens in a new tab)存在问题,不应该再使用(opens in a new tab)。 幸运的是,外围合约无状态,也不拥有任何资产,弃用外围合约比较容易。建议使用 UniswapV2Router02 来替代。UniswapV2Router02.sol在大多数情况下,您会通过该合约(opens in a new tab)使用 Uniswap。 有关使用说明,您可以在这里(opens in a new tab)找到。1pragma solidity =0.6.6;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';56import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';显示全部 复制其中大部分我们都曾遇到过,或相当明显。 一个例外是 IWETH.sol。 Uniswapv2 允许兑换任意一对 ERC-20 代币,但以太币 (ETH) 本身并不是 ERC-20 代币。 它早于该标准出现,并采用独特的机制转换。 为了在适用于 ERC-20 代币的合约中使用以太币,人们制定出包装以太币 (WETH)(opens in a new tab) 合约。 你发送以太币到该合约,它会为您铸造相同金额的包装以太币。 或者您可以销毁包装以太币,然后换回以太币。1contract UniswapV2Router02 is IUniswapV2Router02 {2 using SafeMath for uint;34 address public immutable override factory;5 address public immutable override WETH; 复制路由需要知道使用哪个工厂,以及对于需要包装以太币的交易,要使用什么包装以太币合约。 这些值是不可修改(opens in a new tab)的,意味着它们只能在构造函数中设置。 这使得用户相信没有人能够改变这些值,让它们指向有风险的合约。1 modifier ensure(uint deadline) {2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');3 _;4 } 复制此修改函数确保有时间限制的交易(如果可以,请在 Y 之前执行 X)不会在时限后发生。1 constructor(address _factory, address _WETH) public {2 factory = _factory;3 WETH = _WETH;4 } 复制构造函数仅用于设置不可变的状态变量。1 receive() external payable {2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract3 } 复制当我们将代币从包装以太币合约换回以太币时,需要调用此函数。 只有我们使用的包装以太币合约才有权完成此操作。增加流动资金这些函数添加代币进行配对交易,从而增大了流动资金池。12 // **** ADD LIQUIDITY ****3 function _addLiquidity( 复制此函数用于计算应存入币对交易所的 A 代币和 B 代币的金额。1 address tokenA,2 address tokenB, 复制这些是 ERC-20 代币合约的地址。1 uint amountADesired,2 uint amountBDesired, 复制这些是流动资金提供者想要存入的代币数额。 它们也是要存入的代币 A 和 B 的最大金额。1 uint amountAMin,2 uint amountBMin 复制这些是可接受的最低存款数额。 如果在达到最小金额或更高金额时交易无法完成,则会回滚交易。 如果不想要此功能,将它们设定为零即可。流动性提供者指定最低金额,往往是因为他们想要限制交易汇率,使其在与当前汇率接近。 如果汇率波动太大,可能意味着基础价值可能发生改变,流动性提供者需要自己决定采取什么措施。例如,想象汇率是一比一时,流动性提供者指定了以下值:参数值amountADesired1000amountBDesired1000amountAMin900amountBMin800只要汇率保持在 0.9 至 1.25 之间,交易就会进行。 如果汇率超出这个范围,交易将被取消。这种预防措施的原因是交易不是即时的,你提交交易,最后验证者才会将它们包含在一个区块中(除非你的燃料价格非常低,在这种情况下你需要提交另一个具有相同随机数的交易以及更高的燃料价格来覆盖它)。 在提交交易和交易包含到区块中之间发生的事情是无法控制的。1 ) internal virtual returns (uint amountA, uint amountB) { 复制该函数返回流动性提供者应存入的金额,存入该金额是为了让比率等于当前储备金之间的比率。1 // create the pair if it doesn't exist yet2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);4 } 复制如果还没有此代币对的兑换交易,则创建一个。1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); 复制获取配对中的当前储备金。1 if (reserveA == 0 && reserveB == 0) {2 (amountA, amountB) = (amountADesired, amountBDesired); 复制如果当前储备金为空,那么这是一笔新的配对交易。 存入的金额应与流动性提供者想要提供的金额完全相同。1 } else {2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); 复制如果我们需要知道这些金额是多少,可以使用此函数(opens in a new tab)获得最佳金额。 我们想要与当前储备相同的比率。1 if (amountBOptimal <= amountBDesired) {2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 (amountA, amountB) = (amountADesired, amountBOptimal); 复制如果 amountBOptimal 小于流动性提供者想要存入的金额,意味着代币 B 目前比流动性存款人所认为的价值更高,所以需要更少的金额。1 } else {2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);3 assert(amountAOptimal <= amountADesired);4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');5 (amountA, amountB) = (amountAOptimal, amountBDesired); 复制如果 B 代币的最佳金额大于想要存入的 B 代币金额,意味着代币 B 目前比流动性存款人所认为的价值更低,所以需要更多的金额。 然而,需要存入的金额是最大值,意味着我们无法存入更多金额的 B 代币。 可以选择的另一种方法是,我们计算所需 B 代币数额对应的最佳 A 代币数额。把数值汇总起来,我们就会得到这张图表。 假定你正在试图存入 1000 个 A 代币(蓝线)和 1000 个 B 代币(红线)。 X 轴是汇率,A/B。 如果 x=1,两种代币价值相等,每种代币各存入 1000 个。 如果 x=2,A 的价值是 B 的两倍(每个 A 代币可换两个 B 代币),因此你存入 1000 个 B 代币,但只能存入 500 个 A 代币。 如果是 x=0.5,情况就会逆转,即可存 1000 个 A 代币或 500 个 B 代币。可以将流动资金直接存入核心合约(使用 UniswapV2Pair::mint(opens in a new tab)),但核心合约只是检查自己没有遭受欺骗。因此,如果汇率在提交交易至执行交易之间发生变化,您将面临损失资金价值的风险。 如果使用外围合约,它会计算你应该存入的金额并会立即存入,所以汇率不会改变,你不会有任何损失。1 function addLiquidity(2 address tokenA,3 address tokenB,4 uint amountADesired,5 uint amountBDesired,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadline显示全部 复制此函数可以在交易中调用,用于存入流动资金。 大多数参数与上述 _addLiquidity 中相同,但有两个例外:. to 是会获取新流动池代币的地址,这些代币铸造用于显示流动资金提供者在池中所占比率 deadline 是交易的时间限制1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 复制我们计算实际存入的金额,然后找到流动资金池的帐户地址。 为了节省燃料,我们不是通过询问工厂执行此操作,而是使用库函数 pairFor(参见如下程序库)1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); 复制将正确数额的代币从用户帐户转到配对交易。1 liquidity = IUniswapV2Pair(pair).mint(to);2 } 复制反过来,将流动资金池的部分所有权赋予 to 地址的流动性代币。 核心合约的 mint 函数查看合约有多少额外代币(与上次流动性发生变化时合约持有的金额比较),并相应地铸造流动性代币。1 function addLiquidityETH(2 address token,3 uint amountTokenDesired, 复制当流动资金提供者想要向代币/以太币配对交易提供流动资金时,存在一些差别。 合约为流动性提供者处理以太币包装。 用户不需要指定想要存入多少以太币,因为用户直接通过交易发送以太币(金额在 msg.value 中)。1 uint amountTokenMin,2 uint amountETHMin,3 address to,4 uint deadline5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {6 (amountToken, amountETH) = _addLiquidity(7 token,8 WETH,9 amountTokenDesired,10 msg.value,11 amountTokenMin,12 amountETHMin13 );14 address pair = UniswapV2Library.pairFor(factory, token, WETH);15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);16 IWETH(WETH).deposit{value: amountETH}();17 assert(IWETH(WETH).transfer(pair, amountETH));显示全部 复制为了将以太币存入合约,首先将其包装成包装以太币,然后将包装以太币转入配对。 请注意转账在 assert 中包装。 这意味着如果转账失败,此合约调用也会失败,因此包装不会真正发生。1 liquidity = IUniswapV2Pair(pair).mint(to);2 // refund dust eth, if any3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);4 } 复制用户已经向我们发送了以太币,因此,如果还有任何额外以太币剩余(因为另一种代币比用户所认为的价值更低),我们需要发起退款。撤回流动资金下面的函数将撤回流动资金并还给流动资金提供者。1 // **** REMOVE LIQUIDITY ****2 function removeLiquidity(3 address tokenA,4 address tokenB,5 uint liquidity,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {显示全部 复制最简单的流动资金撤回案例。 对于每种代币,都有一个流动性提供者同意接受的最低金额,必须在截止时间之前完成。1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); 复制核心合约的 burn 函数处理返还给用户的代币。1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); 复制某个函数返回多个值时,如果我们只对其中部分值感兴趣,以下便是我们只获取那些值的方式。 从消耗燃料的角度来说,这样比读取那些从来不用的值更加经济。1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); 复制将按从核心合约返回代币的路径(低位代币地址优先)调整为用户期望的方式(对应于 tokenA 和 tokenB)。1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 } 复制可以首先进行转账,然后再核实转账是否合法,因为如果不合法,我们可以回滚所有的状态更改。1 function removeLiquidityETH(2 address token,3 uint liquidity,4 uint amountTokenMin,5 uint amountETHMin,6 address to,7 uint deadline8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {9 (amountToken, amountETH) = removeLiquidity(10 token,11 WETH,12 liquidity,13 amountTokenMin,14 amountETHMin,15 address(this),16 deadline17 );18 TransferHelper.safeTransfer(token, to, amountToken);19 IWETH(WETH).withdraw(amountETH);20 TransferHelper.safeTransferETH(to, amountETH);21 }显示全部 复制撤回以太币流动性的方式几乎是一样的,区别在于我们首先会收到包装以太币代币,然后将它们兑换成以太币并退还给流动性提供者。1 function removeLiquidityWithPermit(2 address tokenA,3 address tokenB,4 uint liquidity,5 uint amountAMin,6 uint amountBMin,7 address to,8 uint deadline,9 bool approveMax, uint8 v, bytes32 r, bytes32 s10 ) external virtual override returns (uint amountA, uint amountB) {11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);12 uint value = approveMax ? uint(-1) : liquidity;13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);15 }161718 function removeLiquidityETHWithPermit(19 address token,20 uint liquidity,21 uint amountTokenMin,22 uint amountETHMin,23 address to,24 uint deadline,25 bool approveMax, uint8 v, bytes32 r, bytes32 s26 ) external virtual override returns (uint amountToken, uint amountETH) {27 address pair = UniswapV2Library.pairFor(factory, token, WETH);28 uint value = approveMax ? uint(-1) : liquidity;29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);31 }显示全部 复制这些函数转发元交易,通过许可证机制使没有以太币的用户能够从资金池中提取资金。12 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****3 function removeLiquidityETHSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountETH) {11 (, amountETH) = removeLiquidity(12 token,13 WETH,14 liquidity,15 amountTokenMin,16 amountETHMin,17 address(this),18 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }24显示全部 复制此函数可以用于在传输或存储时收取费用的代币。 当代币有这类费用时,我们无法依靠 removeLiquidity 函数来告诉我们可以撤回多少代币。因此,我们需要先提取然后查询余额。123 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline,10 bool approveMax, uint8 v, bytes32 r, bytes32 s11 ) external virtual override returns (uint amountETH) {12 address pair = UniswapV2Library.pairFor(factory, token, WETH);13 uint value = approveMax ? uint(-1) : liquidity;14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(16 token, liquidity, amountTokenMin, amountETHMin, to, deadline17 );18 }显示全部 复制最后这个函数将存储费用计入元交易。交易1 // **** SWAP ****2 // requires the initial amount to have already been sent to the first pair3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { 复制公开给交易者的函数可以调用此函数以执行内部处理。1 for (uint i; i < path.length - 1; i++) { 复制在撰写此教程时,已有 388,160 个 ERC-20 代币(opens in a new tab)。 如果每个代币对都有币对交易所,币对交易所将超过 1500 亿个。 目前,整个链上的帐户数量仅为该数量的 0.1%(opens in a new tab)。 实际上,兑换函数支持路径这一概念。 交易者可以将 A 代币兑换成 B、B 代币兑换成 C、C 代币兑换成 D,因此不需要直接的 A-D 币对交易所。这些市场上的价格往往是同步的,因为当价格不同步时,就会为套利创造机会。 设想一下,例如有三种代币 A、B 和 C。有三个币对交易所,每对代币一个。初始情况交易者出售 24.695 A 代币,获得 25.305 B 代币。交易者卖出 24.695 个 B 代币得到 25.305 个 C 代币,大约获得 0.61 个 B 代币的利润。随后,该交易者卖出 24.695 个 C 代币得到 25.305 个 A 代币,大约获得 0.61 个 C 代币的利润。 该交易者还多出了 0.61 个 A 代币(交易者最终拥有的 25.305 个 A 代币,减去原始投资 24.695 个 A 代币)。步骤A-B 兑换B-C 兑换A-C 兑换1A:1000 B:1050 A/B=1.05B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.052A:1024.695 B:1024.695 A/B=1B:1000 C:1050 B/C=1.05A:1050 C:1000 C/A=1.053A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1050 C:1000 C/A=1.054A:1024.695 B:1024.695 A/B=1B:1024.695 C:1024.695 B/C=1A:1024.695 C:1024.695 C/A=11 (address input, address output) = (path[i], path[i + 1]);2 (address token0,) = UniswapV2Library.sortTokens(input, output);3 uint amountOut = amounts[i + 1]; 复制获取我们当前处理的配对,排序后(以便与配对一起使用)获得预期的输出金额。1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); 复制获得预期的金额后,按配对交易所需方式排序。1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; 复制这是最后一次兑换吗? 如果是,将收到用于交易的代币发送到目的地址。 如果不是,则将代币发送到下一个币对交易所。12 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(3 amount0Out, amount1Out, to, new bytes(0)4 );5 }6 } 复制真正调用配对交易来兑换代币。 我们不需要回调函数来了解交易信息,因此没有在该字段中发送任何字节。1 function swapExactTokensForTokens( 复制交易者直接使用此函数来兑换代币。1 uint amountIn,2 uint amountOutMin,3 address[] calldata path, 复制此参数包含 ERC-20 合约的地址。 如上文所述,此参数是一个数组,因为可能需要通过多个币对交易所将现有资产变为想要的资产。Solidity 中的函数参数可以存入 memory 或者 calldata。 如果此函数是合约的入口点,在由用户(通过交易)直接调用或从另一个合约调用时,那么参数的值可以直接从调用数据中获取。 如果函数是内部调用,如上述 _swap 函数,则参数必须存储在 memory 中。 从所调用合约的角度来看,calldata 为只读变量。对于标量类型,如 uint 或 address,编译器可以为我们处理存储选择,但对于数组,由于它们需要更多的存储空间也消耗更多的燃料,我们需要指定要使用的存储类型。1 address to,2 uint deadline3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) { 复制返回值总是返回内存中。1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 复制计算每次兑换时要购买的代币金额。 如果金额低于交易者愿意接受的最低金额,则回滚该交易。1 TransferHelper.safeTransferFrom(2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]3 );4 _swap(amounts, path, to);5 } 复制最后,将初始的 ERC-20 代币转到第一个配对交易的帐户中,然后调用 _swap。 所有这些都发生在同一笔交易中,因此币对交易所知道任何意料之外的代币都是此次转账的一部分。1 function swapTokensForExactTokens(2 uint amountOut,3 uint amountInMax,4 address[] calldata path,5 address to,6 uint deadline7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');10 TransferHelper.safeTransferFrom(11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]12 );13 _swap(amounts, path, to);14 }显示全部 复制前一个函数 swapTokensForTokens,使交易者可以指定自己愿意提供的输入代币的准确数量和愿意接受的输出代币的最低数量。 此函数可以撤销兑换,使交易者能够指定想要的输出代币数量以及愿意支付的输入代币最大数量。在这两种情况下,交易者必须首先给予此外围合约一定的额度,用于转账。1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)2 external3 virtual4 override5 payable6 ensure(deadline)7 returns (uint[] memory amounts)8 {9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');12 IWETH(WETH).deposit{value: amounts[0]}();13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));14 _swap(amounts, path, to);15 }161718 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)19 external20 virtual21 override22 ensure(deadline)23 returns (uint[] memory amounts)24 {25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');28 TransferHelper.safeTransferFrom(29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]30 );31 _swap(amounts, path, address(this));32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);34 }35363738 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)39 external40 virtual41 override42 ensure(deadline)43 returns (uint[] memory amounts)44 {45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');48 TransferHelper.safeTransferFrom(49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]50 );51 _swap(amounts, path, address(this));52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);54 }555657 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)58 external59 virtual60 override61 payable62 ensure(deadline)63 returns (uint[] memory amounts)64 {65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');68 IWETH(WETH).deposit{value: amounts[0]}();69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));70 _swap(amounts, path, to);71 // refund dust eth, if any72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);73 }显示全部 复制这四种转换方式都涉及到以太币和代币之间的交易。 唯一不同的是,我们要么从交易者处收到以太币,并使用以太币铸造包装以太币,要么从路径上的最后一个交易所收到包装以太币并销毁,然后将产生的以太币再发送给交易者。1 // **** SWAP (supporting fee-on-transfer tokens) ****2 // requires the initial amount to have already been sent to the first pair3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { 复制此内部函数用于兑换有转账或存储费用的代币,以解决(此问题(opens in a new tab))。1 for (uint i; i < path.length - 1; i++) {2 (address input, address output) = (path[i], path[i + 1]);3 (address token0,) = UniswapV2Library.sortTokens(input, output);4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));5 uint amountInput;6 uint amountOutput;7 { // scope to avoid stack too deep errors8 (uint reserve0, uint reserve1,) = pair.getReserves();9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);显示全部 复制由于有转账费用,我们不能依靠 getAmountsOut 函数来告诉我们每次转账完成后的金额(调用原来的 _swap 函数之前可以这样做)。 相反,我们必须先完成转账然后再查看我们获得的代币数量。注意:理论上我们可以使用此函数而非 _swap,但在某些情况下(例如,如果因为在最后无法满足所需最低金额而导致转账回滚),最终会消耗更多燃料。 有转账费用的代币很少见,所以,尽管我们需要接纳它们,但不需要让所有的兑换都假定至少需要兑换一种需要收取转账费用的代币。1 }2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;4 pair.swap(amount0Out, amount1Out, to, new bytes(0));5 }6 }789 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint deadline15 ) external virtual override ensure(deadline) {16 TransferHelper.safeTransferFrom(17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn18 );19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);20 _swapSupportingFeeOnTransferTokens(path, to);21 require(22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'24 );25 }262728 function swapExactETHForTokensSupportingFeeOnTransferTokens(29 uint amountOutMin,30 address[] calldata path,31 address to,32 uint deadline33 )34 external35 virtual36 override37 payable38 ensure(deadline)39 {40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');41 uint amountIn = msg.value;42 IWETH(WETH).deposit{value: amountIn}();43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);45 _swapSupportingFeeOnTransferTokens(path, to);46 require(47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'49 );50 }515253 function swapExactTokensForETHSupportingFeeOnTransferTokens(54 uint amountIn,55 uint amountOutMin,56 address[] calldata path,57 address to,58 uint deadline59 )60 external61 virtual62 override63 ensure(deadline)64 {65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');66 TransferHelper.safeTransferFrom(67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn68 );69 _swapSupportingFeeOnTransferTokens(path, address(this));70 uint amountOut = IERC20(WETH).balanceOf(address(this));71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');72 IWETH(WETH).withdraw(amountOut);73 TransferHelper.safeTransferETH(to, amountOut);74 }显示全部 复制这些方式与用于普通代币的相同,区别在于它们调用的是_swapSupportingFeeOnTransferTokens。1 // **** LIBRARY FUNCTIONS ****2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {3 return UniswapV2Library.quote(amountA, reserveA, reserveB);4 }56 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }1516 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }2526 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }3536 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 returns (uint[] memory amounts)42 {43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);44 }45}显示全部 复制这些函数仅仅是调用 UniswapV2Library 函数的代理。UniswapV2Migrator.sol这个合约用于将交易从旧版 v1 迁移至 v2。 目前版本已经迁移,便不再相关。程序库SafeMath 库(opens in a new tab)是一个文档很完备的程序库,这里便无需赘述了。数学此库包含一些 Solidity 代码通常不需要的数学函数,因而它们不是 Solidity 语言的一部分。1pragma solidity =0.5.16;23// a library for performing various math operations45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // babylonian method (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)11 function sqrt(uint y) internal pure returns (uint z) {12 if (y > 3) {13 z = y;14 uint x = y / 2 + 1;显示全部 复制首先赋予 x 一个大于平方根的估值(这是我们需要把 1-3 当作特殊情况处理的原因)。1 while (x < z) {2 z = x;3 x = (y / x + x) / 2; 复制获取一个更接近的估值,即前一个估值与我们试图找到其方根值的数值的平均数除以前一个估值。 重复计算,直到新的估值不再低于现有估值。 欲了解更多详情,请参见此处(opens in a new tab)。1 }2 } else if (y != 0) {3 z = 1; 复制我们永远不需要零的平方根。 1、2 和 3 的平方根大致为 1(我们使用的是整数,所以忽略小数)。1 }2 }3} 复制定点小数 (UQ112x112)该库处理小数,这些小数通常不属于以太坊计算的一部分。 为此,它将数值编码x为 x*2^112。 这使我们能够使用原来的加法和减法操作码,无需更改。1pragma solidity =0.5.16;23// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))45// range: [0, 2**112 - 1]6// resolution: 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;显示全部 复制Q112 是 1 的编码。1 // encode a uint112 as a UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // never overflows4 } 复制因为 y 是uint112,所以最多可以是 2^112-1。 该数值还可以编码为 UQ112x112。1 // divide a UQ112x112 by a uint112, returning a UQ112x1122 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {3 z = x / uint224(y);4 }5} 复制如果我们需要两个 UQ112x112 值相除,结果不需要再乘以 2^112。 因此,我们为分母取一个整数。 我们需要使用类似的技巧来做乘法,但不需要将 UQ112x112 的值相乘。UniswapV2Library此库仅被外围合约使用1pragma solidity >=0.5.0;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // returns sorted token addresses, used to handle return values from pairs sorted in this order11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');15 }显示全部 复制按地址对这两个代币排序,所以我们将能够获得相应的配对交易地址。 这很有必要,否则就会出现两种可能性,一种是参数 A、B,而另一种是参数 B、A,这导致两次交易而非一次。1 // calculates the CREATE2 address for a pair without making any external calls2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {3 (address token0, address token1) = sortTokens(tokenA, tokenB);4 pair = address(uint(keccak256(abi.encodePacked(5 hex'ff',6 factory,7 keccak256(abi.encodePacked(token0, token1)),8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash9 ))));10 }显示全部 复制此函数计算两种代币的配对交易地址。 此合约使用 CREATE2 操作码(opens in a new tab)创建,如果我们知道它使用的参数,我们可以使用相同的算法计算地址。 这比查询工厂便宜得多,而且1 // fetches and sorts the reserves for a pair2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {3 (address token0,) = sortTokens(tokenA, tokenB);4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);6 } 复制此函数返回配对交易所拥有的两种代币的储备金。 请注意,它可以任意顺序接收代币并将代币排序,以便内部使用。1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 amountB = amountA.mul(reserveB) / reserveA;6 } 复制如果不涉及交易费用的话,此函数将返回给您代币 A 兑换得到的代币 B。 此计算考虑到转账可能会改变汇率。1 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { 复制如果使用配对交易没有手续费,上述 quote 函数非常有效。 然而,如果有 0.3% 的手续费,您实际得到的金额就会低于此值。 此函数可以计算缴纳交易费用后的金额。12 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');4 uint amountInWithFee = amountIn.mul(997);5 uint numerator = amountInWithFee.mul(reserveOut);6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);7 amountOut = numerator / denominator;8 } 复制Solidity 本身不能进行小数计算,所以不能简单地将金额乘以 0.997。 作为替代方法,我们将分子乘以 997,分母乘以 1000,也能取得相同的效果。1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 uint numerator = reserveIn.mul(amountOut).mul(1000);6 uint denominator = reserveOut.sub(amountOut).mul(997);7 amountIn = (numerator / denominator).add(1);8 } 复制此函数大致完成相同的功能,但它会获取输出数额并提供输入代币的数量。12 // performs chained getAmountOut calculations on any number of pairs3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');5 amounts = new uint[](path.length);6 amounts[0] = amountIn;7 for (uint i; i < path.length - 1; i++) {8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);10 }11 }1213 // performs chained getAmountIn calculations on any number of pairs14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');16 amounts = new uint[](path.length);17 amounts[amounts.length - 1] = amountOut;18 for (uint i = path.length - 1; i > 0; i--) {19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);21 }22 }23}显示全部 复制在需要进行数次配对交易时,可以通过这两个函数获得相应数值。转账帮助此库(opens in a new tab)添加了围绕 ERC-20 和以太坊转账的成功检查,并以同样的方式处理回退和返回 false 值。1// SPDX-License-Identifier: GPL-3.0-or-later23pragma solidity >=0.6.0;45// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14显示全部 复制我们可以通过以下两种方式调用不同的合约:使用一个接口定义创建函数调用使用应用程序二进制接口 (ABI)(opens in a new tab)“手动”创建调用。 这是代码作者的决定。1 require(2 success && (data.length == 0 || abi.decode(data, (bool))),3 'TransferHelper::safeApprove: approve failed'4 );5 } 复制为了与之前的 ERC-20 标准创建的代币反向兼容,ERC-20 调用失败可能有两种情况:回退(在这种情况下 success 即是 false),或者调用成功但返回 false 值(在这种情况下有输出数据,将其解码为布尔值,会得到 false)。123 function safeTransfer(4 address token,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transfer(address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::safeTransfer: transfer failed'13 );14 }显示全部 复制此函数实现了 ERC-20 的转账功能(opens in a new tab),可使一个帐户花掉由不同帐户所提供的额度。12 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::transferFrom: transferFrom failed'13 );14 }显示全部 复制此函数实现了 ERC-20 的 transferFrom 功能(opens in a new tab),可使一个帐户花掉由不同帐户所提供的额度。12 function safeTransferETH(address to, uint256 value) internal {3 (bool success, ) = to.call{value: value}(new bytes(0));4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');5 }6} 复制此函数将以太币转至一个帐户。 任何对不同合约的调用都可以尝试发送以太币。 因为我们实际上不需要调用任何函数,就不需要在调用中发送任何数据。结论本篇文章较长,约有 50 页。 如果您已读到此处,恭喜您! 希望你现在已经了解编写真实应用程序(相对于短小的示例程序)时的考虑因素,并且能够更好地为自己的用例编写合约。现在去写点实用的东西吧,希望您能给我们惊喜。f上次修改时间: @finereganyue(opens in a new tab), Invalid DateTime查看贡献者本教程对你有帮助吗?是否编辑页面(opens in a new tab)在本页面介绍Uniswap 是做什么的?为什么选择 v2? 而不是 v3?核心合约与外围合约数据和控制流程兑换增加流动资金撤回流动资金核心合约UniswapV2Pair.solUniswapV2Factory.solUniswapV2ERC20.sol外围合约UniswapV2Router01.solUniswapV2Router02.solUniswapV2Migrator.sol程序库数学定点小数 (UQ112x112)UniswapV2Library转账帮助结论网站最后更新: 2024年3月13日(opens in a new tab)(opens in a new tab)(opens in a new tab)学习学习中心什么是以太坊?什么是以太币 (ETH)?以太坊钱包什么是 Web3?智能合约Gas fees运行节点以太坊安全和预防欺诈措施测试中心以太坊词汇表用法指南选择钱包获取以太币Dapps - 去中心化应用稳定币NFT - 非同质化代币DeFi - 去中心化金融DAO - 去中心化自治组织去中心化身份质押ETH二层网络构建构建者首页教程相关文档通过编码来学习设置本地环境资助基础主题用户体验/用户界面设计基础Enterprise - Mainnet EthereumEnterprise - Private Ethereum参与社区中心在线社区以太坊活动为 ethereum.org 做贡献翻译计划以太坊漏洞悬赏计划以太坊基金会以太坊基金会的博客(opens in a new tab)生态系统支持方案(opens in a new tab)Devcon(opens in a new tab)研究以太坊白皮书以太坊路线图安全性增强以太坊技术史开放研究以太坊改进提案 (Eip)以太坊治理关于我们以太坊品牌资产Code of conduct工作机会隐私政策使用条款缓存政策联系我们(opens in a new tab)本页面对你有帮

去中心化交易所:Uniswap v2白皮书中文版 - 知乎

去中心化交易所:Uniswap v2白皮书中文版 - 知乎切换模式写文章登录/注册去中心化交易所:Uniswap v2白皮书中文版Yeefea原文:Uniswap v2 Core作者:Hayden Adams - hayden@uniswap.orgNoah Zinsmeister - noah@uniswap.orgDan Robinson - dan@paradigm.xyz2020年3月Uniswap v2 核心摘要这篇技术白皮书解释了Uniswap v2核心合约背后的设计决策。内容覆盖了合约的新特性,包括任意ERC20代币的交易对,强化的价格预言机制(price oracle),允许交易者先接收资产并使用,随后在转账中支付的快速互换(flash swaps)功能,在未来可以开启的协议手续费(protocol fee)为了减少受攻击面,重新设计了合约的架构。本白皮书描述了Uniswap v2核心合约的运行机制,包括储存流动性提供者资金的交易对合约,和用于实例化交易对合约的工厂合约。1 介绍Uniswap v1是一个以太坊区块链上的智能合约系统,基于常数乘积公式实现了自动流动性协议。每个Uniswap v1交易对在资金池里储存两种资产,并位这两种资产提供流动性,维持这两种资产的乘积不减少。交易者支付30bp(译者注:0.3%)的交易手续费给流动性提供者。合约是不能升级的。Uniswap v2基于同样的公式实现,添加了几个特性。最重要的一个是支持ERC20代币/ERC20代币交易对,而不像v1只支持ERC20代币/ETH交易对。除此以外,它也强化了价格预言,在每个区块开始处累积两个资产的相对价格。这允许其他以太坊合约计算两个资产在任意时间区间上的TWAP价格。最后,它启用了快速互换(flash swaps)功能,用户可以自由地接收资产并把他们用在链上的其他地方,只要在转账的最后支付(或返还)即可。虽然合约通常来说不可升级,但是有一个私钥能够更新一个变量,打开链上5bp的手续费,这个变量一开始是关闭的,可以在未来打开,打开之后流动性提供者在每笔交易上赚取25bp,而不是30bp。Uniswap v2修复了一些v1中的小问题,也在实现中重新设计了架构,减少了Uniswap的受攻击面,通过把逻辑最小化放在核心合约(core contract)中,这个核心合约维持了流动性提供者的资金,从而使系统变得更容易升级。这篇论文描述了核心合约(core contract)的运行机制,以及用来实例化核心合约的工厂合约(factory contract)。实际上使用Uniswap v2需要通过一个“路由”合约(router contract),计算交易/存款数额并转移资金到交易对合约(pair contract)。译者注:合约代码:https://github.com/Uniswap/uniswap-v2-coreUniswap v1 白皮书:https://hackmd.io/@HaydenAdams/HJ9jLsfTz#%F0%9F%A6%84-Uniswap-Whitepaper2 新特性2.1 ERC20交易对Uniswap v1使用ETH作为过渡货币(bridge currency),每个交易对都包含ETH。这使得路由更简单(ABC和XYZ之间的每笔交易都通过ETH/ABC交易对和ETH/XYZ交易对进行),并且减少了流动性的分散。但是,这个规则给流动性提供者增加了可观的成本。所有流动资金提供者都有ETH的敞口,并且由于其他资产相对于ETH的价格变化而遭受无常损失(impermanent loss)。当两个资产ABC和XYZ相关时(例如,如果它们都是锚定美元的稳定币),则在Uniswap交易对ABC/XYZ上的流动性提供者遭受的无常损失会比ABC/ETH或XYZ/ETH交易对少。使用ETH作为强制性过渡货币也会给交易者带来成本。交易者必须支付的费用是直接购买ABC/XYZ交易对的费用的两倍,因此他们遭受两次滑点。 Uniswap v2允许流动性提供商为任意两个ERC20创建对合约。任意ERC20之间的交易对数量的激增可能会使寻找交易特定货币对的最佳路径变得更加困难,但是路由可以在更高的层面上处理(在链下处理或通过链上路由器或聚合器)。2.2 价格预言(Price oracle)Uniswap提供的t时刻的边际价格(marginal price,不包括手续费)可以用资产a的储备除以资产b的储备来计算。p_t=\frac{r^{a}_{t}}{r^{b}_{t}}\tag{1} 如果这个价格偏离(超出手续费足够的数额),套利者会和Uniswap进行交易使价格回归正常,所以Uniswap提供的价格趋向于追踪资产在相关市场的价格。这意味着它可以被用作一个近似的价格预言。但是用Uniswap v1作为链上价格预言是不安全的,因为它非常容易被操纵。假设一个其他的合约用当前的ETH-DAI的价格交割某个衍生品,操纵价格的攻击者会从ETH-DAI交易对买入ETH,在衍生品上触发结算(引起失真的结算价),然后再把ETH卖给交易对,用真实价格反向交易。这甚至可以用一个原子的转账来完成,或者通过一个控制区块内交易(transaction)顺序的矿工。Uniswap v2改进了预言的功能,通过测算和记录每个区块第一笔交易之前的价格(也就是前一个区块最后的价格)。这个价格比一个区块内的价格更难被操纵。如果攻击者提交了一笔交易(transaction)尝试在区块末尾处操纵价格,其他的套利者可以提交另一个交易(transaction)立即进行反向交易。某个矿工(或有足够gas填满整个区块的攻击者)可以操纵区块末尾处的价格,除非他们可以挖出下一个区块,否则他们他们没有特殊的的套利优势。具体来说,Uniswap v2追踪每个和合约交互的区块开始处的价格的累加和,来累加价格。每个价格用距离上一个更新价格的区块的时间进行加权,根据区块时间戳。这意思是累加器的值在任意时间(更新后)的值等于合约历史上每秒的现货价格的和。a_t = \sum_{i=1}^{t}p_i \tag{2} 要计算从时间​ t1到t2​的时间加权平均价(译者注:TWAP价格),一个外部调用者可以检查​t1和t2时间的累加器的值,将后值减去前值,再除以期间经过的秒数。(注意,合约本身并不记录历史累加值,调用者必须在时间段开始处调用合约来读取和储存这个值。)p_{t_1,t_2} = \frac{\sum\nolimits_{i=t_1}^{t2}p_i}{t_2-t_1}=\frac{\sum\nolimits_{i=1}^{t_2}p_i-\sum\nolimits_{i=1}^{t_1}p_i}{t_2-t_1}=\frac{a_{t_2}-a_{t_1}}{t_2-t_1} \tag{3} 预言的用户可以选择这个区间的起始和结束时间。选择更长的区间可以让攻击者操纵价格的成本更高,虽然这会导致价格变化滞后。一个复杂之处:我们应该用资产B来计算资产A的价格还是用资产A来计算资产B的价格?虽然用B计算的A的现货价格总是用A计算的B的现货价格的倒数,但是在某一时间段内用B计算的A的均价不等于用A计算的B的均价的倒数。举个例子,如果USD/ETH价格在区块1中是100,在区块2中是300,USD/ETH的均价是200 USD/ETH,但是ETH/USD的均价是1/150 ETH/USD(译者注:他们的均价不是倒数关系)。因为合约无法知道用户想要用哪个资产作为账户单位,所以Uniswap v2会同时追踪两个价格。另一个复杂之处:有没有可能某个人发送资产给交易对合约,用来改变它的余额和边际价格,但又不和它交互,因此不会触发价格更新。如果合约简单的检查它自己的余额然后更新预言,攻击者可以在某一个区块中第一次调用合约之前向合约发送资产,从而操纵价格。如果上一次交易的区块是X秒以前,合约会在累加之前错误地把新价格乘以X,即使没有用户用那个价格进行过交易。为了防止这个问题,核心合约在每次交互后缓存它的资金储备,用缓存的资金储备更新价格预言而不用当前资金储备。除了保护价格预言被操纵,这个改动也启用3.2节中描述的合约重新架构。2.2.1 精度Solidity没有一等的非整型数的数据结构的支持,Uniswap v2用简单的二进制定点数格式编码和控制价格。具体来说,某一时间的价格存储为UQ112.112格式,意思是在小数点的任意一边都有112位精度,无符号。这些数字的范围是 [0,​2^{112}-1] ,精度是 \frac{1}{2^{112}} 。 选择UQ112.112格式是由于实用的原因,因为这些数可以被存在uint224中,在256位中剩余的32位空余。储备资金各自存在uint112中,剩余32位存储空间。这些空闲空间被用于之前描述的累加过程。具体来说,储备资金和时间戳存储在至少有一个交易的最近的区块中,mod 232(译者注:取余数)之后可以存进32位空间。另外,虽然任意时间的价格(UQ112.112数字)确保可以储存进224位中,但某段时间的累加值确保能存下。存储A/B和B/A累加价格空间尾部附加的32位用来存连续累加溢出的位。这样设计意味着价格预言只在每一个区块的第一次交易中增加了3次SSTORE操作(目前花费15000gas)。主要的缺点是32位不足以储存时间戳并确保不溢出。事实上32位Unix时间戳的溢出日期是2106年7月2日。为了确保系统在这个日期后以及每 2^{32}-1 秒的间隔能够继续运行,预言简单要求每个间隔至少检查一次价格(大约136年一次)。这是由于累加的核心函数(mod取余运算)是溢出安全的,意思是预言用溢出算法计算差值,跨越溢出区间的交易可以被合理计算。2.3 快速互换(Flash Swaps)Uniswap v1中,用户用XYZ买ABC需要发送XYZ到合约,然后才能收到ABC。如果用户需要ABC为了获取他们支付的XYZ,这种方式不方便的。举个例子,用户可能在其他合约中用ABC买XYZ,为了对冲Uniswap上的价格,或者他们可能在Maker或Compound上卖出抵押品平仓然后返还给Uniswap。Uniswap v2添加了一个新的特性,允许用户在支付前接收和使用资产,只要他们在同一个原子的转账中完成支付。swap函数调用一个可选的用户指定的回调合约,在这之间转出用户请求的代币并且强制确保不变。一旦回调完成,合约检查新余额并且确保满足不变(在经过支付手续费调整后)。如果合约没有足够的资金,它会回滚整个交易。用户也可以用同样的代币返还给Uniswap资金池而不完成互换。这高效地让任何人从Uniswap资金池中快速借取任何资产(Uniswap收取同样的千分之三的交易手续费)。2.4 协议手续费(Protocol fee)Uniswap v2包括0.05%协议手续费,可以打开或关闭,如果打开,手续费会被发送给工厂合约中指定的feeTo地址。初始时,feeTo没有被设定,不收手续费。一个预先指定的地址feeToSetter可以在Uniswap v2工厂合约上调用setFeeTo函数,设置feeTo地址。feeToSetter也可以自己调用setFeeToSetter修改feeToSetter地址。如果feeTo地址被设置,协议会收取5pb的手续费,从流动性提供者的30bp手续费中抽取1/6。交易者将在所有交易上支付0.3%手续费,83.3%的手续费给流动性提供者,16.6手续费给feeTo地址。总共收集的手续费可以用自从上次手续费收集以来(译者注:k是常数乘积,可以看v1白皮书)的增长来计算(也就是 )。以下公式给出了t1和t2之间的累加手续费占t2时间资金池中流动性的百分比:f_{1,2} = 1 - \frac{\sqrt{k_1}}{\sqrt{k_2}} \tag{4} 如果fee在时间t1前启用,feeTo地址应该获得1/6的t1到t2时间段内的累加手续费。因此,我们要铸造新的流动性代币给feeTo地址​ \phi \cdot f_{1,2} ,其中​ \phi = \frac{1}{6} 。我们要选择一个sm满足以下关系,其中s1是t1时刻的流通份额(outstanding shares)总量:\frac{s_m}{s_m+s_1}=\phi \cdot f_{1,2} \tag{5} 经过变换,将​替换为后,解得​s_m=\frac{\sqrt{k_2}-\sqrt{k_1}}{(\frac{1}{\phi}-1)\cdot\sqrt{k_2}+\sqrt{k_1}} \cdot s_1 \tag{6} 设​,得到以下公式s_m=\frac{\sqrt{k_2}-\sqrt{k_1}}{5\cdot\sqrt{k_2}+\sqrt{k_1}} \cdot s_1 \tag{7} 假设初始存款人存了100DAI和1ETH在交易对中,收到10份额。一段时间后(如果没有其他存款人参与)他们把钱转出,这时交易对有96DAI和1.5ETH,用上面得公式可以得出:s_m=\frac{\sqrt{1.5 \cdot 96}-\sqrt{1 \cdot 100}}{5\cdot\sqrt{1.5 \cdot 96}+\sqrt{1 \cdot 100}} \cdot 10 \approx 0.0286 \tag{8} 2.5 资金池份额的元交易(Meta transactions for pool shares)Uniswap v2交易对铸造得资金池份额原生支持元转账。这意思是用户可以用签名授权一个他们的资金池份额的转账,而不用从他们的地址进行链上转账。任何人都可以调用permit函数来以用户的名义发送签名,支付gas手续费并在同一个转账中执行其他操作。3 其他修改3.1 SolidityUniswap v1是用Vyper实现的,一种类似于Python的智能合约预言。Uniswap v2是用更广泛使用的Solidity实现的,因为它在开发的时候需要一些Vyper中不可用的功能(比如翻译非标准ERC20代币的返回值,通过内联汇编访问新的字节码chanid)。3.2 合约重新架构Uniswap v2在设计时优先考虑的一个方面是最小化受攻击表面积和核心交易对合约的复杂度,交易对合约储存了流动性提供者的资产。这个合约中的任何bug都可能是灾难性的,因为数百万美元的流动性可能会被窃取或冻结。在评估核心合约的安全性时,最重要的问题时它是否保护流动性提供者以防他们的资产被窃取或冻结。除了基本的允许资产在资金池中互换的功能,任何支持或保护交易者的特性,都可以在路由合约中处理。事实上,甚至一部分的互换功能也可以被抽出放到路由合约中。之前提到过,Uniswap v2储存有每种资产最近一次记录的余额(为了防止预言机制被操纵)。新的架构利用了这一点来进一步简化Uniswap v1合约。Uniswap v2中,卖方在调用互换函数之前发送资产到核心合约。然后合约通过比较上次记录的余额和最新余额来计算它收到了多少资产。这意味着核心合约对交易者转账的方式是不可知的。除了transferFrom以外,也可能是元交易(meta transaction),或未来任何其他的授权ERC20代币转账的机制。3.2.1 手续费调整Uniswap v1通过转入合约的代币数量,在保持常数乘积不变之前收取交易手续费。合约强制确保了以下公式:(x_{1} - 0.003 \cdot x_{in}) \cdot y_1 >= x_0 \cdot y_0 \tag{9} 使用flash swaps时,Uniswap v2引入了xin和yin可以同时为非零的可能性(当用户想要返还同样的资产,而不是互换时)。为了处理这种情况,同时正确地收取手续费,合约强制确保:( x_1 - 0.003 \cdot x_{in}) \cdot (y_1 - 0.003 \cdot y_{in}) >= x_0 \cdot y_0 \tag{10} 为了简化链上计算,两边同时乘以1000000,得到:(1000 \cdot x_1 - 3 \cdot x_{in}) \cdot (1000 \cdot y_1 - 3 \cdot y_{in}) >= 1000000 \cdot x_0 \cdot y_0 \tag{11} 3.2.2 sync()和skim()函数为了防止特殊实现用来修改交易对合约余额的代币,并且更优雅地处理总发行量超过​的代币,Uniswap v2有两个救援函数:sync()和skim()。sync()作用是在代币异步地减少交易对的余额时的恢复机制。这种情况下,交易会收到次优的汇率,如果没有流动性提供者作出反应,交易对会卡住。sync()的作用是设置合约的储备金为合约当前的余额,提供一种稍微优雅一点的恢复机制。skim()作用是在发送到代币的数量溢出了uint112大小的储备金存储空间时的恢复机制,否则交易会失败。skim()函数允许用户将提出交易对当前余额和​的差值大于0时,将差值提出到调用者的地址。3.3 处理非标准和非常规代币ERC20标准要求transfer()函数和transferFrom()函数返回布尔值表示调用的成功或失败。一些代币对这两个函数的实现没有返回值,比如泰达币(USDT)和币安币(BNB)。Uniswap v1将这种不标准的函数返回值转换成false,表示转账不成功,并且回滚交易,导致转账失败。Uniswap v2用不同的方式处理非标准的代币实现。具体来说,如果一次transfer()调用没有返回值,Uniswap v2把它转换为成功而非失败。这个改动不应该影响任何遵从ERC20协议的代币(因为那些代币中transfer()总是有返回值)。Uniswap v1此外假设调用transfer()和transferFrom()不能触发Uniswap交易对合约的重入调用。某些ERC20代币违反了这个假设,包括支持ERC777协议的"hooks"的代币。为了完全支持这些代币,Uniswap v2包含了一个“锁”,直接防止重入所有公共的修改状态的函数。这也保护防止了在快速互换(flash swaps)中从用户定义的回调函数重入,如2.3节所描述的那样。3.4 初始化流动性代币供给当新的流动性提供者向现有的Uniswap交易对中存代币时,计算铸造的流动性代币(译者注:流动性代币需要看Uniswap v1白皮书)数量基于现有的代币数量:s_{minted}=\frac{x_{deposited}}{x_{starting}} \cdot s_{starting} \tag{12} 如果是第一个存款人呢?在​ x_{starting} 为0的情况下,这个公式不能用。Uniswap v1设初始份额供给等于存入地ETH数量(以Wei计)。这有一定的合理价值,因为如果初始流动性是在合理价格存入的,那么1流动性份额(和ETH一样是18位小数精度代币)大约值2ETH。但是这意味着流动性资金池份额的价值依赖于初始存入的比例,这完全可能是任意值,尤其是因为没有任何比例可以反应真实价格的保证。另外,Uniswap v2支持任意交易对,有许多交易对根本不包含ETH。相反,Uniswap v2初始铸造份额等于存入代币数量的几何平均值:s_{minted}=\sqrt{x_{deposited}\cdot y_{deposited}} \tag{13} 这个公式确保了流动性资金池份额的价值在任意时间和在本质上和初始存入的比例无关。举个例子,假设当前1 ABC的价格是100XYZ。如果初始存款是2 ABC和200 XYZ(比例1:100),存款人会获得​份额。这些份额现在应该任然值2 ABC和200 XYZ,加上累加手续费。如果初始存款是2 ABC和800 XYZ(1:400比例),存款人会收到​资金池份额。以上公式确保了流动性资金池不会少于资金池中储备金额的几何平均值。但是,流动性资金池份额的价值随时间增长是可能的,通过累加交易手续费或者向流动性资金池“捐款”。理论上,这会导致一种极端情况,最小的流动性份额数量(1e-18份额)过于值钱,以至于无法位小流动性提供者提供任何流动性。为了减轻这种情况,Uniswap v2销毁第一次铸造的1e-15资金池份额,发送到全零地址而不是铸造者。这对任何代币交易对都是微不足道的。但是这显著地提高了上述攻击地成本。为了提高流动性资金池份额价值到100美元,攻击者需要捐献100000美元到资金池总,这会被作为流动性永久锁定。3.5 包装ETH以太坊原生资产ETH的转账接口和ERC20交互用的标准接口不同。结果,以太坊上许多其他的协议不支持ETH,而是使用了一种标准的“包装的ETH”代币,WETH。Uniswap v1是一个例外。因为每个Uniswap v1交易对包含了ETH作为其中一种资产,所以它可以直接处理ETH,而且更高效地使用gas。因为Uniswap v2支持任意ERC20交易对,它现在不再支持无包装的ETH。如果添加这个特性需要两倍的核心代码,并且产生ETH和WETH流动性分散的风险。原生ETH需要包装后才能在Uniswap v2上交易。3.6 确定交易对地址和Uniswap v1一样,Uniswap v2交易对也是通过单一的工厂合约进行实例化的。在Uniswap v1中,交易对合约用CREATE运算码进行创建,这意味着合约地址依赖于交易对创建的顺序。Uniswap v2使用以太坊新的CREATE2运算码来创建确定地址的交易对合约,这意味着可以在链下计算某个交易对的地址,不用查看以太坊区块链的状态。3.7 最大代币余额为了高效地实现预言机制,Uniswap v2只支持最高储备余额 2^{112}-1 ​。这个数字足以支持总发行量超过一千万亿的18位小数精度的代币。如果储备余额超过了​,任何swap函数调用都会失败(由于_update()函数中的检查逻辑)。要从这个状态中恢复,任何用户都可以调用skim()函数从流动性池中删除超出的资产。引用[1] Hayden Adams. 2018. url: https://hackmd.io/@477aQ9OrQTCbVR3fq1Qzxg/HJ9jLsfTz?type=view.[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:https://samczsun.com/taking-undercollateralized-loans-for-fun-and-for-profit/.[4] Fabian Vogelsteller and Vitalik Buterin. Nov. 2015. url: https://eips.ethereum.org/EIPS/eip-20.[5] Jordi Baylina Jacques Dafflon and Thomas Shababi. EIP 777: ERC777 Token Standard.Nov. 2017. url: https://eips.ethereum.org/EIPS/eip-777.[6] Radar. WTF is WETH? url: https://weth.io/.[7] Uniswap.info. Wrapped Ether (WETH). url: https://uniswap.info/token/0xc02aaa39b223fe8d0a0e5c4f27e[8] Vitalik Buterin. EIP 10144 声明这篇论文的目的仅作为一般信息。不构成买卖任何投资产品的投资建议或推荐或请求,也不应该被用来评估任何投资决策的业绩,不应该被用于会计、法律或税务建议或投资推荐。本文反映了作者当前的观点,不代表Paradigm和其附属机构,也不一定反映Paradigm、其附属机构以及隶属于Paradigm的个人的观点。本文反映的观点在没有另行更新的情况下可能发生变化。翻译不易,别忘了点赞收藏加关注哦^_^编辑于 2020-10-11 13:26区块链(Blockchain)区块链技术​赞同 58​​2 条评论​分享​喜欢​收藏​申请

Uniswap V3 介绍 - 知乎

Uniswap V3 介绍 - 知乎首发于加密财富切换模式写文章登录/注册Uniswap V3 介绍谈财富密码专为币圈人解答问题Uniswap v1于2018年11月推出,作为自动做市商(AMMs)概念的验证,这是一种任何人都可以将资产集中到共同做市策略中的交易类型。2020年5月,Uniswap v2推出了新的功能和优化,为AMM的采用以指数级增长奠定了基础。在推出不到一年的时间里,v2促成了超过1350亿美元的交易量,成为世界上最大的加密货币现货交易所之一。Uniswap现在是分散式融资的关键基础设施,使开发者、交易员和流动性提供者能够参与到一个安全和稳健的金融市场中。今天,我们很高兴地向大家介绍Uniswap v3的概况。我们的目标是在5月5日发布L1以太坊主网,不久后将在optimistic上部署L2。Uniswap v3介绍:集中的流动性,让个别有限合伙人能够对其资本配置的价格范围进行细微的控制。单个的头寸被聚合成一个单一的池,形成一条组合曲线供用户交易多重收费层次,允许有限合伙人在承担不同程度的风险时获得适当补偿这些特性使Uniswap v3成为有史以来最灵活和高效的AMM设计:有限合伙人可以提供高达4000倍资本效率的流动性相对于Uniswap v2,获得更高的资本回报资本效率为低滑移交易执行铺平了道路,这将超越集中式交易所和以稳定币为重点的amm有限合伙人可以显著增加其对首选资产的敞口,并降低其下行风险有限合伙人可以将一种资产出售给另一种资产,方法是在完全高于或低于市场价格的价格区间内增加流动性,接近沿平滑曲线执行的收费上限指令Uniswap的甲骨文集成起来要容易得多,成本也低得多。V3 oracle能够根据需求提供过去9天内任何时期的时间加权平均价格(TWAPs)。这消除了积分器对检查点历史值的需求。即使有了这些突破性的设计改进,以太坊主网v3交换的燃气成本也比v2略低。在乐观部署上进行的交易可能会便宜得多!继续阅读关于Uniswap v3的更多细节。要获得更深入的技术概述,请查看Uniswap v3核心白皮书《Uniswap v3核心智能合约》。集中的流动性在Uniswap v2中,流动性沿着x*y=k的价格曲线均匀分布,所有价格的资产保留在0到无穷之间。对于大多数资金池来说,这些流动性中的大部分从未被使用过。例如,v2 DAI/USDC组合在0.99美元至1.01美元之间的交易仅预留约0.50%的资金,而在这个价格区间内,有限合伙人预计交易量最大,因此获得的费用也最多。V2有限合伙人仅对其资本的一小部分收取费用,这可能无法适当补偿他们持有两种代币的大量库存所承担的价格风险(“临时损失”)。此外,由于流动性分散在所有价格区间,交易员经常受到高度的滑脱。在Uniswap v3中,有限合伙人可以将资金集中在定制的价格范围内,以期望的价格提供更多的流动性。这样一来,有限合伙人就能构建出反映自己偏好的个性化价格曲线。有限合伙人可以将任意数量的不同集中位置合并到一个单一的池中。例如,ETH/DAI池中的LP可以选择将100美元分配到1000 - 2000美元的价格区间,另外将50美元分配到1500 - 1750美元的价格区间。资本效率通过集中其流动性,有限合伙人可以在特定的价格范围内提供与v2相同的流动性深度,同时投入的资本风险要小得多。节省下来的资金可以在外部持有,投资于不同的资产,存放在DeFi的其他地方,或者用于在指定的价格范围内增加敞口以赚取更多的交易费用。让我们用一个例子来说明:Alice和Bob都想在Uniswap v3的ETH/DAI池中提供流动性。他们每人都有100万美元。目前ETH的价格是1500 DAI。Alice决定在整个价格区间内部署她的资本(就像她在Uniswap v2中所做的那样)。她存入50万DAI和333.33 ETH(总价值100万美元)。相反,Bob创建了一个集中仓位,只在1000至2250的价格区间内存入。他存了91,751 DAI和61.17 ETH,总共价值约183,500美元。剩下的816,500美元他自己留着,以自己喜欢的方式投资。虽然Alice的首付金额是Bob的5.44倍,但只要ETH/DAI价格保持在1000到2250之间,他们就能获得相同的费用。通过这样做,LP可以近似于任何自动做市商或主动订单簿的形状。鲍勃的定制头寸也对他的流动性起到了止损的作用。如果ETH的价格降至0美元,Alice和Bob的流动性都将完全以ETH计价。然而,鲍勃只损失了159,000美元,而爱丽丝损失了100万美元。鲍勃可以用他额外的816,500美元来对冲下跌风险,或者投资任何其他可能的策略。用户根据所有单个流动性曲线的组合流动性进行交易,而每个流动性提供者的成本不会增加。有限合伙人按一定价格区间收取的交易费用,按照他们在该区间贡献的流动性数量,按比例分摊。v3 LPs可以选择用与v2 LPs相同的资金提供更大的深度,而不是像v2 LPs一样提供更少的资金。这就需要承担更多的价格风险(“短期损失”),同时支持更多的交易,赚取更高的费用。在更稳定的资金池中,有限合伙人可能会在特别狭窄的范围内提供流动性。如果目前Uniswap v2 DAI/USDC的约2500万美元集中在v3的0.99 - 1.01之间,只要价格保持在这个范围内,它将提供与Uniswap v2的50亿美元相同的深度。如果2500万美元集中在0.999 - 1.001区间,它将提供与Uniswap v2中的500亿美元相同的深度。下面的工具计算集中流动性头寸(以当前价格为中心)相对于在整个价格曲线上配置资本的资本效率收益。在发行时,如果有限合伙人在0.10%的价格区间内提供流动性,资本效率收益将达到4000倍。v3池工厂在技术上能够支持0.02%的粒度范围,这意味着相对于v2最大可获得20,000倍的资本效率。然而,更多的颗粒池会增加交换气体的成本,在第2层可能会更有用。如果市场价格超出有限合伙人指定的价格范围,他们的流动性就会被有效地从池中移除,不再赚取费用。在这种情况下,有限合伙人的流动性完全由两种资产中价值较低的部分组成,直到市场价格回到其指定的价格区间,或者他们决定更新其区间以适应当前价格。在v3中,理论上有可能在给定的价格范围内没有流动性。然而,我们期望rational有限合伙人不断更新他们的价格范围以覆盖当前的市场价格。范围内的订单V3的LP可定制性开辟了一个新的订单功能,以补充市场订单,我们称之为“范围订单”。有限合伙人可以在高于或低于当前价格的自定义价格范围内存放单个代币:如果市场价格进入指定的范围,他们就会沿着一条平滑的曲线出售一种资产,同时在这个过程中赚取掉期费用。在一个狭窄的范围内存入类似于传统的限价指令。例如,如果DAI的当前价格低于1.001 USDC, Alice可以将价值1000万美元的DAI增加到1.001 - 1.002 DAI/USDC的范围。一旦DAI交易超过1.002 DAI/USDC, Alice的流动性将完全转换为USDC。Alice必须提取她的流动性(或使用第三方服务代表她提取),以避免在DAI/USDC开始低于1.002时自动转换回DAI。完全执行的范围订单的平均执行价格是最小价格和最大价格的几何平均值:在Alice的例子中,执行价格等于1.001499 DAI/USDC,总共为$1,001,499。此执行价格不包括在1.001 - 1.002 DAI/USDC区间内交易期间所赚取的额外掉期费用。在更大范围内的区间订单可能被证明对获利回赎、购买下跌和主要发行事件特别有用:在后来的使用案例中,发行者现在能够将流动性存入单一资产,并指定他们希望出售代币的确切价格范围。加以流动性作为每lp自定义价格曲线的副产品,流动性头寸不再是可替代的,在核心协议中不表示为ERC20代币。相反,LP位置将由非可替换令牌(NFTs)表示。然而,公共共享位置可以通过外围合约或其他合作伙伴协议被替换(ERC20)。此外,交易费用不再自动地代表有限合伙人再投资到池中。随着时间的推移,我们期望越来越复杂的策略被token化,使LPs能够参与其中,同时保持被动的用户体验。这可能包括多头寸、围绕市场价格的自动再平衡、费用再投资、贷款等等。灵活的费用Uniswap v3为LPs提供了三种不同的收费层——0.05%、0.30%和1.00%。这一系列的期权确保了LPs根据预期的货币对波动率调整其利润:LPs在非相关货币对(如ETH/DAI)中承担更多风险,相反,在相关货币对(如USDC/DAI)中承担最小风险尽管不同的费用等级可能会导致一定程度的流动性分散,但我们相信,大多数对将校准到一个“明显的”费用等级,然后成为规范的市场。我们预计同类资产组合将集中在0.05%的费用级别,ETH/DAI等组合将使用0.30%的费用级别,而外来资产可能会发现1.00%的掉期费用更合适。治理可以根据需要添加额外的费用级别。Uniswap v2引入了协议费用开关,允许由治理启动一个固定的5个基点(LP费用的16.66%)的费用。Uniswap v3协议的收费要灵活得多。费用在默认情况下是关闭的,但可以通过治理按每个池打开,并设置为LP费用的10%至25%。先进的神谕Uniswap v2推出了时间加权平均价格(TWAP)甲骨文。这些oracle作为DeFi基础设施的关键部分,已经被集成到几十个项目中,包括Compound和Reflexer。V2 oracle通过以每秒为基础存储Uniswap对价格的累计总和来工作。这些价格总和可以在一个时期的开始和结束时检查一次,以计算该时期的准确TWAP。Uniswap v3对TWAP oracle提供了显著的改进,使它能够在一次链上调用中计算出过去9天内的任何TWAP。这是通过存储累积和的数组而不仅仅是一个来实现的。这些历史价格累加器使得创建包括简单移动平均线(SMA)、指数移动平均线(EMA)、离群值过滤等在内的更高级的预测更容易、更便宜。尽管有了这一重大改进,但Uniswap公司的oracle更新成本相对于v2降低了约50%。在外部智能合约中计算TWAPs的成本也要便宜得多。许可证我们坚信去中心化的金融基础设施最终应该是自由、开源的软件。同时,我们认为Uniswap社区应该首先围绕Uniswap v3核心代码库建立一个生态系统。考虑到这一点,Uniswap v3 Core将在Business Source License 1.1下发布——实际上是一个延迟的gpl -2.0或更高版本的License。许可证将v3源代码在商业或生产环境中的使用期限限制为两年,到那时它将永久转换为GPL许可证。通过在v3-core-license-date.uniswap更新文件,Uniswap的治理可以加速GPL的变更或在任何时候授予许可证豁免。eth和v3-core-license-grants.uniswap。乙。注意,BUSL 1.1许可不影响集成。所有外部集成可能需要的代码都在GPL或MIT下获得了许可,包括数学库、外围契约、接口和开发人员sdk。任何钱包、界面、移动应用程序、协议或其他项目都能够如预期的那样与v3集成。有关此许可的更多信息,请参阅此FAQ。审计和Bug奖励我们的团队相信安全是最重要的——我们花了数百个小时的测试才放心地推出Uniswap v3。我们v3的安全流程包括:一个完整长度的审计跟踪位来自ABDK的完整审计来自samczsun的全面审计(无报告)我们工程团队的审核和审查过程一个全面的测试套件,包括自动工具Echidna和Manticore在测试和审核过程中发现的主要错误被修复。然而,我们想指出的是,Uniswap v3是一个极其复杂的协议,我们不能保证所有的错误已经被发现和解决。为了帮助发现任何公开的漏洞,将在未来30天内提供公开漏洞奖励,对关键漏洞的奖励最高可达50万美元。更多关于公共bug赏金的细节可以在这里找到。发射细节Uniswap v3智能合约将在未来几天部署到Ropsten、Rinkeby、Kovan和Görli testnets,让开发人员有时间在正式发布之前开始试验该协议。Uniswap v3核心存储库包含基本的、底层的智能合约,为协议提供动力。Uniswap v3外围存储库包含一组智能合约,旨在简化用户与核心合约的交互。一个流动性提供者迁移门户将在发布时提供,允许v2 LPs无缝地将其流动性转移到新协议。在此期间,有几项未完成的项目:合作伙伴和集成可以立即开始在Uniswap v3上进行构建,为主网的发布做准备!界面、分析站点、API和开发人员SDK正在重新设计,以便与Uniswap v3一起工作。最初的Uniswap v3路由器和位置管理器合同几乎已经完成。虽然仍有变化,但它们在这里可用。文档、指南和进一步的示例正在编写中。在主网启动后,Uniswap实验室将与Uniswap社区一起建立额外的基础设施,以支持可替代的职位、流动性挖掘、更复杂的战略和各种其他用例发布于 2021-03-26 07:43Uniswap​赞同 3​​1 条评论​分享​喜欢​收藏​申请转载​文章被以下专栏收录加密财富专注区块链,去中心化金融Defi,NFT的科

uniwap_百度百科

ap_百度百科 网页新闻贴吧知道网盘图片视频地图文库资讯采购百科百度首页登录注册进入词条全站搜索帮助首页秒懂百科特色百科知识专题加入百科百科团队权威合作下载百科APP个人中心收藏查看我的收藏0有用+10uniwap播报讨论上传视频GPRS应用模式WAP只是一种GPRS应用模式,它与GPRS的接入方式是无关的。WAP应用采用的实现方式是“终端+WAP网关+WAP服务器”的模式,不同于一般Internet的“终端+服务器”的工作模式。主要的目的是通过WAP网关完成WAP-WEB的协议转换以达到节省网络流量和兼容现有WEB应用的目的。 WAP网关从技术的角度讲,只是一个提供代理服务的主机,它不一定由网络运营商提供。但据我所知,中国联通GPRS网络只有唯一的一个WAP网关:10.0.0.172,由中国联通提供,用于WAP浏览(HTTP)服务。有一点需要注意,WAP网关和一般意义上的局域网网关是有差别的,标准的WAP网关仅仅实现了HTTP 代理的功能,并未完成路由、NAT等局域网网关的功能。这就决定了它在应用上所受到的限制。外文名uniwap所属企业中国联通目录1UNIWAP2适用范围3主要区别4限制UNIWAP播报编辑UNIWAP即中国联通的wap-gprs网络数据接入服务。适用范围播报编辑UNINET拥有完全的Internet访问权,主要看看UNIWAP。因为有了限制,UNIWAP的适用范围就要看WAP网关所提供的支持了。中国联通的WAP网关对外只提供HTTP代理协议(80和8080端口)和WAP网关协议(9201端口)。(据有网友提到1080端口也是开放的,但无法连接。也许是联通内部使用的一个Socks后门)。因此,只有满足以下两个条件的应用才能在中国联通的UNIWAP接入方式下正常工作:1.应用程序的网络请求基于HTTP协议;2.应用程序支持HTTP代理协议或WAP网关协议。主要区别播报编辑在国际上,通常只有一种GPRS接入方式,UNIWAP 和UNINET只是中国联通人为划分的两个GPRS接入方式。前者是为手机WAP上网而设立的,后者则主要是为PC、笔记本电脑、PDA等利用GPRS上网服务。它们在实现方式上并没有任何差别,但因为定位不同,所以和UNINET相比,UNIWAP便有了部分限制,资费上也存在差别。限制播报编辑为了从应用中区别两者的定位,中国联通对UNIWAP作了一定的限制,主要表现在UNIWAP接入时只能访问GPRS网络内的IP(10.*.*.*),而无法通过路由访问Internet。(少数地区的联通网络可能不存在这一限制)用UNIWAP浏览Internet上的网页就是通过WAP网关协议或它提供的HTTP代理服务实现的。说到这里,就自然而然的联想到公司的网络。公司的网络在网关上不提供路由和NAT,仅仅提供一个可以访问外网的HTTP代理。这样,就无法直接使用QQ、MSN等非HTTP协议的应用软件了(好在它们还能提供的其它的HTTP代理的连接方式如sockscap、ccproxy)。新手上路成长任务编辑入门编辑规则本人编辑我有疑问内容质疑在线客服官方贴吧意见反馈投诉建议举报不良信息未通过词条申诉投诉侵权信息封禁查询与解封©2024 Baidu 使用百度前必读 | 百科协议 | 隐私政策 | 百度百科合作平台 | 京ICP证030173号 京公网安备110000020000