Uniswap 解析:恒定乘积做市商模型Constant Product Market Maker Model 的Vyper 实作

news2024/12/28 17:54:24

大纲

一. 前言
二. 恒定乘积做市商模型Constant Product Market Maker Model
1. 计入手续费
2. 程式码结构
3. 演算法核心与实作
4. 段落小结
三. 流动性Liquidity
1. 第一笔流动性注入、决定k值
2. 除了第一笔以外的情况
四. 结语

一. 前言

暨上一篇开始接触了Vyper 后,我找了Uniswap的程式码来更加熟悉Vyper 的实作方法,顺便研究了其演算法,然后就又写了一篇xD

Uniswap 是以太坊上非常成功的自动做市商Automated Market Maker (AMM)。本次我将用的Uniswap 的程式码搭配由Runtime Verification这家审计公司对Uniswap 所做的形式化验证结果来解释恒定乘积做市商模型的Vyper 实作(2018 审计时Uniswap 就已经是用Vyper 而非Solidity 了):

  1. 智能合约程式码:v1-contracts/uniswap_exchange.vy at master · Uniswap/v1-contracts · GitHub
  2. 合约审计结果:https://github.com/runtimeverification/verified-smart-contracts/blob/master/uniswap/xyk.pdf

本文将以讲解实作概念及数学推导为重点,程式码的部分只是辅助。审计结果将恒定乘积做市商模型演算法的数学推导写得非常清楚而有趣(?),建议有兴趣者可以整份看过一遍,相信得到很多收获!

至于更多Uniswap 的介绍有兴趣者可以参考

吴冠融Roger Wu

所撰写的简介与使用流程:

  1. 解析DeFi 项目《Uniswap》(一)Uniswap 是什么?
  2. 解析DeFi 项目《Uniswap》(二)Uniswap 如何使用?

在开始前的最后,先预告本文颇长

二. 恒定乘积做市商模型Constant Product Market Maker Model

交易所如果要去中心化、也不使用挂单order book,就需要靠演算法自动算出交易标的的数量与价格,而Uniswap 使用名为恒定乘积的演算法,其来源可追溯自Vitalik 的这篇文章:点我。

公式非常的简单:x * y = k。令交易的两虚拟货币为X 和Y,各自数量为x 和y,两货币数量的乘积x * y 恒等于k,k 值是由第一笔注入的流动性所决定(于 三. 流动性Liquidity解释)。

因此,用∆x 数量的X 币来购买Y 币所能得到的数量∆y、或是为了购买∆y 需要付出的∆x 数量,依照此公式进行计算:(x+∆x)(y-∆y) = k,而交易的价格就是两币量∆x 和∆y 的比。

以下公式用α = ∆x / x 和β = ∆y / y 来表示∆x 和∆y 及XY 两币在交易发生后的新均衡数量:

图一

1. 计入手续费

在Uniswap 进行的每一笔交易都会被收取ρ = 0.003 / 0.3% 的手续费回馈给流动性提供者liquidity provider ,因此要将手续费纳入公式的考量:

图二

上图的公式或许不太直觉,我建议不要从x'ρ 及y'ρ 开始理解,而是从∆x 和∆y 两值开始:手续费ρ = 0.3% 的意思是会从付款中扣掉0.3 %,也就是从∆x 扣。在有手续费的情况下∆x 就变成了(1-ρ)∆x ,若令γ = 1-ρ 则为γ∆x。因此,将图一中的∆x 换成γ∆x,就会得到以下式子:

来源:https ://private.codecogs.com/latex/eqneditor.php

将等号左方的γ 移到右方后就得到了图二中的∆x。同理,由于∆y 中的α = ∆x / x ,用γ∆x 代换∆x 就会得到图二中的∆y (有α 的地方乘上γ )。而x' 还有y' 就可以由∆x 和∆y 推出来了!

然而,将图二中得到的x' 和y' 相乘,会得到:

来源:https ://private.codecogs.com/latex/eqneditor.php

也就是说,当有手续费使得γ != 1 /ρ != 0,x'ρ * y'ρ 的值其实会稍微和xy = k 不同:在实作上γ = 0.997 / ρ = 0.003,因此1/γ-1 ≒ 0.003。β = ∆y / y 代表的是换得的Y 币占总量的比例,即使最大值为1,误差也只有1 * 0.003,故可知手续费= 0.3% 对于k 值的影响极小。

