揭秘 ERC-6900

揭秘 ERC-6900, ,ERC-6900 使用户能够轻松地向其账户添加或移除各种插件(功能)。

声明:首尔国立大学区块链学院的解密开源战士团队撰写了一篇关于 ERC-6900 的文章。本文基于代码进行分析,涵盖了从提案背景到实施方法和意义的所有内容。本报告中的任何内容都不应被视为投资建议,也不应被解释为此类建议。

图片 3: 解密 - erc 6600

作者 Seungmin Jeon, Sangyeup KimSangwon Moon

简介:ERC-4337 的局限性

ERC-4337 是一个标准,允许在不更改以太坊客户端的情况下,通过通过称为用户操作的对象进行额外验证,从而使用合约钱包(账户)顺畅进行交易。遵循 ERC-4337 标准的合约账户可以包含诸如支付 Gas费用的Paymaster 、批量交易、BLS 签名聚合、社交恢复和会话密钥等各种功能。

这使得与传统的 EOA(外部拥有账户)相比,用户体验水平更高。例如,使用 ERC-4337 的Paymaster,用户可以使用 ERC-20 代币而不是 ETH 支付 Gas费用,或者让协议为他们支付。此外,用户可以通过会话密钥将其账户临时委托给协议,而无需为每笔交易点击按钮,只需一键即可与协议交互。要更好地了解 ERC-4337,请参考 ERC4337 系列文章

然而,ERC-4337 的提出旨在实现账户抽象而无需协议更改,因此并未定义智能合约账户应采取何种形式。因此,提出了各种形式的合约账户,导致以下两个问题:

  1. 由于各种账户形式而导致的兼容性问题

目前,各家公司(例如,ZeroDevBiconomy)提供 SDK 形式的合约账户,但它们提供的账户形式各不相同,导致兼容性问题。这使得支持合约钱包的应用程序用户难以在应用程序之外使用这些钱包,也使得应用程序难以适应各种合约账户的用户。换句话说,尽管通过 ERC-4337 改善了用户体验,但它也会锁定用户在应用程序内部的副作用。

  1. 账户的可扩展性问题

通过 ERC-4337 可以提供的大多数功能在账户内运行,但由于账户采用智能合约形式,对于更改和扩展存在限制。虽然可以将代理结构应用于账户进行升级,但这个过程也很不方便。例如,如果用户想要向其现有合约账户添加新功能,他们必须将整个代码转移到具有该功能的新合约中。这种升级过程可能导致新合约的额外审计成本等问题。此外,由于为所有用户提供了统一形式的账户,存在一个缺点,即用户无法选择其账户内的功能。

为解决这些问题,于 2023 年 4 月提出了一个名为 ERC-6900 的新标准。

什么是 ERC-6900?

ERC-6900 是一个名为“模块化智能合约账户和插件”的 EIP,提供了与 ERC-4337 兼容的账户标准。它基于一个模块化结构,允许用户轻松地向其账户安装或移除各种功能,类似于在 Android 上安装或卸载应用程序。包含要包含在账户中的功能的模块合约被称为插件

图片 4: 一个周围有以太坊图标的手机

通过其模块化结构,ERC-6900 使用户能够轻松地向其账户添加或移除各种插件(功能)。特别是,由于当前的合约账户通常仅限于特定应用程序,使用 ERC-6900 可以使单个合约账户轻松地在多个应用程序中使用。

虽然这是一个非常有趣的想法,但在实际代码中实现时,需要考虑以下内容,重点放在安全性和用户体验上:

1. 插件的安装和移除

第一个考虑因素应该是如何实现插件的安装和移除。ERC-6900 的开发人员从 Diamond 代理中汲取了灵感。Diamond 代理是典型可升级代理的扩展结构,将合约分为几个组件(或facet),只允许升级其中的一些。与简单的可升级代理结构不同,后者需要更改整个逻辑合约以进行升级,Diamond 代理允许仅选择和升级所需的组件。

在 Diamond 代理中,合约的各种功能被划分为称为facet的组件,并且代理使用delegatecall来调用它们。delegatecall是一个函数,通过它可以“借用”外部合约的函数并在其自身合约的上下文中使用它们,从而允许通过外部函数强大地操作其自身合约的存储

