捕捉以太坊黑暗森林中的通用型抢跑机器人

  • EthFans
  • 更新于 2021-01-08 09:57
  • 阅读 5148

抢跑交易就是在一个已知的未来交易发生前,将另一个交易插入执行队列的第一位。抢跑交易可能带来巨大利润。

以太坊黑暗森林里野兽横行,这可不是耸人听闻。这些抢跑机器人可以靠着分析合约的调用发现潜在的获利空间,哪怕这些调用和合约都是它们前所未见、一无所知的东西。

鉴于大部分人对抢跑机器人的认识不足,我们将深入分析这一情况,并了解抢跑机器人的普遍性。我们成功 “捕获” 了一些通用的抢跑交易机器人,并分析了它们的行为。我们研究了这些机器人的效率以及交易被抢跑的概率。我们还测试了不同的方式来抵御抢跑交易机器人。

什么是抢跑交易?

一般而言,抢跑交易就是在一个已知的未来交易发生前,将另一个交易插入执行队列的第一位。

一个简单的例子是交易所的交易过程。假设有人想要在 Uniswap 上买入大量 ETH(足以导致 ETH 价格上涨),一种牟利方法就是趁价格还未上涨时抢先买入 ETH,等到大批量买入交易推动 ETH 价格上涨后再卖出 ETH。

除了这个例子之外,抢跑其它交易也有可能带来巨大利润,如清算购买稀有 NFT(非同质化代币),或简单的用户错误。【一个交易或一组交易的可抽取价值就是矿工可抽取价值(MEV)

在以太坊上,要抢跑一笔交易可以通过提供比目标交易略高一点的 gas 价格、激励矿工在构建区块时把自己的交易(而非目标交易)排在前面来实现。Gas 价格高的交易会优先执行。因此,如果有两个想要从同一个交易合约调用中获利的交易被打包进同一个区块内,只有排在前面的交易能成功套利。

动物行为学

Dan Robinson 和 Georgios Konstantopoulos 在《以太坊是黑暗森林》中提到了一次价值抽取,有 1.2 万美元落入了 “捕食者” 的魔爪。这个捕食者非常强大,能够追踪以太坊交易池中的套利交易,并通过抢跑该交易来牟利。

黑暗森林故事听起来如此 “恐怖”,乍读之下令人难以置信。我们确实有理由怀疑这种捕食者的存在。像 Uniswap 这样受到大量机器人监视的平台,如何能从中取出资金?Uniswap 是否已经变成了一个 “常规” 套利者?

今年早些时候,我们的深度分析文章证明了事实并非如此。这不是普通的机器人。据我们所知,它成功调用了此前从未被调用过的合约函数,而且还是在原始交易被代理智能合约混淆的情况下成功抽取了资金。

这至少应该引起我们的警觉。能够监视交易池中交易就等于拥有了一个大杀器。因此,令人不安的是,一些服务商开始公开提供 “暗池” 交易层。

此类排他性的交易服务不会使用 Infura 之类的服务,甚至不需要运行私有节点,它会直接将交易直接发送给矿工,并承诺不会将交易传播给网络中的其它节点,从而保护该交易不被捕食者发现。

samczun 等人也采用了同样的方法,成功从一个有漏洞的合约中抽取了 960 万美元。不难想象,矿工也会开始运行抢跑交易机器人,同时只为那些支付额外费用的用户提供安全通道。

与以太坊的其它部分(如,可以在开发环境或测试网中测试的智能合约)不同,这些机器人只在以太坊主网上运行。对于这些机器人来说,除了进行初期实验之外,抢跑测试网交易赚不了钱,没什么意思。此外,它们不需要像其他人那样遵守规则,因为它们的逻辑是隐藏的。

我们不知道在什么情况下,这些抢跑交易捕食者才会罢手。因此,从某种程度上来说,追踪这些捕食者就像是在寻找稀有动物。我们不想找出所有抢跑交易机器人,这里只针对某几类通用的机器人。

为了确保我们捕获的抢跑者是 “真正的” 通用型抢跑者,我们需要设下一个特殊的陷阱。这个陷阱就是,使用一个秘密字符串的 SHA256 哈希值创建一个新的合约,并在合约内存入一些资金。只有提供秘密字符串的人才能取出锁定在合约内的资金。一旦有人发送取款交易,这些资金会直接发送给交易发送方。

