tp钱包下载官网|uniswap合约地址

作者: tp钱包下载官网
2024-03-16 09:24:22

手把手教你部署自己的uniswap交易所 - 知乎

手把手教你部署自己的uniswap交易所 - 知乎首发于登链社区切换模式写文章登录/注册手把手教你部署自己的uniswap交易所登链社区区块链技术爱好者的家园本文作者:崔棉大师演示地址: https://fankouzu.github.io/cuiswap/ (请在测试网使用)准备Uniswap合约源码源码结构Uniswap在Github上面开源了全部合约代码,其中包括核心合约,周边合约两部分.Uniswap还开源了前端代码,前端代码使用React开发核心合约周边合约前端代码在Uniswap的核心代码中,主要包含3个合约:工厂合约,配对合约,ERC20合约.其中配对合约继承了ERC20合约,我们可以把它们看作一个合约.工厂合约通过create2方法部署配对合约,所以在部署合约时只需要部署工厂合约.周边合约中包括一些示例代码,例如价格预言机,闪电交换,其中最重要的是路由合约.在周边合约的代码库中,包含两个路由合约:UnsiwapV2Router01,UnsiwapV2Router02.工厂合约和配对合约需要通过路由合约调用才能更好的完成交易所的全部功能,所以我们还要部署路由合约两个合约大部分相同,有小部分不同,如果将两个合约的差异化合并成一个合约,部署的时候将会出现out of gas,所以才被分成了两个合约.常用功能两个合约中都包括,所以我们部署其中任意一个路由合约都可以继承引用调用ERC20合约配对合约工厂合约路由合约从浏览器中下载合约源码如果你对合约代码并不熟悉,也可以跳过上面这部分,接下来我们将从以太坊浏览器中直接拷贝线上版合约源码工厂合约路由合约01[可选]路由合约02部署合约准备部署账户Uniswap的路由合约部署在以太坊的主网和Ropsten,Rinkeby,Goerli,Kovan几个测试网的合约地址都是相同的,这样可以使Uniswap的前端不管切换到任何一个网络,路由地址都不会变.要想实现这个相同地址的部署,我们需要准备一个全新的账户用来部署合约.全新的账户指的是在部署合约之前的nonce值为0.因为合约的地址是根据你的账户地址和nonce值计算出来的,所以在不同网络中,如果nonce值相同,部署出的合约地址也相同.通过助记词生成新账户可以通过我之前录制的视频学习操作方法B站视频油管视频生成好助记词之后,记得用英文助记词,保存好助记词,还有助记词对应的账户地址向新地址转帐ETH部署合约需要的gas费约为0.18个Ether,目前主网可能需要的更多.通过一个已有Ether的账户向新账户转帐.测试网的Ether可以通过每个测试网的水龙头申请到测试币.获取测试币方法转账完成后,将助记词导入到Metamask中准备WETH合约地址在部署路由合约时,构造函数中需要填入工厂合约的地址和WETH合约的地址,由于WETH合约的地址在主网和测试网的地址都不相同,所以需要找到每个网络中WETH合约的地址.WETH合约用于将Eth交换为erc20的Eth,由于Eth不是erc20的token,所以我们必须使用WETH作为交换媒介{

mainnet:'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',

ropsten:'0xc778417E063141139Fce010982780140Aa0cD5Ab',

rinkeby:'0xc778417E063141139Fce010982780140Aa0cD5Ab',

goerli:'0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',

kovan:'0xd0A1E359811322d97991E03f863a0C30C2cF029C'

}申请infuraKey在部署合约之前,我们还需要使用infura作为免费节点,所以需要申请一个infuraKey申请地址:infura.io申请方法使用remix部署合约将工厂合约和路由合约的线上版本导入到remix中,在编译合约的选项中,EVM VERSION选择istanbul,COMPILER CONFIGURATION中选择Enable optimization部署顺序和构造函数部署工厂合约构造函数的参数是一个自己常用的账户地址部署路由合约01 [可选], 02构造函数的参数1是工厂合约的地址参数2 是当前网络中WETH合约的地址,参考前文部署Uniswap前端克隆前端代码在项目目录运行命令:$ git clone https://github.com/Uniswap/uniswap-interface.git安装依赖库在项目目录运行命令:$ cd uniswap-interface

$ yarn安装完成后,可以先测试运行一下,在uniswap-interface目录运行命令$ yarn start如果运行成功,将会打开一个浏览器,同时打开Uniswap的前端界面修改路由地址在Uniswap的前端中以常量的形式定义了Uniswap的路由地址,我们只需要修改路由地址就可以让前端链接到你的路由合约中修改文件: 项目目录/uniswap-interface/src/constants/index.ts 第6行import { AbstractConnector } from '@web3-react/abstract-connector'

import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'

import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'

export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' //修改成你的路由合约地址

......保存后运行yarn start即可看到效果将代码部署到GitHub Pages创建GitHub项目创建项目的方法就不在这里讲了,不会的同学可以去搜索一下将前端代码添加到GitHub项目仓库首先要删除原先Uniswap项目中的.git目录,在项目目录运行命令:$ cd uniswap-interface

$ rm -rf .git然后初始化git,并将Unsiwap前端代码添加到自己的项目仓库中git init

git remote add origin https://github.com/用户名/项目名.git安装并部署gh-pages我们将通过gh-pages模块将前端代码部署到github.io,在前端代码的目录运行:$ yarn add gh-pages接下来要编译react和部署gh-pages,在前端代码的目录运行:$ yarn build修改前端代码目录中的package.json$ vim package.json