2. 程式码结构

了解了基本的公式后,就可以开始研究程式码是怎么撰写的。首先来看各个函式的功能:

  • addLiquidity()removeLiquidity():转入与转出资金,留到 三. 流动性Liquidity中说明
  • getInputPrice()getOutputPrice()最主要的函式,用以计算给∆x 所能换得的∆y 数量、以及为了得到∆y 所要支付∆x 的数量。此两函式会被其他负责进行交易、汇币的函式使用
  • 三组(eth->Token, Token->eth, Token->Token) 的swap()transfer():swap() 的收币人就是付款人、transfer() 的收币人不是付款人而是指定的对象。基本上这两函式就是呼叫getInputPrice() 或是getOutputPrice() 后进行汇币的动作,因此不再多做解释

3. 演算法核心与实作

在研读程式码前,先回顾一下∆x 和∆y 的公式:

首先我们考虑用∆x 所能购买到的∆y 的getInputPrice():

什么…就这几行程式码?是的。

以上的程式码和公式表达方式不同,因此先将α = ∆x / x 和β = ∆y / y 代换回来并将上下同乘x:

来源:https ://private.codecogs.com/latex/eqneditor.php

由于γ = 0.997,可以将上下同乘1000 后得到:

来源:https ://private.codecogs.com/latex/eqneditor.php

接着就能来对照程式码了:

  • (109行) numerator : input_amount是欲支付的X 币数量∆x、output_reserve是Y 币数量y,再乘上997 后就是等式右边的上方(= 997∆xy)
  • (110行) denominator : input_reserve是X 币的数量,乘上1000 再加上刚刚算过的997∆x,就得到了等式右边的下方(= 1000x + 997∆x)
  • 此处要注意的是Vyper 的除法是无条件舍去,等同于floor() 函式。这会不会造成严重的影响呢?如果熟悉ERC20 的人应该记得,在发币时输入的四个参数中有一个参数代表小数点的位数,如同下方程式码中的2 代表最后两位在小数点后。举例来说,当getInputPrice() 收到1234567 为这个币的input_amount时,代表使用者拥有的币的数目实际上是12345.67。因此,即使将结果舍去0.67 后的数字,影响真的不大,况且如果不舍去而选择无条件进位,那代表交易所反而要亏损一点点啦,太佛心了吧xD 有兴趣者可以看看审计报告的内容,有更详细地去定义这些误差所影响的范围!

再来我们看若要购买∆y 需要付出多少∆x 的getOutputPrice()。

一样先将α = ∆x / x 、β = ∆y / y 和γ = 0.003 代换并上下同乘1000y 得到:

来源:https ://private.codecogs.com/latex/eqneditor.php

我们已经看过getInputPrice() 一次了,所以应该能发现第122–124 行得出的结果和上式相同。要注意的是这边的结果反而是无条件舍去后直接+1,因为这是在计算使用者要付多少∆x 才能购买到∆y,为了不让交易所亏只能选择请使用者多付一点点。

4. 段落小结

以上就是撇除汇币等函示,恒定乘积做市商的Vyper 实作,没错就这样而已!Uniswap 之所以可以做到低gas 消耗就是因为这个演算法本身就非常简单,所需的运算也就是两三次乘除法而已!

不过我们还没结束,接下来要谈谈如何投入资金/注入流动性,而这部分也包含了决定k 值的精妙机制!

三. 流动性Liquidity

流动性指的是交易市场中能够交易的资金/标的物的量。使用自动做市商(AMM) 而非挂单的最大好处就是市场一定会有流动性,而缺点就是如果交易量越大就会造成越大的滑点Slippage,意思就是交易价格变动会越大、得到的价格越差。

来源:https ://ethresear.ch/t/improving-front-running-resistance-of-xyk-market-makers/1281

我们可以用上面提到的V 文章中的图片来迅速带过,毕竟有关注Uniswap 的读者大概都已经看过这图很多次了。

当要兑换的币的数量越大/占比越重,例如:20% Y 币的流动性,就会造成要付出比兑换少量时极为不对称的高额X 币。