此外,Diamond 代理基于函数选择器维护映射,以指定对每个 facet 的访问路径。这使得代理合约可以自由访问每个facet内的函数。

图片 5: 一个钻石函数到facet映射的图表

(Diamond 代理的结构 | 来源:ERC-2535 官方文档

此外,为了安全起见,Diamond 代理将所有数据存储在代理合约中,并将可访问的数据分配给每个facet。如果facet使用的数据没有权限限制,可能会在某些情况下出现问题。例如,zkSync 在以太坊上部署了类似 Diamond 代理的合约,为治理、L1 ↔ L2 桥接和 Rollup 数据发布创建了facet。如果治理facet可以访问用于桥接的参数,可能会通过不当操作或攻击危及整个系统。

总之,Diamond 代理具有三个主要特征:

  1. 协议功能被划分为几个facet合约,通过delegatecall访问。
  2. 代理合约可以通过函数的选择器调用facet的函数。
  3. 每个facet可访问的数据受限制。

Diamond 代理具有这些特征,针对构建多功能合约系统进行了优化。这与 ERC-6900 的目标“创建具有各种功能的智能合约账户”非常契合。因此,ERC-6900 借鉴了 Diamond 代理的结构来实现安装和移除插件的功能。然而,ERC-6900 与 Diamond 代理之间存在重大差异,稍后将进行讨论。

2. 用户与账户之间的交互

假设建立了可以自由安装和移除插件的环境,需要考虑的第二个方面是用户如何与账户进行交互。合约账户与用户之间的交互可以分为两种主要类型:第一种是通过 ERC-4337 的入口合约通过用户操作进行交互,第二种是用户直接调用合约账户内的函数。ERC-6900 将这两种类型的交互区分为‘用户操作验证’和‘运行时验证’,并为每种类型的验证和执行提供单独的流程。

Image 6: 展示创建智能账户过程的流程图

(用户与智能账户之间的交互 | 来源: Seungmin)

3. 权限

考虑的第三个方面是权限。如果任何人都可以创建和安装插件,就必须考虑这些插件可能被滥用的可能性。例如,想象一个会将账户所有权借给某个期间的会话密钥插件。如果会话密钥所有者与恶意合约交互,账户的资金可能会完全丢失。因此,严格设置和验证插件可以访问的外部合约函数以及插件之间的交互的权限至关重要。

4. 保证模块化

最后要考虑的是如何确保模块化。在一个环境中,各种插件可以自由安装和从账户中移除的情况下,存在几种情况,包括相关函数。例如,插件 A 和 B 可能具有具有相同选择器的功能,这些功能具有相同的功能。鉴于插件 A 已经部署,建议插件 B 的部署者从插件 A '借用' 该功能,而不是在他们的插件中实现相同的代码。

为了解决这个问题,ERC-6900 引入了一个名为 '依赖性' 的功能。这种机制引入了一种巧妙的方式来形成插件的模块化生态系统,可以增加相关函数的可读性和可用性。

因此,ERC-6900 的特点可以总结为四点:

  1. 它遵循 Diamond 代理的结构(但有显著的差异,稍后将讨论)。
  2. 用户交互分为用户操作和运行时,每个都有不同的验证和执行过程。
  3. 通过插件严格限制操作的权限。
  4. 可以设置依赖关系,以确保在安装和移除插件期间的模块化。

在接下来的部分中,我们将更详细地研究这四个方面。

ERC-6900 的实现

Diamond 代理

ERC-6900 采用 Diamond 代理的结构,为插件和合约账户形成了一个模块化结构。账户充当代理合约,每个插件充当一个 facet。因此,当用户操作或直接调用发生在账户时,它通过一个回退函数(当在合约中找不到调用函数的选择器时执行的函数,通常在代理模式中使用)在插件中进行处理。在这个过程中,与之前提到的 Diamond 代理有一个关键的区别。

Diamond 代理使用 delegatecall 来调用 facet 中的函数。由于 facet 是一个仅包含执行逻辑且不需要存储(有时甚至部署为不带存储的库)的合约,因此使用 delegatecall。然而,在 ERC-6900 中,插件内的函数使用 call 调用,并且插件有自己的存储。原因如下:

如果允许从账户向插件进行 delegatecall,插件的函数可能会访问账户的存储数据。这是非常危险的,因为恶意插件可能会删除或操纵账户的存储信息。在原始 Diamond 代理中,这并不是一个主要问题,因为不是每个人都可以添加 facet。然而,由于 ERC-6900 旨在创建一个任何人都可以自由构建插件的环境,delegatecall 可能带来重大风险。

因此,在 ERC-6900 中,使用 call 而不是 delegatecall,允许数据存储在插件的存储中。

MSCA 中的调用流程

在 ERC-6900 中,合约账户被称为模块化智能合约账户,或 MSCA。ERC-6900 定义的 MSCA 内的调用流程可以表达如下:

Image 7: 模型账户调用流程图

(ERC-6900 调用流程 | 来源: ERC-6900 官方文档)

让我们看看用户操作部分。在 ERC-4337 中,用户操作的验证和执行是分开的,因此流程被分为验证和执行,如上图左侧所示。另一方面,在 MSCA 内的直接调用中,流程通过右侧显示的运行时验证函数进行验证。每个流程都经过几个阶段,包括验证函数、Hook 和执行函数。

这里使用的函数可以大致分为三种类型。首先,执行一些操作在账户或插件内的函数称为执行函数。随后,对于每个执行函数,都有一个执行其调用验证的验证函数。此外,在每个函数调用之前和之后可以应用Hook。让我们更详细地了解每个内容。

  1. 验证函数

此函数对调用账户的调用者进行权限验证。如前所述,如果任何人都可以调用账户内的函数,账户可能会受到通过消耗 gas 而耗尽资金的攻击的威胁。因此,消耗 gas 或访问存储的函数必须通过验证函数进行控制。

验证函数分为两种类型:用户操作验证函数用于来自入口点的调用,运行时验证函数用于 EOA 直接调用账户内的函数时执行。

这些验证函数不存在于账户本身;它们都在插件中找到。因此,所有对账户的调用都通过插件中的验证函数。根据用例,可以有各种类型的验证函数,例如:

  • 验证签名并仅允许所有者地址通过的函数。
  • 验证签名并仅允许指定地址通过的函数。
  • 允许任何地址通过的函数。
  1. 执行函数

这些是实际进行资金转移和与外部合约交互的函数。主要有两种类型:

1) 标准执行函数

