# 清算机器人，从0到1跑通全流程

By [想住大房子的java程序员](https://paragraph.com/@java-3) · 2022-11-14

---

**本文翻译自_Robert Miller_的**[**Anatomy of an MEV Strategy: Synthetix**](https://www.bertcmiller.com/2021/09/05/mev-synthetix.html)

几个月前，臭名昭著的阿尔法泄密者KALEB在Flashbots公共搜索者不和谐中，发布了以下消息：

![](https://storage.googleapis.com/papyrus_images/50f4906dad88cde49a6531e581131e8640628349b89d7adebf3e25d8453a4406.png)

KALEB此前曾泄露了数十万美元的Alpha版本，用于对Synthetix的更改。

在这个机器人操作者的巢穴里分享阿尔法就像向狮子扔红肉一样，快速浏览一下合同，就可以确认这是一笔令人眼花缭乱的资金。

在接下来的几周里，我计划并试图执行一项策略来捕获上面分享的MEV KALEB。

### 第一步，识别机会

我不是一个Synthetix专家，因此第一步是去学习我将要涉及的操作。具体如下：

*   我找出了相关合约
    
*   我在Synthetix博客里读了它们的高级别功能并且搜索了相关文档
    
*   我确保理解了将要实行的治理变动范围
    
*   我查找了相关函数
    

总结一下这阶段的工作，Synthetix已经试验了以ETH为抵押去铸造(mint)sUSD和sETH。你可以在合约中存入ETH并铸造那些资产，只要你留意你的抵押资产不要下跌到低于你铸造的资产的一定水平。(可能亏钱)

然后，在一年后，协议投票通过结束了此试验。当还有数百万差不多未偿还时他们要怎么处理呢？很好，你将可以清算任何头寸。

在一段很长的警告期后，贷款将在一个区块内从安全变为可让任何人清算，不管任何金额的抵押物。这将在公共的mempool里被由pDAO地址发出的一笔交易而触发。

为了清算贷款，我需要偿还我已借的未还金额(sUSD或者sETH)。做为回报，我将收到我关闭的贷款的抵押物ETH。做为清算激励，我将得到比我偿还的sETH或者sUSD更高价值的抵押物。当前还有数百分美金仍然处于贷款状态，这意味着有一大笔钱可以让清算者来赚。因此，我将会尾追pDAO的交易，尽最大可能性让我从这个机会中获利。

### 第 2 步：了解机会

现在，我知道了基础的机制，并且找到了一些相关的函数。我继续深入研究哪些函数是我要调用到的，哪些数据是我需要的，以及如何生成这些数据。

以下是两个要在生产环境调用的函数：

*   setLoanLiquidationOpen() 只能被合约所有者(pDAO)调用。允许清算未关闭的贷款。
    
*   liquidateUnclosedLoan() 传入贷款ID和帐户地址并清算该贷款。在setLoanLiquidationOpen()被调用之后，任何人都可以调用此方法。
    

请注意，还有其他的，但我很快发现它们并不相关。现在我需要弄清楚我将如何选择要清算的贷款以及我需要多少sETH/sUSD。下列函数是我可以开始调用的：

*   openLoanIDsByAccount():返回某地址相关的未关闭贷款ID
    
*   getLoanInformation():传入ID和地址返回贷款数据
    

然而，有两个问题。让我感到困惑，这些合同不会告诉你哪些地址有未偿还的贷款。我在几分钟内找到了解决方法，通过在Etherscan上下载与合约相关的所有交易，并使用Excel创建与它们交互的唯一帐户列表。

*   用getLoans去找到此地址是否有未还贷款，如果有就记录下贷款ID
    
*   用getLoanInformation去获取抵押物数量，铸造了多少uUSD/sETH，累积多少利息
    
*   按贷款金额从大到小排序并保存到json文件里
    

第二个难点是并不能马上清楚的知道在清算某个贷款后我可以获得多少抵押物。因为有一个未还贷款的函数可以粗略的估算，但我需要更精确一些。因此，我研究了清算的代码并了解了相关数字是如何生成的。

以上是我如何获取链上数据并计算的过程。但是这么做很消耗gas。考虑到我将要与其它机器人在合约gas交易上竞争，实行链下计算，最小化gas消耗对我来说很重要。

经过几次迭代之后，我了解了需要从链上获取的最小数量的数据，是一些关于贷款的变量，我可以在链下处理完成后传入我的合约。然而，这些获取信息的函数太复杂了，查询所有的贷款信息会耗费超过一个区块的时间。这不能够接受。为解决这些问题，我写了一个简短的合约去批量获取数据，这提高了超过10倍的效率。核心代码：

    function batchGetLoanInformation(address[] calldata _addresses, uint256[] calldata _loanIDs, address _contractAddress) external view returns ( 
            uint256[] memory, 
            uint256[] memory){
    
            uint256[] memory totalRepayment = new uint256Unsupported embed;
            uint256[] memory totalCollateralLiquidated = new uint256Unsupported embed;
    
            for (uint i = 0; i < _addresses.length; i++){
                uint loanAmount;
                uint accruedInterest;
    
                (,, 
                loanAmount,,,, 
                accruedInterest,
                ) = collateralContract(_contractAddress).getLoan(_addresses[i], _loanIDs[i]);
                
                totalRepayment[i] = loanAmount + accruedInterest;
    
                totalCollateralLiquidated[i] = getCollateralAmountSUSD(sUSD, totalRepayment[i], COLLATERAL);
            }
            return (totalRepayment, totalCollateralLiquidated);
        }
    

现在我有一个快速的方式告诉你我需要多少sETH/sUSD，我可以获得多少抵押物，有多少利润。

总结：这阶段的工作是深入理解机会并用高效的方式收集需要的数据。你需要尽量在链下操作去减小gas消耗。成果是[两个](https://github.com/bertmiller/sMEV/blob/1e37690638bf42e849b87a25b468eae20e65545d/execute/monitor-sUSD.js)快速的[脚本](https://github.com/bertmiller/sMEV/blob/1e37690638bf42e849b87a25b468eae20e65545d/execute/monitor-sETH.js)去获取我需要的数据。

### 步骤 3：创建执行合同

你可能需要一个专门的[合约](https://github.com/bertmiller/sMEV/tree/main/hardhat/contracts)来提取MEV。我写了一个合约并在[测试环境](https://github.com/bertmiller/sMEV/tree/main/hardhat)中测试以更好的理解合约并确保的我数据是正确的。这跟第2步和第4步是同步进行的。

闪电贷可以解决我数百万美金的sUSD/uETH资金需求。经过一番思考，我意识到无论如何我都需要把ETH换成其它资产，但我要选择在清算前还是清算后去做这个动作。

*   选择1：Flashloan ETH -> swap for USDC -> swap for sUSD -> liquidate sUSD loan -> receive ETH -> pay back ETH flashloan
    
*   选择2：Flashloan sUSD -> liquidate sUSD loan -> receive ETH -> swap for USDC -> swap for sUSD -> pay back sUSD flashloan
    

借出sUSD只有Aave支持，而ETH可以在很多闪电贷提供商那里借出，最终就是选择哪家闪电贷提供商的问题。最终，我选择了方式1，因为dYdX没有手续费，而Aave有手续费。

dYdX会加2wei的费用，相当于免费；Aave闪电贷需要0.09%的手续费（franx.eth的解释）

### Gas优化

你可以在这里找到[完整的合约](https://github.com/bertmiller/sMEV/blob/main/contracts/dYdXLiquidator.sol)，下面是从dYdX借到WETH后处理清算sUSD贷款的一段代码：

    // This is the function called by dydx after giving us the loan
        function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external {
            // Use chi tokens 
            uint256 gasStart = gasleft();
    
            // Let the executor or the dYdX contract call this function
            // probably fine to restrict to dYdX
            require(msg.sender == executor || msg.sender == address(soloMargin));
            
            // Decode the passed variables from the data object
            (
                address[] memory sUSDAddresses,
                uint256[] memory sUSDLoanIDs,
                uint256 wethEstimate,
                uint256 usdcEstimate,
                uint256 ethToCoinbase
            ) 
                = abi.decode(data, 
            (
                address[],
                uint256[],
                uint256,
                uint256,
                uint256
            ));
    
            // Swap WETH for USDC on uniswap v3
            uniswapRouter.exactOutputSingle(
                ISwapRouter.ExactOutputSingleParams(
                    address(WETH),        // address tokenIn;
                    usdcTokenAddress,     // address tokenOut;
                    3000,                 // uint24 fee;
                    address(this),        // address recipient;
                    10**18,               // uint256 deadline;
                    usdcEstimate,         // uint256 amountOut;
                    wethEstimate,         // uint256 amountInMaximum;
                    0                     // uint160 sqrtPriceLimitX96;
                )
            );
    
            // Swap USDC for sUSD on Curve
            curvePoolSUSD.exchange_underlying(
                1, // usdc
                3, // sUSD
                usdcEstimate, // usdc input
                1); // min sUSD, generally not advisible to make a trade with a min amount out of 1, but its fine here I think because the overall risk of getting rekt is low
            
            // Liquidate the loans
            for (uint256 i = 0; i < sUSDAddresses.length; i++) {
                sUSDLoansAddress.liquidateUnclosedLoan(sUSDAddresses[i], sUSDLoanIDs[i]);
            }
    
            // We got back ETH but must pay dYdX in WETH, so deposit our whole balance sans what is paid to miners
            WETH.deposit{value: address(this).balance - ethToCoinbase}();
    
            // Pay the miner
            block.coinbase.transfer(ethToCoinbase);
    
            // Use for chi tokens
            uint256 gasSpent = 21000 + gasStart - gasleft() + (16 * msg.data.length);
            CHI.freeFromUpTo(owner, (gasSpent + 14154) / 41947);
        }
    

[https://raw.githubusercontent.com/bertmiller/sMEV/main/contracts/dYdXLiquidator.sol](https://raw.githubusercontent.com/bertmiller/sMEV/main/contracts/dYdXLiquidator.sol)

我花了很多时间去最小化gas消耗。下面列出我的一些设计准则：

*   替代发出多个单独的清算和交换交易，我选择将多个清算合并到一个交易，这样可以在清算中摊销固定gas费用，提高bundle的竞争力。
    
*   我需要寻找最佳的路径把ETH换成USDC再换成sUSD，需要决定在Uniswap v3里用exactInput还是exactOutput函数。不管如何，我都会遭遇一些滑点，所以我选择用**exactOutput去避免调用balanceOf**。
    
*   在交易精确性和gas交易上存在一些权衡。只要能成功偿还贷款，在精度上都会有些小缺点，因为我要在gas交易上进行竞争，我选择了它。
    

需要注意的更多_战术_事项：

*   [在合约构造函数里前置授权所有](https://github.com/bertmiller/sMEV/blob/main/contracts/dYdXLiquidator.sol#L265-L268)。这种方式，将在部署合约的时候支付gas费用，并降低在执行过程中的gas费。
    
*   在合约里燃烧gas token，而不是在我的地址里燃烧。
    
    > 作者用了chi gastoken做gas的回补，可以减小gas的消耗，又因为gas fee是一次性通过coinbase.transfer支付的，因此，gas总量越低，gas price就越高，对于miner来说越有吸引力。
    
*   函数命名让[selectors](https://solidity-by-example.org/function-selector/)以0s前导，这也可以降低一点gas消耗
    
    > 函数flashloan\_4247(uint \_loanAmount, bytes memory \_params, uint8 \_trigger)的selector是0x0000eaff。
    
*   直接使用require比用modifier更省gas
    
*   还有一些可以被优化的点，比例：用gas fee代替coinbase.transfer
    
*   0xSisyphus慷慨的要提供ETH贷款给我让我代替闪电贷，可以节省一笔gas支出。然后在一些大笔贷款被偿还了之后，总体机会在减小。我决定不从Sisyphus那里借款，因为机会不再足够大到值得去这么做。
    

总结：在这个阶段，我创建了智能合约去执行可用的MEV机会。要做到最佳需要努力思考正确的关于如何最小化gas消耗的策略。这个合约是迭代开发的，与数据工作同步进行，并且在测试环境中测试。

### 第四步，计划执行

### 清算MEV和优化gas费用的经济学

有了精心编写的合约和对机会的深入理解，我需要去优化如何执行的策略。回顾了Flashbots MEV-Geth客户端高效执行拍卖时根据[高gas price bundle赢](https://docs.flashbots.net/flashbots-auction/searchers/advanced/bundle-pricing)并上链的方式。最重要的因素是我需要最大化bundle的gas price，而不是总的支付了多少ETH。

考虑到这一点，我将之前收集到的数据整理到表格里去优化我的gas price。我的合约有固定的gas支出和可变的gas支出。固定的gas支出是闪电贷和交换。可变的gas支出是我想要清算多少个贷款。很直观的可以想到将有一些点到达清算的边际收益而不再值得去消耗gas。我运行了几次测试去获取实际的数字。

下面是我的结果：

![](https://storage.googleapis.com/papyrus_images/05508a319d03d911824e281ab733ac9a61dd99e3ea49785cbb5122c862ab3c6e.png)

结果有些惊喜，只清算30个sUSD贷款中的前4个是gas最高效的。在这之后的每个贷款都将增加总收益，但会降低bundle的gas price，让其减少竞争力。如果其它人试图一次性清算前10个sUSD贷款，那么他们将降低至少30%的gas效用！

考虑到未偿还的sETH贷款比较少，在一个交易里只清算sUSD而不是将sUSD和sETH合并清算会更有意义。因此潜在的奖励会比较少，也就支付更少的费用给矿工，意味着缺少gas竞争力。想到这我不禁笑起来。如果有人贪婪的一次性清算所有贷款，或者懒惰的一个一个头寸单独清算，那么我将胜出。

然而，其它贷款还在那里且有利润可供清算！再次，我试图去优化gas price，发现如果我清算前4个sUSD贷款再接着清算6个最大的sUSD贷款再接着2个sETH贷款，这样是gas最高效的。进而，假设我赢了，我还可以用前面bundle收益的ETH来代替闪电贷。

### Flashbots拍卖和我的bundle排序策略

重复一遍：我在GAS效率方面竞争，但我也想通过清算每笔贷款来最大化我的利润。最佳战略是每一bundle提交一些清算，分别bundle。然后，这些将由闪电机器人拍卖独立进行评估。然后，这些将由Flashbots拍卖独立评估。但是，_每个_bundle_都依赖于 pDAO 的交易，使任何人都可以清算贷款。_

如果pDAO交易没有在bundle里，那bundle将会失败。但是如果我包括的pDAO交易在每个bundle里，就只会有一个bundle成功。在一个bundle打包后其它bundle将不再有效，因此它们尝试去二次打包pDAO交易。因此，我需要一些方式只在第一个bundle里发送pDAO交易，但是确保其它bundle不会因为没有包括pDAO交易而失败和被抛弃。

解决方案是Flashbots拍卖的一个细微差别。在搜索者开始”[_在bundle合并后降低矿工费用来戏弄拍卖_](https://twitter.com/bertcmiller/status/1407305924600029189)_”_，Flashbots设置了两轮的模拟。

首先，所有bundle单独模拟以获取他们的gas price并检查失败。再来，[成功的bundle按gas price从大到小排序，再次模拟去找到冲突的bundle并确保没有bundle的gas price是低于期望的](https://github.com/flashbots/mev-geth/blob/master/miner/worker.go#L1292-L1301)。除非你试图去做些工作，否则你的bundle的gas price将不会在合并后降低。

意识到我可以做一些与上述搜索者相反的工作，来让我的bundle不会支付低于预期的矿工费，而是在第二轮的模拟中超付费。为了实现这个方案，我将要在第一个bundle里提交带pDAO交易，但是要额外检测剩余的bundle。这些bundle将会推测出他们在第几轮，并改变相关的执行。如果他们在第一轮他们将不会清算，因为如果他们想要清算将会失败，并不顾一切支付给矿工高额的gas price以通过第一轮的模拟。

我是如何检测我的bundle处于第几轮的呢？用检查合约余额的方式。如果在前面的bundle成功清算了，我的余额会因为利润而增加。因此，我增加了一个[是否获得WETH利润的条件](https://github.com/bertmiller/sMEV/blob/main/contracts/dYdXLiquidator.sol#L128-L133)检查，再处理清算。这在测试中通过了。

也就是说假设，一次提交三个bundle，第一个bundle带有pDAO交易，第二三个没有pDAO交易。在Flashbots在第一轮模拟时，正常情况下第二三个bundle会因为没有带pDAO而执行失败，然后会被抛弃，从而不会被打包入块。作者在合约里做了一个条件判断，用合约的WETH余额来判断是第几轮，因为第一轮是单独模拟，所以WETH余额肯定是0，还有另外一个trigger参数是用来区分是不是第一个bundle的，这样能判断出是第二三个bundle且是第一轮模拟，然后就强制用coinbase.transfer转gas给矿工，让交易不会失败且有gas费，让矿工愿意打包这个bundle。（译文作者注释）

总结：这个阶段还是关于策略。我用之前获取的数据、合约和测试环境来思考我要争夺的 MEV 机会的经济学逻辑，以及最优策略会是什么。用真实数据，我发现了一个令人惊讶的显性因子，但它很难执行。执行它需要一种新方式来提交bundle。

第 5 步：执行
--------

有了数据，合约，计划，我就可以开始执行了。本质上，我需要编写能够执行我上述计划的bundle并且监控mempool里相关的Synthetix交易去尾追。这些大部分都是实现上的问题。 首先，我用[Blocknative去监控pDAO帐户的相关交易](https://github.com/bertmiller/sMEV/blob/main/execute/builder.js#L40-L66)。我把pDAO相关的所有交易都流向我的机器人。

同时，我运行了两个监控脚本([sETH](https://github.com/bertmiller/sMEV/blob/main/execute/monitor-sETH.js)&[sUSD](https://github.com/bertmiller/sMEV/blob/main/execute/monitor-sUSD.js))去获取链上的数据，获取最佳的bundle策略(例如：闪电贷x个ETH去清算前3个sETH贷款，再以同样的做法清算下面两个，以此类推）并生成我的合约需要的数据。我需要每个区块都运行，以防价格发生变化或者某人关闭了贷款，来改变我的最佳策略。把[结果](https://github.com/bertmiller/sMEV/blob/main/data/sUSD-optimalConfig.json)保存在本地。

**最后，我有一个**[**执行脚本**](https://github.com/bertmiller/sMEV/blob/main/execute/executor.js#L15https://github.com/bertmiller/sMEV/blob/main/execute/executor.js#L15)**将会收到pending交易并流向我的机器人，加载最佳策略，编写bundle并发送到Flashbots。** 剩下的就是等待了。在这段时间内，最大的sETH贷款被借款人偿还，因此我关闭了部分机器人。一些最大的sUSD贷款也被关闭了，这显著的降低了潜在的钱款。

### 第 6 步：关键时刻到来

有趣的是有人试图发送相关合约的交易去诱饵机器人让它在早期失灵。我不确定这是否对别的机器人生效，但我的机器人没有上钩。 几个小时过后，真正的交易从pDAO发出了。经过几周的研究和准备，关键时刻来临了。一切都进展顺利在我这边，我的监控脚本运行完美，交易被成功捕获到，bundle成功创建并提交。

...然后，灾难发生了。连续几个区块都没有Flashbots块被挖出。我不止因此失去了机会，而且也没有Flashbots搜索者成功。没有Flashbots bundle在区块头部去阻止[企业级](https://etherscan.io/tx/0x1e68bd612fd47b68ee01abb500e45f5dbf640e7efa9b92a1ee925c3a658d3341)的[mempool](https://etherscan.io/tx/0x8296b0a8cae7e42f7989438c2ead9ea94d794930f9c456f5feff63758abe4b44)[机器人](https://etherscan.io/tx/0xaca5780d61aba2f58ce783f97511083a1ade323f8fe3f868518d4cbf85570ceb)进入并狙击到所有的有利润贷款。

尽管我输了，但我想我的方法是正确的。我的优势在于策略和找寻新机会，而不是参与PGAs（Priority Gas Auction最优gas费竞拍, 简称PGA）。因此使用Flashbots给了我获胜的最好机会，鉴于Flashbots的广泛使用，连续几个区块没有Flashbots块产出是有点不可因议的不走运了。

MEV有时候被看做是隐藏的超级科学家领域，但这并不一定。它很好玩，有趣并可模拟。然后游戏规则-可以说-如果你想寻找就是开放的。这篇文章是关于我学习游戏规则，开发这些规则对应策略并最终执行策略的过程。尽管我输了，但我学到了很多，并且享受这个过程。我希望我下次再来做，你也可以在下回加入我。 gg，mempool机器人，你赢了这次。但下回我会赢。

---

*Originally published on [想住大房子的java程序员](https://paragraph.com/@java-3/0-1)*
