ERC4337 和智能钱包的安全注意事项
账户抽象是一个广泛的话题,但在非常高的层面上,其理念是将账户的概念抽象为一个智能合约(即 智能钱包),这比大多数人今天在与区块链交互时使用的外部拥有账户(EOA)提供了更多的灵活性。其一些好处包括:
一旦与EOA的限制脱钩,账户的灵活性仅受限于智能合约中可以编程的内容。
在以太坊中实现账户抽象的提议标准定义在EIP-4337中。
这可能是目前最好的描述ERC-4337的图表,感谢The Red Guild。
操作由称为用户操作的对象表示。
可以在这里找到每个详细解释,但我们可以看到一些熟悉的名称,它们也是以太坊交易的一部分:sender
,nonce
,callData
,gasFees
(打包了maxFeePerGas
和maxPriorityFeePerGas
)。签名属性是一个任意的有效载荷,将由钱包实现使用。
捆绑器运行一个服务器,在备用内存池中收集用户操作。他们负责执行这些操作,最终将其打包成一批用户操作并发送到Entrypoint。
捆绑器支付gas,但期望他们的成本得到补偿,并收取服务费。
Entrypoint 是链上中心角色。它是一个单例智能合约,受到其他各方的信任,将处理捆绑器、账户和赞助交易方之间的交互,以协调操作的验证和执行。
工厂负责创建实际的账户。如果账户尚未部署,用户操作可以包含一个initCode
,将通过工厂用于初始化合约。
合约创建利用CREATE2
操作码提供确定性地址。这有助于模拟EOA的行为:无需事先部署代码即可安全地预计算发送者地址。
账户合约通常会在第一次交互期间创建。
标准的一个关键特性是能够赞助交易。赞助交易方是生态系统中定义的另一个实体,可以提供所需资金以支付操作的成本。这可以极大地帮助新用户入驻并改善整体体验,并且一直是账户抽象的主导用例之一。
例如,特定协议可能会赞助其合约的交易以激励交互。另一个有用的场景是赞助交易方从账户中提取ERC20代币作为支付,启用无gas交易。
正如提案所述,主要挑战是防止拒绝服务(DoS)攻击。虽然某些链上交互很容易验证(例如检查签名是否有效),但确保愿意执行(不受信任的)操作的构建者得到补偿(否则不执行)。如果攻击者包含故意回滚的操作会发生什么?我们可以添加验证,但谁来支付验证所花费的gas成本?如果是捆绑器故意给用户带来麻烦呢?操作可以在链下模拟,但如何确保这些操作在链上运行时具有相同的结果?
此外,想象一下如果捆绑器直接与账户交互会发生什么。捆绑器不能信任账户会偿还费用,账户也不能信任捆绑器不会发送无法执行但会消耗gas(并且必须支付)的无效操作。
解决这个问题的方法是将验证与执行分开。这种方法允许我们在验证阶段应用严格的约束,而不干扰操作本身的执行。两个主要限制是:
TIMESTAMP
,NUMBER
或GASPRICE
,完整列表见EIP-7562部分操作码规则)。关键在于使验证步骤尽可能纯净,希望其链下模拟可以准确预测链上会发生什么。捆绑器只需关心操作的验证。任何无效操作支付的gas将归于捆绑器,但失败的操作(经授权后)由发送者支付。这样,捆绑器可以在验证步骤上运行模拟,提高其链上执行成功的信心。
验证与执行的分离是拥有中心 Entrypoint 的主要原因。我们可以在验证步骤中施加限制,同时在稍后允许任意执行。通过Entrypoint运行操作为捆绑器提供了更好的保证,并允许账户安全地将验证与执行分离(记住Entrypoint是一个受信任的实体)。
简而言之,Entrypoint执行以下操作:
validateUserOp()
)我们可以在参考实现中清楚地看到这种模式:
注意这里的循环如何工作:验证全部在一个单独的循环中完成。我们不希望一个操作的执行干扰另一个操作的验证。
拥有一个中心Entrypoint合约来协调这个过程,使得不同的参与者能够验证其他人是否行为正确。在执行时,账户只需检查调用者是否为Entrypoint,因为它可以信任Entrypoint已经验证了操作。如果没有这个受信任的实体,验证与执行的分离是不可能的。
账户执行仅检查调用者是否为Entrypoint,因为它可以信任操作已被先前验证。示例来自SimpleAccount.sol。
当涉及到paymaster时,同样的冲突也会出现,我们需要调用validatePaymasterOp()
来检查paymaster是否愿意赞助该操作。然而,这里的情况有些不同。单个paymaster可能会处理来自不同发送者的多个用户操作,这意味着一个操作的验证可能会干扰另一个操作,因为paymaster的存储在所有具有相同paymaster的操作包中是共享的。在此函数中限制存储访问将非常有限,这将严重减少paymaster的能力(参见EIP-7562部分Unstaked Paymasters Reputation Rules)。
恶意的paymaster可以导致拒绝服务。为了减轻这种攻击向量,标准提出了一个质押和声誉系统。paymaster需要质押ETH。捆绑器也会跟踪失败的验证,并可以限制或直接禁止不合作的paymaster。请注意,质押永远不会被削减。质押的目的是为了减轻女巫攻击,以便paymaster不能简单地转移到一个新的账户并拥有新的声誉。
一个重要的细节是,paymaster也被允许在主要操作完成后通过调用postOp()
来执行。在验证阶段,paymaster可以检查在操作执行之前是否满足某些条件,但在执行过程中这些条件可能很容易失效。例如,一个拉取ERC20代币来支付费用的paymaster可以验证发送者是否有足够的代币(以及足够的授权),但操作的执行可能有意或无意地改变这一点。对postOp()
的失败调用可能会使操作回滚,但此时gas已经被消耗,这将由paymaster承担,从而使恶意账户能够进行悲伤攻击(griefing attacks)。
这个问题的解决方案非常有趣,因为它非常简单,我们调用paymaster的postOp()
两次。第一次调用发生在与操作的主要执行一起的内部上下文中。如果第一次调用回滚,那么操作也会回滚,并触发第二次调用,此时操作的效果被取消。
工厂不仅能够实现确定性部署,还为各种参与者提供了更强的保证。与处理浅层字节码字符串不同,具体且已知的工厂的存在允许更好的可见性和分析,同时提供额外的安全性。例如,paymaster可以通过简单地检查目标工厂来决定是否赞助钱包创建。对于捆绑器来说,通过拥有一个确保没有链上回滚的知名实现,模拟的复杂性大大降低。此外,它为用户提供了更好的安全性,因为工厂合约地址比任意初始化代码更容易分析,从而实现更好的工具和用户体验。
账户在验证阶段使用工厂创建。摘录自SenderCreator.sol。
由于钱包部署本质上与所需的授权账户分离,因此将钱包初始化与其地址链接非常重要,正如标准所指出的。否则,攻击者最终可能会使用他们的凭证部署一个钱包。这通常通过将签名与创建参数(可能是salt或init代码哈希)相关联来实现。因此,更改授权账户将导致不同的地址。
由于账户创建也是验证阶段的一部分(我们需要在验证账户上的操作之前将其部署),工厂与paymaster具有相同的条件。它们必须要么质押,要么将其存储空间限制在钱包的域内。
工厂通常使用克隆模式来部署新钱包。它们有一个实现实例,并创建指向该实现的代理。如果任何人可以接管实现实例并销毁它,那将使所有代理无法使用,导致所有钱包失效。这已通过自毁的弃用得到缓解,但在其他链上仍然可以被利用。
Gas在系统中起着至关重要的作用,它是确保操作成功执行和适当补偿的关键。由于其使用的众多规则,正确的gas跟踪是一项困难的任务,这可能导致许多潜在的陷阱。
攻击者故意增加数据大小以增加费用。
恶意捆绑器通过提供不足的gas来阻碍操作执行。即使用户操作指定了gas限制,如果运行上下文没有足够的gas,它仍将使用可用的量执行调用。
如前所述,将钱包地址与其所有权相关联非常重要,否则任何人都可以部署并恶意初始化它。
签名问题值得专门撰写一篇文章,但许多常见问题也可以在账户抽象方案中看到。
最轻微的不正确假设可能会为系统中的某个行为者带来负面影响的窗口。
在以下问题中,攻击者可以抢先调用 handleOps()
来执行至少一个捆绑操作,导致原始批处理回滚。
通过滥用 EIP-150,可以强制可选调用失败。在下一个问题中,执行者可以故意提供较少的gas,以便回滚内部调用,同时在外部上下文中仍有足够的gas来完成交易。
该标准相当复杂且实现起来并非易事。遵守所有细微差别可能是一项艰巨的任务。
在这个主题中,不正确的验证并不少见。在这个问题中,Entrypoint 不允许在钱包上执行操作,完全破坏了账户抽象集成。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!