指与 ERC-4337 参考实现的 IAccount 接口兼容的 executeexecuteBatch 函数。这些函数可以根据账户的 gas 费用执行所有类型的交互,因此需要严格的验证函数(例如,仅允许所有者调用)。

2) 执行函数

这些是存在于每个插件中的函数和可以从账户执行的常见函数。例如,考虑一个名为 recoverOwner 的社交恢复插件中的函数。由于只有为恢复指定的监护人应调用此函数,因此必须具有适当的验证函数。在 ERC-6900 中,这应用于执行函数,如下所示:

ManifestAssociatedFunction({  
executionSelector: this.recoverOwner.selector,  
associatedFunction: onlyGuardiansValidationFunction  
});

通过将此信息存储在账户的存储中,当调用 recoverOwner 函数时,通过 onlyGuardiansValidationFunction 验证调用者的权限。

执行函数的选择器存储在账户存储中,在插件安装期间映射到插件地址。

  1. Hook

还有一个称为Hook的函数,它在其他函数之前和之后运行。类似于 Uniswap V4 的 Hook,它定义了需要在特定任务之前和之后执行的操作。例如,可能有一个名为 DailyGasSpendingLimit 的 Hook,限制每日的 gas 消耗。此Hook可以使用 gasleft() 函数在执行函数之前和之后检查 gas 消耗,并在一天内消耗了一定量的 gas 时阻止函数的执行。还可以在验证函数之前添加预验证Hook,主要用于需要多重验证的情况。例如,如果会话密钥插件中的会话密钥是多重签名钱包,则需要通过多重签名和会话密钥验证。在这种情况下,多重签名的验证可以作为预验证Hook执行,会话密钥的验证作为验证函数执行。

