理解账户抽象 #3 - 钱包创建
前面 2 篇我们介绍了账户抽象,第一篇:如何使用智能合约钱包来实现 EOA 钱包一样的功能,第二篇 : 介绍如何使用第三方来代为支付手续费。
钱包创建
我们还没有解决的问题是每个用户的钱包合约首先是如何在区块链上如何生成的。部署合约的 "传统" 方式是使用EOA来发送一个没有接收者(没有to字段)的交易,其中包含合约的部署代码。这在将是相当不满意的,因为我们刚刚做了很多工作,使某人可以在没有EOA的情况下与链交互。如果用户需要自己的EOA才能开始,那这一切是为了什么?
为了明确我们的想法,一个想要钱包但还没有钱包的人应该能够在链上拥有一个全新的钱包,要么用ETH支付他们自己的Gas(即使他们还没有钱包),要么通过找到一个愿意为他们的交易付Gas的支付者(我们在第二部分中涉及到),需要应该能够做到这一点,而不需要创建一个EOA。
还有一个不太明显的目标也相当重要。
当我创建一个新的EOA时,我可以在本地生成我的私钥,并在不发送任何交易的情况下申请我的账户。
我可以告诉别人我的地址,并在我自己发送交易之前开始接收ETH或代币。
我们希望我们的钱包有同样的属性,也就是说,我们应该能够在实际部署我们的钱包合约之前告诉别人我们的地址并接收资产。
前提:用CREATE2确定的合约地址
关于在我们实际部署合约之前就能在我们的地址接收资产, 这一点是对我们需要实现他的一点提示。这意味着,尽管我们可能还没有部署钱包合约,但我们需要知道当我们最终能够真正部署它时,它将在什么地址上。
一个合约最终将被部署但尚未部署的地址被称为反事实地址(counterfactual address)。
实现这一目标的关键因素是CREATE2操作码,它在一个地址上部署一个合约,这个地址可以通过以下输入确定地计算出来。
- 调用CREATE2的合约的地址
- 一个盐(salt),它可以是任何32字节的值
- 被部署的合约的init代码
初始代码是一些EVM字节码,指定了一个函数,该函数在执行时返回一个不同的EVM字节码,保存作为新部署的智能合约。这是一个有趣的花絮:很多人都没有意识到。当你部署一个合约时,你提交的代码并不是最终出现在链上合约中的代码。特别是,多次使用相同的初始代码并不能保证部署的合约会有相同的代码,因为初始代码可能会从存储中读取或使用TIMESTAMP等操作码。
译者注:合约的字节码分为创建字节码及运行时字节码, 可参考 理解 EVM 专栏 的 解构Solidity合约 #1 - 字节码
第一次尝试: 入口点部署任意的合约
既然我们知道了CREATE2,我们的第一个计划就很简单。我们将让用户传入初始代码,如果合约还不存在,则由入口点部署合约。首先,我们将为用户操作添加一个新的字段:
struct UserOperation {
// ...
bytes initCode;
}
然后,我们将更新入口点handleOps的验证部分,做以下工作:
作为验证用户操作的一部分,如果该操作有非空的initCode,则使用CREATE2来部署一个带有该initCode的合约。
然后继续进行其他正常的验证工作。
- 调用新创建的钱包的validateOp方法
- 然后,如果该操作有一个paymaster,调用paymaster的validatePaymasterOp方法
这是一个很好的尝试!
它实现了上面讨论的所有目标:用户可以部署任意的合约,并提前知道它们最终的地址,而且部署可以由paymasters或用户自己赞助(如果他们将ETH存入合约最终的地址)。
但是有一些缺陷,这些缺陷都是围绕着这样一个事实:我们要求用户提交及入口点要验证一个任意的字节码。
- 当一个paymaster 看到一个用户操作时,它不能合理地分析这一串字节码来决定它是否要为其付款。
- 当用户提交一串字节码来部署合约时,他们不能轻易地验证他们提交的字节码是否符合他们的要求。如果用户使用一个工具来部署他们的合约,那么如果该工具是恶意的或被黑客攻击的,它可以提交initCode,将后门安装到部署的合约中,而这种攻击不容易被发现。
- 回顾第一篇,捆绑者希望对它包含在捆绑中的每个操作进行模拟验证,这样它就不会最终包括那些未能通过验证的操作,然后它就不得不自掏腰包支付Gas。但由于initCode是任意代码,它很容易在模拟过程中成功,但在执行过程中失败。
我们需要一种方法让用户在不提交任意字节码的情况下部署合约,并让其他参与者能够对部署行为有一些保证。
像往常一样,当我们想要更多的执行保证时,就是引入新的智能合约的时候了。
更好的尝试: 引入工厂
与其让入口点接受任意字节码并调用CREATE2,我们将允许用户选择一个合约作为调用CREATE2的合约。这些选择的合约,我们称之为工厂,如果他们愿意,可以专门创建不同种类的钱包合约。
例如,可能有一个工厂生产保护他们的 Carbonated Courage 代币的钱包,另一个工厂生产 3/5 多签来签署交易的钱包。
工厂将暴露一个可以被调用的方法,以创建一个合约:
contract Factory {
function deployContract(bytes data) returns (address);
}
我们让工厂返回新创建的合约的地址,这样用户就可以模拟这个方法,在部署合约之前找出他们的合约会在什么地址上,这也是我们最初的目标之一。
我们还将在用户操作中添加字段,这样,如果操作试图部署一个钱包,那么它就会指定使用哪个工厂,以及传递工厂将收到的数据作为输入:。
struct UserOperation {
// ...
address factory;
bytes factoryData;
}
用户可以调用称为工厂的合约,这些工厂专门创建不同种类的钱包合约。
这就解决了上一节中的前两个问题:
- 如果用户调用工厂生成保护 Carbonated Courage 代币的钱包,假设工厂合约经过审核,他们肯定知道最终会得到一个保护 Carbonated Courage 的钱包,没有后门,而且他们不需要审查任何字节码就能做到。
- paymasters可以选择为某些经授权的工厂的部署付款。
上一节的最后一个问题是,部署代码在模拟过程中可能会成功,但在执行过程中会失败。
这正是我们在paymaster的validatePaymasterOp方法中遇到的问题,我们将以同样的方式解决它。
捆绑器将限制工厂只能访问他们自己的相关存储和他们正在部署的钱包的存储,并且不允许他们调用被禁止的方法,如TIMESTAMP。
我们也会要求工厂使用入口点的addStake方法质押一些ETH,然后捆绑者可以根据工厂最近的模拟伪造频率来限制或禁止工厂。
和paymaster一样,如果工厂的部署方法只访问它所部署的钱包的关联存储,而不是工厂自己的关联存储,就不需要质押。
我们已经做的不错,钱包创建从来没有这么好过。
在这一点上,我们创建的架构可以执行实际EIP-4337的所有功能!
我们将在第四篇介绍的唯一剩下的目标是关于聚合签名,实现优化以节省Gas,我们可以继续期待下一篇。
转载自:https://learnblockchain.cn/article/5442