MultiSwap:如何用Solidity在多个DEX中套利

在一次交易中,在不同的去中心化交易所进行多次兑换.

如果你想获得最大的套利,可以需要在一次交易里在DEX(去中心化交易所)之间兑换代币。或者你想定期进行的某些兑换中节省Gas。或者你有在多个DEX之间进行定制的兑换场景,当然,也许你也可以仅仅是学习。

无论你是什么原因,我们试着做一个MultiSwap,MultiSwap 将结合多个交易所到一个合约中的进行交易。它看起来像这样:

  1. 在Bancor上用ETH购买BNT
  2. 在SushiSwap上卖出BNT换取INJ
  3. 在Uniswap 3上卖出INJ换取DAI

那么,我们如何才能实现这一目标?

套利备忘录

先手动操作

首先,我们想手动尝试所有的交易。由于是测试阶段,我们将在一个测试网上进行,这个测试网需要满足我们想要使用的每个协议部署了合约。在我们的案例中,这刚好是在Ropsten网络。

  • 如果你想交易的代币在测试网上不存在,可以通过Remix自己部署一个。
  • 如果DEX上的代币流动池在测试网上还不存在,那就自己创建一下。

1.Banchor:ETH -> BNT

因此,首先去Banchor,将你在Ropsten网络上的资金从ETH兑换到BNT。

Banchor App

进行兑换后,可以点击查看以太坊交易:

Banchor Etherscan

你会很容易在Etherscan交易中找到函数名称和传递的参数,记录下来,还有文档也是有帮助。

2. SushiSwap兑换: BNT -> INJ

然后去SushiSwap,将代币从BNT换成INJ:

SushiSwap

再次记下Etherscan 交易中的函数名称和参数。SushiSwap是基于Uniswap 2的,可以在之前的博文这里中找到更详细的解释,说明它是如何工作的。

3. Uniswap 兑换:INJ -> DAI

最后到Uniswap,将你的代币从INJ换成DAI:

Uniswap

再次记下Etherscan 交易中的函数名称和参数。你还可以在以前的博文这里中找到更详细的解释,说明Uniswap v3是如何工作的。

使用Solidity完成交易

乐趣开始的备忘录

有了前面的信息,建立交易逻辑就很简单了。首先在Bancor上交易,用收到的资金在SushiSwap上交易,然后再次用收到的资金在Uniswap上交易。

1.在Bancor上交易

IBancorNetwork private constant bancorNetwork = IBancorNetwork(0xb3fa5DcF7506D146485856439eb5e401E0796B5D);
address private constant BANCOR_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address private constant BANCOR_ETHBNT_POOL = 0x1aCE5DD13Ba14CA42695A905526f2ec366720b13;
address private constant BNT = 0xF35cCfbcE1228014F66809EDaFCDB836BFE388f5;

function _tradeOnBancor(uint256 amountIn, uint256 amountOutMin) private {
  bancorNetwork.convertByPath{value: msg.value}(_getPathForBancor(), amountIn, amountOutMin, address(0), address(0), 0);
}

function _getPathForBancor() private pure returns (address[] memory) {
    address[] memory path = new address[](3);
    path[0] = BANCOR_ETH_ADDRESS;
    path[1] = BANCOR_ETHBNT_POOL;
    path[2] = BNT;

    return path;
}

我们在Banchor上交易的功能简单明了。从前面例子交易中获得了交易路径和Bancor网络的地址。

2. 在Sushi上交易

IUniswapV2Router02 private constant sushiRouter = IUniswapV2Router02(0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506);
address private constant INJ = 0x9108Ab1bb7D054a3C1Cd62329668536f925397e5;

function _tradeOnSushi(uint256 amountIn, uint256 amountOutMin, uint256 deadline) private {
    address recipient = address(this);

    sushiRouter.swapExactTokensForTokens(
        amountIn,
        amountOutMin,
        _getPathForSushiSwap(),
        recipient,
        deadline
    );
}

function _getPathForSushiSwap() private pure returns (address[] memory) {
    address[] memory path = new address[](2);
    path[0] = BNT;
    path[1] = INJ;

    return path;
}

然后我们使用swapExactTokensForTokens将BNT兑换到INJ。兑换路径由代币组成。相关的地址可以从前面的交易例子中获得。

3. 在Uniswap上交易

IUniswapRouter private constant uniswapRouter = IUniswapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address private constant DAI = 0xaD6D458402F60fD3Bd25163575031ACDce07538D;

