# 超详细回顾 2000 万 OP 被盗始末

By [kittenyang](https://paragraph.com/@kittenyang) · 2022-06-10

---

这几天加密圈最劲爆的新闻应该非 2000w OP 被盗一事莫属，社区用户炸开了锅。作为对技术感兴趣的码农，对这样的新闻当然不仅仅停留在凑热闹层面，还是花了点时间研究了一下其中缘由，也顺便加深了对加密世界的理解。

废话不多说，直接进入正题。

背景
==

OP 作为 Layer2 四大天王之首的生态链，一直以来拥有强大的社区。而6月刚好发币，热度火爆，因此官方需要做市商提供 OP 的流动性，让市场活跃起来。Wintermute 就是最知名的老牌做市商，OP 官方也是看中了 Wintermute 的背书，故而决定转 2000W OP 币给 Wintermute 让其帮忙做市。那么很自然的， Wintermute 展示了自己的多签钱包合约地址。由于本篇涉及到太多地址，为了方便读者记忆，我们称 Wintermute 的多签钱包合约地址为【最终地址-L2】（[0x4f3a120E72C76c22ae802D129F599BFDbc31cb81](https://optimistic.etherscan.io/address/0x4f3a120E72C76c22ae802D129F599BFDbc31cb81)）。

时间线
===

*   May-26-2022 11:55:44 PM +UTC OP 官方发送 1 个币到【最终地址-L2】，Wintermute 说收到了！
    
*   May-27-2022 04:05:27 PM +UTC OP 官方分两笔发送剩下的 100W+1900W 共计2000W OP 到【最终地址-L2】
    
*   May-30-2022 Wintermute 发现这个多签钱包只是在主网上存在，地址相同，只不过在主网上，我们称之为【最终地址-L1】。但【最终地址-L2】还没有人认领，也就是 2000W OP 转到了一个尚未被人认领的地址。
    
    [https://twitter.com/optimismPBC/status/1534631770330746880?s=20&t=r9YHgdPyaQgmbsTWcZNZqw](https://twitter.com/optimismPBC/status/1534631770330746880?s=20&t=r9YHgdPyaQgmbsTWcZNZqw)
    
*   May-30-2022 Wintermute 联系多签钱包开发团队 Gnosis Safe ，请求认领这个地址。但不知道什么原因，竟然计划在 06.07 日修复账户权限。
    
    > 这里是我觉得本次事件是**自导自演**最可疑的地方，太匪夷所思了，心这么大吗？你想想，明明知道这个地址谁都可以去认领，还要拖一个星期？难道就不怕内部员工或者但凡闻到点风声的技术人员提前下手吗?
    
*   Jun-01-2022 10:19:11 AM +UTC 黑客部署了攻击合约。说明此时黑客已确定了完整的攻击流程。
    
*   Jun-05-2022 03:54:19 AM +UTC 不知为何，黑客直到四天之后才完成了认领。
    
    > 也就是说，5月30号 ~ 6月5号之间，真正5天时间，Wintermute 和 Gnosis Safe 两个各自领域决定领头的团队，竟然无动于衷？？ 我不相信这里面没有技术人员意识到这件事分秒必争的重要性。再次加深了我对其自导自演的怀疑。
    

基础知识
====

在以太坊中，一个常见的 0x 地址可以是2种账户：外部账户（EOA）、合约账户。

![](https://storage.googleapis.com/papyrus_images/4b8ea8602d03e6e0ec9c8518a066d5b5229d12785c673f5e8856f6f1ca194b60.png)

而决定合约地址的代码有CREATE 和 CREATE2 两种，公式如下：

*   CREATE： 与【发起部署的地址】 & 【该地址关联的 nonce 】 有关
    
*   CREATE2： `new_address = hash(0xFF, sender, salt, bytecode)`
    
    1.  0xff 是固定的常数
        
    2.  sender为发起部署的地址
        
    3.  salt为 sender指定的任意值
        
    4.  bytecode 为要部署的合约代码
        
    
    > CREATE2 避免了引入递增的 nonce 。将其替换为sender可控的 salt，这样可以更好地控制合约部署地址。
    

合约就是一个链上的 App，可以由开发者按医院编写。因此合约完成自身创建后，开发者可能内部又继续调用 CREATE 或者 CREATE2 方法创造了新的合约地址。

**对于 EOA 账户，每发起一笔交易 nonce 就会 +1**

**对于合约账户，每次部署一个新的合约则 nonce +1。**

因此如果一个合约内部通过 CREATE 方法创建了其他合约，根据上面的公式，【发起部署的地址】就是自身发起的，所以是固定的，【该地址关联的 nonce 】会不断累加。因此只要确定了自身地址，子合约的地址**都是可以预测**的。

接下去我们顺藤摸瓜，从目标地址 0x4f3a120E72C76c22ae802D129F599BFDbc31cb81 一步一步往前倒。

先放大图，方便大家对后面的知识有个全局认识。

![](https://storage.googleapis.com/papyrus_images/93fe52883b5e24c2b5252c5d52320d17308a2ba3cb2dbc97770244e004853105.png)

第一个运气
=====

打开主网上[【最终地址-L1】](https://etherscan.io/address/0x4f3a120E72C76c22ae802D129F599BFDbc31cb81)的合约地址，点击 Creator 的 txn

![](https://storage.googleapis.com/papyrus_images/2353ae1637b02b5e291f9c67b5a5c7383caa69aba29ae3743085610bcafa1709.png)

拉到最下面，可以看到，是一个叫 `createProxy` 的方法。

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

这里有一个基础知识，就是所有最终的多签钱包都是由这个 Proxy Factory 合约（下面简称【工厂合约-L2】）生成的。我们看一下【工厂合约】的代码，点开上图1中的链接即可。代码地址：[0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b](https://etherscan.io/address/0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b#code)。

![](https://storage.googleapis.com/papyrus_images/4ad8685065358fcbea785b9aeb6c34d6ee6a9a070f4349d68c5c1a282422a174.png)

第一个运气来了。 `createProxy` 方法内部使用了 `new Proxy()` 从而间接使用 **CREATE** 底层方法部署合约。从上面的背景知识我们知道，CREATE 我们只与【发起部署的地址】 & 【该地址关联的 nonce 】 有关。

因此，接下去我们的目标就转换成了如何去「认领」layer2 上的【工厂合约-L2】地址（即 0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b） 。我们继续往前倒。

第二个运气
=====

我们在主网上继续搜索 0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b 这个【工厂合约-L1】，点击右边的三个点，选择 **View Contract Creation,** 把 raw hash 用代码 decode 一下 .

    const tx_receipt = ethers.utils.parseTransaction(raw_transaction_has)
    console.log("tx_receipt:", tx_receipt)
    

    {
            nonce: 2,
            gasPrice: BigNumber { _hex: '0x02540be400', _isBigNumber: true },
            gasLimit: BigNumber { _hex: '0x114343', _isBigNumber: true },
            to: null,
            value: BigNumber { _hex: '0x00', _isBigNumber: true },
            data: '0x......',
            chainId: 0,
            v: 28,
            r: '0xc7841dea9284aeb34c2fb783843910adfdc057a37e92011676fddcc33c712926',
            s: '0x4e59ce12b6a06da8f7ec7c2d734787bd413c284fc3d1be3a70903ebc23945e8c',
            from: '0x1aa7451DD11b8cb16AC089ED7fE05eFa00100A6A',
            hash: '0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261',
            type: null
        }
    

**nonce = 2; 并且 v = 28**

这意味着我们可以很轻松实现「重放攻击」！

### 什么是重放？

我们知道数字签名就是在数字世界模拟了现实世界里的手动签名的含义。现实世界中，大到总统对政策签署文件，小到朋友间对借条签字画押，作用都是 A 同意当前自己的操作，通过签名画押这个操作留下凭证。那么同样的，数字世界里的签名也是同样的作用。

由于以太坊第二层解决方案(L2)底层方案都是基于以太主网，所以主网上的一些交易可以重新在L2 上发生。比如我在主网上签名了一个交易，而基于区块链的特性，签名的内容都是透明的，所以任何人拿这个签名都可以在任何一个L2平行链上重新执行这个交易，并且产生的结果一模一样。

在没有出现多链之前，以太坊并没有考虑这种情况，随着后面 L2 不断壮大， 官方才推出了 EIP155 补丁，不再允许这种跨链重放的行为。

根据 EIP-155 ，v = 28 或者 v = 27 的交易未使用 EIP-155 签名，不会将 ChainID 引入到交易签名中，因此可以进行重放攻击。

同时因为 nonce = 2，非常小，黑客手动把前两笔 nonce=0,1 的也重放了，花不了太多力气。

![重放代码](https://storage.googleapis.com/papyrus_images/cb53b4019d726181674266d5a573fda27052f60fd2775fda8e10d88987a48db5.png)

重放代码

大家亲自打开用浏览器并排对比下下面两个链接，只是host 不一样，其他一模一样的两个链接

[https://etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261](https://etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261)

[https://optimistic.etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261](https://optimistic.etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261)

![](https://storage.googleapis.com/papyrus_images/5dc8071530fab49d38e8798b47b0fcadf0f60548989249b4281c62974f84ced3.png)

至此，一模一样的【工厂合约-L2】已经在 OP 上拷贝出来了，剩下的只需要第一时间疯狂刷nonce， 创建最终的【目标多签钱包】即可，这一步就比谁快了。从时间记录来看，黑客在工厂合约诞生后 18 秒就启动了刷 nonce 程序，最终只用了1分24秒就创建出了【目标多签钱包】。

[https://optimistic.etherscan.io/tx/0x00a3da68f0f6a69cb067f09c3f7e741a01636cbc27a84c603b468f65271d415b](https://optimistic.etherscan.io/tx/0x00a3da68f0f6a69cb067f09c3f7e741a01636cbc27a84c603b468f65271d415b)

### 为什么【第一个运气】里面不能直接用重放呢？

1.  因为 decode raw transaction hash 出来发现 v=25，说明这一条交易 hash 已经包含了 chain id，不能在 layer2 上重放了。
    
2.  虽然关上了这扇门，但还留了一扇窗 —— Create Factory 方法底层使用 CREATE 部署合约，让我们得以靠刷 nonce 的方式实现目的。
    
3.  即使第一个运气里 v=27 或 v=28 可以重放，但因为正常多签钱包的 Creator 都是个人，我们可以看到主网上这个多签钱包的 Creator 是一个 EOA 0xF3B8FbE5Efb62E36855c51f4678F2bF6aE064DEd，并且 nonce == 395 ，也就是说即使可以重放，你也重放这个 EOA 账户前面 395 次，还不如直接用 CREATE 刷 nonce 部署来得方便。
    

![](https://storage.googleapis.com/papyrus_images/92cfdbbd9e6f8cf42bb1b01a8750bb3d18d2feecd313b573505935f8cb71df4a.png)

![](https://storage.googleapis.com/papyrus_images/28843a7dea36bd10f68ab6bb0b9b230ce520c0f4c2e5cb6e242d2968564c6d03.png)

第三个运气
=====

非得说第三个的运气的话，我想应该就是 OP 官方开发团队竟然还允许 v=27、28 的交易重放，要知道现在很多测试链都做了放重放限制。

![Polygon 不允许 EIP-155 之前的交易重放](https://storage.googleapis.com/papyrus_images/c9a999ca346f7cabcd9aa3ac15cb1319791031d810bc4aa94575ec897c0ebbef.png)

Polygon 不允许 EIP-155 之前的交易重放

![OP 没有做防护](https://storage.googleapis.com/papyrus_images/b5062d191d1e73d0a25aef1dc8da3d34acbedd2164e29241550659a7da0d7b44.png)

OP 没有做防护

总结
==

最后再次附上这张大图。

![](https://storage.googleapis.com/papyrus_images/93fe52883b5e24c2b5252c5d52320d17308a2ba3cb2dbc97770244e004853105.png)

---

*Originally published on [kittenyang](https://paragraph.com/@kittenyang/2000-op)*
