来!写台印钞机!

算是个 Solidity 入门级应用?

MDEX 应该是这几天币圈最火的矿了。考虑到现在各种币都在高歌猛进,奋力暴涨,我实在是没有当 LP 的勇气,生怕被无常损失给收拾了,所以只拿了点大饼进去白嫖。当时也有注意到 MDEX 引入了曾经风靡各大 CEX 的交易挖矿,但是白嫖的想法太强烈,也就没有在意,直到前两天看到了神鱼的微博:

这个类似闪电贷的玩法勾起了我的兴趣,有赚就成交,没赚就返回,简直就是印钞机啊!百无聊赖的周日夜里,我决定写出我的第一个 Solidity 应用——把这个套利合约实现出来。

交易对选择

MDEX 启动了挖矿奖励的交易对中,HUSD/USDT 的奖励是最高的,所以其流动性也是最好的,因此我将它选为目标。

奖励分配机制

我们首先需要确定一下 MDEX 交易挖矿的奖励分配逻辑,以免掉到什么奇怪的坑里去。

由于 MDEX 并未在官网列出他们的合约地址,所以我们得自己找出来。先在 MDEX 上做一次兑换,之后在hecochain.com 上查看这次兑换交易是和哪个合约交互的,就可以找到 MDEX 的兑换合约 MdexRouter:0xed7d5f38c79115ca12fe6c0041abb22f0a06c300,点开浏览器交易页面的 inputData,可以找到这次合约交互调用的函数:

之后我们进入合约详情页,可以看到 MDEX 已经将合约提交给 hecochain.com,所以我们可以直接在区块浏览器中阅读合约源码。搜索👆的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = IMdexFactory(factory).getAmountsOut(amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'MdexRouter: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}

函数输入分别为:1. 用于兑换的代币 A 的数量;2. 希望能换到的代币 B 的最小数量;3. 兑换路径,比如 USDT → HUSD 或者 USDT → WETH → HUSD;4. 代币接收地址;5. 交易截止时间。这个函数是用来做代币兑换的,函数体第一行返回的 amounts 是个列表,记录兑换过程中每种代币的数量,最终换得的代币数量就是列表的最后一个元素。

对于我们来说,代币兑换过程并不重要,我们想要知道的是刷量的时候,奖励是如何分配的。搜索最后一行的 _swap 函数,得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// **** SWAP ****
// requires the initial amount to have already been sent to the first pair
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = IMdexFactory(factory).sortTokens(input, output);
uint amountOut = amounts[i + 1];
if (swapMining != address(0)) {
ISwapMining(swapMining).swap(msg.sender, input, output, amountOut);
}
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
address to = i < path.length - 2 ? pairFor(output, path[i + 2]) : _to;
IMdexPair(pairFor(input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}

我们可以看到想要的关键词出现了“Mining”。从代码中可以看出 swapMining 是负责挖矿逻辑的合约,我们需要找出它的地址。由于 hecochain.com 暂不支持 Read Contract,所以我们需要调用 MdexRouter 来查看:

1
2
3
4
5
6
7
8
9
10
11
12
const Web3 = require('web3')

const web3 = new Web3('https://http-mainnet.hecochain.com')

const ROUTER_ABI= require('./abis/router_abi.json')
const ROUTER_CONTRACT_ADDR = web3.utils.toChecksumAddress('0xed7d5f38c79115ca12fe6c0041abb22f0a06c300')
const ROUTER = new web3.eth.Contract(ROUTER_ABI, ROUTER_CONTRACT_ADDR)

async function querySwapMining() {
let result = await ROUTER.methods.swapMining().call()
console.log(result)
}

执行脚本,得到 SwapMining 的地址:0x7373c42502874C88954bDd6D50b53061F018422e。在这个合约中我们可以找到奖励计算逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function swap(...) {
...
pool.quantity = pool.quantity.add(quantity);
pool.totalQuantity = pool.totalQuantity.add(quantity);
UserInfo storage user = userInfo[pairOfPid[pair]][account];
user.quantity = user.quantity.add(quantity);
...
}

function takerWithdraw(...) {
...
uint256 userReward = pool.allocMdxAmount.mul(user.quantity).div(pool.quantity);
...
}

可见逻辑很简单,每次刷量累加用户交易量,奖励依据用户交易量占交易对总交易量的比例分配。

套利过程

现在我们可以决定套利过程了,很简单:

  1. 使用 USDT 兑换 HUSD

  2. 使用 1 中得到的 HUSD 兑换 USDT

  3. 领取 MDX 奖励,并卖成 USDT

  4. 计算 USDT 总额,如果扣除 gas 成本后能够大于 0 的话,执行交易,否则回滚。gas 使用 HT 计价的,为了简化计算,我们将其设置为一个固定值

代码编写

让我们将套利逻辑用 Solidity 描述出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function makeMoney(...) public onlyOwner {
// ...
// 使用 USDT 兑换 HUSD
uint[] memory round1Amounts = router.swapExactTokensForTokens(round1AmountIn, round1AmountMinOut, round1Path, miner, deadline);
uint256 round2AmountIn = round1Amounts[round1Path.length - 1];

// 使用 HUSD 兑换 USDT
uint[] memory round2Amounts = router.swapExactTokensForTokens(round2AmountIn, round2AmountMinOut, round2Path, miner, deadline);
uint256 amountOut = round2Amounts[round2Path.length - 1];
// 计算成本
uint256 cost = round1AmountIn.sub(amountOut);

// 领取 MDX 奖励
swapMining.takerWithdraw();
uint256 rewardBalance = rewardToken.balanceOf(miner);

// 卖出 MDX 套现,得到 USDT
uint[] memory cashOutAmounts = router.swapExactTokensForTokens(rewardBalance, 0, cashOutPath, miner, deadline);
uint256 cashOutToken1 = cashOutAmounts[cashOutPath.length - 1];

// 如果 checkProfit 为 false,则不检查收益,只执行上述交易
// 否则如果收益不符合设定就回滚交易
if (checkProfit) {
// 如果收益是负数,这里会直接回滚交易
// fixCost 是用来表示手续费的
uint256 profit = cashOutToken1.sub(cost).sub(fixCost);
if (profit < minProfit) {
revert('No Profit');
}
}
}

完整代码可以看这里

写完合约,部署之,再向里面打入刷量用的 USDT,然后不断使用脚本进行调用,印钞机岂不是就造好啦!

走一个?

我先后尝试使用不同资金量进行刷量(checkProfit 设置为 false):

10 刀,收益 -0.0028 刀,收益率 -0.028%(考虑 gas,下同):

300 刀,收益 0.01 刀,收益率 0.0035%:

3000 刀,收益 -0.03 刀,收益率 -0.001%:

资金量的增加意味着刷更多的量,但也意味着兑换手续费的增长,也意味着更高的滑点,上面的简单测试没有看出资金量增加有明显的好处。

此外我们也可以看出,现在刷量利润是多么的微薄。按照 HT现价计算,每次回滚交易也要支付 0.003 刀的手续费,假设每次交易成功能获得 0.01 刀利润(300 刀的例子),则我们四次交易必须有一次要成功才能收支平衡。

当然,我们也可以使用 eth_call 来模拟执行交易,如果模拟交易失败,则不发送交易到链上,以求尽量把 gas 费也省下来。但是总体而言,在 MDX 现在的价格水平(3.59 刀)下,利润水平是非常非常低的。

总结一下

虽然我们现在有了一台印钞机,但是它的效率实在是太太太低啦~怀揣印钞梦想的各位,散了吧(手动狗头)

成功把神鱼的一条微博水出了几百字