{

"name": "@uniswap/interface",

"description": "Uniswap Interface",

"homepage": "https://用户名.github.io/项目名称",//修改这里

......

// 添加部署的脚本,还是在package.json中

......

"scripts": {

......

"deploy": "gh-pages -d build" //添加这一行

},保存退出之后,在前端代码的目录运行:$ git add .

$ git commit -m "first commit"

$ git push

$ yarn deploy现在在浏览器中打开https://用户名.github.io/项目名称/index.html就可以打开自己的交易所啦.如果不输入地址结尾的index.html在项目刚部署之后会报错,过一段时间就可以不输入了.扩展部署自己的weth可以将以太坊浏览器中的weth源码拷贝下来,自己部署一个属于自己的weth合约可信token列表Uniswap有一个自己的可信token列表,同样被设置在项目目录/uniswap-interface/src/constants/index.ts文件中,在最后一行就是.你可以将这个链接地址的文件拷贝下来,设置成自己需要的可信token地址列表,然后上传到github目录中,再修改index.ts文件中的链接地址,这样就可以让你自己的交易所中拥有自己设置的可信token列表了原文链接:https://learnblockchain.cn...作者主页:https://learnblockchain.cn... ,欢迎阅读作者更多好文登链社区-区块链技术爱好者的家园发布于 2020-09-02 15:19交易所区块链技术​赞同 16​​2 条评论​分享​喜欢​收藏​申请转载​文章被以下专栏收录登链社区高质量区块链技术文

Uniswap-v2 合约概览 | ethereum.org

wap-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 V3 Book 中文版

| Uniswap V3 Book 中文版

Uniswap V3 Book 中文版Milestone 0. 简介交易市场简介恒定函数做市商(CFMM)Uniswap V3开发环境Milestone 1. 第一笔交易简介计算流动性提供流动性第一笔交易管理合约部署合约用户界面Milestone 2. 第二笔交易简介输出金额计算Solidity中的数学运算Tick Bitmap Index通用mint通用swap报价合约用户界面Milestone 3. 跨tick交易简介不同价格区间跨tick交易滑点保护流动性计算关于定点数的拓展闪电贷用户界面Milestone 4. 多池子交易简介工厂合约交易路径多池子交易用户界面Tick 舍入Milestone 5. 费率和价格预言机简介交易费率闪电贷费率协议费率价格预言机用户界面Milestone 6: NFT positions简介ERC721 概述NFT 管理员合约NFT 渲染器补充资料中英名词对照

部署合约

部署选择本地网络运行本地区块链第一次部署与合约交互,ABIToken余额现在的 tick 和价格ABI

\[ \]部署

#我们的第一版合约已经完成了。现在,让我们来看看如何把它部署在一个本地以太坊网络上,方便我们之后用前端 app 交互。选择本地网络

#智能合约的开发需要运行一个本地的网络来在开发过程中进行部署和测试。这样的网络需要具有以下特点:真实的区块链。它必须是一个真实的区块链网络而不是一个模拟器,我们希望我们的合约如果能够在这样的网络上正常工作,那也一定能在主网上正常工作;速度。我们希望我们的交易能够快速被执行,这样我们能快速迭代;以太币。为了支付gas费,我们需要一些eth,因此我们希望这个网络能够允许我们生成任意数量的eth;cheat code。除了提供标准的 API,我们还希望这个网络能让我们做更多的事,例如:在任何地址上部署合约,以任何地址执行交易,直接修改合约状态等等。今天,有许多的工具能够提供这样的功能:Truffle套件中的GanacheHardhat,一套智能合约开发环境,除了包含本地网络节点以外还有很多有用的工具Foundry中的Anvil所有这些解决方案都能够满足我们的需求。尽管如此,项目现在都逐渐从 Ganache(最早的解决方案)迁移到 Hardhat(目前使用最广的方案),而 Foundry 也成为开发者的新宠。Foundry 也是上述三个方案中唯一使用 Solidity 来编写测试的框架(其他框架都使用 JavaScript)。除此以外,Foundry 还允许使用 Solidity 来编写部署脚本。因此,由于我们想一直使用 Solidity,我们会使用 Anvil 来运行一个本地区块链,并且使用 Solidity 编写部署脚本。运行本地区块链

#Anvil 不需要进行配置,我们可以直接在命令行运行:$ anvil

_ _

(_) | |

__ _ _ __ __ __ _ | |

/ _` | | '_ \ \ \ / / | | | |

| (_| | | | | | \ V / | | | |

\__,_| |_| |_| \_/ |_| |_|

0.1.0 (d89f6af 2022-06-24T00:15:17.897682Z)

https://github.com/foundry-rs/foundry

...

Listening on 127.0.0.1:8545

Anvil运行一个以太坊节点,所以它实际上并不是个网络,但也没什么问题。默认配置下它会创建 10 个账户,每个有 10000 ETH。它会把这些账户和对应私钥打印在命令行,我们会使用其中一个来部署合约和与其交互。Anvil在 127.0.0.1:8545 开放了 JSON-RPC API 接口——这个接口是与以太坊节点交互的主要方式。你可以在这里找到完整的 API 文档。在这里,你可以用 curl 与其交互:$ curl -X POST -H 'Content-Type: application/json' \

--data '{"id":1,"jsonrpc":"2.0","method":"eth_chainId"}' \

http://127.0.0.1:8545

{"jsonrpc":"2.0","id":1,"result":"0x7a69"}

$ curl -X POST -H 'Content-Type: application/json' \

--data '{"id":1,"jsonrpc":"2.0","method":"eth_getBalance","params":["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","latest"]}' \

http://127.0.0.1:8545

{"jsonrpc":"2.0","id":1,"result":"0x21e19e0c9bab2400000"}

你也可以使用 cast(foundry 中的另一个组件)来访问:$ cast chain-id

31337

$ cast balance 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

10000000000000000000000

现在,我们来将池子合约和管理合约都部署在本地网络上。第一次部署

#根本上来讲,部署一个合约意味着:将源代码编译成 EVM 字节码发送一个包含这些字节码的交易新建一个地址,执行字节码中构造函数的部分,将初始化的字节码存放在该地址。这一步是由以太坊节点自动完成的,在这笔交易被打包上链时。部署通常包含很多个步骤:准备参数,部署辅助合约,部署主合约,初始化合约等等。脚本能帮助我们自动化完成这些步骤,并且我们现在能用 Solidity 来编写脚本!创建 scripts/DeployDevelopment.sol 文件,写入以下内容:// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.14;

import "forge-std/Script.sol";

contract DeployDevelopment is Script {

function run() public {

...

}

}

它看起来与测试合约十分相似,唯一的差别在于它继承自 Script 合约而不是 Test。并且按照惯例,我们需要定义一个 run 函数作为部署脚本的主体。在 run 函数中,我们首先定义部署需要的参数:uint256 wethBalance = 1 ether;

uint256 usdcBalance = 5042 ether;

int24 currentTick = 85176;

uint160 currentSqrtP = 5602277097478614198912276234240;

这些正是我们之前用过的值。这里我们需要铸造 5042 个 USDC——其中 5000 个用来提供流动性,42 个用来交易。接下来,我们定义一系列在部署过程中需要执行的交易(每一步都是独立的交易)。我们使用startBroadcast/endBroadcast这个 cheat code:vm.startBroadcast();

...

vm.stopBroadcast();

这些 cheat code 可以在 forge的文档中找到,我们是从继承的 forge-std/Script.sol 里面得到这些功能的。在 broadcast() cheat code后面,或者 startBroadcast()/stopBroadcast() 之间的所有语句都会被转化成交易,这些交易会被发送到执行这个脚本的节点。在这两个 cheat code 之间,我们开始真正的部署步骤。首先需要部署两种 token:ERC20Mintable token0 = new ERC20Mintable("Wrapped Ether", "WETH", 18);

ERC20Mintable token1 = new ERC20Mintable("USD Coin", "USDC", 18);

没有 token 我们就无法部署池子,因此需要先部署 token 合约。

(译者注:由于原生代币 ETH 没有 approve 功能,因此在这里使用的是 WETH。在各种金融协议中,原生 ETH 通常都被单独拿出来处理)由于我们是在本地网络上进行部署,我们需要自行部署对应的 token。在主网上和公开测试网上(Ropsten, Goerli, Sepolia),这些 token 已经被部署。因此,如果想要在这些网络上进行部署,我们需要写网络特定的部署脚本。接下来就是部署池子合约:UniswapV3Pool pool = new UniswapV3Pool(

address(token0),

address(token1),

currentSqrtP,

currentTick

);

然后部署管理合约:UniswapV3Manager manager = new UniswapV3Manager();

最后,我们给我们自己的地址铸造一些token用来之后交易:token0.mint(msg.sender, wethBalance);

token1.mint(msg.sender, usdcBalance);

在Foundry脚本中,msg.sender 是在 broadcast 块中发送交易的地址。我们可以在运行脚本的时候对其进行设置。在脚本的最后,使用 console.log 打印出对应的地址信息:console.log("WETH address", address(token0));

console.log("USDC address", address(token1));

console.log("Pool address", address(pool));

console.log("Manager address", address(manager));

现在我们来运行这个脚本(确保 Anvil 在另一个窗口中正在运行):$ forge script scripts/DeployDevelopment.s.sol --broadcast --fork-url http://localhost:8545 --private-key $PRIVATE_KEY

--broadcast 启动交易的广播。它并不是默认开启的因为并不是每一个脚本都需要发送交易。--fork-url 设置我们要发送交易的节点地址。--private-key 设置调用者的钱包:需要私钥来签名交易。我们可以选择之前 Anvil 启动时在命令行打印出来的任何一个私钥。作者选择了第一个地址和私钥。部署需要消耗几秒钟。最后,你会看到它发送了一系列的交易。它同时也将交易收据存在了 broadcast 文件夹。在 Anvil 运行的窗口里,你也能看到很多行例如 eth_sendRawTransaction, eth_getTransactionByHash,

和 eth_getTransactionReceipt 这样的信息——在你向 Anvil 发送交易后,Forge 使用 JSON-RPC API 来检查它们的状态并且获得交易执行的结果(收据)。恭喜!你刚刚成功部署了一个智能合约!与合约交互,ABI

#现在,让我们来看看我们如何与已经部署的合约交互。每个合约都由一系列的public函数开放。以池子合约为例,包含 mint(...) 和 swap(...)。除此之外,Solidity 为每一个 public 的变量创建了 getter,所以我们也可以调用 token0(),token1(),positions() 等等,来访问对应变量的值。然而,由于合约都被编译成字节码,函数名在编译过程中丢失并且不存储在区块链上。在链上,每个函数都用一个函数选择器(selector)来表示,即函数签名哈希的前 4 字节。伪代码为:hash("transfer(address,address,uint256)")[0:4]

EVM 使用的是 Keccak 哈希算法,标准化名字为 SHA-3。特别地,Solidity 中的哈希函数名字为keccak256。根据以上信息,我们展示两种对于部署的合约进行调用的方法:一种使用 curl 来进行底层的调用,一种使用 cast。Token余额

#我们来检查一下部署者地址种的WETH余额。这个函数的签名是 balanceOf(address)(在 ERC-20 标准中定义),为了计算这个函数的 ID(即选择器),我们计算哈希并取前 4 字节:$ cast keccak "balanceOf(address)"| cut -b 1-10

0x70a08231

要把地址作为参数传递进去,我们把它附在函数选择器的后面(在左边 padding 到 32 个字节,因为地址在函数调用中占 32 字节):0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 是我们要查看余额的地址。这是本书选择的部署者的地址,也是 Anvil 创建时的第一个地址。接下来,我们会使用 eth_call JSON-RPC 方法来进行这个调用。注意到,这一步不需要发送一个交易——我们仅仅是从合约中读取数据。$ params='{"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0xe7f1725e7734ce288f8367e1bb143e90bb3f0512","data":"0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"}'

$ curl -X POST -H 'Content-Type: application/json' \

--data '{"id":1,"jsonrpc":"2.0","method":"eth_call","params":['"$params"',"latest"]}' \

http://127.0.0.1:8545

{"jsonrpc":"2.0","id":1,"result":"0x00000000000000000000000000000000000000000000011153ce5e56cf880000"}

“to” 地址是 USDC token 的地址,它是在上一步部署脚本运行时打印出的日志里面的地址。以太坊节点返回的结果是字节流,为了理解结果我们需要知道返回值的类型。在 balanceOf 函数中,返回值的类型是 uint256。用 cast 可以把结果转换成十进制然后转换成 ethers 单位:$ cast --to-dec 0x00000000000000000000000000000000000000000000011153ce5e56cf880000| cast --from-wei

5042.000000000000000000

余额是正确的,我们在自己的地址中有 5042 USDC。现在的 tick 和价格

#上述例子展示了底层的合约调用。通常来说,你永远不会使用 curl 来发起调用,而是使用某个更友好的工具或库。cast 在这里也能够帮助我们简化这一过程。我们来使用 cast 获得当前合约的 tick 和 price:$ cast call POOL_ADDRESS "slot0()"| xargs cast --abi-decode "a()(uint160,int24)"

5602277097478614198912276234240

85176

如此容易!第一个值就是我们的 $\sqrt{P}$,第二个值就是现在的 tick。由于 --abi-decode 需要完整的函数签名,我们必须声明一个函数名 “a()",尽管我们只是为了解码函数的输出。ABI

#为了简化与合约的交易,Solidity 编译器可以输出 ABI,Application Binary Interface(应用二进制接口)。ABI 是一个包含了合约中所有 public 方法和事件的 JSON 文件。文件的目的在于使得编码函数参数和解码函数输出都更加容易。我们可以通过 Forge 来获取 ABI:$ forge inspect UniswapV3Pool abi

可以看一看生成的文件,来更好地理解 ABI 意味着什么。部署选择本地网络运行本地区块链第一次部署与合约交互,ABIToken余额现在的 tick 和价格

[Uniswap v2 合约代码解析] Uniswap v2 Pair合约代码 - 知乎

[Uniswap v2 合约代码解析] Uniswap v2 Pair合约代码 - 知乎切换模式写文章登录/注册[Uniswap v2 合约代码解析] Uniswap v2 Pair合约代码落花1. 概述Uniswap作为一个去中心化的交易所,它舍弃了传统订单薄的撮合方式,采用流动池加恒定乘积公式算法( x*y=k )为不同加密资产提供即时报价和兑换服务。Uniswap会为每一对token生成一个交易对,每一个交易对就是UniswapV2Pair部署上去产生的。UniswapV2Pair合约在Uniswap V2的核心合约代码库中,本文将会参照v2的白皮书来解读UniswapV2Pair合约。 2. UniswapV2Pair合约继承关系UniswapV2Pair合约有一个接口合约IUniswapV2Pair定义了UniswapV2Pair所要实现的方法,除此之外还继承了之前文章所分析的UniswapV2ERC20合约,这就说明这个Pair合约实际上就是一个token。3. UniswapV2Pair合约代码解析3.1 类型方法增强首先是将SafeMath和UQ112x112的库方法给到对应的类型上。using SafeMath for uint;

using UQ112x112 for uint224;

为什么要赋予unit224类型新的方法?这是因为在Solidity里面没有非整型的类型,但是token的数量肯定会出现小数位,使用UQ112x112的库去模拟浮点数类型。将unit224中的112位当作浮点数的整数部分,另外112位当作浮点数的小数部分,这样的话其精度可以达到 \frac{1}{2^{112}} , 剩余的32位主要是存放blockTimestampLast变量,该变量的作用以及更新会在后面讲到。library UQ112x112 {

uint224 constant Q112 = 2**112;

// encode a uint112 as a UQ112x112

function encode(uint112 y) internal pure returns (uint224 z) {

z = uint224(y) * Q112; // never overflows

}

// divide a UQ112x112 by a uint112, returning a UQ112x112

function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {

z = x / uint224(y);

}

}

3.2 常量最小流动性定义 最小流动性的定义是1000,具体细节我会在后面铸币方法的解析中提到,为什么是1000在白皮书中也提到了,具体细节可以查看v2的白皮书中的3.4节。uint public constant MINIMUM_LIQUIDITY = 10**3;

SELECTORSELECTOR常量值为transfer(address,unit256)字符串哈希值的前4个字节,这个用于直接使用call方法调用token的转账方法。bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

工厂地址 因为pair合约是通过工厂合约进行部署的,所有会有一个变量专门存放工厂合约的地址。address public factory;

token地址 pair合约的含义,就是一对token,所有在合约中会存放两个token的地址,便于调用。address public token0;

address public token1;

储备量相关储备量是当前pair合约所持有的token数量,blockTimestampLast主要用于判断是不是区块的第一笔交易。reserve0、reserve1和blockTimestampLast三者的位数加起来正好是unit的位数。uint112 private reserve0;

uint112 private reserve1;

uint32 private blockTimestampLast;

价格最后累计这里的价格最后累计,是用于Uniswap v2所提供的价格预言机上,该数值会在每个区块的第一笔交易进行更新。uint public price0CumulativeLast;

uint public price1CumulativeLast;

k值kLast这个变量在没有开启收费的时候,是等于0的,只有当开启平台收费的时候,这个值才等于k值,因为一般开启平台收费,那么k值就不会一直等于两个储备量向乘的结果来。uint public kLast;

3.3 事件铸造事件event Mint(address indexed sender, uint amount0, uint amount1);

销毁事件event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);

交换事件event Swap(

address indexed sender,

uint amount0In,

uint amount1In,

uint amount0Out,

uint amount1Out,

address indexed to

);

同步事件event Sync(uint112 reserve0, uint112 reserve1);

3.4 修饰方法在pair合约里面定义了一个修饰方法,主要是防止重入攻击的。// 锁定变量,防止重入

uint private unlocked = 1;

/*

* @dev 修饰方法,锁定运行防止重入

*/

modifier lock() {

require(unlocked == 1, 'UniswapV2: LOCKED');

unlocked = 0;

_;

unlocked = 1;

}

3.5 方法3.5.1 构造方法构造方法比较简单,直接给factory变量赋予msg.sender的值,为什么要这样赋值,因为pair合约是通过factory合约进行部署的,所以msg.sender的值就等于工厂合约的地址。constructor() public {

factory = msg.sender;

}

3.5.2 初始化方法initialze方法是Solidity中一个比较特殊的方法,它仅仅只有在合约创建之后调用一次,为什么使用initialze方法初始化pair合约而不是在构造函数中初始化,这是因为pair合约是通过create2部署的,create2部署合约的特点就在于部署合约的地址是可预测的,并且后一次部署的合约可以把前一次部署的合约给覆盖,这样可以实现合约的升级。如果想要实现升级,就需要构造函数不能有任何参数,这样才能让每次部署的地址都保持一致,具体细节可以查看create2的文档。 在这个initialize方法中,主要是将两个token的地址分别赋予。function initialize(address _token0, address _token1) external {

// 确认调用者为工厂地址

require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check

token0 = _token0;

token1 = _token1;

}

3.5.3 外部调用方法获取储备量方法返回三个信息,token0的储备量,token1的储备量,blockTimestampLast:上一个区块的时间戳。function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {

_reserve0 = reserve0;

_reserve1 = reserve1;

_blockTimestampLast = blockTimestampLast;

}

铸币方法mint函数的输入为一个地址to,输出为该地址所提供的流动性,在Uniswap中,流动性也被体现成token即LP token。 铸币流程发生在router合约向pair合约发送代币之后,因此此次的储备量和合约的token余额是不相等的,中间的差值就是需要铸币的token金额,即amount0和amount1。然后是收取平台手续费,这部分代码会在后面进行解释。然后获取总的流动性的供应量totoalSupply,如果totalSupply等于0的话,就代表是首次铸币。 在Uniswap V2中,首次铸币获取的流动性由以下公式得到s_{minted} = \sqrt {x_{deposited} * y_{deposited}} = \sqrt {k} 这里的k就是恒定乘积公式中的k。这个公式确保了在任意时刻添加流动性时,LP token的价值与初始供应的tokenA和tokenB的比例无关,比如在最开始的时候,一个ABC的价格等于100个XYZ的价格,有人存入2个ABC和200个XYZ,这个时候他会获得20个LP token,这20个LP token的价值为2个ABC加上200个XYZ,如果有人在最开始的时候存入2个ABC和800个XYZ,这个时候他会获得40个LP token。但是我们可以看到代码里面,当第一个人存入token的时候,并没有完全获得对应数量的LP token,有MININUM_LIQUIDITY数量的LP token被转入0地址销毁了。这里是为了防止有人刻意抬高流动性单价从而垄断交易对,使得散户无力参与,即无法停供流动性。 具体的攻击流程如下: 1. 首先发送小额toekn(比如1 wei)到交易对并且mint(),获得价值1 wei的LP token,此时的totalSupply等于1 wei, reserve0和reserve也为1 wei。 2. 然后发送大额token(比如2000 ether)到交易对,但不调用mint(),而是直接调用sync(),此时totalSupply为1 wei, reserve0和reserve1分别为1 wei + 2000 ether。 3. 这个时候,1 wei的LP token的价值就约等于2000 ether了,后来的人再想获得1 wei的流动性,就需要付出价值2000 ether的token了。因此为了防止这种攻击的发生,只有去限制流动性的下限,在首次铸币的时候,会要求铸币者提供大于MININUM_LIQUIDITY数量的流动性,否则就无法注入流动性。并且为了防止攻击者通过burn()将流动性销毁,导致总流动性不低于MININUM_LIQUIDIY的限制被绕过,代码直接在第一次铸币的时候就把首次铸币者应该获得的流动性扣除了MININUM_LIQUIDITY数量的LP token发送到address(0)锁住,依次达到限制。然后我们接着看如果不是第一次铸币的时候,流动性的获取公式如下:s_{minted} = min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1) 根据以上公式,流动性的获取会根据存入的两种token分别计算,然后取最小的那个。之后就判断流动性是否大于0,如果不大于0,就revert整个流程。之后就是将LP token 给到address(to), 更新储备量, 更新储备量这个方法我们会在后面进行解析。之后如果开启了平台手续费收取,那么就会在这里重新计算k值。最后触发铸造事件。/*

* @dev 铸造方法

* @param to to地址

* @return liquidity 流动性

* @notice 应该从执行重要安全检查的合约中调用此底层方法

* this low-level function should be called from a contract which performs important safety checks

*/

function mint(address to) external lock returns (uint liquidity) {

// 获取储备量0和储备量1

(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings

// 获取当前合约在token0合约内的余额

uint balance0 = IERC20(token0).balanceOf(address(this));

// 获取当前合约在token1合约内的余额

uint balance1 = IERC20(token1).balanceOf(address(this));

// amount0 = 余额0 - 储备0

uint amount0 = balance0.sub(_reserve0);

// amount1 = 余额1 - 储备1

uint amount1 = balance1.sub(_reserve1);

// 返回铸造费开关

bool feeOn = _mintFee(_reserve0, _reserve1);

// 获取totalSupply

uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee

// 如果_totalSupply等于0

if (_totalSupply == 0) {

// 流动性 = (数量0 * 数量1)的平方根 - 最小流动性1000

liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);

// 在总量为0的初始状态,永久锁定最低流动性

_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens

} else {

// 流动性 = 最小值(amount0 * _totalSupply / _reserve0 和 (amount1 * _totalSupply) / reserve1)

liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);

}

// 确认流动性 > 0

require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');

// 铸造流动性给to地址

_mint(to, liquidity);

// 更新储备量

_update(balance0, balance1, _reserve0, _reserve1);

// 如果铸造费开关为true,k值 = 储备0 * 储备1

if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date

// 触发铸造事件

emit Mint(msg.sender, amount0, amount1);

}

销毁流动性方法如果流动性的提供者想要收回流动性,那么就需要调用该方法。首先通过getReserves()方法获取现在的储备量。然后获取token0和token1在当前pair合约的余额然后从当前合约的balanceOf获取要销毁的流动性金额,这里为什么是从自身获取,是因为当前合约的余额是流动性提供者通过路由合约发送到pair合约要销毁的金额。计算平台手续费,这部分代码在后面解析。获取totalSupply,然后计算流动性提供者可以取出的token0和token1的数量,数量分别为amount0和amount1。直接通过如下公式计算得到amount0 = liquidity * balance0 / totalSupply amount1 = liquidity * balance1 / totalSUpply 实际上,上述公式可以转换成如下公式,取出来token的数量与持有的流动性占总流动性的比例有关,这样可以将流动性提供者在存入流动性期间所获取的流动性挖矿的收益也取出。amount0 / balance0 = liquidity / totalSupply amount1 / balance1 = liquidity / totalSupply 然后确保取出的amount大于0销毁合约内的流动性数量,发送token给address(to), 更新储备量。如果有平台手续费收取的话,重新计算k值。发送销毁代币事件。/*

* @dev 销毁方法

* @param to to地址

* @return amount0

* @return amount1

* @notice 应该从执行重要安全检查的合同中调用此低级功能

* this low-level function should be called from a contract which performs important safety checks

*/

function burn(address to) external lock returns (uint amount0, uint amount1) {

// 获取储备量0,储备量1

(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings

// 带入变量

address _token0 = token0; // gas savings

address _token1 = token1; // gas savings

// 获取当前合约在token0合约内的余额

uint balance0 = IERC20(_token0).balanceOf(address(this));

// 获取当前合约在token1合约内的余额

uint balance1 = IERC20(_token1).balanceOf(address(this));

// 从当前合约的balanceOf映射中获取当前合约自身流动性数量

// 当前合约的余额是用户通过路由合约发送到pair合约要销毁的金额

uint liquidity = balanceOf[address(this)];

// 返回铸造费开关

bool feeOn = _mintFee(_reserve0, _reserve1);

// 获取totalSupply

uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee

// amount0和amount1是用户能取出来多少的数额

// amount0 = 流动性数量 * 余额0 / totalSupply 使用余额确保按比例分配

// 取出来的时候包含了很多个千分之三的手续费

amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution

// amount1 = 流动性数量 * 余额1 / totalSupply 使用余额确保按比例分配

amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution

// 确认amount0和amount1都大于0

require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');

// 销毁当前合约内的流动性数量

_burn(address(this), liquidity);

// 将amount0数量的_token0发送给to地址

_safeTransfer(_token0, to, amount0);

// 将amount1数量的_toekn1发给to地址

_safeTransfer(_token1, to, amount1);

// 更新balance0和balance1

balance0 = IERC20(_token0).balanceOf(address(this));

balance1 = IERC20(_token1).balanceOf(address(this));

_update(balance0, balance1, _reserve0, _reserve1);

if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date

emit Burn(msg.sender, amount0, amount1, to);

}

交换token方法交换token方法一般通过路由合约调用,功能是交换token,需要的参数包括:amount0Out:token0要交换出的数额;amount1Out:token1要交换出的数额,to:交换token要发到的地址,一般是其它pair合约地址;data用于闪电贷回调使用。首先确认amount0Out或者amount1Out有一个大于0,然后确保储备量大于要取出的金额。然后确保address(to)不等于对应的token地址。然后发送token到对应的地址上。然后data有数据,就执行闪电贷的调用。之后获取两个token的余额,判断是否在交换之间,有token的输入,如果没有输入就revert。如果有输入,还需要保证交换之后的储备量的乘积等于k,具体代码中计算公式如下:(balance0 * 1000 - amount0In * 3) * (balance1 * 1000 - amount1In * 2) = (reserve0 * reserve1) * 1000^2 代码中的公式这样的原因还是因为Solidity不支持小数运算,上述公式可以改写成如下形态:(balance0 - amount0In * 0.003) * (balance1 - amount1In * 0.003) = reserve0 * reserve1 其中 reserve0 * reserve1相当于k值。这个公式就对应白皮书所写的每次交易收取千分之三的手续费。最后更新储备量,触发交换事件。/*

* @dev 交换方法

* @param amount0Out 输出数额0

* @param amount1Out 输出数额1

* @param to to地址

* @param data 用于回调的数据

* @notice 应该从执行重要安全检查的合同中调用此低级功能

* this low-level function should be called from a contract which performs important safety checks

*/

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

// 确认amount0Out和amount1Out都大于0

require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');

// 获取储备量0和储备量1

(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings

// 确认取出的量不能大于它的 储备量

require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

// 初始化变量

uint balance0;

uint balance1;

{ // scope for _token{0,1}, avoids stack too deep errors

// 标记_toekn{0,1}的作用域,避免堆栈太深

address _token0 = token0;

address _token1 = token1;

// 确保to地址不等于token0和token1的地址

require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');

// 发送token0代币

if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens

// 发送token1代币

if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens

// 如果data的长度大于0,调用to地址的接口

// 闪电贷

if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);

// 余额0,1 = 当前合约在token0,1合约内的余额

balance0 = IERC20(_token0).balanceOf(address(this));

balance1 = IERC20(_token1).balanceOf(address(this));

}

// 如果余额0 > 大于储备0 - amount0Out 则 amount0In = 余额0 - (储备0 - amount0Out) 否则amount0In = 0

uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;

uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;

// 确保输入数量0||1大于0

require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');

{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors

// 调整后的余额0 = 余额0 * 1000 - (amount0In * 3)

uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));

// 调整后的余额1 = 余额1 * 1000 - (amount1In * 3)

uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));

// 确保balance0Adjusted * balance1Adjusted >= 储备0 * 储备1 * 1000000

require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');

}

// 更新储备量

_update(balance0, balance1, _reserve0, _reserve1);

// 触发交换事件

emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);

}

skim方法skim方法的功能是强制让余额等于储备量,一般用于储备量溢出的情况下,将多余的余额转出到address(to)上,使余额重新等于储备量。具体的逻辑也非常简单,就是将余额减去储备量的token发送到address(t0)上/*

* @dev 强制平衡以匹配储备,按照储备量匹配余额

* @param to to地址

force balances to match reserves

*/

function skim(address to) external lock {

address _token0 = token0; // gas savings

address _token1 = token1; // gas savings

// 将当前合约在token1,2的余额-储备量0,1安全发送到to地址上

_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));

_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));

}

sync方法 刚才skim方法是强制让余额与储备量对等,sync方法则是强制让储备量与余额对等,直接调用就是更新储备量的私有方法。/*

* @dev 强制准备金与余额匹配,按照余额匹配储备量

* force reserves to match balances

*/

function sync() external lock {

_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);

}

3.5.4 私有方法私有转账方法 该方法实现了只知道token合约地址就可以直接调用transfer方法的功能,具体实现如下,这个方法传入了三个参数,分别是token:合约的地址,to:要转账的地址,value:要转账的金额。然后直接使用call方法直接调用对应token合约的transfer方法,获取返回值,需要判断返回值为true并且返回的data长度为0或者解码后为true。使用call方法的优势在于可以在不知道token合约具体代码的前提下调用其方法。/*

* @dev 私有安全发送

* @param token token地址

* @param to to地址

* @param value 数额

*/

function _safeTransfer(address token, address to, uint value) private {

(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));

require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');

}

私有更新储备量方法私有更新储备量方法主要用于每次添加流动性或者减少流动性之后调用,用于将余额同步给储备量。并且会判断时间流逝,在每一个区块的第一次调用时候,更新价格累加器,用于Uniswap v2的价格预言机。在方法最开始的时候,会判断余额会不会导致储备量溢出,如果溢出的话,就revert,这个时候就需要有人从外部调用skim方法,修正溢出,将多出的token转出。在这个方法里面除了更新reserve0和reserve1之外,还更新了blockTimestampLast,它的取值是当前区块时间对 2^{32} 取余得到的结果。/*

* @dev 更新储备量,并在每一个区块的第一次调用时,更新价格累加器

* @param balance0 余额0

* @param balance1 余额1

* @param _reserve0 储备量0

* @param _reserve1 储备量1

* update reserves and, on the first call per block, price accumulators

*/

function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {

// 确认余额0和余额1小于等于最大的uint112

require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');

// 区块时间戳,将时间戳转换成uint32

uint32 blockTimestamp = uint32(block.timestamp % 2**32);

// 计算时间流逝

uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired

// 如果时间流逝>0,并且储备量0、1不等于0,也就是第一个调用

if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {

// * never overflows, and + overflow is desired

// 价格0最后累计 += 储备量1 * 2**112 / 储备量0 * 时间流逝

price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;

// 价格1最后累计 += 储备量0 * 2**112 / 储备量1 * 时间流逝

price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;

}

// 余额0,1放入储备量0,1

reserve0 = uint112(balance0);

reserve1 = uint112(balance1);

// 更新最后时间戳

blockTimestampLast = blockTimestamp;

// 触发同步事件

emit Sync(reserve0, reserve1);

}

平台手续费收取方法 平台手续费的是否开启的开关是在factory合约中定义的,后续会解析factory合约。如果收取平台收费的话,那么收取的金额为交易手续费的1/6。具体计算方式可以去参考白皮书。_mintFee函数首先获取factory合约里面的address(feeTo), 这是平台收取手续费的地址。然后判断address(feeTo)是否等于address(0)决定是否收取平台手续费。如果开启平台手续费收取,首先会判断kLast是否为0,如果不为0就进行平台手续费的计算。平台手续费的计算,首先要明确一点,因为每一笔交易都会有千分之三的手续费,那么k值也会随着缓慢增加,所有连续两个时刻之间的k值差值就是这段时间的手续费。以上过程可以用如下公式表达:f_{1, 2} = \frac{\sqrt{k_{2}} - \sqrt{k_{1}}}{\sqrt{k_{2}}} 其中 f_{1, 2} 表示当前时刻的平台手续费占当前时刻流动性的比重, {k_1} 和 {k_2}分别表示前一时刻流动性的值和当前时刻流动性的值。此时假设当前时刻的LP token的总供应量是 s_{1},需要发放给address(feeTo)的LP token的量为 s_{m}, 由于LP token不能减发,且发送到address(feeTo)的LP token需要在发送给流动性提供者LP token之前发放,故而需要满足如下等式:\frac{s_{m}}{s_{m} + s_{1}} = \frac{1}{6} * f_{1, 2} 其中 \frac{1}{6} 是平台手续费占交易手续费的比例。由上述两式可推算出 s_{m} 的结果如下:s_{m} = \frac{\sqrt{k_{2}} - \sqrt{k_{1}}}{5 * \sqrt{k_{2}} + \sqrt{k_{1}}} * s_{1} 上述式子,就等价于如下代码中计算平台手续费的过程。/*

* @dev 如果收费,铸造流动性相当于1/6的增长sqrt(k)

* @param _reserve0 储备量0

* @param _reserve1 储备量1

* @return feeOn

* 这一部分可参考白皮书协议费用那部分·

* if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)

*/

function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {

// 查询工厂合约的feeTo变量值

address feeTo = IUniswapV2Factory(factory).feeTo();

// 如果feeTo不等于0地址,feeOn等于true否则false

feeOn = feeTo != address(0);

// 定义k值

uint _kLast = kLast; // gas savings

// 如果feeOn等于true

if (feeOn) {

// 如果k值不等于0

if (_kLast != 0) {

// 计算(_reserve0*_reserve1)的平方根

uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));

// 计算k值的平方根

uint rootKLast = Math.sqrt(_kLast);

// 如果rootK>rootKLast

if (rootK > rootKLast) {

// 分子 = erc20总量 * (rootK - rootKLast)

uint numerator = totalSupply.mul(rootK.sub(rootKLast));

// 分母 = rootK * 5 + rootKLast

uint denominator = rootK.mul(5).add(rootKLast);

// 流动性 = 分子 / 分母

uint liquidity = numerator / denominator;

// 如果流动性 > 0 将流动性铸造给feeTo地址

if (liquidity > 0) _mint(feeTo, liquidity);

}

}

// 否则如果_kLast不等于0

} else if (_kLast != 0) {

kLast = 0;

}}

4. 总结本篇文章解析了Uniswap V2核心合约代码中的pair合约的代码,在实际部署当作pair合约不会直接进行部署,而是通过factory合约进行部署的,接下来会继续解析factory合约的代码。5. Refenceshttps://learnblockchain.cn/article/3004https://learnblockchain.cn/article/2753https://learnblockchain.cn/article/1448发布于 2022-01-09 16:00区块链(Blockchain)去中心化应用(DApp)Uniswap​赞同 19​​添加评论​分享​喜欢​收藏​申请

UniSwap学习笔记1: 概览以及交易对地址计算-腾讯云开发者社区-腾讯云

wap学习笔记1: 概览以及交易对地址计算-腾讯云开发者社区-腾讯云Tiny熊UniSwap学习笔记1: 概览以及交易对地址计算关注作者腾讯云开发者社区文档建议反馈控制台首页学习活动专区工具TVP最新优惠活动文章/答案/技术大牛搜索搜索关闭发布登录/注册首页学习活动专区工具TVP最新优惠活动返回腾讯云官网Tiny熊首页学习活动专区工具TVP最新优惠活动返回腾讯云官网社区首页 >专栏 >UniSwap学习笔记1: 概览以及交易对地址计算UniSwap学习笔记1: 概览以及交易对地址计算Tiny熊关注发布于 2022-05-25 15:58:321.7K1发布于 2022-05-25 15:58:32举报文章被收录于专栏:深入浅出区块链技术深入浅出区块链技术本文作者:tony.ho[1]hello everyone, 我是 tony, 今天开始为大家分享我最近学习 Uniswap[2] 的小小心得, 希望各位看官多多提意见, 大家一起进步.Uniswap V2 代码地址:核心代码: 包括 Factory, Pair, WETH 3 个合约 https://github.com/Uniswap/v2-core外围代码: Router 合约 https://github.com/Uniswap/v2-periphery概览Uniswap core 有 3 个合约: Factory, Pair, WETH Uniswap periphery 有 1 个合约: RouterFactory 合约 (UniswapV2Factory.sol): 负责创建交易对, 保存所有交易对的地址 .Pair 合约 (UniswapV2Pair.sol) : 保存单个交易对的资金池信息, 包括每个交易对中两个 ERC20 代币的地址, 以及各自资金余额(reserve). 处理 mint, burn 和 交易(swap) 操作. Pair 合约是 Uniswap 代码的核心 UniswapV2Pair 本身也是 ERC20, 当添加流动性时会 mint 新的 pair 代币(pair token, or LP token), 删除流动性时会销毁一定数量的代币, 确保 pair token 的 totalSupply 始终等于池子中的两个交易对代币余额的几何平均值: totalSupply of pair token === sqrt(reserve1 * reserve2)WETH 合约: 这个合约是一个特殊的 ERC20, 它的目的是方便用户直接用 ETH 和 ERC20 进行交易, 你可以把 WETH 看作是一个全额担保的债券: 这个合约里面存储的 ETH 总是等于它发行的代币 WETH, (除非有人直接使用 WETH_address.transfer 向它发送了 ETH ). Pair 合约的交易对总是 ERC20 代币, 不能进行 ETH 和 ERC20 之间的交易, 所以 WETH 就被创造出来, 它有两个方法, deposit() 和 withdraw(), 前者是存入 ETH 获得 WETH, 后者是 销毁 WETH 获得 ETH. 但是这两个方法通常是由 Router 来调用的, 例如 Router 中的 swapExactETHForTokens 函数:function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)

external

virtual

override

payable

ensure(deadline)

returns (uint[] memory amounts)

{

require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');

amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);

require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');

IWETH(WETH).deposit{value: amounts[0]}();

assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));

_swap(amounts, path, to);

}

复制上面的代码中, 用户需要用 ETH 换成 ERC20 代币, 那么只能使用 WETH-ERC20 交易对, 用户将 ETH 发送给 Router, Router 获得 ETH 后用它换出 WETH ( 调用 WETH.deposit() ), 再将 WETH 发送给 Pair, 换出目标 ERC20 代币发送给参数 to 中指定的接收账户.Router 合约: (UniswapV2Router02.sol) 这个合约包含了几乎所有提供给应用层使用的函数 (除了 Factory 的 getPair 和 createPair), 包括 <添加,撤销流动性> 和 <交易>, 它和 Pair 的区别如下: Pair 中没有添加, 撤销流动性操作, 只有 mint 和 burn. Pair 中的 swap 操作是单个交易对的操作, 而 Router 的 swap 是可以给定路径, 进行一系列交易后得到最终目标代币 token1 -> token2 ... -> token_target 在此过程中, Router 会依次调用 (token_i, token_i+1) 所对应的交易对的 swap 函数, 并将换出的代币发送到 (token_i+1, token_i+2)交易对合约地址, 再次执行 swap, 发送... 直到换出最后的 token_target在后面的介绍中, 我们会阅读源码, 研究 swap 的流程细节.Pair 的创建 和 create2 的地址计算:交易对的创建是在 Factory 中完成, UniswapV2Factory.ceatePair 函数定义如下:function createPair(address tokenA, address tokenB) external returns (address pair) {

require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');

(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');

require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient

bytes memory bytecode = type(UniswapV2Pair).creationCode;

bytes32 salt = keccak256(abi.encodePacked(token0, token1));

assembly {

pair := create2(0, add(bytecode, 32), mload(bytecode), salt)

}

IUniswapV2Pair(pair).initialize(token0, token1);

getPair[token0][token1] = pair;

getPair[token1][token0] = pair; // populate mapping in the reverse direction

allPairs.push(pair);

emit PairCreated(token0, token1, pair, allPairs.length);

}

复制这里 Factory 使用了 create2 操作码部署新的 Pair 合约, 而不是 pair = new UniswapV2Pair(tokenA, tokenB):assembly {

pair := create2(0, add(bytecode, 32), mload(bytecode), salt)

}

复制这样做的好处是: 它可以得到一个确定的 pair 地址, 使得 Router 中就可以通过 tokenA, tokenB 计算出 pair 地址, 不再需要执行一次 Factory.getPair(tokenA, tokenB) 的跨合约调用, create2 生成地址的规则是:keccak256(abi.encodePacked(

hex'ff',

deployer_address, // 由Factory创建, 这里就是 factory合约地址

salt, // salt 是创建者自定义的数据, 避免重复, 这里的

// salt = keccak256(abi.encodePacked(token0, token1)), 且 token0 < token1

keccak256(type(Contract_to_Deploy).creationCode) // 此处 Contract_to_Deploy = UniswapV2Pair

))))

复制参考 periphery 的 libraries/UniswapV2Library.solfunction pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {

(address token0, address token1) = sortTokens(tokenA, tokenB);

pair = address(uint(keccak256(abi.encodePacked(

hex'ff',

factory,

keccak256(abi.encodePacked(token0, token1)),

hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'

))));

}

复制在这里, Router 为了节省 gas, 使用 pairFor 获取 tokenA 和 tokenB 的交易对地址, 而且最后的字节码哈希也是预先计算出来, 硬编码到代码里面.如果我们自己修改了 Pair 的源代码合约, 重新部署后, Router 就无法找到这个 Pair 地址了, 解决办法要么直接返回 factory.getPair(tokenA, tokenB), 要么就把以前部署的 Pair 全部舍弃, 重新部署 Factory 和所有 Pair, 再重新部署 Router.下面的 python 示例代码用于计算 pair 的 hash, 我们可以在重新部署 Pair 后调用这个函数, 再把 hash 复制到 UniswapV2Library.pairFor() 进行替换: 同时修改 UniswapV2Factory.sol , 增加两个函数用于测试:contract UniswapV2Factory is IUniswapV2Factory {

...

/*

* 返回 Pair的字节码

*/

function pair_code() public view returns(bytes memory){

return type(UniswapV2Pair).creationCode;

}

/*

* 返回 Pair的字节码哈希

*/

function pair_codehash() public view returns(bytes32 ){

return keccak256( type(UniswapV2Pair).creationCode );

}

...

}

复制以下是 python 代码, 用于计算 pair 的 code_hash, 并且模拟 create2 计算交易对地址, 如果与 factory.getPair 得到的地址相同, 证明我们的 code_hash 计算正确, 可以拷贝到 UniswapV2Library.sol 的 pairFor 函数###########################################################################################

#

# 重新编译并部署 pair 和 Factory 后, 调用此函数计算出hash, 放到 peripery/libraries/UniswapV2Library.sol 的

# 'pairFor' 函数, 替换原来的hash,

# 然后重新编译并部署Router

#

###########################################################################################

def calc_pair_address():

# 执行本函数前, 确保已经重新编译并部署 Factory 和 Pair, 同时部署测试代币 WBTC, USDT及交易对

# 执行完本函数后, 需要修改 Router源代码, 重新编译部署Router

abi, bytecode = _helper.get_contract_info(r"core/artifacts/UniswapV2Pair.json")

WBTC, USDT = _contracts["WBTC"], _contracts["USDT"]

factory = _contracts["Factory"]

pair_codehash = Web3.keccak(bytes.fromhex(bytecode))

addr1, addr2 = sorted((WBTC.address, USDT.address))

salt = Web3.solidityKeccak(['address' , 'address'], [addr1, addr2])

pair_address = Web3.solidityKeccak(['uint8', 'address' , 'bytes', 'bytes'],

[0xff,

factory.address,

salt,

pair_codehash])

print(f"pair code(python): 0x{bytecode}", )

evm_code = factory.functions.pair_code().call()

print(f"pair code (evm): 0x{evm_code.hex()}")

print(f"code hash(python): \n\t{pair_codehash.hex()}", )

evm_codehash = factory.functions.pair_codehash().call()

print(f"code hash(evm): \n\t0x{evm_codehash.hex()}")

print(f"pair address of WBTC-USDT(calculated from python): \n\t{pair_address.hex()}")

evm_pair_address = factory.functions.getPair(WBTC.address, USDT.address).call()

print(f"pair address of WBTC-USDT(in EVM create2): \n\t{evm_pair_address}")

复制得到以下结果, 证明我们的 python 端计算 pair 地址的方法是正确的:/*

* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释

* pair code(python): 0x60806040526001600c5534801561001557600080fd5b50604051469080605261240d8239604080519182900360520182208282018252600a8352692ab734b9bbb0b8102b1960b11b6020938401528151808301835260018152603160f81b908401528151808401919091527fbfcc8ef98ffbf7b6c3fec7bf5185b566b9863e35a9d83acd49ad6824b5969738818301527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6606082015260808101949094523060a0808601919091528151808603909101815260c09094019052825192019190912060035550600580546001600160a01b03191633179055612308806101056000396000f3fe608060405234801561001057600080fd5b50600436106101a95760003560e01c80636a627842116100f9578063ba9a7a5611610097578063d21220a711610071578063d21220a714610534578063d505accf1461053c578063dd62ed3e1461058d578063fff6cae9146105bb576101a9565b8063ba9a7a56146104fe578063bc25cf7714610506578063c45a01551461052c576101a9565b80637ecebe00116100d35780637ecebe001461046557806389afcb441461048b57806395d89b41146104ca578063a9059cbb146104d2576101a9565b80636a6278421461041157806370a08231146104375780637464fc3d1461045d576101a9565b806323b872dd116101665780633644e515116101405780633644e515146103cb578063485cc955146103d35780635909c0d5146104015780635a3d549314610409576101a9565b806323b872dd1461036f57806330adf81f146103a5578063313ce567146103ad576101a9565b8063022c0d9f146101ae57806306fdde031461023c5780630902f1ac146102b9578063095ea7b3146102f15780630dfe16811461033157806318160ddd14610355575b600080fd5b61023a600480360360808110156101c457600080fd5b8135916020810135916001600160a01b0360408301351691908101906080810160608201356401000000008111156101fb57600080fd5b82018360208201111561020d57600080fd5b8035906020019184600183028401116401000000008311171561022f57600080fd5b5090925090506105c3565b005b610244610afe565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561027e578181015183820152602001610266565b50505050905090810190601f1680156102ab5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6102c1610b24565b604080516001600160701b03948516815292909316602083015263ffffffff168183015290519081900360600190f35b61031d6004803603604081101561030757600080fd5b506001600160a01b038135169060200135610b4e565b604080519115158252519081900360200190f35b610339610b65565b604080516001600160a01b039092168252519081900360200190f35b61035d610b74565b60408051918252519081900360200190f35b61031d6004803603606081101561038557600080fd5b506001600160a01b03813581169160208101359091169060400135610b7a565b61035d610c76565b6103b5610c9a565b6040805160ff9092168252519081900360200190f35b61035d610c9f565b61023a600480360360408110156103e957600080fd5b506001600160a01b0381358116916020013516610ca5565b61035d610d29565b61035d610d2f565b61035d6004803603602081101561042757600080fd5b50356001600160a01b0316610d35565b61035d6004803603602081101561044d57600080fd5b50356001600160a01b0316611035565b61035d611047565b61035d6004803603602081101561047b57600080fd5b50356001600160a01b031661104d565b6104b1600480360360208110156104a157600080fd5b50356001600160a01b031661105f565b6040805192835260208301919091528051918290030190f35b610244611405565b61031d600480360360408110156104e857600080fd5b506001600160a01b038135169060200135611427565b61035d611434565b61023a6004803603602081101561051c57600080fd5b50356001600160a01b031661143a565b6103396115a5565b6103396115b4565b61023a600480360360e081101561055257600080fd5b506001600160a01b03813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c001356115c3565b61035d600480360360408110156105a357600080fd5b506001600160a01b03813581169160200135166117c5565b61023a6117e2565b600c5460011461060e576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c55841515806106215750600084115b61065c5760405162461bcd60e51b81526004018080602001828103825260258152602001806121f56025913960400191505060405180910390fd5b600080610667610b24565b5091509150816001600160701b03168710801561068c5750806001600160701b031686105b6106c75760405162461bcd60e51b815260040180806020018281038252602181526020018061223e6021913960400191505060405180910390fd5b60065460075460009182916001600160a01b039182169190811690891682148015906107055750806001600160a01b0316896001600160a01b031614155b61074e576040805162461bcd60e51b8152602060048201526015602482015274556e697377617056323a20494e56414c49445f544f60581b604482015290519081900360640190fd5b8a1561075f5761075f828a8d611944565b891561077057610770818a8c611944565b861561082b57886001600160a01b03166310d1e85c338d8d8c8c6040518663ffffffff1660e01b815260040180866001600160a01b03166001600160a01b03168152602001858152602001848152602001806020018281038252848482818152602001925080828437600081840152601f19601f8201169050808301925050509650505050505050600060405180830381600087803b15801561081257600080fd5b505af1158015610826573d6000803e3d6000fd5b505050505b604080516370a0823160e01b815230600482015290516001600160a01b038416916370a08231916024808301926020929190829003018186803b15801561087157600080fd5b505afa158015610885573d6000803e3d6000fd5b505050506040513d602081101561089b57600080fd5b5051604080516370a0823160e01b815230600482015290519195506001600160a01b038316916370a0823191602480820192602092909190829003018186803b1580156108e757600080fd5b505afa1580156108fb573d6000803e3d6000fd5b505050506040513d602081101561091157600080fd5b5051925060009150506001600160701b0385168a90038311610934576000610943565b89856001600160701b03160383035b9050600089856001600160701b031603831161096057600061096f565b89856001600160701b03160383035b905060008211806109805750600081115b6109bb5760405162461bcd60e51b815260040180806020018281038252602481526020018061221a6024913960400191505060405180910390fd5b60006109ef6109d184600363ffffffff611ade16565b6109e3876103e863ffffffff611ade16565b9063ffffffff611b4116565b90506000610a076109d184600363ffffffff611ade16565b9050610a38620f4240610a2c6001600160701b038b8116908b1663ffffffff611ade16565b9063ffffffff611ade16565b610a48838363ffffffff611ade16565b1015610a8a576040805162461bcd60e51b815260206004820152600c60248201526b556e697377617056323a204b60a01b604482015290519081900360640190fd5b5050610a9884848888611b91565b60408051838152602081018390528082018d9052606081018c905290516001600160a01b038b169133917fd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d8229181900360800190a350506001600c55505050505050505050565b6040518060400160405280600a8152602001692ab734b9bbb0b8102b1960b11b81525081565b6008546001600160701b0380821692600160701b830490911691600160e01b900463ffffffff1690565b6000610b5b338484611d56565b5060015b92915050565b6006546001600160a01b031681565b60005481565b6001600160a01b0383166000908152600260209081526040808320338452909152812054821115610bdc5760405162461bcd60e51b81526004018080602001828103825260258152602001806122af6025913960400191505060405180910390fd5b6001600160a01b038416600090815260026020908152604080832033845290915290205460001914610c61576001600160a01b0384166000908152600260209081526040808320338452909152902054610c3c908363ffffffff611b4116565b6001600160a01b03851660009081526002602090815260408083203384529091529020555b610c6c848484611db8565b5060019392505050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b601281565b60035481565b6005546001600160a01b03163314610cfb576040805162461bcd60e51b81526020600482015260146024820152732ab734b9bbb0b82b191d102327a92124a22222a760611b604482015290519081900360640190fd5b600680546001600160a01b039384166001600160a01b03199182161790915560078054929093169116179055565b60095481565b600a5481565b6000600c54600114610d82576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c81905580610d92610b24565b50600654604080516370a0823160e01b815230600482015290519395509193506000926001600160a01b03909116916370a08231916024808301926020929190829003018186803b158015610de657600080fd5b505afa158015610dfa573d6000803e3d6000fd5b505050506040513d6020811015610e1057600080fd5b5051600754604080516370a0823160e01b815230600482015290519293506000926001600160a01b03909216916370a0823191602480820192602092909190829003018186803b158015610e6357600080fd5b505afa158015610e77573d6000803e3d6000fd5b505050506040513d6020811015610e8d57600080fd5b505190506000610eac836001600160701b03871663ffffffff611b4116565b90506000610ec9836001600160701b03871663ffffffff611b4116565b90506000610ed78787611e72565b60005490915080610f1457610f006103e86109e3610efb878763ffffffff611ade16565b611fd0565b9850610f0f60006103e8612022565b610f63565b610f606001600160701b038916610f31868463ffffffff611ade16565b81610f3857fe5b046001600160701b038916610f53868563ffffffff611ade16565b81610f5a57fe5b046120b8565b98505b60008911610fa25760405162461bcd60e51b81526004018080602001828103825260288152602001806122876028913960400191505060405180910390fd5b610fac8a8a612022565b610fb886868a8a611b91565b8115610fe857600854610fe4906001600160701b0380821691600160701b90041663ffffffff611ade16565b600b555b6040805185815260208101859052815133927f4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f928290030190a250506001600c5550949695505050505050565b60016020526000908152604090205481565b600b5481565b60046020526000908152604090205481565b600080600c546001146110ad576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c819055806110bd610b24565b50600654600754604080516370a0823160e01b815230600482015290519496509294506001600160a01b039182169391169160009184916370a08231916024808301926020929190829003018186803b15801561111957600080fd5b505afa15801561112d573d6000803e3d6000fd5b505050506040513d602081101561114357600080fd5b5051604080516370a0823160e01b815230600482015290519192506000916001600160a01b038516916370a08231916024808301926020929190829003018186803b15801561119157600080fd5b505afa1580156111a5573d6000803e3d6000fd5b505050506040513d60208110156111bb57600080fd5b5051306000908152600160205260408120549192506111da8888611e72565b600054909150806111f1848763ffffffff611ade16565b816111f857fe5b049a508061120c848663ffffffff611ade16565b8161121357fe5b04995060008b118015611226575060008a115b6112615760405162461bcd60e51b815260040180806020018281038252602881526020018061225f6028913960400191505060405180910390fd5b61126b30846120d0565b611276878d8d611944565b611281868d8c611944565b604080516370a0823160e01b815230600482015290516001600160a01b038916916370a08231916024808301926020929190829003018186803b1580156112c757600080fd5b505afa1580156112db573d6000803e3d6000fd5b505050506040513d60208110156112f157600080fd5b5051604080516370a0823160e01b815230600482015290519196506001600160a01b038816916370a0823191602480820192602092909190829003018186803b15801561133d57600080fd5b505afa158015611351573d6000803e3d6000fd5b505050506040513d602081101561136757600080fd5b5051935061137785858b8b611b91565b81156113a7576008546113a3906001600160701b0380821691600160701b90041663ffffffff611ade16565b600b555b604080518c8152602081018c905281516001600160a01b038f169233927fdccd412f0b1252819cb1fd330b93224ca42612892bb3f4f789976e6d81936496929081900390910190a35050505050505050506001600c81905550915091565b604051806040016040528060068152602001652aa72496ab1960d11b81525081565b6000610b5b338484611db8565b6103e881565b600c54600114611485576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c55600654600754600854604080516370a0823160e01b815230600482015290516001600160a01b039485169490931692611534928592879261152f926001600160701b03169185916370a0823191602480820192602092909190829003018186803b1580156114f757600080fd5b505afa15801561150b573d6000803e3d6000fd5b505050506040513d602081101561152157600080fd5b50519063ffffffff611b4116565b611944565b600854604080516370a0823160e01b8152306004820152905161159b928492879261152f92600160701b90046001600160701b0316916001600160a01b038616916370a0823191602480820192602092909190829003018186803b1580156114f757600080fd5b50506001600c5550565b6005546001600160a01b031681565b6007546001600160a01b031681565b4284101561160d576040805162461bcd60e51b8152602060048201526012602482015271155b9a5cddd85c158c8e881156141254915160721b604482015290519081900360640190fd5b6003546001600160a01b0380891660008181526004602090815260408083208054600180820190925582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98186015280840196909652958d166060860152608085018c905260a085019590955260c08085018b90528151808603909101815260e08501825280519083012061190160f01b6101008601526101028501969096526101228085019690965280518085039096018652610142840180825286519683019690962095839052610162840180825286905260ff89166101828501526101a284018890526101c28401879052519193926101e280820193601f1981019281900390910190855afa158015611728573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381161580159061175e5750886001600160a01b0316816001600160a01b0316145b6117af576040805162461bcd60e51b815260206004820152601c60248201527f556e697377617056323a20494e56414c49445f5349474e415455524500000000604482015290519081900360640190fd5b6117ba898989611d56565b505050505050505050565b600260209081526000928352604080842090915290825290205481565b600c5460011461182d576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c55600654604080516370a0823160e01b8152306004820152905161193d926001600160a01b0316916370a08231916024808301926020929190829003018186803b15801561187e57600080fd5b505afa158015611892573d6000803e3d6000fd5b505050506040513d60208110156118a857600080fd5b5051600754604080516370a0823160e01b815230600482015290516001600160a01b03909216916370a0823191602480820192602092909190829003018186803b1580156118f557600080fd5b505afa158015611909573d6000803e3d6000fd5b505050506040513d602081101561191f57600080fd5b50516008546001600160701b0380821691600160701b900416611b91565b6001600c55565b604080518082018252601981527f7472616e7366657228616464726573732c75696e74323536290000000000000060209182015281516001600160a01b0385811660248301526044808301869052845180840390910181526064909201845291810180516001600160e01b031663a9059cbb60e01b1781529251815160009460609489169392918291908083835b602083106119f15780518252601f1990920191602091820191016119d2565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114611a53576040519150601f19603f3d011682016040523d82523d6000602084013e611a58565b606091505b5091509150818015611a86575080511580611a865750808060200190516020811015611a8357600080fd5b50515b611ad7576040805162461bcd60e51b815260206004820152601a60248201527f556e697377617056323a205452414e534645525f4641494c4544000000000000604482015290519081900360640190fd5b5050505050565b6000811580611af957505080820282828281611af657fe5b04145b610b5f576040805162461bcd60e51b815260206004820152601460248201527364732d6d6174682d6d756c2d6f766572666c6f7760601b604482015290519081900360640190fd5b80820382811115610b5f576040805162461bcd60e51b815260206004820152601560248201527464732d6d6174682d7375622d756e646572666c6f7760581b604482015290519081900360640190fd5b6001600160701b038411801590611baf57506001600160701b038311155b611bf6576040805162461bcd60e51b8152602060048201526013602482015272556e697377617056323a204f564552464c4f5760681b604482015290519081900360640190fd5b60085463ffffffff42811691600160e01b90048116820390811615801590611c2657506001600160701b03841615155b8015611c3a57506001600160701b03831615155b15611cab578063ffffffff16611c6885611c538661216e565b6001600160e01b03169063ffffffff61218016565b600980546001600160e01b03929092169290920201905563ffffffff8116611c9384611c538761216e565b600a80546001600160e01b0392909216929092020190555b600880546dffffffffffffffffffffffffffff19166001600160701b03888116919091176dffffffffffffffffffffffffffff60701b1916600160701b8883168102919091176001600160e01b0316600160e01b63ffffffff871602179283905560408051848416815291909304909116602082015281517f1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1929181900390910190a1505050505050565b6001600160a01b03808416600081815260026020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6001600160a01b038316600090815260016020526040902054611de1908263ffffffff611b4116565b6001600160a01b038085166000908152600160205260408082209390935590841681522054611e16908263ffffffff6121a516565b6001600160a01b0380841660008181526001602090815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b600080600560009054906101000a90046001600160a01b03166001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b158015611ec357600080fd5b505afa158015611ed7573d6000803e3d6000fd5b505050506040513d6020811015611eed57600080fd5b5051600b546001600160a01b038216158015945091925090611fbc578015611fb7576000611f30610efb6001600160701b0388811690881663ffffffff611ade16565b90506000611f3d83611fd0565b905080821115611fb4576000611f6b611f5c848463ffffffff611b4116565b6000549063ffffffff611ade16565b90506000611f9083611f8486600563ffffffff611ade16565b9063ffffffff6121a516565b90506000818381611f9d57fe5b0490508015611fb057611fb08782612022565b5050505b50505b611fc8565b8015611fc8576000600b555b505092915050565b60006003821115612013575080600160028204015b8181101561200d57809150600281828581611ffc57fe5b04018161200557fe5b049050611fe5565b5061201d565b811561201d575060015b919050565b600054612035908263ffffffff6121a516565b60009081556001600160a01b038316815260016020526040902054612060908263ffffffff6121a516565b6001600160a01b03831660008181526001602090815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b60008183106120c757816120c9565b825b9392505050565b6001600160a01b0382166000908152600160205260409020546120f9908263ffffffff611b4116565b6001600160a01b03831660009081526001602052604081209190915554612126908263ffffffff611b4116565b60009081556040805183815290516001600160a01b038516917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef919081900360200190a35050565b6001600160701b0316600160701b0290565b60006001600160701b0382166001600160e01b0384168161219d57fe5b049392505050565b80820182811015610b5f576040805162461bcd60e51b815260206004820152601460248201527364732d6d6174682d6164642d6f766572666c6f7760601b604482015290519081900360640190fdfe556e697377617056323a20494e53554646494349454e545f4f55545055545f414d4f554e54556e697377617056323a20494e53554646494349454e545f494e5055545f414d4f554e54556e697377617056323a20494e53554646494349454e545f4c4951554944495459556e697377617056323a20494e53554646494349454e545f4c49515549444954595f4255524e4544556e697377617056323a20494e53554646494349454e545f4c49515549444954595f4d494e544544556e6973776170563245524332303a206e6f7420656e6f75676820616c6c6f77616e63652ea265627a7a72315820bf165e44001b5284076af3e56685540f3d6256d9ddeae5a8f22ed60b1397a80064736f6c63430005100032454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429

*/

/*

* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释

* pair code (evm): 0x60806040526001600c5534801561001557600080fd5b50604051469080605261240d8239604080519182900360520182208282018252600a8352692ab734b9bbb0b8102b1960b11b6020938401528151808301835260018152603160f81b908401528151808401919091527fbfcc8ef98ffbf7b6c3fec7bf5185b566b9863e35a9d83acd49ad6824b5969738818301527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6606082015260808101949094523060a0808601919091528151808603909101815260c09094019052825192019190912060035550600580546001600160a01b03191633179055612308806101056000396000f3fe608060405234801561001057600080fd5b50600436106101a95760003560e01c80636a627842116100f9578063ba9a7a5611610097578063d21220a711610071578063d21220a714610534578063d505accf1461053c578063dd62ed3e1461058d578063fff6cae9146105bb576101a9565b8063ba9a7a56146104fe578063bc25cf7714610506578063c45a01551461052c576101a9565b80637ecebe00116100d35780637ecebe001461046557806389afcb441461048b57806395d89b41146104ca578063a9059cbb146104d2576101a9565b80636a6278421461041157806370a08231146104375780637464fc3d1461045d576101a9565b806323b872dd116101665780633644e515116101405780633644e515146103cb578063485cc955146103d35780635909c0d5146104015780635a3d549314610409576101a9565b806323b872dd1461036f57806330adf81f146103a5578063313ce567146103ad576101a9565b8063022c0d9f146101ae57806306fdde031461023c5780630902f1ac146102b9578063095ea7b3146102f15780630dfe16811461033157806318160ddd14610355575b600080fd5b61023a600480360360808110156101c457600080fd5b8135916020810135916001600160a01b0360408301351691908101906080810160608201356401000000008111156101fb57600080fd5b82018360208201111561020d57600080fd5b8035906020019184600183028401116401000000008311171561022f57600080fd5b5090925090506105c3565b005b610244610afe565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561027e578181015183820152602001610266565b50505050905090810190601f1680156102ab5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6102c1610b24565b604080516001600160701b03948516815292909316602083015263ffffffff168183015290519081900360600190f35b61031d6004803603604081101561030757600080fd5b506001600160a01b038135169060200135610b4e565b604080519115158252519081900360200190f35b610339610b65565b604080516001600160a01b039092168252519081900360200190f35b61035d610b74565b60408051918252519081900360200190f35b61031d6004803603606081101561038557600080fd5b506001600160a01b03813581169160208101359091169060400135610b7a565b61035d610c76565b6103b5610c9a565b6040805160ff9092168252519081900360200190f35b61035d610c9f565b61023a600480360360408110156103e957600080fd5b506001600160a01b0381358116916020013516610ca5565b61035d610d29565b61035d610d2f565b61035d6004803603602081101561042757600080fd5b50356001600160a01b0316610d35565b61035d6004803603602081101561044d57600080fd5b50356001600160a01b0316611035565b61035d611047565b61035d6004803603602081101561047b57600080fd5b50356001600160a01b031661104d565b6104b1600480360360208110156104a157600080fd5b50356001600160a01b031661105f565b6040805192835260208301919091528051918290030190f35b610244611405565b61031d600480360360408110156104e857600080fd5b506001600160a01b038135169060200135611427565b61035d611434565b61023a6004803603602081101561051c57600080fd5b50356001600160a01b031661143a565b6103396115a5565b6103396115b4565b61023a600480360360e081101561055257600080fd5b506001600160a01b03813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c001356115c3565b61035d600480360360408110156105a357600080fd5b506001600160a01b03813581169160200135166117c5565b61023a6117e2565b600c5460011461060e576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c55841515806106215750600084115b61065c5760405162461bcd60e51b81526004018080602001828103825260258152602001806121f56025913960400191505060405180910390fd5b600080610667610b24565b5091509150816001600160701b03168710801561068c5750806001600160701b031686105b6106c75760405162461bcd60e51b815260040180806020018281038252602181526020018061223e6021913960400191505060405180910390fd5b60065460075460009182916001600160a01b039182169190811690891682148015906107055750806001600160a01b0316896001600160a01b031614155b61074e576040805162461bcd60e51b8152602060048201526015602482015274556e697377617056323a20494e56414c49445f544f60581b604482015290519081900360640190fd5b8a1561075f5761075f828a8d611944565b891561077057610770818a8c611944565b861561082b57886001600160a01b03166310d1e85c338d8d8c8c6040518663ffffffff1660e01b815260040180866001600160a01b03166001600160a01b03168152602001858152602001848152602001806020018281038252848482818152602001925080828437600081840152601f19601f8201169050808301925050509650505050505050600060405180830381600087803b15801561081257600080fd5b505af1158015610826573d6000803e3d6000fd5b505050505b604080516370a0823160e01b815230600482015290516001600160a01b038416916370a08231916024808301926020929190829003018186803b15801561087157600080fd5b505afa158015610885573d6000803e3d6000fd5b505050506040513d602081101561089b57600080fd5b5051604080516370a0823160e01b815230600482015290519195506001600160a01b038316916370a0823191602480820192602092909190829003018186803b1580156108e757600080fd5b505afa1580156108fb573d6000803e3d6000fd5b505050506040513d602081101561091157600080fd5b5051925060009150506001600160701b0385168a90038311610934576000610943565b89856001600160701b03160383035b9050600089856001600160701b031603831161096057600061096f565b89856001600160701b03160383035b905060008211806109805750600081115b6109bb5760405162461bcd60e51b815260040180806020018281038252602481526020018061221a6024913960400191505060405180910390fd5b60006109ef6109d184600363ffffffff611ade16565b6109e3876103e863ffffffff611ade16565b9063ffffffff611b4116565b90506000610a076109d184600363ffffffff611ade16565b9050610a38620f4240610a2c6001600160701b038b8116908b1663ffffffff611ade16565b9063ffffffff611ade16565b610a48838363ffffffff611ade16565b1015610a8a576040805162461bcd60e51b815260206004820152600c60248201526b556e697377617056323a204b60a01b604482015290519081900360640190fd5b5050610a9884848888611b91565b60408051838152602081018390528082018d9052606081018c905290516001600160a01b038b169133917fd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d8229181900360800190a350506001600c55505050505050505050565b6040518060400160405280600a8152602001692ab734b9bbb0b8102b1960b11b81525081565b6008546001600160701b0380821692600160701b830490911691600160e01b900463ffffffff1690565b6000610b5b338484611d56565b5060015b92915050565b6006546001600160a01b031681565b60005481565b6001600160a01b0383166000908152600260209081526040808320338452909152812054821115610bdc5760405162461bcd60e51b81526004018080602001828103825260258152602001806122af6025913960400191505060405180910390fd5b6001600160a01b038416600090815260026020908152604080832033845290915290205460001914610c61576001600160a01b0384166000908152600260209081526040808320338452909152902054610c3c908363ffffffff611b4116565b6001600160a01b03851660009081526002602090815260408083203384529091529020555b610c6c848484611db8565b5060019392505050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b601281565b60035481565b6005546001600160a01b03163314610cfb576040805162461bcd60e51b81526020600482015260146024820152732ab734b9bbb0b82b191d102327a92124a22222a760611b604482015290519081900360640190fd5b600680546001600160a01b039384166001600160a01b03199182161790915560078054929093169116179055565b60095481565b600a5481565b6000600c54600114610d82576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c81905580610d92610b24565b50600654604080516370a0823160e01b815230600482015290519395509193506000926001600160a01b03909116916370a08231916024808301926020929190829003018186803b158015610de657600080fd5b505afa158015610dfa573d6000803e3d6000fd5b505050506040513d6020811015610e1057600080fd5b5051600754604080516370a0823160e01b815230600482015290519293506000926001600160a01b03909216916370a0823191602480820192602092909190829003018186803b158015610e6357600080fd5b505afa158015610e77573d6000803e3d6000fd5b505050506040513d6020811015610e8d57600080fd5b505190506000610eac836001600160701b03871663ffffffff611b4116565b90506000610ec9836001600160701b03871663ffffffff611b4116565b90506000610ed78787611e72565b60005490915080610f1457610f006103e86109e3610efb878763ffffffff611ade16565b611fd0565b9850610f0f60006103e8612022565b610f63565b610f606001600160701b038916610f31868463ffffffff611ade16565b81610f3857fe5b046001600160701b038916610f53868563ffffffff611ade16565b81610f5a57fe5b046120b8565b98505b60008911610fa25760405162461bcd60e51b81526004018080602001828103825260288152602001806122876028913960400191505060405180910390fd5b610fac8a8a612022565b610fb886868a8a611b91565b8115610fe857600854610fe4906001600160701b0380821691600160701b90041663ffffffff611ade16565b600b555b6040805185815260208101859052815133927f4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f928290030190a250506001600c5550949695505050505050565b60016020526000908152604090205481565b600b5481565b60046020526000908152604090205481565b600080600c546001146110ad576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c819055806110bd610b24565b50600654600754604080516370a0823160e01b815230600482015290519496509294506001600160a01b039182169391169160009184916370a08231916024808301926020929190829003018186803b15801561111957600080fd5b505afa15801561112d573d6000803e3d6000fd5b505050506040513d602081101561114357600080fd5b5051604080516370a0823160e01b815230600482015290519192506000916001600160a01b038516916370a08231916024808301926020929190829003018186803b15801561119157600080fd5b505afa1580156111a5573d6000803e3d6000fd5b505050506040513d60208110156111bb57600080fd5b5051306000908152600160205260408120549192506111da8888611e72565b600054909150806111f1848763ffffffff611ade16565b816111f857fe5b049a508061120c848663ffffffff611ade16565b8161121357fe5b04995060008b118015611226575060008a115b6112615760405162461bcd60e51b815260040180806020018281038252602881526020018061225f6028913960400191505060405180910390fd5b61126b30846120d0565b611276878d8d611944565b611281868d8c611944565b604080516370a0823160e01b815230600482015290516001600160a01b038916916370a08231916024808301926020929190829003018186803b1580156112c757600080fd5b505afa1580156112db573d6000803e3d6000fd5b505050506040513d60208110156112f157600080fd5b5051604080516370a0823160e01b815230600482015290519196506001600160a01b038816916370a0823191602480820192602092909190829003018186803b15801561133d57600080fd5b505afa158015611351573d6000803e3d6000fd5b505050506040513d602081101561136757600080fd5b5051935061137785858b8b611b91565b81156113a7576008546113a3906001600160701b0380821691600160701b90041663ffffffff611ade16565b600b555b604080518c8152602081018c905281516001600160a01b038f169233927fdccd412f0b1252819cb1fd330b93224ca42612892bb3f4f789976e6d81936496929081900390910190a35050505050505050506001600c81905550915091565b604051806040016040528060068152602001652aa72496ab1960d11b81525081565b6000610b5b338484611db8565b6103e881565b600c54600114611485576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c55600654600754600854604080516370a0823160e01b815230600482015290516001600160a01b039485169490931692611534928592879261152f926001600160701b03169185916370a0823191602480820192602092909190829003018186803b1580156114f757600080fd5b505afa15801561150b573d6000803e3d6000fd5b505050506040513d602081101561152157600080fd5b50519063ffffffff611b4116565b611944565b600854604080516370a0823160e01b8152306004820152905161159b928492879261152f92600160701b90046001600160701b0316916001600160a01b038616916370a0823191602480820192602092909190829003018186803b1580156114f757600080fd5b50506001600c5550565b6005546001600160a01b031681565b6007546001600160a01b031681565b4284101561160d576040805162461bcd60e51b8152602060048201526012602482015271155b9a5cddd85c158c8e881156141254915160721b604482015290519081900360640190fd5b6003546001600160a01b0380891660008181526004602090815260408083208054600180820190925582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98186015280840196909652958d166060860152608085018c905260a085019590955260c08085018b90528151808603909101815260e08501825280519083012061190160f01b6101008601526101028501969096526101228085019690965280518085039096018652610142840180825286519683019690962095839052610162840180825286905260ff89166101828501526101a284018890526101c28401879052519193926101e280820193601f1981019281900390910190855afa158015611728573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381161580159061175e5750886001600160a01b0316816001600160a01b0316145b6117af576040805162461bcd60e51b815260206004820152601c60248201527f556e697377617056323a20494e56414c49445f5349474e415455524500000000604482015290519081900360640190fd5b6117ba898989611d56565b505050505050505050565b600260209081526000928352604080842090915290825290205481565b600c5460011461182d576040805162461bcd60e51b8152602060048201526011602482015270155b9a5cddd85c158c8e881313d0d2d151607a1b604482015290519081900360640190fd5b6000600c55600654604080516370a0823160e01b8152306004820152905161193d926001600160a01b0316916370a08231916024808301926020929190829003018186803b15801561187e57600080fd5b505afa158015611892573d6000803e3d6000fd5b505050506040513d60208110156118a857600080fd5b5051600754604080516370a0823160e01b815230600482015290516001600160a01b03909216916370a0823191602480820192602092909190829003018186803b1580156118f557600080fd5b505afa158015611909573d6000803e3d6000fd5b505050506040513d602081101561191f57600080fd5b50516008546001600160701b0380821691600160701b900416611b91565b6001600c55565b604080518082018252601981527f7472616e7366657228616464726573732c75696e74323536290000000000000060209182015281516001600160a01b0385811660248301526044808301869052845180840390910181526064909201845291810180516001600160e01b031663a9059cbb60e01b1781529251815160009460609489169392918291908083835b602083106119f15780518252601f1990920191602091820191016119d2565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114611a53576040519150601f19603f3d011682016040523d82523d6000602084013e611a58565b606091505b5091509150818015611a86575080511580611a865750808060200190516020811015611a8357600080fd5b50515b611ad7576040805162461bcd60e51b815260206004820152601a60248201527f556e697377617056323a205452414e534645525f4641494c4544000000000000604482015290519081900360640190fd5b5050505050565b6000811580611af957505080820282828281611af657fe5b04145b610b5f576040805162461bcd60e51b815260206004820152601460248201527364732d6d6174682d6d756c2d6f766572666c6f7760601b604482015290519081900360640190fd5b80820382811115610b5f576040805162461bcd60e51b815260206004820152601560248201527464732d6d6174682d7375622d756e646572666c6f7760581b604482015290519081900360640190fd5b6001600160701b038411801590611baf57506001600160701b038311155b611bf6576040805162461bcd60e51b8152602060048201526013602482015272556e697377617056323a204f564552464c4f5760681b604482015290519081900360640190fd5b60085463ffffffff42811691600160e01b90048116820390811615801590611c2657506001600160701b03841615155b8015611c3a57506001600160701b03831615155b15611cab578063ffffffff16611c6885611c538661216e565b6001600160e01b03169063ffffffff61218016565b600980546001600160e01b03929092169290920201905563ffffffff8116611c9384611c538761216e565b600a80546001600160e01b0392909216929092020190555b600880546dffffffffffffffffffffffffffff19166001600160701b03888116919091176dffffffffffffffffffffffffffff60701b1916600160701b8883168102919091176001600160e01b0316600160e01b63ffffffff871602179283905560408051848416815291909304909116602082015281517f1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1929181900390910190a1505050505050565b6001600160a01b03808416600081815260026020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6001600160a01b038316600090815260016020526040902054611de1908263ffffffff611b4116565b6001600160a01b038085166000908152600160205260408082209390935590841681522054611e16908263ffffffff6121a516565b6001600160a01b0380841660008181526001602090815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b600080600560009054906101000a90046001600160a01b03166001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b158015611ec357600080fd5b505afa158015611ed7573d6000803e3d6000fd5b505050506040513d6020811015611eed57600080fd5b5051600b546001600160a01b038216158015945091925090611fbc578015611fb7576000611f30610efb6001600160701b0388811690881663ffffffff611ade16565b90506000611f3d83611fd0565b905080821115611fb4576000611f6b611f5c848463ffffffff611b4116565b6000549063ffffffff611ade16565b90506000611f9083611f8486600563ffffffff611ade16565b9063ffffffff6121a516565b90506000818381611f9d57fe5b0490508015611fb057611fb08782612022565b5050505b50505b611fc8565b8015611fc8576000600b555b505092915050565b60006003821115612013575080600160028204015b8181101561200d57809150600281828581611ffc57fe5b04018161200557fe5b049050611fe5565b5061201d565b811561201d575060015b919050565b600054612035908263ffffffff6121a516565b60009081556001600160a01b038316815260016020526040902054612060908263ffffffff6121a516565b6001600160a01b03831660008181526001602090815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b60008183106120c757816120c9565b825b9392505050565b6001600160a01b0382166000908152600160205260409020546120f9908263ffffffff611b4116565b6001600160a01b03831660009081526001602052604081209190915554612126908263ffffffff611b4116565b60009081556040805183815290516001600160a01b038516917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef919081900360200190a35050565b6001600160701b0316600160701b0290565b60006001600160701b0382166001600160e01b0384168161219d57fe5b049392505050565b80820182811015610b5f576040805162461bcd60e51b815260206004820152601460248201527364732d6d6174682d6164642d6f766572666c6f7760601b604482015290519081900360640190fdfe556e697377617056323a20494e53554646494349454e545f4f55545055545f414d4f554e54556e697377617056323a20494e53554646494349454e545f494e5055545f414d4f554e54556e697377617056323a20494e53554646494349454e545f4c4951554944495459556e697377617056323a20494e53554646494349454e545f4c49515549444954595f4255524e4544556e697377617056323a20494e53554646494349454e545f4c49515549444954595f4d494e544544556e6973776170563245524332303a206e6f7420656e6f75676820616c6c6f77616e63652ea265627a7a72315820bf165e44001b5284076af3e56685540f3d6256d9ddeae5a8f22ed60b1397a80064736f6c63430005100032454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429

*/

pair code hash(python):

0xd0b942f04f5999da18c6933b57c7a558fc3b364ba0e233f5828591ee21b66669

pair code hash(evm):

0xd0b942f04f5999da18c6933b57c7a558fc3b364ba0e233f5828591ee21b66669

pair address of WBTC-USDT(calculated from python):

0x841f5e40d7573fd7fbe5a8a002a10759ad5e3bf3

pair address of WBTC-USDT(from EVM create2):

0x841F5E40d7573FD7FbE5A8A002a10759aD5E3BF3

复制ok, 第一部分暂时分享到这里, 接下来我们继续一起学习吧.作者 mail:star4evar@gmail.com参考资料[1]tony.ho: https://learnblockchain.cn/people/8619[2]Uniswap: https://learnblockchain.cn/article/274本文参与 腾讯云自媒体分享计划,分享自微信公众号。原始发表:2022-04-22,如有侵权请联系 cloudcommunity@tencent.com 删除https网络安全python本文分享自 深入浅出区块链技术 微信公众号,前往查看如有侵权,请联系 cloudcommunity@tencent.com 删除。本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!https网络安全python评论登录后参与评论0 条评论热度最新登录 后参与评论推荐阅读LV.关注文章0获赞0目录概览Pair 的创建 和 create2 的地址计算:参考资料领券社区专栏文章阅读清单互动问答技术沙龙技术视频团队主页腾讯云TI平台活动自媒体分享计划邀请作者入驻自荐上首页技术竞赛资源技术周刊社区标签开发者手册开发者实验室关于社区规范免责声明联系我们友情链接腾讯云开发者扫码关注腾讯云开发者领取腾讯云代金券热门产品域名注册云服务器区块链服务消息队列网络加速云数据库域名解析云存储视频直播热门推荐人脸识别腾讯会议企业云CDN加速视频通话图像分析MySQL 数据库SSL 证书语音识别更多推荐数据安全负载均衡短信文字识别云点播商标注册小程序开发网站监控数据迁移Copyright © 2013 - 2024 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有 深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569腾讯云计算(北京)有限责任公司 京ICP证150476号 |  京ICP备11018762号 | 京公网安备号11010802020287问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档Copyright © 2013 - 2024 Tencent Cloud.All Rights Reserved. 腾讯云 版权所有登录 后参与评论100

工厂合约 | Uniswap V3 Book 中文版

| Uniswap V3 Book 中文版

Uniswap V3 Book 中文版Milestone 0. 简介交易市场简介恒定函数做市商(CFMM)Uniswap V3开发环境Milestone 1. 第一笔交易简介计算流动性提供流动性第一笔交易管理合约部署合约用户界面Milestone 2. 第二笔交易简介输出金额计算Solidity中的数学运算Tick Bitmap Index通用mint通用swap报价合约用户界面Milestone 3. 跨tick交易简介不同价格区间跨tick交易滑点保护流动性计算关于定点数的拓展闪电贷用户界面Milestone 4. 多池子交易简介工厂合约交易路径多池子交易用户界面Tick 舍入Milestone 5. 费率和价格预言机简介交易费率闪电贷费率协议费率价格预言机用户界面Milestone 6: NFT positions简介ERC721 概述NFT 管理员合约NFT 渲染器补充资料中英名词对照

工厂合约

工厂合约CREATE 和 CREATE2 OpcodesTick 间隔工厂合约实现池子初始化PoolAddress 库简化 Manager 和 Quoter 的接口工厂合约

#Uniswap 由多个离散的池子合约构成,每个池子负责一对 token 的交易。这看起来会有些问题,因为当我们想要在两个没有池子的 token 之间进行交易时——由于没有赤字,我们无法进行交易。然而,我们仍然可以进行中间交易:第一笔交易把一种 token 转换成另一种有交易对的 token,然后再把这种 token 转换成目标 token。这个路径可以更长并且有更多种的中间 token。然而,手动进行这一个操作会非常繁琐,幸运的是,我们可以在我们的智能合约中实现这个功能,让其更简便。*工厂(Factory)*合约是一个拥有以下功能的合约:它作为池子合约的中心化注册点。在工厂中,你可以找到所有已部署的池子,对应的 token 和地址。它简化了池子合约的部署流程。EVM允许在智能合约中部署智能合约——工厂合约使用这个性质来让池子合约的部署变得十分简单。它让池子合约的地址可预测,并且能够在注册池子之前就计算出这个地址。这让池子更容易被发现。让我们来搭建工厂合约吧!但在此之前,我们还需要学一些新东西。CREATE 和 CREATE2 Opcodes

#EVM 有两种部署合约的方式:通过 CREATE 或者 CREATE2 opcode。两者之间的唯一区别试新地址如何产生:CREATE 使用部署者账户的 nonce 来产生新的合约地址(伪代码如下):KECCAK256(deployer.address, deployer.nonce)

nonce 是一个账户特定的交易计数器。在产生合约地址过程中使用 nonce 会使得在其他合约或者链下app中计算合约地址变得非常困难,主要是因为想要找到对应的 nonce,我们需要扫描账户的交易历史。CREATE2 使用一个特殊的*盐值(salt)*来产生合约地址。这是一个由开发者选择的任意序列,能够使得地址产生更加确定性(并降低碰撞概率):KECCAK256(deployer.address, salt, contractCodeHash)

我们需要知道两者的区别,因为工厂在部署池子合约时使用的是 CREATE2,所以池子可以获得唯一并且确定性的、能够由其他合约和链下 app 计算出来的地址。在盐值方面,工厂使用这些池子的参数计算哈希:keccak256(abi.encodePacked(token0, token1, tickSpacing))

token0 和 token1 是池子里两种 token 的地址,而 tickSpacing 是我们下面将要讨论的内容。Tick 间隔

#回顾一下我们在 swap 函数中的循环:while (

state.amountSpecifiedRemaining > 0 &&

state.sqrtPriceX96 != sqrtPriceLimitX96

) {

...

(step.nextTick, ) = tickBitmap.nextInitializedTickWithinOneWord(...);

(state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath.computeSwapStep(...);

...

}

这个循环通过在一个方向上遍历来寻找拥有流动性的已初始化的 tick。然而,这个循环时非常昂贵的:如果一个 tick 离得很远,代码将会经过两个 tick 之间的所有 tick,十分消耗 gas。为了让循环更节约 gas,Uniswap 的池子有一个叫做 tickSpacing 的参数设定;正如其名字所示,代表两个 tick 之间的距离——距离越大,越节省 gas。然而,tick 间隔越大,精度越低。价格波动性低的交易对(例如两种稳定币的池子)需要更高的精度,因为在这样的池子中价格移动很小;价格波动性中等和较高的交易对可以有更低的精度,因为在这样的交易对中价格移动会很大。为了处理这样的多样性,Uniswap 允许在交易对创建时设定一个 tick 间隔。Uniswap 允许部署者在下列选项中选择:10,60,200,而简单起见我们的实现中只考虑10和60。在实际中,tick index只能够是 tickSpacing 的整数倍:如果 tickSpacing 是 10,仅有 10 的倍数才是有效的 tick index(10,20,5000,5010等,但是8,12,5001这些不可以)。然而,需要注意的是,这个限制对于现价不起作用——现价所在的 tick 仍然可以是任意的 tick,因为我们希望价格尽可能精确。tickSpacing 参数仅仅限制价格区间对应 tick。因此,一个池子可以由以下参数唯一确定:token0,token1,tickSpacing;正如你想的那样,可以有 token 相同但是 tickSpacing 不同的池子存在。工厂合约使用这组参数来作为池子的唯一定位,并且把他们作为盐值来产生池子合约地址。从现在开始,我们假定所有池子的 tick 间隔为60,而在稳定币交易对中使用10。工厂合约实现

#在工厂合约的构造函数中,我们需要初始化支持的 tick 间隔:// src/UniswapV3Factory.sol

contract UniswapV3Factory is IUniswapV3PoolDeployer {

mapping(uint24 => bool) public tickSpacings;

constructor() {

tickSpacings[10] = true;

tickSpacings[60] = true;

}

...

我们的确可以让它们是常数,但在后面的 milestone 中我们会希望它是一个映射(不同的 tick 间隔会有不同的交易费)。工厂合约中的唯一函数是 createPool。这个函数首先检查创建池子所需要的所有必要条件:// src/UniswapV3Factory.sol

contract UniswapV3Factory is IUniswapV3PoolDeployer {

PoolParameters public parameters;

mapping(address => mapping(address => mapping(uint24 => address)))

public pools;

...

function createPool(

address tokenX,

address tokenY,

uint24 tickSpacing

) public returns (address pool) {

if (tokenX == tokenY) revert TokensMustBeDifferent();

if (!tickSpacings[tickSpacing]) revert UnsupportedTickSpacing();

(tokenX, tokenY) = tokenX < tokenY

? (tokenX, tokenY)

: (tokenY, tokenX);

if (tokenX == address(0)) revert TokenXCannotBeZero();

if (pools[tokenX][tokenY][tickSpacing] != address(0))

revert PoolAlreadyExists();

...

注意到这是我们第一次在代码中看到 token 的排序:(tokenX, tokenY) = tokenX < tokenY

? (tokenX, tokenY)

: (tokenY, tokenX);

从现在开始,我们将永远认为池子的 token 地址是有序的,也即 token0 地址小于 token1。我们需要强制这一点来保证盐值和合约地址的计算永远一致。这个改变也会影响我们在测试和部署脚本中部署 token 的方式:我们需要确保 WETH 总是 token0,来使得 Solidity 中的价格计算更简单(否则,我们的价格就会是分数,比如1/5000这样)。如果 WETH 在你的测试中不是 token0,改变一下 token 部署的顺序。之后,我们准备部署合约需要的参数:parameters = PoolParameters({

factory: address(this),

token0: tokenX,

token1: tokenY,

tickSpacing: tickSpacing

});

pool = address(

new UniswapV3Pool{

salt: keccak256(abi.encodePacked(tokenX, tokenY, tickSpacing))

}()

);

delete parameters;

这段代码看起来很奇怪,因为 parameters 并没有用到。Uniswap 这里使用了控制反转(Inversion of Control)来在池子创建的过程中传递参数。译者注:控制反转,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度(来源 wiki)让我们看一下更新后的池子合约构造函数:// src/UniswapV3Pool.sol

contract UniswapV3Pool is IUniswapV3Pool {

...

constructor() {

(factory, token0, token1, tickSpacing) = IUniswapV3PoolDeployer(

msg.sender

).parameters();

}

..

}

池子合约需要部署者实现了 IUniswapV3PoolDeployer 接口(仅仅定义了 parameters() 这样一个 getter)并且在构造函数中调用它来获取参数。控制流长下面这样:Factory:定义 parameters 状态变量(实现 IUniswapV3PoolDeployer)并在部署池子之前设置其值。Factory:部署池子。Pool:在构造函数中,调用部署者的 parameters() 函数,希望从返回值中获取参数。Factory 调用 delete parameters; 来清理 parameter 状态变量占用的 slot 来减少 gas 开销。这仅仅是一个临时的状态变量,只在调用 createPool() 时有值。在池子创建后,我们把它存储在 pools 映射中(这样就能够被找到),并发出一个事件: pools[tokenX][tokenY][tickSpacing] = pool;

pools[tokenY][tokenX][tickSpacing] = pool;

emit PoolCreated(tokenX, tokenY, tickSpacing, pool);

}

池子初始化

#正如你在上面代码中看到的,我们不再在池子的构造函数中设置 sqrtPriceX96 和 tick——它现在在另一个函数 initialize 中完成,这个函数在池子部署后调用:// src/UniswapV3Pool.sol

function initialize(uint160 sqrtPriceX96) public {

if (slot0.sqrtPriceX96 != 0) revert AlreadyInitialized();

int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);

slot0 = Slot0({sqrtPriceX96: sqrtPriceX96, tick: tick});

}

这是我们现在部署池子的方式:UniswapV3Factory factory = new UniswapV3Factory();

UniswapV3Pool pool = UniswapV3Pool(factory.createPool(token0, token1, tickSpacing));

pool.initialize(sqrtP(currentPrice));

PoolAddress 库

#现在我们来实现一个库帮助我们计算池子合约的地址。这个库只有一个函数,computeAddress:// src/lib/PoolAddress.sol

library PoolAddress {

function computeAddress(

address factory,

address token0,

address token1,

uint24 tickSpacing

) internal pure returns (address pool) {

require(token0 < token1);

...

这个函数需要知道池子的参数(用来构成盐值)和工厂合约的地址。它需要 token 事先被排序,正如上文所述。这个函数的核心部分:pool = address(

uint160(

uint256(

keccak256(

abi.encodePacked(

hex"ff",

factory,

keccak256(

abi.encodePacked(token0, token1, tickSpacing)

),

keccak256(type(UniswapV3Pool).creationCode)

)

)

)

)

);

这正是 CREATE2 计算新合约地址的底层实现方式。我们来拆解一下:首先,我们计算盐值(abi.encodePacked(token0, token1, tickSpacing))并求哈希;接下来,我们获取池子合约的代码(type(UniswapV3Pool).creationCode)并求哈希;然后,我们构建这样一个字节序列:0xff,工厂合约地址,哈希后的盐值,哈希后的池子合约代码最后求这个序列的哈希并转换成地址。这些步骤实现了在 EIP-1014 中定义的地址产生方式,这也就是那个增加了 CREATE2 操作码的 EIP。我们来进一步看一下组成这个字节序列的值:0xff 是在 EIP 中定义的,为了区分由 CREATE 和 CREATE2 创建的合约地址。factory 是调用者的地址,也即我们这里的工厂合约盐值在之前提到过——用来唯一定位池子合约代码的哈希用来防止碰撞——不同的合约可以有相同的盐值,但是它们的代码哈希会不相同。根据这样的模式,一个合约的地址由一系列唯一标识这个合约得值哈希得到,包含它的部署者、代码、和唯一参数。我们可以在任何地方使用这个函数来求出池子的地址,而不需要进行任何外部调用或者请求工厂合约。简化 Manager 和 Quoter 的接口

#在管理员合约和报价合约中,我们不再需要向用户请求池子地址!这使得与合约的交互更简单,因为用户不需要知道池子的地址,他们只需要知道 token。然而,用户仍然需要指定 tick 间隔,因为求池子的盐值需要它。而且,我们也不再需要向用户请求 zeroForOne 参数,因为有了 token 的排序我们能够直接求出这个参数了。zeroForOne 在 “from token” 小于 “to token” 的时候为 true,因为池子的 token0 总是小于 token1。类似地,zeroForOne 在 “from token” 大于 “to token” 的时候为 false。地址是哈希,哈希也是数字,所以我们能够用“大于”或者“小于”来比较地址。工厂合约CREATE 和 CREATE2 OpcodesTick 间隔工厂合约实现池子初始化PoolAddress 库简化 Manager 和 Quoter

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)_部署 uniswap v2-CSDN博客

>

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)_部署 uniswap v2-CSDN博客

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)

最新推荐文章于 2023-05-25 20:38:21 发布

zgf1991

最新推荐文章于 2023-05-25 20:38:21 发布

阅读量1.9w

收藏

97

点赞数

17

分类专栏:

以太坊

文章标签:

uniswap

以太坊

DEFI

uniswap交易所

去中心化交易所

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/zgf1991/article/details/109127260

版权

以太坊

专栏收录该内容

7 篇文章

6 订阅

订阅专栏

文章目录

前提条件部署合约部署工厂和WETH合约部署路由合约(重要环节!!)步骤1 获取字节码步骤2 获得initCode步骤3 替换路由中的initCode

当前部署结果部署前端部署前端到其他平台(bsc/heco)需要替换的地方1、interface-2.6.0\node_modules\@uniswap\sdk\dist\constants.d.ts (chainID,工厂)2、interface-2.6.0\node_modules\@uniswap\sdk\dist\sdk.esm.js (chainID,工厂,weth)3、interface-2.6.0\node_modules\@uniswap\default-token-list\build\uniswap-default.tokenlist.json(weth)4、interface-2.6.0\node_modules\@uniswap\sdk\dist\entities\token.d.ts(weth)5、interface-2.6.0\src\connectors\index.ts(网页支持的chainID)6、interface-2.6.0\src\constants\index.ts(路由,weth)7、interface-2.6.0\src\constants\multicall\index.ts(multicall)8、interface-2.6.0\src\constants\v1\index.ts(v1 factory避免报错)9、interface-2.6.0\src\state\lists\hooks.ts(增加chainID配置)10、interface-2.6.0\src\utils\index.ts(浏览器跳转查看hash)11、interface-2.6.0\src\components\Header\index.tsx(显示的网络名称)

其他补充(懂solidity的可以看看)添加流动性交换方法工具in/out计算公式推导

参考链接 崔棉大师的教程

手把手教你部署自己的uniswap交易所

之前部署是跟着崔棉大师的教程走的,但是部署完了,没法实际使用,添加流动性还是交易会报错 这里主要是做补充;

前提条件

自己有账号,且申请测试以太坊 (ropsten直接小狐狸 buy 打开链接领,rinkeby需要推特发链接再去领取)会使用 remix 部署合约部署前端需会使用 npm / yarn

部署合约

合约源代码 此处只部署routerV2

工厂合约WETH路由02

注意事项

部署工厂合约和路由合约时,EVM VERSION 选择 istanbul, COMPILER CONFIGURATION 中勾选 Enable optimization ; WETH部署时 EVM VERSION 选择 default; 工厂合约和WETH合约可以直接部署,路由合约需要修改一个Code

部署工厂和WETH合约

该步骤略, 这两个直接部署即可 weth代码中添加以下代码,便于直接获取任意数量WETH,方便测试大额交易

//直接获取WETH

function mint(uint _value)public payable{

balanceOf[msg.sender] += _value;

Deposit(msg.sender, _value);

}

部署路由合约(重要环节!!)

initCode 重要环节,我刚开始部署的时候就是这一步不清楚导致的部署的合约,无法使用, 至于为啥会不一样,不太清楚!

步骤1 获取字节码

编译工厂合约,获取pair的字节码, 看下图

获得类似这样的

得到以下结构的内容, 只需要object字段的内容; 复制

{

"linkReferences": {},

"object": "取这里的内容",

"opcodes": "-",

"sourceMap": "-"

}

步骤2 获得initCode

打开网址 http://emn178.github.io/online-tools/keccak_256.html

将刚才得到的object字段内容粘贴,选择input type HEX 如下图

步骤3 替换路由中的initCode

将 96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f 替换成 de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17 (步骤2中获取的)

// 路由中该代码

// calculates the CREATE2 address for a pair without making any external calls

function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {

(address token0, address token1) = sortTokens(tokenA, tokenB);

pair = address(uint(keccak256(abi.encodePacked(

hex'ff',

factory,

keccak256(abi.encodePacked(token0, token1)),

hex'de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17' //init code hash

//hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' //init code hash

))));

}

编译部署即可

当前部署结果

添加流动性 ropsten https://ropsten.etherscan.io/tx/0x90a860e95f2796b08985b02a1163eccb58efefd053e0a80ebf75cca8f7f5b8fa

rinkeby https://rinkeby.etherscan.io/tx/0x4c82a23ec995bf404a98e39b490c5e7893945c4341495c7fe6de43b90e646aeb

新账号,所以rinkeby和ropsten两个测试网都是部署的以下地址

工厂

0x2CD020750216583CCF657a0949F0843ec1f73EFE

WETH

0x57E25a96A6dBA2cA02e9C96d08f672574c6E6B13

路由

0x9A36D38C6De905f969C172a85dD362E3Bc36B936

initCode

de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17

测试Token

0xa5BA457F1DfdCC3E65515E69E545292D203b0E76

另外吐槽下… ropsten 获取测试币最简便,但是打包忒慢了 rinkeby 打包很快,不过要发推后再领, 有梯子的建议使用这个

部署前端

前端代码 可以clone最好, 太慢的话就直接下载zip解压 1、自行下载好源码 2、安装好yarn 3、修改代码

修改文件: 项目目录/uniswap-interface/src/constants/index.ts 第 6 行

export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' //修改成你的路由合约地址

修改2 node_modules下面有个@uniswap/sdk/dist/constants.d.ts和sdk.esm.js 这两个文件里面修改factory地址和initCode;改完添加流动性,或者查找pair就没问题了;

如果想修改默认的weth;路径@uniswap/default-token-list/build 新版本的路径可能改了, 全局搜索下主网的weth,找到对应的替换就行

注: 前端代码可以打开IDE, 然后全局搜索替换成自己部署的信息,之后编译代码就行了!!!

//uniswap官方部署的信息

工厂

0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f

WETH

0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

路由

0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D

initCode

96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f

将以上4个信息都替换成自己部署的合约地址

工厂

0x2CD020750216583CCF657a0949F0843ec1f73EFE

WETH

0x57E25a96A6dBA2cA02e9C96d08f672574c6E6B13

路由

0x9A36D38C6De905f969C172a85dD362E3Bc36B936

initCode

de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17

如要替换weth,需要注意环境替换

{

mainnet:'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',

ropsten:'0xc778417E063141139Fce010982780140Aa0cD5Ab', ( "chainId": 3,)

rinkeby:'0xc778417E063141139Fce010982780140Aa0cD5Ab', ("chainId": 4)

goerli:'0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',

kovan:'0xd0A1E359811322d97991E03f863a0C30C2cF029C'

}

替换完后,编译

$ cd uniswap-interface

$ yarn

$ yarn start

//执行 yarn start 后跳出个网页 http://localhost:3000/#/swap

部署前端到其他平台(bsc/heco)需要替换的地方

部分参考崔棉大师最新的部署视频

当前以uniswap-interface-2.6.0为例

以下替换没有整理顺序,以我这边测试所需要的替换全部列出 当前是部署到BSC测试网例子

1、interface-2.6.0\node_modules@uniswap\sdk\dist\constants.d.ts (chainID,工厂)

增加chainID,替换factory,initcode

//line3,增加BSC 97

export declare enum ChainId {

MAINNET = 1,

ROPSTEN = 3,

RINKEBY = 4,

GÖRLI = 5,

KOVAN = 42,

BSC = 97

}

//line20,替换工厂和initcode

export declare const FACTORY_ADDRESS = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f";

export declare const INIT_CODE_HASH = "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f";

2、interface-2.6.0\node_modules@uniswap\sdk\dist\sdk.esm.js (chainID,工厂,weth)

增加chainID,替换factory,initcode 增加weth

同目录下sdk.cjs.development.js,如有需要,也一样的方式替换

//line18,增加BSC

(function (ChainId) {

ChainId[ChainId["MAINNET"] = 1] = "MAINNET";

ChainId[ChainId["ROPSTEN"] = 3] = "ROPSTEN";

ChainId[ChainId["RINKEBY"] = 4] = "RINKEBY";

ChainId[ChainId["G\xD6RLI"] = 5] = "G\xD6RLI";

ChainId[ChainId["KOVAN"] = 42] = "KOVAN";

ChainId[ChainId["BSC"] = 97] = "BSC";

})(ChainId || (ChainId = {}));

//替换factory,initcode

var FACTORY_ADDRESS = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f';

var INIT_CODE_HASH = '0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f';

//line442,增加对应的weth

var WETH = (_WETH = {}, _WETH[ChainId.MAINNET] = /*#__PURE__*/new Token(ChainId.MAINNET, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.ROPSTEN] = /*#__PURE__*/new Token(ChainId.ROPSTEN, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.RINKEBY] = /*#__PURE__*/new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.GÖRLI] = /*#__PURE__*/new Token(ChainId.GÖRLI, '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.KOVAN] = /*#__PURE__*/new Token(ChainId.KOVAN, '0xd0A1E359811322d97991E03f863a0C30C2cF029C', 18, 'WETH', 'Wrapped Ether'), _WETH[ChainId.BSC] = /*#__PURE__*/new Token(ChainId.BSC, '0x替换成自己的WETH地址', 18, 'WETH', 'Wrapped Ether'), _WETH);

3、interface-2.6.0\node_modules@uniswap\default-token-list\build\uniswap-default.tokenlist.json(weth)

WETH

//文件最下面按照格式增加一个weth

{

"name": "Wrapped Ether",

"address": "0x替换自己部署的weth",

"symbol": "WETH",

"decimals": 18,

"chainId": 97,

"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xd0A1E359811322d97991E03f863a0C30C2cF029C/logo.png"

}

4、interface-2.6.0\node_modules@uniswap\sdk\dist\entities\token.d.ts(weth)

WETH

export declare const WETH: {

1: Token;

3: Token;

4: Token;

5: Token;

42: Token;

97: Token;

};

5、interface-2.6.0\src\connectors\index.ts(网页支持的chainID)

增加支持的chainID

//line29,支持的网络 增加97

export const injected = new InjectedConnector({

supportedChainIds: [1, 3, 4, 5, 42, 97]

})

6、interface-2.6.0\src\constants\index.ts(路由,weth)

替换路由,增加weth

//line6,替换路由地址

export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'

//line20,增加BSC

const WETH_ONLY: ChainTokenList = {

[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],

[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],

[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],

[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],

[ChainId.KOVAN]: [WETH[ChainId.KOVAN]],

[ChainId.BSC]: [WETH[ChainId.BSC]]

}

7、interface-2.6.0\src\constants\multicall\index.ts(multicall)

替换multicall 合约地址

合约代码 https://cn.etherscan.com/address/0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441#code

直接复制下,部署一个即可

const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = {

[ChainId.MAINNET]: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',

[ChainId.ROPSTEN]: '0x53C43764255c17BD724F74c4eF150724AC50a3ed',

[ChainId.KOVAN]: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A',

[ChainId.RINKEBY]: '0x42Ad527de7d4e9d9d011aC45B31D8551f8Fe9821',

[ChainId.GÖRLI]: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e',

[ChainId.BSC]: '0x替换成自己部署的地址'

}

8、interface-2.6.0\src\constants\v1\index.ts(v1 factory避免报错)

v1的factory,增加一个空的就好了

const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {

[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',

[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',

[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',

[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',

[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30',

[ChainId.BSC]: ''

}

9、interface-2.6.0\src\state\lists\hooks.ts(增加chainID配置)

//line33,增加一个

/**

* An empty result, useful as a default.

*/

const EMPTY_LIST: TokenAddressMap = {

[ChainId.KOVAN]: {},

[ChainId.RINKEBY]: {},

[ChainId.ROPSTEN]: {},

[ChainId.GÖRLI]: {},

[ChainId.MAINNET]: {},

[ChainId.BSC]: {}

}

10、interface-2.6.0\src\utils\index.ts(浏览器跳转查看hash)

该修改主要用于交易后的提示hash,可以直接点击到浏览器查看 注意host不要斜杠 / 结尾

//line20-30, 直接用下面的替换

const ETHERSCAN_PREFIXES: { [chainId in ChainId]: string } = {

1: 'https://etherscan.io',

3: 'https://ropsten.etherscan.io',

4: 'https://rinkeby.etherscan.io',

5: 'https://goerli.etherscan.io',

42: 'https://kovan.etherscan.io',

97: 'https://testnet.bscscan.com'

}

export function getEtherscanLink(chainId: ChainId, data: string, type: 'transaction' | 'token' | 'address'): string {

const prefix = `${ETHERSCAN_PREFIXES[chainId] || ETHERSCAN_PREFIXES[1]}`

11、interface-2.6.0\src\components\Header\index.tsx(显示的网络名称)

网页右上角显示网络名称

//line129, ChainId.BSC是在sdk中添加的, 对应的值Bsc只是一个展示

const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {

[ChainId.MAINNET]: null,

[ChainId.RINKEBY]: 'Rinkeby',

[ChainId.ROPSTEN]: 'Ropsten',

[ChainId.GÖRLI]: 'Görli',

[ChainId.KOVAN]: 'Kovan',

[ChainId.BSC]: 'Bsc'

}

其他补充(懂solidity的可以看看)

如果会solidity,且看uniswap源码的,可以往下看看

崔棉大师有个Uniswap源码中文注解的文档,有需要的可以去购买

添加流动性

添加流动性需要输入两个token, 带ETH的方法,router会帮你转成WETH,最终实际就是该方法

function addLiquidity(

address tokenA,

address tokenB,

uint amountADesired,

uint amountBDesired,

uint amountAMin,

uint amountBMin,

address to,

uint deadline

)

交换方法

这里主要说明交易的方法 这里主要归纳为3个方法(带ETH的方法,router会帮你转成WETH,最终都是两个ERC20交换)

说明:

交换方法中,不存在买卖的说法,只有in/out; 我下面的方法注释写买/卖是为了便于理解交换方法中所有参数in/out 都是相对于路由自己

//方法1 需要获取精确的输出,输入不确定金额(也可以理解买,如购买100个UNI TOKEN,需要未知weth)

function swapTokensForExactTokens(

uint amountOut,//期望输出金额

uint amountInMax,//最大输入 (如果到你打包的交易时,如果实际需要输入的金额大于该金额,交易失败!)

address[] calldata path,

address to,

uint deadline

)

//方法2 通过输入金额,输出不确定金额 (也可以理解为卖, 如卖掉100个UNI TOKEN,可以获得未知WETH)

function swapExactTokensForTokens(

uint amountIn,//实际输入金额

uint amountOutMin,//最小输出 (如果到打包你的交易时,实际输出小于该金额,交易失败!)

address[] calldata path,

address to,

uint deadline

)

//方法3 通过输入金额,输出不确定金额

function swapExactTokensForTokensSupportingFeeOnTransferTokens(

uint amountIn,

uint amountOutMin,

address[] calldata path,

address to,

uint deadline

)

此处说明上面方法2/3 的区别

方法3是以交易对实际获取到了多少代币,去调用交易对的交换 方法2是以用户调用转账输入的金额(该金额可能是错的,比如没有实际输入,或者扣了手续费),去调用交易对交换

比如黑币,在你转账的时候,扣除你20% 30%等 如果使用方法2 是无法成功 会提示UniswapV2: K 如果使用方法3,把amountOutMin填0,那么这个交易一定可以成功,哪怕交易对只给返回0.00000000001个以太坊

工具

// returns sorted token addresses, used to handle return values from pairs sorted in this order

//两个地址排序

function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {

require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');

(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');

}

// calculates the CREATE2 address for a pair without making any external calls

// 计算交易对地址, 注意这个init code hash... 这是个坑

function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {

(address token0, address token1) = sortTokens(tokenA, tokenB);

pair = address(uint(keccak256(abi.encodePacked(

hex'ff',

factory,

keccak256(abi.encodePacked(token0, token1)),

hex'de683b3097cb455dd2d3ea50f1f95386fdeca75180cc01bb6b12207c44272e17' // init code hash

))));

}

// fetches and sorts the reserves for a pair

//获取当前储备量,返回值会根据你输入的token排序

function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {

(address token0,) = sortTokens(tokenA, tokenB);

(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();

(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);

}

// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset

//添加流动性时,通过tokenA输入额,计算tokenB需要输入多少

function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {

require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');

require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');

amountB = amountA.mul(reserveB) / reserveA;

}

// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset

//通过in计算out (后面详细说明)

function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {

require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');

require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');

uint amountInWithFee = amountIn.mul(997);

uint numerator = amountInWithFee.mul(reserveOut);

uint denominator = reserveIn.mul(1000).add(amountInWithFee);

amountOut = numerator / denominator;

}

// given an output amount of an asset and pair reserves, returns a required input amount of the other asset

//通过out 计算in (后面详细说明)

function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {

require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');

require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');

uint numerator = reserveIn.mul(amountOut).mul(1000);

uint denominator = reserveOut.sub(amountOut).mul(997);

amountIn = (numerator / denominator).add(1);

}

in/out计算公式推导

注:

此处主要说getAmountOut 和 getAmountIn 两个方法 上面工具中的两个方法内部算法是简化过的。

/**

*

*

* 推导公式

* in 输入金额, out 输出金额

* rIn tokenIn的流动性, rOut,tokenOut的流动性

* fee 手续费,注:当前带入0.997 也就是997/1000

*

* 两个计算公式实际是一样的, 只是一个求in,一个求out

* (rIn + in * f) * (rOut - out) = rIn * rOut

*

*

* 由out计算in

* (rIn + in * f) * (rOut - out) = rIn * rOut

* rIn * rOut + in * f * rOut - rIn * out - in * f * out = rIn * rOut

* rIn * out = in * f * rOut - in * f * out

* in = rIn * out / (f * (rOut - out)) + 1 (尾部的 +1应该是避免精度计算,最后一位小了,会成交不了)

*

*

* 由in计算out

* (rIn + in * f) * (rOut - out) = rIn * rOut

* rIn * rOut + in * f * rOut - rIn * out - in * f * out = rIn * rOut

* in * f * rOut = rIn * out + in * f * out

* out = in * f * rOut / rIn + in *f

*

*/

UniswapV2: K 校验手续费

//注:正常amount0 或者amount1有一个是0值

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {

require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');

(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings

require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

uint balance0;

uint balance1;

{ // scope for _token{0,1}, avoids stack too deep errors

address _token0 = token0;

address _token1 = token1;

require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');

//其中一个不是0的转出

if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens

if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens

//闪电贷,略

if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);

balance0 = IERC20(_token0).balanceOf(address(this));

balance1 = IERC20(_token1).balanceOf(address(this));

}

//以下代码校验,查看下面说明

uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;

uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;

require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');

{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors

uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));

uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));

require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');

}

_update(balance0, balance1, _reserve0, _reserve1);

emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);

}

校验K推导说明

针对K值的校验计算

转入金额是实际带手续费的, out金额是in去掉手续费后,计算出来的out.

正常情况 (举例: 0in, 1 out) --即amount0out会是0,不需要转出

(交易前已经将0 in转入)

获取之前的流动性 r0,r1

转出out金额

获取交易对中两个地址的余额 b0,b1

amount0in = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;(0是in,0Out=0,所以前面的比较是true,结果是 )

amount0in = b0>r0 结果是 b0-r0,即实际进入金额(带手续费金额)

amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; (1是out)

正常是r1-out1=b1 所以走false,

得到 amount1In=0;

一定要这两个其中之一大于0

校验手续费

b0a = b0*1000 - amount0in * 3

b1a = b1*1000 - 0*3

req(b0a*b1a >= r0 * r1 * 1000 * 1000)

r0*r1是上一个k值,

公式 (rIn + in * f) * (rOut - out) = rIn * rOut

(b0-fee)*(b1) = r0*r1(上一次的k)

所以判断条件是(b0-fee)*(b1) >= r0*r1(上一次的k)

对比b0a,b1a 实际就是千3的手续费,没法用小数,所以两边都*1000,就可以用3计算

优惠劵

zgf1991

关注

关注

17

点赞

97

收藏

觉得还不错?

一键收藏

知道了

48

评论

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)

文章目录前提条件部署合约部署工厂和WETH合约部署路由合约(重要环节!!)步骤1 获取字节码步骤2 获得initCode步骤3 替换路由中的initCode当前部署结果部署前端其他补充(懂solidity的可以看看)添加流动性交换方法工具in/out计算公式推导参考链接 崔棉大师的教程手把手教你部署自己的uniswap交易所之前部署是跟着崔棉大师的教程走的,但是部署完了,没法实际使用,添加流动性还是交易会报错这里主要是做补充;前提条件自己有账号,且申请测试以太坊 (ropsten直接小狐狸 b

复制链接

扫一扫

专栏目录

智能合约USDT转入转出

07-11

智能合约USDT转入转出,简单逻辑

pragma solidity ^0.8.0;

//引入USDT

interface IERC20 {

function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

function approve(address spender, uint256 amount) external returns (bool);

function transfer(address to, uint256 amount) external returns (bool);

function balanceOf(address account) external view returns (uint256);

function allowance(address _owner, address spender) external view returns (uint256);

}

contract

在Sepolia测试网中部署uniswap合约

最新发布

sinat_33794480的博客

01-08

931

部署uniswap合约

48 条评论

您还未登录,请先

登录

后发表或查看评论

uniswap合约解读和部署

q_776355102的博客

01-21

1万+

了解uniswap

官方中文:http://uniswap.defiplot.com/#/swap

官方英文:https://app.uniswap.org

github源码:Uniswap · GitHub

如何工作:https://uniswap.org/docs/v2/protocol-overview/how-uniswap-works/

术语解释:https://uniswap.org/docs/v2/protocol-overview/glossary/

视频教程:手把手教你开发去

使用官方项目部署 Uniswap-v2

qq_41943430的博客

02-03

1918

使用官方项目部署 Uniswap-v2

BSC预售发布和教程.docx

01-07

教程仅供参考,请勿违法,谢谢配合。

QTUM链部署UniSwapV2

wb25409498的博客

11-21

363

regtest模式下,量子链部署UniSwapV2

UniswapV2代码中文注释

m0_60938243的博客

05-25

325

Uniswap V2是一种基于以太坊的去中心化交易协议,旨在提供快速、安全、无信任的代币交换服务。它是Uniswap协议的第二个版本,是对第一个版本的改进和升级。

在测试机部署react_手把手教你部署自己的uniswap交易所

weixin_34824510的博客

01-05

1559

本文作者:崔棉大师演示地址: https://fankouzu.github.io/cuiswap/ (请在测试网使用)准备Uniswap合约源码源码结构Uniswap在Github上面开源了全部合约代码,其中包括核心合约,周边合约两部分.Uniswap还开源了前端代码,前端代码使用React开发核心合约周边合约前端代码在Uniswap的核心代码中,主要包含3个合约:工厂合约,配对合约,ERC20...

二次开发uniswap-01-SDK

【03】的博客

03-11

2157

实现一个swap

uniswap源码+注释

wjl__ai__的博客

11-29

1万+

最近,看到一个比较厉害的机器人,每次夹人可以获取0.2个ETH,利润还是十分的诱人。

兴趣既然萌生了,那么也去研究了解一下。

那么,首先还是要读一下去中心化交易所的源码(uniswap)。

uniswap底层源码分三个模块,Factory,Pair,Router。

Factory存放在多个Pair,Router是通过调用Factory,Factory再来找到Pair,更加详细的交易逻辑,还得看底层源码。

Factory

pragma solidity =0.5.16;

import './interfa

bsc:BSC的前端开发人员,2015年版本

05-26

平衡计分卡

BSC的前端开发人员,2015年版本

安装

git clone https://github.com/evaluecz/bsc.git

cd bsc

npm install

启动应用程序

npm start app.js 然后,该应用程序在

量角器测试

在test目录中protractor test/protractor-cfg.js

Pancakeswap 和 Uniswap 交易客户端

03-19

Pancakeswap 和 Uniswap 交易客户端

Pancakeswap 和 Uniswap 交易客户端(和机器人),带有市价单、限价单、止损、自定义 gas 策略、GUI 等等。

如果您有任何问题或疑问,或想要此机器人的完整版本,您可以通过 Discord 联系我的支持:AviddotSupport#8131 或电报: 或 (请注意:这些不是机器人的制造商,他们提供支持) 。 另请查看网站:

变更日志 v2.0

添加了多个 DEX(Pcs v1、Uniswap v2)

强制买入和强制卖出按钮,当点击它时,它会以您选择的设置买入或卖出(不包括限价)

速度提升

许多,许多错误修复(不再强制关闭或不工作的令牌)

添加 USDT 作为主币选项

该程序现在自动确定令牌的名称和小数

先决条件

一个以太坊/bsc 地址

一台 Windows 机器(即将添加对 Mac OS 和 Linu

BSC、HECO通用限制买卖合约

12-29

可动态设置买入次数卖出次数

uniswap v2 代码解读注释

热门推荐

zgf1991 IT新人

07-16

1万+

文章目录注意事项UniswapV2FactoryUniswapV2Router02uniswap 闪电贷(待补充)uniswap 添加/移除lp时手续费公式(待补充)

注意事项

部分说明

首次添加和二次添加lp算法有点差别

查找pair和创建pair,使用了create2,有需要可以具体了解

uniswap v2都是两个erc20组合成一个交易对,跟eth相关的内部都是使用的weth交易

交易方法中都是in/out,相对于pair, 方法中swapExactTokensForTokens,for的两侧带有

estimateGas gas required exceeds allowance (6989519) or always failing transaction

zgf1991 IT新人

06-10

1万+

在使用estimateGas计算gas的时候报错,

Node error: {"code":-32000,"message":"gas required exceeds allowance (6989519) or always failing transaction"}

开始都是参考官网的例子

var result = web3.eth.estimateGas({

to: "0x...

以太坊签名,验证签名, EIP712domain Permit授权并转账

zgf1991 IT新人

01-27

9526

文章目录一、Dapp 验签登录二、token EIP712DomainDomain 格式Permit 格式如何签名node签名网页小狐狸签名根据Dai的代码修改的demo

需求:

dapp 签名/验签登录 主要针对中心化接口鉴权;小狐狸签名时最好能让用户看到签名内容

学习EIP712Domain

一、Dapp 验签登录

参考链接

第二十九课 如何实现MetaMask签名授权后DAPP一键登录功能?

以太坊签名数据以及验证

两种签名

1、直接对内容签名(小狐狸可以看到hello)

web3.persona

remix Gas estimation failed

zgf1991 IT新人

05-29

5023

使用remix调用合约的approve方法给予授权,第一次没啥问题,之后都提示Gas estimation failed,以为gas给少了。 搞了一下午,后面注意到还有两个方法decreaseApproval,increaseApproval,发现使用后授权量有改变...

这个帖子给了点提示【Solidity】Gas estimation failed-Gas estimation erro...

bsc heco eth浏览器开源智能合约代码,图文说明

zgf1991 IT新人

03-02

4668

文章目录配置基本配置其他配置library构造参数方式一方式二开源流程示例情况1 基本配置都正确,没有构造参数和library情况2 补上library,不填构造参数情况3 填写构造参数(一般可以通过,也可能无法通过)情况4 从浏览器提示中取构造参数其他

在浏览器上开源合约代码,会碰到一些常见问题,这里做一个汇总以及相关解决方案

配置

基本配置

以下基本配置必须一样

编译版本 (这个指的是remix编译的版本,而不是合约代码里面写的版本)

EVM 版本 (一般都是default,如果有需要选了版本时,开源

uniapp 合约转账bsc

06-13

要使用 Uni-app 进行合约转账操作,需要先安装 web3.js,然后在代码中引入该库。接着,你需要连接到 BSC 网络,可以使用 Infura 提供的节点。

下面是一个示例代码:

```javascript

import Web3 from 'web3';

// 连接到 BSC 网络

const web3 = new Web3(new Web3.providers.HttpProvider('https://bsc-dataseed1.binance.org:443'));

// 合约地址和 ABI

const contractAddress = '0x0000000000000000000000000000000000000000';

const contractAbi = [{...}];

// 创建合约实例

const contract = new web3.eth.Contract(contractAbi, contractAddress);

// 转账

const fromAddress = '0x...'; // 发送方地址

const privateKey = '0x...'; // 发送方私钥

const recipientAddress = '0x...'; // 接收方地址

const amount = web3.utils.toWei('1', 'ether'); // 转账金额

const tx = {

from: fromAddress,

to: contractAddress,

gas: 200000,

data: contract.methods.transfer(recipientAddress, amount).encodeABI()

};

web3.eth.accounts.signTransaction(tx, privateKey).then(signed => {

web3.eth.sendSignedTransaction(signed.rawTransaction).on('receipt', console.log);

});

```

其中,`contractAddress` 和 `contractAbi` 分别是你要调用的合约的地址和 ABI。`fromAddress` 是发送方的地址,`privateKey` 是发送方的私钥,`recipientAddress` 是接收方的地址,`amount` 是转账金额,需要将其转化为 wei 单位。

最后,使用 `web3.eth.sendSignedTransaction` 方法发送签名后的交易,通过监听 `receipt` 事件获取交易的结果。

“相关推荐”对你有帮助么?

非常没帮助

没帮助

一般

有帮助

非常有帮助

提交

zgf1991

CSDN认证博客专家

CSDN认证企业博客

码龄14年

暂无认证

98

原创

6万+

周排名

55万+

总排名

87万+

访问

等级

8543

积分

151

粉丝

103

获赞

221

评论

299

收藏

私信

关注

热门文章

android 通过http网络下载图片 并

32034

Android 中 使用 Gson解析json

24364

Caused by: java.lang.IllegalArgumentException: the bind value at index 1 is null

23535

Android中Paint字体属性的设置

22888

百度地图获取自己位置

22068

分类专栏

filecoin-lotus

3篇

android

39篇

android 百度地图

3篇

java基础学习

7篇

正则

微信小程序

GO

5篇

以太坊

7篇

nodejs

1篇

最新评论

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)

zgf1991:

没试过,都是一样的,按照指定版本应该还是可以的,

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)

一个Arno:

大佬有尝试过部署到arbi上嘛,我按照文章中的方法来了好像不行欸

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)

zgf1991:

情况太多了, 是不是没授权

完整部署uniswap 合约、前端教程(可部署uniswap到bsc、heco)

agys14:

主币(bnb)可以兑换代币,代币不能兑换主币是什么原因?

solidity基础以及版本变化

zale013:

合约那么重要得每个细节搞懂

您愿意向朋友推荐“博客详情页”吗?

强烈不推荐

不推荐

一般般

推荐

强烈推荐

提交

最新文章

bsc heco eth浏览器开源智能合约代码,图文说明

solidity基础以及版本变化

uniswap v2 代码解读注释

2022年2篇

2021年2篇

2020年6篇

2019年7篇

2018年3篇

2017年5篇

2016年1篇

2015年14篇

2014年24篇

2013年36篇

2012年56篇

2011年6篇

目录

目录

分类专栏

filecoin-lotus

3篇

android

39篇

android 百度地图

3篇

java基础学习

7篇

正则

微信小程序

GO

5篇

以太坊

7篇

nodejs

1篇

目录

评论 48

被折叠的  条评论

为什么被折叠?

到【灌水乐园】发言

查看更多评论

添加红包

祝福语

请填写红包祝福语或标题

红包数量

红包个数最小为10个

红包总金额

红包金额最低5元

余额支付

当前余额3.43元

前往充值 >

需支付:10.00元

取消

确定

下一步

知道了

成就一亿技术人!

领取后你会自动成为博主和红包主的粉丝

规则

hope_wisdom 发出的红包

实付元

使用余额支付

点击重新获取

扫码支付

钱包余额

0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。 2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

手把手教你部署自己的uniswap交易所 - 知乎

手把手教你部署自己的uniswap交易所 - 知乎首发于登链社区切换模式写文章登录/注册手把手教你部署自己的uniswap交易所登链社区区块链技术爱好者的家园本文作者:崔棉大师演示地址: https://fankouzu.github.io/cuiswap/ (请在测试网使用)准备Uniswap合约源码源码结构Uniswap在Github上面开源了全部合约代码,其中包括核心合约,周边合约两部分.Uniswap还开源了前端代码,前端代码使用React开发核心合约周边合约前端代码在Uniswap的核心代码中,主要包含3个合约:工厂合约,配对合约,ERC20合约.其中配对合约继承了ERC20合约,我们可以把它们看作一个合约.工厂合约通过create2方法部署配对合约,所以在部署合约时只需要部署工厂合约.周边合约中包括一些示例代码,例如价格预言机,闪电交换,其中最重要的是路由合约.在周边合约的代码库中,包含两个路由合约:UnsiwapV2Router01,UnsiwapV2Router02.工厂合约和配对合约需要通过路由合约调用才能更好的完成交易所的全部功能,所以我们还要部署路由合约两个合约大部分相同,有小部分不同,如果将两个合约的差异化合并成一个合约,部署的时候将会出现out of gas,所以才被分成了两个合约.常用功能两个合约中都包括,所以我们部署其中任意一个路由合约都可以继承引用调用ERC20合约配对合约工厂合约路由合约从浏览器中下载合约源码如果你对合约代码并不熟悉,也可以跳过上面这部分,接下来我们将从以太坊浏览器中直接拷贝线上版合约源码工厂合约路由合约01[可选]路由合约02部署合约准备部署账户Uniswap的路由合约部署在以太坊的主网和Ropsten,Rinkeby,Goerli,Kovan几个测试网的合约地址都是相同的,这样可以使Uniswap的前端不管切换到任何一个网络,路由地址都不会变.要想实现这个相同地址的部署,我们需要准备一个全新的账户用来部署合约.全新的账户指的是在部署合约之前的nonce值为0.因为合约的地址是根据你的账户地址和nonce值计算出来的,所以在不同网络中,如果nonce值相同,部署出的合约地址也相同.通过助记词生成新账户可以通过我之前录制的视频学习操作方法B站视频油管视频生成好助记词之后,记得用英文助记词,保存好助记词,还有助记词对应的账户地址向新地址转帐ETH部署合约需要的gas费约为0.18个Ether,目前主网可能需要的更多.通过一个已有Ether的账户向新账户转帐.测试网的Ether可以通过每个测试网的水龙头申请到测试币.获取测试币方法转账完成后,将助记词导入到Metamask中准备WETH合约地址在部署路由合约时,构造函数中需要填入工厂合约的地址和WETH合约的地址,由于WETH合约的地址在主网和测试网的地址都不相同,所以需要找到每个网络中WETH合约的地址.WETH合约用于将Eth交换为erc20的Eth,由于Eth不是erc20的token,所以我们必须使用WETH作为交换媒介{

mainnet:'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',

ropsten:'0xc778417E063141139Fce010982780140Aa0cD5Ab',

rinkeby:'0xc778417E063141139Fce010982780140Aa0cD5Ab',

goerli:'0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',

kovan:'0xd0A1E359811322d97991E03f863a0C30C2cF029C'

}申请infuraKey在部署合约之前,我们还需要使用infura作为免费节点,所以需要申请一个infuraKey申请地址:infura.io申请方法使用remix部署合约将工厂合约和路由合约的线上版本导入到remix中,在编译合约的选项中,EVM VERSION选择istanbul,COMPILER CONFIGURATION中选择Enable optimization部署顺序和构造函数部署工厂合约构造函数的参数是一个自己常用的账户地址部署路由合约01 [可选], 02构造函数的参数1是工厂合约的地址参数2 是当前网络中WETH合约的地址,参考前文部署Uniswap前端克隆前端代码在项目目录运行命令:$ git clone https://github.com/Uniswap/uniswap-interface.git安装依赖库在项目目录运行命令:$ cd uniswap-interface

$ yarn安装完成后,可以先测试运行一下,在uniswap-interface目录运行命令$ yarn start如果运行成功,将会打开一个浏览器,同时打开Uniswap的前端界面修改路由地址在Uniswap的前端中以常量的形式定义了Uniswap的路由地址,我们只需要修改路由地址就可以让前端链接到你的路由合约中修改文件: 项目目录/uniswap-interface/src/constants/index.ts 第6行import { AbstractConnector } from '@web3-react/abstract-connector'

import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'

import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'

export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' //修改成你的路由合约地址

......保存后运行yarn start即可看到效果将代码部署到GitHub Pages创建GitHub项目创建项目的方法就不在这里讲了,不会的同学可以去搜索一下将前端代码添加到GitHub项目仓库首先要删除原先Uniswap项目中的.git目录,在项目目录运行命令:$ cd uniswap-interface

$ rm -rf .git然后初始化git,并将Unsiwap前端代码添加到自己的项目仓库中git init

git remote add origin https://github.com/用户名/项目名.git安装并部署gh-pages我们将通过gh-pages模块将前端代码部署到github.io,在前端代码的目录运行:$ yarn add gh-pages接下来要编译react和部署gh-pages,在前端代码的目录运行:$ yarn build修改前端代码目录中的package.json$ vim package.json

{

"name": "@uniswap/interface",

"description": "Uniswap Interface",

"homepage": "https://用户名.github.io/项目名称",//修改这里

......

// 添加部署的脚本,还是在package.json中

......

"scripts": {

......

"deploy": "gh-pages -d build" //添加这一行

},保存退出之后,在前端代码的目录运行:$ git add .

$ git commit -m "first commit"

$ git push

$ yarn deploy现在在浏览器中打开https://用户名.github.io/项目名称/index.html就可以打开自己的交易所啦.如果不输入地址结尾的index.html在项目刚部署之后会报错,过一段时间就可以不输入了.扩展部署自己的weth可以将以太坊浏览器中的weth源码拷贝下来,自己部署一个属于自己的weth合约可信token列表Uniswap有一个自己的可信token列表,同样被设置在项目目录/uniswap-interface/src/constants/index.ts文件中,在最后一行就是.你可以将这个链接地址的文件拷贝下来,设置成自己需要的可信token地址列表,然后上传到github目录中,再修改index.ts文件中的链接地址,这样就可以让你自己的交易所中拥有自己设置的可信token列表了原文链接:https://learnblockchain.cn...作者主页:https://learnblockchain.cn... ,欢迎阅读作者更多好文登链社区-区块链技术爱好者的家园发布于 2020-09-02 15:19交易所区块链技术​赞同 16​​2 条评论​分享​喜欢​收藏​申请转载​文章被以下专栏收录登链社区高质量区块链技术文

金融衍生产品(基于基础金融工具的金融合约)_百度百科

产品(基于基础金融工具的金融合约)_百度百科 网页新闻贴吧知道网盘图片视频地图文库资讯采购百科百度首页登录注册进入词条全站搜索帮助首页秒懂百科特色百科知识专题加入百科百科团队权威合作下载百科APP个人中心SWaP是一个多义词,请在下列义项上选择浏览(共10个义项)展开添加义项金融衍生产品播报讨论上传视频基于基础金融工具的金融合约收藏查看我的收藏0有用+10同义词SWaP(金融衍生工具)一般指金融衍生产品(基于基础金融工具的金融合约)金融衍生品(derivatives),是指一种基于基础金融工具的金融合约,其价值取决于一种或多种基础资产或指数,合约的基本种类包括远期合约、期货、掉期(互换)和期权。金融衍生品还包括具有远期、期货、掉期(互换)和期权中一种或多种特征的混合金融工具。 [1]这种合约可以是标准化的,也可以是非标准化的。标准化合约是指其标的物(基础资产)的交易价格、交易时间、资产特征、交易方式等都是事先标准化的,因此此类合约大多在交易所上市交易,如期货。非标准化合约是指以上各项由交易的双方自行约定,因此具有很强的灵活性,比如远期协议。金融衍生产品是与金融相关的派生物,通常是指从原生资产(英文为Underlying Assets)派生出来的金融工具。其共同特征是保证金交易,即只要支付一定比例的保证金就可进行全额交易,不需实际上的本金转移,合约的了结一般也采用现金差价结算的方式进行,只有在满期日以实物交割方式履约的合约才需要买方交足贷款。因此,金融衍生产品交易具有杠杆效应。保证金越低,杠杆效应越大,风险也就越大。中文名金融衍生品外文名derivatives别    名金融衍生工具价值依赖基础资产价值变动性    质合约目录1产品特点2作用3种类4风险成因5风险类别6风险管理7区域分布▪交易所▪OTC8投资结构▪金融机构▪非机构9发展历程10会计计量▪计量原则▪原则优势▪原则缺陷11课程开设12发展对策产品特点播报编辑金融衍生产品具有以下几个特点:1、零和博弈即合约交易的双方(在标准化合约中由于可以交易是不确定的)盈亏完全负相关,并且净损益为零,因此称"零和"。2、跨期性金融衍生产品金融衍生工具是交易双方通过对利率、汇率、股价等因素变动的趋势的预测,约定在未来某一时间按一定的条件进行交易或选择是否交易的合约。无论是哪一种金融衍生工具,都会影响交易者在未来一段时间内或未来某时间上的现金流,跨期交易的特点十分突出。这就要求交易的双方对利率、汇率、股价等价格因素的未来变动趋势作出判断,而判断的准确与否直接决定了交易者的交易盈亏。3、联动性这里指金融衍生工具的价值与基础产品或基础变量紧密联系,规则变动。通常,金融衍生工具与基础变量相联系的支付特征有衍生工具合约所规定,其联动关系既可以是简单的线性关系,也可以表达为非线性函数或者分段函数。4、不确定性或高风险性金融衍生工具的交易后果取决于交易者对基础工具未来价格的预测和判断的准确程度。基础工具价格的变幻莫测决定了金融衍生工具交易盈亏的不稳定性,这是金融衍生工具具有高风险的重要诱因。5、高杠杆性衍生产品的交易采用保证金(margin)制度.即交易所需的最低资金只需满足基础资产价值的某个百分比.保证金可以分为初始保证金(initial margin),维持保证金(maintains margin),并且在交易所交易时采取盯市(marking to market)制度,如果交易过程中的保证金比例低于维持保证金比例,那么将收到追加保证金通知(追加保证金,其将被强行平仓.可见,衍生品交易具有高风险高收益的特点.6、契约性金融衍生产品交易是对基础工具在未来某种条件下的权利和义务的处理,从法律上理解是合同,是一种建立在高度发达的社会信用基础上的经济合同关系。7、交易对象的虚拟性金融衍生产品合约交易的对象是对基础金融工具在未来各种条件下处置的权利和义务,如期权的买权或卖权、互换的债务交换义务等,构成所谓“产品”,表现出一定的虚拟性。8、交易目的的多重性金融衍生产品交易通常有套期保值、投机、套利和资产负债管理等四大目的。其交易的主要目的并不在于所涉及的基础金融商品所有权的转移,而在于转移与该金融商品相关的价值变化的风险或通过风险投资获取经济利益。此外,金融衍生产品还具有未来性、表外性和射幸性等特点作用播报编辑金融衍生产品金融衍生产品的作用有规避风险,价格发现,它是对冲资产风险的好方法。但是,任何事情有好的一面也有坏的一面,风险规避了一定是有人去承担了,衍生产品的高杠杆性就是将巨大的风险转移给了愿意承担的人手中。交易者可以分为三类:套期保值者或对冲者(hedger)、投机者(speculator)以及套利者(arbitrageur)。对冲者采用衍生产品合约来减少自身面临的由于市场变化而产生的风险。投机者利用这些产品对今后市场变量的走向下赌注。套利者采用两个或更多相互抵消的交易来锁定盈利。这三类交易者共同维护了金融衍生产品市场上述功能的发挥。金融衍生产品交易不当将导致巨大的风险,有的甚至是灾难性的,国外的有巴林银行事件,宝洁事件,LTCM事件,信孚银行,国内的有国储铜事件,中航油事件。种类播报编辑(1)根据产品形态,可以分为远期、期货、期权和掉期四大类。金融衍生产品远期合约和期货合约都是交易双方约定在未来某一特定时间、以某一特定价格、买卖某一特定数量和质量资产的交易形式。期货合约是期货交易所制定的标准化合约,对合约到期日及其买卖的资产的种类、数量、质量作出了统一规定。远期合约是根据买卖双方的特殊需求由买卖双方自行签订的合约。因此,期货交易流动性较高,远期交易流动性较低。掉期合约是一种内交易双方签订的在未来某一时期相互交换某种资产的合约。更为准确地说,掉期合约是当事人之间签订的在未来某一期间内相互交换他们认为具有相等经济价值的现金流(Cash Flow)的合约。较为常见的是利率掉期合约和货币掉期合约。掉期合约中规定的交换货币是同种货币,则为利率掉期;是异种货币,则为货币掉期。期权交易是买卖权利的交易。期权合约规定了在某一特定时间、以某一特定价格买卖某一特定种类、数量、质量原生资产的权利。期权合同有在交易所上市的标准化合同,也有在柜台交易的非标准化合同。(2)根据原生资产大致可以分为四类,即股票、利率、货币和商品。如果再加以细分,股票类中又包括具体的股票和由股票组合形成的股票指数;利率类中又可分为以短期存款利率为代表的短期利率和以长期债券利率为代表的长期利率;货币类中包括各种不同币种之间的比值:商品类中包括各类大宗实物商品。具体见表3—1表3—1 根据原生资产对金融衍生产品的分类对象原生资产金融衍生产品利率短期存款利率期货、利率远期、利率期权、利率掉期合约等-长期债券债券期货、债券期权合约等股票股票股票期货、股票期权合约等-股票指数股票指数期货、股票指数期权合约等货币各类现汇货币远期、货币期货、货币期权、货币掉期合约等商品各类实物商品商品远期、商品期货,商品期权、商品掉期合约等(3)根据交易方法,可分为场内交易和场外交易。场内交易,又称交易所交易,指所有的供求方集中在交易所进行竞价交易的交易方式。这种交易方式具有交易所向交易参与者收取保证金、同时负责进行清算和承担履约担保责任的特点。此外,由于每个投资者都有不同的需求,交易所事先设计出标准化的金融合同,由投资者选择与自身需求最接近的合同和数量进行交易。所有的交易者集中在一个场所进行交易,这就增加了交易的密度,一般可以形成流动性较高的市场。期货交易和部分标准化期权合同交易都属于这种交易方式。场外交易,又称柜台交易,指交易双方直接成为交易对手的交易方式。这种交易方式有许多形态,可以根据每个使用者的不同需求设计出不同内容的产品。同时,为了满足客户的具体要求、出售衍生产品的金融机构需要有高超的金融技术和风险管理能力。场外交易不断产生金融创新。但是,由于每个交易的清算是由交易双方相互负责进行的,交易参与者仅限于信用程度高的客户。掉期交易和远期交易是具有代表性的柜台交易的衍生产品。据统计,在金融衍生产品的持仓量中,按交易形态分类,远期交易的持仓量最大,占整体持仓量的42%,以下依次是掉期(27%)、期货(18%)和期权(13%)。按交易对象分类,以利率掉期、利率远期交易等为代表的有关利率的金融衍生产品交易占市场份额最大,为62%,以下依次是货币衍生产品(37%)和股票、商品衍生产品(1%),1989年到1995年的6年间,金融衍生产品市场规模扩大了5.7倍。各种交易形态和各种交易对象之间的差距并不大,整体上呈高速扩大的趋势。到目前为止,国际金融领域中,流行的衍生产品有如下四种:互换、期货、期权和远期利率协议。采取这些衍生产品的最主要目的均为保值或投机。但是这些衍生产品所以能存在与发展都有其前提条件,那就是发达的远期市场。风险成因播报编辑金融衍生产品风险产生的微观主体原因 内部控制薄弱,对交易员缺乏有效的监督,是造成金融衍生产品风险的一个重要原因。例如,内部风险管理混乱到了极点是巴林银行覆灭的主要原因。首先,巴林银行内部缺乏基本的风险防范机制,里森一人身兼清算和交易两职,缺乏制衡,很容易通过改写交易记录来掩盖风险或亏损。同时,巴林银行也缺乏一个独立的风险控制检查部门对里森所为进行监控;其次,巴林银行管理层监管不严,风险意识薄弱。在日本关西大地震之后,里森因其衍生合约保证金不足而求助于总部时,总部竟然还将数亿美元调至新加坡分行,为其提供无限制的资金支持;再者,巴林银行领导层分裂,内部各业务环节之间关系紧张,令许多知情管理人员忽视市场人士和内部审检小组多次发出的警告,以至最后导致整个巴林集团的覆没。另外,过度的激励机制激发了交易员的冒险精神,增大了交易过程中的风险系数。金融衍生产品风险的宏观成因金融监管不力也是造成金融衍生产品风险的另一主要原因。英国和新加坡的金融监管当局事先监管不力,或未协力合作,是导致巴林银行倒闭的重要原因之一。英国监管部门出现的问题是:第一,负责监管巴林等投资银行的部门曾口头上给与宽免,巴林将巨额款项汇出炒卖日经指数时,无需请示英格兰银行。第二,英格兰银行允许巴林集团内部银行给予证券部门无限制资金的支持。新加坡金融监管当局存在的问题是:首先,新加坡国际金融交易所面对激烈的国际竞争,为了促进业务的发展,在持仓量的控制方面过于宽松,没有严格执行持仓上限,允许单一交易账户大量积累日经期指和日债期货仓位,对会员公司可持有合约数量和缴纳保证金情况没有进行及时监督。其次,里森频繁地从事对倒交易,且交易数额异常庞大,却竟然没有引起交易所的关注。如果英格兰银行、新加坡和大阪交易所之间能够加强交流,共享充分的信息,就会及时发现巴林银行在两个交易所持有的巨额头寸,或许巴林银行不会倒闭。美国长期资本管理公司(LTCM)曾是美国最大的对冲基金,却在俄罗斯上演了人类有史以来最大的金融滑铁卢。监管中存在真空状态是导致其巨额亏损的制度性原因,甚至在LTCM出事后,美国的金融管理当局都还不清楚其资产负债情况。由于政府对银行、证券机构监管的放松,使得许多国际商业银行集团和证券机构无限制地为其提供巨额融资,瑞士银行(UBS)和意大利外汇管理部门(UIC)因此分别损失7.1亿美元和2.5亿美元。另外中国“327国债”期货风波,除当时市场需求不强、发展衍生工具的条件不够以外,过度投机和监管能力不足是不可忽视的原因。风险类别播报编辑(一)市场风险(二)信用风险(三)流动性风险(四)操作风险(五)法律风险风险管理播报编辑对于金融衍生产品的监管,国际上基本上采取企业自控、行业协会和交易所自律、政府部门监管的三级风险管理模式。微观金融主体内部自我监督管理金融衍生产品首先,建立风险决策机制和内部监管制度,包括限定交易的目的、对象、目标价格、合约类型、持仓数量、止损点位、交易流程以及不同部门的职责分配等。其次,加强内部控制,严格控制交易程序,将操作权、结算权、监督权分开,有严格的层次分明的业务授权,加大对越权交易的处罚力度;再次,设立专门的风险管理部门,通过“风险价值方法”(VAR)和“压力试验法”对交易人员的交易进行记录、确认、市价计值,评价、度量和防范在金融衍生产品交易过程中面临的信用风险、市场风险、流动性风险、结算风险、操作风险等。交易所内部监管交易所是衍生产品交易组织者和市场管理者,它通过制定场内交易规则,监督市场的业务操作,保证交易在公开、公正、竞争的条件下进行。第一,创建完备的金融衍生市场制度,包括:严格的市场信息披露制度,增强透明度;大额报告制度;完善的市场准入制度,对衍生市场交易者的市场信用状况进行调查和评估,制定资本充足要求;以及其他场内和场外市场交易规则等等。第二,建立衍生市场的担保制度,包括:合理制定并及时调整保证金比例,起到第一道防线的作用;持仓限额制度,发挥第二道防线的作用;日间保证金追加条款;逐日盯市制度或称按市价计值(mark to market)加强清算、结算和支付系统的管理;价格限额制度等。第三,加强财务监督,根据衍生产品的特点,改革传统的会计记账方法和原则,制定统一的信息披露规则和程序,以便管理层和用户可以清晰明了地掌握风险敞口情况。政府部门的宏观调控与监管首先,完善立法,对金融衍生产品设立专门完备的法律,制定有关交易管理的统一标准;其次,加强对从事金融衍生产品交易的金融机构的监管,规定从事交易的金融机构的最低资本额,确定风险承担限额,对金融机构进行定期与不定期的现场与非现场的检查,形成有效的控制与约束机制;负责审批衍生产品交易所的成立和交易所申请的衍生产品品种。再次,严格区分银行业务与非银行业务,控制金融机构业务交叉的程度。同时,中央银行在某个金融机构因突发事件发生危机时,应及时采取相应的挽救措施,迅速注入资金或进行暂时干预,以避免金融市场产生过度震荡。另外,金融衍生产品交易在世界范围内超国界和超政府地蓬勃开展,单一国家和地区已无法对其风险进行全面的控制,因此加强对金融衍生产品的国际监管和国际合作,成为国际金融界和各国金融当局的共识。在巴林银行事件之后,国际清算银行已着手对衍生产品交易进行全面的调查与监督,加强对银行表外业务资本充足性的监督。区域分布播报编辑金融衍生品区域分布结构:交易所欧美发达国家集中了世界上绝大部分的交易所金融衍生品交易,全球80%以上的交易分布在北美和欧洲,这种集中趋势更加明显。1999年末的未清偿金融期货和期权合约名义价值中,有全球80.5%属于北美和欧洲,到2002年6月末,这一比例上升到了93.7%,北美地区的合约价值占到总价值的64.6%。美国是全球交易所金融衍生品交易的主要市场,但其地位正在趋于下降,美国交易所成交的金融衍生品合约在1986、1988、1990、1992、1994年分别占全球交易量的91.4%、74.7%、65.1%、53.5%、44.7%;欧洲市场的增长最为显著,1994年的交易量是1986年的399%;其间日本的交易量大约增长了7倍。从交易额统计看,直至1986年,美国尚占有交易所市场交易额和未清偿合约价值的80%份额。1990年后,美国以外的市场日趋活跃,交易增长率开始超过美国,到1995年,美国以外市场的交易额已超出美国,而未清偿合约价值稍逊于美国。从交易量统计看,1990年后美国以外市场衍生品交易的活跃趋势更加明显。OTC与交易所市场类似,OTC 金融衍生品市场也主要分布在欧美国家。英国一直保持着OTC市场的领先地位,而且市场份额不断上升,之外的OTC交易主要分布在美国、德国、法国、日本等国家(见表6)。伦敦是OTC金融衍生品市场最重要的中心,2001年日均交易额达6280亿美元,较1998年增长6%。纽约日均交易额位居第二,为2850亿美元,较1998年下降3%,法兰克福交易额名列第三,业已取代东京在OTC市场中的位置,法兰克福地位的上升明显得益于引入欧元和欧洲中央银行(ECB)的设立。投资结构播报编辑金融机构金融机构是金融衍生品市场的主要参与者,以美国为例,参与衍生品交易的金融机构主要有商业银行、非银行储贷机构(Thrift)和人寿保险公司三类,其中商业银行是最早和最熟练的参与者。根据三十国集团1993年的一份报告,被调查的大部分金融机构参与了金融衍生品交易,其中有92%的机构使用过利率互换,69%的机构使用过远期外汇合约,69%的机构使用过利率期权,46%的机构使用过货币互换,23%的机构使用过货币期权。BIS的统计显示,金融机构在全球OTC金融衍生品市场中的交易额稳步增长,2001年较1995年提高60%。交易主要发生在金融机构之间,日均交易额由1995年的7100亿美元提升至2001年的1.2兆美元,金融机构间交易占市场的份额由1995年的80.7%提高到2001年的86.7%。银行无疑是金融衍生品市场中的主角(尤其在OTC市场上),1970年代末以来,银行日益热衷于金融衍生品交易,例如,美国银行业在金融衍生品交易中十分活跃,从1990年到1995年,银行持有与衍生品相关的资产增长了约35%,达到3.1兆美元,其间银行持有的衍生品合约名义价值增加了2倍。银行是金融互换市场的主要参与者,1992年末全球利率互换合约的未清偿价值达6兆美元,持有头寸最大的20家金融机构占到三分之二强,其中银行占了18家。非机构非金融机构在金融衍生品交易中显然不如金融机构活跃,例如非金融机构只占到OTC金融衍生品交易额的10%,与1995年相比其市场份额有明显萎缩。根据三十国集团1993年报告,被调查的非金融类公司中,使用过有利率互换、货币互换、远期外汇合约、利率期权和货币期权的公司的比例分别是87%、64%、78%、40%和31%。发展历程播报编辑1865年,芝加哥谷物交易所推出了一种被称为“期货合约”的标准化协议,取代1851年以来沿用的远期合同,成为人类历史上最早开发出来的金融衍生品。期货市场早期的参与者,主要是以对冲远期风险为目的的套期保值者。事实上,以期货、期权、远期、互换等为代表的金融衍生品,已经成为了能够有效管理和降低市场参与者风险的工具。在金融衍生品150年的发展历程中,美国一直是“领头羊”,在这里有最广泛的市场和最尖端的技术。我国的金融业正处在蓬勃发展期,而金融衍生品市场的发展速度则明显滞后,主要表现在三个方面:金融衍生产品本身、金融衍生品市场监管、行业自律和规范。目前国内的金融衍生品产品种类虽然正在逐步增加,但是和欧美以及香港等发达国家或地区相比还是不可同日而语的。成熟的金融市场体系中,衍生品的种类和数量往往应该远超过股票、债券等金融产品,而我国在这方面还有很长的路要走,不仅品种较少,而且产品同质化比较严重,这样就更限制了产品应用领域的拓展。而金融衍生品的本质是为了分散风险,为投资者服务,由此可见,只有对金融衍生品不断地的深入研究与创新拓展,才能逐步满足广大投资者的投资需求。我国对金融衍生品的法律监管相对滞后,相关法律体系尚待完善,其中一方面原因是由于缺乏行业发展经验和不了解产品之间的内在联动性的原因。而这也正是中国金融衍生品研究院(以下简称“中金院”)研究工作的重点内容之一。中金院结合当前国情与发达国家成功经验,以自身坚实的研究基础为依托,形成专业性的、具备参考价值和实践意义的行业报告,进而履行中金院专业引导和策略建议的社会责任。2000年12月和2007年9月,我国分别成立了中国期货业协会和中国银行间市场交易商协会,两者的性质都属于金融衍生品市场中的行业自律组织,而由于管理体制上的原因,监管难度很大,监管效果难以得到有效的发挥。另一方面,目前我国金融衍生品参与者主要是个人和机构,而他们的素质普遍不高,缺乏行业相关知识,容易盲目投资,个别机构在金钱的诱惑下,甚至没有最基本的职业操守,致使“黑庄”滋生,使投资者蒙受巨大损失的同时,更扰乱了正常的金融秩序。在美国的监管机构中,政府监管机构有联邦商品期货交易委员会(CFTC)以及证券交易委员会(SEC),此外美国衍生品市场还有全国期货业协会(NFA)这样的自律监管组织。根据美国等发达国家金融衍生品市场的发展经验,在发展初期,建立行业自律体系和规范行业行为准则尤为重要。金融衍生品市场有着自身的运行规律,我国在建设金融期货市场过程中,需要认真学习和借鉴发达国家经验,但不能盲目全盘照搬,要结合我国的具体国情,加以改造、完善后吸收利用。在这个过程中,我们应逐步完善制定行业的培训教育计划、对机构和广大投资者进行专业教育以及对市场参与者的职业水平考核等措施。而这,也正是我们不懈努力之方向。会计计量播报编辑计量原则金融产品可分为传统金融产品和衍生金融产品两大类。衍生金融产品又称衍生金融工具,它是在传统的金融产品如货币、股票、债券等基本金融工具的基础上派生而来的金融工具,是以转移风险或收益为目的、以某一(某些)金融工具或金融变量为标的、价值随有关金融工具价格或金融变量变动而变动的跨期合约,包括资产支持证券(ABS)、抵押贷款支持证券(M BS)和抵押债务债券(CDO)等。20世纪70年代,布雷顿森林体系解体,世界性石油危机爆发,国际金融竞争加剧。金融机构为了规避风险,在激烈的市场竞争中求生存、求发展,对传统的金融工具进行创新。作为新兴的风险管理重要手段,衍生金融工具应运而生,且发展速度惊人。由于衍生金融工具具有高收益、高风险以及是未来交易的特点,它不仅成了企业筹集资金和转移风险的手段,更成为企业投机、套利和进行金融投机的工具。这种两面性使得一系列震惊世界的金融风波和危机的出现,几乎无不与衍生金融工具有关。由于衍生金融工具常常处于“游离”状态,无法进入传统的财务报告体系,对传统的会计理论与实务造成了巨大的冲击。因而,对衍生金融工具的会计确认、计量和披露一直以来是会计学界的一大热点问题。本次由美国次贷危机引发全球性的金融危机,又一次将这个问题凸显出来。原则优势公允价值计量原则的优势公允价值计量的会计处理方法较之于过去的历史成本计量有着以下几个方面的优点:1.公允价值计量符合会计的决策有用性日标:随着证券市场的逐步打一大和规范,资源的委托与受托关系通过发达的证券市场建立起来,证券市场上收益与风险的并存使得投资者更加关注与收益风险相关的信息,从而权衡利弊做出决策。可以说随着资本市场的发展,交易性质和工具的日益复杂,社会对则务信息决策有用性的要求越来越高,面向未来的决策有用观也就被越来越多的人所认同,而与会计的决策有用观相适应的公允价值计量自然应发挥出它的作用。2.公允价值计量有利于资本保全:按照资本保全的理论,企业在生产经营过程中成本的补偿和利润的分配要保持资本的完整性,保证权益不受侵蚀。企业收益的计量,都应以不侵蚀原投入资本为前提,只有在原资本己得到保全或成本己经弥补之后,才确认收益。采用公允价值作为计量属性,符合实物资本维护的理论。因为如果采用历史成本计量,则计量得出的金额在物价上涨的经济环境中,将不能购回原来相应规模的生产能力,企业的生产只能在萎缩的状态下进行;而采用公允价值计量,尤其是在以公允价值进行初始计量与后续计量的情况下,各要素的价值表现能够不断地反映现实中变化了的资产价值,从而有效地维打‘实物生产能力,更好的保全资本。3.公允价值的动态计量,使得则务会计信息更具备相关性:公允价值作为一种计量属性,具有较强的时效性,强调资产或负债在计量日这一时点的价值。根据FASB发布的第133号则务会计准则以及IASC(国际会计准则委员会)发布的第32号和第39号国际会计准则,所有的衍生金融工具均要在表内确认,并目指出公允价值是计量金融工具的最佳计量属性,对衍生工具而言则是惟一相关的计量属性。因为衍生金融工具着眼于未来,其风险和报酬的转移不是在交易完成之日,而是在合约签订之时,在这期间价格变动频繁,要求不断地反映,若按历史成本计量,显然不够具备相关性。而公允价值却能够真实、及时、可核实地反映资产或负债的价值。原则缺陷任何事物的发展都具有两面性,公允价值计量方式也存在着不尽如人意的缺陷。1.按照定义,公允价值是以契约为基础对未来交易的估计,是估计的未实际发生但将进行现行交易的价格,估计和尚未实际发生的现行交易成为公允价值的重要特点。虽然市场价格是公允价值的基础,甚至可以说是公允价值的最佳估计,然而公允价值不同于市场价格:第一,公允价值不是建立在过去己发生的交易(含事项)基础上,甚至也不是建立在现行交易的基础上;第一,它是熟悉交易的双方意欲进行交易,而参照现行交易所达成的购买一项(或一批)资产,转移(清偿)一项负债的金额;第二,由于契约(双方愿意买卖)己经签订,交易尚未开始进行或i1,在进行中,但尚未完成。在这种情况下,不可能产生己发生交易的成本或价格。因此,公允价值只能是一种参照现行交易的估计价格。而历史成本和现行成本都是实际价格,这种估计价格给会计人员出了难题同时也留下了操作的空间。2.按国内外关于公允价值的定义,投机、炒作等非理性行为所体现的不合理定价,只要不是强迫交易的结果,也包含在公允价值之中,这不能不说是其一大缺陷。3.美国第157号准则《公允价值计量洲守公允价值定义为“市场参与者在计量日的有序交易中,假设将一项资产出售可收到或将一项负债转让应支付的价格”。这个最新定义假设所计量的资产或负债存在着一个习以为常的交易市场。但本次次贷危机表明,这一假设并非永远成立。例如因为投资者过度恐慌和信贷极度萎缩,CDO的市场交易己经名存实亡。同样地,准则也没有考虑流动性缺失的资产对公允价值计量的影响问题。4.鉴于公允价值作为一种计量属性,其确认过程本身需要评估人员主观的判断、估计和预测,可能由此而影响按照公允价值披露的会计信息的可靠性。同时,公允价值的计量还要求能够得到具有较高的独立性和专业性估价信息,否则就有可能会导致这一类会计准则在执行的随意性,出现人为操纵利润的情况。5.公允价值原则在次贷危机中造成了顺周期效应,即市场高涨时,由于交易价格高,容易造成相关金融产品价值的高估,市场低落时,由于交易价格低往往造成相关产品价值的低估。在本次危泪L中,由于债券价格的公允价值下降,投资人的信心受到打击从而继续抛售债券,进而造成债券价格的新一轮下跌。而对于金融机构来说,很容易就陷入交易价格下跌一提取跌价准备、核减权益—恐慌性抛售一价格进一步下跌一继续加大跌价准备的计提、继续核减权益的恶性循环。以花旗、美林、瑞银,AIG(美国国际集团)、白-仕通为代表的金融机构就指出,在次贷危机中,按公允价值对ABS(资产支持证券),MBS(抵押贷款支持证券)和CDO(抵押债务债券)等次债产品进行计量,导致金融机构确认巨额的未实现目未涉及现金流量的损失。这些天文数字般的“账面损失”,扭曲了投资者的心理,造成恐慌性抛售持有次债产品的金融机构股票的风潮。这种非理性投机行为反过来又迫使金融机构不惜代价降低次债产品的风险暴露,本己脆弱不堪的次债产品市场濒临崩溃,金融机构不得不在账上进一步确认减值损失,从而形成极具破坏性的恶性循环。公允价值会计这种独特的反馈效应,使其在次贷危机中事实上起了推波助澜的作用。课程开设播报编辑开设院校对外经贸大学开设学院统计学院课程名称:金融衍生产品投资与管理课程背景伴随着金融全球化的快速发展,衍生产品市场已经成为风险管理和金融投资关注的重要领域,衍生产品的重要功能不仅体现在未来资产价格发现、风险管理等,也体现在衍生产品投资、套利策略制定,更融入到结构化产品设计和应用中。如何艺术地应用衍生产品为企业进行套期保值、为资产负债管理提供风险管理工具、为投资者利用多市场进行产品投资已经成为业界关注的热点。我国的衍生产品市场发生了飞跃变化,商品期货交易量位居全球首位,股指期货、外汇期货、外汇期权、互换等已经推出,还有很多品种呼之欲出。为适应政府、金融机构以及各类企事业单位对衍生产品投资与管理人才迅速增长的需求,提高从事金融市场投资、风险管理、结构化金融产品设计、套期保值等领域在职人员的专业理论水平,对外经贸大学特开设金融学专业衍生产品投资与管理方向在职研究生课程,旨在培养复合型专业化人才。外经贸金融衍生产品在职研究生课程设置1) 学位基础课程微观经济学宏观经济学社会主义经济理论货币银行学国际经济学财政学2) 专业必修课程金融市场实务企业套期保值设计金融期货投资期权投资与结构产品设计金融风险管理金融工程与量化投资投资组合管理实务财务报表分析互换与资产负债管理统计套利与程序交易实物衍生品投资OTC衍生品市场3) 实践课程系列实务讲座:在完成上述课程学习同时,邀请政府和业内知名专家举办系列关于经济金融政策分析、金融衍生产品监管、金融市场投资、风险管理等方面专题讲座。包括:债券类衍生品、股票类衍生品、商品期货市场、外汇市场及衍生品、贵金属衍生品等投资专题;对冲基金专题,结构化产品创新专题,金融衍生产品风险管理专题等。发展对策播报编辑1.丰富和扩大金融衍生品的种类。2.自主创新,在设计上完善金融衍生品。3.明确职责,加强对金融衍生品的监督和管理。4.制定法律法规,使监督有法可依。5.强化信息披露,提高市场的透明度。 [2]EOTSOH*新手上路成长任务编辑入门编辑规则本人编辑我有疑问内容质疑在线客服官方贴吧意见反馈投诉建议举报不良信息未通过词条申诉投诉侵权信息封禁查询与解封©2024 Baidu 使用百度前必读 | 百科协议 | 隐私政策 | 百度百科合作平台 | 京ICP证030173号 京公网安备110000020000

金融衍生产品(基于基础金融工具的金融合约)_百度百科

产品(基于基础金融工具的金融合约)_百度百科 网页新闻贴吧知道网盘图片视频地图文库资讯采购百科百度首页登录注册进入词条全站搜索帮助首页秒懂百科特色百科知识专题加入百科百科团队权威合作下载百科APP个人中心SWaP是一个多义词,请在下列义项上选择浏览(共10个义项)展开添加义项金融衍生产品播报讨论上传视频基于基础金融工具的金融合约收藏查看我的收藏0有用+10同义词SWaP(金融衍生工具)一般指金融衍生产品(基于基础金融工具的金融合约)金融衍生品(derivatives),是指一种基于基础金融工具的金融合约,其价值取决于一种或多种基础资产或指数,合约的基本种类包括远期合约、期货、掉期(互换)和期权。金融衍生品还包括具有远期、期货、掉期(互换)和期权中一种或多种特征的混合金融工具。 [1]这种合约可以是标准化的,也可以是非标准化的。标准化合约是指其标的物(基础资产)的交易价格、交易时间、资产特征、交易方式等都是事先标准化的,因此此类合约大多在交易所上市交易,如期货。非标准化合约是指以上各项由交易的双方自行约定,因此具有很强的灵活性,比如远期协议。金融衍生产品是与金融相关的派生物,通常是指从原生资产(英文为Underlying Assets)派生出来的金融工具。其共同特征是保证金交易,即只要支付一定比例的保证金就可进行全额交易,不需实际上的本金转移,合约的了结一般也采用现金差价结算的方式进行,只有在满期日以实物交割方式履约的合约才需要买方交足贷款。因此,金融衍生产品交易具有杠杆效应。保证金越低,杠杆效应越大,风险也就越大。中文名金融衍生品外文名derivatives别    名金融衍生工具价值依赖基础资产价值变动性    质合约目录1产品特点2作用3种类4风险成因5风险类别6风险管理7区域分布▪交易所▪OTC8投资结构▪金融机构▪非机构9发展历程10会计计量▪计量原则▪原则优势▪原则缺陷11课程开设12发展对策产品特点播报编辑金融衍生产品具有以下几个特点:1、零和博弈即合约交易的双方(在标准化合约中由于可以交易是不确定的)盈亏完全负相关,并且净损益为零,因此称"零和"。2、跨期性金融衍生产品金融衍生工具是交易双方通过对利率、汇率、股价等因素变动的趋势的预测,约定在未来某一时间按一定的条件进行交易或选择是否交易的合约。无论是哪一种金融衍生工具,都会影响交易者在未来一段时间内或未来某时间上的现金流,跨期交易的特点十分突出。这就要求交易的双方对利率、汇率、股价等价格因素的未来变动趋势作出判断,而判断的准确与否直接决定了交易者的交易盈亏。3、联动性这里指金融衍生工具的价值与基础产品或基础变量紧密联系,规则变动。通常,金融衍生工具与基础变量相联系的支付特征有衍生工具合约所规定,其联动关系既可以是简单的线性关系,也可以表达为非线性函数或者分段函数。4、不确定性或高风险性金融衍生工具的交易后果取决于交易者对基础工具未来价格的预测和判断的准确程度。基础工具价格的变幻莫测决定了金融衍生工具交易盈亏的不稳定性,这是金融衍生工具具有高风险的重要诱因。5、高杠杆性衍生产品的交易采用保证金(margin)制度.即交易所需的最低资金只需满足基础资产价值的某个百分比.保证金可以分为初始保证金(initial margin),维持保证金(maintains margin),并且在交易所交易时采取盯市(marking to market)制度,如果交易过程中的保证金比例低于维持保证金比例,那么将收到追加保证金通知(追加保证金,其将被强行平仓.可见,衍生品交易具有高风险高收益的特点.6、契约性金融衍生产品交易是对基础工具在未来某种条件下的权利和义务的处理,从法律上理解是合同,是一种建立在高度发达的社会信用基础上的经济合同关系。7、交易对象的虚拟性金融衍生产品合约交易的对象是对基础金融工具在未来各种条件下处置的权利和义务,如期权的买权或卖权、互换的债务交换义务等,构成所谓“产品”,表现出一定的虚拟性。8、交易目的的多重性金融衍生产品交易通常有套期保值、投机、套利和资产负债管理等四大目的。其交易的主要目的并不在于所涉及的基础金融商品所有权的转移,而在于转移与该金融商品相关的价值变化的风险或通过风险投资获取经济利益。此外,金融衍生产品还具有未来性、表外性和射幸性等特点作用播报编辑金融衍生产品金融衍生产品的作用有规避风险,价格发现,它是对冲资产风险的好方法。但是,任何事情有好的一面也有坏的一面,风险规避了一定是有人去承担了,衍生产品的高杠杆性就是将巨大的风险转移给了愿意承担的人手中。交易者可以分为三类:套期保值者或对冲者(hedger)、投机者(speculator)以及套利者(arbitrageur)。对冲者采用衍生产品合约来减少自身面临的由于市场变化而产生的风险。投机者利用这些产品对今后市场变量的走向下赌注。套利者采用两个或更多相互抵消的交易来锁定盈利。这三类交易者共同维护了金融衍生产品市场上述功能的发挥。金融衍生产品交易不当将导致巨大的风险,有的甚至是灾难性的,国外的有巴林银行事件,宝洁事件,LTCM事件,信孚银行,国内的有国储铜事件,中航油事件。种类播报编辑(1)根据产品形态,可以分为远期、期货、期权和掉期四大类。金融衍生产品远期合约和期货合约都是交易双方约定在未来某一特定时间、以某一特定价格、买卖某一特定数量和质量资产的交易形式。期货合约是期货交易所制定的标准化合约,对合约到期日及其买卖的资产的种类、数量、质量作出了统一规定。远期合约是根据买卖双方的特殊需求由买卖双方自行签订的合约。因此,期货交易流动性较高,远期交易流动性较低。掉期合约是一种内交易双方签订的在未来某一时期相互交换某种资产的合约。更为准确地说,掉期合约是当事人之间签订的在未来某一期间内相互交换他们认为具有相等经济价值的现金流(Cash Flow)的合约。较为常见的是利率掉期合约和货币掉期合约。掉期合约中规定的交换货币是同种货币,则为利率掉期;是异种货币,则为货币掉期。期权交易是买卖权利的交易。期权合约规定了在某一特定时间、以某一特定价格买卖某一特定种类、数量、质量原生资产的权利。期权合同有在交易所上市的标准化合同,也有在柜台交易的非标准化合同。(2)根据原生资产大致可以分为四类,即股票、利率、货币和商品。如果再加以细分,股票类中又包括具体的股票和由股票组合形成的股票指数;利率类中又可分为以短期存款利率为代表的短期利率和以长期债券利率为代表的长期利率;货币类中包括各种不同币种之间的比值:商品类中包括各类大宗实物商品。具体见表3—1表3—1 根据原生资产对金融衍生产品的分类对象原生资产金融衍生产品利率短期存款利率期货、利率远期、利率期权、利率掉期合约等-长期债券债券期货、债券期权合约等股票股票股票期货、股票期权合约等-股票指数股票指数期货、股票指数期权合约等货币各类现汇货币远期、货币期货、货币期权、货币掉期合约等商品各类实物商品商品远期、商品期货,商品期权、商品掉期合约等(3)根据交易方法,可分为场内交易和场外交易。场内交易,又称交易所交易,指所有的供求方集中在交易所进行竞价交易的交易方式。这种交易方式具有交易所向交易参与者收取保证金、同时负责进行清算和承担履约担保责任的特点。此外,由于每个投资者都有不同的需求,交易所事先设计出标准化的金融合同,由投资者选择与自身需求最接近的合同和数量进行交易。所有的交易者集中在一个场所进行交易,这就增加了交易的密度,一般可以形成流动性较高的市场。期货交易和部分标准化期权合同交易都属于这种交易方式。场外交易,又称柜台交易,指交易双方直接成为交易对手的交易方式。这种交易方式有许多形态,可以根据每个使用者的不同需求设计出不同内容的产品。同时,为了满足客户的具体要求、出售衍生产品的金融机构需要有高超的金融技术和风险管理能力。场外交易不断产生金融创新。但是,由于每个交易的清算是由交易双方相互负责进行的,交易参与者仅限于信用程度高的客户。掉期交易和远期交易是具有代表性的柜台交易的衍生产品。据统计,在金融衍生产品的持仓量中,按交易形态分类,远期交易的持仓量最大,占整体持仓量的42%,以下依次是掉期(27%)、期货(18%)和期权(13%)。按交易对象分类,以利率掉期、利率远期交易等为代表的有关利率的金融衍生产品交易占市场份额最大,为62%,以下依次是货币衍生产品(37%)和股票、商品衍生产品(1%),1989年到1995年的6年间,金融衍生产品市场规模扩大了5.7倍。各种交易形态和各种交易对象之间的差距并不大,整体上呈高速扩大的趋势。到目前为止,国际金融领域中,流行的衍生产品有如下四种:互换、期货、期权和远期利率协议。采取这些衍生产品的最主要目的均为保值或投机。但是这些衍生产品所以能存在与发展都有其前提条件,那就是发达的远期市场。风险成因播报编辑金融衍生产品风险产生的微观主体原因 内部控制薄弱,对交易员缺乏有效的监督,是造成金融衍生产品风险的一个重要原因。例如,内部风险管理混乱到了极点是巴林银行覆灭的主要原因。首先,巴林银行内部缺乏基本的风险防范机制,里森一人身兼清算和交易两职,缺乏制衡,很容易通过改写交易记录来掩盖风险或亏损。同时,巴林银行也缺乏一个独立的风险控制检查部门对里森所为进行监控;其次,巴林银行管理层监管不严,风险意识薄弱。在日本关西大地震之后,里森因其衍生合约保证金不足而求助于总部时,总部竟然还将数亿美元调至新加坡分行,为其提供无限制的资金支持;再者,巴林银行领导层分裂,内部各业务环节之间关系紧张,令许多知情管理人员忽视市场人士和内部审检小组多次发出的警告,以至最后导致整个巴林集团的覆没。另外,过度的激励机制激发了交易员的冒险精神,增大了交易过程中的风险系数。金融衍生产品风险的宏观成因金融监管不力也是造成金融衍生产品风险的另一主要原因。英国和新加坡的金融监管当局事先监管不力,或未协力合作,是导致巴林银行倒闭的重要原因之一。英国监管部门出现的问题是:第一,负责监管巴林等投资银行的部门曾口头上给与宽免,巴林将巨额款项汇出炒卖日经指数时,无需请示英格兰银行。第二,英格兰银行允许巴林集团内部银行给予证券部门无限制资金的支持。新加坡金融监管当局存在的问题是:首先,新加坡国际金融交易所面对激烈的国际竞争,为了促进业务的发展,在持仓量的控制方面过于宽松,没有严格执行持仓上限,允许单一交易账户大量积累日经期指和日债期货仓位,对会员公司可持有合约数量和缴纳保证金情况没有进行及时监督。其次,里森频繁地从事对倒交易,且交易数额异常庞大,却竟然没有引起交易所的关注。如果英格兰银行、新加坡和大阪交易所之间能够加强交流,共享充分的信息,就会及时发现巴林银行在两个交易所持有的巨额头寸,或许巴林银行不会倒闭。美国长期资本管理公司(LTCM)曾是美国最大的对冲基金,却在俄罗斯上演了人类有史以来最大的金融滑铁卢。监管中存在真空状态是导致其巨额亏损的制度性原因,甚至在LTCM出事后,美国的金融管理当局都还不清楚其资产负债情况。由于政府对银行、证券机构监管的放松,使得许多国际商业银行集团和证券机构无限制地为其提供巨额融资,瑞士银行(UBS)和意大利外汇管理部门(UIC)因此分别损失7.1亿美元和2.5亿美元。另外中国“327国债”期货风波,除当时市场需求不强、发展衍生工具的条件不够以外,过度投机和监管能力不足是不可忽视的原因。风险类别播报编辑(一)市场风险(二)信用风险(三)流动性风险(四)操作风险(五)法律风险风险管理播报编辑对于金融衍生产品的监管,国际上基本上采取企业自控、行业协会和交易所自律、政府部门监管的三级风险管理模式。微观金融主体内部自我监督管理金融衍生产品首先,建立风险决策机制和内部监管制度,包括限定交易的目的、对象、目标价格、合约类型、持仓数量、止损点位、交易流程以及不同部门的职责分配等。其次,加强内部控制,严格控制交易程序,将操作权、结算权、监督权分开,有严格的层次分明的业务授权,加大对越权交易的处罚力度;再次,设立专门的风险管理部门,通过“风险价值方法”(VAR)和“压力试验法”对交易人员的交易进行记录、确认、市价计值,评价、度量和防范在金融衍生产品交易过程中面临的信用风险、市场风险、流动性风险、结算风险、操作风险等。交易所内部监管交易所是衍生产品交易组织者和市场管理者,它通过制定场内交易规则,监督市场的业务操作,保证交易在公开、公正、竞争的条件下进行。第一,创建完备的金融衍生市场制度,包括:严格的市场信息披露制度,增强透明度;大额报告制度;完善的市场准入制度,对衍生市场交易者的市场信用状况进行调查和评估,制定资本充足要求;以及其他场内和场外市场交易规则等等。第二,建立衍生市场的担保制度,包括:合理制定并及时调整保证金比例,起到第一道防线的作用;持仓限额制度,发挥第二道防线的作用;日间保证金追加条款;逐日盯市制度或称按市价计值(mark to market)加强清算、结算和支付系统的管理;价格限额制度等。第三,加强财务监督,根据衍生产品的特点,改革传统的会计记账方法和原则,制定统一的信息披露规则和程序,以便管理层和用户可以清晰明了地掌握风险敞口情况。政府部门的宏观调控与监管首先,完善立法,对金融衍生产品设立专门完备的法律,制定有关交易管理的统一标准;其次,加强对从事金融衍生产品交易的金融机构的监管,规定从事交易的金融机构的最低资本额,确定风险承担限额,对金融机构进行定期与不定期的现场与非现场的检查,形成有效的控制与约束机制;负责审批衍生产品交易所的成立和交易所申请的衍生产品品种。再次,严格区分银行业务与非银行业务,控制金融机构业务交叉的程度。同时,中央银行在某个金融机构因突发事件发生危机时,应及时采取相应的挽救措施,迅速注入资金或进行暂时干预,以避免金融市场产生过度震荡。另外,金融衍生产品交易在世界范围内超国界和超政府地蓬勃开展,单一国家和地区已无法对其风险进行全面的控制,因此加强对金融衍生产品的国际监管和国际合作,成为国际金融界和各国金融当局的共识。在巴林银行事件之后,国际清算银行已着手对衍生产品交易进行全面的调查与监督,加强对银行表外业务资本充足性的监督。区域分布播报编辑金融衍生品区域分布结构:交易所欧美发达国家集中了世界上绝大部分的交易所金融衍生品交易,全球80%以上的交易分布在北美和欧洲,这种集中趋势更加明显。1999年末的未清偿金融期货和期权合约名义价值中,有全球80.5%属于北美和欧洲,到2002年6月末,这一比例上升到了93.7%,北美地区的合约价值占到总价值的64.6%。美国是全球交易所金融衍生品交易的主要市场,但其地位正在趋于下降,美国交易所成交的金融衍生品合约在1986、1988、1990、1992、1994年分别占全球交易量的91.4%、74.7%、65.1%、53.5%、44.7%;欧洲市场的增长最为显著,1994年的交易量是1986年的399%;其间日本的交易量大约增长了7倍。从交易额统计看,直至1986年,美国尚占有交易所市场交易额和未清偿合约价值的80%份额。1990年后,美国以外的市场日趋活跃,交易增长率开始超过美国,到1995年,美国以外市场的交易额已超出美国,而未清偿合约价值稍逊于美国。从交易量统计看,1990年后美国以外市场衍生品交易的活跃趋势更加明显。OTC与交易所市场类似,OTC 金融衍生品市场也主要分布在欧美国家。英国一直保持着OTC市场的领先地位,而且市场份额不断上升,之外的OTC交易主要分布在美国、德国、法国、日本等国家(见表6)。伦敦是OTC金融衍生品市场最重要的中心,2001年日均交易额达6280亿美元,较1998年增长6%。纽约日均交易额位居第二,为2850亿美元,较1998年下降3%,法兰克福交易额名列第三,业已取代东京在OTC市场中的位置,法兰克福地位的上升明显得益于引入欧元和欧洲中央银行(ECB)的设立。投资结构播报编辑金融机构金融机构是金融衍生品市场的主要参与者,以美国为例,参与衍生品交易的金融机构主要有商业银行、非银行储贷机构(Thrift)和人寿保险公司三类,其中商业银行是最早和最熟练的参与者。根据三十国集团1993年的一份报告,被调查的大部分金融机构参与了金融衍生品交易,其中有92%的机构使用过利率互换,69%的机构使用过远期外汇合约,69%的机构使用过利率期权,46%的机构使用过货币互换,23%的机构使用过货币期权。BIS的统计显示,金融机构在全球OTC金融衍生品市场中的交易额稳步增长,2001年较1995年提高60%。交易主要发生在金融机构之间,日均交易额由1995年的7100亿美元提升至2001年的1.2兆美元,金融机构间交易占市场的份额由1995年的80.7%提高到2001年的86.7%。银行无疑是金融衍生品市场中的主角(尤其在OTC市场上),1970年代末以来,银行日益热衷于金融衍生品交易,例如,美国银行业在金融衍生品交易中十分活跃,从1990年到1995年,银行持有与衍生品相关的资产增长了约35%,达到3.1兆美元,其间银行持有的衍生品合约名义价值增加了2倍。银行是金融互换市场的主要参与者,1992年末全球利率互换合约的未清偿价值达6兆美元,持有头寸最大的20家金融机构占到三分之二强,其中银行占了18家。非机构非金融机构在金融衍生品交易中显然不如金融机构活跃,例如非金融机构只占到OTC金融衍生品交易额的10%,与1995年相比其市场份额有明显萎缩。根据三十国集团1993年报告,被调查的非金融类公司中,使用过有利率互换、货币互换、远期外汇合约、利率期权和货币期权的公司的比例分别是87%、64%、78%、40%和31%。发展历程播报编辑1865年,芝加哥谷物交易所推出了一种被称为“期货合约”的标准化协议,取代1851年以来沿用的远期合同,成为人类历史上最早开发出来的金融衍生品。期货市场早期的参与者,主要是以对冲远期风险为目的的套期保值者。事实上,以期货、期权、远期、互换等为代表的金融衍生品,已经成为了能够有效管理和降低市场参与者风险的工具。在金融衍生品150年的发展历程中,美国一直是“领头羊”,在这里有最广泛的市场和最尖端的技术。我国的金融业正处在蓬勃发展期,而金融衍生品市场的发展速度则明显滞后,主要表现在三个方面:金融衍生产品本身、金融衍生品市场监管、行业自律和规范。目前国内的金融衍生品产品种类虽然正在逐步增加,但是和欧美以及香港等发达国家或地区相比还是不可同日而语的。成熟的金融市场体系中,衍生品的种类和数量往往应该远超过股票、债券等金融产品,而我国在这方面还有很长的路要走,不仅品种较少,而且产品同质化比较严重,这样就更限制了产品应用领域的拓展。而金融衍生品的本质是为了分散风险,为投资者服务,由此可见,只有对金融衍生品不断地的深入研究与创新拓展,才能逐步满足广大投资者的投资需求。我国对金融衍生品的法律监管相对滞后,相关法律体系尚待完善,其中一方面原因是由于缺乏行业发展经验和不了解产品之间的内在联动性的原因。而这也正是中国金融衍生品研究院(以下简称“中金院”)研究工作的重点内容之一。中金院结合当前国情与发达国家成功经验,以自身坚实的研究基础为依托,形成专业性的、具备参考价值和实践意义的行业报告,进而履行中金院专业引导和策略建议的社会责任。2000年12月和2007年9月,我国分别成立了中国期货业协会和中国银行间市场交易商协会,两者的性质都属于金融衍生品市场中的行业自律组织,而由于管理体制上的原因,监管难度很大,监管效果难以得到有效的发挥。另一方面,目前我国金融衍生品参与者主要是个人和机构,而他们的素质普遍不高,缺乏行业相关知识,容易盲目投资,个别机构在金钱的诱惑下,甚至没有最基本的职业操守,致使“黑庄”滋生,使投资者蒙受巨大损失的同时,更扰乱了正常的金融秩序。在美国的监管机构中,政府监管机构有联邦商品期货交易委员会(CFTC)以及证券交易委员会(SEC),此外美国衍生品市场还有全国期货业协会(NFA)这样的自律监管组织。根据美国等发达国家金融衍生品市场的发展经验,在发展初期,建立行业自律体系和规范行业行为准则尤为重要。金融衍生品市场有着自身的运行规律,我国在建设金融期货市场过程中,需要认真学习和借鉴发达国家经验,但不能盲目全盘照搬,要结合我国的具体国情,加以改造、完善后吸收利用。在这个过程中,我们应逐步完善制定行业的培训教育计划、对机构和广大投资者进行专业教育以及对市场参与者的职业水平考核等措施。而这,也正是我们不懈努力之方向。会计计量播报编辑计量原则金融产品可分为传统金融产品和衍生金融产品两大类。衍生金融产品又称衍生金融工具,它是在传统的金融产品如货币、股票、债券等基本金融工具的基础上派生而来的金融工具,是以转移风险或收益为目的、以某一(某些)金融工具或金融变量为标的、价值随有关金融工具价格或金融变量变动而变动的跨期合约,包括资产支持证券(ABS)、抵押贷款支持证券(M BS)和抵押债务债券(CDO)等。20世纪70年代,布雷顿森林体系解体,世界性石油危机爆发,国际金融竞争加剧。金融机构为了规避风险,在激烈的市场竞争中求生存、求发展,对传统的金融工具进行创新。作为新兴的风险管理重要手段,衍生金融工具应运而生,且发展速度惊人。由于衍生金融工具具有高收益、高风险以及是未来交易的特点,它不仅成了企业筹集资金和转移风险的手段,更成为企业投机、套利和进行金融投机的工具。这种两面性使得一系列震惊世界的金融风波和危机的出现,几乎无不与衍生金融工具有关。由于衍生金融工具常常处于“游离”状态,无法进入传统的财务报告体系,对传统的会计理论与实务造成了巨大的冲击。因而,对衍生金融工具的会计确认、计量和披露一直以来是会计学界的一大热点问题。本次由美国次贷危机引发全球性的金融危机,又一次将这个问题凸显出来。原则优势公允价值计量原则的优势公允价值计量的会计处理方法较之于过去的历史成本计量有着以下几个方面的优点:1.公允价值计量符合会计的决策有用性日标:随着证券市场的逐步打一大和规范,资源的委托与受托关系通过发达的证券市场建立起来,证券市场上收益与风险的并存使得投资者更加关注与收益风险相关的信息,从而权衡利弊做出决策。可以说随着资本市场的发展,交易性质和工具的日益复杂,社会对则务信息决策有用性的要求越来越高,面向未来的决策有用观也就被越来越多的人所认同,而与会计的决策有用观相适应的公允价值计量自然应发挥出它的作用。2.公允价值计量有利于资本保全:按照资本保全的理论,企业在生产经营过程中成本的补偿和利润的分配要保持资本的完整性,保证权益不受侵蚀。企业收益的计量,都应以不侵蚀原投入资本为前提,只有在原资本己得到保全或成本己经弥补之后,才确认收益。采用公允价值作为计量属性,符合实物资本维护的理论。因为如果采用历史成本计量,则计量得出的金额在物价上涨的经济环境中,将不能购回原来相应规模的生产能力,企业的生产只能在萎缩的状态下进行;而采用公允价值计量,尤其是在以公允价值进行初始计量与后续计量的情况下,各要素的价值表现能够不断地反映现实中变化了的资产价值,从而有效地维打‘实物生产能力,更好的保全资本。3.公允价值的动态计量,使得则务会计信息更具备相关性:公允价值作为一种计量属性,具有较强的时效性,强调资产或负债在计量日这一时点的价值。根据FASB发布的第133号则务会计准则以及IASC(国际会计准则委员会)发布的第32号和第39号国际会计准则,所有的衍生金融工具均要在表内确认,并目指出公允价值是计量金融工具的最佳计量属性,对衍生工具而言则是惟一相关的计量属性。因为衍生金融工具着眼于未来,其风险和报酬的转移不是在交易完成之日,而是在合约签订之时,在这期间价格变动频繁,要求不断地反映,若按历史成本计量,显然不够具备相关性。而公允价值却能够真实、及时、可核实地反映资产或负债的价值。原则缺陷任何事物的发展都具有两面性,公允价值计量方式也存在着不尽如人意的缺陷。1.按照定义,公允价值是以契约为基础对未来交易的估计,是估计的未实际发生但将进行现行交易的价格,估计和尚未实际发生的现行交易成为公允价值的重要特点。虽然市场价格是公允价值的基础,甚至可以说是公允价值的最佳估计,然而公允价值不同于市场价格:第一,公允价值不是建立在过去己发生的交易(含事项)基础上,甚至也不是建立在现行交易的基础上;第一,它是熟悉交易的双方意欲进行交易,而参照现行交易所达成的购买一项(或一批)资产,转移(清偿)一项负债的金额;第二,由于契约(双方愿意买卖)己经签订,交易尚未开始进行或i1,在进行中,但尚未完成。在这种情况下,不可能产生己发生交易的成本或价格。因此,公允价值只能是一种参照现行交易的估计价格。而历史成本和现行成本都是实际价格,这种估计价格给会计人员出了难题同时也留下了操作的空间。2.按国内外关于公允价值的定义,投机、炒作等非理性行为所体现的不合理定价,只要不是强迫交易的结果,也包含在公允价值之中,这不能不说是其一大缺陷。3.美国第157号准则《公允价值计量洲守公允价值定义为“市场参与者在计量日的有序交易中,假设将一项资产出售可收到或将一项负债转让应支付的价格”。这个最新定义假设所计量的资产或负债存在着一个习以为常的交易市场。但本次次贷危机表明,这一假设并非永远成立。例如因为投资者过度恐慌和信贷极度萎缩,CDO的市场交易己经名存实亡。同样地,准则也没有考虑流动性缺失的资产对公允价值计量的影响问题。4.鉴于公允价值作为一种计量属性,其确认过程本身需要评估人员主观的判断、估计和预测,可能由此而影响按照公允价值披露的会计信息的可靠性。同时,公允价值的计量还要求能够得到具有较高的独立性和专业性估价信息,否则就有可能会导致这一类会计准则在执行的随意性,出现人为操纵利润的情况。5.公允价值原则在次贷危机中造成了顺周期效应,即市场高涨时,由于交易价格高,容易造成相关金融产品价值的高估,市场低落时,由于交易价格低往往造成相关产品价值的低估。在本次危泪L中,由于债券价格的公允价值下降,投资人的信心受到打击从而继续抛售债券,进而造成债券价格的新一轮下跌。而对于金融机构来说,很容易就陷入交易价格下跌一提取跌价准备、核减权益—恐慌性抛售一价格进一步下跌一继续加大跌价准备的计提、继续核减权益的恶性循环。以花旗、美林、瑞银,AIG(美国国际集团)、白-仕通为代表的金融机构就指出,在次贷危机中,按公允价值对ABS(资产支持证券),MBS(抵押贷款支持证券)和CDO(抵押债务债券)等次债产品进行计量,导致金融机构确认巨额的未实现目未涉及现金流量的损失。这些天文数字般的“账面损失”,扭曲了投资者的心理,造成恐慌性抛售持有次债产品的金融机构股票的风潮。这种非理性投机行为反过来又迫使金融机构不惜代价降低次债产品的风险暴露,本己脆弱不堪的次债产品市场濒临崩溃,金融机构不得不在账上进一步确认减值损失,从而形成极具破坏性的恶性循环。公允价值会计这种独特的反馈效应,使其在次贷危机中事实上起了推波助澜的作用。课程开设播报编辑开设院校对外经贸大学开设学院统计学院课程名称:金融衍生产品投资与管理课程背景伴随着金融全球化的快速发展,衍生产品市场已经成为风险管理和金融投资关注的重要领域,衍生产品的重要功能不仅体现在未来资产价格发现、风险管理等,也体现在衍生产品投资、套利策略制定,更融入到结构化产品设计和应用中。如何艺术地应用衍生产品为企业进行套期保值、为资产负债管理提供风险管理工具、为投资者利用多市场进行产品投资已经成为业界关注的热点。我国的衍生产品市场发生了飞跃变化,商品期货交易量位居全球首位,股指期货、外汇期货、外汇期权、互换等已经推出,还有很多品种呼之欲出。为适应政府、金融机构以及各类企事业单位对衍生产品投资与管理人才迅速增长的需求,提高从事金融市场投资、风险管理、结构化金融产品设计、套期保值等领域在职人员的专业理论水平,对外经贸大学特开设金融学专业衍生产品投资与管理方向在职研究生课程,旨在培养复合型专业化人才。外经贸金融衍生产品在职研究生课程设置1) 学位基础课程微观经济学宏观经济学社会主义经济理论货币银行学国际经济学财政学2) 专业必修课程金融市场实务企业套期保值设计金融期货投资期权投资与结构产品设计金融风险管理金融工程与量化投资投资组合管理实务财务报表分析互换与资产负债管理统计套利与程序交易实物衍生品投资OTC衍生品市场3) 实践课程系列实务讲座:在完成上述课程学习同时,邀请政府和业内知名专家举办系列关于经济金融政策分析、金融衍生产品监管、金融市场投资、风险管理等方面专题讲座。包括:债券类衍生品、股票类衍生品、商品期货市场、外汇市场及衍生品、贵金属衍生品等投资专题;对冲基金专题,结构化产品创新专题,金融衍生产品风险管理专题等。发展对策播报编辑1.丰富和扩大金融衍生品的种类。2.自主创新,在设计上完善金融衍生品。3.明确职责,加强对金融衍生品的监督和管理。4.制定法律法规,使监督有法可依。5.强化信息披露,提高市场的透明度。 [2]EOTSOH*新手上路成长任务编辑入门编辑规则本人编辑我有疑问内容质疑在线客服官方贴吧意见反馈投诉建议举报不良信息未通过词条申诉投诉侵权信息封禁查询与解封©2024 Baidu 使用百度前必读 | 百科协议 | 隐私政策 | 百度百科合作平台 | 京ICP证030173号 京公网安备110000020000

Uniswap-v2 合约概览 | ethereum.org

wap-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)本页面对你有帮