Skip to main content

ERC6900 reference-implementation

自定义的插件分几块内容

定义插件的接口 from src/samples/plugins/interfaces/ISessionKeyPlugin.sol
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface enum ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface event ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface error ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface funs ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

插件接口的实现 from src/samples/plugins/ModularSessionKeyPlugin.sol

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin view functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


// ┏━━━━━━━━━━━━━━━┓
// ┃ EIP-165 ┃
// ┗━━━━━━━━━━━━━━━┛


// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Internal / Private functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

6900-reference-implementation

https://github.com/erc6900/reference-implementation

AccountExecutor.sol
AccountLoupe.sol 账户放大镜(很形象,拿着放大镜仔细看看账户里有什么)
AccountStorage.sol
AccountStorageInitializable.sol
PluginManagerInternals.sol
UpgradeableModularAccount.sol

以下是每个合约可能的作用:

  1. AccountExecutor.sol: 这个合约可能是负责执行与账户相关的操作,比如转账、调用其他合约等。Executor 通常指代执行事务的实体,所以这个合约可能包含了一些核心的逻辑,用于处理账户的交易。

  2. AccountLoupe.sol: Loupe 在编程和智能合约的语境中通常指的是审视或检查工具。这个合约可能提供了一些只读的功能,用来检查账户的状态,例如余额、授权情况、交易历史等。

  3. AccountStorage.sol: 这个合约很可能是用来存储账户的状态,比如余额、授权信息和其他重要数据。Storage 合约通常与逻辑合约分离,以便在升级合约时保留状态。

  4. AccountStorageInitializable.sol: 这个合约可能是 AccountStorage 的一个变体,它添加了初始化功能。在可升级合约的模式中,初始化功能通常用来替代构造函数,确保合约在被代理合约使用时能正确地设置其初始状态。

  5. PluginManagerInternals.sol: 这个合约可能包含了插件管理的内部逻辑。在一个模块化和可扩展的系统中,PluginManager 可能用于管理各种插件的生命周期,比如安装、更新、卸载等。

  6. UpgradeableModularAccount.sol: 这个合约可能是一个可升级的账户合约,它支持模块化设计。这意味着合约的功能可以通过添加或替换模块来扩展,同时还支持通过某种升级机制来更新合约的逻辑而不丢失状态。

用于管理用户账户、权限、以及与特定功能模块的交互。在实际的应用中,这样的系统可能用于去中心化金融(DeFi)平台、多签钱包、企业级区块链解决方案等场景。

AccountExecutor.sol

  • 这段代码定义了一个名为 AccountExecutor 的抽象合约,它包含了一个名为 _exec 的内部函数,用于向其他合约发送调用。
  • 合约的主要目的是确保调用的目标不是一个插件,以避免数据不一致和意外行为。
  • 如果目标合约是一个插件(通过支持 IPlugin 接口来判断),那么调用会被拒绝,并抛出 PluginExecutionDenied 错误。

以下是 _exec 函数的工作原理:

  1. 检查插件接口: 使用 OpenZeppelin 的 ERC165Checker 工具,_exec 函数首先检查目标地址是否支持 IPlugin 接口。如果目标合约被认定为插件,函数会立即中断执行并抛出 PluginExecutionDenied 错误。

  2. 调用执行: 如果目标不是插件,函数会继续使用 call 低级函数向目标合约发送调用。调用时,可以附带 value(以太)和 data(调用数据)。

  3. 错误处理: 如果调用失败(例如,如果被调用的合约函数中断执行并回退),_exec 函数会使用 assembly 块来直接将错误信息冒泡上来。这意味着调用者会收到原始的错误信息,而不是通用的错误。

assembly 块使用 "memory-safe" 指令来确保操作是内存安全的,这是 Solidity 0.8.x 版本引入的一个特性。assembly 块中的 revert 指令用于抛出错误,它使用 result 中的数据作为错误信息。add(result, 32) 是错误信息的起始位置,mload(result) 是错误信息的长度。

总结来说,AccountExecutor 合约提供了一种安全的执行机制,以确保智能合约系统中的模块化账户不会意外地直接调用插件,从而保持系统的完整性和预期行为。

AccountLoupe.sol

这段代码定义了一个名为 AccountLoupe 的抽象合约,它实现了 IAccountLoupe 接口。该合约似乎是一个工具合约,用于检索有关账户执行功能配置、执行钩子(hooks)以及已安装插件的信息。它使用了 OpenZeppelin 的 UUPSUpgradeable 合约,这表明它是设计为可升级的,而且它与插件管理器有关,因为它引用了 IPluginManager 接口。

以下是合约的主要部分及其可能的作用:

  1. getExecutionFunctionConfig: 这个函数根据传入的选择器(函数签名的前 4 个字节)返回执行函数的配置。这包括关联的插件地址、用户操作验证函数和运行时验证函数。它检查选择器是否对应于一组预定义的内部函数或插件管理器函数,如果不是,则从存储中检索相关数据。

  2. getExecutionHooks: 这个函数返回与特定选择器相关联的执行钩子,包括执行前和执行后的钩子。这些钩子可能用于执行额外的逻辑,例如验证、状态更新或事件记录。

  3. getPreValidationHooks: 这个函数返回与特定选择器相关联的预验证钩子,包括用户操作预验证钩子和运行时预验证钩子。这些可能用于在实际执行操作之前进行一些检查。

  4. getInstalledPlugins: 这个函数返回所有已安装插件的地址数组。这可能用于检索合约当前使用的所有插件,以便进行管理或审计。

此外,合约使用了 OpenZeppelin 的 EnumerableMapEnumerableSet 实用库来管理映射和集合,这些库提供了迭代映射和集合的功能。

错误处理:

  • ManifestDiscrepancy: 如果插件地址与预期不符,这个错误将被抛出。

合约使用了 assembly 块来调整动态数组的大小,这是一个低级操作,通常用于优化目的。

总结来说,AccountLoupe 合约是一个为账户提供只读访问权限的工具合约,它允许外部调用者检索有关账户执行策略和已安装插件的详细信息。这种类型的合约在复杂的智能合约系统中很有用,尤其是在需要透明度和可升级性的去中心化应用程序中。

AccountStorage.sol

这段代码是一个Solidity智能合约的一部分,它定义了一系列结构体(structs),这些结构体用于表示与模块化和可升级的以太坊账户相关的复杂状态。这个合约利用了 OpenZeppelin 的 EnumerableMapEnumerableSet 库来管理集合和映射,以便能够枚举它们的元素。

让我们逐一看看每个结构体和函数:

结构体(Structs)

  1. PluginData:

    • anyExternalExecPermitted: 表示插件是否允许任何外部执行。
    • canSpendNativeToken: 表示插件是否可以花费原生代币。
    • manifestHash: 插件清单的哈希值。
    • dependencies: 插件依赖的函数引用数组。
    • dependentCount: 作为依赖函数使用的次数。
  2. PermittedExternalCallData:

    • addressPermitted: 指定地址是否允许。
    • anySelectorPermitted: 是否允许任何选择器。
    • permittedSelectors: 允许的选择器映射。
  3. HookGroup:

    • preHooks: 执行前的钩子。
    • associatedPostHooks: 与执行前钩子相关联的执行后钩子。
    • postOnlyHooks: 仅在执行后的钩子。
  4. SelectorData:

    • plugin: 实现此执行函数的插件地址。
    • userOpValidation: 用户操作验证函数引用。
    • runtimeValidation: 运行时验证函数引用。
    • preUserOpValidationHooks: 执行前用户操作验证钩子。
    • preRuntimeValidationHooks: 执行前运行时验证钩子。
    • executionHooks: 执行函数的钩子。
  5. AccountStorage:

    • initializedinitializing: 用于初始化状态的变量。
    • plugins: 插件地址集合。
    • pluginData: 插件数据映射。
    • selectorData: 选择器数据映射。
    • callPermitted: 允许的调用映射。
    • permittedExternalCalls: 允许的外部调用映射。
    • supportedIfaces: 用于ERC165自省的接口支持映射。

