Skip to main content

ERC6900

ERC-6900 vs ERC4337

alt text

ERC6900 摘要

  1. 智能合约账户的标准化: EIP-6900 的目标是标准化智能合约账户的结构,这可以带来更可预测的行为,并简化不同系统间的互操作性。

  2. 账户插件: 提案引入了“账户插件”的概念,这些是允许在智能合约账户中加入可组合逻辑的智能合约接口。这意味着可以以模块化的方式向智能合约账户添加各种功能,类似于软件应用程序中的插件。

  3. 受 ERC-2535 启发: 提案借鉴了 ERC-2535(钻石标准,支持模块化智能合约架构)的理念,用于定义更新和查询智能合约账户内模块化函数实现的接口。

  4. 模块化方法: 账户内的功能被划分为三个主要领域,并且这些功能在外部合约中实现。提案还概述了从这些模块化组件使用时账户应如何执行流程。

ERC6900 动机

  1. 扩展 ERC-4337: 虽然 ERC-4337 将执行和验证逻辑抽象到单个智能合约账户,但 EIP-6900 寻求通过标准化如何在这些账户中构建附加功能来扩展这一点。

  2. 通过定制实现功能: 提案认识到账户可以具有各种功能,如会话密钥、订阅、消费限额和基于角色的访问控制,这些目前要么内置于特定的智能合约账户中,要么通过专有插件系统实现。

  3. 用户体验和开发者努力: 提案识别出一个问题,即管理具有不同功能和安全配置的多个账户实例会导致用户体验碎片化。对于开发者来说,支持多个平台可能会导致平台锁定或需要重复的开发努力。

  4. 标准化的模块化账户: EIP-6900 提出了一个模块化智能合约账户的标准,该标准可以支持所有符合标准的插件。这将增强用户的数据可移植性,并减轻插件开发者需要承诺支持特定账户实现的需求。

总结来说,EIP-6900 旨在为智能合约账户创建一个更统一和灵活的框架,使得开发者更容易创建和实现新功能,同时通过减少碎片化和复杂性来改善用户体验。

Plugin manifest

插件清单负责描述安装期间将在 MSCA 上配置的执行函数、验证函数和钩子,以及插件的元数据、依赖项和权限。

ERC-7579 vs ERC-6900



Why we are building Kernel on ERC-7579 (and not ERC-6900)

架构推荐

alt text

https://docs.stackup.sh/docs/recommendations

谁来调用插件

在基于插件的智能合约账户系统中,插件是一种可以赋予账户额外功能的模块。这些插件可以定义新的函数,增强现有功能,或者与其他智能合约交互。当一个插件被安装到账户合约中时,它通常会注册一些函数选择器(function selectors),这些选择器与特定的函数逻辑相对应。

在这样的系统中,调用通常按以下方式进行:

  1. 用户交互:用户(或另一个合约)通过发送一个交易来调用账户合约的函数。这个交易包含了一个函数选择器和可能的参数,这些信息告诉合约需要执行哪个函数。

  2. 合约逻辑:账户合约接收到调用后,会根据传入的函数选择器确定需要执行哪个插件中的代码。这通常是通过查找一个映射(mapping)或者一个注册表(registry)来完成的,这个映射或注册表会将函数选择器与相应插件中的函数逻辑关联起来。

  3. 插件执行:一旦确定了正确的插件和函数,合约账户会执行该插件中的代码。这个过程可能涉及读取或修改合约的状态,或者与其他合约进行交互。

  4. 权限和验证:为了安全起见,合约账户可能会实施一些权限检查,以确保调用者有权执行请求的操作。这些检查可能是基于内置的权限系统,或者通过使用钩子(hooks)和验证器(validators)来实现的。

  5. 钩子和事件:在执行插件逻辑之前或之后,合约账户可能会调用一些预定义的钩子。这些钩子可以用于执行额外的检查,记录事件,或者触发其他合约的动作。

在这样的系统中,"谁来调用" 取决于交易的发起者。通常情况下,这些调用者可以是:

  • 合约所有者:拥有特定权限的用户,可以安装插件或执行特定的操作。
  • 外部用户:任何知道如何与合约交互的用户,可以调用公开的函数。
  • 其他合约:在复杂的交互模式中,一个合约可能会调用另一个合约的函数。

在基于 ERC-4337 或类似的账户抽象框架中,还可能涉及到验证者(Relayers),它们负责将用户操作打包并发送到区块链,用户操作可能会触发合约账户中的插件逻辑。

这种模块化的方法提供了极大的灵活性和可扩展性,因为它允许合约的功能随着时间的推移而增长和适应,而不必重新部署整个合约。

会不会调用到没有的插件

会,如果用户试图调用一个不存在的函数或者插件,通常会导致调用失败。在智能合约和插件系统中,通常会有以下几种方式来处理这种情况:

  1. 函数选择器检查:智能合约可以设计成在执行任何操作之前先检查函数选择器是否对应于一个有效的函数或插件。如果没有找到匹配项,合约可以优雅地拒绝交易,而不是让它失败。

  2. 错误处理:合约可以捕获错误并提供有用的反馈。例如,它可以使用 Solidity 的 requirerevertassert 语句来提供错误信息,告知用户调用失败的原因。

  3. 查询接口:合约可以提供一个查询接口,允许用户在调用之前查询可用的插件和函数。这可以通过公开的函数来完成,用户可以调用这些函数来获取当前激活的插件列表和它们提供的功能。

  4. 事件日志:智能合约可以在插件被添加或移除时发出事件。监听这些事件可以帮助用户了解合约的当前状态和可用的功能。

  5. 文档和外部工具:合约开发者通常会提供文档和外部工具,以帮助用户了解如何与合约交互,包括可用的插件和函数。

  6. 权限管理:如果某些函数或插件只对特定用户开放,合约可以实现复杂的权限管理系统来确保只有授权用户才能调用特定的功能。

  7. 用户界面:为合约提供一个友好的用户界面(如 Web 前端)可以帮助用户理解可用的操作,并防止他们意外调用不存在的函数。

  8. 代理调用:在某些高级的合约设计中,可以使用代理(proxy)模式,其中一个代理合约负责转发调用到实际的逻辑合约。如果调用失败,代理可以更优雅地处理这种情况。

