信息来源自substack,略有修改,作者foobar
1.6亿美元不见了。该领域最敏锐的做市基金Wintermute在9月的一个早晨醒来,发现一个重要的钱包少了9位数。是什么原因导致了这次被盗?是Vanity Address生成器糟糕的随机性。黑客只是从头重复了搜索迭代,直到他们重新创建了私钥和公共地址。然后,数以百万计的资金不翼而飞。
此外,还有一个关于Indexed Finance的故事,黑客在2021年10月盗取了1600万美元,然后将被盗资金转移到一个以0xba5ed…开头的地址。他们不知道的是,这个Vanity Address也受到了困扰Wintermute的糟糕随机性漏洞的影响,2022年9月,所有的钱都再次被盗,流向了另一个黑客。
这些天才开发者到底出了什么问题,我们能从中学到什么?
首先,什么是Vanity Address?Vanity Address是指用户创建的与其钱包或智能合约相关的公共地址。它可能以0x0000000开始,可能以0xdeadbeef开始,也可能是其他东西。它们受欢迎的原因有几个:
1. gas优化。仅仅因为他们的EOA有一些前导零,Wintermute就节省了15,000美元的gas成本。听起来很傻吗?但这就是EVM的工作原理! 如果你的地址里有很多零,交易gas费用就会下降。
2. 协议的品牌效应。你知道1inch代币合约的开头是0x111111111吗……?
3. 多链再现性。在我看来,这是重中之重,也是为什么每个协议都应该在部署时使用一个Vanity Address。你的应用程序可以存在于15个不同的EVM链上,并且在任何地方都有相同的地址!这对开发者和用户来说不是更容易吗?
它在什么时候是安全的?
以太坊地址有两种类型:外部拥有账户(EOA)和智能合约账户。如果你用过MetaMask这样的钱包,里面的每个地址都有一个EOA。它被用来签署消息和进行交易处理。与Uniswap合约这样的智能合约账户相比,人们可以与之交互,但它不能在不被触发的情况下自己采取行动。很简单——Vanity Address对EOA不安全,但对智能合约是安全的。
为什么会这样?我们将在下文中详细解释,但这取决于Vanity Address如何生成。对于EOA,你将在数以百万计的私钥中循环,直到找到一个与公共地址相对应的、具有美感的地址。然而,私钥控制着EOA中的资金,所以如果你用来迭代私钥的随机性被破坏了,那么你的整个账户就毁了。另一方面,创建智能合约Vanity Address只需要通过公共种子进行迭代,而这些种子并不授予智能合约任何管理权限。
这就是Wintermute失败而OpenSea成功的原因——使用不安全的软件在不安全的内存中生成私钥是不行的。但是用这种方式生成公有种子是完全没问题的!所以EOA vanity是一条通往破产的道路,而智能合约vanity则是通往成功的道路。
为什么协议需要Vanity Address
更简约的文档。你可以在所有链上指向一个合约地址
用户可验证。只有在字节码完全逐个匹配的情况下,才会出现相同的合约地址
开发者可验证。由于相同的合约地址只出现在完全匹配的情况下,因此你可以在部署脚本中捕捉到棘手的小问题
简单的集成。其他协议可以将你的合约地址硬编码到他们的多链代码中,而不需要基于链上的if语句。
智能合约Vanity Address
有一种方法可以生成100%安全的vanity智能合约地址。使用哪种软件并不重要,迭代技术是否公开泄露也不重要。这就是所谓的“CREATE2 Factory法”。这不仅提供了Vanity Address,也是确保你在多个链上拥有相同合约部署地址的万无一失的方法。而且,它可以让其他人在没有任何私钥共享或nonce假设的情况下,代表你无信任地部署代码。
首先,简单介绍一下智能合约地址的选择方式。有两个部署选项CREATE和CREATE2。当你直接从EOA部署智能合约时,默认流程是CREATE。该地址是通过将合约创建者地址与合约创建者nonce进行哈希运算来确定的。nonce是指一个地址发送了多少交易,所以新的钱包是从0开始,每次发送一个新交易都会增加1。下面是CREATE部署的智能合约地址的神奇公式:
New_address = hash(sender, nonce)
虽不太常见但更有趣的是,下面是使用CREATE2部署的智能合约地址的公式:
new_address = hash(0xFF, sender, salt, byteccode)
前者看起来更简单,对吧?但是,让我们举一个例子,看看与更强大的CREATE2流程相比,这种简单性在哪些方面会产生不利影响。
粗心的Alice:多链会出问题
想象一下,一个名叫Alice的加密开发者创建了两个智能合约:一个名为GriddleSwap的Uniswap分叉,以及一个名为ph00ts的 NFT项目。这两者都是不可变的独立原语,这意味着不存在外部依赖关系或跨链桥风险。Alice以nonce 0将GriddleSwap部署到以太坊,然后以nonce 1将ph00ts部署到以太坊。遗憾的是,Alice的注意力不够集中,在将她的工作部署到第二大智能合约平台币安智能链之前,她在加密推特上分了几分钟心。
她搞砸了部署顺序,在GriddleSwap之前部署了ph00ts。因为智能合约地址完全取决于部署区块链中的创建者地址和nonce,以太坊GriddleSwap的地址与BSC ph00ts的地址完全相同!雪上加霜的是,以太坊ph00ts的地址与BSC GriddleSwap的地址相同。这不仅仅会使终端用户会感到困惑是。事实上,恶意部署者可能会滥用它来欺骗人们认为跨链合约行为是相同的。
细心的Alice:还是会有问题
即使Alice在部署时很认真,从不混淆她的nonce顺序,也会有其他问题。如果Alice正确地部署到了以太坊和BSC上,但随后在Polygon上进行了不相关的交易,那么nonce 0已被使用了。她将永远无法在那里部署GriddleSwap,因为她的nonce已经增加了。因此,人们必须不惜一切代价保护部署者私钥。如果Alice泄露了信息,恶意破坏者就可以进行不相关的交易。 如果Alice丢失了它,她也失去了在新的链上再次部署到该地址的能力。这是一个永久性的漏洞,依赖于诚实的个人来保护私钥。如果比特币核心开发者都不能做到这一点,我们其他人又怎么能做到呢?
CREATE2:解决方案
值得庆幸的是,有一种更好的方法可以跨链获得一致的地址,而不依赖于秘密私钥、不依赖于单一的部署者,并且在整个过程中可以抵抗部署者的错误。请记住用于查找使用CREATE2部署的智能合约地址的公式:
new_address = hash(0xFF, sender, salt, byteccode)
第一个参数0xFF是一个可以忽略的常数值。第二个参数(发件人地址)可以通过在大多数EVM链中选择z0age的CREATE2 Factory部署0x0000000000FFe8B47B3e2130213B802212439497来保持一致。第三个参数是用户选择的salt,我们可以用它来找到一个Vanity Address,然后跨链保持不变。第四个是合约字节码,它可以作为有用的完整性检查,以确保我们在链上部署完全相同的功能。无论任何部署者做什么,所有这四个参数都可以保持一致。
为什么这样做更好?与私钥不同的是,部署者选择的salt可以被公开!知道salt可以部署合约,但对合约资产或功能却没有任何控制权。因为它不绑定任何秘密信息,所以任何人都可以在不泄露或共享私钥的情况下将合约部署到新的链上。字节码参数还确保当且仅当字节码相同时,这些新的无权限部署将具有相同的地址。因此,终端用户无需进行详细的代码差异比较,就能得到更强的保证。
创建你自己的Vanity Address
你以为工作证明在以太坊合并后就完蛋了吗?那些帮助比特币区块寻找带有大量前导零的哈希原像的GPU能力,在为EVM智能合约找到带有大量前导零的哈希原像方面也非常出色。来自OpenSea的z0age发现了一个简单的设置方法来创建你自己的Vanity Address。
Spin up a GPU Example instance using vast.ai that makes ~2 *billion* attempts per second and costs ~25 cents / hr:Image: nvidia/opencl
GPU: 1x RTX 3090
Disk space to allocate: 1.83 GB
SSH in and install rust + create2crunchsudo apt install build-essential -y; curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh -s — -y; source “$HOME/.cargo/env”; git clone https://github.com/0age/create2crunch && cd create2crunch; sed -i ‘s/0x4/0x40/g’ src/lib.rs
运行种子搜索。对于环境变量,INIT_CODE_HASH是是合约创建代码的keccak256。LEADING是你想要的前导零字节数,TOTAL是你想要的合约地址中的总零字节数。
export
FACTORY=”0x0000000000ffe8b47b3e2130213b802212439497″; export CALLER=”0x0000000000000000000000000000000000000000″; export INIT_CODE_HASH=”0xabc…def”; export LEADING=5; export TOTAL=7; cargo run –release $FACTORY $CALLER $INIT_CODE_HASH 0 $LEADING $TOTAL
当z0age首次发布他的repo时,它能够在上述vastAI硬件上每秒运行19亿次尝试。从那以后,向量化在一些OpenGL内核上疯狂运行,我观察到每秒有21.5亿次尝试。这意味着找到一个5前导零字节的地址将花费256^5/(2150000000 * 60)~= 8分钟,找到一个6前导零字节的地址将花费256^6/(2150000000 * 3600)~= 36小时,而找到一个7前导零字节的地址将花费256^7/(2150000000 * 86400)~= 387天。请注意,一个字节等于两个十六进制字符,因此一个5前导字节的地址将有10个零。当然,这种搜索可以完全并行化,而且随着时间的推移,实际的成功概率将遵循泊松分布。
部署CREATE2 Factory
精明的读者可能已经注意到,CREATE2 Factory已经跨所有链在0x0000000000FFe8B47B3e2130213B802212439497上运行。这有点像鸡生蛋还是蛋生鸡的问题,一致的地址部署如何依赖于一致的地址部署?
当我最初了解这种方法时,我认为它只是一个由比我更聪明的人安全持有的私钥。但实际上,它比这要强大得多! ENS创始人Nick Johnson的提出的“无密钥交易”方法利用了这样一个事实:你可以从任何交易签名中恢复公共地址,而不需要知道签署该签名的相应私钥。因此,人们可以创建一个交易(“部署CREATE2 Factory”),然后为其创建一个假签名,例如仅由2组成的签名。这个伪造的签名有一个私钥,但没人知道它是什么。但是我们可以恢复“无密钥签名”对应的公共地址,向其发送一些ETH,然后将签名交易提交给mempool。尽管这种方法很抽象,但它是一个有效的交易,而且实际上是唯一可以从这个公共地址发出的有效交易。
结果呢?任何人都可以在没有任何专有信息的情况下将factory部署到新的链上,同时防止恶意行为者造成损害。对于创建一个只能部署一次交易的单一用途的EOA来说,这是相当巧妙的技术。
这可以通过三个简单的“forge cast”命令来完成。该字节码太长,无法在此处复制,但你可以按照https://github.com/ProjectOpenSea/seaport/blob/main/docs/Deployment.md上的说明,在你选择的任何链上无权限地部署CREATE2 Factory !当然,如果已经部署过了,就没有必要再部署一次。