译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

Uniswap Flash Swap
在上一篇[4]文章中,我们了解了如何使用 Uniswap 实现代币之间的兑换。
在这篇文章中,我们将通过探讨闪电兑换(Flash Swaps)来向前推进一个层次。
在阅读本文之前,强烈建议你阅读之前之前的文章了解 Uniswap 的运行规则。
与传统贷款不同,在闪电贷中,在一个交易里完成资金借入和归还。这一点是必须的。
在Defi里,交易者(通常通过机器人)不断寻找套利机会,通过在为同一资产提供不同价格的平台之间进行交易来获得利益。
这就是闪电贷出现的地方(通常是)。
在闪电贷的帮助下,交易者可以借到一大笔钱来执行套利交易。闪电贷和闪电兑换其实是一回事。

考虑一种情况,Ethan 从书店花 10 美元买了一本书,然后以 20 美元的价格将这本书卖给 Jennifer。在这种情况下,Ethan 用自己的钱买了一本书,然后通过卖给 Jennifer 直接翻倍。
这正是交易套利的运作方式。
但与 Ethan 用自己的钱从书店买书不同的是,在这里,我们可以简单地使用闪电贷借 10 美元,然后执行交易,类似于卖书,然后偿还贷款(是的,所有这些都在一个交易中)。
让我们潜心编写我们自己的 Flash 兑换合约,并进行测试吧!😎
在命令行终端(CLI)上使用以下命令来初始化项目:
mkdir Flash_swap && cd Flash_swap
npm init -y
现在,安装我们将在项目中使用的依赖项。
使用下面提供的命令,在命令行终端上运行来安装它们:
npm install --save hardhat @nomiclabs/hardhat-ethers @nomiclabs/hardhat-waffle ethers @uniswap/v2-core dotenv hardhat chai
在你的 CLI 上运行 npx hardhat 命令,并创建一个空的 hardhat 配置文件(然后定制 Hardhat 配置),因为我们要从头开始构建一切。
因为我们要主网的 fork 来测试 Flashswap。因此,你的 Hardhat 配置应该看起来类似于这样:

注意:用你自己的Alchemy[5]API 密钥替换 URL 中的<key>部分。
另外,如果你不了解主网 fork,请阅读我们的这篇文章[6],然后再跟着这篇文章走。
为合约和测试创建新目录,以便更好地组织代码,在你的 CLI 中使用以下代码:
mkdir contracts && mkdir tests
为了编写 flash swap 合约,在合约目录内创建一个文件,并将其命名为flashswap.sol。
编写智能合约:
首先,导入所需的接口并创建一个名为 flashSwap的合约。
我们将导入Uniswap的接口,以使用其功能。你可以使用这个链接[7]获得该接口文件。
我们还导入了IUniswapV2Callee接口。当我们执行 flash swap 时,Uniswap 将调用这个函数。技术上来说,这是 Uniswap 将调用的回调函数。
之后代码看起来应该类似于这样:

接下来,编写我们的flashSwap合约,它继承自IUniswapV2Callee。Solidity 支持智能合约之间的继承,多个合约可以被一个合约继承。从中继承功能的合约被称为基础合约,而继承功能的合约被称为派生合约。
这个合约将有 2 个功能:

现在编写testFlashSwap 函数:
这个函数将接受 2 个参数:(A)要从 Uniswap 借入的代币{地址}和(B)我们想借入的金额。
注意:在 Uniswap v2 中,所有的代币对都是以 WETH(代替 ETH)作为其中一个币种,因此,要检查一个特定的代币时候与 ETH 在 Uniswap 上具有配对,我们只需检查它与 WETH 的配对即可。更多关于什么是 WETH → https://weth.io/
address pair = IUniswapV2Factory(UniswapV2Factory).getPair(_tokenBorrow, WETH);
require(pair != address(0), "!pair");_请注意,我们在合约中已经将 WETH 定义为一个变量。请参考文章最后的完整合约_。
在 UniswapV2Pair 中有两个代币,token0和token1。
我们检查*_tokenBorrow是否等于_token0*,如果相等,那么amount0Out将是我们传递给函数的_amount 参数,否则,它将等于 0。
同样地,我们将对*_token1*进行同样的检查。
address token0 = IUniswapV2Pair(pair).token0();
address token1 = IUniswapV2Pair(pair).token1();
uint256 amount0Out = _tokenBorrow == token0 ? _amount : 0;
uint256 amount1Out = _tokenBorrow == token1 ? _amount : 0;结果是,要么amount0Out等于_amount 和 amount1Out等于 0,或相反。
然后在 Uniswap 的 swap 函数中传递这些数值:
bytes memory data = abi.encode(_tokenBorrow, _amount);
IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(this), data);你会注意到,这和我们在 Uniswap 上执行简单兑换时调用的函数完全一样。请参考我们之前的文章here.[8]
唯一的区别是最后一个参数。如果它是空的,那么 Uniswap 将尝试执行一个简单的兑换。
如果最后一个参数不是空的,而是有附加数据,那么它将会触发一个闪电兑换。
为了传递输入参数,我们将把tokenBorrow和amount编码为字节,然后传递给 swap 函数。
testFlashSwap函数看起来应该类似于:

现在我们来写一下uniswapV2Call:
address token0 = IUniswapV2Pair(msg.sender).token0();
address token1 = IUniswapV2Pair(msg.sender).token1();
// call uniswapv2factory to getpair
address pair = IUniswapV2Factory(UniswapV2Factory).getPair(token0, token1);
require(msg.sender == pair, "!pair");(address tokenBorrow, uint amount) = abi.decode(_data, (address, uint));
uint fee = ((amount * 3) / 997) + 1;
uint amountToRepay = amount + fee;
IERC20(tokenBorrow).transfer(pair, amountToRepay);
这就完成了我们的uniswapV2Call函数,它看起来类似于下面:

现在,我们的 flashSwap 合约已经完成,应该类似于这样:

在本文的例子中,为了简化,uniswapV2Call 函数中并没有使用收到的代币进行套利操作,读者可以根据需要在uniswapV2Call中调用其他的 DEX 进行套利。
首先,我们将导入必要的库、ERC20 ABI 、以及创建测试脚本的基本结构。

我们还将定义要借入的金额。
使用下面的方法:
const USDCHolder = "0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE";
const USDCAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const borrowAmount = 1000000000; // 因为 USDC 是 6 为小数,对应 1000 USDC
before块开始使用我们创建的合约并部署它。before(async () => {
const TestFlashSwapFactory = await ethers.getContractFactory("flashSwap");
TestFlashSwapContract = await TestFlashSwapFactory.deploy();
await TestFlashSwapContract.deployed();
});
A:首先,我们将冒充要使用的账户:
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [USDCHolder],
});
const impersonateSigner = await ethers.getSigner(USDCHolder);
然后,定义 USDC 合约:
const USDCContract = new ethers.Contract(USDCAddress, ERC20ABI, impersonateSigner)
B: 我们已经知道,Uniswap 对实现 Flash Swap 收取一定的费用。因此需要计算该费用:
const fee = Math.round(((borrowAmount * 3) / 997)) + 1;
C:我们需要了解,由于合约刚刚在before代码区块中得到部署,它没有任何以太币或任何 USDC。那么,一旦闪电借款被执行,合约就必须归还所借的金额和费用。
因此,我们必须把费用的金额从冒充的账户转移到我们的合约,以便完成交易。
因此,我们将使用以下代码将费用转移到合约中。
await USDCContract.connect(impersonateSigner).transfer(TestFlashSwapContract.address, fee)
D: 然后,调用在合约中定义的testFlashSwap函数。
await TestFlashSwapContract.testFlashSwap(USDCContract.address, borrowAmount)
E: 现在是时候检查执行的 Flashswap 是否正确了。为此,我们将检查合约余额,在 Flashswap 和支付我们计算的确切费用后,是否为 0,因为它应该是这样的。
const TestFlashSwapContractBalance = await USDCContract.balanceOf(TestFlashSwapContract.address)
expect(TestFlashSwapContractBalance.eq(BigNumber.from("0"))).to.be.true;
你的最终测试脚本应该是这样的。

最后,在 CLI 中使用npx hardhat test tests/flashswaptest命令运行测试。
结果应该是这样的:

到这里,我们的闪电兑换测试就通过。
闪电兑换允许在 Uniswap 上借用任何 ERC20 代币并执行任何代码逻辑(在 uniswapV2Call 中),只要你在同一笔交易中偿还相同的代币或相同价值的任何其他代币以及费用。
希望你可以在本文的基础上,并尝试自己运行闪电兑换进行更多的交易:)
同样,本文运行的所有代码都在这里Github 仓库[9]。
本翻译由 Duet Protocol[10] 赞助支持。
原文:https://medium.com/uv-labs/flash-swap-5bcdbd9aaa14
[1]
登链翻译计划: https://github.com/lbc-team/Pioneer
[2]
翻译小组: https://learnblockchain.cn/people/412
[3]
Tiny 熊: https://learnblockchain.cn/people/15
[4]
上一篇: https://learnblockchain.cn/article/4197
[5]
Alchemy: https://alchemy.com/?r=7d60e34c-b30a-4ffa-89d4-3c4efea4e14b
[6]
这篇文章: https://medium.com/uv-labs/fork-the-f-ing-ethereum-blockchain-transfer-tokens-from-vitaliks-account-46d408f7356c
[7]
链接: https://github.com/UV-Labs/Tutorials/blob/main/Flash_swap/contracts/interfaces/Uniswap.sol
[8]
here.: https://medium.com/uv-labs/uniswap-testing-1d88ca523bf0
[9]
Github仓库: https://github.com/UV-Labs/Tutorials
[10]
Duet Protocol: https://duet.finance/?utm_souce=learnblockchain