通过这些方法,开发者可以最大限度地减少用户因不了解合约状态或接口而导致的错误调用。然而,这也要求合约开发者在设计时考虑到易用性和错误处理,以提供良好的用户体验。

合约账户的插件安装与获取

https://accountkit.alchemy.com/extending-smart-accounts/get-installed-plugins.html

interface

IPluginExecutor

interface IPluginExecutor {
/// @notice Execute a call from a plugin through the account.
/// @dev Permissions must be granted to the calling plugin for the call to go through.
/// @param data The calldata to send to the account.
/// @return The return data from the call.
function executeFromPlugin(bytes calldata data) external payable returns (bytes memory);

/// @notice Execute a call from a plugin to a non-plugin address.
/// @dev If the target is a plugin, the call SHOULD revert. Permissions must be granted to the calling plugin
/// for the call to go through.
/// @param target The address to be called.
/// @param value The value to send with the call.
/// @param data The calldata to send to the target.
/// @return The return data from the call.
function executeFromPluginExternal(address target, uint256 value, bytes calldata data)
external
payable
returns (bytes memory);
}

这个 IPluginExecutor 是一个智能合约的接口,定义了两个可以由外部调用的函数,它们允许合约执行与插件相关的操作。这里的 "插件" 可能是指一些能够与该合约交互的其他合约或代码模块。下面是对这两个函数的解释:

  1. executeFromPlugin(bytes calldata data) external payable returns (bytes memory)

    这个函数允许一个插件通过 IPluginExecutor 实现者的账户执行一个调用。这意味着调用者(即插件)可以利用 IPluginExecutor 实现者的权限来执行特定的操作。

    • bytes calldata data: 这是一个编码后的数据参数,包含了要执行的函数调用的信息。
    • external: 表示这个函数是为了从合约外部调用而设计的。
    • payable: 表示这个函数可以接收以太币(ETH)。
    • returns (bytes memory): 函数执行后会返回一些数据,这些数据是执行调用的结果。

    在调用这个函数之前,必须确保调用插件已经被授权。如果未被授权,调用应当会被回退。

  2. executeFromPluginExternal(address target, uint256 value, bytes calldata data) external payable returns (bytes memory)

    这个函数允许一个插件通过 IPluginExecutor 实现者的账户向非插件地址执行一个调用。

    • address target: 目标地址,即要调用的合约或账户的地址。
    • uint256 value: 随着调用发送的以太币(ETH)数量。
    • bytes calldata data: 要发送到目标地址的编码后的调用数据。
    • external: 表示这个函数是为了从合约外部调用而设计的。
    • payable: 表示这个函数可以接收以太币(ETH)。
    • returns (bytes memory): 函数执行后会返回一些数据,这些数据是执行调用的结果。

    如果目标地址是另一个插件,调用应当回退。这可能是为了防止插件之间的循环调用或其他潜在的安全问题。同样的,调用插件需要有适当的权限才能通过这种方式执行调用。

在实际应用中,这个接口可以被用作一个通用的执行器,允许插件通过实现了这个接口的合约进行交互,同时确保交互是在明确授权的前提下进行的。这样的设计模式可以帮助构建可扩展和模块化的智能合约系统。

Modular Smart Contract Accounts and Plugins

这个的意思是模块合约账户,是插件化的,一个账户多个插件构成?

是的,"Modular Smart Contract Accounts and Plugins" 这个概念指的是智能合约账户和插件之间的一种架构设计,它允许智能合约具有模块化和可扩展性。在这种架构中,智能合约账户可以被视为一个核心平台或框架,而插件则是可以附加到这个平台上的独立模块,每个插件都能增加特定的功能或行为。

这里的关键点在于:

  • 模块化(Modularity):智能合约的设计允许将复杂系统分解成更小、更易于管理的部分。每个模块(或插件)都可以独立于其他模块开发、测试和更新,这样做提高了系统的可维护性和灵活性。

  • 智能合约账户(Smart Contract Accounts):这是部署在区块链上的合约,它拥有一个地址和与之相关的状态。这些账户可以接收和发送资金,执行代码,并与其他合约进行交互。

  • 插件(Plugins):插件是附加到智能合约账户的组件,它们可以提供特定的功能,比如资产管理、投票机制或与其他合约的接口。插件使得智能合约能够在不改变核心逻辑的情况下,增加或变更功能。

  • 可扩展性(Extensibility):通过允许插件化,合约账户可以在不需要重新部署或修改现有代码的情况下添加新功能。这使得合约能够适应新的需求和条件,提高了合约的长期可用性。

例如,一个基于插件化的智能合约系统可能包含一个基础的钱包合约,它可以通过添加不同的插件来实现多签名功能、自动化交易、权限控制等高级功能。每个插件都可以作为一个独立的合约存在,只有在被钱包合约明确调用时才会执行其代码。

这种架构的优点是它允许开发者灵活地构建和扩展智能合约系统,同时也可以通过更换插件来快速适应市场和技术的变化。然而,这也要求合约的设计必须考虑到安全性和权限控制,以确保只有授权的插件可以与核心合约账户交互。

合约中abi.encodeWithSignature 与 abi.encodeWithSelector