函数

  1. getAccountStorage: 使用内联汇编和 memory-safe 指令,它返回一个指向预定义存储槽 _ACCOUNT_STORAGE_SLOTAccountStorage 结构体的引用。

  2. getPermittedCallKey: 生成一个 bytes24 类型的键,该键由地址和选择器组成,用于在 callPermitted 映射中标识权限。

  3. toFunctionReferenceArray: 将 EnumerableMap.Bytes32ToUintMap 映射转换为 FunctionReference 数组。这用于从映射中提取所有元素并将它们存储在内存中的数组中。

总的来说,这段代码是一个高度模块化和可升级的智能合约系统的一部分,它使用复杂的状态管理来跟踪插件、执行函数、权限和钩子。这种模式允许智能合约在维护核心状态的同时,添加新功能或改进现有功能。

AccountStorageInitializable.sol

这段代码是一个用于以太坊智能合约的初始化控制抽象合约,它确保初始化函数只能在合约生命周期中被正确地调用一次。这是为了防止在合约升级时重置或破坏合约的状态。

关键点如下:

  • AlreadyInitializedAlreadyInitializing 错误:用于防止重复初始化。
  • initializer 修饰符:确保函数只在合约尚未初始化时调用。
  • _disableInitializers 函数:用于在初始化后禁用进一步的初始化调用。

这个模式对于可升级的智能合约特别重要,因为它们需要在升级后保持状态不变,同时允许新的初始化逻辑执行。

这段代码定义了一个名为 AccountStorageInitializable 的抽象合约,它是一个使用初始化模式的合约,确保某些函数只能在合约生命周期中被调用一次。这是一个常见的模式,用于可升级合约,以确保在升级过程中,状态变量可以被正确地初始化,而不会被重复初始化。这个抽象合约使用了之前定义的 AccountStorage 结构体和 getAccountStorage 函数。

错误定义

  • AlreadyInitialized: 如果尝试对已经初始化的合约进行初始化,则会抛出这个错误。
  • AlreadyInitializing: 如果在初始化过程中再次尝试初始化,则会抛出这个错误。

Modifier

  • initializer: 这个修饰符用于确保它修饰的函数只能在合约的生命周期中被调用一次。它首先检查是否是最顶层的调用(即没有其他初始化函数正在被执行),然后检查合约是否已经初始化。如果这些检查通过,它将设置 initializing 标志,执行函数,然后清除 initializing 标志。如果检查失败,则会抛出 AlreadyInitialized 错误。

函数

  • _disableInitializers: 这个内部函数用于禁用初始化函数,以防止它们在合约生命周期中被再次调用。它首先检查是否正在执行初始化过程,如果是,则抛出 AlreadyInitializing 错误。如果不是,它会将 _storage.initialized 设置为 uint8 的最大值,从而永久禁用初始化函数。

这个合约的设计使得它可以被用作其他合约的基础,这些合约需要确保它们的初始化逻辑只被执行一次,例如在部署时或在合约升级后。这是一个关键的安全特性,用于防止在合约升级过程中的不当初始化,可能会导致合约状态被意外地篡改。

PluginManagerInternals.sol

这个智能合约 PluginManagerInternals 是一个抽象合约,它实现了 IPluginManager 接口的一部分,用于管理插件的内部逻辑。这个合约包含了添加、移除和管理插件的功能,以及与插件相关的验证函数和钩子(hooks)。

合约使用了OpenZeppelin的库和结构,例如 EnumerableMapEnumerableSet,以及自定义的 FunctionReferenceLib 库来处理函数引用。

合约定义了一系列错误类型,用于处理各种异常情况,例如数组长度不匹配、插件已安装、插件未安装、依赖项不匹配等。

以下是合约中的一些关键点:

  1. 修饰符(Modifiers):

    • notNullFunction: 确保传递的函数引用不是空的。
    • notNullPlugin: 确保传递的插件地址不是空地址。
  2. 内部函数:

    • _setExecutionFunction: 为特定的函数选择器设置执行插件。
    • _removeExecutionFunction: 移除特定选择器的执行插件。
    • _addUserOpValidationFunction_removeUserOpValidationFunction: 添加和移除用户操作验证函数。
    • _addRuntimeValidationFunction_removeRuntimeValidationFunction: 添加和移除运行时验证函数。
    • _addExecHooks_removeExecHooks: 添加和移除执行钩子。
    • _installPlugin: 安装插件,并根据插件的清单更新账户存储。
    • _uninstallPlugin: 卸载插件,并清理相关的账户存储。
    • _addOrIncrement_removeOrDecrement: 用于在 EnumerableMap 中添加或递增键值对,或者移除或递减键值对。
    • _isValidPluginManifest: 验证插件的清单哈希是否有效。
    • _resolveManifestFunction: 解析清单中的函数引用,将它们映射到实际的函数引用。
  3. 事件(Events):

    • PluginInstalled: 当插件安装成功时触发。
    • PluginUninstalled: 当插件卸载成功时触发。

这个合约是一个复杂的插件管理系统的一部分,它允许智能合约动态地添加功能和逻辑,而无需更改或重新部署主合约。这种模式是以太坊智能合约开发中的模块化和灵活性的一个示例。

插件示例

ModularSessionKeyPlugin

这个智能合约 ModularSessionKeyPlugin 是一个插件,用于管理一个模块化账户的临时所有权。它允许指定的外部拥有账户(EOA)或智能合约在一定时间内拥有模块化账户的权限。这个插件主要用于演示 ERC-6900 功能,并不适合生产环境使用。

合约的关键特性和功能包括:

  1. 添加和移除会话密钥:

    • addSessionKey: 添加一个新的会话密钥,设置其允许的选择器以及有效时间范围。
    • removeSessionKey: 移除一个已存在的会话密钥。
    • addSessionKeyBatch: 批量添加会话密钥。
    • removeSessionKeyBatch: 批量移除会话密钥。
  2. 查看函数:

    • getSessionDuration: 获取特定会话密钥的有效时间范围。
    • getSessionKeysAndSelectors: 获取账户的所有会话密钥和它们允许的选择器。
  3. 插件安装和卸载:

    • onInstall: 安装插件时调用,可以初始化会话密钥。
    • onUninstall: 卸载插件时调用,会清除所有会话密钥。
  4. 验证函数:

    • userOpValidationFunction: 用于验证用户操作的签名是否有效。
    • runtimeValidationFunction: 用于运行时验证调用者是否有权执行操作。
  5. 插件清单和元数据:

    • pluginManifest: 提供插件的清单信息,包括执行函数、验证函数和依赖项。
    • pluginMetadata: 提供插件的元数据,如名称、版本和作者。
  6. 会话信息结构 (SessionInfo):

    • 存储会话密钥的有效时间范围。
  7. 事件:

    • SessionKeyAdded: 当添加新的会话密钥时触发。
    • SessionKeyRemoved: 当移除会话密钥时触发。
    • SessionKeysAdded: 当批量添加会话密钥时触发。
    • SessionKeysRemoved: 当批量移除会话密钥时触发。

此外,合约使用了 OpenZeppelin 的 ECDSA 库来处理签名,以及 EnumerableSet 来管理会话密钥集合。合约还使用了 PluginStorageLib 库来管理关联存储。

最后,合约遵循 EIP-165 标准,实现了 supportsInterface 函数,以便可以查询合约是否支持特定的接口。

这个合约的设计旨在演示如何通过插件为模块化智能合约添加临时权限管理功能。通过使用会话密钥,账户的临时所有者可以在指定的时间范围内,使用指定的函数选择器来执行操作。这增加了账户的灵活性和安全性,因为可以严格控制谁有权在何时进行何种操作。

TokenSessionKeyPlugin

这个智能合约 TokenSessionKeyPluginModularSessionKeyPlugin 的子插件,用于实现允许调用 ERC-20 transferFrom 函数的会话密钥的逻辑。它允许会话密钥的持有者通过 transferFromSessionKey 函数访问模块化智能合约账户(MSCA),这个函数会调用 PluginExecutor 合约中的 executeFromPluginExternal 函数。