接着我们要来探讨注入流动性的原则,依照市场是否已经有流动性而区分为两种情形:

1. 第一笔流动性注入、决定k 值

以下程式码是addLiquidity() 函式中46-48, 51, 及64-74 行。当市场上还没有任何流动性时,不会满足第51 行而是进入64 行的else。

在第65 行我们可以看到msg.value ≥ 10¹⁰,以及在67 行token_amount就是其中一个输入值max_tokens。这边代表的是第一个注入流动性的使用者可以自行决定要注入多少Ether (≥ 10¹⁰) (= x) 以及相应的币的数量(= y),也就是上方提到的k 值(= x* y),在本例的X 币就是Ether。(本处先不解释剩余的程式码,留到2. 除了第一笔以外的情况)

那么问题来了:第一个注入流动性的人要怎么决定提供各自多少的两种币呢?最好的办法是依照当时两币的市价比,让两者的价值(数量* 价格) 相同,例如:当1 Ether 的价格为100 Dai,注入1 Ether 以及100 Dai 是最好的,因为两种币的总价值是一样的,以下举例说明原因。

当1 Ether 市价为100 Dai 时,假设第一人决定注入1 Ether 和50 Dai (k = 50),总价值为150 Dai,我们考虑两种兑换方法:

  1. Ether -> Dai:用0.1 Ether 来购买Dai,依照上方公式(1+0.1)(50-y) = 50 可得y ≒ 4.55,也就是说得到的价格是0.1 Ether = 4.55 Dai,远低于市价0.1 Ether = 10 Dai,相信没有人这么傻~
  2. Dai -> Ether:用2 Dai 来购买Ether,依照上方公式(1-x)(50+2) = 50 可得x ≒ 0.038,也就是说得到的价格是2 Dai = 0.038 Ether,高于市价2 Dai = 0.02 Ether,那么眼尖的人就会立刻冲来套利了xD

那么即使如此,第一人有所损失吗?当然有!假设路人A 手上有30 Dai (= 0.3 Ether),A 看到机会后就把30 Dai 全换成Ether:(1-x)(50+30) = 50 可得x = 0.375,大于原本持有的Dai 的价值0.3 Ether。此时,第一人即使立刻抽出现存的全部资金Ether = 0.625 及Dai = 80,总价值也只剩下142.5 Dai,比起原本的150 Dai 还少。以上的计算还有手续费没有纳入考量,但也只有30 Dai 的0.3% = 0.09 Dai。

由上例可知,第一位提供流动性的人为了避免自己的损失,确实得依照当时两币的市价比去提供相应的数量。杰克,这真是太神奇了0…0

2. 除了第一笔以外的情况

如果市场已经有流动性,使用addLiquidity() 来注入流动性就会进入第51 行的if。

来源:https ://github.com/Uniswap/uniswap-v1/blob/master/contracts/uniswap_exchange.vy

  • (53行) eth_reserve : 由于使用者已经透过函式addLiquidity() 将钱汇入了合约,因此将合约所拥有的Ether 数量self.balance (= x + ∆x) 减去使用者汇入的钱msg.value (= ∆x),得到使用者汇钱之前合约内所拥有的Ether 数量(= x)
  • (54行) token_reserve : self.token是一个喂入币地址的ERC20 instance;透过呼叫ERC20 的函式balanceOf()即可查出合约所拥有的Y 币的数量(= y)
  • (55行) token_amount : 透过将合约所拥有的Y 币的数量token_reserve (= y) 乘上使用者汇入的钱msg.value (= ∆x) 对合约原本拥有的Ether 数量eth_reserve (= x) 的比例,代表使用者应该相应地注入多少Y 币(∆y = y * ∆x / x)。除法一样是无条件舍去
  • (56行) liquidity_minted : 将原本交易所中的总流动性total_liquidity乘上增加的比率msg.value / eth_reserve (= ∆x / x) ,代表增加的流动性,随后会在第58 行记录下来
  • (60行) transferFrom()函式将使用者应付的Y 币数量token_amount (= ∆y) 汇入当前合约,就完成了流动性的注入。小提示:智能合约中的assert()会确保函式内的条件如果失败就整笔交易transaction直接取消,因此只要传入的参数已经被计算好,于60 行再进行transferFrom()其实与放在前面并没有太大的差别