function _tradeOnUniswap(uint256 amountIn, uint256 amountOutMin, uint256 deadline) private {
    address tokenIn = INJ;
    address tokenOut = DAI;
    uint24 fee = 3000;
    address recipient = msg.sender;
    uint160 sqrtPriceLimitX96 = 0;

    ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams(
        tokenIn,
        tokenOut,
        fee,
        recipient,
        deadline,
        amountIn,
        amountOutMin,
        sqrtPriceLimitX96
    );

    uniswapRouter.exactInputSingle(params);

    // 译者注: 这里应该是原作者的疏忽,例子中的兑换不涉及 ETH ,应该是不需要返回 ETH。
    uniswapRouter.refundETH();
    // refund leftover ETH to user
    (bool success,) = msg.sender.call{ value: address(this).balance }("");
    require(success, "refund failed");
}

4. 集合在一个交易里

我们还需要批准SushiSwap合约来使用BNT,批准Uniswap合约来使用INJ。在部署时只做一次会更省力,所以可以把它放在构造函数中:

constructor() {
  IERC20(BNT).safeApprove(address(sushiRouter), type(uint256).max);
  IERC20(INJ).safeApprove(address(uniswapRouter), type(uint256).max);
}

现在我们有了需要的一切,创建一个multiSwap函数:

function multiSwap(uint256 deadline, uint256 amountOutMinUniswap) external payable {
    uint256 amountOutMinBancor = 1;
    uint256 amountOutMinSushiSwap = 1;

    _tradeOnBancor(msg.value, amountOutMinBancor);
    _tradeOnSushi(IERC20(BNT).balanceOf(address(this)), amountOutMinSushiSwap, deadline);
    _tradeOnUniswap(IERC20(INJ).balanceOf(address(this)), amountOutMinUniswap, deadline);
}

如你所见,现在兑换代币很容易。对于Bancor和SushiSwap,我们不关心我们收到多少代币,所以我们把最小值设为1。唯一重要的是我们在最后一次兑换中收到多少DAI代币。这个值从外部传来,作为UNIX时间戳的最后交易期限也是类似。如果你不关心交易何时执行,可以传递一个很高的截止时间戳。

但是如何获得一个合理的amountOutMinUniswap值呢? 为了获得它,我们可以创建第二个函数,只作为视图函数来调用。

// meant to be called as view function
function multiSwapPreview() external payable returns(uint256) {
    uint256 daiBalanceUserBeforeTrade = IERC20(DAI).balanceOf(msg.sender);
    uint256 deadline = block.timestamp + 300;

    uint256 amountOutMinBancor = 1;
    uint256 amountOutMinSushiSwap = 1;
    uint256 amountOutMinUniswap = 1;

    _tradeOnBancor(msg.value, amountOutMinBancor);
    _tradeOnSushi(IERC20(BNT).balanceOf(address(this)), amountOutMinSushiSwap, deadline);
    _tradeOnUniswap(IERC20(INJ).balanceOf(address(this)), amountOutMinUniswap, deadline);

    uint256 daiBalanceUserAfterTrade = IERC20(DAI).balanceOf(msg.sender);
    return daiBalanceUserAfterTrade - daiBalanceUserBeforeTrade;
}

但是请注意,我们没有把它声明为视图函数,因为它使用非视图函数来计算结果,所以不可能将它本身声明为一个视图函数(Solidity 语法特性的限制)。

我们没有在链上调用这个函数。它仍然是作为一个视图函数来调用的,例如在前端使用 Web3 的 call() 功能来读取结果。

现在可以在我们的前端调用multiSwapPreview,为了增加交易不被退回的机会,可以将收到的DAI的估计金额减少一点(称之为滑点)。

const estimatedDAI = (await myContract.multiSwapPreview({ value: ethAmount }).call())[0];
const amountOutMinUniswap = estimatedDAI * 0.96;

现在我们只需要一笔交易就可以完成整个兑换。

多重兑换 ethscan

你可以在这里找到一个完全可行的交易代码 。如果你在测试网掌握了它,就可以在主网上重复这个过程。如果你不想花额外的ETH进行手工交易(来获取地址),你可以在提交任何东西之前检查交易数据和合约地址,因为你需要改变的就是合约地址。


本翻译由 CellETF 赞助支持。

点赞 6
收藏 11
分享

2 条评论

请先 登录 后评论
翻译小组
翻译小组
我代表了好多社区小伙伴~