合约的关键特性和功能包括:

  1. 执行函数:

    • transferFromSessionKey: 允许会话密钥的持有者调用目标 ERC-20 合约的 transferFrom 函数。
  2. 插件安装和卸载:

    • onInstall: 安装插件时调用,但在此合约中没有实现任何逻辑。
    • onUninstall: 卸载插件时调用,但在此合约中没有实现任何逻辑。
  3. 插件清单 (pluginManifest):

    • 提供插件的清单信息,包括执行函数、验证函数、依赖项和允许的外部调用。
    • 在这个插件中,目标 ERC-20 合约和 transferFrom 函数的选择器是硬编码的,因为 pluginManifest 函数要求未来不改变允许的外部调用信息。
  4. 插件元数据 (pluginMetadata):

    • 提供插件的元数据,如名称、版本和作者。
  5. EIP-165 支持:

    • supportsInterface: 实现了 EIP-165 标准,以便可以查询合约是否支持特定的接口。

这个合约的设计允许会话密钥的持有者在指定的时间范围内,使用 transferFrom 函数来转移代币。它通过扩展 ModularSessionKeyPlugin 的功能,为会话密钥添加了特定的用例——允许调用 ERC-20 合约的 transferFrom 方法。这种设计模式允许开发者根据需要创建更多的子插件,以实现对不同类型外部调用的权限管理。

TokenSessionKeyPlugin 是 ModularSessionKeyPlugin 的子插件?

是的,根据您提供的智能合约代码,TokenSessionKeyPlugin 是作为 ModularSessionKeyPlugin 的子插件设计的。在注释中提到了这一点:

/// @notice This plugin acts as a 'child plugin' for ModularSessionKeyPlugin.

这意味着 TokenSessionKeyPlugin 依赖于 ModularSessionKeyPlugin 的架构,并可能扩展或特化了其功能。它实现了特定的逻辑,允许会话密钥(session keys)调用 ERC-20 的 transferFrom 函数。这种设计允许在 ModularSessionKeyPlugin 的框架内创建具有特定功能的多个插件,而 TokenSessionKeyPlugin 只是其中之一,专注于处理与代币转移相关的会话密钥权限。

除了注释,我没看到TokenSessionKeyPlugin 是ModularSessionKeyPlugin的子插件

您正确地指出,从合约代码本身来看,并没有明确的继承关系表明 TokenSessionKeyPluginModularSessionKeyPlugin 的子类。在 Solidity 中,子类通过使用 is 关键词来继承一个或多个父合约。例如:

contract ChildContract is ParentContract {
// ...
}

在您提供的 TokenSessionKeyPlugin 合约代码中,并没有出现这样的继承声明。相反,TokenSessionKeyPlugin 继承自 BasePlugin 并实现了 ITokenSessionKeyPlugin 接口。这里的“子插件”这个术语可能是指 TokenSessionKeyPlugin 在概念上是作为 ModularSessionKeyPlugin 系统的一部分设计的,而不是在 Solidity 继承的意义上。

TokenSessionKeyPlugin 通过其 pluginManifest 函数定义了与 ModularSessionKeyPlugin 相关的依赖关系,它指定了 IModularSessionKeyPlugin 接口的 ID 作为依赖项:

manifest.dependencyInterfaceIds[0] = type(IModularSessionKeyPlugin).interfaceId;
manifest.dependencyInterfaceIds[1] = type(IModularSessionKeyPlugin).interfaceId;

这表明 TokenSessionKeyPlugin 需要与实现了 IModularSessionKeyPlugin 接口的某个系统(可能是 ModularSessionKeyPlugin 或其它符合该接口的合约)进行交互,但并不意味着 TokenSessionKeyPlugin 直接从 ModularSessionKeyPlugin 继承。

在实际的插件架构中,TokenSessionKeyPlugin 可能被设计为与 ModularSessionKeyPlugin 一起工作,即使它不是通过 Solidity 的继承机制直接继承的。这种关系可能是通过接口、事件、函数调用或其他合约级别的交互来实现的。

helps

FunctionReferenceLib.sol 智能合约中引用,用于管理和比较函数引用

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

import {FunctionReference} from "../interfaces/IPluginManager.sol";

library FunctionReferenceLib {
// Empty or unset function reference.
FunctionReference internal constant _EMPTY_FUNCTION_REFERENCE = FunctionReference.wrap(bytes21(0));
// Magic value for runtime validation functions that always allow access.
FunctionReference internal constant _RUNTIME_VALIDATION_ALWAYS_ALLOW =
FunctionReference.wrap(bytes21(uint168(1)));
// Magic value for hooks that should always revert.
FunctionReference internal constant _PRE_HOOK_ALWAYS_DENY = FunctionReference.wrap(bytes21(uint168(2)));

function pack(address addr, uint8 functionId) internal pure returns (FunctionReference) {
return FunctionReference.wrap(bytes21(bytes20(addr)) | bytes21(uint168(functionId)));
}

function unpack(FunctionReference fr) internal pure returns (address addr, uint8 functionId) {
bytes21 underlying = FunctionReference.unwrap(fr);
addr = address(bytes20(underlying));
functionId = uint8(bytes1(underlying << 160));
}

function isEmpty(FunctionReference fr) internal pure returns (bool) {
return FunctionReference.unwrap(fr) == bytes21(0);
}

function isEmptyOrMagicValue(FunctionReference fr) internal pure returns (bool) {
return FunctionReference.unwrap(fr) <= bytes21(uint168(2));
}

function eq(FunctionReference a, FunctionReference b) internal pure returns (bool) {
return FunctionReference.unwrap(a) == FunctionReference.unwrap(b);
}

function notEq(FunctionReference a, FunctionReference b) internal pure returns (bool) {
return FunctionReference.unwrap(a) != FunctionReference.unwrap(b);
}
}

您提供了一个Solidity库 FunctionReferenceLib 的代码,这个库提供了一系列的函数来处理 FunctionReference 类型的数据。FunctionReference 是一个封装了 bytes21 类型数据的结构体,可能用于表示智能合约中的函数引用。

这个库定义了一些常量和函数:

  • _EMPTY_FUNCTION_REFERENCE: 一个常量,表示空或未设置的函数引用。
  • _RUNTIME_VALIDATION_ALWAYS_ALLOW: 一个常量,用于表示在运行时验证中总是允许访问的魔术值。
  • _PRE_HOOK_ALWAYS_DENY: 一个常量,用于表示那些应该总是被拒绝的钩子。

库中的函数包括:

  • pack: 用于将地址和函数ID打包成一个 FunctionReference
  • unpack: 用于将 FunctionReference 解包成地址和函数ID。
  • isEmpty: 检查 FunctionReference 是否为空。
  • isEmptyOrMagicValue: 检查 FunctionReference 是否为空或者是一个魔术值。
  • eq: 检查两个 FunctionReference 是否相等。
  • notEq: 检查两个 FunctionReference 是否不相等。

这个库可以在智能合约中引用,用于管理和比较函数引用,这可能在实现某些权限管理或插件系统时非常有用。例如,可以使用这个库来确认一个特定的函数调用是否被允许,或者在智能合约的不同部分之间传递函数引用。

ValidationDataHelpers.sol


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

// solhint-disable-next-line private-vars-leading-underscore
function _coalescePreValidation(uint256 validationData1, uint256 validationData2)
pure
returns (uint256 resValidationData)
{
uint48 validUntil1 = uint48(validationData1 >> 160);
if (validUntil1 == 0) {
validUntil1 = type(uint48).max;
}
uint48 validUntil2 = uint48(validationData2 >> 160);
if (validUntil2 == 0) {
validUntil2 = type(uint48).max;
}
resValidationData = ((validUntil1 > validUntil2) ? uint256(validUntil2) << 160 : uint256(validUntil1) << 160);

uint48 validAfter1 = uint48(validationData1 >> 208);
uint48 validAfter2 = uint48(validationData2 >> 208);

resValidationData |= ((validAfter1 < validAfter2) ? uint256(validAfter2) << 208 : uint256(validAfter1) << 208);

// Once we know that the authorizer field is 0 or 1, we can safely bubble up SIG_FAIL with bitwise OR
// 一旦我们知道授权者字段是0或1,我们就可以使用按位或安全地将SIG_FAIL冒泡上来
resValidationData |= uint160(validationData1) | uint160(validationData2);
}