以上就是注入流动性的大致实作内容。取出资金removeLiquidity() 其实与addLiquidity() 的做法大同小异,因此就不再赘述。

四. 结语

呼,真的累。恒定乘积做市商模型的概念虽然简单,但解释起来还是挺复杂的!其实本文并未着墨于审计报告中的主要议题:评估因为整数除法(不使用浮点数) 而造成的误差范围,因为讲起来非常复杂、也不是真的这么需要知道。不过,恰巧就是这些程式码的细节有可能让程式产生预期之外的结果!因此,对于有兴趣了解该如何去分析智能合约整数除法的读者,可以研究一下;而Uniswap 的程式码因为是用Vyper 实作,可读性非常高、同时也不难,因此也非常值得打开来看看、甚至动手实作自己的版本!

最后,如果本文有任何错误,请不吝提出,我会尽快做修正;而如果我的文章有帮助到你,可以看看我的其他文章,欢迎一起交流:)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/189967.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(2)-初识Fiddler让你理性认识一下

1.前言 今天的理性认识主要就是讲解和分享Fiddler的一些理论基础知识。其实这部分也没有什么,主要是给小伙伴或者童鞋们讲一些实际工作中的场景,然后隆重推出我们的猪脚(主角)-Fiddler。 1.1工作场景 做app测试,你是…

SpringMVC的工作原理

SpringMVC的工作原理图: SpringMVC流程 1、 用户发送请求至前端控制器DispatcherServlet。 2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。 3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处…

二叉树29:二叉搜索树中的插入操作

主要是我自己刷题的一些记录过程。如果有错可以指出哦,大家一起进步。 转载代码随想录 原文链接: 代码随想录 leetcode链接:701.二叉搜索树中的插入操作 题目: 给定二叉搜索树(BST)的根节点 root 和要插入…

苏嵌实训——day19

文章目录一、数据库1.1 在ubuntu中安装数据库1.2 数据库的操作1.2.1 数据库命令的分类1.2.2 常用的系统命令1.2.3 数据中的常用的语句1.3 sqlite数据库中常用api1. sqlite3_open2. int sqlite3_close(sqlite3 * db)3. sqlite3_exec4 sqlite3_get_table5 void sqlite3_free_tabl…

JavaEE12-Spring MVC程序开发

目录 1.什么是Spring MVC? 1.1.MVC定义 1.1.1.Model(模型) 1.1.2.View(视图) 1.1.3.Controller(控制器) 1.2.MVC和Spring MVC的关系 2.为什么要学Spring MVC? 3.怎么学Spring MVC? 3.1.Spring …

LeetCode——2319. 判断矩阵是否是一个 X 矩阵

一、题目 如果一个正方形矩阵满足下述 全部 条件,则称之为一个 X 矩阵 : 矩阵对角线上的所有元素都 不是 0 矩阵中所有其他元素都是 0 给你一个大小为 n x n 的二维整数数组 grid ,表示一个正方形矩阵。如果 grid 是一个 X 矩阵 &#xff0c…

Linux的shell入门和版本控制(五)

0、前言 这部分简单介绍了Linux系统中的shell编程 1、服务监听 在Linux中的服务监听,相当于在windows中的任务管理器。常用指令: 示例一:查询进程 ps -aux|grep 要查询的程序名:这样查询会连带这条查询指令的进程一起查询出来。…

【深度学习笔记】LSTM的介绍及理解

问题 LSTM是深度学习语音领域必须掌握的一个概念,久仰大名,现在终于要来学习它了,真是世事无常,之前以为永远不会接触到呢,因此每次碰到这个就跳过了。 前言 LSTM (Long short-term memory,长短期记忆) 是一种特殊的RNN,主要是为了解决长序列训练过程中梯度消失与梯度…

Linux中SELinux、Shell简介、touch命令的应用知识总结

✅作者简介:热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏:Java案例分…

Spark SQL的生命旅程之底层解析

一、内容提要一条SQL语句是如何被解析的?一条SQL是如何转换为代码被机器执行的?SQL从逻辑计划到物理计划的转换经历了怎样的优化?二、Antlr4Antlr4 Java编写的强大的语法解析生成器# 命令行使用方式 curl -O https://raw.githubusercontent.c…

