朋友们大家好,接下来是一个新的系列,逐行拆解Defi产品经典项目代码。
首先我们从最经典的UniswapV2开始。
Uniswap简介
UniswapV2是Defi中非常经典的一个项目,被称为Defi乐高,很多项目都借鉴它的代码或者由它分叉而来。
Uniswap V2本质是一个运行在以太坊上的去中心化自动做市商(AMM)协议,用户不再需要依赖传统的订单簿,可以直接进行代币的兑换。
拆解代码的过程中,我们将充分理解什么是恒定乘积做市模型(CPMM)、手续费是如何收取的、如何支持闪电贷、价格预言机如何抗操纵。如果这些名词你都不熟悉,那这个系列的内容可太适合你了,我们一起从零开始勇闯Defi!
实现功能的核心代码主要由两部分组成,一是v2-core,二是v2-periphery。命名也非常简单粗暴,一个是核心core,一个是外围periphery。
我们先从核心core开始。在core的代码库里,有三个核心文件,分别是ERC20、Factory、Pair。
我们先从ERC20.so开始,非常简短,只有94行。

pragma

首先是pragma开头,这个关键字是在指明编译器版本,指定了0.5.16solidity版本,这个单词本来的意思就是“编译指示”,词汇量+1。
import

接着引入了两个库,是因为用到了关键字import,import的本意是“进口、输入”,在计算机里通常指“导入”。一会用到的时候我们再点开看,根据路径都能找到,分别在interfaces和libraries中。
interfaces通常被翻译成接口,也是非常妙的翻译,interface由两部分组成,分别是inter表示在……中间,face是脸、面向,字面意思就是“在……之间的那个表面” 或 “相互面对的那个面”,比如两片大陆板块之间的接触面,或者水和空气之间的分界面。抽象应用在计算机领域里,也很妙,形容的是两个不同程序相遇的边界,在边界上,信息进行交换。
libraries本意是图书馆,在这个语境里我们通常会翻译成“库”,但你认识library就能明白它想表达的是什么意思,图书馆里有很多书,你用的时候引用就行。
接下来进入正题。
contract is

首先关键字contract告诉我们这是一个合约,contract更常见的翻译是“合同”,在code is law,代码即法律的defi世界里,项目放把合同公开展示细节,用户能够读懂合同确实是必要的。
UniswapV2ERC20是这个合约的名称,接着是is IUniswapV2ERC20,is是关键字,这是说UniswapV2ERC20是继承了IUniswapV2ERC20。
IUniswapV2ERC20在接口文件夹中,我们去看发现这是一个interface关键词开头,里边定义了一堆函数。

solidity语法规定合约可以继承自接口,接口其实也是一种抽象的合约,只能声明函数但是没有函数体,具体的实现需要在继承处完成。这样做的好处是解耦,其他合约只需要知道IUniswapV2ERC20接口的入参和出参就能和UniswapV2ERC20交互。
SafeMath

Using safemath for uint, using A for B 是说将函数A作为操作符附加到用户定义的值类型。这句话是说uint类型的值都将可以用safemath作为操作符,这是什么意思呢?
我们先看A函数也就是safemath在干什么,从字面理解safemath,就是安全地做算数。

定义了几个函数,分别是add加、sub减、mul乘
这是一个函数,名字叫add,需要输入的参数分别是uint格式的x和uint格式的y,internal、pure也是关键字先忽略,一会我们再看,return是返回的意思,最终返回一个uint格式的z。
花括号

一个大括号括住的是函数体,solidity是花括号类型的语言,这是在描述语法的外观,看见{}就知道是代码块的开始和结束,C/C++/Java都是花括号风格,Solidity 选择这种风格,是因为它借鉴了主流编程语言的习惯,让开发者更容易上手。除了花括号风格之外,还有缩进风格,比如Python,通过严格的缩进格式来表示代码块,还有关键字风格比如Pascal。
花括号中的内容是一句require,require的意思是要求,在solidity的语法中,require可以用来检查条件,如果不符合条件就抛出异常。所以这句话的意思是说,把x+y的值赋给z,并且要求z>=x,如果不满足条件抛出异常'ds-math-add-overflow'算加法的时候溢出了。类似的,下边算减法、乘法也是做了相同的校验。
这个库叫safemath,安全地做运算,那意思是原生的运算不安全呗?是的,在solidity0.8.0版本之前,算数运算是不会自动检查结果是否超过了数据类型的范围。数据类型是说这个东西是什么类型的,比如它是一串文本还是一个xx位的数字。