在以太坊智能合约编程中,abi.encodeWithSignatureabi.encodeWithSelector 是两个用于编码函数调用数据的函数,它们都属于合约的抽象二进制接口(ABI)。这些函数允许开发者以一种适合在区块链上进行传输的格式来编码函数调用和参数。这些编码后的数据可以用于发起合约调用或者在合约内部进行消息传递。

abi.encodeWithSignature

abi.encodeWithSignature 函数使用函数的文本签名来编码函数调用。函数签名是由函数名和参数类型列表组成的字符串。例如,如果你有一个函数 transfer(address,uint256),它的签名就是 "transfer(address,uint256)"

function encodeWithSignatureExample() external {
// 编码一个函数调用,包括它的函数签名 "transfer(address,uint256)" 和参数
bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123);
}

在上面的代码中,abi.encodeWithSignature 会返回一个字节序列,这个字节序列代表了对函数 transfer 的调用,包括它的参数。

abi.encodeWithSelector

abi.encodeWithSelector 函数使用函数的选择器(selector)来编码函数调用。函数选择器是函数签名的Keccak-256哈希的前4个字节。选择器是函数的唯一标识符,用于在以太坊虚拟机(EVM)内部路由调用到正确的函数。

function encodeWithSelectorExample() external {
// 首先,计算函数选择器
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
// 然后,使用该选择器和参数来编码函数调用
bytes memory data = abi.encodeWithSelector(selector, 0xSomeAddress, 123);
}

在上面的例子中,abi.encodeWithSelector 使用一个已经计算好的函数选择器来编码调用。这个函数选择器是一个固定的值,与函数签名的哈希值相对应。

区别

  • abi.encodeWithSignature 需要完整的函数签名作为字符串输入,它在内部计算函数选择器。
  • abi.encodeWithSelector 直接接受预先计算好的函数选择器作为输入。

在实践中,使用 abi.encodeWithSignature 可能更方便,因为你可以直接传递函数签名,但这会在每次调用时计算哈希值,可能会略微增加gas成本。使用 abi.encodeWithSelector 可以节省这部分成本,因为选择器是预先计算好的,但它需要额外的步骤来先计算选择器。

开发者会根据具体情况选择使用哪一个,有时为了方便和可读性会选择 abi.encodeWithSignature,而在对gas优化极其重视的场景下可能会选择 abi.encodeWithSelector

interfaceID == type(IERC165).interfaceId?这个接口的id如何获取的?

在Solidity中,interfaceId 是一个类型的属性,它是由编译器自动生成的,代表了该接口的唯一标识符。这个标识符是根据ERC-165标准计算出来的,即对接口中所有函数的选择器进行异或(XOR)运算的结果。

对于任何给定的接口,你可以通过 type(InterfaceName).interfaceId 来获取其对应的接口ID。这里的 InterfaceName 是你定义的接口的名称。Solidity编译器会自动为每个接口计算出一个对应的 interfaceId

例如,假设有一个接口 IERC165,它包含一个函数 supportsInterface(bytes4 interfaceID)。这个接口的Solidity定义可能如下所示:

interface IERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

为了计算这个接口的标识符,Solidity会取这个接口中所有函数的选择器,然后进行异或运算。函数选择器是函数签名的Keccak-256哈希的前4个字节。在这个例子中,只有一个函数,所以接口ID就是这个函数选择器的值。

以下是如何在Solidity合约中获取IERC165接口ID的示例:

bytes4 public constant ierc165Id = type(IERC165).interfaceId;

这行代码会在部署合约时计算并存储IERC165接口的ID。这个ID通常是硬编码的,因为它是由接口定义确定的,不会改变。

在实际的合约中,supportsInterface 函数通常会检查传入的 interfaceID 是否与合约支持的一个或多个接口ID匹配。这使得其他合约可以通过调用 supportsInterface 方法来检查某个合约是否实现了特定的接口。

合约插件管理


// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {FunctionReferenceLib} from "../helpers/FunctionReferenceLib.sol";
import {
IPlugin,
ManifestExecutionHook,
ManifestFunction,
ManifestAssociatedFunctionType,
ManifestAssociatedFunction,
ManifestExternalCallPermission,
PluginManifest
} from "../interfaces/IPlugin.sol";
import {FunctionReference, IPluginManager} from "../interfaces/IPluginManager.sol";
import {
AccountStorage,
getAccountStorage,
SelectorData,
getPermittedCallKey,
HookGroup,
PermittedExternalCallData
} from "./AccountStorage.sol";