【MyBatis持久层框架】核心配置文件详细解读

文章目录1. 前言2. 多环境配置3. 类型别名4. 对象工厂5. 总结1. 前言 前面我们在使用 MyBatis 开发时,编写核心配置文件替换 JDBC 中的连接信息,解决了 JDBC 硬编码的问题。其实,MyBatis 核心配置文件中还可以配置很多的内容。 MyBatis 的配…

微信小程序java+nodejs+vue校园美食点餐评论餐饮配送系统

开发语言:Java 小程序前端框架:uniapp 小程序运行软件:微信开发者 后端技术:Ssm(SpringSpringMVCMyBatis)vue.js 后端开发环境:idea/eclipse 数据库:mysql 基于校园餐饮配送小程序的设计基于现有的手机,可以实现首页、个人中心、学…

[JavaWeb]JS

目录1.JavaScript特点2. script 标签写JS代码2.1 script 标签中写JS代码2.2 使用 script 标签引入 JS 文件2.3 使用 js 的两种方式,是二选一,不能混用3. 查看 JS 错误信息3.1 chrome 浏览器查看错误信息3.2 Microsoft Edge浏览器查看错误信息4.JS变量4.1 var定义变量4.2 JavaSc…

Go 基础Interface

Go Interface 今天学习下Golang语言中Interface基本语法与使用,通过代码示例了解Go编程中的接口及其实现。跟Java语言类似,在Go编程中,开发者可以使用接口来存储一组没有实现的方法。也就是说,接口的方法不会有方法体。 接口定义…

谷歌正开发苹果AirTag的竞品,但苹果Find My需求强劲

据安卓爆料专家 Mishaal Rahman 的消息,谷歌似乎正在开发苹果 AirTag 的竞品。 从图中可以看到,谷歌 Fast Pair 蓝牙快速配对中,出现了一个名为“Locator tag(定位器标签)”的分类,这一类别此前没有出现过…

RXXW300/汇川MD500变频器MODBUS通信应用指导手册

本篇博客属于工具文档篇,方便大家查询通信相关的参数和MODBUS地址,有关PLC的MODBUS通信大家可以参看下面的文章链接: PLC MODBUS通信优化、提高通信效率避免权限冲突(程序+算法描述)_RXXW_Dor的博客-CSDN博客_modbus读写冲突MODBUS通讯非常简单、应用也非常广泛,有些老生…

51单片机多路电压检测数码管显示( proteus仿真+程序+报告+讲解视频)

51单片机多路电压检测数码管显示演示视频1.主要功能:2.仿真3. 程序4.设计报告系统需求及方案设计5. 设计资料内容清单51单片机多路电压检测数码管显示( proteus仿真程序报告讲解视频)仿真图proteus 7.8及以上 程序编译器:keil 4/keil 5 编程…

MySQL从入门到精通(第二篇):MySQL的底层原理及其结构,结合多篇文章

Mysql进阶一、事务ACIDAUTOCOMMIT二、并发一致性问题1. 修改丢失三、封锁1. 封锁粒度2. 封锁类型读写锁意向锁3. 封锁协议1. 一级封锁协议2. 二级封锁协议3. 三级封锁协议4. 两段锁协议a. 概念b. 例子c. 两段锁是可串行化的充分条件d. 两段锁与死锁四、MySQL系统变量1. 查看系统…

苏嵌实训——day18

文章目录一 wirkeshark 抓包工具1.1 软件介绍1.2 软件安装1.3 wireshark工具的使用1.4 TCP三次握手和四次挥手二 TCP循环服务器2.1 IO多路复用2.2 使用select实现IO多路复用2.3 epoll一 wirkeshark 抓包工具 1.1 软件介绍 wireshark用于抓取经过我当前主机网卡的所有的数据包…

ffmpeg解封、解码实战

1 概述 2 解封装相关函数接口 avformat_alloc_context();负责申请一个AVFormatContext结构的内存,并进行简单初始化 avformat_free_context();释放该结构里的所有东西以及该结构本身 avformat_close_input();关闭解复用器。 avformat_open_input();打开输入视频文件 avformat_…