yueying007

Posted on May 06, 2022Read on Mirror.xyz

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

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

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 SwapData[](data.length);
    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

Recommended Reading