abstract contract PluginManagerInternals is IPluginManager {
using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
using EnumerableSet for EnumerableSet.AddressSet;
using FunctionReferenceLib for FunctionReference;

error ArrayLengthMismatch();
error ExecutionFunctionAlreadySet(bytes4 selector);
error InvalidDependenciesProvided();
error InvalidPluginManifest();
error MissingPluginDependency(address dependency);
error NullFunctionReference();
error NullPlugin();
error PluginAlreadyInstalled(address plugin);
error PluginDependencyViolation(address plugin);
error PluginInstallCallbackFailed(address plugin, bytes revertReason);
error PluginInterfaceNotSupported(address plugin);
error PluginNotInstalled(address plugin);
error RuntimeValidationFunctionAlreadySet(bytes4 selector, FunctionReference validationFunction);
error UserOpValidationFunctionAlreadySet(bytes4 selector, FunctionReference validationFunction);

modifier notNullFunction(FunctionReference functionReference) {
if (functionReference.isEmpty()) {
revert NullFunctionReference();
}
_;
}

modifier notNullPlugin(address plugin) {
if (plugin == address(0)) {
revert NullPlugin();
}
_;
}

// Storage update operations

function _setExecutionFunction(bytes4 selector, address plugin) internal notNullPlugin(plugin) {
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

if (_selectorData.plugin != address(0)) {
revert ExecutionFunctionAlreadySet(selector);
}

_selectorData.plugin = plugin;
}

function _removeExecutionFunction(bytes4 selector) internal {
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

_selectorData.plugin = address(0);
}

function _addUserOpValidationFunction(bytes4 selector, FunctionReference validationFunction)
internal
notNullFunction(validationFunction)
{
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

if (!_selectorData.userOpValidation.isEmpty()) {
revert UserOpValidationFunctionAlreadySet(selector, validationFunction);
}

_selectorData.userOpValidation = validationFunction;
}

function _removeUserOpValidationFunction(bytes4 selector, FunctionReference validationFunction)
internal
notNullFunction(validationFunction)
{
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

_selectorData.userOpValidation = FunctionReferenceLib._EMPTY_FUNCTION_REFERENCE;
}

function _addRuntimeValidationFunction(bytes4 selector, FunctionReference validationFunction)
internal
notNullFunction(validationFunction)
{
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

if (!_selectorData.runtimeValidation.isEmpty()) {
revert RuntimeValidationFunctionAlreadySet(selector, validationFunction);
}

_selectorData.runtimeValidation = validationFunction;
}

function _removeRuntimeValidationFunction(bytes4 selector, FunctionReference validationFunction)
internal
notNullFunction(validationFunction)
{
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

_selectorData.runtimeValidation = FunctionReferenceLib._EMPTY_FUNCTION_REFERENCE;
}

function _addExecHooks(bytes4 selector, FunctionReference preExecHook, FunctionReference postExecHook)
internal
{
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

_addHooks(_selectorData.executionHooks, preExecHook, postExecHook);
}

function _removeExecHooks(bytes4 selector, FunctionReference preExecHook, FunctionReference postExecHook)
internal
{
SelectorData storage _selectorData = getAccountStorage().selectorData[selector];

_removeHooks(_selectorData.executionHooks, preExecHook, postExecHook);
}

function _addHooks(HookGroup storage hooks, FunctionReference preExecHook, FunctionReference postExecHook)
internal
{
if (!preExecHook.isEmpty()) {
_addOrIncrement(hooks.preHooks, _toSetValue(preExecHook));

if (!postExecHook.isEmpty()) {
_addOrIncrement(hooks.associatedPostHooks[preExecHook], _toSetValue(postExecHook));
}
} else {
if (postExecHook.isEmpty()) {
// both pre and post hooks cannot be null
revert NullFunctionReference();
}

_addOrIncrement(hooks.postOnlyHooks, _toSetValue(postExecHook));
}
}

function _removeHooks(HookGroup storage hooks, FunctionReference preExecHook, FunctionReference postExecHook)
internal
{
if (!preExecHook.isEmpty()) {
_removeOrDecrement(hooks.preHooks, _toSetValue(preExecHook));

if (!postExecHook.isEmpty()) {
_removeOrDecrement(hooks.associatedPostHooks[preExecHook], _toSetValue(postExecHook));
}
} else {
// The case where both pre and post hooks are null was checked during installation.

// May ignore return value, as the manifest hash is validated to ensure that the hook exists.
_removeOrDecrement(hooks.postOnlyHooks, _toSetValue(postExecHook));
}
}

function _addPreUserOpValidationHook(bytes4 selector, FunctionReference preUserOpValidationHook)
internal
notNullFunction(preUserOpValidationHook)
{
_addOrIncrement(
getAccountStorage().selectorData[selector].preUserOpValidationHooks,
_toSetValue(preUserOpValidationHook)
);
}

function _removePreUserOpValidationHook(bytes4 selector, FunctionReference preUserOpValidationHook)
internal
notNullFunction(preUserOpValidationHook)
{
// May ignore return value, as the manifest hash is validated to ensure that the hook exists.
_removeOrDecrement(
getAccountStorage().selectorData[selector].preUserOpValidationHooks,
_toSetValue(preUserOpValidationHook)
);
}

function _addPreRuntimeValidationHook(bytes4 selector, FunctionReference preRuntimeValidationHook)
internal
notNullFunction(preRuntimeValidationHook)
{
_addOrIncrement(
getAccountStorage().selectorData[selector].preRuntimeValidationHooks,
_toSetValue(preRuntimeValidationHook)
);
}

function _removePreRuntimeValidationHook(bytes4 selector, FunctionReference preRuntimeValidationHook)
internal
notNullFunction(preRuntimeValidationHook)
{
// May ignore return value, as the manifest hash is validated to ensure that the hook exists.
_removeOrDecrement(
getAccountStorage().selectorData[selector].preRuntimeValidationHooks,
_toSetValue(preRuntimeValidationHook)
);
}

function _installPlugin(
address plugin,
bytes32 manifestHash,
bytes memory pluginInstallData,
FunctionReference[] memory dependencies
) internal {
AccountStorage storage _storage = getAccountStorage();

// Check if the plugin exists.
if (!_storage.plugins.add(plugin)) {
revert PluginAlreadyInstalled(plugin);
}

// Check that the plugin supports the IPlugin interface.
if (!ERC165Checker.supportsInterface(plugin, type(IPlugin).interfaceId)) {
revert PluginInterfaceNotSupported(plugin);
}

// Check manifest hash.
PluginManifest memory manifest = IPlugin(plugin).pluginManifest();
if (!_isValidPluginManifest(manifest, manifestHash)) {
revert InvalidPluginManifest();
}

// Check that the dependencies match the manifest.
if (dependencies.length != manifest.dependencyInterfaceIds.length) {
revert InvalidDependenciesProvided();
}

uint256 length = dependencies.length;
for (uint256 i = 0; i < length;) {
// Check the dependency interface id over the address of the dependency.
(address dependencyAddr,) = dependencies[i].unpack();

// Check that the dependency is installed.
if (_storage.pluginData[dependencyAddr].manifestHash == bytes32(0)) {
revert MissingPluginDependency(dependencyAddr);
}

// Check that the dependency supports the expected interface.
if (!ERC165Checker.supportsInterface(dependencyAddr, manifest.dependencyInterfaceIds[i])) {
revert InvalidDependenciesProvided();
}

// Increment the dependency's dependents counter.
_storage.pluginData[dependencyAddr].dependentCount += 1;

unchecked {
++i;
}
}

// Add the plugin metadata to the account
_storage.pluginData[plugin].manifestHash = manifestHash;
_storage.pluginData[plugin].dependencies = dependencies;

// Update components according to the manifest.

// Mark whether or not this plugin may spend native token amounts
if (manifest.canSpendNativeToken) {
_storage.pluginData[plugin].canSpendNativeToken = true;
}

length = manifest.executionFunctions.length;
for (uint256 i = 0; i < length;) {
_setExecutionFunction(manifest.executionFunctions[i], plugin);

unchecked {
++i;
}
}

// Add installed plugin and selectors this plugin can call
length = manifest.permittedExecutionSelectors.length;
for (uint256 i = 0; i < length;) {
// If there are duplicates, this will just enable the flag again. This is not a problem, since the
// boolean will be set to false twice during uninstall, which is fine.
_storage.callPermitted[getPermittedCallKey(plugin, manifest.permittedExecutionSelectors[i])] = true;

unchecked {
++i;
}
}

// Add the permitted external calls to the account.
if (manifest.permitAnyExternalAddress) {
_storage.pluginData[plugin].anyExternalExecPermitted = true;
} else {
// Only store the specific permitted external calls if "permit any" flag was not set.
length = manifest.permittedExternalCalls.length;
for (uint256 i = 0; i < length;) {
ManifestExternalCallPermission memory externalCallPermission = manifest.permittedExternalCalls[i];

PermittedExternalCallData storage permittedExternalCallData =
_storage.permittedExternalCalls[IPlugin(plugin)][externalCallPermission.externalAddress];

permittedExternalCallData.addressPermitted = true;

if (externalCallPermission.permitAnySelector) {
permittedExternalCallData.anySelectorPermitted = true;
} else {
uint256 externalContractSelectorsLength = externalCallPermission.selectors.length;
for (uint256 j = 0; j < externalContractSelectorsLength;) {
permittedExternalCallData.permittedSelectors[externalCallPermission.selectors[j]] = true;

unchecked {
++j;
}
}
}

unchecked {
++i;
}
}
}

length = manifest.userOpValidationFunctions.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mv = manifest.userOpValidationFunctions[i];
_addUserOpValidationFunction(
mv.executionSelector,
_resolveManifestFunction(
mv.associatedFunction, plugin, dependencies, ManifestAssociatedFunctionType.NONE
)
);

unchecked {
++i;
}
}

length = manifest.runtimeValidationFunctions.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mv = manifest.runtimeValidationFunctions[i];
_addRuntimeValidationFunction(
mv.executionSelector,
_resolveManifestFunction(
mv.associatedFunction,
plugin,
dependencies,
ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW
)
);

unchecked {
++i;
}
}