我们的设想是,发送一个提供正确秘密字符串的 “诱饵” 交易,看看是否有人会复制这个字符串,并自己发送提供一个包含该字符串的交易来取走合约内的资金。如果有人能在诱饵交易之前取走这笔资金,则表明有人能分析交易池中的诱饵交易,复制其相关内容,并自己发送提供该内容的交易。

有趣的是,它们会向一个之前从未见过的合约提供一个它们之前从未见过的秘密字符串,来取走资金——这就是真正的通用型抢跑者。

通用型抢跑者是如何运作的?

这个实验最重要的一部分是,理解通用型抢跑者是如何运作的。然而,如果有人造了一台赚钱机器,是不可能将它分享到 Github 上的。因此,我们只能观察捕食者的行为并进行逆向工程。

构建一个通用型抢跑者通常需要两个组件。第一个组件是带有或不带有智能钱包代理的以太坊账户,可以发送修改过的交易。第二个组件是 “后端”,也就是整个操作的大脑,通常发生在链下。运营者可以采用某种技术检查交易池中的每个交易,并解析交易,替换交易的参数(例如,交易调用者),判断交易是否有利可图。

1

- 抢跑交易机器人的处理流程 -

一个理性的机器人不会试图抢跑一个交易费超过潜在收益的交易。交易手续费可能会很高,尤其是在 gas 价格很高的情况下。因此,我们预计至少需要一定的利润才能引诱机器人上钩。

此外,交易池中有很多交易,要想分析每个交易,时间也至关重要。挖出一个以太坊区块平均需要12 秒。如果一个交易支付的 gas 价格足够高,抢跑者必须赶在下一个区块挖出前迅速分析并替换该交易。

这个过程有一定的概率性。有可能一个交易被广播后,包含该交易的区块就立即被挖出,机器人根本来不及分析该交易并广播抢跑交易。

考虑到种种因素,我们测试了能让机器人上钩的方法。

设下陷阱

我们的合约(诱饵)的初始余额为 0.035 ETH,当时价值约 20 美元。只要有人能提供该合约中存储的哈希值的正确原像,就可以获得这笔钱。为了取走这笔资金并引诱捕食者,另一个账户(取款者)会尝试通过提供正确的原像来获取这笔钱。

第一回合:直接调用合约

为了确保我们的陷阱正常运作,我们首先使用取款者的账户调用诱饵合约。在第一次尝试时,gas 价格相对较高(由 ethers 框架设定),我们成功取回了诱饵合约中的资金。

这可能是因为利润太低,不足以引诱捕食者,或是因为交易很快就被挖出,捕食者来不及反应。显然,这不是理想结果,因为让捕食者上钩才是我们的目标。

第二回合:给机器人足够的时间

在这一回合中,我们解决了之前遇到的问题。我们提高了潜在利益,并降低了 gas 价格,因此交易没有很快就被挖出,让机器人有足够的时间发现它。我们还把合约内的资金增加到了 0.04 ETH(比之前多了 0.005 ETH)。

这次,我们成功了。这个交易在被挖出之前等待了大约 3 分钟,最终没能取回诱饵合约中的资金。查看该合约的内部交易后,我们发现这笔资金被其他人取走了。

这个抢跑交易使用了25.000001111 Gwei(比我们多花了 0.000001111Gwei),而且和我们的交易被打包进了同一个区块。

蛛丝马迹

既然我们已经成功捕获了一个机器人(并为此付出了一些代价),我们可以发现了一些有趣之处。第一,这个抢跑交易显示,这个机器人并没有直接调用合约。它没有复制取款者的交易并通过自己所拥有的账户发送交易,而是将交易发送给代理智能合约,由后者充当智能钱包来执行交易。

现在,我们可以追踪这个机器人之前和之后的交易地址,看看它到底成功抢跑了多少交易以及它是如何运作的。

2

- 机器人正在使用它自己的代理合约 -

对这个代理合约进行反编译后,我们发现它主要有两个函数:

