# 区块链开发课第六讲 智能合约开发(3) **Published by:** [yueying007](https://paragraph.com/@yueying007/) **Published on:** 2022-05-06 **URL:** https://paragraph.com/@yueying007/3 ## Content 这节课,我们继续完善SimpleArbi.sol,完成一笔完整的闪电贷套利操作。GitHub - yueying007/blockchainclassContribute to yueying007/blockchainclass development by creating an account on GitHub.https://github.com流程我们在智能合约中实现一个双边套利的操作:发起闪电贷WETH在Curve中把WETH兑换为USDT在Uniswap中把USDT兑换为WETH归还闪电贷WETH结构体在上一节中,我们已经分别实现了Curve和Uniswap的兑换操作,接下来,需要定义一个结构体,来保存一笔兑换的参数信息:struct SwapData { uint function_id; uint256 token_in_id; uint256 token_out_id; address token_in; address token_out; address pool; } 同时,在还款信息RepayData中加入一个SwapData类型的数组SwapData[],用来告诉合约这些兑换参数的信息。并加入一个标志direct_repay用来区分获得闪电贷后的操作(直接归还/进行套利):struct RepayData { SwapData[] swap_data; address repay_token; uint256 repay_amount; address recipient; bool direct_repay; } 入口函数定义一个execute()函数作为套利的入口函数。function execute(bytes[] memory data, uint256 amount_in) public onlyOwner Lock returns(uint256) { SwapData[] memory _swap_data = new SwapDataUnsupported embed; for (uint i = 0; i <= data.length - 1; i++) { _swap_data[i] = abi.decode(data[i], (SwapData)); } uint256 balance_before = IERC20(_swap_data[0].token_in).balanceOf(address(this)); RepayData memory _repay_data = RepayData(_swap_data, _swap_data[0].token_in, amount_in, liquidityPool, false); ILiquidity(liquidityPool).borrow(_swap_data[0].token_in, amount_in, abi.encodeWithSignature("receiveLoan(bytes)", abi.encode(_repay_data))); uint256 balance_after = IERC20(_swap_data[0].token_in).balanceOf(address(this)); require(balance_after > balance_before, "No Profit!"); return balance_after - balance_before; } 它接收两个参数: data: bytes类型的数组,用来存放兑换参数信息 amount_in: 需要闪电贷WETH的数量 首先,定义一个临时变量_swap_data,从data中解析出兑换参数的列表。 然后记录一下合约中WETH的数量。 然后定义一个临时变量_repay_data来存储兑换参数、还款信息(还款token、还款数量和还款地址)以及direct_repay标志。这里把direct_repay设为false,表示在获得闪电贷后执行套利操作,而不是直接还款。 接着调用liquidityPool的borrow()函数发起一笔闪电贷,并告诉它接收闪电贷的回调函数名称及参数类型(receiveLoan/bytes),以及参数值(把_repay_data加密为一段bytes)回调函数发起闪电贷后,我们收到一笔WETH的贷款,并且回调函数receiveLoan()被调用:// callback function receiveLoan(bytes memory data) public { require(!lock, "Locked"); RepayData memory _repay_data = abi.decode(data, (RepayData)); if (_repay_data.direct_repay) { IERC20(_repay_data.repay_token).safeTransfer(_repay_data.recipient, _repay_data.repay_amount); } else { uint _length = _repay_data.swap_data.length; uint256 out_amount; for (uint i = 0; i <= _length - 1; i++) { out_amount = SwapBase(_repay_data.swap_data[i].pool, _repay_data.swap_data[i].function_id, i == 0 ? _repay_data.repay_amount : out_amount, _repay_data.swap_data[i].token_in_id, _repay_data.swap_data[i].token_out_id, _repay_data.swap_data[i].token_in, _repay_data.swap_data[i].token_out); } IERC20(_repay_data.repay_token).safeTransfer(_repay_data.recipient, _repay_data.repay_amount); } } 首先,定义临时变量_repay_data,解析出data中的还款信息,如果direct_repay为true,直接还款(在Uniswap兑换的回调中用到),否则进行如下的套利操作: 遍历_repay_data.swap_data数组,依次调用SwapBase()函数进行多笔兑换。最后进行还款操作。检查利润运行到这里,一笔闪电贷套利就完成了,最后我们在execute()函数的末尾要检查一下是否有利润:uint256 balance_after = IERC20(_swap_data[0].token_in).balanceOf(address(this)); require(balance_after > balance_before, "No Profit!"); return balance_after - balance_before; 如果进行套利之后,合约中的WETH数量反而变少了,要进行revert回滚,因为不能允许一笔套利交易是亏损的。 如果有利润的话,最后返回利润的数值。这里为什么要返回利润值,不是多此一举吗?因为在生产环境下,在发出一笔交易前,我们需要先进行模拟,这里的模拟返回结果可以帮助我们进一步分析套利的成本、净利润等,从而调整gas价格策略,在后面的课程中会详细讲解。测试首先搭建mainet-fork测试环境(见第四讲):ganache-cli --fork https://eth-mainnet.alchemyapi.io/v2/your_api_key 编译并部署合约:truffle compile truffle migrate 然后我们使用一个python脚本test_contract.py来进行测试。 开始测试前,建立一个python的虚拟环境:sudo apt install python-virtualenv cd ~/Projects/blockchainclass virtualenv -p /usr/bin/python3.9 venv 安装web3.pycd ~/Projects/blockchainclass source venv/bin/activate pip install web3 然后在test_contract.py中,填入合约部署地址,以及ganache-cli中生成的第一个账户地址和第一个密钥:if __name__ == '__main__': test_contract(contract_address='', account='', private_key='') 最后运行:python test_contract.py 可以看出,最后返回的结果是revert No Profit! 表明套利交易运行到最后,检查利润小于0,交易回滚了。结语以上我们在智能合约中实现了一个简单的双边套利操作,同样,我们可以继续拓展,实现三边、四边、五边..套利,并且可以在Curve和Uniswap以外的Dex中进行套利。 在本例中,Curve在前,Uniswap在后,所以我们用闪电贷获取初始资金,如果是Uniswap在前,Curve在后,就可以使用Uniswap的闪电兑功能,即先在Uniswap中发起一笔swap(),然后在回调函数中在Curve中进行兑换。 这些都可以作为思考题,留给有心的读者进行深入研究。要提醒的是,这里的示例代码只是做演示用,请在进行深入研究并开发出有利可图的策略之前,不要把示例代码部署到生产环境下。 下一讲,我们将会构建一个python脚本,实时监控以太坊上的套利机会,并调用智能合约进行套利。 欢迎来即刻App与我互动,即刻账号: 月影007 ## Publication Information - [yueying007](https://paragraph.com/@yueying007/): Publication homepage - [All Posts](https://paragraph.com/@yueying007/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@yueying007): Subscribe to updates