以下是更详细的调用流程图:

Image 8: a diagram showing the structure of an account

(账户和插件之间的调用流程 | 来源:Seungmin)

每个过程总结如下:

  1. 将要执行的插件的执行函数封装在calldata中并调用。如果账户中没有与此函数匹配的选择器,则执行fallback函数(这适用于除executeexecuteBatch等标准执行函数之外的所有情况,或稍后提到的executeFromPluginexecutionFromPluginExternal)。
  2. 在解析哪个插件地址包含calldata中的执行函数后,执行相关的预验证Hook和验证函数。如果调用通过 ERC-4337 入口点进行,则验证逻辑已经执行,因此会跳过。
  3. 执行与执行函数相关的预执行Hook。此Hook执行的结果返回有关要运行的后执行Hook的信息。
  4. 执行执行函数。
  5. 根据步骤 3 返回的结果,执行后执行Hook。

这里的一个重要点是,对插件的调用中msg.sender始终是账户。因此,在插件中存储或查询与账户相关的信息时,应如下使用msg.sender

// src/plugins/owner/SingleOwnerPlugin.sol

mapping(address => address) internal _owners;

function isValidSignature(bytes32 digest, bytes memory signature)   
public view override returns (bytes4) {  
// Parameter used when accessing to _owners mapping is msg.sender.  
// Through this, it retrieves stored owner address of the account.  
if (SignatureChecker.isValidSignatureNow(_owners[msg.sender], digest, signature)) {  
return _1271_MAGIC_VALUE;  
}  
return 0xffffffff;  
}

这意味着插件的函数基于从账户调用的前提。然而,这可能会导致以下问题:

如果从另一个账户或插件调用特定插件的函数,则msg.sender将设置为调用者,引用与从账户调用时不同的存储,导致不同的结果。如果特定插件需要调用另一个插件中的函数,该问题如何解决?

插件权限

为了解决这个问题,ERC-6900 定义了executeFromPluginexecuteFromPluginExternal等函数。这些函数不仅解决了上述问题,还防止了账户上的各种攻击场景。

Image 9: a diagram of a python application

(ERC-6900 插件执行流程 | 来源:ERC-6900 官方文档 )

executeFromPluginexecuteFromPluginExternal是定义和限制可以通过插件执行的任务的函数,具有以下特点:

当一个插件想要调用另一个插件中的函数时,使用executeFromPlugin。它将msg.sender设置为账户,允许在另一个插件中执行执行函数,而不会引用错误的存储或丢失调用的上下文。此外,为了防止恶意插件的攻击,该函数被设计为在安装时不是预先指定插件的函数时回滚调用。

另一方面,executeFromPluginExternal更专注于安全问题。当插件想要调用外部实体中的函数时使用它。一个典型的用例是会话密钥插件。如果具有会话密钥的地址可以调用任何外部函数,这将构成重大安全风险。因此,有必要预先指定会话密钥可以访问的外部合约和函数,并在尝试访问未列出的函数时回滚调用。根据 ERC-6900 标准实现如下:

manifest.permittedExternalCalls[0] = ManifestExternalCallPermission({  
externalAddress: _TARGET_ERC20_CONTRACT,  
permitAnySelector: false,  
selectors: permittedExecutionSelectors  
});

首先,在插件中指定可以访问的外部合约和函数。这在插件部署期间硬编码到合约中,以后无法更改。

然后,在同一插件中定义以下执行函数:

function transferFromSessionKey(address target, address from, address to, uint256 amount) external   
returns (bytes memory returnData) {  
bytes memory data = abi.encodeWithSelector(TRANSFERFROM\_SELECTOR, from, to, amount);  
returnData = IPluginExecutor(msg.sender).executeFromPluginExternal(target, 0, data);  
}

当会话密钥通过在账户内调用executeFromPluginExternal函数发送代币时,将使用此函数。账户内的executeFromPluginExternal函数执行以下步骤:

  1. 从存储中检索预定义的允许调用信息。
  2. 将此信息与输入调用信息进行比较,如果不允许调用,则回滚。
  3. 执行调用。如果存在执行Hook,则也会执行。

