朋友们大家好!今天我们继续读uniswap v2core代码库中的factory.sol的文件。
Factory直译是工厂的意思,uniswap为什么需要一个工厂呢?它要生产加工什么东西?看完全部代码我们就能理解了。
factory.sol的文件比erc20还要简短,只有49行,我们逐行来看。
变量定义

前三行是熟悉的基本动作,指定编译器版本,导入库文件。

第6行,contract开头,定义了这个合约的名称叫UniswapV2Factory,继承自IUniswapV2Factory。IUniswapV2Factory是一个接口,定义了一些函数和事件的入参和出参,具体的函数实现在UniswapV2Factory中完成。

第7-8行,定义了两个地址类型的变量,分别是feeTo和feeToSetter。
从字面理解,feeTo就是手续费到哪里去,是一个接收手续费的地址,feeToSetter是设置feeTo的人,所以它也是一个地址,可以完成feeTo的设置。

第10行是一个嵌套的mapping,从左往右解开就是从一个地址找到一个mapping关系,中间这层的mapping存的是一套地址和地址的关系。这个变量被命名为getPair。
Pair是一对儿的意思,在Defi中,我们通常翻译为代币对,比如在做兑换中,把eth换成usdt,那么eth和usdt就是一个pair,它们是一个代币对,存储的时候会存这两个代币的地址。getPair,是说获取代币对,也就是说我们给每一个pair生成了一个地址存着,当我们想找到getPair[token0][token1]的时候,就能返回对应的pair的地址。

第11行定义了一个地址类型的数组,方括号[]是代表数组的意思,数组顾名思义就是一组数字,这个变量的名字叫allPairs,就是把所有的代币对的地址都存进了这个地址类型的数组中,每一个代币对都有一个地址,这个变量就是把它们都存在一起,通过索引的编号来找到第几个格子里是什么代币对。

第13行,定义了一个事件event,名字叫PairCreated,当代币对pair被创建之后向外同步的日志。入参是三个地址分别是token0、token1、pair和一个uint整数,最后这个整数是用来做序号的,就是刚才allPairs这个数组中,创建的新pair是存在第几格柜子里,方便直接根据序号找到代币对的地址。

第15行是一个构造函数,入参是一个地址,花括号内的函数体只有一行,把入参传入的地址_feeToSetter赋值给第8行定义的变量feeToSetter。

第19行定义的一个函数,allPairsLength,这是一个外部的函数,在external之后,还有一个关键词叫view,view是一个修饰器,除了这里的view之外,我们在erc20.sol中还见过pure和constant:
pure:官方文档里写的是:“修饰函数时不允许修改或访问状态变量”,直译过来是说“纯的”,通常只会有一些算数运算在里边,基本就是不与外界交互:不访问也不修改外部,只是纯纯的做自己view修饰函数时:不允许修改状态变量。view的意思是看、看法,也就是说view可以看可以访问状态,但是只看不动手,不能修改。payable修饰函数时:允许从调用中接收以太币。这个很好理解payable就是有pay-able有付钱能力的,能付钱肯定能收钱。constant修饰状态变量时:不允许赋值(除初始化以外),不会占据存储插槽。constant的含义就是常数,所以在初始化的时候给定一个值之后,就不许再变化了。immutable修饰状态变量时:允许在构造时分配并在部署时保持不变。存储在代码中。immutable直译过来就是不可变的,在构造函数的时候赋值,在之后该合约实例的整个生命周期里,这个变量都是常量。
再有其他的我们碰到了再说。
回到allPairsLength的函数中,这个函数的出参返回的是一个无符号整数,函数体只有一句话,返回allPairs.length,allPairs是第11行定义的地址类型数组,length是长度,这句话的意思是返回这个数组里到底有多少个数,到底创建了多少个格子来存放pair代币对。
函数createPair

第23行,这个函数叫createPair,从这一行开始,就进入正题了,读到这里就能明白为什么这个文件叫factory工厂,因为这个文件的主要功能就是创造出新的pair,起名叫工厂确实也挺贴切的。
入参是两个token的地址,出参是返回这个两个代币组成的代币对的地址。
24行是这个函数主体的第一句,一个require判定,要求满足tokenA!=tokenB,这是在要求两个token不一样,如果不能满足这个条件,就会返回'UniswapV2: IDENTICAL_ADDRESSES'identical是一样的意思,如果两个token一样,那根本就没有兑换的必要,会在一开始就拒绝。
第25行是在比大小,然后给两个token排序,为什么要这么做?比如我们要给eth和usdt创建一个代币对,那要不要给usdt和eth再创建一个呢?这两个没有其他区别只是顺序不一样。uniswapv2关于这个问题的答案是,创建一个地址就行,但给eth+usdt和usdt+eth的pair地址都赋值成一样的,这样后期在使用的时候就比较方便,方便检索和调用。我们看代码里具体是怎么实现的。
先看等号的右边,问号?和冒号是两个分隔符,这句话的意思是tokenA小于tokenB吗?如果是采纳冒号左边的方案,否则采纳冒号右边的方案。地址是可以比大小的,这句话的意思是,如果tokenA比tokenB小,就按tokenA在前,tokenB在后的格式输出,如果tokenB比tokenA小,就tokenB在前输出(这俩不会相等,相等就一样了,第一句话已经拒绝它们了),永远小的在前排列,然后把这两个地址分别赋值给token0和token1。token0和token1在之前并没有出现过,它们俩是在函数内的局部变量,仅在函数内可见。
接着requir要求token0不能为0地址,否则就会返回'UniswapV2: ZERO_ADDRESS',这里校验token0就够了,因为token1一定比token0大,只要token0不为0,token1也一定不会为0。
第27行,这是一个reqiure的判定,getPair[token0][token1],getPair是第10行定义的一个两层mapping,也就是找到[token0][token1]对应的代币对的地址,这个require要求这个代币对的地址是0,就是找不到对应的地址,只有这样才证明这个代币对之前没有被创建过,才需要factory工厂来造一个。如果已经存在了的话,就返回'UniswapV2: PAIR_EXISTS'代币对已经存在了。后边还有一句注释single check is sufficient,单一的检查就够了,是说不用分别检查[token0][token1]和 [token1][token0],因为如果这个代币对已经存在了的话,肯定做过第25行的token的排序,所以检查一个就可以。
Create2&initialize
从28行开始略略有些精妙的操作