// solhint-disable-next-line private-vars-leading-underscore
function _coalesceValidation(uint256 preValidationData, uint256 validationData)
pure
returns (uint256 resValidationData)
{
uint48 validUntil1 = uint48(preValidationData >> 160);
if (validUntil1 == 0) {
validUntil1 = type(uint48).max;
}
uint48 validUntil2 = uint48(validationData >> 160);
if (validUntil2 == 0) {
validUntil2 = type(uint48).max;
}
resValidationData = ((validUntil1 > validUntil2) ? uint256(validUntil2) << 160 : uint256(validUntil1) << 160);

uint48 validAfter1 = uint48(preValidationData >> 208);
uint48 validAfter2 = uint48(validationData >> 208);

resValidationData |= ((validAfter1 < validAfter2) ? uint256(validAfter2) << 208 : uint256(validAfter1) << 208);

// If prevalidation failed, bubble up failure
resValidationData |= uint160(preValidationData) == 1 ? 1 : uint160(validationData);
}

为什么要合并数据?

合并验证数据的原因通常与智能合约中的权限管理和状态控制有关。在智能合约的执行过程中,可能需要根据多个条件或规则来判断一个操作是否应该被允许。这些条件可能来源于不同的环节,比如用户的权限、合约的状态、时间限制或其他业务逻辑。

以下是一些合并验证数据的可能原因:

  1. 层级验证: 在某些情况下,操作可能需要通过多层验证。例如,用户的请求可能需要先通过预验证(如一个预设的白名单),然后再通过主验证(如时间戳或额外的权限检查)。合并验证数据允许智能合约在不同验证层级之间传递状态。

  2. 时间窗口控制: 如果操作只能在特定的时间窗口内执行,那么合约可能需要考虑多个时间戳。比如,一个操作可能有一个全局的时间窗口和一个特定用户的时间窗口。合并这些时间戳意味着找到一个共同的时间窗口,在这个时间窗口内,操作对所有相关方都是有效的。

  3. 状态组合: 合并验证数据可以用来结合多个状态或标志。例如,如果一个操作需要两个独立条件的同意,合并这些条件可以确保只有当两个条件都满足时,操作才会被执行。

  4. 简化逻辑: 通过合并验证数据,智能合约的开发者可以简化合约逻辑。这样,代替在合约的多个地方重复相同的验证逻辑,开发者可以创建一个单独的、集中的验证函数,这个函数可以处理多个来源的验证数据。

  5. 安全性: 合并验证数据还可以提高合约的安全性。确保所有相关的验证规则都得到了考虑,可以防止由于遗漏单个规则而导致的安全漏洞。

在上述代码中,_coalescePreValidation_coalesceValidation 函数通过合并 validUntilvalidAfter 时间戳以及状态标志,创建了一个综合的验证数据,这个数据可以一次性用于决定操作是否应该被允许。这种方法减少了代码重复,提高了效率,并且允许在逻辑上将多个条件合并为单一的判断基础。

_coalescePreValidation

src/helpers/ValidationDataHelpers.sol
function _coalescePreValidation(uint256 validationData1, uint256 validationData2)
pure
returns (uint256 resValidationData)
{
uint48 validUntil1 = uint48(validationData1 >> 160);
if (validUntil1 == 0) {
validUntil1 = type(uint48).max;
}
uint48 validUntil2 = uint48(validationData2 >> 160);
if (validUntil2 == 0) {
validUntil2 = type(uint48).max;
}
resValidationData = ((validUntil1 > validUntil2) ? uint256(validUntil2) << 160 : uint256(validUntil1) << 160);

uint48 validAfter1 = uint48(validationData1 >> 208);
uint48 validAfter2 = uint48(validationData2 >> 208);

resValidationData |= ((validAfter1 < validAfter2) ? uint256(validAfter2) << 208 : uint256(validAfter1) << 208);

// Once we know that the authorizer field is 0 or 1, we can safely bubble up SIG_FAIL with bitwise OR
resValidationData |= uint160(validationData1) | uint160(validationData2);
}



什么意思,怎么都是位运算