比如这里出现的uint,int是指整数全称是integer,u是指无符号,所以uint就是无符号的整数。通常uint后边会跟一个数字,比如uint8,也就是说这是一个8位长度的无符号整数,那这个uint8的上限是最大多大呢?在计算机的2进制世界里,每个位置只有0或者1两种情况,有8位意味着有2^8种不同的组合,由于是从0开始计数,所以一共有2的8次方-1个数,最大到255,如果再+1,uint8就会绕回去变成0。
safemath就是在做安全检查,如果计算结果溢出了,就直接让交易失败,避免更多的资金损失。虽然现在0.8.0之后solidity原生内置了溢出检查,但还是有项目会出于gas优化或者代码前后逻辑兼容的原因自己做safemath。
定义变量
从safemath里回来,继续

接下来是一些变量的定义,string是说这个字段是字符串,public是一个可见性的声明。
可见性的选择还有
Public:意思是公共的,内部、外部均可见
Private:意思是私有的,仅在当前合约内可见
External:意思是外部的,仅在外部可见,仅可用于消息调用
Internal:意思是内部的,仅在内部可见
接下来四行是说
这是一个字符串,是一个public公共的 constant常量,字段名字叫 name,它的值是'Uniswap V2',也就是说我们这个token的名字叫Uniswap V2。
这是一个字符串,是一个public公共的 constant常量,字段名字叫 symbol,它的值是'UNI-V2',也就是说我们这个token的symbol是UNI-V2。
这是一个uint8无符号的整数位数是8,是一个public公共的 constant常量,字段名字叫 decimals,它的值是'18',也就是说我们这个token的小数位数是18位。
这是一个uint无符号的整数,当uint后边不跟任何数字的时候,其实省略的默认值是256,是一个public公共的变量,名字叫totalSupply,也就是说我们定义了一个变量是token的总供应量。

接下来是两个mapping类型的变量,mapping的本意是映射,你拿着地图按图索骥,地图上的每一个点都对应一个实际的地方,地图和实际的城市、村庄之间是有“映射”关系在的。
这里的mapping也是类似的意思,你根据一个“指引”找到了一个“东西”。这句话里的意思是address和uint之间有对应关系,每给你一个address你就能找到它对应的一个无符号整数,我们把这个关系和里边存的数据认为是一个mapping类型的变量,它是公共的,名字叫做balanceOf。
balance更常见的含义是平衡,但是在这里是“余额”的含义,因为因为它最初描述天平两边“平衡”的状态,你把天平一边放砝码,另一边放物品,最终让天平平衡时,砝码的总重量就是物品的“余额”。所以这句话是定义了一个变量,通过一个地址就能知道这个地址里的账户余额。
接下来mapping嵌套了两层,通过一个address找到了另一个address,然后在第二次的mapping中找到了一个uint,这个变量的名字叫allowance。allowance是许可、允许,这个变量是在定义可供转账的额度,我对身边人的信任程度不同,那他们在我这里的额度也是不同的,所以这个变量是定义了地址和地址之间的关系,以及每一个关系对应的额度。
接下来三句在后续无gas授权中会用到,到时候再说。
event事件

两句event开头的,event是事件,主要是为了记录状态的变更,是一个可以对外界发声的日志。
一个是approval,授权,记录了owner、spender、value,谁授权给谁,授权了多少金额。
另一个是Transfer,转账,记录了from、to、value,从哪转到哪,转了多少钱。
constructor构造函数

