定义流动性挖矿是一种网络参与策略，用户向协议提供流动资金，以换取该协议的原生token或奖励。最原始的POW挖矿类似btc、eth等是用矿机去挖，这就需要大量机器、场地、技术团队等成本。后来有了POS挖矿，只要很少的机器和场地和技术团队成本，但是额外的为了防止女巫攻击，每个矿工都要质押币。他们挖的币来自哪里呢？一个是出块奖励，另一个是转账交易的手续费。但我们只有获得出块权才能获得奖励，这竞争还是很大的，所以一般人挖矿都是加入到大矿池，出块之后奖励大家按照比例分。对于类似Uniswap交易所的协议，都希望做大它的池子，所以需要我们向池子里添加流动性，它把交易的手续费作为奖励按比例分配给我们。DEX一般是智能合约，不需要出块，但也会发自己的代币，所以也会有一部分代币奖励给流动性提供者（简称LP）。这看起来与传统挖矿区别就不大了，质押虚拟币，然后按比例分交易手续费和平台代币，代币一般会越来越少，跟出块奖励越来越少一样。流动性挖矿项目根据设计和目的，可以分为三类：公平启动。主要目标是通过一些客观的标准（例如成为协议的活跃用户）来分配大部分的token，而不是直接销售，并确保每个人都有平等的机会来获得token。编程式去中心化：主要目标是逐步实现社区所有权并尽量减少资金管理。增长型营销：主要目标是在一段时间内激励特定的用户行为。每一个类别都有优点和缺点，某些协议的挖矿方案可能综合了多个类别。去中心化交易所去中心化交易所Uniswap就是流动性挖矿的典型应用，用户向Uniswap流动性提供流动资金然后获取手续费奖励。最早启动流动性挖矿的项目已经不好考证，但是让流动性挖矿一夜火爆起来的是Compound这个借贷项目。后来Sushi复制了Uniswap V2的代码，通过搞流动性挖矿，一度成功地把Uniswap的流动性给吸走了大部分。因为Compound的流动性挖矿是基于它的借贷模式，为了便于大家理解，我们用Sushi来解释流动性挖矿的原理，因为Sushi的代码和Uniswap是完全一样的，对于Swap，我们通过对Uniswap的AMM机制剖析，得出结论如下：一个交易池需要LP放入两种资产，才能实现AMM，让交易员实现Swap交易；一个LP放入两种资产到交易池后，他会得到相应数量的代币作为LP凭证，这个代币通常就称为LP，因为稍后可以用LP提取前期放入的流动性资产并获取手续费收入。注意到每个交易池对应的LP凭证代币都是不同的，不能用A池的LP代币去B池提取流动性。手续费是激励LP放入流动性的关键。手续费通常为0.1%～0.3%，其中大部分归LP，小部分由项目本身收取以支付开发运营费用等。Swap的手续费虽然看上去比传统的撮合交易要高几倍，但是链上交易频率较低，如果交易额度不是很大，手续费实际收入是很少的。越是交易额低的池子，手续费收入越低，做LP的人越少，反过来导致交易额越低。因此，如何激励LP，尤其是没有什么流动性的新交易池，就是一个大问题。我们来看看Sushi的流动性挖矿，是如何激励LP的。假设一个新的交易池ETH/USDC没有什么流动性，为了鼓励大家去做LP，Sushi引入了一种锁仓挖矿的方式：一个做市商放入ETH/USDC后，获得相应数量的LP；不要在手里持有LP，而是把LP质押到挖矿合约中，此合约按照设定的规则，按质押金额和质押时长发放奖励，奖励的代币就是SUSHI；一段时间后，从挖矿合约中解除质押，获得SUSHI奖励，再把解除质押的LP用于提取流动性资产，获得ETH/USDC以及手续费。由于存在挖矿，因此收益除了手续费，还额外多了挖矿奖励SUSHI。把SUSHI卖掉，那就是双份奖励，自然收获双份快乐。我们假设第一步投入总价值$10000的资产到交易池，获得10000个LP，再假定10000个LP放入挖矿合约中，每天收获100个SUSHI，一个SUSHI如果是$0.1，那么投入$10000，每天的挖矿收入就是$10，日收益率0.1%，年化收益率36.5%。但是，且慢，SUSHI也是凭空发出的代币，你挖我挖他也挖，挖出来你卖我卖他也卖，很快就能把SUSHI卖成$0.01，$0.001，直到归零。不下点套，能让你随随便便卖掉挖出来的SUSHI？怎么下套？这就是流动性挖矿的“精髓”所在。对ETH/USDC这种普通的交易池，每10000个LP每日可挖出100个SUSHI，这叫普通矿池。还有一种交易池，SUSHI/USDC，每10000个LP每日可挖出1000个SUSHI，这叫超级矿池。那谁会挖普通矿池啊，当然是直接挖超级矿池了！看仔细了，超级矿池是SUSHI/USDC，为了能挖超级矿池，首先要获得它的LP，而它的LP需要注入的资产是SUSHI和USDC。一开始挖矿的时候，大家手里有SUSHI么？为了挖超级矿池，你得先挖普通矿池，等待一段时间，获得SUSHI奖励后，不能把SUSHI卖掉，而是再用USDC配对，去SUSHI/USDC做LP，才能挖超级矿池。这就是老司机说的一池、二池。一池就是普通矿池，二池就是奖励加倍的超级矿池。下图就是如何在手里没有SUSHI的情况下先在一池挖矿，再去二池挖矿，以便获得更高的收益： ┌──────┐ │ ▼ ETH USDC │ SUSHI USDC │ │ │ │ │ ▼ ▼ │ ▼ ▼ ┌─────────────┐ │ ┌─────────────┐ │Swap: │ │ │Swap: │ │ETH/USDC │ │ │SUSHI/USDC │ └─────────────┘ │ └─────────────┘ │ │ │ ETH-USDC-LP │ SUSHI-USDC-LP │ │ │ ▼ │ ▼ ┌─────────────┐ │ ┌─────────────┐ │Mining: │──┘ │Mining: │──▶ SUSHI │ETH-USDC-LP │ │SUSHI-USDC-LP│ └─────────────┘ └─────────────┘也有人会想，既然大家手里都没有SUSHI，要挖二池就要有SUSHI，但是挖一池获得SUSHI奖励太费时间，我直接去SUSHI/USDC池子里买点SUSHI，买到了SUSHI直接挖二池它不香么？恭喜你有这样聪明的头脑。回顾上文我们讲Uniswap的x * y = k恒乘积公式，买入SUSHI就会直接推高SUSHI的价格，比如把SUSHI从$0.1买到了$1。这个时候，一池矿工的收益率就从年化36.5%直接变成了365%，二池的收益率更不得了了，这会导致更多的人冲进来挖矿。更多的人去买SUSHI，导致SUSHI价格更高，比如涨到$10，挖矿收益率更高，如此反复循环。那SUSHI不得涨到天上去？那也不会，因为矿池每天的产出是固定的。比如一池每天产出100个SUSHI，一个人投入$10000每天收获100个SUSHI，但是另一个人也投入$10000，两人每天就各自只能收获50个SUSHI了。因此，随着矿工的涌入，挖矿收益率会降低，低到一个程度，某些矿工就会主动撤退。撤退的矿工会将手里的SUSHI卖出以获得实际的挖矿收益。有的人在$1的价格卖出，有的人在$10的价格卖出。随着挖出的SUSHI越来越多，必然有越来越多的矿工开始抛售SUSHI，直到某个时间点，卖出的SUSHI比买入的SUSHI多，SUSHI价格开始下降。这一降不要紧，关键是收益率随之下降。一旦价格下跌的预期形成，矿工就会争先恐后地抛售SUSHI，SUSHI价格也随之断崖下跌，收益率砍掉一个0或者两个0不叫个事儿，老司机称这种现象叫“矿塌了”。所以流动性挖矿是短期内迅速实现冷启动的一种模式，通过激励，短期内迅速获得大量用户。但是它的泡沫来得快去得也快，早期的矿工获得的收益实际上是由高位接盘的后期买家支付的，参与流动性挖矿一定要认清潜在的风险，包括合约漏洞的风险，不能被表面虚高的年化收益率给蒙蔽了双眼。也有人会想，这么好的一本万利的生意，为什么以前没有赌场做？这是因为区块链的数学加密算法保证了计算公开透明，没有任何人能篡改规则。大家只有在确信规则透明可靠的情况下，才会参与这个游戏。如果是传统的网站，能随意修改数据库，自然不会有人相信这个游戏规则本身是公平的。可以肯定的是，大多数流动性挖矿的项目，最终除了早期矿工，肯定是发行代币的项目方大赚特赚。但我们也应该看到流动性挖矿的积极一面：如果一个类似Uniswap的项目可以长期稳定地运行，且项目收益定期分配给持有UNI的投资人，那么流动性挖矿也是一种早期代币分发的机制，因为早期流动性挖矿的矿工作为参与者为项目提供了大量流动性，客观上推动了项目的快速发展。事实上Uniswap也搞过一段时间的流动性挖矿。
一、新特性提供任意ERC20-ERC20交易对，不再局限于V1的ERC20-ETH交易对价格预言机，有效防止价格操纵，减缓价格波动闪电兑换（闪电贷），自由借出并使用代币，并在该交易的最后归还代币一个0.05%的协议手续费开关。如果开启，流动性提供者将获得0.25%手续费ERC20交易对Uniswap v1使用ETH作为桥梁货币，每个交易对都包含ETH作为其资产之一。这使得路由更简单，比如要想实现ABC和XYZ的交易，只需要分别使用ETH/ABC和ETH/XYZ交易对即可，减少了流动性的分散。然而，这条规则给流动性提供者带来了巨大的成本。所有流动性提供者都接触ETH，由于x * y = k引入的滑点，并根据其他资产相对于ETH的价格变化而遭受无常损失（类似股票的浮亏），简单而言就是在代币价格单方面（上涨或下跌）波动时，做市者手中持有的代币实际总价值反而减少。使用ETH作为强制的交易代币也会增加交易成本。相比直接使用ABC/XYZ交易对，他们将支付两倍的交易手续费，同时承受两倍的滑点。因为在v1，要想从ABC交易到XYZ，必须依次交易ABC/ETH和ETH/XYZ，因此手续费和滑点都需要两倍。v2允许流动性提供者为任意两个ERC-20代币创建交易对合约。但交易对数量的激增将给寻找最优交易路径带来困难，但是路由问题可以在上层解决（比如通过链下或链上的路由器或聚合器）v2不直接支持ETH的交易对，它需要包装成遵循ERC20标准的WETH Token。价格预言机Uniswap流动池在某个时间点的代币边际价格（不包含手续费），可以通过代币a和代币b的总量相除得出：p=a/b。当提供的价格不正确时，套利者可以在Uniswap交易套利，因此Uniswap提供的代币价格将跟随市场价格。这意味着Uniswap提供的代币价格可以作为一种近似的价格预言机。然而，Uniswap v1无法提供安全的链上预言机，因为它的价格很容易被操控。假设其他合约使用当前ETH-DAI价格作为衍生品交易的基准价格。攻击者可以从ETH-DAI交易对买入ETH来操控价格，并触发衍生品合约的清算，接着再将ETH卖回以使价格回归正常。上述操作可以通过一个原子交易完成，或者被矿工通过排序同一区块中的不同的交易来实现。注：由于V1的采样的价格是瞬时的，因此很容易通过买入卖出大额代币来操纵实时价格。Uniswap v2改进了预言机功能，通过在每个区块的第一笔交易前记录累计价格来实现，每个价格会以时间权重记录（基于当前区块与上一次更新价格的区块的时间差）。这意味着在任意时间点，该累计价格将是此合约历史上每秒的现货价格之和。操纵这个价格会比操纵区块中任意时间点的价格要困难。如果攻击者通过在区块的最后阶段提交一笔交易来操纵价格，其他套利者（发现价格差异后）可以在同一区块中提交另一笔交易来将价格恢复正常。矿工（或者支付了足够gas费用填充整个区块的攻击者）可以在区块的末尾操控价格，但是除非他们同时挖出了下一个区块，否则他们没有特殊的优势可以进行套利。注：由于价格预言机仅在每个区块记录一次，因此除非同一个人控制了两个区块的所有交易，否则他们将没有足够的套利优势。为了估算在t1到t2时间段内的时间加权平均价格（TWAP），外部调用者可以分别记录t1和t2的累计价格，将t2价格减去t1价格，并除以t2-t1的时间差。预言机的用户可以自行选择区间的开始和结束。选择一个更长的区间，意味着攻击者将花费更高的代价来操控该区间的时间加权平均价格，虽然这将导致该平均价格与实时价格相差较大。注：由于合约仅记录当前的累计价格，因此如果需要计算区间的平均价格，外部应用要自己记录并保存历史价格，合约本身不保存历史数据。Uniswap v2的TWAP计算方式实际上使用的是（加权）算数平均数。在数学上有一个毕达哥拉斯平均的概念，指的是三种经典平均数，分别是：算数平均数、几何平均数和调和平均数。其中，算术平均数是最常见的一种平均数，其优点是计算简单，缺点是容易受到极端数据的影响，导致均值误差；几何平均数相比算术平均数，更适用于在金融市场场景，因为金融市场价格本身是一种布朗运动；调和平均数更易受到极小值的影响，一般应用于计算平均速率等场景。从应用场景上，Uniswap价格均值应该使用几何平均数更合适，均值的误差更小，但由于几何平均数在以太坊合约上实现难度较大，所以Uniswap v2版本采用算数平均数；但是Uniswap v3则使用几何平均数计算价格预言机。一个难题：我们应该计算以B代币计价的A代币价格，还是以A代币计价的B代币价格？虽然在现货价格上，以B代币计价的A代币价格（B/A）与以A代币计价的B代币价格（A/B）总是互为倒数，但在计算某个时间区间的算数平均数时，二者却不是互为倒数关系。比如，假设在区块1的价格为100 USD/ETH（B为USD，A为ETH），区块2的价格为300 USD/ETH，则其平均价格为200 USD/ETH，但ETH/USD的平均价格却是1/150 ETH/USD。因为合约无法知道交易对中哪一个代币将被用户用作计价单位，因此Uniswap v2同时记录了两个代币的价格。另一个难题是用户可以不通过交易而直接向交易对合约发送代币（这将改变代币余额并影响价格），此时将无法触发预言机价格更新。因为预言机价格需要在区块的第一笔交易之前更新，因此如果不交易，将绕开预言机更新。如果合约只是简单地检查它的余额，并使用当前余额计算价格来更新预言机，那么攻击者可以在区块的第一笔交易之前，立即向合约发送代币来操控预言机价格。如果上一笔交易是在X秒之前的某个区块，合约将错误的使用（被操纵后的）新价格乘以X来累计，即使并没有人使用该价格交易过。为了防止这个问题，core合约在每次交互后缓存了两种代币余额，并且使用缓存余额（而非实时余额）更新预言机价格。精度因为Solidity原生不支持非整数数据类型，Uniswap v2使用了简单的二进制定点制进行编码和操作价格。确切地说，任意时间的价格都被保存为UQ112.112格式的数据，它表示在小数点的左右两边都有112位比特表示精度，无符号（注：非负数）。这个格式能表示的范围为[0, (2的112次方)-1]，精度为(2的112次方)分之一。择UQ112.112格式是出于（Solidty合约）编程实践的考虑，因为这些格式的数字能够使用一个uint224类型（占用224位比特，28个字节）的变量表示，在一个256位（比特）的存储槽（注：EVM中一个Storage Slot是256位）中正好剩余32位可用。而对于缓存的代币余额变量，每一个代币余额可以使用一个uint112类型（112比特位，14个字节）的变量，（在声明时）也正好在256位的存储槽中剩余32位可用。这些剩余空间可用于上述的累计运算使用。具体来说，代币余额与最近一个有交易区块的时间戳一起保存，该时间戳针对2的32次方取模，以确保可以使用32位表示。此外，虽然在任意时间点的价格（使用UQ112.112格式的数字）一定符合224位，但是一段时间的累计价格却不是这样。在存储槽末尾的多余32位空间将用于存储由于重复累计价格导致的溢出数据。这样的设计意味着价格预言机仅仅在每个区块的第一笔交易增加了3个SSTORE操作（当前消耗15,000 gas）。为了避免每次交易都更新预言机给用户带来额外交易成本，Uniswap v2设计成只在每个区块的第一笔交易之前更新。每个代币余额使用uint112表示，时间戳使用32位表示，总共112+112+32=256位，正好占用一个storage slot。更少的storage slot意味着交互时需要花费的gas更小，有利于减少用户操作成本。累计价格则采用256位表示。这个设计最主要的缺点是32位无法确保时间戳永不溢出。事实上，Unix时间戳溢出32位（可表示的最大值）将发生在02/07/2106。闪电贷在Uniswap v1，用户如果想使用XYZ购买ABC，则需要先将XYZ发送到合约才能收到ABC。这将给那些希望使用ABC购买XYZ的用户带来不便。比如，当Uniswap与其他合约出现套利机会时，用户可能希望使用ABC在别的合约购买XYZ；或者用户希望通过卖出抵押物来释放他们在Maker或Compound的头寸，以此偿还Uniswap的借款。Uniswap v2增加了一个新特性，允许用户在支付费用前先收到并使用代币，只要他们在同一个交易中完成支付。swap方法会在转出代币和检查k值两个步骤之间，调用一个可选的用户指定的回调合约。一旦回调完成，Uniswap合约会检查当前代币余额，并且确认其满足k值条件（在扣除手续费后）。如果当前合约没有足够的余额，整个交易将被回滚。用户可以只归还原始代币，而不需要执行交易操作。这个功能将使得任何人可以闪电借出Uniswap池子中的任意数量的代币（闪电贷手续费与交易手续费一致，都是0.30%）。Uniswap v2合约中的闪电贷与交易功能实际上使用同一个swap方法。协议手续费Uniswap v2包含一个0.05%的协议手续费开关。如果打开，该手续费将被发送到合约中的feeTo地址。默认情况下没有设置feeTo地址，因此不收取协议手续费。预定义的feeToSetter地址可以调用Uniswap v2工厂合约中的setFeeTo方法来修改feeTo地址。feeToSetter也可以调用setFeeToSetter修改合约中feeToSetter地址。如果feeTo地址被设置了，协议将开始收取5个基点（0.05%）的手续费，也就是流动性提供者收取的30个基点（0.30%）手续费中的1/6将分配给协议。如果在每笔交易时收取0.05%的手续费，将带来额外的gas消耗。为了避免这个问题，累计的手续费只在提供或销毁流动性时收取。合约计算累计手续费，并且在流动性代币铸造或销毁的时候，为手续费受益者铸造新的流动性代币。二、改动SolidityUniswap v1使用Vyper语言实现，这是一个类Python的智能合约语言。Uniswap v2使用更流行的Solidity语言实现。合约重构Uniswap v2的一个设计重点在于最小化core交易对合约的对外接口范围和复杂度。core合约仅保留最基础最重要的功能，以保证安全性，因为所有流动性资产将存放在core合约中。其它功能都被抽取放到router（路由）合约，由路由合约对外提供接口和服务。在Uniswap v2，卖方在执行swap方法前，会发送代币到core合约。合约将通过比较缓存余额和当前余额来判断收到多少代币。这意味着core合约无法知道交易者是通过什么方式发送代币。事实上，他可以通过离线签名的元交易方式手续费调整Uniswap v1的交易手续费是通过减少存入合约的代币数量来实现，在比较k恒等式之前，需要先减去0.3%的交易手续费。合约隐式约束为：(xin - 0.003xin)yin>=k，通过闪电贷功能，Uniswap v2引入了一种可能性，即xin和yin可能同时不为0（当一个用户希望通过归还借出的代币，而不是做交易时）。为了处理这种情况下的手续费问题，合约强制要求约束为：(xin - 0.003xin)(yin - 0.003*yin)>=k。因为当通过闪电贷同时借出x和y两种代币时，需要分别对x和y收取0.3%的手续费，因此需要先扣除手续费，再保证余额满足k值约束。sync() 和 skim()为了防止某些可以修改交易对合约余额的定制代币，同时也为了更优雅地解决那些总量超过2的112次方的代币，Uniswap v2提供了两个方法：sync()和skim()。简单来说，由于某些（非Uniswap导致的）外部因素，交易对合约中的缓存余额与实际余额可能出现算法外的不一致问题。sync()方法可以更新缓存余额到实际余额（以流动池实际持有的代币数量进行修正），skim()方法可以更新实际余额到缓存余额（取出多余的代币，大于2的112次方部分），从而保证系统继续运行。任何人都可以执行这两个方法。如果有人误将交易对中的代币转入合约，任何人都可以取出这些代币。处理非标准和罕见代币ERC-20标准要求transfer()和transferFrom()返回一个布尔值表示该请求是否成功。然而某些代币在实现这两个（或其中一个）方法时并没有返回值，比如USDT和BNB。Uniswap v1在解析无返回值的方法时，将其当作失败处理，因此将回滚交易，从而导致交易失败。Uniswap v2引入了”lock”机制用来解决所有公开修改状态方法的重入问题。这也可以防止在闪电贷中用户自定义回调的重入问题。lock实际上是一个Solidity modifer，通过一个unlock变量控制同步锁初始化流动性代币供应当一个新的流动性提供者将代币存入一个已存在的Uniswap交易对，新铸造的流动性代币数量可根据当前代币数量计算，按照提供的份额等比例增发。因为Uniswap v1/v2提供流动性时需要注入两边等值的代币，如果份额等同于ETH数量，则1份额表示需要存入1ETH，而在价格正确时，另一个代币的价值也同样是1ETH，因此1个流动性份额的流动性总价值是2ETH。理论上可能存在这种情况，最小的流动性代币单位（1的18次方分之一，即1 wei）的价值太高，以至于无法让其他（小）流动性提供者加入。为了解决这个问题，Uniswap v2销毁首次铸造10的15次方分之一（最小代币单位的1000倍）流动性代币。这个损耗对于大部分交易对而言都是微不足道的。但是这将极大提到首次铸币攻击的代价。首次铸币攻击是指攻击者在第一次添加流动性时存入最小单位（10的-18次方，即1 wei）的流动性，比如1 wei ABC和1 wei XYZ，此时将铸造1 wei 流动性代币（根号1）；同时，攻击者在同一个交易中继续向池子转入（非铸造）100万个ABC和100万个XYZ，接着调用 sync()方法更新缓存余额，此时1 wei的流动性代币价值100万+(10的-18次方)ABC和100万+(10的-18次方)XYZ，其他流动性参与者要想添加流动性，需要等价的大量代币，其价格可能高到大部分人无法参与。包装ETH为WETH由于Uniswap v2支持任意ERC-20交易对，因此没有必要支持原生ETH交易。增加这种支持将使core合约代码量翻倍，并且将使流动性分裂为ETH和WETH交易对。原生ETH需要先封装为WETH才能在Uniswap v2交易。事实上，Uniswap v2只是core合约不支持原生ETH，periphery合约仍然支持原生ETH交易，合约会自动将ETH转为WETH，然后再调用core合约进行交易。这里也反映出Uniswap一直倡导的开发原则，保持core合约最简化，应用和用户体验的逻辑依靠periphery合约解决。确定的交易对地址与Uniswap v1一样，所有Uniswap v2交易对合约都由一个统一的工厂合约初始化生成。在Uniswap v1，这些合约使用CREATE操作码创建，这意味着这些合约的地址依赖于合约生成的顺序。Uniswap v2使用以太坊新的CREATE2操作码生成具有确定地址的交易对合约。这意味着交易对合约的地址是可以通过链下计算的，而无需查询链上状态。最大代币余额为了更有效地实现预言机功能，Uniswap v2只支持缓存代币余额的最大值为(2的112次方)-1。该数字已经大到可以支持代币总量超过千万亿的18位小数代币。如果任意一种代币余额超过最大值，swap方法的调用将会失败（由于_update()方法的检查导致）。为了从这种状况中恢复，任何人都可以调用skim()方法来从池子中移除多余的代币。Uniswap v2合约概览
Uniswap V1本质是一个去中心化的自动交易所，提供ETH和任意ERC20代币之间相互兑换的功能。特点是去中心化、去监管化和安全性，是一个开源的公共产品，不收取交易费用。概览Uniswap采用恒定乘积自动做市系统，即资金池内的ETH和ERC20代币数量的乘积总体是恒定的。与传统交易所基于订单簿并促进买卖双方的代币交易方式不同，Uniswap是基于各种代币的流动性储备来完成代币兑换交易的，这样的好处是买卖双方能快速直接地完成代币兑换而不必挂单等待。代币兑换的价格由恒定乘积做市场来决定（x*y=k），这种自动做市的机制能使得代币的总体储备相对均衡。代币的储备由流动性提供者保障，流动性提供者会按代币交易费用的一定比例获取奖励。Uniswap的一个重要特征是利用工厂/注册（factory/registry）合约，为每个ERC20代币部署一个单独的代币兑换合约，并且这些交易合约各自持有ETH及其相关的ERC20代币来用于代币兑换。 代币兑换合约通过注册表链接，因此可以以ETH作为中介直接进行ERC20代币到ERC20代币的兑换交易。Uniswap V1的体系相对来说比较简单，分为两个合约：Exchange，用于进行ETH和ERC-20代币之间的兑换。Factory，用于创建和记录所有的Exchange，也用于查询代币对应的Exchange。Factory合约contract Exchange(): def setup(token_addr: address): modifying NewExchange: event({token: indexed(address), exchange: indexed(address)}) exchangeTemplate: public(address) tokenCount: public(uint256) token_to_exchange: address[address] exchange_to_token: address[address] id_to_token: address[uint256] @public def initializeFactory(template: address): assert self.exchangeTemplate == ZERO_ADDRESS assert template != ZERO_ADDRESS self.exchangeTemplate = template @public def createExchange(token: address) -> address: assert token != ZERO_ADDRESS assert self.exchangeTemplate != ZERO_ADDRESS assert self.token_to_exchange[token] == ZERO_ADDRESS exchange: address = create_with_code_of(self.exchangeTemplate) Exchange(exchange).setup(token) self.token_to_exchange[token] = exchange self.exchange_to_token[exchange] = token token_id: uint256 = self.tokenCount + 1 self.tokenCount = token_id self.id_to_token[token_id] = token log.NewExchange(token, exchange) return exchange @public @constant def getExchange(token: address) -> address: return self.token_to_exchange[token] @public @constant def getToken(exchange: address) -> address: return self.exchange_to_token[exchange] @public @constant def getTokenWithId(token_id: uint256) -> address: return self.id_to_token[token_id]initializeFactory只在创建的时候被调用，一旦设置了template参数后就无法更改，确保了用于创建Exchange的代码模板不会被修改。template是链上部署的合约，用于作为后续创建的Exchange的模板。createExchange用于从模板创建一个Exchange。在做一些必要的校验之后，代码调用内置函数create_with_code_of拷贝exchangeTemplate所指示的地址中的代码创建一个新的合约并返回其地址。随后调用新创建的Exchange的setup函数设置代币地址，并将新创建的Exchange记录在合约内。注意到在做验证的过程中，函数约束每一个代币只能对应一个Exchange，这是为了约束某个代币的所有流动性都划分在一个池子中，增加池子中对应的存储量，降低交易的滑点。所有的代币兑换合约地址都存储在工厂合约中，并且ERC20代币地址和兑换合约地址之间可以相互查询。getExchange()函数可以根据传入的代币地址找到对应的兑换合约地址，getToken()函数可以根据传入的兑换合约地址找到相应的ERC20代币地址。Exchange合约Exchange的实现略有复杂，下面是其接口定义// 只保留了Exchange的核心功能接口 interface UniswapExchangeInterface { // 流动性 function addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) external payable returns (uint256); function removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline) external returns (uint256, uint256); // 价格查询 function getEthToTokenInputPrice(uint256 eth_sold) external view returns (uint256 tokens_bought); function getEthToTokenOutputPrice(uint256 tokens_bought) external view returns (uint256 eth_sold); function getTokenToEthInputPrice(uint256 tokens_sold) external view returns (uint256 eth_bought); function getTokenToEthOutputPrice(uint256 eth_bought) external view returns (uint256 tokens_sold); // 提供ETH以兑换代币 function ethToTokenSwapInput(uint256 min_tokens, uint256 deadline) external payable returns (uint256 tokens_bought); function ethToTokenTransferInput(uint256 min_tokens, uint256 deadline, address recipient) external payable returns (uint256 tokens_bought); function ethToTokenSwapOutput(uint256 tokens_bought, uint256 deadline) external payable returns (uint256 eth_sold); function ethToTokenTransferOutput(uint256 tokens_bought, uint256 deadline, address recipient) external payable returns (uint256 eth_sold); // 提供代币以兑换ETH function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256 eth_bought); function tokenToEthTransferInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline, address recipient) external returns (uint256 eth_bought); function tokenToEthSwapOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline) external returns (uint256 tokens_sold); function tokenToEthTransferOutput(uint256 eth_bought, uint256 max_tokens, uint256 deadline, address recipient) external returns (uint256 tokens_sold); // 代币之间的互换 function tokenToTokenSwapInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address token_addr) external returns (uint256 tokens_bought); function tokenToTokenTransferInput(uint256 tokens_sold, uint256 min_tokens_bought, uint256 min_eth_bought, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_bought); function tokenToTokenSwapOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address token_addr) external returns (uint256 tokens_sold); function tokenToTokenTransferOutput(uint256 tokens_bought, uint256 max_tokens_sold, uint256 max_eth_sold, uint256 deadline, address recipient, address token_addr) external returns (uint256 tokens_sold); }添加/移除流动性用户可以调用addLiquidity和removeLiquidity向资金池中添加和取回流动性。添加流动性的过程简述如下：用户调用addLiquidity函数并存入（发送）一定数量的ETHExchange合约按照当前兑换比例计算用户需要支付的代币数量，并将该数量的代币从用户转给自己为了证明用户确实提供了流动性及用户流动性所占的份额，Exchange将向用户发放LP（Liquidity Pool）代币存入Token数量 = Token总量 * 存入ETH数量 / ETH总量 发放LP数量 = LP总量 * 存入ETH数量 / ETH总量 由于Uniswap去中心化的特性，添加流动性的交易发出时和确认时流动性池的兑换比例可能不同。为了避免这个问题给用户造成的损失，addLiquidity函数提供了三个参数进行控制：min_liquidity：用户期望的LP代币数量。如果最终产生的LP代币数量过少，则交易回滚max_tokens：用户想要提供的最大代币量。如果计算得出的代币数量大于这个参数，则交易回滚deadline：时限。如果交易确认的区块时间大于deadline，则交易回滚@public @payable def addLiquidity(min_liquidity: uint256, max_tokens: uint256, deadline: timestamp) -> uint256: assert deadline > block.timestamp and (max_tokens > 0 and msg.value > 0) # total_liquidity = totalSupply of LP token total_liquidity: uint256 = self.totalSupply if total_liquidity > 0: assert min_liquidity > 0 # eth & token reserve in the pool eth_reserve: uint256(wei) = self.balance - msg.value token_reserve: uint256 = self.token.balanceOf(self) # the amount of token user should also provide token_amount: uint256 = msg.value * token_reserve / eth_reserve + 1 # minted amount of LP token liquidity_minted: uint256 = msg.value * total_liquidity / eth_reserve assert max_tokens >= token_amount and liquidity_minted >= min_liquidity # record LP token balance & totalSupply self.balances[msg.sender] += liquidity_minted self.totalSupply = total_liquidity + liquidity_minted # transfer tokens from user to Exchange assert self.token.transferFrom(msg.sender, self, token_amount) log.AddLiquidity(msg.sender, msg.value, token_amount) log.Transfer(ZERO_ADDRESS, msg.sender, liquidity_minted) return liquidity_minted流动性添加者可以随时通过销毁他们的流动性代币，从代币兑换池中按他们所持流动性代币所占比例提取相同比例的ETH和ERC20代币。所能提取的ETH和ERC20代币计算公式如下：取回ETH数量 = ETH总量 * 要销毁的LP数量 / LP总量 取回Token数量 = Token总量 * 要销毁的LP数量 / LP总量 ETH和ERC20代币按照代币兑换池当前的价格（比例）提取，而不是流动性提供者提供流动性时的价格，因此流动性提供者在移除流动性时可能会因为市场波动而蒙受损失。def removeLiquidity(amount: uint256, min_eth: uint256(wei), min_tokens: uint256, deadline: timestamp) -> (uint256(wei), uint256): assert (amount > 0 and deadline > block.timestamp) and (min_eth > 0 and min_tokens > 0) # total_liquidity = totalSupply of LP token total_liquidity: uint256 = self.totalSupply assert total_liquidity > 0 token_reserve: uint256 = self.token.balanceOf(self) # calculate returned eth & token amount eth_amount: uint256(wei) = amount * self.balance / total_liquidity token_amount: uint256 = amount * token_reserve / total_liquidity # check assert eth_amount >= min_eth and token_amount >= min_tokens # update status self.balances[msg.sender] -= amount self.totalSupply = total_liquidity - amount # send eth & token back send(msg.sender, eth_amount) assert self.token.transfer(msg.sender, token_amount) # log event log.RemoveLiquidity(msg.sender, eth_amount, token_amount) log.Transfer(msg.sender, ZERO_ADDRESS, amount) return eth_amount, token_amount价格计算每个Exchange（或者说一个池子）中有且只有两种资产：ETH和代币，池子中两个资产存量（Reserve）的比率构成了兑换比例（价格）。因此，用户有两种指定价格的方式：精确指定换出（Output）值，并限定最大的输入值（Input）；或者精确指定换入（Input）值，并设置最小的输出值（Output）。另外：Exchange会收取0.3%的手续费（在输入端收取，剩下的部分按实际价格兑换）奖励给所有流动性提供者。因此，Uniswap实现了两个私有函数作为定价体系：getInputPrice和getOutputPrice。def getInputPrice(input_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256: assert input_reserve > 0 and output_reserve > 0 input_amount_with_fee: uint256 = input_amount * 997 numerator: uint256 = input_amount_with_fee * output_reserve denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee return numerator / denominator def getOutputPrice(output_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256: assert input_reserve > 0 and output_reserve > 0 numerator: uint256 = input_reserve * output_amount * 1000 denominator: uint256 = (output_reserve - output_amount) * 997 return numerator / denominator + 1实际返回的不是价格，而是可以获得资金的数量，并且包括了0.3%手续费，获取输出数量公式根据恒定乘积公式和手续费推导，再进行公式变换可获得输入数量公式，具体计算公式如下：输出数量 = 输出总量 * （输入数量*997 / （输入数量*997 + 输入总量*1000）） 输入数量 = 输入总量 * （输出数量*1000 / （输出总量 - 输出数量）* 997） + 1 在确定池子中输入单位和输出单位的存量时，精确的输入数量能换出的输出数量；在确定池子中输入单位和输出单位的存量时，精确的输出数量能换出的输入数量。Uniswap提供价格查询的四个函数：getEthToTokenInputPricegetEthToTokenOutputPricegetTokenToEthInputPricegetTokenToEthOutputPrice均在getInputPrice和getOutputPrice这两个函数的基础上进行实现。以getEthToTokenInputPrice为例：def getEthToTokenInputPrice(eth_sold: uint256(wei)) -> uint256: assert eth_sold > 0 token_reserve: uint256 = self.token.balanceOf(self) return self.getInputPrice(as_unitless_number(eth_sold), as_unitless_number(self.balance), token_reserve)ETH和代币间的互换有了价格计算函数，ETH和代币间的互换就变得非常直观。首先来看内部实现的四个函数：通过精确的ETH输入量（eth_sold）计算价格并交换代币。通过getInputPrice计算输出的代币数量。包含了min_tokens最小代币输出量和deadline的时间限制。@private def ethToTokenInput(eth_sold: uint256(wei), min_tokens: uint256, deadline: timestamp, buyer: address, recipient: address) -> uint256: # check assert deadline >= block.timestamp and (eth_sold > 0 and min_tokens > 0) # calculate output token token_reserve: uint256 = self.token.balanceOf(self) tokens_bought: uint256 = self.getInputPrice(as_unitless_number(eth_sold), as_unitless_number(self.balance - eth_sold), token_reserve) assert tokens_bought >= min_tokens # transfer token to recipient assert self.token.transfer(recipient, tokens_bought) log.TokenPurchase(buyer, eth_sold, tokens_bought) return tokens_boughttokenToEthInput和tokenToEthOutput两个函数在实现上面基本一致,主要区别在于通过getOutputPrice计算输入的ETH数量。@private def ethToTokenOutput(tokens_bought: uint256, max_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256(wei): # check assert deadline >= block.timestamp and (tokens_bought > 0 and max_eth > 0) # calculate input ETH token_reserve: uint256 = self.token.balanceOf(self) eth_sold: uint256 = self.getOutputPrice(tokens_bought, as_unitless_number(self.balance - max_eth), token_reserve) # may have refund, also check (revert) if eth_sold > max_eth eth_refund: uint256(wei) = max_eth - as_wei_value(eth_sold, 'wei') if eth_refund > 0: send(buyer, eth_refund) # transfer token assert self.token.transfer(recipient, tokens_bought) log.TokenPurchase(buyer, as_wei_value(eth_sold, 'wei'), tokens_bought) return as_wei_value(eth_sold, 'wei')在交易机制上，Uniswap实现了两种交易方式：Swap和Transfer。两者的唯一差别在于，Swap调用的接收者固定为交易发送者（即msg.sender），而Transfer调用可以额外指定一个接收者。通过如下函数对可以清晰地看出：@public @payable def ethToTokenSwapInput(min_tokens: uint256, deadline: timestamp) -> uint256: # 'receipient' is msg.sender return self.ethToTokenInput(msg.value, min_tokens, deadline, msg.sender, msg.sender) @public @payable def ethToTokenTransferInput(min_tokens: uint256, deadline: timestamp, recipient: address) -> uint256: assert recipient != self and recipient != ZERO_ADDRESS # 'receipient' is specified as parameter return self.ethToTokenInput(msg.value, min_tokens, deadline, msg.sender, recipient)代币和代币间的互换由于ETH被用作所有代币的公共对，它可以被用来作为代币与代币之间兑换的桥梁。比如如果想要用ABC代币兑换XYZ，则可以先用ABC兑换成ETH，然后再用ETH兑换成XYZ。手续费ETH兑换ERC20代币用0.3%的ETH支付手续费ERC20代币兑换ETH用0.3%的ERC20代币支付手续费ERC20代币兑换ERC20代币用0.3%的ERC20代币支付从ERC20到ETH的手续费用0.3%的ETH代币支付从ETH到ERC20的手续费（相对于扣除完第一次手续费之后的0.3%）最终的手续费比率为0.5991%ERC20代币到ERC20代币的交易包括ERC20代币到ETH和ETH到ERC20代币两笔兑换，因此手续费是分别支付给两个兑换合约的。Uniwap没有收取额外的平台费用。手续费会直接被加入池子且没有额外铸造流动性代币，这相当于增加了所有流动性代币的价值，因此流动性提供者会因为用户在该池子进行代币兑换而获益，这部分收益最终可以通过销毁流动性代币来获得。Uniswap V1在Ethereum Mainnet中的合约详情：Factory合约Exchange合约
需求分析可以支持多条链，只要链支持Solidity智能合约即可可以支持多个代币，需要符合ERC20标准的代币由去中心化的中继节点采用投票方式来完成跨链请求的确认每个中继节点需要质押保证金，每成功一笔跨链交易可获取奖励引入惩罚机制，中继节点每次作恶都会被罚没，保证金低于一定值时被封禁，重新充入保证金可解封引入黑名单管理机制，符合监管需求，禁止名单内账户使用跨链桥功能引入中继节点准入机制，维持中继节点集群稳定，保障跨链桥的稳定工作跨链交易规则跨链有最低金额限制（比如100USDT）每笔收取总金额的一定比例作为手续费（比如万分之五）一定比例的手续费平分给参与投票的中继节点（比如70%）剩下比例的手续费发给平台（比如30%）用户需手动去领取跨链资产，也可以在指定一个帮忙自动提取的中继节点（需额外付费）中继节点规则中继节点列表由平台进行管理维护，需保持稳定（避免在短时间内大量增加或减少）保证金由中继节点自行维护，存入自由，取出需要限制（比如每隔24小时可取一次）中继节点数量需保持一定合适的数量，至少也得3个，太多中继节点也不好（浪费机器资源，也拖慢投票速度）投票罚没规则中继节点需保证金大于指定门槛的才可以参与投票（比如10万），低于的处于封禁状态中继节点的投票权重都一样，和保证金大小无关每次跨链请求需要大于2/3的中继节点（所有的包括封禁的）投票才算通过投票时需要附带中继节点的签名（合约可以验证签名来核查投票结果并允许用户提取对应资产）每次作恶罚没一定比例的保证金（比如5万），罚没的保证金转给平台地址如果某个节点持续作恶，会不断损失保证金，平台也可以将其从中继节点列表中移除跨链通道要进行跨链交易，需要在两条链之间创建一个通道通道是有方向的，一般情况要添加双向的：A链->B链和B链->A链通道需指定两条链关联的代币地址相同的两条链可添加多对代币地址参与角色普通用户-web前端：发起跨链请求、领取跨链资产、查看交易记录（历史的和进行中的）中继节点-后台服务：跨链交易投票、帮用户自动领取跨链资产中继节点-web前端：查看管理保证金、查看收益、查看投票记录平台管理-web前端：查看管理中继节点、查看保证金、查看收益列表、查看修改黑名单、查看交易记录数据分析与监控：交易汇总分析、中继节点收益分析、资金流动分析、跨链桥异常预警合约设计核心流程：用户在A链存入（冻结）资产、中继节点投票（签名）确认、使用超2/3的签名在B链取出（解冻）资产引入一个廉价的链来存储投票过程，而不是中继节点自行共识，投票过程更安全，也可追溯，提高系统的可用性、安全性用来投票的链这里称为桥链（bridge chain），它还可用来存储一些其它数据或运行一些服务使用签名技术减少合约调用次数，简化设计与节省交易gas费为了节省交易gas费，所有投票在桥链上完成，其它功能也尽量在桥链上实现投票就是签名，用户投票则是将签名写入到桥链合约中跨链通道定义：通道id=keccak256(来源链id,来源链代币地址,目标链id,目标链代币地址)跨链交易定义：交易id=keccak256(通道id,时间戳,接收者,金额,手续费)跨链交易记为KR，投票就是对KR进行签名，记为：KS，有效投票需满足数据无篡改和签名可验证架构桥核心合约（资产）：跨链资金出入口、中继成员列表、黑名单列表桥治理合约（投票）：签名存储与验证、保证金管理、惩罚机制、手续费分发桥链部署：治理合约A链部署：资产合约B链部署：资产合约C链部署：资产合约桥核心合约接口-添加中继节点接口-移除中继节点接口-将账户地址加入黑名单接口-将账户地址从黑名单删除接口-添加跨链通道接口-移除跨链通道接口-跨链转入资产（用户资产充足且未在黑名单内）接口-跨链取出资产（需通过2/3的中继节点签名验证）事件-中继节点变更、黑名单变更、资产转入、资产转出、资产解冻、交易取消查询-中继节点、黑名单、跨链USDT额度（到不同链上的额度）变量-跨链USDT额度列表、中继节点列表、黑名单列表桥治理合约接口-增加保证金接口-减少保证金接口-上传签名（验证签名，作恶罚没）事件-保证金变更、签名提交、投票结果、罚没查询-保证金、签名、罚没次数、罚没金额变量-保证金余额列表、签名列表、跨链交易列表、惩罚记录表合约执行以以太坊和波场为例，以稳定币USDT为例初始化两条链的桥核心合约设置相同的10个中继节点地址和黑名单列表在桥链的桥治理合约中为10个中继节点地址冲入20万保证金平台往两条链的桥核心合约中存入10万USDT作为流动资金Ethereum >>> Tron 跨链用户调用Ethereum链资产合约的跨链转入接口检查用户不在黑名单内，黑名单用户交易直接失败将用户的1万个USDT转到资产合约地址名下更新变量：到Tron链的跨链USDT额度减少1万发出事件：跨链转入（起始链ID + 目标链ID + 金额 + 目标链代币合约地址）中继节点调用桥链上的桥治理合约上传签名接口接收用户的跨链请求事件，可以等待N个区块确认（避免区块分叉导致交易失效）对此跨链事件进行签名并提交到投票合约中（如果不认可则可以忽略）在签名满7个时检查投票结果，如果有超2/3的有效票则进行以下操作：对此次跨链请求作恶的中继节点执行罚没分发跨链交易手续费发出事件：投票完成（跨链请求交易哈希 + 可提取代币地址 + 可提取金额 + 7个签名）中继节点调用Tron链桥核心合约的跨链转出接口检查合约持有的USDT是否足够检查7个签名是否有效，且签名地址在中继节点列表中给用户指定的接收地址转9995个USDT（从桥核心合约地址名下）更新变量：Ethereum链的跨链USDT额度增加1万发出事件：跨链取出（跨链请求交易哈希 + 代币地址 + 金额）状态变更总结Ethereum链的资产合约:持有的USDT数量：10万 -> 11万桥链的投票合约：签名列表：+ 跨链请求的7个有效签名Tron链的资产合约：持有的USDT数量：10万 -> 9万用户在Ethereum上的USDT减少1万，在Tron链上获得9995个7个中继节点在桥链上获得3.5/7个USDT平台获得1.5个USDTTron >>> Ethereum 跨链此操作和Ethereum >>> Tron跨链操作流程一样跨链手续费同样在桥治理合约中分发一开始只有两条链上的USDT很少，后期会随着交易量增长用户兑换时可检查目标链上是否有充足的币来决定是否发起交易如果用户一时提取不到资产，待桥核心合约有足够的资金再重试跨链桥特点同一笔跨链交易需按顺序执行：A链转入、投票、B链转出需要2/3的中继节点投票，确保每一笔交易都是有效的不同跨链交易可异步执行，桥链的TPS高，速度快使用相对廉价的链合约来处理投票过程，安全性高，透明度高惩罚机制，提高相同的稳定性多链多币支持，拓展性强平台只是审查中继节点列表，跨链交易由中继节点共识投票确认，去中心化程度高
与以太坊的账户模型不同，比特币是没有余额概念的，使用的是UTXO模型。所以在比特币网络中，存储比特币的是交易输出，准确点说就是未使用过的交易输出。UTXOUTXO是Unspent Transaction Outputs的缩写（Transaction可简写为TX），即未使用过的交易输出。比特币节点会追踪UTXO集合，从而确定哪些代币未被花费，以及哪些人可以花费它们。最初的比特币是通过铸币交易创造的，它是用来奖励矿工的一种特殊交易，因为是凭空创造的，所以它没有输入的。新的UTXO是通过花费现有UTXO创建的，UTXO包含了接收地址和金额。每个比特币交易都由输入和输出组成。输入花费已有UTXO，输出则创建新的UTXO，UTXO只要被用一次，就不存在“未花费”了，以后就不会再被引用，这样就解决双花（双重支付）问题。当我们要使用一个UTXO时，需要对UTXO进行解锁（一般是用私钥对UTXO进行签名），虽然UTXO是公开的，但是如果没有私钥去解锁，则不能花费UTXO。交易结构交易是比特币的核心数据结构，包括区块在内的数据结构都是在为交易服务。交易主要由未花费的输入和输出组成，输出总金额不能大于输入的总金额，多余的部分为交易矿工费有三种类型交易铸币交易（Generation TX），有额外的Coinbase字段，矿工可以随意填写一些信息，因为是挖矿所得，所以这种交易类型没有输入，输出数量就是矿工所得，地址是矿工的地址。普通交易（Pubkey Hash TX），转账交易，一个或多个输入，一个或两个输出，第一个输出称为支付，第二个输出称为找零脚本交易（Script Hash TX），一般为多签脚本，为普通交易升级版本，可以生成灵活的锁定脚本，然后使用特定的解锁脚本去花费比特币字段表数据项大小(Byte)数据类型描述nVersion4int32_t交易版本vin不定CTxIn列表交易输入vout不定CTxOut列表交易输出nLockTime4uint32_t交易锁定时间交易输入CTxIn数据项大小(Byte)数据类型描述prevout不定COutPoint上一个交易的输出，由交易哈希和索引值确定scriptSig不定char[]解锁脚本nSequence4uint32_t序列号，可用于相对时间锁定交易输出CTxOut数据项大小(Byte)数据类型描述nValue8int64_t交易输出金额，单位为SatoshisscriptPubKey不定char[]锁定脚本，定义花费须满足的条件创币交易CoinbaseTransaction每一个区块内包含的第一条交易为CoinbaseTransaction，它作为对挖出该区块的矿工的比特币奖励交易。没有交易输入交易哈希为0输出的索引值固定，为0xffffffffBIP34规定增加一个4字节的height的字段，这个参数是必须的源码参考代码路径：https://github.com/bitcoin/bitcoin/blob/master/src/primitives/transaction.hCOutPut/** An outpoint - a combination of a transaction hash and an index n into its vout * ** 一个交易哈希值与输出下标的集合 */ class COutPoint { public: uint256 hash; //交易哈西 uint32_t n; //对应序列号 COutPoint(): n((uint32_t) -1) { } COutPoint(const uint256& hashIn, uint32_t nIn): hash(hashIn), n(nIn) { } ADD_SERIALIZE_METHODS; //用来序列化数据结构，方便存储和传输 template <typename Stream, typename Operation> inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(hash); READWRITE(n); } void SetNull() { hash.SetNull(); n = (uint32_t) -1; } bool IsNull() const { return (hash.IsNull() && n == (uint32_t) -1); } //小于号<重载函数 friend bool operator<(const COutPoint& a, const COutPoint& b) { int cmp = a.hash.Compare(b.hash); return cmp < 0 || (cmp == 0 && a.n < b.n); } //==重载函数 friend bool operator==(const COutPoint& a, const COutPoint& b) { return (a.hash == b.hash && a.n == b.n); } //!=重载函数 friend bool operator!=(const COutPoint& a, const COutPoint& b) { return !(a == b); } std::string ToString() const; };CTxIn/** An input of a transaction. It contains the location of the previous * transaction's output that it claims and a signature that matches the * output's public key. * **交易的输入，包括当前输入所对应上一笔交易的输出位置， *并且还包括上一笔输出所需要的签名脚本 */ class CTxIn { public: COutPoint prevout; //上一笔交易输出位置 CScript scriptSig; //解锁脚本 uint32_t nSequence; /**序列号，可用于交易的锁定 nSequence字段的设计初心是想让交易能在在内存中修改，可惜后面从未运用过 对于具有nLocktime或CHECKLOCKTIMEVERIFY的交易， nSequence值必须设置为小于2^32，以使时间锁定器有效。通常设置为2^32-1 由于BIP-68的激活，新的共识规则适用于任何包含nSequence值小于2^31的输入的交易（bit 1<<31 is not set）。 以编程方式，这意味着如果没有设置最高有效（bit 1<<31），它是一个表示“相对锁定时间”的标志。 否则（bit 1<<31set），nSequence值被保留用于其他用途， 例如启用CHECKLOCKTIMEVERIFY，nLocktime，Opt-In-Replace-By-Fee以及其他未来的新产品。 一笔输入交易，当输入脚本中的nSequence值小于2^31时，就是相对时间锁定的输入交易。 这种交易只有到了相对锁定时间后才生效。例如， 具有30个区块的nSequence相对时间锁的一个输入的交易 只有在从输入中引用的UTXO开始的时间起至少有30个块时才有效。 由于nSequence是每个输入字段，因此交易可能包含任何数量的时间锁定输入， 所有这些都必须具有足够的时间以使交易有效。 */ CScriptWitness scriptWitness; //! Only serialized through CTransaction /* Setting nSequence to this value for every input in a transaction * disables nLockTime. * * 规则1:如果一笔交易中所有的SEQUENCE_FINAL都被赋值了相应的nSequence，那么nLockTime就会被禁用 */ static const uint32_t SEQUENCE_FINAL = 0xffffffff; /* Below flags apply in the context of BIP 68*/ /* If this flag set, CTxIn::nSequence is NOT interpreted as a * relative lock-time. * * 规则2:如果设置了该值，nSequence不被用于相对时间锁定。规则1失效 */ static const uint32_t SEQUENCE_LOCKTIME_DISABLE_FLAG = (1 << 31); /* If CTxIn::nSequence encodes a relative lock-time and this flag * is set, the relative lock-time has units of 512 seconds, * otherwise it specifies blocks with a granularity of 1. * * 规则3：如果规则1有效并且设置了此变量，那么相对锁定时间单位为512秒，否则锁定时间就为1个区块 */ static const uint32_t SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22); /* If CTxIn::nSequence encodes a relative lock-time, this mask is * applied to extract that lock-time from the sequence field. * * 规则4：如果nSequence用于相对时间锁，即规则1有效，那么这个变量就用来从nSequence计算对应的锁定时间 */ static const uint32_t SEQUENCE_LOCKTIME_MASK = 0x0000ffff; /* In order to use the same number of bits to encode roughly the * same wall-clock duration, and because blocks are naturally * limited to occur every 600s on average, the minimum granularity * for time-based relative lock-time is fixed at 512 seconds. * Converting from CTxIn::nSequence to seconds is performed by * multiplying by 512 = 2^9, or equivalently shifting up by * 9 bits. * * 相对时间锁粒度 * 为了使用相同的位数来粗略地编码相同的挂钟时间， * 因为区块的产生限制于每600s产生一个， * 相对时间锁定的最小单位为512是，512 = 2^9 * 所以相对时间锁定的时间转化为相当于当前值左移9位 */ static const int SEQUENCE_LOCKTIME_GRANULARITY = 9; CTxIn() { nSequence = SEQUENCE_FINAL; } explicit CTxIn(COutPoint prevoutIn, CScript scriptSigIn=CScript(), uint32_t nSequenceIn=SEQUENCE_FINAL); CTxIn(uint256 hashPrevTx, uint32_t nOut, CScript scriptSigIn=CScript(), uint32_t nSequenceIn=SEQUENCE_FINAL); ADD_SERIALIZE_METHODS; template <typename Stream, typename Operation> inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(prevout); READWRITE(scriptSig); READWRITE(nSequence); } friend bool operator==(const CTxIn& a, const CTxIn& b) { return (a.prevout == b.prevout && a.scriptSig == b.scriptSig && a.nSequence == b.nSequence); } friend bool operator!=(const CTxIn& a, const CTxIn& b) { return !(a == b); } std::string ToString() const; };CTxOut/** An output of a transaction. It contains the public key that the next input * must be able to sign with to claim it. * **交易输出，包含输出金额和锁定脚本 */ class CTxOut { public: CAmount nValue; //输出金额 CScript scriptPubKey; //锁定脚本 CTxOut() { SetNull(); } CTxOut(const CAmount& nValueIn, CScript scriptPubKeyIn); ADD_SERIALIZE_METHODS; template <typename Stream, typename Operation> inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(nValue); READWRITE(scriptPubKey); } void SetNull() { nValue = -1; scriptPubKey.clear(); } bool IsNull() const { return (nValue == -1); } friend bool operator==(const CTxOut& a, const CTxOut& b) { return (a.nValue == b.nValue && a.scriptPubKey == b.scriptPubKey); } friend bool operator!=(const CTxOut& a, const CTxOut& b) { return !(a == b); } std::string ToString() const; };CTransaction/** The basic transaction that is broadcasted on the network and contained in * blocks. A transaction can contain multiple inputs and outputs. * * ** 基本的交易，就是那些在网络中广播并被最终打包到区块中的数据结构。 * 一个交易可以包含多个交易输入和输出 */ class CTransaction { public: // Default transaction version. static const int32_t CURRENT_VERSION=2; //默认交易版本 // Changing the default transaction version requires a two step process: first // adapting relay policy by bumping MAX_STANDARD_VERSION, and then later date // bumping the default CURRENT_VERSION at which point both CURRENT_VERSION and // MAX_STANDARD_VERSION will be equal. /** 更改默认交易版本需要两个步骤： * 1.首先通过碰撞MAX_STANDARD_VERSION来调整中继策略， * 2.然后在稍后的日期碰撞默认的CURRENT_VERSION * * 最终MAX_STANDARD_VERSION和CURRENT_VERSION会一致 */ static const int32_t MAX_STANDARD_VERSION=2; // The local variables are made const to prevent unintended modification // without updating the cached hash value. However, CTransaction is not // actually immutable; deserialization and assignment are implemented, // and bypass the constness. This is safe, as they update the entire // strcture, including the hash. /** 下面这些变量都被定义为常量类型，从而避免无意识的修改了交易而没有更新缓存的hash值； * 然而CTransaction不是可变的 * 反序列化和分配被执行的时候会绕过常量 * 这才是安全的，因为更新整个结构包括哈希值 */ const std::vector<CTxIn> vin; //交易输入 const std::vector<CTxOut> vout; //交易输出 const int32_t nVersion; //版本 const uint32_t nLockTime; //锁定时间 private: /** Memory only. */ const uint256 hash; uint256 ComputeHash() const; public: /** Construct a CTransaction that qualifies as IsNull() */ CTransaction(); /** Convert a CMutableTransaction into a CTransaction. */ /**可变交易转换为交易*/ CTransaction(const CMutableTransaction &tx); CTransaction(CMutableTransaction &&tx); template <typename Stream> inline void Serialize(Stream& s) const { SerializeTransaction(*this, s); } /** This deserializing constructor is provided instead of an Unserialize method. * Unserialize is not possible, since it would require overwriting const fields. * ** 提供此反序列化构造函数而不是Unserialize方法。 * 反序列化是不可能的，因为它需要覆盖const字段 */ template <typename Stream> CTransaction(deserialize_type, Stream& s) : CTransaction(CMutableTransaction(deserialize, s)) {} bool IsNull() const { return vin.empty() && vout.empty(); } const uint256& GetHash() const { return hash; } // Compute a hash that includes both transaction and witness data uint256 GetWitnessHash() const; //计算包含交易和witness数据的散列 // Return sum of txouts. CAmount GetValueOut() const; //返回交易出书金额总和 // GetValueIn() is a method on CCoinsViewCache, because // inputs must be known to compute value in. /** * Get the total transaction size in bytes, including witness data. * "Total Size" defined in BIP141 and BIP144. * @return Total transaction size in bytes */ unsigned int GetTotalSize() const; // 返回交易大小 bool IsCoinBase() const //判断是否是创币交易 { return (vin.size() == 1 && vin[0].prevout.IsNull()); } friend bool operator==(const CTransaction& a, const CTransaction& b) { return a.hash == b.hash; } friend bool operator!=(const CTransaction& a, const CTransaction& b) { return a.hash != b.hash; } std::string ToString() const; bool HasWitness() const { for (size_t i = 0; i < vin.size(); i++) { if (!vin[i].scriptWitness.IsNull()) { return true; } } return false; } };