这个函数 _coalescePreValidation 是合并两个验证数据的函数。它使用位运算来处理和合并两个 uint256 类型的验证数据,这些数据可能包含如下信息:

  • 有效期开始时间(validAfter
  • 有效期结束时间(validUntil
  • 额外的验证信息(比如授权信息)

在以太坊智能合约中,由于存储和操作的成本较高,通常会尝试尽量减少存储的变量数量和大小。一种常见的做法是将多个信息打包到一个单一的整数中,这就需要使用位运算来提取和设置这些打包的数据。

这个函数的主要步骤如下:

  1. 提取 validUntil 时间

    • validationData1validationData2validUntil 时间分别从每个变量的第161位到第208位提取出来(因为 uint48 占用48位,所以从第160位偏移48位)。
    • 如果提取的时间为0,则将其设置为 uint48 的最大值,表示永久有效。
  2. 合并 validUntil 时间

    • 比较两个 validUntil 时间,取较早的时间作为结果的 validUntil 时间。
    • 将该时间左移160位,放回到结果的对应位置。
  3. 提取 validAfter 时间

    • validationData1validationData2validAfter 时间分别从每个变量的第209位到第256位提取出来。
  4. 合并 validAfter 时间

    • 比较两个 validAfter 时间,取较晚的时间作为结果的 validAfter 时间。
    • 将该时间左移208位,放回到结果的对应位置。
  5. 合并额外的验证信息

    • 使用位或运算 | 合并 validationData1validationData2 的前160位,这通常包含了验证失败的标志或其他信息。

这个函数的结果是一个新的 uint256,它结合了两个输入中最严格的验证时间范围,并且保留了任何验证失败的标志。这种方式非常高效,因为它避免了使用更多的变量和更复杂的逻辑判断,而是直接在二进制层面上操作数据。

为什么在计算validUntil1的时候要右移,在计算resValidationData时候要左移?

 uint48 validUntil1 = uint48(validationData1 >> 160);
if (validUntil1 == 0) {
validUntil1 = type(uint48).max;
}
uint48 validUntil2 = uint48(validationData2 >> 160);
if (validUntil2 == 0) {
validUntil2 = type(uint48).max;
}
resValidationData = ((validUntil1 > validUntil2) ? uint256(validUntil2) << 160 : uint256(validUntil1) << 160);

这里的右移和左移操作都是位运算,用于在一个较大的整数(在这种情况下是 uint256)内编码和解码较小的整数值(uint48)。在智能合约中,这样做是为了节省空间和优化交易的成本,因为在以太坊上进行存储和修改状态是需要消耗gas的。

右移操作(>>

当我们对 uint256 类型的 validationData1 变量进行右移160位操作时,我们实际上是在提取这个较大整数的一部分。在这个函数中,validUntil 时间戳被假定为存储在 validationData1 的第161到208位。通过右移160位,这段时间戳就被移动到了 uint256 的最低位位置,这样就可以将其转换为 uint48 类型,因为 uint48 类型只能存储最低的48位。

左移操作(<<

在确定了 validUntil1validUntil2 之间较小的一个之后,我们需要将这个 uint48 类型的值放回原来的位置,即 validationData1validationData2 中的第161到208位。为此,我们需要进行左移160位操作。这样,这个48位的值就被放置回了原来的位置,而其他位则被保留为0,从而不会干扰 uint256 中的其他数据。

总结

右移是为了提取 uint256 中某个特定范围的位作为一个较小的整数值。左移则是为了将这个较小的整数值放回 uint256 的相同位置。这就是为什么在计算 validUntil1validUntil2 时要右移,而在将结果放入 resValidationData 时要左移,因为我们在编码和解码相同的信息位置。

为什么validUntil1与validAfter1,一个是右移160,一个是右移208?

在这个函数中,validUntilvalidAfter 是作为 uint256 类型的 validationData1validationData2 的一部分而存储的。这些数据被打包在一个 uint256 类型的变量中,每个部分占据特定的位段。这就要求在提取这些值时使用不同的位移量。

这里是如何确定位移量的:

  1. validUntil:

    • validUntil 被假设为存储在 validationData1validationData2 的第161位到第208位。
    • 要将这48位数据提取出来,你需要进行右移160位操作,这样这些位就会移动到 uint256 的最低位,可以将其转换为 uint48 类型。
  2. validAfter:

    • validAfter 被假设为存储在 validationData1validationData2 的第209位到第256位。
    • 因为 uint48 占用48位,所以 validAfter 直接紧跟在 validUntil 后面。
    • 要提取 validAfter,需要进行右移208位操作,将第209位到第256位移动到 uint256 的最低位。

位移量的计算基于信息在 uint256 中的存储位置。每个部分的位移量是为了确保当你提取或者重新放置这些值时,它们能够回到原始的、正确的位置。在这个特定的函数中,validUntilvalidAfter 占据了不同的位置,所以它们需要不同的位移量来进行正确的提取和存储。

开始时间存在第161位到第208位,结束时间存在第209位到第256位?为什么这么确定是161到208,这之间是48位?

在您的问题中,您提到了“开始时间存在第161位到第208位,结束时间存在第209位到第256位”,这似乎是您对某种数据编码方式的描述。这里的位数是基于二进制位的位置。

在一个 uint256 类型的变量中,可以存储256位的信息。通常,我们从右到左计数,最右边的位是第0位,最左边的位是第255位。

如果“开始时间”占据从第161位到第208位,那么:

  • 第161位是“开始时间”的第一个位。
  • 第208位是“开始时间”的最后一个位。

为了计算这之间有多少位,我们可以简单地从结束位数减去开始位数,然后加1(因为我们要包括第161位):

208161+1=48208 - 161 + 1 = 48

这意味着“开始时间”占用了48位。

同理,如果“结束时间”从第209位开始,那么它紧跟在“开始时间”之后,从第209位到第256位:

  • 第209位是“结束时间”的第一个位。
  • 第256位是“结束时间”的最后一个位。

计算这部分的位数:

256209+1=48256 - 209 + 1 = 48

“结束时间”也占用了48位。

这种位的分配方式是由设计决定的,也就是说,某个系统或协议决定了如何在一个 uint256 类型的变量中编码这些时间戳。这样的设计允许在一个单独的 uint256 变量中存储两个不同的时间戳,每个时间戳占用48位,总共使用了96位,剩下的160位可以用于存储其他信息或者保留不用。

时间戳需要48位表示?

通常情况下,时间戳并不需要48位来表示。一个标准的UNIX时间戳,记录自1970年1月1日以来的秒数,可以在32位整数中完全表示,这足以覆盖从1970年到2038年之间的时间(这就是所谓的2038年问题,因为在2038年之后32位时间戳会溢出)。

如果我们使用48位来表示时间戳,这将极大地扩展我们能表示的时间范围。在二进制中,48位可以表示的数值范围是从0到24812^{48} - 1,这是一个非常大的数字。如果我们假设这个48位数是一个以秒为单位的时间戳,那么它可以表示的时间范围将远远超过人类文明的历史。

为了具体说明,我们可以计算一下48位可以表示多少年:

248 seconds2.814×1014 seconds2^{48} \text{ seconds} \approx 2.814 \times 10^{14} \text{ seconds}

将这个数字转换为年:

2.814×1014 seconds60×60×24×365.258.9×106 years\frac{2.814 \times 10^{14} \text{ seconds}}{60 \times 60 \times 24 \times 365.25} \approx 8.9 \times 10^{6} \text{ years}

所以,48位可以表示大约890万年。这远远超过了UNIX时间戳的设计需求。

在实际应用中,如果有48位用于表示时间戳,可能是因为这个时间戳不仅仅包含了秒数,还可能包括毫秒或微秒等更精细的时间单位,或者是为了将来的可扩展性而设计的。在区块链和智能合约的上下文中,可能有特殊的考虑,比如需要更精确的时间戳,或者是为了与其他系统兼容而采用的设计。

PluginManagerInternals 与 _resolveManifestFunction 和 FunctionReferenceLib

在这段智能合约代码中,_resolveManifestFunction 函数是 PluginManagerInternals 抽象合约的一部分,它用于解析与插件相关的函数。这个函数在安装(_installPlugin)和卸载(_uninstallPlugin)插件时都会被调用,以确保插件的函数和钩子(hooks)正确地与合约的其他部分集成。

_resolveManifestFunction 的核心目的是将 ManifestFunction 结构体中定义的函数引用转换为 FunctionReference 类型。FunctionReference 是一个包装类型,用于表示对合约中函数的引用,它可能指向合约自身的函数、依赖插件的函数,或者是一些特殊的“魔法”值,这些值表示一些特定的行为(如总是允许或总是拒绝)。

这个函数使用 FunctionReferenceLib 库来执行以下操作:

  1. 打包函数引用:将插件地址和函数选择器(function selector)封装成一个 FunctionReference 对象。
  2. 解析依赖项:如果函数引用指向一个依赖插件,它会从提供的依赖数组中获取正确的引用。
  3. 处理特殊的“魔法”值:根据 allowedMagicValue 参数,它可能返回一个表示总是允许或总是拒绝的特殊 FunctionReference

FunctionReferenceLib 库可能定义了如下功能:

  • pack:将地址和函数ID打包成一个 FunctionReference
  • unpack:将 FunctionReference 解包为地址和函数ID。
  • isEmpty:检查一个 FunctionReference 是否为空。
  • _EMPTY_FUNCTION_REFERENCE:一个空的 FunctionReference 常量。
  • _RUNTIME_VALIDATION_ALWAYS_ALLOW:一个总是允许的 FunctionReference 常量,用于运行时验证。
  • _PRE_HOOK_ALWAYS_DENY:一个总是拒绝的 FunctionReference 常量,用于前置钩子。

PluginManagerInternals 合约中,_resolveManifestFunction 被用来处理插件清单中声明的各种函数。这些函数可能包括执行函数、验证函数、用户操作验证钩子、运行时验证钩子和执行钩子。通过解析这些函数并将它们与相应的合约选择器关联起来,合约能够在执行相应的操作时调用正确的插件函数。

这种设计使得插件管理系统能够灵活地集成和管理不同的插件,同时保持了合约的可升级性和模块化。通过将复杂的解析逻辑封装在 _resolveManifestFunction 中,开发者可以更容易地添加、更新和移除插件,而不需要修改合约的核心逻辑。

FunctionReferenceLib

src/helpers/FunctionReferenceLib.sol

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

import {FunctionReference} from "../interfaces/IPluginManager.sol";

library FunctionReferenceLib {
// Empty or unset function reference.
FunctionReference internal constant _EMPTY_FUNCTION_REFERENCE = FunctionReference.wrap(bytes21(0));
// Magic value for runtime validation functions that always allow access.
FunctionReference internal constant _RUNTIME_VALIDATION_ALWAYS_ALLOW =
FunctionReference.wrap(bytes21(uint168(1)));
// Magic value for hooks that should always revert.
FunctionReference internal constant _PRE_HOOK_ALWAYS_DENY = FunctionReference.wrap(bytes21(uint168(2)));

function pack(address addr, uint8 functionId) internal pure returns (FunctionReference) {
return FunctionReference.wrap(bytes21(bytes20(addr)) | bytes21(uint168(functionId)));
}

function unpack(FunctionReference fr) internal pure returns (address addr, uint8 functionId) {
bytes21 underlying = FunctionReference.unwrap(fr);
addr = address(bytes20(underlying));
functionId = uint8(bytes1(underlying << 160));
}

function isEmpty(FunctionReference fr) internal pure returns (bool) {
return FunctionReference.unwrap(fr) == bytes21(0);
}

function isEmptyOrMagicValue(FunctionReference fr) internal pure returns (bool) {
return FunctionReference.unwrap(fr) <= bytes21(uint168(2));
}

function eq(FunctionReference a, FunctionReference b) internal pure returns (bool) {
return FunctionReference.unwrap(a) == FunctionReference.unwrap(b);
}

function notEq(FunctionReference a, FunctionReference b) internal pure returns (bool) {
return FunctionReference.unwrap(a) != FunctionReference.unwrap(b);
}
}

这段代码是Solidity智能合约的一部分,用于在以太坊区块链上管理函数引用。它定义了一个名为 FunctionReferenceLib 的库,该库提供了一些工具函数来处理 FunctionReference 对象。FunctionReference 对象是一个封装了智能合约地址和特定函数标识符的自定义数据类型。

  • _EMPTY_FUNCTION_REFERENCE 是一个表示空函数引用的常量。
  • _RUNTIME_VALIDATION_ALWAYS_ALLOW 是一个特殊值,表示总是允许访问的函数。
  • _PRE_HOOK_ALWAYS_DENY 是一个特殊值,表示总是拒绝访问的函数。

库中的函数允许你:

  • pack:将合约地址和函数ID打包成一个 FunctionReference
  • unpack:将 FunctionReference 解包为原始的合约地址和函数ID。
  • isEmpty:检查 FunctionReference 是否为空。
  • isEmptyOrMagicValue:检查 FunctionReference 是否为空或等于特殊值(1或2)。
  • eqnotEq:比较两个 FunctionReference 是否相等或不相等。

这些功能通常用于智能合约中,以确保函数调用的正确性和安全性。

@inheritdoc

// 继承文档

// @inheritdoc ITokenSessionKeyPlugin

// 意思是:继承自ITokenSessionKeyPlugin,请参照其文档

src/samples/plugins/TokenSessionKeyPlugin.sol

src/samples/plugins/TokenSessionKeyPlugin.sol
    /// @inheritdoc BasePlugin
function pluginManifest() external pure override returns (PluginManifest memory) {
PluginManifest memory manifest;

manifest.executionFunctions = new bytes4[](1);
manifest.executionFunctions[0] = this.transferFromSessionKey.selector;

ManifestFunction memory tempOwnerUserOpValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.DEPENDENCY,
functionId: 0, // Unused
dependencyIndex: 0 // Used as first index
});
ManifestFunction memory tempOwnerRuntimeValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.DEPENDENCY,
functionId: 0, // Unused
dependencyIndex: 1 // Used as second index
});
// 声明op验证函数数组
manifest.userOpValidationFunctions = new ManifestAssociatedFunction[](1);
// 填充数据
manifest.userOpValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.transferFromSessionKey.selector, // 函数选择器
associatedFunction: tempOwnerUserOpValidationFunction // 关联的函数
});
// 声明运行时验证函数数组
manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](1);
manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.transferFromSessionKey.selector, // 函数选择器
associatedFunction: tempOwnerRuntimeValidationFunction // 关联的函数
});
// 声明以来接口id数组, 这里很奇怪,数组第一个和第二个一摸一样
manifest.dependencyInterfaceIds = new bytes4[](2);
manifest.dependencyInterfaceIds[0] = type(IModularSessionKeyPlugin).interfaceId;
manifest.dependencyInterfaceIds[1] = type(IModularSessionKeyPlugin).interfaceId;
// 这里前一个给pre用的,后一个给post用的