接下来是constructor开头的一段,constructor本意是建造者,我们这里翻译为构造函数,public定位它为公共的,外部可访问。
首先定义了一个chainId,当没有赋值的时候,默认缺省值是0。
然后是assembly,意思是接下来要嵌入内联汇编(Inline Assembly)了。Solidity是高级语言,隐藏了很多底层细节,比如要访问 EVM 操作码、要手动管理内存指针就无法操作,所以需要更底层的指令。
chainid是 EVM 的一个操作码,用于获取当前链的 ID(例如以太坊主网为 1,Goerli 测试网为 5)。然后把获得的chainid赋值给上边定义的chainId
接着给DOMAIN_SEPARATOR赋值,DOMAIN_SEPARATOR在第16行被定义了,是一个bytes32 长度为32字节的一个公共变量。
最外层是keccak256函数,keccak256是Solidity中内置的一个哈希函数,哈希函数是一种特殊的函数,可以认为它是一个“数据指纹提取器”,输入任意大小的数据(无论是一个字母、一本书还是一部电影),它都会输出一个固定长度的、看起来完全随机的字符串,输出的这个字符串叫哈希值。
叫它“指纹提取器”是因为,确定性:同一个输入永远得到同一个输出,没有随机性,什么时候看我的指纹都是一样的。单向性:从输入可以很简单地算出输出,但是从输出几乎不可能反向推出输入,找到一只鸟然后看它的掌纹很简单,但是从一个掌纹在全世界定位出到底是哪只鸟几乎不可能。抗碰撞性:几乎不可能找到两个不同的输入得到相同的输出,就像拥有相同指纹的人概率非常低。
再内一层是abi.encode函数,abi.encode是说按照标准 ABI 规则将变量打包成字节数组的函数,粗暴理解就是把内层的这些信息打包在一起。
接下来EIP712Domain的一句,是一个结构声明,起到一个表头的作用,接下来的内容有name、version、chainid和一个待验证的地址。接着的四行,分别是这四个字段的内容。
合起来这段话就是在给DOMAIN_SEPARATOR赋值,是把这个token叫什么名字、版本、所在链、地址打包之后取哈希,把计算后的哈希值塞进了DOMAIN_SEPARATOR。这是一个域分割符,里边存的内容是这个合约的基本信息,主要是为了后续签名的时候做校验,确保是跟“这个合约”做的交互。
mint&burn函数

接着是mint和burn两个函数,这两个函数的可见性都是internal,当前合约和派生合约都可以调用internal的函数。
首先看_mint函数,mint的意思是铸造,铸造了一个新的金元宝,会导致两个结果,首先市面上整体的供应量变多了,其次新铸造的这个金元宝是谁的要记在谁的账上。
入参是地址和金额,需要知道是谁铸造的,铸造了多少个。
首先更新整体的供应量。totalSupply.add()的写法就是用了safemath的库,完成totalSupply+value,然后把相加之后的结果赋值给totalSupply。这就完成了第一步,新铸造的token产生,要在总供应量上体现出来。
第二步这是谁铸造的要加在他自己的账上,上边提到balanceOf是一个mapping类型的变量,存储了地址和它对应的余额,to这个变量是传入的地址,balanceOf[to]找到的就是to这个地址的余额,.add(value)就是给余额加上这次铸造的金额,然后赋值给balanceOf[to],完成余额的更新。
最后emit是发射的意思,首字母大写的Transfer是上边提到的event事件,广播日志:有一笔从地址(0)到地址to,金额是value的转账。
_burn函数逻辑和_mint逻辑类似,burn是燃烧的意思,用火烧把金币融掉,在defi语境中通常会翻译为销毁。入参是地址和金额,先完成销毁账户的余额减少,然后再减少整体的供应量,最后对外广播transfer转账事件。
对比mint和burn来看,从transfer记录的日志来看,从0地址转出的是铸造,转入到0地址的是销毁。
_transfer和transfer函数

先看_transfer和transfer两个函数,它们俩名字起得很像。
_transfer是一个private私有函数,只能在合约内部被调用。它实现的功能就跟它的名字一样——转账。入参是三个参数,分别是from,to,value,从哪里转到哪里多少钱。
第一句,把from地址的余额减少value个;第二句,把to地址的余额增加value个;第三句,广播事件,从from地址向to地址转账了value个代币。
transfer是一个external外部函数,意味着它只在外部可见,主要是被用来调用的。它的入参只有两个,分别是to和value,向谁+转账多少。出参有一个,返回本次转账的结果。
第一句是在调用_transfer,msg.sender是在获取谁给他发的消息,谁发消息就是谁需要转账,然后把传进来的to和value合在一起调用私有函数_transfer,完成消息发送地址向to地址转账value个代币,完成之后返回true。
这两个名字很像的函数是需要组合使用的,因为external的transfer总是需要调用private的_transfer函数。这样做的好处实现了封装和权限分离。封装是指,外部合约如果需要交互,只需要理解transfer函数,不用管转账具体是怎么发生的,完成之后会返回true告知。权限分离是指强制转账需要通过公开接口,负责功能的部分不做权限的校验,相对更安全一些。
_approve和approve函数