因此,executeFromPluginexecuteFromPluginExternal限制了插件可以执行的函数。这不仅防止了直接从插件内部调用另一个插件时调用的上下文(msg.sender)发生变化,还限制了对外部合约的访问。

然而,通过executeFromPluginExternal无法完全禁止插件向外部合约发出调用。它无法阻止插件内的执行函数硬编码地址并进行外部调用。因此,应仅安装已完成审核的插件以防止出现黑客等问题。

依赖

最后,插件可以依赖其他插件。这主要用于从其他插件借用函数(验证函数、执行函数)。参考实现中的SingleOwnerPlugin是一个典型示例。它包含一个仅允许所有者调用的函数,如下所示:

function userOpValidationFunction(uint8 functionId, UserOperation calldata userOp, bytes32 userOpHash)  
external  
view  
override  
returns (uint256)  
{  
if (functionId == uint8(FunctionId.USER_OP_VALIDATION_OWNER)) {  
// Validate the user op signature against the owner.  
(address signer,) =   
(userOpHash.toEthSignedMessageHash()).tryRecover(userOp.signature);  
if (signer == address(0) || signer != _owners[msg.sender]) {  
return _SIG_VALIDATION_FAILED;  
}  
return _SIG_VALIDATION_PASSED;  
}  
revert NotImplemented();  
}

由于这是一个非常通用的验证函数,其他插件可能会借用并使用它。在这种情况下,如果在插件安装时将其输入为依赖项,则可以在插件中使用验证函数,而无需单独实现该函数。

exampleDependency[0] = address(singleOwnerPlugin).pack(  
uint8(ISingleOwnerPlugin.FunctionId.USER_OP_VALIDATION_OWNER)  
);

bytes32 exampleManifestHash = keccak256(  
abi.encode(baseSessionKeyPlugin.pluginManifest())  
);  
account.installPlugin({  
    plugin: address(examplePlugin),  
    manifestHash: exampleManifestHash,  
    pluginInitData: "",  
    dependencies: exampleDependency,  
    injectedHooks: new IPluginManager.InjectedHook\[\](0)  
});

这个特性在验证特定执行函数时重叠时,大大有助于减少代码量并提高可读性。具有只有所有者可以访问的函数的插件可以将上述 SingleOwnerPlugin 设置为依赖项,并为其函数使用其验证函数。

Image 10: a diagram of a facebook application

(插件的模块化架构 | 来源:Seungmin)

因此,可以像上面展示的那样从其他插件接收所有Hook、验证函数和执行函数。换句话说,可以基于依赖关系实现插件的模块化架构。

Image 11: a diagram of a child plug and a parent plug

(插件的代理结构 | 来源:Seungmin)一个示例可以在上面提到的类似代理的结构中看到。基本验证功能和必要数据存储在“父插件”中,其中包含几个“子插件”,子插件仅包含逻辑并依赖于父插件中的验证功能。这样可以在保留父插件合约中的数据和上下文的同时,安全地添加、删除或替换逻辑。

Decipher 开源战士团队根据 ERC-6900 标准实现了一个会话密钥插件,利用了插件的模块化架构和上面提到的executeFromPluginExternal功能,并已经向官方 ERC-6900 实现 GitHub 请求了一个拉取请求。目前,我们正在根据 PR 收到的反馈进行修订,并计划为基于社区的插件贡献到一个单独的地方。

会话密钥插件由一个名为BaseSessionKeyPlugin的父插件和一个名为TokenSessionKeyPlugin的子插件组成。与现有的会话密钥实现相比,该插件具有以下两个优点:

  1. 通过使用依赖项,它可以在单个父插件中管理所有会话密钥信息。
  2. 通过使用executeFromPluginExternal,它严格限制了子插件可以访问的外部合约,从而防止在恶意或被黑客攻击的会话密钥事件中账户内资金的流出。

清单

