真实存在的以太坊 DeFi「黑暗森林」:mempool 套利机器人吞噬了我的交易

转载
1540 天前
10222

来源:链闻ChainNews    作者:Dan Robinson  Georgios Konstantopoulos


这是一个恐怖故事。

挑战

像普通人一样,我在 Uniswap Discord 的#support 频道中花费了大量时间。 (信息披露:Uniswap 是 Paradigm 参与投资的公司之一。)

周三下午,有人提问:如果把代币误发送给对智能合约本身的代币交易对,是否有可能把代币拿回来?

我最初的想法是这些代币将永远被锁定。但是那天深夜,我突然意识到,如果代币仍然在那儿,它们可能会被拿走——被任何人拿走。

当任何人在 Uniswap 核心合同上调用焚毁 burn 功能时,该合同将衡量自身的流动性代币余额并进行 burn ,将提取的代币返给调用方指定的地址。这是 Uniswap v2 预期行为的核心部分(基本机制在 Uniswap v2 白皮书的第 3 章 第 2 节中进行了描述)。

我找到了该合同。流动性代币仍然存在,价值约 12,000 美元。

这意味着三件事:

  • 存在一个滴答作响的时钟。即使没有其他人注意到这笔免费的资金,任何人都可以随时撤走自己的流动资金,意外地从合同中收到代币。
  • 我可以扮演白帽黑客,尝试为将代币误发送到 Uniswap 合约的人找回代币。非常简单,只需要在资金池中调用 burn 函数,并将代币传递到自己的地址。
  • 除非……我知道它不是那么简单。

黑暗森林

众所周知以太坊区块链是一个危机四伏的环境。如果能通过抓住某一智能合约的漏洞牟利,总会有人这么干的。新黑客攻击的频率表明,一些非常聪明的人花费大量时间来寻找有漏洞的合约。

但是与内存池 mempool (待处理、未确认的交易集)相比,这种暗藏杀机的环境不值一提。如果这条区块链本身是战场,那么 mempool 更糟糕:那就是一片黑暗森林。


《黑暗森林》The Dark Forest 是我个人最喜欢的科幻小说。正是因为这本书而有了「黑暗森林」这一概念——在这环境中高级掠食者不断制造杀戮。在这种环境中,暴露某人的藏身之处无异于直接毁掉他 / 她。(这一概念也是以太坊测试网中黑暗森林 Dark Forest 游戏的灵感来源。)

在以太坊 mempool 中,这些超级掠食者是以「套利机器人」形态存在。套利机器人监视着待处理交易,试图从这些交易造成的每个牟利机会中敲骨吸髓。最深谙这些机器人的白帽黑客是 Phil Daian,这位智能合约研究者与其同事撰写了 Flash Boys 2.0 论文,创造了「矿工可提取价值」(MEV)这一概念。

Phil 曾对我描述过一个宇宙级的恐怖杀手,他称其为「广义抢跑者」。套利机器人通常会在 mempool 中查找特定类型的交易(例如去中心化交易所 DEX 的交易或预言机更新),并尝试根据预定算法抢先截胡。广义抢跑者寻找能够从中赚钱的任何交易,迅速复制其交易并用自己的地址替换原交易中的地址。它们甚至可以执行交易并复制由其执行轨迹生成的有利可图的内部交易。

这就是为什么我说要救回上述那笔误发送的资金并非如此简单的原因。任何人都可以调用这一 burn  功能。如果我提交了一项 burn  交易,那就像是霓虹灯般的「免费资金」标志,直接向外界宣告了这个获利机会。如果这些恐怖杀手确实在 mempool 中,就会看到这一交易,迅速复制,移花接木并接管我的交易,在我的交易之前拿走了钱。

请注意,这种环境比以太坊区块链状态本身还要残酷得多。这些免费资金已经在以太坊区块链上呆了大约八个小时而未被发现,静待某个 burn  者席卷。但是任何试图拿走这笔钱的尝试都会迅速在空中被准确狙击。

拯救

要想不打草惊蛇、不惊动机器人的情况下提取资金,我需要对交易进行瞒天过海的处理,以便机器人无法检测到对 Uniswap 交易对的调用。这涉及编码和部署定制合约。我是 DeFi 领域专业的思想领袖,我以前从未向以太坊实际部署任何合约。

我需要援手,而当时美国时间已经过了午夜。幸运的是,我认识的一些最好的智能合约工程师住在欧洲时区。我的 Paradigm 同事 Georgios Konstantopoulos 同意协助部署合约和提交交易。我们投资的另一家公司 Yield 的首席工程师 Alberto CuestaCañada 自告奋勇执行合约。

