交换意味着使用一定数量的TokenA来换取Tokenb。但我们需要一些额外的辅助服务:
1.提供实际汇率。
2.保证所有的交易都是在正确的汇率下进行的。
GitHub - XuHugo/solidityproject: DApp go go go !!!
我们在研究流动性供应时学到了一些关于 DEX 定价的知识:决定汇率的是池中的流动性数量。成功互换的主要条件:互换后的reserve乘积必须等于或大于互换前的reserve乘积。无论Pool中的reserve数量是多少,恒等乘积都必须保持不变。这基本上是我们必须保证的唯一条件,而且令人惊讶的是,这个条件让我们无需计算互换价格。
正如我在介绍中提到的,Pair合约是一个核心合约,这意味着它的功能必须尽可能底层化和最小化。这也影响到我们如何向合约发送代币。有两种方法可以将代币转移给他人:
1、调用Token合约的transfer方法,并传递接收者的地址和要发送的金额。
2、调用 approve 方法,将一定数量的代币授权,允许其他用户或合约使用。其他人必须调用 transferFrom 才能转移你的Token。你只需为批准一定数量的Token支付费用,而对方则需为实际转账支付费用。
调用 approve 模式在以太坊应用中非常常见:dapp 要求用户批准最大金额的消费,这样用户就不需要一次又一次地调用批准。这可以改善用户体验。而这并不是我们目前需要考虑的,因此,我们将采用手动转入Pair合约的方式。
函数入参需要两个输出金额,每个token一个。这些是调用者希望用token换取的金额。为什么要这样做呢——需要两个token?因为我们不想强制执行交换的方向:调用者可以指定任一个金额或两个金额,我们只需执行必要的检查。
function swap(uint256 amount0Out,uint256 amount1Out,address to) public {if (amount0Out == 0 && amount1Out == 0)revert InsufficientOutputAmount();...
接下来,我们需要确保有足够的reserve发送给用户。
...(uint112 reserve0_, uint112 reserve1_, ) = getReserves();if (amount0Out > reserve0_ || amount1Out > reserve1_)revert InsufficientLiquidity();...
在获得reserve并进行预检查后,我们要做的第一件事就是将token转移给用户。有趣的是,我们可以提前做这件事,反而是未了更安全,后边我们会介绍原因,也许你现在就知道了原因。转账完成后,我们再计算输入金额:
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);uint256 balance0 = IERC20(token0).balanceOf(address(this));uint256 balance1 = IERC20(token1).balanceOf(address(this));uint256 amount0In = balance0 > reserve0 - amount0Out? balance0 - (reserve0 - amount0Out): 0;uint256 amount1In = balance1 > reserve1 - amount1Out? balance1 - (reserve1 - amount1Out): 0;if (amount0In == 0 && amount1In == 0) revert InsufficientInputAmount();
为了让这部分的逻辑更清晰,我们可以把 reserve0 和 reserve1 看作 "旧余额",即交换开始前合约的余额。
在交换token时,我们通常会提供 amount0Out 或 amount1Out。因此,通常会有 amount0In 或 amount1In。但这里允许我们同时设置 amount0Out 和 amount1Out,因此 amount0In 和 amount1In 也有可能都大于零。但如果这两个值都为零,用户就没有向合约发送任何Token,这是不允许的。
因此,在这几行中,我们发现了新的余额:它们不包括输出金额,但包括输入金额。
然后就是我们之前讨论过的常数乘积检验。我们预计这个合约token的余额与其reserve不同,我们需要确保它们的乘积等于或大于当前储备的乘积。如果满足此要求,则:
1.调用方正确计算了汇率(包括滑点)。
2.输出量正确。
3.转到合约中的金额也是正确的。
uint256 balance0Adjusted = (balance0 * 1000) - (amount0In * 3);uint256 balance1Adjusted = (balance1 * 1000) - (amount1In * 3);if (balance0Adjusted * balance1Adjusted <uint256(reserve0_) * uint256(reserve1_) * (1000**2)) revert InvalidK();
首先,我们计算调整后的余额:即当前余额减去swap fee,后者适用于输入金额。同样,由于整除的原因,我们必须将余额乘以 1000,金额乘以 3,以 "模拟 "输入金额乘以 0.003(0.3%)。
接下来,我们要为调整后的余额计算一个新的 K,并将其与当前的 K 进行比较。为了补偿调整后余额乘以 1000 的结果,我们将旧储备金乘以 1000 * 1000。
基本上,我们是用新余额减去掉期费来计算新的 K 值。这个新 K 必须大于或等于旧 K。
让我们测试一下,当我们试图获取过多的输出代币时,是否会出现 InvalidK 错误:
function testSwapUnpaidFee() public {token0.transfer(address(pair), 1 ether);token1.transfer(address(pair), 2 ether);pair.mint(address(this));token0.transfer(address(pair), 0.1 ether);vm.expectRevert(encodeError("InvalidK()"));pair.swap(0, 0.181322178776029827 ether, address(this), "");}
在这里,我们试图用 0.181322178776029827 ether 的 token1 交换 0.1 ether 的 token0,但失败了。如果将代币 1 的金额减少 1,测试就会通过。我使用 getAmountOut 计算了这个数额!