学习核心合约UniswapV2Pair,在父合约UniswapV2ERC20的基础上增加资产交易及流动性提供等功能。
交易对合约本身是erc20合约,代币表示流动性,代币在提供流动性的地址里,当提供流动性,就会增发代币给提供者,反正提取流动性,就烧掉提取者的代币
1 pragma solidity =0.5.16;
2
3 import './interfaces/IUniswapV2Pair.sol';
4 import './UniswapV2ERC20.sol';
5 import './libraries/Math.sol';
6 import './libraries/UQ112x112.sol';
7 import './interfaces/IERC20.sol';
8 import './interfaces/IUniswapV2Factory.sol';
9 import './interfaces/IUniswapV2Callee.sol';
10
11 contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
12 using SafeMath for uint;
13 using UQ112x112 for uint224;
14
15 uint public constant MINIMUM_LIQUIDITY = 10**3;
16 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
17
18 address public factory;
19 address public token0;
20 address public token1;
21
22 uint112 private reserve0; // uses single storage slot, accessible via getReserves
23 uint112 private reserve1; // uses single storage slot, accessible via getReserves
24 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
25
26 uint public price0CumulativeLast;
27 uint public price1CumulativeLast;
28 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
29
30 uint private unlocked = 1;
31 modifier lock() {
32 require(unlocked == 1, 'UniswapV2: LOCKED');
33 unlocked = 0;
34 _;
35 unlocked = 1;
36 }
37
38 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
39 _reserve0 = reserve0;
40 _reserve1 = reserve1;
41 _blockTimestampLast = blockTimestampLast;
42 }
43
44 function _safeTransfer(address token, address to, uint value) private {
45 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
46 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
47 }
48
49 event Mint(address indexed sender, uint amount0, uint amount1);
50 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
51 event Swap(
52 address indexed sender,
53 uint amount0In,
54 uint amount1In,
55 uint amount0Out,
56 uint amount1Out,
57 address indexed to
58 );
59 event Sync(uint112 reserve0, uint112 reserve1);
60
61 constructor() public {
62 factory = msg.sender;
63 }
64
65 // called once by the factory at time of deployment
66 function initialize(address _token0, address _token1) external {
67 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
68 token0 = _token0;
69 token1 = _token1;
70 }
71
72 // update reserves and, on the first call per block, price accumulators
73 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
74 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
75 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
76 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
77 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
78 // * never overflows, and + overflow is desired
79 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
80 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
81 }
82 reserve0 = uint112(balance0);
83 reserve1 = uint112(balance1);
84 blockTimestampLast = blockTimestamp;
85 emit Sync(reserve0, reserve1);
86 }
87
88 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
89 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
90 address feeTo = IUniswapV2Factory(factory).feeTo();
91 feeOn = feeTo != address(0);
92 uint _kLast = kLast; // gas savings
93 if (feeOn) {
94 if (_kLast != 0) {
95 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
96 uint rootKLast = Math.sqrt(_kLast);
97 if (rootK > rootKLast) {
98 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
99 uint denominator = rootK.mul(5).add(rootKLast);
100 uint liquidity = numerator / denominator;
101 if (liquidity > 0) _mint(feeTo, liquidity);
102 }
103 }
104 } else if (_kLast != 0) {
105 kLast = 0;
106 }
107 }
108
109 // this low-level function should be called from a contract which performs important safety checks
110 function mint(address to) external lock returns (uint liquidity) {
111 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
112 uint balance0 = IERC20(token0).balanceOf(address(this));
113 uint balance1 = IERC20(token1).balanceOf(address(this));
114 uint amount0 = balance0.sub(_reserve0);
115 uint amount1 = balance1.sub(_reserve1);
116
117 bool feeOn = _mintFee(_reserve0, _reserve1);
118 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
119 if (_totalSupply == 0) {
120 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
121 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
122 } else {
123 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
124 }
125 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
126 _mint(to, liquidity);
127
128 _update(balance0, balance1, _reserve0, _reserve1);
129 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
130 emit Mint(msg.sender, amount0, amount1);
131 }
132
133 // this low-level function should be called from a contract which performs important safety checks
134 function burn(address to) external lock returns (uint amount0, uint amount1) {
135 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
136 address _token0 = token0; // gas savings
137 address _token1 = token1; // gas savings
138 uint balance0 = IERC20(_token0).balanceOf(address(this));
139 uint balance1 = IERC20(_token1).balanceOf(address(this));
140 uint liquidity = balanceOf[address(this)];
141
142 bool feeOn = _mintFee(_reserve0, _reserve1);
143 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
144 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
145 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
146 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
147 _burn(address(this), liquidity);
148 _safeTransfer(_token0, to, amount0);
149 _safeTransfer(_token1, to, amount1);
150 balance0 = IERC20(_token0).balanceOf(address(this));
151 balance1 = IERC20(_token1).balanceOf(address(this));
152
153 _update(balance0, balance1, _reserve0, _reserve1);
154 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
155 emit Burn(msg.sender, amount0, amount1, to);
156 }
157
158 // this low-level function should be called from a contract which performs important safety checks
159 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
160 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
161 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
162 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
163
164 uint balance0;
165 uint balance1;
166 { // scope for _token{0,1}, avoids stack too deep errors
167 address _token0 = token0;
168 address _token1 = token1;
169 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
170 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
171 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
172 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
173 balance0 = IERC20(_token0).balanceOf(address(this));
174 balance1 = IERC20(_token1).balanceOf(address(this));
175 }
176 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
177 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
178 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
179 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
180 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
181 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
182 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
183 }
184
185 _update(balance0, balance1, _reserve0, _reserve1);
186 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
187 }
188
189 // force balances to match reserves
190 function skim(address to) external lock {
191 address _token0 = token0; // gas savings
192 address _token1 = token1; // gas savings
193 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
194 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
195 }
196
197 // force reserves to match balances
198 function sync() external lock {
199 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
200 }
201 }
1 指定Solidity编译版本。
2 第三-第九行: 导入交易对合约需要实现的接口和交易对的父合约。
导入Math库,导入UQ112x112这是自定义的数据格式库,在V2中使用两种代币的比值作为价格,为提高精度使用uint112来表示价格,UQ112x112 前面代表整数,后面代表小数。导入标准ERC20接口目的:获取交易对合约流通池的代币余额。
导入IUniswapV2Factory合约,目的获取开发团队手续费地址。
导入IUniswapV2Callee.sol.规定第三方合约要实现的接口格式。定义次接口可进行FlasSwap
3 定义UniswapV2Pair 实现了IUniswapV2Pair,并且继承UniswapV2ERC20.继承了父合约所有非私有接口与状态变量
4 12-13行 ;实现SafeMath、UQ112x112库函数。
5 定义最小流动性,在提供初始流动性时烧掉。
6 计算transfer的函数选择器。
7 18-20行:定义3个 address类型的公共状态变量,记录factory合约地址,2个代币的地址
8 21-23 reserve0,reserve1 记录最新的恒定乘积中的代币数量,blockTimestampLast 交易时的区块创建时间。
9 price0CumulativeLast ,price1CumulativeLast ,j记录交易对中两种价格的累计值。
10 klast 记录恒定乘积中积的值,用于开发团队手续费计算
11 防止重入攻击,在函数修饰器中, _;表示执行被修饰的函数体,当函数被外部调用,unlocked 为0,函数执行完设置为1,在未执行完前,如果重入该函数,l
1 uint private unlocked = 1;
2 modifier lock() {
3 require(unlocked == 1, 'UniswapV2: LOCKED');
4 unlocked = 0;
5 _;
6 unlocked = 1;
7 }
12 getReserves函数 获取当前交易对的资产信息以及最后交易的区块时间
13 _safeTransfer 函数,使用call函数进行代币合约transfer的调用(函数选择器),检查返回值
14 定义四个event事件 mint swap burn sync 便于追踪。
15 constructor 构造器,工厂合约 factory 设为msg.sender。
16 initialize 初始化函数,由于使用create2创建合约,不能向构造函数传递参数。所以没有之间在构造函数传参。
17 _update 函数, 更新reserves,在每个block的第一次调用更新价格累积值。
1 // update reserves and, on the first call per block, price accumulators
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
3 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
4 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
5 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
6 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
7 // * never overflows, and + overflow is desired
8 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
9 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
10 }
11 reserve0 = uint112(balance0);
12 reserve1 = uint112(balance1);
13 blockTimestampLast = blockTimestamp;
14 emit Sync(reserve0, reserve1);
15 }
16
1) 四个参数 当前合约代币的两种余额,保存的恒定乘积中两种代币数值,将保存的数值更新为实时代币余额,并且同时进行价格累计的计算。
2)第一行验证余额不能大于uint112类型的最大值。
3)存储槽位是256位,两个代币数量各112位, 剩下32位 记录当前区块时间
4)计算当前block 和上一次block的时间差值
5) if 语句 当同区块的第二笔交易,timeElapsed为0,就不会计算累积值
6)计算价格的累积值
7) 更新reserve的值,更新bloc的 时间为当前时间,触发同步事件
18 _mintFree 函数 计算并发送开发团队手续费,
1 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
2 address feeTo = IUniswapV2Factory(factory).feeTo();
3 feeOn = feeTo != address(0);
4 uint _kLast = kLast; // gas savings
5 if (feeOn) {
6 if (_kLast != 0) {
7 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
8 uint rootKLast = Math.sqrt(_kLast);
9 if (rootK > rootKLast) {
10 uint numerator = totalSupply.mul(rootK.sub(rootKLast));
11 uint denominator = rootK.mul(5).add(rootKLast);
12 uint liquidity = numerator / denominator;
13 if (liquidity > 0) _mint(feeTo, liquidity);
14 }
15 }
16 } else if (_kLast != 0) {
17 kLast = 0;
18 }
19 }
1) 函数参数:交易对中保存的恒定乘积中的代币数,返回类型 bool
2)获取开发团队手续费地址,判断地址是否是零地址来判断开关是否打开
3) 局部变量保存恒定乘积中积的值。(减少gas)
4) if 判断 ,如果手续费开关打开,计算手续费值
5)下面就是具体数学运算。
19 mint 函数:提供流动性时增发流动性代币给提供者
1 function mint(address to) external lock returns (uint liquidity) {
2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
3 uint balance0 = IERC20(token0).balanceOf(address(this));
4 uint balance1 = IERC20(token1).balanceOf(address(this));
5 uint amount0 = balance0.sub(_reserve0);
6 uint amount1 = balance1.sub(_reserve1);
7
8 bool feeOn = _mintFee(_reserve0, _reserve1);
9 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
10 if (_totalSupply == 0) {
11 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
12 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
13 } else {
14 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
15 }
16 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
17 _mint(to, liquidity);
18
19 _update(balance0, balance1, _reserve0, _reserve1);
20 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
21 emit Mint(msg.sender, amount0, amount1);
22 }
23
1)函数参数 接受流动性代币的地址,返回值 增发的数量,函数修饰器lock 防重入
2) 获取当前交易对的resverse
3)第三-四行 获取两个代币的当前余额, 再减去reserve(池子里两个代币原有 的数量) 就得到 两个代币的投入量。
4)计算开发团队手续费
5)使用局部变量保存已经发行了流动性的数量。减少gas
6) if(判断是否是初次提供)如果是初次, 计算恒定乘积公式中积的平方根,再减去最初始的流动性值(1000). 如果不是, 则每种代币按比例计算增发的流动性数量那流动性则是取两个值中较小的那个。
7)require验证 增发的流动性大于0,
8)_mint(to,liquidity) 增发流动性给接收者.
9):调用_update ,更新当前保存的恒定乘积中两个代币的值.
10)如果手续费打开,则更新乘积值.这个值只再计算手续费会用
11)触发Mint 事件
20 burn函数:通过燃烧流动性LP,来提取对应的代币,减少教育对的流动性
1 // this low-level function should be called from a contract which performs important safety checks
2 function burn(address to) external lock returns (uint amount0, uint amount1) {
3 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
4 address _token0 = token0; // gas savings
5 address _token1 = token1; // gas savings
6 uint balance0 = IERC20(_token0).balanceOf(address(this));
7 uint balance1 = IERC20(_token1).balanceOf(address(this));
8 uint liquidity = balanceOf[address(this)];
9
10 bool feeOn = _mintFee(_reserve0, _reserve1);
11 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
12 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
13 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
14 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
15 _burn(address(this), liquidity);
16 _safeTransfer(_token0, to, amount0);
17 _safeTransfer(_token1, to, amount1);
18 balance0 = IERC20(_token0).balanceOf(address(this));
19 balance1 = IERC20(_token1).balanceOf(address(this));
20
21 _update(balance0, balance1, _reserve0, _reserve1);
22 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
23 emit Burn(msg.sender, amount0, amount1, to);
24 }
25
1) 参数 代币接收者的地址,返回值 提取的两种代币数量,lock 防重入。
2) 先获取交易对的reserse,两个代币地址,保存在局部变量中,减少gas
3)获取交易对合约地址拥有的两种代币数量.
4) 获取当前合约地址的流动性代币余额,正常情况下,交易对合约里是不会有流动性代币的,因为所有流动性代币都是给到了流动性提供者的。而这里有值,其实是因为路由合约会先把用户的流动性代币划转到该配对合约里
5) 计算手续
6) 局部变量保存总流动性,节约gas
7)按比例计算资产。提取数量=用户流动性/总流动性*代币总余额
8)require 提取数量大于0。
9) _burn 将用户转让的流动性燃烧掉。
10)_把相应的代币发送给接收者。
11) 重新获取交易对合约地址所拥有的代币余额
12)_uodate 更新当前保存的恒定乘积中两种代币的值
13 更新Klast值
14 触发燃烧事件
21 Swap函数: 交易对中 资产的交换
1 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
2 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
3 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
4 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
8 address _token0 = token0;
9 address _token1 = token1;
10 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
11 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
12 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
13 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
14 balance0 = IERC20(_token0).balanceOf(address(this));
15 balance1 = IERC20(_token1).balanceOf(address(this));
16 }
17 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
18 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
19 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
20 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
21 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
22 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
23 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
24 }
25
26 _update(balance0, balance1, _reserve0, _reserve1);
27 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
28 }
29
1)4个参数 转出token0的数量,token1的数量,通常者两个值有一个为0,to为接收者地址,data参数执行回调时传递的数据,通过路由合约兑换 该值为0。Lock 防重入攻击
2)第2行 检验传入参数不能为0,第三行,获取revrese。
3)第四行:兑换出的数量必须小于reverse
4) 定义局部变量 保存当前交易对的两种代币余额
5) 7-16行用{},特殊语法,避免堆栈过深
6)8-10行:局部变量保存代币合约地址,并校验 接收者地址不i能为要兑换的代币地址
7)if amount0/1Out 谁大于0就转出,
8) if 如果data参数为空,则使用路由合约兑换(普通交易都是data为0)若不为空,to 地址转为 IUniswapV2Callee 并调用其 uniswapV2Call() 函数,这其实就是一个回调函数,to 地址需要实现该接口。并把data传过去
9)获取交易对合约地址的余额(此时已经减去了要转出的代币数量)
10)17-19行: 计算转入的代币数量 根据amountIn = balance - (reserve - amountOut),实际转入的一个0一个不为0。然后验证 其中一个转入值要大于0
11)20-24 行:防止堆栈过深, 进行最终的恒定乘积验证。公式:blance-0.003*amountOut。新的恒定乘积的积大于旧的值
12)更新恒定乘积的值reserve为balance
13)触发swap事件。
22 skim 函数 强制交易对合约中两种代币实际余额和保存的恒定乘积资产数量保存一致。如果有多则发送给调用者。任何人都可以调用
1 // force balances to match reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
223 sync函数 和skim函数相反,强制保存的恒定乘积的资产数量为交易对合约中两种代币的实际余额。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章