一个是 “取款” 函数:就是将合约中的所有资金发送给该合约的运营者。另一个函数负责接收参数:需调用的合约、一个参数列表和一个传递值参数。

有了取款函数,代理合约就可以充当运营者的智能钱包。除了能够执行对外部函数的调用外,代理合约还能确保交易开始时的余额不低于交易结束时的余额,否则就会还原,从而避免因调用未知合约而造成潜在损失(当然不包括 gas 费)。

使用 Dune Analytics,我们发现这个机器人自开始运行以来(可追溯至 2018 年 5 月)可谓 “战功赫赫”。

如果这个机器人始终使用同一个代理合约并发送地址来进行交易,我们估计这个机器人共赚取 17 ETH 左右。

3

- 该机器人自运行以来抽取的所有收益(以 ETH 为单位)-

第三回合:抢跑交易机器人有多智能?

既然我们已经确信机器人是存在的,我们想要测试是否可以通过第二个合约来混淆我们的调用,从而成功取回诱饵合约内的资金。第二个合约是一个代理合约,将调用函数取走诱饵合约内的资金。(该合约还具有 “取回” 函数,可以让我们取回资金。)

我们部署了一个ProxyTaker 合约,并调用函数来试图取走我们的资金。由于使用代理合约取走资金的成本较高,我们又将诱饵合约中的资金增加到了 0.055 ETH。果不其然,我们的交易马上就被另一个机器人抢跑了。

这次抢跑给我们留下了更深刻的印象。这个机器人不仅能够检测到我们的取款交易,居然还能识别出这个交易来自另一个完全不同的合约的内部调用!而且在超短时间内做到了这点。我们的交易仅在几秒内就成功挖出(这个机器人的交易也是)。

这个机器人的身份也相当有趣。这个机器人的合约主要由下图所示的账户控制。该账户在 Etherscan 上发表了一条评论,使人联想到白帽子黑客尝试从有漏洞的 Bancor 智能合约中取走用户资金的活动。

4

- 机器人运营者账户的评论(来源:etherscan ) -

鉴于这个机器人的行为模式与上一个机器人相同(调用来自账户 A 的代理合约,将资金发送至账户 B)而且两次抢跑事件情况相似,我们有理由怀疑这个机器人在白帽黑客的营救活动中,也对白帽黑客发起了抢跑。

这个机器人比上个机器人复杂得多。它不只聚焦于 ETH,还执行其它不同的套利交易。

根据取走资金的账户的余额来判断,这个机器人成功得多。目前,其账户余额持有约 300 ETH。我们还可以通过所有流入和流出代理合约地址的资金来估算这个机器人的收益,总计大约 900 ETH。这还只是粗略估算,因为该账户可能进行过与抢跑交易无关的交易。(点击此处,查看该机器人的所有盈利交易。)

最终回合:成功取回资金

为了让这场挑战变得更加有趣,我们再一次尝试了混淆调用的做法,这次使用的是只有我们能使用的代理合约OwnedTaker。

这次的设置与第三回合差不多。我们往诱饵合约中充入 0.05 ETH。这笔资金同样会通过交易返回给我们,而不是留在合约中。

在第一次尝试中,发送至 OwnedTaker 合约交易在大约 1 分钟后才被挖出,我们成功取回了诱饵合约中的资金。

我们又重复实验了一次。这次,我们往诱饵合约中重新充入 0.06 ETH,这是历次实验中最大的一只诱饵,来排除抢跑机器人因诱饵太小而不上钩的可能性。

我们还故意将 gas 价格设为较低值。这个交易等待了大约 7 分钟,但是依然成功取回了诱饵。

使用只能由所有者用来转发交易的 Owned 代理合约有效防止了机器人解析数据并抢跑交易。

5

- 成功取回资金 -

或许是因为只有合约所有者能执行调用,或资金被发送至与调用合约不同的地址这一事实,使得我们成功躲避了抢跑交易机器人。

这些机器人很可能被设置成了以自我保护为首位。毕竟,当它们调用未知合约时,该合约有可能执行任意代码。因此,我们可以合理地假设,在不能保证利润的情况下,它们会选择避免不必要的风险或交易费损失。

那么,我们赢了吗?

