这节课,我们继续完善SimpleArbi.sol,完成一笔完整的闪电贷套利操作。
我们在智能合约中实现一个双边套利的操作:
在上一节中,我们已经分别实现了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