账户抽象之路#3-钱包创建

我们尚未解决的问题是,每个用户的智能钱包合约最初是如何在区块链上创建的。部署合约的“传统”方法是使用EOA发送一个没有接收人的交易,这个交易中包含了部署的合约代码。在这里会非常恼人,因为我们刚刚做了很多工作,就是让人可以在没有EOA的情况下与区块链交互。如果用户需要自己的EOA来开始,这一切都是为了什么?

需要澄清我们的目的是,我们想要这样一个钱包:没有钱包的人应该能够最终在链上获得一个全新的钱包,要么用ETH支付自己的gas费(即使他们还没有钱包),要么通过找到一个代付者帮忙付gas费(我们在第2部分中涵盖了这一点),他们应该能够在不创建EOA的情况下做到这一点。

还有一个不太明显的目标,但也很重要。

当我创建新的EOA时,我可以在本地生成我的私钥,并在不发送任何交易的情况下认领我的帐户。

我可以告诉其它人我的地址,并在我自己发送交易之前,就可以开始接收ETH或其它代币。

我们希望我们的智能钱包拥有相同的属性,这意味着在我们实际部署钱包合约之前,我们应该能够告诉其他人我们的地址,并能够接收资产。

前提条件:用CREATE2创建确定性的合约地址

在我们实际部署合约之前,我们的地址要可以接收资产,这其实也告诉了我们需要如何实现这一点。尽管我们还没有部署我们的钱包合约,但我们需要知道当我们最终真正部署它时,它最终会是什么地址。

最终将部署合约但尚未部署到的地址被称为反事实地址(counterfactual address),是不是很有意思。

实现这一目标的关键因素是CREATE2操作码,它正是为此而设计的,它在可以根据以下输入,确定性的计算出部署合约的地址:

  • 调用CREATE2的合约地址

  • 一个椒盐噪声,可以是任何32字节的值

  • 真正部署合约的init code

init code是EVM字节码的blob,指定一个函数,该函数在执行时返回不同的EVM字节码blob,该字节码保存为新部署的智能合约。这是许多人没有意识到的一个有趣的现象:当你部署合约时,你提交的代码与合约中的代码不同。特别是,多次使用相同的init code并不能保证已部署的合约具有相同的代码,因为init code可以从存储中读取或使用TIMESTAMP等操作码。

第一次尝试:入口点部署任意合约

现在我们知道了CREATE2,我们的第一个计划很简单。我们让用户传递initCode,如果钱包合约还不存在,则让入口点部署该合约。首先,我们将为用户操作添加一个新字段:

struct UserOperation {
  // ...
  bytes initCode;
}

然后,我们将更新入口点的handleOps的验证部分,以执行以下操作:

作为验证用户操作的一部分,如果操作具有非空的initCode,则使用CREATE2部署具有该initCode的合约

然后像往常一样进行其余的验证:

  • 调用新创建的钱包的validateOp方法

  • 然后,如果操作有代付者,则调用代付者的validatePaymasterOp方法

它实现了上面讨论的所有目标:用户可以部署任意合约,并提前知道他们最终的地址,部署可以由代付者或用户自己支付gas费(如果他们提前将ETH存入确定但未部署的合约地址)。

但有一些缺陷,这些缺陷都围绕着这样一个事实,即我们要求用户提交字节码,然后入口点来验证所有的字节码码:

  • 当代付者查看用户操作时,它无法合理地分析字节码来决定是否要为它付费。

  • 当用户提交字节码来部署合约时,他们无法立即验证他们提交的字节码是否做到了他们想要的东西。如果用户正在使用第三方工具来部署他们的合约,那么该工具是一个恶意的或被黑客入侵的工具,它可以提交initCode,然后将后门安装到部署的合约中,而这个行为无法轻易检测到。

  • 回想一下第1部分,打包者希望为它包含在捆绑包中的每个操作模拟验证,这样它最终就不会包括验证失败的操作,然后它必须先自付gas费用。但由于initCode可以是任何代码,它很容易在模拟期间成功,但在执行期间失败。

我们需要一种方法,让用户在不提交任意字节码的情况下部署合约,并让其他参与者能够对部署行为获得一些保证。

像往常一样,当我们想要更多的执行保证时,是时候引入一个新的智能合约了。

更好尝试:引入工厂合约

我们不再让入口点合约可以接受任意的字节码,并调用CREATE2方法,而是让用户自己选择一个合约去调用CREATE2方法,然后这些我们统称为工厂合约,他们可以根据特定的需求创建不同类型的智能钱包。

例如,可能有一家工厂合约生成保护其 Carbonated Courage 代币的智能钱包,而另一家工厂合约生产的钱包需要5个多签中至少3个密钥来签署交易。

工厂合约将对外暴露一个可以创建钱包合约的方法:

contract Factory {
  function deployContract(bytes data) returns (address);
}

我们让工厂合约返回新创建的钱包合约的地址,以便用户可以在部署之前模拟此方法,获得合约创建的地址,这也是我们最初的目标之一。

用户可以调用称为工厂的合约,专门创建不同种类的钱包合约
用户可以调用称为工厂的合约,专门创建不同种类的钱包合约

用户可以调用称为工厂的合约,专门创建不同种类的钱包合约。

这解决了上一节中的前两个问题!

  • 如果用户调用工厂合约要求保护 Carbonated Courage 代币的钱包,那么假设工厂合约经过审计,他们肯定知道他们最终会得到一个保护 Carbonated Courage 的钱包,没有后门,他们不需要检查任何字节码就可以做到。

  • 代付者可以选择为那些经过批准的工厂合约,支付他们的部署付款(gas费)。

上一节中的最后一个问题是部署代码在模拟期间可能成功,但在执行期间失败。

这正与我们在代付者 的 validatePaymasterOp 方法中遇到的问题类似,我们将以同样的方式解决它。

打包者将限制工厂合约只能访问他们自己的关联存储和他们正在部署的钱包的存储,并且不允许他们调用被禁止的方法,如 TIMESTAMP

我们还将要求工厂合约通过入口点的 addStake 方法质押一些 ETH,然后打包者可以根据最近模拟被伪造的频率来限制或禁止此工厂合约。

与打包者paymasters一样,如果工厂的部署方法仅访问其正在部署的钱包的关联存储,而不访问工厂合约自己的关联存储,则工厂合约无需质押。

我们做到了!

钱包创作从未如此美好。

此时,我们创建的架构可以执行实际 ERC-4337 的所有功能!

我们将在第 4 部分中讨论的关于聚合签名的唯一剩余目标:启用优化以节省 gas,但实际上并没有添加任何新功能。

我们可以愉快地停在这里享受我们的成功创作,但如果我们想要节省那些gas费,我们可以继续前进了......

更多文章请阅读

原文:

Subscribe to 0xBitFly
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.