以太坊是一个黑暗森林

  • Paradigm
  • 发布于 2020-08-29 11:28
  • 阅读 63

这篇文章讲述了一个在以太坊区块链上恢复意外发送至交易对合同的流动性代币的故事,涉及到复杂的智能合约操作和前置跑机制。尽管实施方案经过精心设计,但由于操作失误和竞争强烈,最终未能完成代币恢复。文章强调了在区块链交易中的风险及应对策略。

挑战

和任何普通人一样,我在 Uniswap Discord 的 #support 频道中闲逛了很多时间。(披露: Uniswap 是 Paradigm 的投资组合公司。)

在星期三下午,有人问是否可以找回意外发送到同一对交易合约的 Uniswap 流动性代币。

我最初的想法是这些代币会被锁定永远。但那天晚上,我突然意识到,如果这些代币仍在,那它们可以被任何人找回。

任何人在 Uniswap 核心合约上调用 burn 函数时,合约会 测量 它自己的流动性代币余额并将其销毁,将被提取的代币交给调用者指定的地址。这是 Uniswap v2 预期行为的核心部分(基本机制在 Uniswap v2 白皮书 的第 3.2 节中描述)。

我找到了合约。流动性代币仍然在那里——价值约 $12,000。

这意味着三件事:

  • 有一个tick作响的时钟。即使没有其他人注意到这笔免费钱,任何人都可以在任何时候撤回他们自己的流动性,意外地从合约中获得这些代币。
  • 我可以采取行动,试图为那个意外发送代币的人找回代币。调用池的 burn 函数,只需传递我的地址就可以了。
  • 但是……我知道这并不会那么简单。

黑暗森林

以太坊区块链是一个高度对抗的环境,这不是秘密。如果智能合约可以被利用以获取利润,它最终会被利用。新黑客攻击的频率表明,一些非常聪明的人花了很多时间检查合约的漏洞。

但是,这种无情的环境与 mempool(待处理的未确认交易集合)相比显得微不足道。如果链本身是一个战场,那么 mempool 则是更可怕的东西:一片黑暗的森林。

黑暗森林 是我最喜欢的科幻小说。它引入了“黑暗森林”的概念——一种检测就意味着在先进捕食者手中必亡的环境。在这个环境中,公开识别其他人的位置就如同直接摧毁他们一样。(这一概念还是以太坊测试网游戏 Dark Forest 的灵感来源。)

在以太坊的 mempool 中,这些顶级捕食者以“套利机器人”的形式出现。套利机器人监控待处理交易,并试图利用这些交易创造的盈利机会。没有人比 Phil Daian 更了解这些机器人,他是一位智能合约研究人员,和他的同事们一起撰写了 Flash Boys 2.0 论文,并创造了“矿工可提取价值”(MEV)这一术语。

Phil 曾告诉我关于一种宇宙恐怖的事情,他称之为“广义抢跑者”。套利机器人通常会在 mempool 中寻找特定类型的交易(如 DEX 交易或预言机更新),并试图按预定义算法抢跑。而广义抢跑者则寻找 任何 他们可以通过复制并将地址替换为自己的交易,以盈利的交易。他们甚至可以执行该交易并复制其执行轨迹生成的盈利 内部交易

这就是为什么救援不会简单。这 burn 函数可以被任何人调用。如果我提交一个调用 burn 的交易,那就像一个闪烁的“免费钱”标志,直接指向这个盈利机会。如果这些怪物真在 mempool 中,他们会看到、复制、变化,并抢跑我的交易,在我的交易被包含之前就拿走钱。

请注意,这种环境比以太坊区块链状态本身更严酷。这笔免费钱在链上静静待了大约八个小时,没有被发现,等待 任何 调用 burn 的人来捞取。但任何尝试去捡起它的行为都会被瞬间截杀。

救援

为了尝试提取这笔钱而不引起机器人的注意,我需要混淆交易,使得对 Uniswap 对交易合约的调用无法被检测到。这将需要编写并部署自定义合约。因为我是专业的 DeFi 思想领袖,所以我实际上从未在以太坊上部署过合约。

我需要帮助,而那时已经过了午夜。幸运的是,我认识的一些顶级智能合约工程师都生活在欧洲时区。我的 Paradigm 同事 Georgios Konstantopoulos 同意帮助我们进行合约部署和交易提交。另一家投资组合公司的首席工程师 Alberto Cuesta Cañada 自愿实现合约。