这就取决于你怎么看了。我们使用了一个新的合约,而且只有我们知道用来控制合约的秘密字符串。显然,我们捕获的两个抢跑机器人会检测交易池中的所有交易。从它们所获得的利润来看,它们确实精于此道。

为什么我们在最后一次实验中成功抵御了抢跑机器人的攻击?

这很难说。可能是因为机器人不愿意冒险与带有身份认证的合约通信,或是因为它们没有料到资金会被转移到另一个地址。这并不意味着,这个方法可以抵御所有抢跑机器人。更有可能是因为交易池中潜伏着更加复杂的机器人,它们不屑为这点蝇头小利而冒险。

结论

通过这次实验,我们已经证明了通用型抢跑机器人不仅存在,而且智能程度很高。

当然了,我们知道这个实验并不完整,也无法得出权威结论。很多其它类型的机器人有着截然不同的触发机制和运行方式。潜在利润、通信模式和最低复杂性(如 gas 上限)等因素也会影响这些机器人的运行方式。

然而,我们相信我们的工作提供了概念证明,并且进一步揭露了这些通用型抢跑机器人的运行方式。

可怕的是,如果有人调用一个能够产生收益且任何人都能调用的合约,即使这个合约非常复杂,也很有可能会被机器人捷足先登。

因此,了解这些机器人以及它们的运行方式至关重要,这样我们才能构建更加安全的系统。Flashbots 等项目试图通过实现 MEV(矿工可抽取收益)提取民主化并使其成为一种公共资源来实现该目标。我们希望这一研究能够有所帮助。

如果你遇到过这类机器人,或是正在研究更复杂的混淆技术,请随时联系我们。你的经历可以帮助我们更好地了解这类机器人,并构建一个更安全的加密货币社区。

感谢 Tal Be’ery、Omer Shlomovitz、Oded Leiba、Dan Robinson 等人对本文的审阅。

附录

诱饵合约

// SPDX-License-Identifier: MIT
 pragma solidity ^0.6.0;
import "@openzeppelin/contracts/utils/Address.sol";
contract Giver {
     using Address for address payable;
address payable sender;
bytes32 imageA;
constructor(bytes32 _imageA) public payable {
    sender = msg.sender;
    imageA = _imageA;
}
function giveSender(bytes32 _preimageA) public {
    sender.sendValue(address(this).balance);
}
function giveAnyone(bytes32 _preimageA) public {
    require(hash(_preimageA) == imageA);
    msg.sender.sendValue(address(this).balance);
}
function hash(bytes32 _preimage) internal pure returns (bytes32 _image) {
    return sha256(abi.encodePacked(_preimage));
}
receive() external payable {}
} 

ProxyTaker 合约

// SPDX-License-Identifier: MIT
 pragma solidity ^0.6.0;
import "@openzeppelin/contracts/utils/Address.sol";
interface GiverContract {
     function giveAnyone(bytes32 _preimageA) external;
 }
contract ProxyTaker {
     using Address for address payable;
address payable owner;
constructor() public {
    owner = msg.sender;
}
function take(address giverAddress, bytes32 _preimageA) public {
    GiverContract(giverAddress).giveAnyone(_preimageA);
}
function collect() public {
    owner.sendValue(address(this).balance);
}
receive() external payable {}
} 

OwnedTaker 合约

// SPDX-License-Identifier: MIT
 pragma solidity ^0.6.0;
import "@openzeppelin/contracts/utils/Address.sol";
interface GiverContract {
     function giveAnyone(bytes32 _preimageA) external;
 }
contract OwnedTaker {
     using Address for address payable;
address payable owner;
constructor() public {
    owner = msg.sender;
}
function take(address giverAddress, bytes32 _preimageA) public {
    require(msg.sender == owner);
    GiverContract(giverAddress).giveAnyone(_preimageA);
    owner.sendValue(address(this).balance);
}
receive() external payable {}
} 

(完)


原文链接: https://zengo.com/ethology-a-safari-tour-in-ethereums-dark-forest/ 作者: Alex Manuskin 翻译&校对: 闵敏 & 阿剑

点赞 5
收藏 10
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

2 条评论

请先 登录 后评论
EthFans
EthFans
以太坊爱好者 https://ethfans.org