// Hooks are not allowed to be provided as dependencies, so we use an empty array for resolving them.
FunctionReference[] memory emptyDependencies;

length = manifest.preUserOpValidationHooks.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mh = manifest.preUserOpValidationHooks[i];
_addPreUserOpValidationHook(
mh.executionSelector,
_resolveManifestFunction(
mh.associatedFunction,
plugin,
emptyDependencies,
ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY
)
);

unchecked {
++i;
}
}

length = manifest.preRuntimeValidationHooks.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mh = manifest.preRuntimeValidationHooks[i];
_addPreRuntimeValidationHook(
mh.executionSelector,
_resolveManifestFunction(
mh.associatedFunction,
plugin,
emptyDependencies,
ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY
)
);
unchecked {
++i;
}
}

length = manifest.executionHooks.length;
for (uint256 i = 0; i < length;) {
ManifestExecutionHook memory mh = manifest.executionHooks[i];
_addExecHooks(
mh.executionSelector,
_resolveManifestFunction(
mh.preExecHook, plugin, emptyDependencies, ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY
),
_resolveManifestFunction(
mh.postExecHook, plugin, emptyDependencies, ManifestAssociatedFunctionType.NONE
)
);

unchecked {
++i;
}
}

length = manifest.interfaceIds.length;
for (uint256 i = 0; i < length;) {
_storage.supportedIfaces[manifest.interfaceIds[i]] += 1;
unchecked {
++i;
}
}

// Initialize the plugin storage for the account.
// solhint-disable-next-line no-empty-blocks
try IPlugin(plugin).onInstall(pluginInstallData) {}
catch (bytes memory revertReason) {
revert PluginInstallCallbackFailed(plugin, revertReason);
}

emit PluginInstalled(plugin, manifestHash, dependencies);
}