/* if (preHook.functionType == ManifestAssociatedFunctionType.DEPENDENCY) {
m2.dependencyInterfaceIds.push(type(IPlugin).interfaceId);
}
if (postHook.functionType == ManifestAssociatedFunctionType.DEPENDENCY) {
m2.dependencyInterfaceIds.push(type(IPlugin).interfaceId);
}*/





// 授权扩展的选择器
bytes4[] memory permittedExternalSelectors = new bytes4[](1);
permittedExternalSelectors[0] = TRANSFERFROM_SELECTOR;
// 授权扩展调用
manifest.permittedExternalCalls = new ManifestExternalCallPermission[](1);
manifest.permittedExternalCalls[0] = ManifestExternalCallPermission({
externalAddress: TARGET_ERC20_CONTRACT,
permitAnySelector: false,
selectors: permittedExternalSelectors
});

return manifest;
}

这个 pluginManifest 函数是一个智能合约中的函数,其作用是定义一个插件的清单(manifest),这个插件可能是一个提供额外功能或逻辑的智能合约模块。这个清单包含了插件的各种属性和配置信息,例如执行函数、验证函数、依赖项以及外部调用权限。

下面是 pluginManifest 函数内部实现的不同部分及其作用:

  1. Execution Functions: 这个部分定义了插件用于执行操作的函数。在这个例子中,它定义了一个包含单个函数选择器的数组 executionFunctions。这个选择器是 transferFromSessionKey 函数的选择器,表示这个插件能够执行这个特定的函数。

  2. UserOp Validation Functions: 用户操作(UserOp)验证函数用于在用户操作被执行之前验证它们。这里,它创建了一个 ManifestFunction 结构体实例 tempOwnerUserOpValidationFunction,并将其设置为依赖项类型,关联到依赖项数组的第一个索引。

  3. Runtime Validation Functions: 运行时验证函数用于在操作执行期间进行验证。同样,它创建了一个 ManifestFunction 结构体实例 tempOwnerRuntimeValidationFunction,并将其设置为依赖项类型,关联到依赖项数组的第二个索引。

  4. Dependency Interface IDs: 这个部分定义了插件所依赖的接口ID。在这个例子中,它定义了一个包含两个相同接口ID的数组 dependencyInterfaceIds,这意味着插件依赖于 IModularSessionKeyPlugin 接口。

  5. Permitted External Calls: 允许的外部调用定义了该插件可以安全调用哪些外部智能合约的函数。在这里,它定义了一个 ManifestExternalCallPermission 结构体实例,指定了目标ERC20合约地址 TARGET_ERC20_CONTRACT 和允许调用的选择器数组 permittedExternalSelectors,这个数组中包含了 TRANSFERFROM_SELECTOR,表示该插件允许调用ERC20的 transferFrom 函数。

最后,这个函数返回了填充好的 PluginManifest 结构体实例。

总之,pluginManifest 函数的作用是为智能合约插件定义一个清单,这个清单详细说明了插件的功能、它可以执行哪些操作、依赖哪些接口,以及它允许哪些外部调用。通过这样的机制,智能合约系统可以更灵活地扩展其功能,同时保持模块化和安全性。

new ManifestAssociatedFunction[](1); 是啥?

在Solidity中,new ManifestAssociatedFunction[](1); 这行代码是用来创建一个新的动态数组,其中包含一个元素的空间,而该元素的类型为 ManifestAssociatedFunction 结构体。这是一个数组字面量的声明方式,用于在合约的内存中分配空间,以便存储一个具有指定类型元素的数组。

让我们分解这行代码:

  • new: 这是一个Solidity关键字,用于创建一个新的动态数组或其他复杂类型的实例。
  • ManifestAssociatedFunction[]: 这指定了数组中元素的类型,即 ManifestAssociatedFunction 结构体。
  • (1): 这指定了数组的初始大小,即它将有一个空间来存储一个 ManifestAssociatedFunction 结构体实例。

ManifestAssociatedFunction 结构体通常是在智能合约中定义的,用来封装与某个特定函数相关的信息。例如,它可能包含函数选择器和与该函数关联的其他验证函数或依赖项。

在给定的上下文中,这个数组被用来存储与插件相关的函数信息,这些信息将被用来验证用户操作或在运行时执行某些验证逻辑。通过将这些信息存储在数组中,智能合约可以在执行时遍历这些函数并根据需要调用它们。


manifest.userOpValidationFunctions = new ManifestAssociatedFunction[](1);
manifest.userOpValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.transferFromSessionKey.selector,
associatedFunction: tempOwnerUserOpValidationFunction
});

// 这个代码的意思是:先创建一个长度为1的ManifestAssociatedFunction空数组,然后对数组的第一个元素赋值ManifestAssociatedFunction的实例,ManifestAssociatedFunction实例是一个函数选择器与另一个函数之间建立了关联?

是的,您的理解是正确的。这段代码首先创建了一个长度为1的 ManifestAssociatedFunction 类型的动态数组,然后设置数组的第一个元素为一个 ManifestAssociatedFunction 结构体的实例。

在Solidity中,ManifestAssociatedFunction 结构体可能被定义为包含如下信息:

  • executionSelector: 用于标识合约中某个函数的选择器(selector)。函数选择器是一个特定的字节序列,用于在EVM(以太坊虚拟机)中唯一标识合约中的函数。
  • associatedFunction: 与 executionSelector 相关联的另一个函数的信息。这个关联的函数可能用于在执行 executionSelector 指定的函数之前进行验证或其他逻辑处理。

