• 译者:翻译小组 [1]

  • 校对:Tiny 熊 [2]

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

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

  1. 在 Bancor 上用 ETH 购买 BNT[3]。

  2. 在 SushiSwap 上卖出 BNT 换取 INJ[4]。

  3. 在 Uniswap 3 上卖出 INJ 换取 DAI[5]。

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

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

先手动操作

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

  • 如果你想交易的代币在测试网上不存在,可以通过 Remix 自己部署一个。

  • 如果 DEX 上的代币流动池在测试网上还不存在,那就自己创建一下。

1.Banchor:ETH -> BNT

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

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

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

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

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

2. SushiSwap 兑换 : BNT -> INJ

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

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

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

3. Uniswap 兑换:INJ -> DAI

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

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

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

使用 Solidity 完成交易

MultiSwap:如何用 Solidity 在多个 DEX 中套利 乐趣开始的备忘录

有了前面的信息,建立交易逻辑就很简单了。首先在 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 "] memory path = new address[");          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 "] memory path = new address[");          path[0] = BNT;          path[1] = INJ;          return path;      }  

然后我们使用 swapExactTokensForTokens[16] 将 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;      }  

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

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

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

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

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

MultiSwap:如何用 Solidity 在多个 DEX 中套利 多重兑换 ethscan

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


本翻译由 CellETF[21] 赞助支持。

来源: https://github.com/lbc-team/Pioneer

参考资料

[1]

翻译小组 : https://learnblockchain.cn/people/412

[2]

Tiny 熊 : https://learnblockchain.cn/people/15

[3]

BNT: https://etherscan.io/token/0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c

[4]

INJ: https://etherscan.io/token/0xe28b3b32b6c345a34ff64674606124dd5aceca30

[5]

DAI: https://etherscan.io/token/0x6b175474e89094c44da98b954eedeac495271d0f

[6]

Banchor: https://app.bancor.network/eth/swap?from=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE &to;=0xF35cCfbcE1228014F66809EDaFCDB836BFE388f5

[7]

Etherscan 交易 : https://ropsten.etherscan.io/tx/0x21b95960b1a7c832c91e705390420edf3faa35b18469a8bc517056d88af9634e

[8]

文档 : https://docs.bancor.network/developer-quick-start/trading-with-bancor#trading-from-your-smart-contract

[9]

SushiSwap: https://app.sushi.com/swap?inputCurrency=0x9108Ab1bb7D054a3C1Cd62329668536f925397e5 &outputCurrency;=0xF35cCfbcE1228014F66809EDaFCDB836BFE388f5

[10]

Etherscan 交易 : https://ropsten.etherscan.io/tx/0x727301c32fcdbb29e14203610b26c7ab7f44f5d940057c2c39ecc0ae9e919c0f

[11]

这里 : https://learnblockchain.cn/article/2580

[12]

Uniswap: https://app.uniswap.org/#/swap

[13]

Etherscan: https://ropsten.etherscan.io/tx/0xc23e6efa4c95747cb1421b582b1d29ce1ae1a529f84c28a94f74536997358262

[14]

交易 : https://ropsten.etherscan.io/tx/0x727301c32fcdbb29e14203610b26c7ab7f44f5d940057c2c39ecc0ae9e919c0f

[15]

这里 : https://learnblockchain.cn/article/2580

[16]

swapExactTokensForTokens: https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-02#swapexacttokensfortokens

[17]

视图函数 : https://learnblockchain.cn/docs/solidity/contracts.html#view

[18]

call(): https://web3js.readthedocs.io/en/v1.3.4/web3-eth-contract.html#methods-mymethod-call

[19]

一笔交易 : https://ropsten.etherscan.io/tx/0xba44787781aac57b462ff7d4a9781506553a4a57e7339070e76eb5ba544394f0

[20]

这里 : https://gist.github.com/gorgos/14fa5f932fc697fd8aa3c223856fce7b

[21]

CellETF: https://celletf.io/?utm_souce=learnblockchain