function _uninstallPlugin(address plugin, PluginManifest memory manifest, bytes memory uninstallData)
internal
{
AccountStorage storage _storage = getAccountStorage();

// Check if the plugin exists.
if (!_storage.plugins.remove(plugin)) {
revert PluginNotInstalled(plugin);
}

// Check manifest hash.
bytes32 manifestHash = _storage.pluginData[plugin].manifestHash;
if (!_isValidPluginManifest(manifest, manifestHash)) {
revert InvalidPluginManifest();
}

// Ensure that there are no dependent plugins.
if (_storage.pluginData[plugin].dependentCount != 0) {
revert PluginDependencyViolation(plugin);
}

// Remove this plugin as a dependent from its dependencies.
FunctionReference[] memory dependencies = _storage.pluginData[plugin].dependencies;
uint256 length = dependencies.length;
for (uint256 i = 0; i < length;) {
FunctionReference dependency = dependencies[i];
(address dependencyAddr,) = dependency.unpack();

// Decrement the dependent count for the dependency function.
_storage.pluginData[dependencyAddr].dependentCount -= 1;

unchecked {
++i;
}
}

// Remove components according to the manifest, in reverse order (by component type) of their installation.

// Hooks are not allowed to be provided as dependencies, so we use an empty array for resolving them.
FunctionReference[] memory emptyDependencies;

length = manifest.executionHooks.length;
for (uint256 i = 0; i < length;) {
ManifestExecutionHook memory mh = manifest.executionHooks[i];
_removeExecHooks(
mh.executionSelector,
_resolveManifestFunction(
mh.preExecHook, plugin, emptyDependencies, ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY
),
_resolveManifestFunction(
mh.postExecHook, plugin, emptyDependencies, ManifestAssociatedFunctionType.NONE
)
);

unchecked {
++i;
}
}

length = manifest.preRuntimeValidationHooks.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mh = manifest.preRuntimeValidationHooks[i];
_removePreRuntimeValidationHook(
mh.executionSelector,
_resolveManifestFunction(
mh.associatedFunction,
plugin,
emptyDependencies,
ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY
)
);

unchecked {
++i;
}
}

length = manifest.preUserOpValidationHooks.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mh = manifest.preUserOpValidationHooks[i];
_removePreUserOpValidationHook(
mh.executionSelector,
_resolveManifestFunction(
mh.associatedFunction,
plugin,
emptyDependencies,
ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY
)
);

unchecked {
++i;
}
}

length = manifest.runtimeValidationFunctions.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mv = manifest.runtimeValidationFunctions[i];
_removeRuntimeValidationFunction(
mv.executionSelector,
_resolveManifestFunction(
mv.associatedFunction,
plugin,
dependencies,
ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW
)
);

unchecked {
++i;
}
}

length = manifest.userOpValidationFunctions.length;
for (uint256 i = 0; i < length;) {
ManifestAssociatedFunction memory mv = manifest.userOpValidationFunctions[i];
_removeUserOpValidationFunction(
mv.executionSelector,
_resolveManifestFunction(
mv.associatedFunction, plugin, dependencies, ManifestAssociatedFunctionType.NONE
)
);

unchecked {
++i;
}
}

// remove external call permissions

if (manifest.permitAnyExternalAddress) {
// Only clear if it was set during install time
_storage.pluginData[plugin].anyExternalExecPermitted = false;
} else {
// Only clear the specific permitted external calls if "permit any" flag was not set.
length = manifest.permittedExternalCalls.length;
for (uint256 i = 0; i < length;) {
ManifestExternalCallPermission memory externalCallPermission = manifest.permittedExternalCalls[i];

PermittedExternalCallData storage permittedExternalCallData =
_storage.permittedExternalCalls[IPlugin(plugin)][externalCallPermission.externalAddress];

permittedExternalCallData.addressPermitted = false;

// Only clear this flag if it was set in the constructor.
if (externalCallPermission.permitAnySelector) {
permittedExternalCallData.anySelectorPermitted = false;
} else {
uint256 externalContractSelectorsLength = externalCallPermission.selectors.length;
for (uint256 j = 0; j < externalContractSelectorsLength;) {
permittedExternalCallData.permittedSelectors[externalCallPermission.selectors[j]] = false;

unchecked {
++j;
}
}
}

unchecked {
++i;
}
}
}

length = manifest.permittedExecutionSelectors.length;
for (uint256 i = 0; i < length;) {
_storage.callPermitted[getPermittedCallKey(plugin, manifest.permittedExecutionSelectors[i])] = false;

unchecked {
++i;
}
}

length = manifest.executionFunctions.length;
for (uint256 i = 0; i < length;) {
_removeExecutionFunction(manifest.executionFunctions[i]);

unchecked {
++i;
}
}

length = manifest.interfaceIds.length;
for (uint256 i = 0; i < length;) {
_storage.supportedIfaces[manifest.interfaceIds[i]] -= 1;
unchecked {
++i;
}
}

// Remove the plugin metadata from the account.
delete _storage.pluginData[plugin];

// Clear the plugin storage for the account.
bool onUninstallSuccess = true;
// solhint-disable-next-line no-empty-blocks
try IPlugin(plugin).onUninstall(uninstallData) {}
catch {
onUninstallSuccess = false;
}

emit PluginUninstalled(plugin, onUninstallSuccess);
}

function _addOrIncrement(EnumerableMap.Bytes32ToUintMap storage map, bytes32 key) internal {
(bool success, uint256 value) = map.tryGet(key);
map.set(key, success ? value + 1 : 0);
}

/// @return True if the key was removed or its value was decremented, false if the key was not found.
function _removeOrDecrement(EnumerableMap.Bytes32ToUintMap storage map, bytes32 key) internal returns (bool) {
(bool success, uint256 value) = map.tryGet(key);
if (!success) {
return false;
}
if (value == 0) {
map.remove(key);
} else {
map.set(key, value - 1);
}
return true;
}

function _toSetValue(FunctionReference functionReference) internal pure returns (bytes32) {
return bytes32(FunctionReference.unwrap(functionReference));
}

function _toFunctionReference(bytes32 setValue) internal pure returns (FunctionReference) {
return FunctionReference.wrap(bytes21(setValue));
}

function _isValidPluginManifest(PluginManifest memory manifest, bytes32 manifestHash)
internal
pure
returns (bool)
{
return manifestHash == keccak256(abi.encode(manifest));
}