在您提供的代码片段中,manifest.userOpValidationFunctions[0] 被设置为一个新的 ManifestAssociatedFunction 结构体实例,其中:

  • executionSelector 被设置为 this.transferFromSessionKey.selector,这表示插件中的 transferFromSessionKey 函数的选择器。
  • associatedFunction 被设置为 tempOwnerUserOpValidationFunction,这是之前创建的一个 ManifestFunction 结构体实例,它可能包含了一些额外的信息,如用于验证用户操作的逻辑。

这样,当插件的 transferFromSessionKey 函数被调用时,智能合约知道它需要先执行与之关联的 tempOwnerUserOpValidationFunction 函数进行用户操作验证。这是一种常见的模式,用于确保在执行关键功能之前满足某些条件或验证,增强智能合约的安全性和可扩展性。

代码都是在给manifest添加属性,从这个pluginManifest看不出来函数的执行顺序

从您提供的代码片段来看,pluginManifest 只是在定义一个数据结构,用于存储与函数相关的信息。这个数据结构本身并不规定函数的执行顺序,而是表明了某些函数(如 transferFromSessionKey)在被调用时应该关联哪些验证函数(如 tempOwnerUserOpValidationFunction)。

在Solidity中,函数的执行顺序是由合约的逻辑和函数调用来决定的,而不是由数据结构来决定的。pluginManifest 可能是用来在合约的逻辑中查找和引用相关的验证函数,这样在执行主要功能之前,合约可以先调用这些验证函数。

例如,如果合约的某个函数被调用,合约的逻辑可能会这样执行:

  1. 查找该函数对应的 pluginManifest 条目。
  2. 检索 pluginManifest 条目中的 associatedFunction
  3. 执行 associatedFunction 中的验证逻辑。
  4. 如果验证通过,继续执行原始调用的函数。

在这个过程中,pluginManifest 起到的是配置的作用,告诉合约在执行特定功能之前需要执行哪些额外的步骤。但是,具体的执行顺序是由合约的编码逻辑决定的,通常在函数调用的处理流程中明确指定。如果需要了解详细的执行顺序,您需要查看包含这些函数调用和引用 pluginManifest 的合约代码部分。

重写为空函数

/// @inheritdoc BasePlugin
function onInstall(bytes calldata data) external override {}

/// @inheritdoc BasePlugin
function onUninstall(bytes calldata data) external override {}

/* BasePlugin中声明为必须重写,否则revert, 见下面代码:

/// @notice Initialize plugin data for the modular account.
/// @dev Called by the modular account during `installPlugin`.
/// @param data Optional bytes array to be decoded and used by the plugin to setup initial plugin data for the
/// modular account.
function onInstall(bytes calldata data) external virtual {
(data);
revert NotImplemented();
}

/// @notice Clear plugin data for the modular account.
/// @dev Called by the modular account during `uninstallPlugin`.
/// @param data Optional bytes array to be decoded and used by the plugin to clear plugin data for the modular
/// account.
function onUninstall(bytes calldata data) external virtual {
(data);
revert NotImplemented();
}

*/

这个是对BasePlugin两个方法进行了重写,重写为空函数,什么都没有做?

是的,您提供的代码片段显示了两个函数 onInstallonUninstall,它们都使用 override 关键字来重写 BasePlugin 中的同名方法。由于这两个函数的实现体是空的(即 {} 中没有任何代码),这意味着它们实际上不执行任何操作。

这种做法在软件开发中是有意义的,通常有几个原因:

  1. 可选实现BasePlugin 可能定义了一些可选的钩子方法,允许派生类根据需要实现它们。如果派生类不需要在安装(onInstall)或卸载(onUninstall)时执行任何特定操作,它可以简单地提供一个空的实现。

  2. 占位实现:这些方法可能是作为占位符提供的,预留了修改的空间,以便将来可以在这些方法中添加实际的逻辑。

  3. 接口兼容性:如果 BasePlugin 要求其所有派生类必须实现这些方法(可能是因为它们是抽象方法或接口中的方法),那么即使不需要任何操作,也必须提供实现来满足接口兼容性。

  4. 默认实现:在某些情况下,BasePlugin 可能提供了这些方法的默认实现,而派生类通过提供空的实现来显式地选择不使用这些默认行为。

总之,重写为一个空函数通常是有意为之,以满足设计上的某些要求,但具体的目的取决于整体的应用程序架构和设计。如果你是代码的维护者或开发者,你可能需要了解更多的上下文或文档来确定这些空实现是否应该保持不变,或者是否需要在未来添加具体的逻辑。

(data)

// BasePlugin 中
function onInstall(bytes calldata data) external virtual {
(data); // 这行代码其实是一个不做任何操作的表达式。它的目的是为了避免编译器警告关于未使用的参数
revert NotImplemented();
}

// (data)是什么?

在 Solidity 中,(data); 这行代码其实是一个不做任何操作的表达式。它的目的是为了避免编译器警告关于未使用的参数。这是一种常见的做法,用于临时地“使用”一个参数,以防止编译器发出警告。

在这个上下文中,data 参数被传入了 onInstall 函数,但函数体内并没有使用它。为了避免编译器关于未使用变量的警告,开发者可能会写一个空的表达式 (data);

然后,revert NotImplemented(); 这行代码会立即终止交易,并抛出一个异常,异常的类型是 NotImplemented。这表明 onInstall 函数是一个虚拟函数,预期在派生类中被重写,而 BasePlugin 类本身并没有提供这个函数的实现。如果派生类没有重写这个函数,而代码试图调用它,那么就会抛出这个异常。

总的来说,这段代码的意图是声明一个需要在子类中被重写的函数,并且通过 (data); 避免编译器警告,同时使用 revert NotImplemented(); 明确指出这个函数在 BasePlugin 中并没有实现。


function getSessionDuration(address account, address sessionKey, bytes4 allowedSelector)
external
view
returns (uint48 validAfter, uint48 validUntil)
{
bytes memory key = account.allocateAssociatedStorageKey(0, 1); // allocateAssociatedStorageKey
StoragePointer ptr = key.associatedStorageLookup(keccak256(abi.encodePacked(sessionKey, allowedSelector))); // associatedStorageLookup
SessionInfo storage sessionInfo = _castPtrToStruct(ptr);
validAfter = sessionInfo.validAfter;
validUntil = sessionInfo.validUntil;
}

// ...

function _castPtrToStruct(StoragePointer ptr) internal pure returns (SessionInfo storage val) {
assembly ("memory-safe") {
val.slot := ptr // 指针赋值给slot
}
}


在提供的Solidity函数getSessionDuration中,函数的签名指明了它将返回两个uint48类型的值,分别是validAftervalidUntil。这两个返回值代表一个会话的有效开始时间和结束时间。

函数的执行流程如下:

  1. 首先,它使用account地址和一些参数调用allocateAssociatedStorageKey方法,这个方法看起来是用来生成一个与特定账户关联的存储键。

  2. 接着,它使用这个键和由sessionKeyallowedSelector组合而成的哈希值来调用associatedStorageLookup方法,这个方法看起来是用来在关联存储中查找特定的数据。

  3. 然后,它通过调用_castPtrToStruct方法将返回的数据转换为SessionInfo结构体的存储指针。

  4. 最后,函数从sessionInfo结构体中提取validAftervalidUntil字段的值,并将它们作为返回值。

因此,函数返回的是sessionInfo结构体中的validAftervalidUntil字段的值,这些值被用来表示某个会话的有效期限。这个函数不会修改任何状态(因为它被标记为view),它只是读取并返回存储在智能合约中的数据。

executeFromPlugin

unit test

test/mocks/plugins/ReturnDataPluginMocks.sol
function checkResultEFPFallback(bytes32 expected) external returns (bool) {
// 这个是从插件调用插件的方法
IPluginExecutor(msg.sender).executeFromPlugin(abi.encodeCall(ResultCreatorPlugin.foo, ()));

bytes32 actual = ResultCreatorPlugin(msg.sender).foo();

return actual == expected;
}