定义了一个bytes类型的变量叫bytecode,有一个关键字叫memory,我们先不管它
type(UniswapV2Pair).creationCode是说获取合约UniswapV2Pair的创建时字节码,什么是创建时字节码?合约在部署时,实际发送给evm的是一段“创建字节码”就是这个creationCode,evm执行这段creationCode之后,会把运行时代码存储到链上,形成我们常见的合约地址,这里就是在获取UniswapV2Pair这个合约的创建字节码,并存储在bytecode里。
定义了个byte32类型的变量,名字叫salt盐,keccak256和abi.encodePacked的组合见过好多次了,是把排序后的token0和token1紧打包之后计算哈希,生成一个确定性的32位的值。
assembly意味着进入内联汇编,create2是evm创建合约操作码,可以根据给定的输入算出来合约地址。
太坊黄皮书定义 CREATE2操作码(0xF5)需要获取 4 个参数:salt、offset (代码在内存中的起始位置)、length (代码长度)、value (发送的 ETH 数量)。回到代码中:pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
0对应value
add(bytecode, 32)对应offset,是代码在内存中的起始位置,就是bytecode从32位开始的地方,
mload(bytecode)对应length,mload函数是固定取前32位,创建字节码前32位存储的就是字节码数组的长度
salt对应salt,是上文把token0和token1紧打包之后计算出的哈希值
合起来这句话的意思是,计算出了token0和token1这个代币对的地址,并将值赋给了pair。
33行是在做代币对的初始化,这时pair合约已经存在链上了,马上需要初始化,如果有空档的话,可能会被其他恶意合约篡改。通常情况下,如果直接使用constructor就不用这么麻烦,会直接把部署+初始化一起完成一步到位。
为什么factory要在这绕路做复杂的操作呢?因为它需要确定性计算出合约地址。我来详细解释一下。
我们先看create2+initialize组合做了什么。
获取了合约UniswapV2Pair的创建时字节码,再加上两个token的信息,合在一起计算出了代币对的地址。合约UniswapV2Pair的constructor构造函数里不会传入token的信息。做一个比喻,UniswapV2Pair做了一个通用的模板框架,它像一个通用的汤底,拿着这个方子谁都可以复刻出标准水平的汤,但是每个餐厅需要的汤是有差异化的,所以用salt盐来调整,把做汤和加盐两步分开,具体要放多少盐视需求来定。
如果把salt盐(代币对)的信息也加到UniswapV2Pair的构造函数中,每一个汤底的方子就都完全不同了,就没办法提前算出来了,只有足够标准化才能做中央厨房和预制菜(不是。
这也是UniswapV2Pair合约中构造函数中没有做任何代币对信息的输入,初始化依靠initialize来实现的原因。

第34行,计算出pair,完成函数的初始化之后,我们把pair的地址赋值给getPair[token0][token1],同时也赋值给getPair[token1][token0],确保在查询或者使用的时候,正反两种组合都能找到对应的代币对地址。
36行是在往allPairs这个数组中写入新创建的这个地址,用的函数是push,push在这里被翻译为“压入”,这个词也很传神,想象一个弹夹,每放入一颗子弹,这个动作就是在压入一颗子弹,压在了弹夹的最上边,所以对应的也是压入数组末尾。和写入对应的是pop,把数组中末尾的元素删除。
37行是在发布日志,PairCreated,这个代币对的组成是什么,代币对的地址,以及代币对在数组中的编号
setFeeTo&setFeeToSetter

第40行,函数叫setFeeTo,入参是传入一个地址,这是一个外部函数。函数体里就两句话非常简单,首先是一个require的判定要求msg.sender == feeToSetter,要求发消息调用函数的地址必须是feeToSetter,否则就返回'UniswapV2: FORBIDDEN'禁止。也就谁能调用这个设定手续费接受地址的函数呢?只能是feeToSetter。只有设定设定手续费转出地址的地址才能设定手续费转出的地址,好像一个绕口令。
当判定你有条件之后,你可以把刚才传入的地址赋值给feeTo,就设置接受手续费的地址了。

第45行,函数叫setFeeToSetter,这就在设定设定手续费地址的地址了,首先也是判断发消息的地址是不是feeToSetter,如果是的话,就能把传入的地址设置为feeToSetter。那最开始的feeToSetter是什么?因为每次都要先做require判定,如果一开始是0地址的话,永远判定不通过,所以一定需要一个初始值的设定,最开始的设定在第15行的constructor函数里,传入了一个地址,赋值给feeToSetter。
总结
Factory的文件也很短只有49行,最关键的部分是creatPair这个函数,这也是为什么这个文件起名叫Factory的原因,是一个能够制造出pair代币对的工厂。用create2+initialize实现了可以前置稳定计算出pair的地址,使得任意第三方都能通过简单的链下计算来找到正确的交易对地址。
下次我们详细解读pair.sol,也是整个core里最核心的部分。
夜雨聆风