function _resolveManifestFunction(
ManifestFunction memory manifestFunction,
address plugin,
FunctionReference[] memory dependencies,
// Indicates which magic value, if any, is permissible for the function to resolve.
ManifestAssociatedFunctionType allowedMagicValue
) internal pure returns (FunctionReference) {
if (manifestFunction.functionType == ManifestAssociatedFunctionType.SELF) {
return FunctionReferenceLib.pack(plugin, manifestFunction.functionId);
} else if (manifestFunction.functionType == ManifestAssociatedFunctionType.DEPENDENCY) {
if (manifestFunction.dependencyIndex >= dependencies.length) {
revert InvalidPluginManifest();
}
return dependencies[manifestFunction.dependencyIndex];
} else if (manifestFunction.functionType == ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW)
{
if (allowedMagicValue == ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW) {
return FunctionReferenceLib._RUNTIME_VALIDATION_ALWAYS_ALLOW;
} else {
revert InvalidPluginManifest();
}
} else if (manifestFunction.functionType == ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY) {
if (allowedMagicValue == ManifestAssociatedFunctionType.PRE_HOOK_ALWAYS_DENY) {
return FunctionReferenceLib._PRE_HOOK_ALWAYS_DENY;
} else {
revert InvalidPluginManifest();
}
}
return FunctionReferenceLib._EMPTY_FUNCTION_REFERENCE; // Empty checks are done elsewhere
}
}


// 插件安装做了什么,插件卸载做了什么?

这段代码是一个智能合约,用于管理插件的安装和卸载。它是一个较为复杂的合约,使用了OpenZeppelin的库和自定义的数据结构和库。下面是对代码中关键函数的分析,以及它们在插件安装和卸载时执行的操作。

插件安装 _installPlugin

当一个插件被安装时,以下步骤会被执行:

  1. 插件存在性检查:使用EnumerableSet检查插件是否已经被添加到插件集合中。如果已经存在,则抛出PluginAlreadyInstalled错误。

  2. 插件接口支持检查:使用ERC165Checker检查插件是否实现了IPlugin接口。如果没有实现,抛出PluginInterfaceNotSupported错误。

  3. 清单哈希验证:验证提供的插件清单哈希是否与插件实际返回的清单哈希相匹配。如果不匹配,抛出InvalidPluginManifest错误。

  4. 依赖性验证:验证插件的依赖项是否满足清单中声明的接口ID。如果依赖项无效或缺失,抛出相应的错误。

  5. 更新内部存储:更新内部存储以反映插件的安装状态,包括设置执行函数、用户操作验证函数、运行时验证函数和执行钩子。

  6. 权限设置:设置插件的外部调用权限,包括允许调用的合约地址和选择器。

  7. 插件初始化:调用插件的onInstall函数以进行初始化。如果调用失败,抛出PluginInstallCallbackFailed错误。

  8. 事件触发:触发一个PluginInstalled事件,记录插件安装的相关信息。

插件卸载 _uninstallPlugin

当一个插件被卸载时,以下步骤会被执行:

  1. 插件存在性检查:检查插件是否存在。如果不存在,抛出PluginNotInstalled错误。

  2. 清单哈希验证:验证存储的插件清单哈希是否与提供的清单哈希相匹配。如果不匹配,抛出InvalidPluginManifest错误。

  3. 依赖性检查:确保没有其他插件依赖于即将卸载的插件。如果有依赖,抛出PluginDependencyViolation错误。

  4. 移除依赖关系:更新依赖于被卸载插件的其他插件的依赖计数。

  5. 清除内部存储:清除与插件相关的所有内部存储,包括移除执行函数、用户操作验证函数、运行时验证函数和执行钩子。

  6. 清除权限设置:清除插件的外部调用权限。

  7. 事件触发:触发一个PluginUninstalled事件,记录插件卸载的相关信息。

  8. 插件清理:调用插件的onUninstall函数以进行清理。如果调用失败,事件记录将标记卸载不成功。

PluginManagerInternals 合约做了什么

PluginManagerInternalsUpgradeableModularAccount 合约的一部分,它是一个智能合约,用于在以太坊区块链上实现一个可升级的、模块化的账户系统。这个系统允许用户通过插件来扩展账户的功能,这些插件可以安装、卸载和管理。

