关于MEV
MEV是什么
MEV全称 Miner Extractable Value(或称Maximal Extractable Value),从宏观上来说MEV指的是矿工能获得的收益,狭义上指的是通过调整交易的打包顺序能获得的收益。目前来看,MEV主要体现在与一些DeFi协议交互的交易上,如Uniswap等。这些DeFi协议的交互交易对于矿工打包交易的顺序比较敏感,不同的打包顺序会导致不同的交易结果,所以就会造成MEV的出现。
MEV常见类型
区块打包与抢跑
以太坊的交易打包规则比较简单,与bitcoin类似,用户发出的交易会被丢到交易池中等待矿工打包。每个区块容纳的交易总数是根据总的gas来计算,即一个区块所包含的交易gas总和不能超过800M。矿工打包交易时,会优先选择gas较高的交易进行打包,gas较低的交易则可能需要等待很久才会被矿工确认。
由于交易池中的数据都是透明的,且没有访问的控制,所以交易池中的每一笔交易都可以进行重放,并且矿工对于交易打包的优先级是基于gas的,那么对于两笔相同的交易,gas出价高的交易自然就会被提前打包确认。这样的通过重放交易池中交易,并且通过设置gas的方式改变交易打包顺序,即交易执行先后顺序的操作,就称之为抢跑(front-running)。抢跑这一基本的操作是绝大多数MEV的基础。
那么抢跑一个相同的交易有什么用呢?可以参照:https://www.youtube.com/watch?v=UZ-NNd6yjFM 这一视频。
这一视频中,作者部署了一个存储hash字符串的合约,并且提前向合约中存储了一定的eth。只有正确输入hash原相的用户才能将其中的eth取出。
contract FrontrunMe{
bytes32 public secretHash;
constructor(bytes32 _secretHash) public payable{
secretHash = _secretHash;
}
function take(string calldata _secret) external{
if(keccak256(abi.encodePacked(_secret)) == secretHash){
msg.sender.transfer(address(this).balance);
}
}
}
按照常理说,这一合约的secretHash原相只有部署者一个人知道,所以其他人并不能通过take函数将存款取出。但是当视频作者将这一合约部署在eth主网上,而后调用take时,这一交易却被抢跑了,因为这一验证手段并不能阻拦抢跑攻击。
后跑(back-running)
与抢跑对应,后跑攻击就是指的将攻击交易放置在被攻击交易后进行从而获利的交易。
一个典型的例子就是攻击者监控uniswapFactory的新swapPair的创建,当新的swapPair创建后,创建者都会将初始的流动性注入交易对,攻击者就针对于这一初始化流动性交易,在这一交易后尽可能多的购买其中的token(一般是用weth、usdt等basetoken购买新的token),而后等待其他用户购买这一token,当这一代币币价上涨后再进行卖出。(似乎可以反撸一波后跑机器人)
另一个例子类似三明治攻击的后半段,即通过后跑一些大额交易,利用大额交易造成的兑换比率波动卖出某一币种进而获利。
清算
在抵押借贷类的DeFi协议,如AAVE、Compound中,借贷者的抵押资产总价值和借贷资产总价值的比值如果低于某个阈值,那么这一借贷者的抵押资产就会被清算,以一个低价进行卖出。
通过上面的逻辑不难得出,清算条件满足时一定是币价波动的时期,而这又被具体的体现为,预言机的币价更新事件的发生。
有两种策略进行清算:
- 抢跑:在前一个区块检测到了清算的机会,在下一个区块与其他的清算者进行抢跑。
- 后跑:在当前区块检测到了清算的机会,通过将清算交易放置在预言机更新交易后来进行清算。
由于上述的攻击思路是建立在协议将第一个调用清算函数的发送方作为清算者,所以防御策略是比较简单的:将一个时间段内的清算者进行投标竞选,出价最高者才能进行清算,即可有效的防御这一MEV攻击。
三明治攻击
三明治攻击从形式上看是攻击者设置两个交易一前一后将被攻击者的交易夹在中间,形式上很像三明治以此得名。
三明治攻击主要发生在AMM类的DeFi协议中,主要针对数额较大的交易进行。
以Uniswap类的AMM为例,当发生了较大金额的swap交易,那么可能就会出现较大的滑点(滑点这一概念可以理解为暂时的币价波动)。如原本的swapPair中X存量为100,Y为10000,正常兑换的比例为1:100,如果一个用户想用50个X兑换Y,那么兑换得出的Y约为3333.33个,此时二者的兑换比率变为了150:3333.33,约为22.22,Y的币价因此上涨。
三明治攻击的理念很简单,如果检测到类似的大额度交易的出现,以上述例子作为阐述对象,那么这样的交易结果就是导致Y的价格上涨,所以攻击者可以通过在这笔交易发生前以低价用X购买入一定数量的Y,然后在大额交易发生后再将Y以高价卖出换回X,以此获利。
有点类似于传统金融市场中通过信息差提前预支了某支股票的涨跌提前做出行动,只不过在DeFi中这一预测的成本和门槛较低,直接在mempool中即可查看。
套利(arbitrage)
套利这一概念比较宽泛,在MEV中套利的概念只是指狭义上的AMM上的套利方式,指的是在同一时间(同一笔交易中)在不同的AMM上购买和出售相同的资产而获利。
众所周知,在一个公链环境中有不同的AMM进行着代币的交换工作,不同的AMM之间的兑换比率不尽相同,比如在Uniswap上,可能X:Y为1:100,但是在sushiswap上可能这一比率为1:80,那么不同的兑换比率就带来了套利的可能。套利者可以在汇率低的交易池中以Y购买X,然后在另一个汇率高的交易池中卖出X换回Y,以此进行获利。
当然,实际上的套利情况可能比上述例子复杂得多,可能存在A->A/B->B/C->C/A这样的情况,不过最终实现形式都是左脚踩右脚获利。
由于套利机器人的存在,各大AMM的基本汇率都会被套利机器人维护在一个相近的水平,也算是一种对DeFi生态的贡献。
Time Bandit Attack
这一攻击类型是矿工实施的,具体做法为将之前已经打包好的区块进行重打包以获得MEV收益。
当矿工发现打包一个区块的收益小于重新打包钱几个区块的收益时,矿工可能就会考虑重写历史区块(当然区块数不会很多)来获得MEV收益,具体获得收益的方式可能是之前提到的MEV攻击方式中的一个或者多个。
在mev-wiki中提出的例子如下:
存在两个矿工:矿工A和矿工B,矿工打包每个区块的收益为100,矿工A打包了3个区块,这三个区块中包含的MEV收益为10000。那么现在矿工B就有两个选择:
- 在矿工A打包的三个区块之上继续进行挖矿。
- 重新打包矿工A挖出的三个区块,获得其中的MEV。
由于10000的mev比起100的奖励要多很多,如果矿工B重写这三个区块并且成为了最长的链,那么矿工B就获得了这些MEV。
MEV检测
目前开源可供参考的MEV检测项目只有flahbots的MEV-inspector,所以本节中仅仅针对于MEV-inspector的检测进行讨论。
代码 Repo:https://github.com/flashbots/mev-inspect-py
功能概述
主要用于检测一个区块内或者多个区块内的 MEV 事件,具体MEV类型包括 AMM 的套利、清算、三明治攻击、pund bids、nft交易等。
MEV-inspector 依赖支持trace的 RPC 节点,每次检查一个区块的内容时,都会将区块中所有的交易 trace 进行分类。其分类的标准以及原理是基于一些已知的知名的项目的函数签名,在trace中如果发现相关的函数调用,则将这个 trace 打上标识,作为初步分类。
将区块内所有交易 trace 都分类完成后会根据每个交易的所属类别进行进一步的分析。
交易分类实现
MEV-inspector 所做的工作也是围绕现有的知名 DeFi 项目的函数调用关系来实现,所涉及到的知名 DeFi项目有:AAVE、Balancer、Bancor、Compound、Cream、CryptoPunks、Curve、Opensea、Uniswap、0x、WETH 以及 ERC20 通用接口。
针对于涉及调用这些 DeFi 项目的函数,具体的某些函数签名会对应一些与 MEV 相关的类别。具体类别有:transfer、swap、liquidation、seize、NFTtrade。
以 AAVE 为例,如果与 AAVELendingPool.liquidationCall 函数发生交互,那么这个交易 trace 就被标记为 liquidation,如果与 aTokens.transferOnLiquidation 函数发生交互,则被标记为 transfer。同样的,如果调用了 uniswap.swap 函数则被标记为 swap。
MEV 细分类实现
当一个区块内的所有交易 trace 都进行了初步的分类后,会针对不同的类型进行进一步的分析。
由于各个协议的实现不尽相同,所以在每个协议的分类器中都分别实现处理各个类别的实现方式。
transfer
实现比较简单,将转帐交易分成了普通 ETH 交易以及 ERC20 交易,如果是简单的 ETH 交易,则直接打包交易数据。如果是 ERC20 交易,则需要调用具体协议对应的分类器中 get_transfer 实现生成打包数据。
swap
swap 分类下的交易数据比较关键,因为 swap 分类下的数据会在之后的套利以及三明治攻击分析中使用。
对于分类后的交易序列,提取出 swap 类别的交易 trace 步骤如下:
- 对初步分类的交易 trace 按照 trace 地址字节序排序。(这里的trace应该是像 tenderly 那样,子 trace 是在父 trace 地址后追加)
- 记录排序后的 trace 中,发生 swap 前的 transfer 以及发生 swap 中的子 transfer。将这三者作为参数调用分类器 parse_swap 。
- 针对不同的协议,parse_swap 实现不尽相同,以 uniswap 为例,其主要思路为从发生 swap 之前和子序列中找到向交易池中转账的操作集合以及交易池向用户转账的交易集合。将这两个集合中间隔最近的两次交易作为 swap 的输入与输出交易,记录其金额
得到的 swap 序列会被存储,作为后续 MEV 事件的分析数据基础。
arbitrage
套利被定义为通过多次swap将原始token换回为原始token,并且盈利的过程。最常见的套利被定义为两种情况,分别为:
- BOT -> A/B -> B/C -> C/A -> BOT
- BOT -> A/B -> BOT -> B/C -> BOT -> C/A -> BOT
由于套利行为都是在一个交易序列内完成,所以首先会对 swap 分类中的 trace 根据交易 hash 进行分组,发生在同一个交易中的多个swap才被定义为套利。
对于每个交易hash中的每一个swap,首先需要找到满足下列条件的套利交易的开始和结束交易:
- swap[start].token_in == swap[end].token_out
- swap[start].from_address == swap[end].to_address
- not swap[start].from_address in all_pool_addresses
- not swap[end].to_address in all_pool_addresses
由于第二种套利情况比较复杂,所以需要递归的查找多个符合条件的路由情况。
liquidation
清算行为的判别首先遍历整个被初步分类的 trace 列表,判断标记为 liquidate 的 trace 是有派生关系,子清算 trace 不会被记录。最终的清算数据解析由特定协议的实现。
sandwiches
首先会将 swap 中的 trace 根据交易发生的时间和 trace_address 进行排序。遍历排序后的 swap,当前swap称之为front_swap,对于每一个在其之后的swap,如果二者发生在同一个交易池中,那么这两个交易才会有三明治攻击的嫌疑。
if other_swap.contract_address == front_swap.contract_address
如果front_swap 和之后的other_swap 仅在参数上的from_address 不同,则这一个other_swap 就有可能是被三明治攻击的对象,将其加入被攻击对象列表中。
if other_swap.contract_address == front_swap.contract_address:
if (
other_swap.token_in_address == front_swap.token_in_address
and other_swap.token_out_address == front_swap.token_out_address
and other_swap.from_address != sandwicher_address
):
sandwiched_swaps.append(other_swap)
如果 front_swap 和 other_swap 的swap逻辑正好相反,则中间记录发生的 swap 就是被三明治攻击的对象。
elif (
other_swap.token_out_address == front_swap.token_in_address
and other_swap.token_in_address == front_swap.token_out_address
and other_swap.from_address == sandwicher_address
):
if len(sandwiched_swaps) > 0:
return Sandwich(
block_number=front_swap.block_number,
sandwicher_address=sandwicher_address,
frontrun_swap=front_swap,
backrun_swap=other_swap,
sandwiched_swaps=sandwiched_swaps,
profit_token_address=front_swap.token_in_address,
profit_amount=other_swap.token_out_amount
- front_swap.token_in_amount,
)
Punk bids
主要是针对于 CryptoPunks 的 NFT 竞价交易相关事件的监控。
首先会将所有 trace 中的竞标信息记录,而后统计每个标的的最终成交价,在每个竞标期间中寻找出价最高者,若最高价高于成交价,则将这个中标者标记为snipe。
Nft trade
和 transfer 差不多,主要依赖于不同的协议实现交易的数据提取。
MEV 防御以及缓解措施
通过之前几个小节的阐述,不难发现除了Time Bandit Attack 外其他MEV都是围绕着抢跑和后跑来进行收益。而抢跑和后跑这两种手段都建立在mempool中交易数据和gas price的透明性上。所以针对于MEV,主要的缓解措施基本都是建立在改善这两点的基础上的。
私有交易
正常的EVM网络中,所有交易都会被广播到所有节点并且加入至mempool,而私有的交易则不会广播给所有的节点,仅会单向的进入私有交易池中。典型的例子是 taichi network,在这一网络中允许用户将交易发送至Sparkpool这一交易池而不是公共的mempool中(不过目前sparkpool已经跑路)。bIoXroute提供相似的功能。
FlashBots
与私有交易不同,flashbots为矿工提供一个节点,这一节点不仅仅监控公共的mempool,同时连接了flashbots管理的一个中继网络,这一网络将矿工和交易发送方直接相连。包含在flashbots协议中的交易与正常的交易不同,交易本身的数据和签名不变,但是其gasprice为0,取而代之的是直接给予矿工的小费。所有的交易信息以及小费都是私有的不能被查看的,一方面保证了交易数据上链前的隐秘性,另一方面,小费的不透明也一定程度上避免了之前在公共交易池中gas竞价,可以有效的防止抢跑行为。
MEVA
全称 MEV Auction,思路是将传统矿工的职能拆分为两个:打包和排序。传统的矿工节点可以决定哪些交易被打包进区块中并可以决定其打包顺序,MEVA的思路是将这两个职能交给不同的角色来做。
负责交易打包的为Block Producer,他和正常的矿工节点基本一样,从交易池中利用贪心算法决定哪些交易被包含进区块中。
当Block Producer确定了打包进一个区块的交易后,此时就需要进行MEVA竞拍,出价最高者就称之为 Sequencer,Sequencer可以决定交易的打包顺序,而竞拍所得的资金可以用做捐献给社区或者其他的用途。
MEVA并没有根本上的解决MEV,其立场是认为多数的MEV都是对于公链环境影响不大的,所以其主要思路就是限制套利机器人的能力,并且从中收税。
典型的实现为Optimism。