# 区块链开发课第六讲 智能合约开发(3)

By [yueying007](https://paragraph.com/@yueying007) · 2022-05-06

---

这节课，我们继续完善SimpleArbi.sol，完成一笔完整的闪电贷套利操作。

[

GitHub - yueying007/blockchainclass
-----------------------------------

Contribute to yueying007/blockchainclass development by creating an account on GitHub.

https://github.com

![](https://storage.googleapis.com/papyrus_images/14df3d9367ff4dd1c02c6b37edbeeaaeecccdec76b9a3ce88a3b9144b747b31c.png)

](https://github.com/yueying007/blockchainclass.git)

流程
--

我们在智能合约中实现一个双边套利的操作：

1.  发起闪电贷WETH
    
2.  在Curve中把WETH兑换为USDT
    
3.  在Uniswap中把USDT兑换为WETH
    
4.  归还闪电贷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.py

    cd ~/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_

---

*Originally published on [yueying007](https://paragraph.com/@yueying007/3)*