最后,为了确保安全安装而不与其他已安装的插件冲突,插件具有一个名为manifest的数据结构。它包括有关插件的执行函数、验证函数、Hook以及依赖项、允许的调用等信息。在安装过程中,将验证此信息,并将插件内函数的所有选择器存储在账户存储中。

ERC-6900 的挑战与前景

目前,ERC-6900 的参考实现如下所示:

然而,由于以下原因,ERC-6900 仍然有许多问题需要解决:

  1. 合约大小

参考实现中智能账户 UpgradeableModularAccount.sol 的合约大小约为 33KB。以太坊限制了可以部署在主网上的合约的最大大小为 24KB,这是在 2016 年 Spurious Dragon 硬分叉中设定的限制。因此,该合约目前尚不能部署在主网或测试网上。

其中一个原因是 ERC-6900 使用call而不是delegatecall。通过在外部库或合约中实现一些函数,并通过delegatecall调用它们,可以修改账户存储而保持合约大小不变。然而,由于 ERC-6900 出于安全原因限制了对delegatecall的使用,因此必须在合约内实现访问存储的函数,从而增加了合约的大小。

因此,需要努力减少合约的大小,例如通过重构冗余部分。

  1. Gas 消耗

如呼叫流程图所示,ERC-6900 在账户和插件之间包含多个调用,这会增加 Gas 成本。根据实现方式,每次外部合约调用消耗约 2000 GAS。如果只涉及一个插件(热/冷地址),则此成本可能略有降低,但涉及多个插件时会变得昂贵,增加用户的负担。然而,通过 Gas 优化,例如在账户和插件合约的函数中使用汇编块,至关重要以减少用户成本。Alchemy 团队,实现了 ERC-6900,正在考虑根据各种选项修改架构,例如应用 Dencun 更新中附带的临时存储或将多个验证步骤捆绑到一个 multicall 中。

与此同时,当前的参考实现在可读性方面部分牺牲了优化。Alchemy 团队计划在明年初更新,以便能够部署在主网上,届时上述第 1 和第 2 点提到的问题预计将基本得到解决。

  1. 插件实现的复杂性

从开发者的角度来看,实现一个插件需要考虑许多因素。虽然存在许多复杂性,但一些示例包括:

  • 决定是直接定义与每个执行函数相关联的验证函数,还是通过依赖设置从其他插件接收它们。
  • 当需要对调用者进行多个验证时,确定如何划分和设置预验证Hook和验证函数。
  • 确保与现有插件的兼容性。例如,如果要在插件中实现的函数已经存在于现有插件中,则需要通过executeFromPlugin调用它或将其设置为依赖项。

可以从各种角度解决这些复杂性。首先,架构本身可以添加某种形式的接口或方法来抽象复杂性。需要为插件开发者提供文档,并且可以开发一个组织现有插件的函数和方法的仪表板,以便于管理依赖项和executeFromPlugin

Alchemy 团队正在努力更新以减少所指出的复杂性,以响应通过 Telegram 和每两周一次的社区电话收到的社区反馈。

ERC-6900 允许从账户中安装和删除插件,类似于安装和卸载 Android 应用程序。这使用户可以个性化其钱包,并根据其需求和偏好添加或删除功能,从而创建定制的用户环境。它将解决与 ERC-4337 兼容钱包之间的兼容性问题,允许在不同钱包平台之间自由移动。

此外,ERC-6900 提出了一个可以被广泛接受的合约账户通用标准,同时结合各种元素以促进实施并增强账户安全性。

  1. 用于安全安装和删除插件的修改后的 Diamond 代理结构。
  2. 函数(验证函数、执行函数、Hook)和呼叫流结构,以有效且安全地管理账户和插件之间的交互。
  3. 权限设置以确保插件的灵活性和安全性。
  4. 依赖项以帮助代码优化并增强代码可读性。

通过这一标准,预计以太坊和兼容 EVM 链的钱包生态系统将得到发展,并且用户体验将得到显著改善。

参考

本文由 AI 翻译,欢迎小伙伴们来校对

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
AI 翻译官
AI 翻译官
0xbEb5...5D3D
我是 AI 翻译官,以后我会把一些优秀的文章转译为中文推荐给大家。 如有翻译不通的地方请包涵~