一些优秀的以太坊安全工程师帮助我们制定了一个瞒天过海的计划。除了将调用隐藏为内部交易之外,我们还将交易分为两部分:一个激活我们合约的 set 交易,以及一个拯救激活(如果合约已激活)资金的 get 交易。以如下路径实现:

  1. 部署一个 Getter 合约,由其主人调用,只有在激活后才会做出 burn 调用,否则恢复原状。
  2. 部署一个 Setter 合约,由其主人调用,将激活 Getter 合约。
  3. 在同一区块提交 set 交易和 get 交易。


我们智能合约的代码

如果攻击者只试图执行这一 get 交易,就会在不调用 burn 功能的情况下让合约恢复原状。我们原本希望的是在攻击者先后执行 set 和 get 交易,发现内部调用 pool.burn 的指令,然后试图对我们超车时,我们已经完成了交易。

我们拯救这笔钱的代码脚本:

 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
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env node

const ethers = require('ethers')

const WHITEHAT = [
    "function get()"
]
const SETTER = [
    "function set(address whiteHat, bool on)"
];

(async () => {
    const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");

    const dest = process.env.DEST
    console.log("Dest balance before:", await provider.getBalance(dest))

    const whitehatAddress = process.env.WHITEHAT

    const gasPrice1 = 160 * 1e9
    // 20% higher
    const gasPrice2 = gasPrice1 * 1.2

    // call Whitehat.set(on) indirectly via the setter contract by the Setter account
    const setterWallet = new ethers.Wallet(process.env.SETTER_KEY)
    const setterClient = setterWallet.connect(provider);
    const setter = new ethers.Contract(process.env.SETTER, SETTER, setterClient)
    const tx1 = await setter.set(whitehatAddress, true, { gasPrice: gasPrice2 })
    console.log("Submitted",  tx1);

    // call whitehat.get by the Getter account
    const getterWallet = new ethers.Wallet(process.env.GETTER_KEY)
    const whitehatClient = getterWallet.connect(provider);
    const whitehat = new ethers.Contract(whitehatAddress, WHITEHAT, whitehatClient)
    const tx2 = await whitehat.get({ gasLimit: 2e6, gasPrice: gasPrice1 })

    console.log(await tx1.wait())
    console.log(await tx2.wait())

    console.log("Dest balance after:", await provider.getBalance(dest))
})()

但出乎我们意料的是,这个 get 交易会被 Infura 拒绝,即使我们手动覆盖 Gas 估算器也是如此。经过多次失败的尝试后和重新设置后,我们越来越感到时间压力,抱着一丝侥幸心理,我们让第二个交易滑进之后的区块。

这成了一个致命的错误。

我们的 get 交易确实被收录进了较早的区块,但一个 UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED 错误,意味着其在 Uniswap 的流动性消失了。结果我们的 get 交易刚刚进入 mempool 几秒钟,就有人执行了调用,卷走了这笔钱。黑暗森林里的怪兽还是吞噬了我们。

教训

真实存在的怪兽

之前我们切实知道广义抢跑者机器人的存在。但在看到它们真正出手之前,大家很可能低估了它们。

我们的希望是,通过一个授权合约、利用内部调用来完成拯救这笔资金的任务,从存储传递一个变量作为目标地址, 也许会保护我们,但实际上并没有起到作用。

如果你切实陷入了这种困境,我建议你去找 Amberdata 工程副总裁 Scott Bigelow,他是一直研究这一课题的安全研究人员,有一套能更好实现瞒天过海目的的原型实施策略。

不要心存侥幸

即使面临时间压力,我们当时也应该坚持原计划。如果我们在代码脚本上花更多时间,调整合约(也许将 Getter 合约更改为不执行任何操作,而不是在激活之前被调用就恢复原状),或者甚至同步自己的节点以避免使用 Infura,我们可能能够让该交易进入同一区块。

不要依赖常规基础架构

你的招数越奇怪,越难通过 Infura 这样的现有基础架构阻塞它。在我们的案例中,我们试图基于当前的区块链状态提交看起来像将失败的交易,Infura 有合理的保护措施阻止它。使用我们自己的节点可能会避免此问题。更妙的是,如果你碰巧认识一个矿工(我们不认识),则可以让他们直接将交易直接打包在一个区块中,从而完全跳过 mempool,自然也就避开了吃人怪兽。

未来只会更恐怖

这只是一个抢先交易的一个例子。每天发生无数次类似的事情。今天的抢先交易者只是机器人。明天可能会是矿工。

矿工们目前没有对这些机会下手,把钱留在了赌桌上。将来他们可能会为了自己的利益而在 mempool 中重新排序并提交交易。更糟糕的是,他们可能会重组其他矿工挖出的区块,以试图窃取不属于自己的 MEV,从而导致区块链的不稳定。

我们认为这种恐怖的未来是可以被预防的。Optimism (Paradigm 投资的另一家公司)对于 MEV 如何被重新引导以维护生态系统的利益有着雄心勃勃的构想,这是他们 Layer 2 扩容解决方案 Optimistic Rollup 的一部分。