在这个函数中:

  • IPluginExecutor(msg.sender).executeFromPlugin(...) 这行代码意味着调用者(msg.sender)是一个实现了IPluginExecutor接口的合约。 这个合约能够执行一个名为executeFromPlugin的函数,该函数以特定的方式处理插件的调用。这里的abi.encodeCall(ResultCreatorPlugin.foo, ())是将ResultCreatorPluginfoo函数调用编码为可以由executeFromPlugin接收的格式。

  • ResultCreatorPlugin(msg.sender).foo() 这行代码则是直接从msg.sender(也就是调用checkResultEFPFallback的合约)上调用ResultCreatorPluginfoo方法。这意味着msg.sender也被视为ResultCreatorPlugin的一个实例。

这段代码的确有些混淆,因为它同时将msg.sender视为IPluginExecutorResultCreatorPlugin的实例。这通常意味着msg.sender是一个实现了两个接口的合约,或者至少是一个能够以这两种方式行事的代理合约。

但是,通常一个插件不会直接调用另一个插件的方法;相反,它会通过一个中心化的执行器来进行,以确保权限和安全性得到管理。因此,IPluginExecutor(msg.sender).executeFromPlugin(...) 这行代码可能是在委托给msg.sender(作为插件执行器)去执行ResultCreatorPlugin.foo的操作。

如果你的意思是这行代码与checkResultEFPFallback函数的其他部分无关联,那么可能需要更多上下文来确定它的作用。在智能合约的编程中,通常会有一个管理逻辑流程的合约,它负责调用其他合约的函数。这种模式有助于分离关注点,提高代码的安全性和可维护性。在这种情况下,executeFromPlugin可能是这样一个管理流程的函数。

这个函数返回值 只与 入参expected 和 计算出来的 actual 有关,在计算actual时候,并没有使用 IPluginExecutor(msg.sender).executeFromPlugin(abi.encodeCall(ResultCreatorPlugin.foo, ())); 这行代码

确实,从您提供的代码片段来看,checkResultEFPFallback 函数的返回值似乎只取决于传入的 expected 参数与 actual 变量的比较结果。actual 变量的值是通过调用 ResultCreatorPlugin(msg.sender).foo() 得到的。

然而,这里存在一个潜在的逻辑或设计问题。第一行代码:

IPluginExecutor(msg.sender).executeFromPlugin(abi.encodeCall(ResultCreatorPlugin.foo, ()));

这行代码调用了 msg.sender(假设它实现了 IPluginExecutor 接口)的 executeFromPlugin 方法,但是这个调用的结果似乎并没有被使用来影响函数的返回值。这可能意味着:

  1. executeFromPlugin 有副作用,比如改变合约状态,这些副作用可能间接影响 actual 变量的计算结果。
  2. 这行代码可能是多余的,如果它的执行结果不影响 actual 的值,也不影响函数的返回值。
  3. 可能存在一种情况,这行代码是用于权限检查或其他形式的预处理,即使它的直接结果不被使用。

如果 executeFromPlugin 的调用不影响 actual 的值,那么它可能是不必要的,除非它有其他的副作用。如果这行代码是故意放在那里的,那么可能需要更多的上下文来理解其目的。例如,它可能是用于触发某种状态变化或事件,这些在代码片段之外的其他地方有重要作用。

要准确地确定这行代码的意图和作用,通常需要审查与这段代码交互的合约的完整源代码,以及理解整个系统的设计和预期行为。在智能合约开发中,考虑合约的整体逻辑和状态是非常重要的,因为合约中的函数通常不是孤立存在的。

example

结合 src/account/UpgradeableModularAccount.sol 来看

src/account/UpgradeableModularAccount.sol

/// @inheritdoc IPluginExecutor
function executeFromPlugin(bytes calldata data) external payable override returns (bytes memory) {
bytes4 selector = bytes4(data[:4]);
address callingPlugin = msg.sender;

bytes24 execFromPluginKey = getPermittedCallKey(callingPlugin, selector);

AccountStorage storage _storage = getAccountStorage();

if (!_storage.callPermitted[execFromPluginKey]) {
revert ExecFromPluginNotPermitted(callingPlugin, selector);
}

address execFunctionPlugin = _storage.selectorData[selector].plugin;

if (execFunctionPlugin == address(0)) {
revert UnrecognizedFunction(selector);
}

PostExecToRun[] memory postExecHooks = _doPreExecHooks(selector, data);

(bool success, bytes memory returnData) = execFunctionPlugin.call(data);

if (!success) {
assembly ("memory-safe") {
revert(add(returnData, 32), mload(returnData))
}
}

_doCachedPostExecHooks(postExecHooks);

return returnData;
}

/// @inheritdoc IPluginExecutor
function executeFromPluginExternal(address target, uint256 value, bytes calldata data)
external
payable
returns (bytes memory)
{
bytes4 selector = bytes4(data);
AccountStorage storage _storage = getAccountStorage();

// Make sure plugin is allowed to spend native token.
if (value > 0 && value > msg.value && !_storage.pluginData[msg.sender].canSpendNativeToken) {
revert NativeTokenSpendingNotPermitted(msg.sender);
}

// Check the caller plugin's permission to make this call

// Check the target contract permission.
// This first checks that the intended target is permitted at all. If it is, then it checks if any selector
// is permitted. If any selector is permitted, then it skips the selector-level permission check.
// If only a subset of selectors are permitted, then it also checks the selector-level permission.
// By checking in the order of [address specified with any selector allowed], [any address allowed],
// [address specified and selector specified], along with the extra bool `permittedCall`, we can
// reduce the number of `sload`s in the worst-case from 3 down to 2.
bool targetContractPermittedCall = _storage.permittedExternalCalls[IPlugin(msg.sender)][target]
.addressPermitted
&& (
_storage.permittedExternalCalls[IPlugin(msg.sender)][target].anySelectorPermitted
|| _storage.permittedExternalCalls[IPlugin(msg.sender)][target].permittedSelectors[selector]
);

// If the target contract is not permitted, check if the caller plugin is permitted to make any external
// calls.
if (!(targetContractPermittedCall || _storage.pluginData[msg.sender].anyExternalExecPermitted)) {
revert ExecFromPluginExternalNotPermitted(msg.sender, target, value, data);
}

// Run any pre exec hooks for this selector
PostExecToRun[] memory postExecHooks =
_doPreExecHooks(IPluginExecutor.executeFromPluginExternal.selector, msg.data);

// Perform the external call
bytes memory returnData = _exec(target, value, data);

// Run any post exec hooks for this selector
_doCachedPostExecHooks(postExecHooks);

return returnData;
}

executeFromPluginUpgradeableModularAccount 合约的一个方法,它允许一个已注册并且被授权的插件(智能合约)调用该合约中的特定功能。这是一个实现了模块化智能合约系统的例子,其中插件可以扩展主合约的功能,而不需要改变主合约的代码。

在这个系统中,executeFromPlugin 方法的作用是:

  1. 权限检查:首先检查调用者(即插件合约)是否有权执行请求的函数。这是通过检查一个权限映射(callPermitted)来实现的,该映射记录了哪些插件被允许调用哪些函数选择器(selector)。

  2. 执行调用:如果调用者有权执行函数,则查找与请求的函数选择器关联的插件地址,并将调用转发到该插件。这允许插件执行它被授权的操作。

  3. 前置和后置钩子executeFromPlugin 方法在执行请求的函数之前和之后,可以运行一系列的钩子(hooks)。这些钩子可以用于各种目的,例如验证、状态更新、事件记录等。这是通过 _doPreExecHooks_doCachedPostExecHooks 方法实现的。

  4. 错误处理:如果插件中的调用失败,该方法会将失败的原因(错误信息)冒泡上去,使得调用者可以知道为什么调用失败。

  5. 返回数据:如果调用成功,executeFromPlugin 会返回插件函数调用的返回数据。

在智能合约的上下文中,这种模式允许开发者创建一个可扩展的合约架构,其中核心功能可以通过安装新的插件来增强,而不必每次都升级整个合约。这种方法还可以用来限制插件的权限,确保它们只能调用它们被授权的函数,从而提高系统的安全性。

在这个特定的实现中,UpgradeableModularAccount 合约可能是一个用户账户代理,它可以通过入口点(IEntryPoint)来处理用户的操作,并且可以通过插件来扩展其功能。这种模式在以太坊的账户抽象化和智能合约升级策略中是非常有用的,因为它提供了更高的灵活性和安全性。

参考链接

  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