UpgradeableModularAccount 合约中,PluginManagerInternals 负责提供与插件管理相关的内部功能。这些功能通常包括:

  1. 插件安装(_installPlugin:这个函数处理插件的安装逻辑,包括验证插件的清单哈希、执行插件的安装数据,并且管理插件的依赖项。

  2. 插件卸载(_uninstallPlugin:这个函数处理插件的卸载逻辑,包括执行插件的卸载数据,并清理与插件相关的状态和依赖关系。

  3. 插件执行(executeFromPluginexecuteFromPluginExternal:这些函数允许已经安装的插件在账户的上下文中执行操作。executeFromPlugin 是用于在合约内部执行操作,而 executeFromPluginExternal 允许插件执行对外部合约的调用。

  4. 权限和钩子管理PluginManagerInternals 还可能涉及到管理权限和钩子(hooks),这些钩子在执行操作之前或之后运行,允许插件在账户的操作执行过程中介入。

  5. 升级逻辑(_authorizeUpgrade:虽然这个函数在 PluginManagerInternals 中是空的,但它是 UUPSUpgradeable 模式的一部分,用于授权合约的升级。在实际的合约中,这个函数可能会包含必要的权限检查,以确保只有授权的实体可以升级合约。

UpgradeableModularAccount 合约使用了 OpenZeppelin 的 UUPSUpgradeable 模式,这是一种代理模式,允许智能合约在保持相同地址的同时升级其逻辑。这意味着用户可以保留他们的账户地址,同时更新账户的功能和逻辑。

此外,合约使用了 OpenZeppelin 的 EnumerableMapEnumerableSet 工具,这些工具提供了可枚举映射和集合的实现,使得可以安全、高效地追踪和操作键值对和集合。

总体来说,PluginManagerInternalsUpgradeableModularAccount 合约的核心部分,它封装了与插件管理相关的逻辑,以支持合约的可扩展性和可升级性。

一个插件是如何触发安装和卸载呢?

UpgradeableModularAccount 合约中,插件的安装和卸载是通过特定的函数来触发的。这些函数是 installPluginuninstallPlugin,它们都是 IPluginManager 接口的一部分。以下是这些函数的基本工作原理:

安装插件(installPlugin

当你想要安装一个新插件时,你会调用 installPlugin 函数。这个函数接收以下参数:

  • plugin:要安装的插件的地址。
  • manifestHash:插件清单的哈希值,用于验证插件的完整性。
  • pluginInstallData:安装插件所需的任何数据。
  • dependencies:一个 FunctionReference[] 数组,列出了插件的依赖函数引用。

installPlugin 函数在 wrapNativeFunction 修饰符的上下文中被调用,这意味着在安装插件之前,会执行运行时验证以及任何预执行钩子(pre-execution hooks)。在插件安装逻辑完成后,任何后执行钩子(post-execution hooks)也会被运行。

卸载插件(uninstallPlugin

当你想要卸载一个插件时,你会调用 uninstallPlugin 函数。这个函数接收以下参数:

  • plugin:要卸载的插件的地址。
  • config:卸载插件时使用的配置数据。
  • pluginUninstallData:卸载插件所需的任何数据。

installPlugin 类似,uninstallPlugin 函数也使用 wrapNativeFunction 修饰符,这意味着在卸载插件之前,同样会执行运行时验证和任何预执行钩子,而在卸载逻辑完成后,会执行任何后执行钩子。

实际的触发

这些函数通常由合约的所有者或有权限的用户调用。在实际的使用场景中,可能会有一个前端应用程序与这个智能合约交互,允许用户通过用户界面选择和管理插件。或者,这些函数可能会被另一个智能合约调用,作为更大的去中心化应用(DApp)的一部分。

安全性

因为安装和卸载插件可能会对合约的行为和安全性产生重大影响,所以这些操作通常会有权限检查来确保只有授权的实体可以执行这些操作。这通常是通过合约的访问控制机制来实现的,例如 OpenZeppelin 的 OwnableAccessControl 模块。

监听事件

合约可能会发出事件来通知监听者关于插件安装和卸载的操作。例如,当一个插件被安装时,可能会发出一个 PluginInstalled 事件,而当一个插件被卸载时,可能会发出一个 PluginUninstalled 事件。这些事件可以被前端应用程序或其他智能合约捕获,以响应这些变化。

所谓:“插件安装” 只是记录插件信息

这并非真实触发插件安装,插件是合约,插件合约在部署后与模块合约账号之间没有联系,UpgradeableModularAccount是通过install来建立联系? 是的,你的理解是正确的。在以太坊这样的智能合约平台上,插件(通常也是智能合约)在部署到区块链上后,本身是孤立存在的;它们并不自动与其他合约建立联系。为了使这些插件能够与 UpgradeableModularAccount 合约(或任何其他类型的模块化智能合约系统)交互,需要某种机制来建立这种联系。

installPlugin 函数正是用于在 UpgradeableModularAccount 合约和插件合约之间建立这种联系的机制。当调用这个函数时,它会执行必要的逻辑来注册插件合约,可能包括以下几个步骤:

  1. 验证: 确认插件合约是预期的合约,可能通过代码哈希、清单哈希或其他机制。

  2. 授权: 确保调用者有权安装插件,通常是合约的所有者或具有特定权限的用户。

  3. 记录: 在 UpgradeableModularAccount 合约内部记录插件的信息,这可能包括插件的地址、相关的状态变量、依赖关系等。

  4. 初始化: 如果插件需要初始化数据或状态,installPlugin 过程可能会包括调用插件合约的初始化函数。

  5. 事件: 发出事件来记录插件的安装,这可以让区块链上的监听者知道插件已被安装。

通过这种方式,UpgradeableModularAccount 合约可以动态地增加新的功能和行为,而无需重新部署整个合约。这种模式提高了合约的灵活性,并允许合约的功能随着时间的推移而扩展和演化。

uninstallPlugin 函数则是用于断开这种联系的机制,它会从 UpgradeableModularAccount 合约中移除插件的注册信息,并执行任何必要的清理工作,如调用插件合约的卸载逻辑,确保插件被安全地移除。

总结

在安装过程中,合约会设置与插件相关的各种执行函数、验证函数、钩子和权限。同时,它也会初始化插件并将其添加到内部插件集合中。

在卸载过程中,合约会做相反的操作:移除这些函数和钩子,清除权限设置,减少依赖计数,最后调用插件的清理函数,并从内部集合中移除插件。

这个过程确保了插件的生命周期管理是清晰和一致的,同时保护了合约的完整性和安全性。

参考链接

  1. https://github.com/erc6900/reference-implementation
  2. https://docs.alchemy.com/docs/account-abstraction-overview
  3. https://docs.zerodev.app/
  4. https://github.com/zerodevapp/kernel
  5. https://eips.ethereum.org/EIPS/eip-165
  6. https://docs.stackup.sh/docs/account-abstraction
  7. https://docs.stackup.sh/docs/recommendations
  8. https://uniswap.org/developers
  9. https://hardhat.org/hardhat-runner/docs/getting-started