_approve函数和approve函数一起看,approve是授权的意思,他们的关系与_transfer和transfer的关系类似。
_approve是一个private私有函数,入参是三个字段:地址owner、地址spender和金额。第一行把value赋值给allowance[owner][spender]。allowance是上方定义过的一个嵌套的mapping字段,从左往右解开,owner给spender的授权额度赋值为value。
第二行对外发布事件Approve,owner授权给spender value个权限。
函数approve是external外部函数,入参是两个字段:spender和金额,返回一个bool类型的值让对方知道是不是执行成功了。
这个函数主要在调用_approve,需要的三个入参传入了两个,缺的那个用消息的发送方补上,完成发消息的地址授权spender这个地址value个权限。
transferFrom函数

从函数transferFrom开始,逐渐开始展示出uniswap的小巧思了。
从字面意思理解,这个函数是完成了从from地址向to地址转账的功能,入参是地址from和to以及金额value,出参是一个bool类型的值,告知是否成功。
转账要分两步,首先有没有额度,然后把钱转过去。
首先判断额度。这是一个if判断,如果满足条件就执行花括号内的语句,如果不满足就跳过。
判定是在看allowance[from][msg.sender]的授权额度,从from到to转账,关msg.sender什么关系?
msg.sender是当前调用transferFrom的地址,它可能与from(代币持有者)相同,也可能不同。这里可以兼容第三方协助转账的动作,A老板给B老板转账100万,大老板不会亲自去银行,msg.sender是跑腿的小弟,首先小弟要看A老板有没有给自己100万的支配额度。
判定条件是!=uint(-1),uint是一个无符号的整数,为什么会有-1这种有符号的出现呢?在二进制中,-1在无符号类型里会被表示为所有位全是1的数。所以 uint(-1)就等于 2^256 - 1,是一个巨大的天文数字。
这句话是在说,如果from地址给msg.sender授权额度不是uint(-1),那么在授权额度减掉这次的value值。
如果你授权给协议把额度设置为最大值uint(-1),这样不管你交易多少次,路由合约每次transferFrom的时候,都不用扣减,因为那个巨大的数字永远够用。后续所有的交易都不用再次调用approve,少一次状态写入,就少一些gas费。
if判断之后,调用了_transfer函数,从from地址向to地址转账了value个代币,然后返回true,告知调用方完成。
permit函数

接下来是permit函数,是整个UniswapV2ERC20.sol里最精妙的设计。
permit的含义是允许,这个函数实现了允许用户通过链下签名完成授权,而无需自己发送approve交易,这意味着用户可以不用支付gas就完成授权,由其他人(比如第三方中继器)代付 Gas 来执行 permit。我们逐行来看。
这个函数叫permit,输入有7个参数,分别是owner、spender、value、deadline、v、r、s,这是一个external的外部函数。
首先是require的判定,要求deadline要比block.timestamp区块的当前时间戳要晚,因为签名是链下产生的,需要确保还在有效期内,如果不满足这个条件会直接返回'UniswapV2: EXPIRED'无效啦。
定义了一个变量digest,是摘要的意思,是我们之前见过的keccak256和abi.encodePacked函数,大概意思是说把这堆内容打包之后算出哈希值,参与计算的有DOMAIN_SEPARATOR我们之前提到过的域分割符,在最开始生命中定义的PERMIT_TYPEHASH、nonces[owner]、以及跟随函数传进来的参数owner, spender, value, deadline。最终这个digest就是用户在链下要签名的消息哈希。
ecrecover是 Solidity 内置函数,它可以根据消息哈希 digest和签名三件套 v, r, s计算出签名者的公钥,并返回其地址。把这个地址赋值给recoveredAddress。
一个require的验证,如果recoveredAddress不为0地址,并且recoveredAddress和owner一致,如果不满足这个条件,就会返回'UniswapV2: INVALID_SIGNATURE'无效签名。
否则就会执行_approve(owner, spender, value)。
这个过程最终是谁调用permit谁来付gas,owner不需要支付任何gas费。
ERC20总结
这个文件一共94行,从头到尾看完也不是一件非常困难的事情。
这个UniswapV2ERC20.sol文件,主要的功能是实现了一遍erc20代币的核心公共函数。erc20代币是指同质化代币,就是这一块钱和零一块钱没有什么区别,它们都是同质化的。相对应的概念叫非同质化,比如这幅画和那幅画都是一个艺术家画的,但他们不一样,比如NFT。
同质化代币的基础功能就是要能铸造、能销毁、能授权、能转账,以及有一些基础的配置,比如总供应量是多少、代币叫什么名字、代号叫什么之类。
所以这个文件就是在实现UniswapV2的ERC20通用功能。
下次我们继续,读第二个文件UniswapV2Factory.sol
夜雨聆风