本文介绍了如何利用 EIP-7702 将以太坊普通账户(EOA)升级为智能合约钱包的安全架构,重点介绍了 EIP7702Proxy 代理合约的设计,该合约解决了 EIP-7702 引入的初始化安全、存储管理和兼容性等挑战,使得 EOA 能够安全地升级并保持原有地址,同时保持与现有 EOA 行为的向后兼容性。
EIP-7702 使简单的以太坊钱包(EOA)能够升级为智能合约钱包,从而提供更高的安全性、高级功能、Gas 赞助的机会和其他好处。从历史上看,智能钱包必须从头开始创建,但随着 EIP-7702 的引入,传统的钱包可以升级,并保留其所有的资产和链上历史记录,且保持相同的钱包地址。这就像从座机切换到智能手机而无需获得新号码一样。
EOA 通过设置“委托指定(delegation designation)”,即指向委托智能合约(delegate smart contract)的指针来进行升级,然后委托智能合约的逻辑来管理 EOA。因此,升级后的 EOA 可以拥有函数、设置存储、发出事件以及执行智能合约可以执行的所有其他操作。EOA 可以随时通过新的、已签名的 EIP-7702 授权来更改或删除此委托。虽然这解锁了许多新的可能性,但这个强大的功能也引入了新的安全挑战,需要仔细考虑和创新解决方案。
为了使 EOA 能够充当智能合约钱包,我们开发了 EIP7702Proxy
,这是一个轻量级的 ERC-1967 proxy 合约,旨在用作 EOA 的 EIP-7702 委托。除了代理执行的基本逻辑转发之外,EIP7702Proxy
还包含其他功能和设计选择,可以解决 EIP-7702 委托账户特有的一些挑战。设计 EIP7702Proxy
的一个目标是使“标准部署”的 Coinbase 智能钱包和 EIP-7702 委托的 Coinbase 智能钱包之间尽可能地保持对等性,这意味着将 EIP-7702 机制所需的额外复杂性抽象到专用代理中,并继续依赖 CoinbaseSmartWallet
的原始实现。这种挑战的解决方案可以有效地应用于任何实现逻辑,而不仅仅是 CoinbaseSmartWallet
实现,同时还有助于 EOA 在启用 7702 的环境中保持安全。
下面我们将介绍具体的挑战和相应的设计解决方案,这些解决方案使我们能够安全地调整任何现有的智能合约钱包实现,以用于 EIP-7702 升级。
实现 EIP-7702 的第一个主要障碍来自其初始化约束。传统的智能合约钱包(包括 CoinbaseSmartWallet
)通常通过单独的工厂合约在其部署期间原子地处理安全初始化(建立账户所有权)。这种原子性意味着许多此类实现都具有未受保护的 initializer 函数,这些函数只能被调用一次。但是,EIP-7702 的设计不允许在代码委托过程(与“部署”相当的步骤)期间执行初始化 calldata,因此无法原子地完成此操作。
这种步骤分离会产生一个关键的漏洞窗口。当通过 EIP-7702 将实现合约“部署”到 EOA 时,无法保证此 7702 升级和初始化钱包的标准 EVM 交易将原子地执行。从技术上讲,即使同时提交,设置授权的代码也可以独立于初始化交易应用,这可能允许攻击者抢先执行初始化交易并声明智能账户的所有权。
请注意,现有的 Coinbase 智能钱包部署在带有 UUPSUpgradeable 实现(实际的 CoinbaseSmartWallet
逻辑)的 ERC-1967 proxy 之后。实际账户地址中的代码是一个代理,该代理使用 ERC-1967 定义的常规存储位置来保存指向其实现逻辑的指针。我们针对 7702 上下文中的初始化漏洞的解决方案包括认识到任何实现逻辑只有在代理实际建立与其的连接时才会变为活动状态(因此才有危险)。因此,如果我们不能强制执行原子性的部署和初始化,我们可以强制执行原子性的实现设置和初始化。
EIP-7702
CoinbaseSmartWallet
合约架构和逻辑委托流程
在 EIP-7702 的上下文中,EOA 本身是对其账户进行任何更改的初始权限,并且必须提供签名以授权初始化并建立新智能账户的任何所有者。我们的 EIP7702Proxy
合约实现了一个 setImplementation
函数,该函数可以原子地设置新的逻辑实现并初始化账户。setImplementation
函数:
验证来自 EOA 的签名,其中包括关键数据,例如新 implementation 合约的地址、初始化 calldata、将评估最终账户状态的安全性的验证器合约的地址,以及基本的签名可重放保护,例如 nonce 和过期时间。
将 ERC-1967 指针的值设置为新的 implementation,并针对新的逻辑 implementation 执行提供的 calldata。
调用 validateAccountState
函数,该函数必须由签名中包含的验证器实现。
验证器是一个特定于 implementation 的合约,其中包含用于评估其是否认为最终账户状态安全的逻辑。例如,对于 CoinbaseSmartWallet
,CoinbaseSmartWalletValidator
将检查账户的所有权状态是否为非空,因此不再容易受到任意初始化的影响。
EIP-7702 最复杂挑战可能与存储管理有关。EOA 可以随时自由地将其逻辑重新委托给不同的合约,或完全删除委托。所有委托共享 EOA 地址上的相同存储空间。随着时间的推移,多个合约共享对同一存储的访问可能会导致“存储冲突”问题。当两个合约对同一存储位置进行不同的更改或做出不同的假设时,会发生存储冲突,这可能会导致不可预测的错误。
存储冲突的管理已经是代理设计领域中一个熟悉的问题,在该领域中,可变的 implementation 逻辑用于管理共享存储。即使可升级代理可以更改 implementation,代理代码本身(对于非 7702 地址)也无法更改。这给升级过程带来了确定性和保证。7702 重新委托为可以管理此共享存储的潜在逻辑引入了另一层完全可变性。这基本上消除了围绕任意委托可能对存储产生的影响的任何保证。例如,如果 EOA 从委托 A 委托到 B 并再次委托回 A,则返回的委托无法对其存储的状态做出假设,委托 B 可能已擦除或将其操纵为仅通过委托 A 的逻辑无法实现的状态。对于任何 7702 委托都是如此,无论委托模式如何,因为先前的委托可能已在任何存储位置存储或删除任何内容。
由 A → B → A 委托模式引起的委托 A 的无效状态示例
EOA 委托会任意影响账户状态。如果 EOA 委托给恶意或破坏性合约,则任何现任委托都无法防范这种情况。与签署 drainer 交易一样,授权恶意 7702 委托可能会造成灾难性后果,而防范这些结果超出了我们的设计范围。
我们设计的 EIP7702Proxy
旨在对多钱包、启用 7702 的生态系统中可预见的问题具有自我防御能力,该生态系统由善意但可能混乱的参与者组成。它无法保护授权真正恶意或有错误的委托的 EOA。
一个可预见的问题涉及 setImplementation
调用的签名以及可变账户状态带来的风险。EIP7702Proxy
依赖于 EOA 签名来设置 implementation 逻辑并初始化为安全状态。如果这些签名可以重放,它们可能会成为负担。例如,如果签名授权了一个初始所有者,该所有者后来遭到入侵并被删除,则可重放签名可能会重新建立遭到入侵的所有者或强制降级 implementation。
防止签名重放的常见保护措施是在签名消息中使用 nonce,并在验证后标记为已使用。7702 账户的风险:其他委托可能会破坏此 nonce 跟踪存储。如果跟踪 nonce 使用情况的存储被删除,则 EOA 的 setImplementation
签名(在 mempool 中公开可用)可以在委托回 EIP7702Proxy 时重新应用。
为了保证签名不可重放,我们实现了一个单独的 NonceTracker 单例,它在账户存储之外的不可变合约位置维护 nonce 状态。只有 EOA 才能影响其 nonce(仅以递增方式),从而防止其他委托操纵这些安全关键值。无论账户存储发生什么变化,NonceTracker
都能确保每个 setImplementation
签名仅工作一次。
像 ERC-1967 定义的标准存储槽特别容易受到潜在存储冲突的影响,因为它们是多个委托实现可能使用的常规位置。ERC-1967 implementation 槽是 EIP7702Proxy
中使用的唯一标准存储位置,它保存着代理指向的逻辑 implementation 的地址。我们的设计保证,无论此存储位置的值如何(该值决定了账户中可用的大部分逻辑),EIP7702Proxy
始终能够成功地将其 implementation 逻辑设置为 EOA 期望的合约。
为了更清楚地说明正在解决的问题,请注意,当一个账户在不同的委托之间转换 (A→B→A) 其中两个委托都实现了 ERC-1967 代理模式,委托 B 自然会使用与委托 A 用于存储其 implementation 地址的同一存储槽。在其任期内,委托 B 可能会修改或覆盖此插槽,无论是故意的还是作为其自身代理操作的正常部分。在 UUPSUpgradeable 代理模式中,升级 implementation 的逻辑在 implementation 合约本身上定义。如果委托 B 放置在此指针位置的 implementation 不包含 implementation 上预期的 upgradeToAndCall
接口,那么当返回到委托 A 时,更改其 implementation 的机制可能不存在于当前可用的逻辑上。
新委托覆盖共享的传统存储位置的示例
EIP7702Proxy
上可用的升级机制我们的 EIP7702Proxy
通过其 setImplementation
函数解决了这个问题,该函数直接在代理本身上提供了一个独立于 implementation 的升级机制。这确保即使中间委托已将 ERC-1967 implementation 指向无效的 implementation(或完全删除了它),在委托回 EIP7702Proxy
后,原始 EOA 仍保持重新配置代理的 ERC-1967 指针的能力,以指向他们选择的逻辑 implementation。
EIP7702Proxy
的一个设计目标是除了新的智能合约功能外,还要保持与账户 EOA 功能的向后兼容性。地址上代码的存在或不存在会影响与该地址交互的协议的执行流程,因为它们以此特性来区分 EOA 和智能合约。这需要考虑两个主要行为:签名验证和代币接收行为。
智能合约的签名验证标准与标准 EOA 不同。智能合约实现了 ERC-1271 定义的 isValidSignature
接口,并且可以自由地定义任意逻辑来确定合约是否认为签名有效。对于标准 EOA,签名通过标准的 ecrecover
检查进行验证,该检查确保签名者恢复到预期的 EOA 地址。
为了确保在 7702 升级后,现有或未来的 EOA 签名将继续在 EOA 上被认可,EIP7702Proxy
实现了 isValidSignature
的一个版本,该版本首先将签名验证委托给应该在逻辑 implementation 上定义的 isValidSignature
函数,但如果验证失败,则会执行最终的 ecrecover
检查。如果此检查通过,则认为签名有效。通过这种方式,使用 EIP7702Proxy
的 EOA 可以保证,无论其智能合约钱包的 isValidSignature 实现如何,简单的 EOA 签名始终将在其地址上被认可。
一些代币标准(特别是 ERC-1155 和 ERC-721)试图防止代币卡在可能无法管理它们的智能合约中。这些代币要求任何将接收此类代币的智能合约通过实施标准代币接收器接口来声明此能力,这些接口由代币合约在代币发送期间调用。同样重要的是,升级后的 EOA 上的逻辑包含标准的 receive
函数或 payable 回退,以便能够接收原生代币。账户不应处于无法接收 ETH 或其他代币的状态,即使是很短的时间。
由于我们的代理缺少初始 implementation,因此我们包含了一个不可变的 DefaultReceiver
implementation 作为 EIP7702Proxy
在缺少 ERC-1967 指针时的默认逻辑。此接收器实现了接收函数和这些常见代币标准的接收器Hook,确保账户可以在显式设置新 implementation 之前接受代币转账。
EIP7702Proxy
设计使我们能够与标准部署的 CoinbaseSmartWallets 保持紧密一致,并继续使用现有的 CoinbaseSmartWallet
implementation,同时解决 EIP-7702 上下文中出现的独特安全挑战。通过仔细考虑初始化安全性、存储暂时性和干扰的影响、对不间断代币处理的需求以及与标准 EOA 签名验证的向后兼容性,我们创建了一个用于安全地委托和管理 EIP-7702 智能合约钱包的代理。虽然 EIP7702Proxy
的设计考虑了 CoinbaseSmartWallet
V1 implementation,但此代理最终与 implementation 无关。我们鼓励开发人员评估此解决方案,以对其他智能合约钱包 implementation 进行 7702 防护。
EIP7702Proxy
委托及其支持合约已部署在 Coinbase Smart Wallet 支持的所有网络上。
合约存储库在 https://github.com/base/eip-7702-proxy 上开源。
部署地址可以在此处找到。
一个演示通过 EIP-7702 将新 EOA 委托给带有 CoinbaseSmartWallet
implementation 的 EIP7702Proxy
的示例应用程序在 Base Sepolia 上上线,源代码可以在此处找到。
- 原文链接: blog.base.dev/securing-e...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!