一些出色的以太坊安全工程师帮助我们制定了一个混淆计划。除了将调用作为内部交易埋起来外,我们还将交易分成两个部分:一个 set 交易,用于激活我们的合约,和一个 get 交易,在合约激活时救回资金。实施如下:

  1. 部署一个 Getter 合约,当其所有者调用时,只有在激活的情况下才会进行 burn 调用,否则将会回退。
  2. 部署一个 Setter 合约,当其所有者调用时,将激活 Getter 合约。
  3. 同一块 中提交 set 交易和 get 交易。

我们自定义智能合约的代码:

如果攻击者只尝试执行 get 交易,它将在未调用 burn 函数的情况下回退。我们希望,当攻击者顺序执行 setget 交易以找到对 pool.burn 的内部调用并抢跑时,我们的交易已经被包含。

令我们惊讶的是,即使我们手动覆盖了Gas估算器,get 交易仍然会被 Infura 拒绝。经过几次失败的尝试和重置,时间压力使我们变得松懈。我们让第二个交易滑到了后面的区块中。

这是一个致命的错误。

我们的 get 交易确实被包含进来了——但伴随着 UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED 错误,这意味着流动性已经消失。结果发现,在我们的 get 交易进入 mempool 后的几秒钟,某人 执行了调用并抢走了资金。

那些怪物吞噬了我们。

教训

怪物是现实的

我们在智力上知道这些广义抢跑 bots 的存在。但直到你真的看到它们的行动,你很可能低估它们。

我们仍然抱有一些希望,认为通过一个授权合约作为内部调用来进行救援,通过存储中的变量传递 to 地址,可能会保护我们。事实并非如此。

如果你发现自己处于这种情况,我们建议你联系 Scott Bigelow,一位已在研究这个主题并拥有更好混淆器原型实现的安全研究员。

别表现得松懈

即使在时间压力下,我们也应坚持计划。如果我们多花些时间编写脚本,稍微调整合约(也许将 Getter 合约更改为在未激活的情况下什么都不做而不是回退),甚至同步我们自己的节点以避免使用 Infura,我们可能就能将交易放入同一块中。

不要依赖正常基础设施

你所做的事情越奇怪,通过现有基础设施(如 Infura)把它完成就会越困难。在我们的例子中,我们试图提交一个看起来基于当前区块链状态会失败的交易,而 Infura 在这方面有合理的保护。使用我们自己的节点可能可以绕过这个问题。

更好的是,如果你碰巧认识一个矿工(我们没有),可以直接让他们将交易包含在一个区块中,完全跳过 mempool——和那些怪物。

未来只会变得更加可怕

这只是一次抢跑事件的一个例子。类似的事情每天都在无数次发生。今天,抢跑者只是机器人。明天,它将会是矿工。

目前,矿工通过不立即采取行动而留下了机会成本。未来,他们将会在他们的 mempool 中重新排队和提交交易以获取自己的利益。更糟的是,他们可以重新组织其他矿工挖掘的区块,试图窃取未被其认领的 MEV,这可能导致链的不稳定。

我们认为这个未来是可以防止的,并对几个雄心勃勃的尝试感到兴奋。Optimism 有一个愿景,说明 MEV 如何可以重新定向以惠及生态系统,这是其Layer2缩放解决方案,乐观 Rollup的一部分。StarkWare 除了其自己的第 2 層缩放系统之外,还建立了一项称为 VeeDo 的“可验证延迟功能”服务,使以太坊应用免受这种抢跑攻击的影响。(Paradigm 投资了 Optimism 和 StarkWare。)

如果你在考虑 MEV 或在这个领域构建某些东西,请与我们联系!

致谢:感谢 Alberto Cuesta Cañada、Scott Bigelow、Phil Daian、Charlie Noyes 和 samczsun 的讨论,这些讨论帮助形成了这篇文章。

interface IGetter {
  function set(bool) external;
}

interface IPool {
  function burn(address to) external returns (uint amount0, uint amount1);
}

contract Setter {

  address private owner;

  constructor () public {
    owner = msg.sender;
  }

  function set(address getter, bool on) public {
    require(msg.sender == owner, "no-owner");
    IGetter(getter).set(on);
  }
}
contract Getter is IGetter {
  IPool private pool;
  address private setter;
  address private getter;
  address private dest;
  bool private on;

  constructor(address pool_, address setter_, address getter_, address dest_) public {
    pool = IPool(pool_);
    setter = setter_;
    getter = getter_;
    dest = dest_;
  }

  function set(bool on_) public override {
    require(msg.sender == setter, "no-setter");
    on = on_;
  }

  function get() public {
    require(msg.sender == getter, "no-getter");
    require(on == true, "no-break");
    pool.burn(dest);
  }
}
  • 原文链接: paradigm.xyz/2020/08/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Paradigm
Paradigm
Paradigm 是一家研究驱动型技术投资公司 https://www.paradigm.xyz/