diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d00625581..bda74a5223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: cd $GITHUB_WORKSPACE rm -rf ERCs - name: Setup Ruby - uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0 + uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0 with: ruby-version: '3.1' # Not needed with a .ruby-version file bundler-cache: true # runs 'bundle install' and caches installed gems automatically @@ -136,7 +136,7 @@ jobs: - name: Checkout EIP Repository uses: actions/checkout@47fbe2df0ad0e27efb67a70beac3555f192b062f - - uses: ethereum/eipw-action@6785fa283773db4819cb0abf17a185a1d9b6eb9f + - uses: ethereum/eipw-action@be3fa642ec311d0b8e1fdb811e5c9b4ada3d3d93 id: eipw with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml index a646532c71..32baad23e7 100644 --- a/.github/workflows/jekyll.yml +++ b/.github/workflows/jekyll.yml @@ -34,7 +34,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Setup Ruby - uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0 + uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0 with: ruby-version: '3.1' # Not needed with a .ruby-version file bundler-cache: true # runs 'bundle install' and caches installed gems automatically diff --git a/ERCS/erc-6900.md b/ERCS/erc-6900.md index 6cd2d28886..1cbc11d32f 100644 --- a/ERCS/erc-6900.md +++ b/ERCS/erc-6900.md @@ -1,19 +1,19 @@ --- eip: 6900 -title: Modular Smart Contract Accounts and Plugins -description: Interfaces for composable contract accounts optionally supporting upgradability and introspection -author: Adam Egyed (@adamegyed), Fangting Liu (@trinity-0111), Jay Paik (@jaypaik), Yoav Weiss (@yoavw), Huawei Gu (@huaweigu), Daniel Lim (@dlim-circle), Zhiyu Zhang (@ZhiyuCircle), Ruben Koch (@0xrubes), David Philipson (@dphilipson), Howy Ho (@howydev) +title: Modular Smart Contract Accounts +description: Interfaces for smart contract accounts and modules, optionally supporting upgradability and introspection +author: Adam Egyed (@adamegyed), Fangting Liu (@trinity-0111), Jay Paik (@jaypaik), Yoav Weiss (@yoavw), Huawei Gu (@huaweigu), Daniel Lim (@dlim-circle), Ruben Koch (@0xrubes), David Philipson (@dphilipson), Howy Ho (@howydev), Nikita Belenkov (@nikita-quantstamp), zer0dot (@zer0dot), David Kim (@PowerStream3604) discussions-to: https://ethereum-magicians.org/t/eip-modular-smart-contract-accounts-and-plugins/13885 status: Draft type: Standards Track category: ERC created: 2023-04-18 -requires: 165, 4337 +requires: 165, 1271, 4337 --- ## Abstract -This proposal standardizes smart contract accounts and account plugins, which are smart contract interfaces that allow for composable logic within smart contract accounts. This proposal is compliant with [ERC-4337](./eip-4337.md), and takes inspiration from [ERC-2535](./eip-2535.md) when defining interfaces for updating and querying modular function implementations. +This proposal standardizes smart contract accounts and account modules, which are smart contracts that allow for composable logic within smart contract accounts. This proposal is compliant with [ERC-4337](./eip-4337.md). This standard emphasizes secure permissioning of modules, and maximal interoperability between all spec-compliant accounts and modules. This modular approach splits account functionality into three categories, implements them in external contracts, and defines an expected execution flow from accounts. @@ -21,24 +21,17 @@ This modular approach splits account functionality into three categories, implem One of the goals that ERC-4337 accomplishes is abstracting the logic for execution and validation to each smart contract account. -Many new features of accounts can be built by customizing the logic that goes into the validation and execution steps. Examples of such features include session keys, subscriptions, spending limits, and role-based access control. Currently, some of these features are implemented natively by specific smart contract accounts, and others are able to be implemented by plugin systems. Examples of proprietary plugin systems include Safe modules and ZeroDev plugins. +Many new features of accounts can be built by customizing the logic that goes into the validation and execution steps. Examples of such features include session keys, subscriptions, spending limits, and role-based access control. Currently, some of these features are implemented natively by specific smart contract accounts, and others are able to be implemented by proprietary module systems like Safe modules. -However, managing multiple account instances provides a worse user experience, fragmenting accounts across supported features and security configurations. Additionally, it requires plugin developers to choose which platforms to support, causing either platform lock-in or duplicated development effort. +However, managing multiple account implementations provides a poor user experience, fragmenting accounts across supported features and security configurations. Additionally, it requires module developers to choose which platforms to support, causing either platform lock-in or duplicated development effort. -We propose a standard that coordinates the implementation work between plugin developers and wallet developers. This standard defines a modular smart contract account capable of supporting all standard-conformant plugins. This allows users to have greater portability of their data, and for plugin developers to not have to choose specific account implementations to support. +We propose a standard that coordinates the implementation work between module developers and account developers. This standard defines a modular smart contract account capable of supporting all standard-conformant modules. This allows users to have greater portability of their data, and for module developers to not have to choose specific account implementations to support. -![diagram showing relationship between accounts and plugins with modular functions](../assets/eip-6900/MSCA_Shared_Components_Diagram.svg) +![diagram showing relationship between accounts and modules with modular functions](../assets/eip-6900/MSCA_Shared_Components_Diagram.svg) -We take inspiration from ERC-2535's diamond pattern for routing execution based on function selectors, and create a similarly composable account. However, the standard does not require the multi-facet proxy pattern. +These modules can contain execution logic, validation functions, and hooks. Validation functions define the circumstances under which the smart contract account will approve actions taken on its behalf, while hooks allow for pre and post execution controls. -These plugins can contain execution logic, validation schemes, and hooks. Validation schemes define the circumstances under which the smart contract account will approve actions taken on its behalf, while hooks allow for pre- and post-execution controls. - -Accounts adopting this standard will support modular, upgradable execution and validation logic. Defining this as a standard for smart contract accounts will make plugins easier to develop securely and will allow for greater interoperability. - -Goals: - -- Provide standards for how validation, execution, and hook functions for smart contract accounts should be written. -- Provide standards for how compliant accounts should add, update, remove, and inspect plugins. +Accounts adopting this standard will support modular, upgradable execution and validation logic. Defining this as a standard for smart contract accounts will make modules easier to develop securely and will allow for greater interoperability. ## Specification @@ -48,105 +41,86 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S - An **account** (or **smart contract account, SCA**) is a smart contract that can be used to send transactions and hold digital assets. It implements the `IAccount` interface from ERC-4337. - A **modular account** (or **modular smart contract account, MSCA**) is an account that supports modular functions. There are three types of modular functions: - - **Validation functions** validate the caller's authenticity and authority to the account. - - **Execution functions** execute any custom logic allowed by the account. - - **Hooks** execute custom logic and checks before and/or after an execution function or validation function. -- A **validation function** is a function that validates authentication and authorization of a caller to the account. There are two types of validation functions: - - **User Operation Validation** functions handle calls to `validateUserOp` and check the validity of an ERC-4337 user operation. - - **Runtime Validation** functions run before an execution function when not called via a user operation, and enforce checks. Common checks include allowing execution only by an owner. -- An **execution function** is a smart contract function that defines the main execution step of a function for a modular account. -- The **standard execute** functions are two specific execute functions that are implemented natively by the modular account, and not on a plugin. These allow for open-ended execution. -- A **hook** is a smart contract function executed before or after another function, with the ability to modify state or cause the entire call to revert. There are four types of hooks: - - **Pre User Operation Validation Hook** functions run before user operation validation functions. These can enforce permissions on what actions a validation function may perform via user operations. - - **Pre Runtime Validation Hook** functions run before runtime validation functions. These can enforce permissions on what actions a validation function may perform via direct calls. - - **Pre Execution Hook** functions run before an execution function. They may optionally return data to be consumed by their related post execution hook functions. - - **Post Execution Hook** functions run after an execution function. They may optionally take returned data from their related pre execution hook functions. -- An **associated function** refers to either a validation function or a hook. -- A **native function** refers to a function implemented natively by the modular account, as opposed to a function added by a plugin. -- A **plugin** is a deployed smart contract that hosts any amount of the above three kinds of modular functions: execution functions, validation functions, or hooks. -- A plugin **manifest** is responsible for describing the execution functions, validation functions, and hooks that will be configured on the MSCA during installation, as well as the plugin’s metadata, dependency requirements, and permissions. + - **Validation functions** validate authorization on behalf of the account. + - **Execution functions** execute custom logic allowed by the account. + - **Hooks** execute custom logic and checks before and/or after an execution function or validation function. There are two types of hooks: + - **Validation hooks** run before a validation function. These can enforce permissions on actions authorized by a validation function. + - **Execution hooks** can run before and/or after an execution function. Execution hooks can be attached either to a specific execution function or a validation function. A pre execution hook may optionally return data to be consumed by a post execution hook. +- A **native function** refers to a function implemented by the modular account, as opposed to a function added by a module. +- A **module** is a deployed smart contract that hosts any amount of the above three kinds of modular functions. +- A module's **manifest** describes the execution functions, interface IDs, and hooks that should be installed on the account. ### Overview -A modular account handles two kinds of calls: either from the `Entrypoint` through ERC-4337, or through direct calls from externally owned accounts (EOAs) and other smart contracts. This standard supports both use cases. - -A call to the smart contract account can be broken down into the steps as shown in the diagram below. The validation steps validate if the caller is allowed to perform the call. The pre execution hook step can be used to do any pre execution checks or updates. It can also be used along with the post execution hook step to perform additional actions or verification. The execution step performs a defined task or collection of tasks. +A modular account handles two kinds of calls: either from the `EntryPoint` through ERC-4337, or through direct calls from externally owned accounts (EOAs) and other smart contracts. This standard supports both use cases. -![diagram showing call flow within an modular account](../assets/eip-6900/Modular_Account_Call_Flow.svg) +A call to the modular account can be broken down into the steps as shown in the diagram below. The validation steps validate if the caller is allowed to perform the call. The pre execution hook step can be used to do any pre execution checks or updates. It can also be used along with the post execution hook step to perform additional actions or verification. The execution step performs a defined task or collection of tasks. -The following diagram shows permitted plugin execution flows. During a plugin's execution step from the above diagram, the plugin may perform a "Plugin Execution Function", using either `executeFromPlugin` or `executeFromPluginExternal`. These can be used by plugins to execute using the account's context. +![diagram showing call flow within a modular account](../assets/eip-6900/Modular_Account_Call_Flow.svg) -- `executeFromPlugin` handles calls to other installed plugin's execution function on the modular account. -- `executeFromPluginExternal` handles calls to external addresses. - -![diagram showing a plugin execution flow](../assets/eip-6900/Plugin_Execution_Flow.svg) - -Each step is modular, supporting different implementations for each execution function, and composable, supporting multiple steps through hooks. Combined, these allow for open-ended programmable accounts. +Each step is modular, supporting different implementations, which allows for open-ended programmable accounts. ### Interfaces -**Modular Smart Contract Accounts** **MUST** implement - -- `IAccount.sol` from [ERC-4337](./eip-4337.md). -- `IPluginManager.sol` to support installing and uninstalling plugins. -- `IStandardExecutor.sol` to support open-ended execution. **Calls to plugins through this SHOULD revert.** -- `IPluginExecutor.sol` to support execution from plugins. **Calls to plugins through `executeFromPluginExternal` SHOULD revert.** - -**Modular Smart Contract Accounts** **MAY** implement +Modular accounts MUST implement: -- `IAccountLoupe.sol` to support visibility in plugin configuration on-chain. +- `IAccount.sol` and `IAccountExecute.sol` from [ERC-4337](./eip-4337.md). +- `IModularAccount.sol` to support module management and usage, and account identification. +- The function `isValidSignature` from [ERC-1271](./eip-1271.md) -**Plugins** **MUST** implement +Modular accounts MAY implement: -- `IPlugin.sol` described below and implement [ERC-165](./eip-165.md) for `IPlugin`. +- `IModularAccountView.sol` to support visibility in account states on-chain. +- [ERC-165](./eip-165.md) for interfaces installed from modules. -#### `IPluginManager.sol` +Modules MUST implement: -Plugin manager interface. Modular Smart Contract Accounts **MUST** implement this interface to support installing and uninstalling plugins. +- `IModule.sol` described below and implement ERC-165 for `IModule`. -```solidity -// Treats the first 20 bytes as an address, and the last byte as a function identifier. -type FunctionReference is bytes21; - -interface IPluginManager { - event PluginInstalled(address indexed plugin, bytes32 manifestHash, FunctionReference[] dependencies); - - event PluginUninstalled(address indexed plugin, bool indexed onUninstallSucceeded); - - /// @notice Install a plugin to the modular account. - /// @param plugin The plugin to install. - /// @param manifestHash The hash of the plugin manifest. - /// @param pluginInstallData Optional data to be decoded and used by the plugin to setup initial plugin data - /// for the modular account. - /// @param dependencies The dependencies of the plugin, as described in the manifest. Each FunctionReference - /// MUST be composed of an installed plugin's address and a function ID of its validation function. - function installPlugin( - address plugin, - bytes32 manifestHash, - bytes calldata pluginInstallData, - FunctionReference[] calldata dependencies - ) external; - - /// @notice Uninstall a plugin from the modular account. - /// @param plugin The plugin to uninstall. - /// @param config An optional, implementation-specific field that accounts may use to ensure consistency - /// guarantees. - /// @param pluginUninstallData Optional data to be decoded and used by the plugin to clear plugin data for the - /// modular account. - function uninstallPlugin(address plugin, bytes calldata config, bytes calldata pluginUninstallData) external; -} - -``` +Modules MAY implement any of the following module types: -#### `IStandardExecutor.sol` +- `IValidationModule` to support validation functions for the account. +- `IValidationHookModule` to support hooks for validation functions. +- `IExecutionModule` to support execution functions and their installations on the account. +- `IExecutionHookModule` to support pre & post execution hooks for execution functions. -Standard execute interface. Modular Smart Contract Accounts **MUST** implement this interface to support open-ended execution. +#### `IModularAccount.sol` -Standard execute functions SHOULD check whether the call's target implements the `IPlugin` interface via ERC-165. - -**If the target is a plugin, the call SHOULD revert.** This prevents accidental misconfiguration or misuse of plugins (both installed and uninstalled). +Module execution and management interface. Modular accounts MUST implement this interface to support installing and uninstalling modules, and open-ended execution. ```solidity +/// @dev A packed representation of a module function. +/// Consists of the following, left-aligned: +/// Module address: 20 bytes +/// Entity ID: 4 bytes +type ModuleEntity is bytes24; + +/// @dev A packed representation of a validation function and its associated flags. +/// Consists of the following, left-aligned: +/// Module address: 20 bytes +/// Entity ID: 4 bytes +/// Flags: 1 byte +/// +/// Validation flags layout: +/// 0b00000___ // unused +/// 0b_____A__ // isGlobal +/// 0b______B_ // isSignatureValidation +/// 0b_______C // isUserOpValidation +type ValidationConfig is bytes25; + +/// @dev A packed representation of a hook function and its associated flags. +/// Consists of the following, left-aligned: +/// Module address: 20 bytes +/// Entity ID: 4 bytes +/// Flags: 1 byte +/// +/// Hook flags layout: +/// 0b00000___ // unused +/// 0b_____A__ // hasPre (exec only) +/// 0b______B_ // hasPost (exec only) +/// 0b_______C // hook type (0 for exec, 1 for validation) +type HookConfig is bytes25; + struct Call { // The target address for the account to call. address target; @@ -156,393 +130,490 @@ struct Call { bytes data; } -interface IStandardExecutor { +interface IModularAccount { + event ExecutionInstalled(address indexed module, ExecutionManifest manifest); + event ExecutionUninstalled(address indexed module, bool onUninstallSucceeded, ExecutionManifest manifest); + event ValidationInstalled(address indexed module, uint32 indexed entityId); + event ValidationUninstalled(address indexed module, uint32 indexed entityId, bool onUninstallSucceeded); + /// @notice Standard execute method. - /// @dev If the target is a plugin, the call SHOULD revert. - /// @param target The target address for account to call. + /// @param target The target address for the account to call. /// @param value The value to send with the call. /// @param data The calldata for the call. /// @return The return data from the call. function execute(address target, uint256 value, bytes calldata data) external payable returns (bytes memory); /// @notice Standard executeBatch method. - /// @dev If the target is a plugin, the call SHOULD revert. If any of the calls revert, the entire batch MUST + /// @dev If the target is a module, the call SHOULD revert. If any of the calls revert, the entire batch MUST /// revert. /// @param calls The array of calls. /// @return An array containing the return data from the calls. function executeBatch(Call[] calldata calls) external payable returns (bytes[] memory); -} -``` - -#### `IPluginExecutor.sol` -Execution interface for calls made from plugins. Modular Smart Contract Accounts **MUST** implement this interface to support execution from plugins. + /// @notice Execute a call using the specified runtime validation. + /// @param data The calldata to send to the account. + /// @param authorization The authorization data to use for the call. The first 24 bytes is a ModuleEntity which + /// specifies which runtime validation to use, and the rest is sent as a parameter to runtime validation. + function executeWithRuntimeValidation(bytes calldata data, bytes calldata authorization) + external + payable + returns (bytes memory); -The `executeFromPluginExternal` function SHOULD check whether the call's target implements the `IPlugin` interface via ERC-165. + /// @notice Install a module to the modular account. + /// @param module The module to install. + /// @param manifest the manifest describing functions to install. + /// @param installData Optional data to be used by the account to handle the initial execution setup. Data encoding + /// is implementation-specific. + function installExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata installData + ) external; -**If the target of `executeFromPluginExternal` function is a plugin, the call SHOULD revert.** + /// @notice Uninstall a module from the modular account. + /// @param module The module to uninstall. + /// @param manifest the manifest describing functions to uninstall. + /// @param uninstallData Optional data to be used by the account to handle the execution uninstallation. Data + /// encoding is implementation-specific. + function uninstallExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata uninstallData + ) external; -This prevents accidental misconfiguration or misuse of plugins (both installed and uninstalled). Installed plugins MAY interact with other installed plugins via the `executeFromPlugin` function. + /// @notice Installs a validation function across a set of execution selectors, and optionally mark it as a + /// global validation function. + /// @dev This does not validate anything against the manifest - the caller must ensure validity. + /// @param validationConfig The validation function to install, along with configuration flags. + /// @param selectors The selectors to install the validation function for. + /// @param installData Optional data to be used by the account to handle the initial validation setup. Data + /// encoding is implementation-specific. + /// @param hooks Optional hooks to install and associate with the validation function. Data encoding is + /// implementation-specific. + function installValidation( + ValidationConfig validationConfig, + bytes4[] calldata selectors, + bytes calldata installData, + bytes[] calldata hooks + ) external; -```solidity -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 Uninstall a validation function from a set of execution selectors. + /// @param validationFunction The validation function to uninstall. + /// @param uninstallData Optional data to be used by the account to handle the validation uninstallation. Data + /// encoding is implementation-specific. + /// @param hookUninstallData Optional data to be used by the account to handle hook uninstallation. Data encoding + /// is implementation-specific. + function uninstallValidation( + ModuleEntity validationFunction, + bytes calldata uninstallData, + bytes[] calldata hookUninstallData + ) external; - /// @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); + /// @notice Return a unique identifier for the account implementation. + /// @dev This function MUST return a string in the format "vendor.account.semver". The vendor and account + /// names MUST NOT contain a period character. + /// @return The account ID. + function accountId() external view returns (string memory); } ``` -#### `IAccountLoupe.sol` +#### `IModularAccountView.sol` -Plugin inspection interface. Modular Smart Contract Accounts **MAY** implement this interface to support visibility in plugin configuration on-chain. +Module inspection interface. Modular accounts MAY implement this interface to support visibility in module configuration. ```solidity -interface IAccountLoupe { - /// @notice Config for an execution function, given a selector. - struct ExecutionFunctionConfig { - address plugin; - FunctionReference userOpValidationFunction; - FunctionReference runtimeValidationFunction; - } - - /// @notice Pre and post hooks for a given selector. - /// @dev It's possible for one of either `preExecHook` or `postExecHook` to be empty. - struct ExecutionHooks { - FunctionReference preExecHook; - FunctionReference postExecHook; - } - - /// @notice Get the validation functions and plugin address for a selector. - /// @dev If the selector is a native function, the plugin address will be the address of the account. - /// @param selector The selector to get the configuration for. - /// @return The configuration for this selector. - function getExecutionFunctionConfig(bytes4 selector) external view returns (ExecutionFunctionConfig memory); - - /// @notice Get the pre and post execution hooks for a selector. - /// @param selector The selector to get the hooks for. - /// @return The pre and post execution hooks for this selector. - function getExecutionHooks(bytes4 selector) external view returns (ExecutionHooks[] memory); - - /// @notice Get the pre user op and runtime validation hooks associated with a selector. - /// @param selector The selector to get the hooks for. - /// @return preUserOpValidationHooks The pre user op validation hooks for this selector. - /// @return preRuntimeValidationHooks The pre runtime validation hooks for this selector. - function getPreValidationHooks(bytes4 selector) +/// @dev Represents data associated with a specific function selector. +struct ExecutionDataView { + // The module that implements this execution function. + // If this is a native function, the address must remain address(0). + address module; + // Whether or not the function needs runtime validation, or can be called by anyone. The function can still be + // state changing if this flag is set to true. + // Note that even if this is set to true, user op validation will still be required, otherwise anyone could + // drain the account of native tokens by wasting gas. + bool skipRuntimeValidation; + // Whether or not a global validation function may be used to validate this function. + bool allowGlobalValidation; + // The execution hooks for this function selector. + HookConfig[] executionHooks; +} + +struct ValidationDataView { + // Whether or not this validation function can be used as a global validation function. + bool isGlobal; + // Whether or not this validation function is a signature validator. + bool isSignatureValidation; + // Whether or not this validation function is a user operation validation function. + bool isUserOpValidation; + // The validation hooks for this validation function. + HookConfig[] validationHooks; + // Execution hooks to run with this validation function. + HookConfig[] executionHooks; + // The set of selectors that may be validated by this validation function. + bytes4[] selectors; +} + +interface IModularAccountView { + /// @notice Get the execution data for a selector. + /// @dev If the selector is a native function, the module address will be the address of the account. + /// @param selector The selector to get the data for. + /// @return The execution data for this selector. + function getExecutionData(bytes4 selector) external view returns (ExecutionDataView memory); + + /// @notice Get the validation data for a validation function. + /// @dev If the selector is a native function, the module address will be the address of the account. + /// @param validationFunction The validation function to get the data for. + /// @return The validation data for this validation function. + function getValidationData(ModuleEntity validationFunction) external view - returns ( - FunctionReference[] memory preUserOpValidationHooks, - FunctionReference[] memory preRuntimeValidationHooks - ); - - /// @notice Get an array of all installed plugins. - /// @return The addresses of all installed plugins. - function getInstalledPlugins() external view returns (address[] memory); + returns (ValidationDataView memory); } ``` -#### `IPlugin.sol` +#### `IModule.sol` -Plugin interface. Plugins **MUST** implement this interface to support plugin management and interactions with MSCAs. +Module interface. Modules MUST implement this interface to support module management and interactions with [ERC-6900](./eip-6900.md) modular accounts. ```solidity -interface IPlugin { - /// @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. +interface IModule is IERC165 { + /// @notice Initialize module data for the modular account. + /// @dev Called by the modular account during `installExecution`. + /// @param data Optional bytes array to be decoded and used by the module to setup initial module data for the + /// modular account. function onInstall(bytes calldata data) external; - /// @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. + /// @notice Clear module data for the modular account. + /// @dev Called by the modular account during `uninstallExecution`. + /// @param data Optional bytes array to be decoded and used by the module to clear module data for the modular + /// account. function onUninstall(bytes calldata data) external; - /// @notice Run the pre user operation validation hook specified by the `functionId`. - /// @dev Pre user operation validation hooks MUST NOT return an authorizer value other than 0 or 1. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. - /// @param userOp The user operation. - /// @param userOpHash The user operation hash. - /// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes). - function preUserOpValidationHook(uint8 functionId, UserOperation memory userOp, bytes32 userOpHash) external returns (uint256); + /// @notice Return a unique identifier for the module. + /// @dev This function MUST return a string in the format "vendor.module.semver". The vendor and module + /// names MUST NOT contain a period character. + /// @return The module ID. + function moduleId() external view returns (string memory); +} +``` + +#### `IValidationModule.sol` + +Validation module interface. Modules MAY implement this interface to provide validation functions for the account. - /// @notice Run the user operation validationFunction specified by the `functionId`. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. +```solidity +interface IValidationModule is IModule { + /// @notice Run the user operation validation function specified by the `entityId`. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param userOp The user operation. /// @param userOpHash The user operation hash. /// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes). - function userOpValidationFunction(uint8 functionId, UserOperation calldata userOp, bytes32 userOpHash) + function validateUserOp(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); - /// @notice Run the pre runtime validation hook specified by the `functionId`. + /// @notice Run the runtime validation function specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. + /// @param account the account to validate for. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. - function preRuntimeValidationHook(uint8 functionId, address sender, uint256 value, bytes calldata data) external; + /// @param authorization Additional data for the validation function to use. + function validateRuntime( + address account, + uint32 entityId, + address sender, + uint256 value, + bytes calldata data, + bytes calldata authorization + ) external; - /// @notice Run the runtime validationFunction specified by the `functionId`. + /// @notice Validates a signature using ERC-1271. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be - /// more than one. - /// @param sender The caller address. - /// @param value The call value. - /// @param data The calldata sent. - function runtimeValidationFunction(uint8 functionId, address sender, uint256 value, bytes calldata data) - external; + /// @param account the account to validate for. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. + /// @param sender the address that sent the ERC-1271 request to the smart account + /// @param hash the hash of the ERC-1271 request + /// @param signature the signature of the ERC-1271 request + /// @return The ERC-1271 `MAGIC_VALUE` if the signature is valid, or 0xFFFFFFFF if invalid. + function validateSignature( + address account, + uint32 entityId, + address sender, + bytes32 hash, + bytes calldata signature + ) external view returns (bytes4); +} +``` - /// @notice Run the pre execution hook specified by the `functionId`. +#### `IValidationHookModule.sol` + +Validation hook module interface. Modules MAY implement this interface to provide hooks for validation functions for the account. + +```solidity +interface IValidationHookModule is IModule { + /// @notice Run the pre user operation validation hook specified by the `entityId`. + /// @dev Pre user operation validation hooks MUST NOT return an authorizer value other than 0 or 1. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. + /// @param userOp The user operation. + /// @param userOpHash The user operation hash. + /// @return Packed validation data for validAfter (6 bytes), validUntil (6 bytes), and authorizer (20 bytes). + function preUserOpValidationHook(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash) + external + returns (uint256); + + /// @notice Run the pre runtime validation hook specified by the `entityId`. /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. /// @param sender The caller address. /// @param value The call value. /// @param data The calldata sent. - /// @return Context to pass to a post execution hook, if present. An empty bytes array MAY be returned. - function preExecutionHook(uint8 functionId, address sender, uint256 value, bytes calldata data) external returns (bytes memory); - - /// @notice Run the post execution hook specified by the `functionId`. - /// @dev To indicate the entire call should revert, the function MUST revert. - /// @param functionId An identifier that routes the call to different internal implementations, should there be more than one. - /// @param preExecHookData The context returned by its associated pre execution hook. - function postExecutionHook(uint8 functionId, bytes calldata preExecHookData) external; - - /// @notice Describe the contents and intended configuration of the plugin. - /// @dev This manifest MUST stay constant over time. - /// @return A manifest describing the contents and intended configuration of the plugin. - function pluginManifest() external pure returns (PluginManifest memory); + /// @param authorization Additional data for the hook to use. + function preRuntimeValidationHook( + uint32 entityId, + address sender, + uint256 value, + bytes calldata data, + bytes calldata authorization + ) external; - /// @notice Describe the metadata of the plugin. - /// @dev This metadata MUST stay constant over time. - /// @return A metadata struct describing the plugin. - function pluginMetadata() external pure returns (PluginMetadata memory); + /// @notice Run the pre signature validation hook specified by the `entityId`. + /// @dev To indicate the call should revert, the function MUST revert. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. + /// @param sender The caller address. + /// @param hash The hash of the message being signed. + /// @param signature The signature of the message. + function preSignatureValidationHook(uint32 entityId, address sender, bytes32 hash, bytes calldata signature) + external + view; } ``` -### Plugin manifest +#### `IExecutionModule.sol` -The plugin manifest is responsible for describing the execution functions, validation functions, and hooks that will be configured on the MSCA during installation, as well as the plugin's metadata, dependencies, and permissions. +Execution module interface. Modules MAY implement this interface to provide execution functions for the account. ```solidity -enum ManifestAssociatedFunctionType { - // Function is not defined. - NONE, - // Function belongs to this plugin. - SELF, - // Function belongs to an external plugin provided as a dependency during plugin installation. Plugins MAY depend - // on external validation functions. It MUST NOT depend on external hooks, or installation will fail. - DEPENDENCY, - // Resolves to a magic value to always bypass runtime validation for a given function. - // This is only assignable on runtime validation functions. If it were to be used on a user op validationFunction, - // it would risk burning gas from the account. When used as a hook in any hook location, it is equivalent to not - // setting a hook and is therefore disallowed. - RUNTIME_VALIDATION_ALWAYS_ALLOW, - // Resolves to a magic value to always fail in a hook for a given function. - // This is only assignable to pre hooks (pre validation and pre execution). It should not be used on - // validation functions themselves, because this is equivalent to leaving the validation functions unset. - // It should not be used in post-exec hooks, because if it is known to always revert, that should happen - // as early as possible to save gas. - PRE_HOOK_ALWAYS_DENY -} - -/// @dev For functions of type `ManifestAssociatedFunctionType.DEPENDENCY`, the MSCA MUST find the plugin address -/// of the function at `dependencies[dependencyIndex]` during the call to `installPlugin(config)`. -struct ManifestFunction { - ManifestAssociatedFunctionType functionType; - uint8 functionId; - uint256 dependencyIndex; -} - -struct ManifestAssociatedFunction { +struct ManifestExecutionFunction { + // The selector to install bytes4 executionSelector; - ManifestFunction associatedFunction; + // If true, the function won't need runtime validation, and can be called by anyone. + bool skipRuntimeValidation; + // If true, the function can be validated by a global validation function. + bool allowGlobalValidation; } struct ManifestExecutionHook { - bytes4 selector; - ManifestFunction preExecHook; - ManifestFunction postExecHook; + bytes4 executionSelector; + uint32 entityId; + bool isPreHook; + bool isPostHook; } -struct ManifestExternalCallPermission { - address externalAddress; - bool permitAnySelector; - bytes4[] selectors; +/// @dev A struct describing how the module should be installed on a modular account. +struct ExecutionManifest { + // Execution functions defined in this module to be installed on the MSCA. + ManifestExecutionFunction[] executionFunctions; + ManifestExecutionHook[] executionHooks; + // List of ERC-165 interface IDs to add to account to support introspection checks. This MUST NOT include + // IModule's interface ID. + bytes4[] interfaceIds; } -struct SelectorPermission { - bytes4 functionSelector; - string permissionDescription; +interface IExecutionModule is IModule { + /// @notice Describe the contents and intended configuration of the module. + /// @dev This manifest MUST stay constant over time. + /// @return A manifest describing the contents and intended configuration of the module. + function executionManifest() external pure returns (ExecutionManifest memory); } +``` -/// @dev A struct holding fields to describe the plugin in a purely view context. Intended for front end clients. -struct PluginMetadata { - // A human-readable name of the plugin. - string name; - // The version of the plugin, following the semantic versioning scheme. - string version; - // The author field SHOULD be a username representing the identity of the user or organization - // that created this plugin. - string author; - // String descriptions of the relative sensitivity of specific functions. The selectors MUST be selectors for - // functions implemented by this plugin. - SelectorPermission[] permissionDescriptors; -} +#### `IExecutionHookModule.sol` -/// @dev A struct describing how the plugin should be installed on a modular account. -struct PluginManifest { - // List of ERC-165 interface IDs to add to account to support introspection checks. This MUST NOT include - // IPlugin's interface ID. - bytes4[] interfaceIds; - // If this plugin depends on other plugins' validation functions, the interface IDs of those plugins MUST be - // provided here, with its position in the array matching the `dependencyIndex` members of `ManifestFunction` - // structs used in the manifest. - bytes4[] dependencyInterfaceIds; - // Execution functions defined in this plugin to be installed on the MSCA. - bytes4[] executionFunctions; - // Plugin execution functions already installed on the MSCA that this plugin will be able to call. - bytes4[] permittedExecutionSelectors; - // Boolean to indicate whether the plugin can call any external address. - bool permitAnyExternalAddress; - // Boolean to indicate whether the plugin needs access to spend native tokens of the account. If false, the - // plugin MUST still be able to spend up to the balance that it sends to the account in the same call. - bool canSpendNativeToken; - ManifestExternalCallPermission[] permittedExternalCalls; - ManifestAssociatedFunction[] userOpValidationFunctions; - ManifestAssociatedFunction[] runtimeValidationFunctions; - ManifestAssociatedFunction[] preUserOpValidationHooks; - ManifestAssociatedFunction[] preRuntimeValidationHooks; - ManifestExecutionHook[] executionHooks; -} +Execution hook module interface. Modules MAY implement this interface to provide hooks for execution functions for the account. + +```solidity +interface IExecutionHookModule is IModule { + /// @notice Run the pre execution hook specified by the `entityId`. + /// @dev To indicate the entire call should revert, the function MUST revert. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. + /// @param sender The caller address. + /// @param value The call value. + /// @param data The calldata sent. For `executeUserOp` calls, hook modules should receive the full msg.data. + /// @return Context to pass to a post execution hook, if present. An empty bytes array MAY be returned. + function preExecutionHook(uint32 entityId, address sender, uint256 value, bytes calldata data) + external + returns (bytes memory); + /// @notice Run the post execution hook specified by the `entityId`. + /// @dev To indicate the entire call should revert, the function MUST revert. + /// @param entityId An identifier that routes the call to different internal implementations, should there + /// be more than one. + /// @param preExecHookData The context returned by its associated pre execution hook. + function postExecutionHook(uint32 entityId, bytes calldata preExecHookData) external; +} ``` -### Expected behavior +### Validation Functions and Their Installation/Uninstallation + +- An account can have more than one validation module/function installed. +- An account can have the same validation module installed more than once. +- The entity ID of a validation function installed on an account MUST be unique. +- Validation installation MAY be deferred until a later time, such as upon first use. -#### Responsibilities of `StandardExecutor` and `PluginExecutor` +#### Installation -`StandardExecutor` functions are used for open-ended calls to external addresses. +During validation installation, the account MUST correctly set flags and other fields based on the incoming data provided by the user. -`PluginExecutor` functions are specifically used by plugins to request the account to execute with account's context. Explicit permissions are required for plugins to use `PluginExecutor`. +- The account MUST install all validation hooks specified by the user and SHOULD call `onInstall` with the user-provided data on the hook module to initialize state if specified by the user. +- The account MUST install all execution hooks specified by the user and SHOULD call `onInstall` with the user-provided data on the hook module to initialize state if specified by the user. +- The account MUST configure the validation function to validate all of the selectors specified by the user. +- The account MUST set all flags as specified, like `isGlobal`, `isSignatureValidation`, and `isUserOpValidation`. +- The account SHOULD call `onInstall` on the validation module to initialize state if specified by the user. +- The account MUST emit `ValidationInstalled` as defined in the interface for all installed validation functions. -The following behavior MUST be followed: +#### Uninstallation -- `StandardExecutor` can NOT call plugin execution functions and/or `PluginExecutor`. This is guaranteed by checking whether the call's target implements the `IPlugin` interface via ERC-165 as required. -- `StandardExecutor` can NOT be called by plugin execution functions and/or `PluginExecutor`. -- Plugin execution functions MUST NOT request access to `StandardExecutor`, they MAY request access to `PluginExecutor`. +During validation uninstallation, the account MUST correctly clear flags and other fields based on the incoming data provided by the user. -#### Calls to `installPlugin` +- The account MUST clear all flags for the validation function, like `isGlobal`, `isSignatureValidation`, and `isUserOpValidation`. +- The account MUST remove all hooks and SHOULD clear hook module states by calling `onUninstall` with the user-provided data for each hook, including both validation hooks and execution hooks, if specified by the user. + - The account MAY ignore the revert from `onUninstall` with try/catch depending on the design principle of the account. +- The account MUST clear the configuration for the selectors that the validation function can validate. +- The account SHOULD call `onUninstall` on the validation module to clean up state if specified by the user. +- The account MUST emit `ValidationUninstalled` as defined in the interface for all uninstalled validation functions. -The function `installPlugin` accepts 4 parameters: the address of the plugin to install, the Keccak-256 hash of the plugin's manifest, ABI-encoded data to pass to the plugin's `onInstall` callback, and an array of function references that represent the plugin's install dependencies. +### Execution Functions and Their Installation/Uninstallation -The function MUST retrieve the plugin's manifest by calling `pluginManifest()` using `staticcall`. +- An account can install any number of execution functions. +- An execution function selector MUST be unique in the account. +- An execution function selector MUST not conflict with native ERC-4337 and ERC-6900 functions. -The function MUST perform the following preliminary checks: +#### Installation -- Revert if the plugin has already been installed on the modular account. -- Revert if the plugin does not implement ERC-165 or does not support the `IPlugin` interface. -- Revert if `manifestHash` does not match the computed Keccak-256 hash of the plugin's returned manifest. This prevents installation of plugins that attempt to install a different plugin configuration than the one that was approved by the client. -- Revert if any address in `dependencies` does not support the interface at its matching index in the manifest's `dependencyInterfaceIds`, or if the two array lengths do not match, or if any of the dependencies are not already installed on the modular account. +During execution installation, the account MUST correctly set flags and other fields based on the incoming data and module manifest provided by the user. -The function MUST record the manifest hash and dependencies that were used for the plugin's installation. Each dependency's record MUST also be updated to reflect that it has a new dependent. These records MUST be used to ensure calls to `uninstallPlugin` are comprehensive and undo all edited configuration state from installation. The mechanism by which these records are stored and validated is up to the implementation. +- The account MUST install all execution functions and set flags and fields as specified in the manifest. +- The account MUST add all execution hooks as specified in the manifest. +- The account SHOULD add all supported interfaces as specified in the manifest. +- The account SHOULD call `onInstall` on the execution module to initialize state if specified by the user. +- The account MUST emit `ExecutionInstalled` as defined in the interface for all installed executions. -The function MUST store the plugin's permitted function selectors, permitted external calls, and whether it can spend the account's native tokens, to be able to validate calls to `executeFromPlugin` and `executeFromPluginExternal`. +#### Uninstallation -The function MUST parse through the execution functions, validation functions, and hooks in the manifest and add them to the modular account after resolving each `ManifestFunction` type. +During execution uninstallation, the account MUST correctly clear flags and other fields based on the incoming data and module manifest provided by the user. -- Each execution function selector MUST be added as a valid execution function on the modular account. If the function selector has already been added or matches the selector of a native function, the function SHOULD revert. -- If a validation function is to be added to a selector that already has that type of validation function, the function SHOULD revert. +- The account MUST remove all execution functions and clear flags and fields as specified in the manifest. +- The account MUST remove all execution hooks as specified in the manifest. +- The account SHOULD remove all supported interfaces as specified in the manifest. +- The account SHOULD call `onUninstall` on the execution module to clean up state and track call success if specified by the user. +- The account MUST emit `ExecutionUninstalled` as defined in the interface for all uninstalled executions. -The function MAY store the interface IDs provided in the manifest's `interfaceIds` and update its `supportsInterface` behavior accordingly. +### Hooks -Next, the function MUST call the plugin's `onInstall` callback with the data provided in the `pluginInstallData` parameter. This serves to initialize the plugin state for the modular account. If `onInstall` reverts, the `installPlugin` function MUST revert. +#### Execution Hooks Data Format -Finally, the function MUST emit the event `PluginInstalled` with the plugin's address, the hash of its manifest, and the dependencies that were used. +For accounts that implement execution hooks, accounts MUST conform to these execution hook formats: -> **⚠️ The ability to install and uninstall plugins is very powerful. The security of these functions determines the security of the account. It is critical for modular account implementers to make sure the implementation of the functions in `IPluginManager` have the proper security consideration and access control in place.** +1. For `executeUserOp` calls, for execution hooks associated with a validation function, accounts MUST send the full `msg.data`, including the `executeUserOp` selector. +2. For `executeUserOp` calls, for execution hooks associated with a selector, accounts MUST send `PackedUserOperation.callData` for `executeUserOp` calls, excluding `executeUserOp.selector` and the rest of the `PackedUserOperation`. +3. For `executeWithRuntimeValidation` calls, for all execution hooks, accounts MUST send the inner `data`. +4. For all other calls, for execution hooks associated with a selector, accounts MUST send over `msg.data`. -#### Calls to `uninstallPlugin` +#### Hook Execution Order -The function `uninstallPlugin` accepts 3 parameters: the address of the plugin to uninstall, a bytes field that may have custom requirements or uses by the implementing account, and ABI-encoded data to pass to the plugin's `onUninstall` callback. +It is RECOMMENDED that an account implementer runs hooks in first installed first executed order. However, an account MAY implement a different execution order. -The function MUST revert if the plugin is not installed on the modular account. +### Validation Call Flow -The function SHOULD perform the following checks: +Modular accounts support three different calls flows for validation: user op validation, runtime validation, and signature validation. User op validation happens within the account's implementation of the function `validateUserOp`, defined in the ERC-4337 interface `IAccount`. Runtime validation happens through the dispatcher function `executeWithRuntimeValidation`, or when using [direct call validation](#direct-call-validation). Signature validation happens within the account's implementation of the function `isValidSignature`, defined in ERC-1271. -- Revert if the hash of the manifest used at install time does not match the computed Keccak-256 hash of the plugin's current manifest. This prevents unclean removal of plugins that attempt to force a removal of a different plugin configuration than the one that was originally approved by the client for installation. To allow for removal of such plugins, the modular account MAY implement the capability for the manifest to be encoded in the config field as a parameter. -- Revert if there is at least 1 other installed plugin that depends on validation functions added by this plugin. Plugins used as dependencies SHOULD NOT be uninstalled while dependent plugins exist. +For each of these validation types, an account implementation MAY specify its own format for selecting which validation function to use, as well as any per-hook data for validation hooks. -The function SHOULD update account storage to reflect the uninstall via inspection functions, such as those defined by `IAccountLoupe`. Each dependency's record SHOULD also be updated to reflect that it has no longer has this plugin as a dependent. +Within the implementation of each type of validation function, the modular account MUST check that the provided validation function applies to the given function selector intended to be run (See [Checking Validation Applicability](#checking-validation-applicability)). Then, the account MUST execute all validation hooks of the corresponding type associated with the validation function in use. After the execution of validation hooks, the account MUST invoke the validation function of the corresponding type. If any of the validation hooks or the validation function reverts, the account MUST revert. It SHOULD include the module's revert data within its revert data. -The function MUST remove records for the plugin's manifest hash, dependencies, permitted function selectors, permitted external calls, and whether it can spend the account's native tokens. +The account MUST define a way to pass data separately for each validation hook and the validation function itself. This data MUST be sent as the `userOp.signature` field for user op validation, the `authorization` field for runtime validation, and the `signature` field for signature validation. -The function MUST parse through the execution functions, validation functions, and hooks in the manifest and remove them from the modular account after resolving each `ManifestFunction` type. If multiple plugins added the same hook, it MUST persist until the last plugin is uninstalled. +The result of user op validation SHOULD be the intersection of time bounds returned by the validation hooks and the validation function. If any validation hooks or the validation functions returns a value of `1` for the authorizer field, indicating a signature verification failure by the ERC-4337 standard, the account MUST return a value of `1` for the authorizer portion of the validation data. -If the account stored the interface IDs provided in the manifest's `interfaceIds` during installation, it MUST remove them and update its `supportsInterface` behavior accordingly. If multiple plugins added the same interface ID, it MUST persist until the last plugin is uninstalled. +The set of validation hooks run MUST be the hooks specified by account state at the start of validation. In other words, if the set of applicable hooks changes during validation, the original set of hooks MUST still run, and only future invocations of the same validation should reflect the changed set of hooks. -Next, the function MUST call the plugin's `onUninstall` callback with the data provided in the `pluginUninstallData` parameter. This serves to clear the plugin state for the modular account. If `onUninstall` reverts, execution SHOULD continue to allow the uninstall to complete. +#### Checking Validation Applicability -Finally, the function MUST emit the event `PluginUninstalled` with the plugin's address and whether the `onUninstall` callback succeeded. +To enforce module permission isolation, the modular account MUST check validation function applicability as part of each validation function implementation. -> **⚠️ Incorrectly uninstalled plugins can prevent uninstalls of their dependencies. Therefore, some form of validation that the uninstall step completely and correctly removes the plugin and its usage of dependencies is required.** +User op validation and runtime validation functions have a configurable range of applicability to functions on the account. This can be configured with selectors installed to a validation. Alternatively, a validation installation MAY specify the `isGlobal` flag as true, which means the account MUST consider it applicable to any module execution function with the `allowGlobalValidation` flag set to true, or for any account native function that the account MAY allow for global validation. -#### Calls to `validateUserOp` +If the selector being checked is `execute` or `executeBatch`, the modular account MUST perform additional checking. If the target of `execute` is the modular account's own address, or if the target of any `Call` within `executeBatch` is the account, validation MUST either revert or check that validation applies to the selector(s) being called. -When the function `validateUserOp` is called on modular account by the `EntryPoint`, it MUST find the user operation validation function associated to the function selector in the first four bytes of `userOp.callData`. If there is no function defined for the selector, or if `userOp.callData.length < 4`, then execution MUST revert. +Installed validation functions have two additional flag variables indicating what they may be used for. If a validation function is attempted to be used for user op validation and the flag `isUserOpValidation` is set to false, validation MUST revert. If the validation function is attempted to be used for signature validation and the flag `isSignatureValidation` is set to false, validation MUST revert. -If the function selector has associated pre user operation validation hooks, then those hooks MUST be run sequentially. If any revert, the outer call MUST revert. If any are set to `PRE_HOOK_ALWAYS_DENY`, the call MUST revert. If any return an `authorizer` value other than 0 or 1, execution MUST revert. If any return an `authorizer` value of 1, indicating an invalid signature, the returned validation data of the outer call MUST also be 1. If any return time-bounded validation by specifying either a `validUntil` or `validBefore` value, the resulting validation data MUST be the intersection of all time bounds provided. +#### Direct Call Validation -Then, the modular account MUST execute the validation function with the user operation and its hash as parameters using the `call` opcode. The returned validation data from the user operation validation function MUST be updated, if necessary, by the return values of any pre user operation validation hooks, then returned by `validateUserOp`. +If a validation function is installed with the entity ID of `0xffffffff`, it may be used as direct call validation. This occurs when a module or other address calls a function on the modular account, without wrapping its call in the dispatcher function `executeWithRuntimeValidation` to use as a selection mechanism for a runtime validation function. -#### Calls to execution functions +To implement direct call validation, the modular account MUST treat direct function calls that are not from the modular account itself or the `EntryPoint` as an attempt to validate using the caller's address and the entity ID of `0xffffffff`. If such a validation function is installed, and applies to the function intended to be called, the modular account MUST allow it to continue, without performing runtime validation. Any validation hooks and execution hooks installed to this validation function MUST still run. -When a function other than a native function is called on an modular account, it MUST find the plugin configuration for the corresponding selector added via plugin installation. If no corresponding plugin is found, the modular account MUST revert. Otherwise, the following steps MUST be performed. +### Execution Call Flow -Additionally, when the modular account natively implements functions in `IPluginManager` and `IStandardExecutor`, the same following steps MUST be performed for those functions. Other native functions MAY perform these steps. +For all non-view functions within `IModularAccount` except `executeWithRuntimeValidation`, all module-defined execution functions, and any additional native functions that the modular account MAY wish to include, the modular account MUST adhere to these steps during execution: -The steps to perform are: +If the caller is not the `EntryPoint` or the account, the account MUST check access control for direct call validation. -- If the call is not from the `EntryPoint`, then find an associated runtime validation function. If one does not exist, execution MUST revert. The modular account MUST execute all pre runtime validation hooks, then the runtime validation function, with the `call` opcode. All of these functions MUST receive the caller, value, and execution function's calldata as parameters. If any of these functions revert, execution MUST revert. If any pre runtime validation hooks are set to `PRE_HOOK_ALWAYS_DENY`, execution MUST revert. If the runtime validation function is set to `RUNTIME_VALIDATION_ALWAYS_ALLOW`, the validation function MUST be bypassed. -- If there are pre execution hooks defined for the execution function, execute those hooks with the caller, value, and execution function's calldata as parameters. If any of these hooks returns data, it MUST be preserved until the call to the post execution hook. The operation MUST be done with the `call` opcode. If there are duplicate pre execution hooks (i.e., hooks with identical `FunctionReference`s), run the hook only once. If any of these functions revert, execution MUST revert. -- Run the execution function. -- If any post execution hooks are defined, run the functions. If a pre execution hook returned data to the account, that data MUST be passed as a parameter to the associated post execution hook. The operation MUST be done with the `call` opcode. If there are duplicate post execution hooks, run them once for each unique associated pre execution hook. For post execution hooks without an associated pre execution hook, run the hook only once. If any of these functions revert, execution MUST revert. +Prior to running the target function, the modular account MUST run all pre execution hooks that apply for the current function call. Pre execution hooks apply if they have been installed to the currently running function selector, or if they are installed as an execution hook to the validation function that was used for the current execution. Pre execution hooks MUST run validation-associated hooks first, then selector-associated hooks second. -The set of hooks run for a given execution function MUST be the hooks specified by account state at the start of the execution phase. This is relevant for functions like `installPlugin` and `uninstallPlugin`, which modify the account state, and possibly other execution or native functions as well. +Next, the modular account MUST run the target function, either an account native function or a module-defined execution function. -#### Calls made from plugins +After the execution of the target function, the modular account MUST run any post execution hooks. These MUST be run in the reverse order of the pre execution hooks. If a hook is defined to be both a pre and a post execution hook, and the pre execution hook returned a non-empty `bytes` value to the account, the account MUST pass that data to the post execution hook. -Plugins MAY interact with other plugins and external addresses through the modular account using the functions defined in the `IPluginExecutor` interface. These functions MAY be called without a defined validation function, but the modular account MUST enforce these checks and behaviors: +The set of hooks run for a given target function MUST be the hooks specified by account state at the start of the execution phase. In other words, if the set of applicable hooks changes during execution, the original set of hooks MUST still run, and only future invocations of the same target function should reflect the changed set of hooks. -The `executeFromPlugin` function MUST allow plugins to call execution functions installed by plugins on the modular account. Hooks matching the function selector provided in `data` MUST be called. If the calling plugin's manifest did not include the provided function selector within `permittedExecutionSelectors` at the time of installation, execution MUST revert. +Module execution functions where the field `skipRuntimeValidation` is set to true, as well as native functions without access control, SHOULD omit the runtime validation step, including any runtime validation hooks. Native functions without access control MAY also omit running execution hooks. -The `executeFromPluginExternal` function MUST allow plugins to call external addresses as specified by its parameters on behalf of the modular account. If the calling plugin's manifest did not explicitly allow the external call within `permittedExternalCalls` at the time of installation, execution MUST revert. +### Extension + +#### Semi-Modular Account + +Account implementers MAY choose to design a semi-modular account, where certain features, such as default validation, are integrated into the core account. This approach SHOULD ensure compatibility with fully modular accounts, as defined in this proposal, to maintain interoperability across different implementations. ## Rationale ERC-4337 compatible accounts must implement the `IAccount` interface, which consists of only one method that bundles validation with execution: `validateUserOp`. A primary design rationale for this proposal is to extend the possible functions for a smart contract account beyond this single method by unbundling these and other functions, while retaining the benefits of account abstraction. -The function routing pattern of ERC-2535 is the logical starting point for achieving this extension into multi-functional accounts. It also meets our other primary design rationale of generalizing execution calls across multiple implementing contracts. However, a strict diamond pattern is constrained by its inability to customize validation schemes for specific execution functions in the context of `validateUserOp`, and its requirement of `delegatecall`. +This proposal includes several interfaces that build on ERC-4337. First, we standardize a set of modular functions that allow smart contract developers greater flexibility in bundling validation, execution, and hook logic. We also propose interfaces that provide methods for querying execution functions, validation functions, and hooks on a modular account. The rest of the interfaces describe a module's methods for exposing its modular functions and desired configuration, and the modular account's methods for installing and removing modules and allowing execution across modules and external addresses. + +### ERC-4337 Dependency -This proposal includes several interfaces that build on ERC-4337 and are inspired by ERC-2535. First, we standardize a set of modular functions that allow smart contract developers greater flexibility in bundling validation, execution, and hook logic. We also propose interfaces that take inspiration from the diamond standard and provide methods for querying execution functions, validation functions, and hooks on a modular account. The rest of the interfaces describe a plugin's methods for exposing its modular functions and desired configuration, and the modular account's methods for installing and removing plugins and allowing execution across plugins and external addresses. +ERC-6900's main objective is to create a secure and interoperable foundation through modular accounts and modules to increase the velocity and security of the smart account ecosystem, and ultimately the wallet ecosystem. Currently, the standard prescribes ERC-4337 for one of its [modular account call flows](#overview). However, this does not dictate that ERC-6900 will continue to be tied to ERC-4337. +It is likely that smart account builders will want to develop modular accounts that do not use ERC-4337 in the future (e.g., native account abstraction on rollups). Moreover, it is expected that ERC-4337 and its interfaces and contracts will continue to evolve until there is a protocol-level account abstraction. + +In the current state of the AA ecosystem, it is tough to predict the direction the builders and industry will take, so ERC-6900 will evolve together with the space's research, development, and adoption. The standard will do its best to address the objectives and create a secure foundation for modular accounts that may eventually be abstracted away from the infrastructure mechanism used. + +### Community Consensus + +While this standard has largely been the result of collaboration among the coauthors, there have been noteworthy contributions from others in the community with respect to improvements, education, and experimentation. Thank you to the contributors: + +- Gerard Persoon (@gpersoon) +- Harry Jeon (@sm-stack) +- Zhiyu Zhang (@ZhiyuCircle) +- Danilo Neves Cruz (@cruzdanilo) +- Iván Alberquilla (@ialberquilla) + +We host community calls and working groups to discuss standard improvements and invite anyone with questions or contributions into our discussion. ## Backwards Compatibility -No backward compatibility issues found. +Existing accounts that are deployed as proxies may have the ability to upgrade account implementations to one that supports this standard for modularity. Depending on implementation logic, existing modules may be wrapped in an adapter contract to adhere to the standard. + +The standard also allows for flexibility in account implementations, including accounts that have certain features implemented without modules, so usage of modules may be gradually introduced. ## Reference Implementation @@ -550,13 +621,11 @@ See `https://github.com/erc6900/reference-implementation` ## Security Considerations -The modular smart contract accounts themselves are trusted components. Installed plugins are trusted to varying degrees, as plugins can interact with an arbitrarily large or small set of resources on an account. For example, a wide-reaching malicious plugin could add reverting hooks to native function selectors, bricking the account, or add execution functions that may drain the funds of the account. However, it is also possible to install a plugin with a very narrow domain, and depend on the correctness of the account behavior to enforce its limited access. Users should therefore be careful in what plugins to add to their account. - -Users should perform careful due diligence before installing a plugin and should be mindful of the fact that plugins are potentially dangerous. The plugin's manifest can give users an understanding of the domain of the plugin, i.e., the requested permissions to install certain validation functions and/or hooks on certain execution selectors. Generally, plugins that include native function selectors in their domain, e.g., plugins that add a validation hook to the native `uninstallPlugin()` function, can introduce significantly more harm than plugins that simply add validation hooks to function selectors that the plugin itself is adding to the account. +The modular smart contract accounts themselves are trusted components. Installed modules are trusted to varying degrees, as modules can interact with an arbitrarily large or small set of resources on an account. For example, a wide-reaching malicious module could add reverting hooks to native function selectors, bricking the account, or add execution functions that may drain the funds of the account. However, it is also possible to install a module with a very narrow domain, and depend on the correctness of the account behavior to enforce its limited access. Users should therefore be careful in what modules to add to their account. -Plugins can also add validation hooks to function selectors installed by other plugins. While usually, such a plugin would, e.g., add additional pre-validation hooks, it can also cause the previously installed plugin to be executed in an unintended context. For example, if a plugin were to only be intended to operate in the user operation context, its plugin manifest might only define user operation validation functions. However, another plugin might add a passing runtime validation function to that function selector, causing, for example, a session key plugin to suddenly be executed in a runtime validation context, circumventing all the parameter-validation that would have happened during user operation validation and granting unrestricted access to all session keys. Therefore, it is strongly recommended to always add reverting validation hooks to the context the plugin is not intended to be executed in. This recommendation may change in the next iteration of the standard. +Users should perform careful due diligence before installing a module and should be mindful of the fact that modules are potentially dangerous. The module's manifest can give users an understanding of the potential risks they are exposed to for that particular module. For instance, a request to install certain validation functions and/or hooks on certain execution selectors could potentially be a vector for DOS. -It is worth mentioning that execution hooks have no awareness of other execution hooks being performed in the same function selector execution setting. Since execution hooks can perform state changes, this reveals an important security consideration: An execution hook can only assure that at the time of its own execution, certain conditions are met, but this can not be generalized to the entire pre-execution context of potentially multiple pre-execution hooks. For example, a pre-execution hook cannot be assured that the storage it performed validation upon does not get further updated in subsequent pre-execution hooks. Even an associated post-execution hook potentially repeating the validation cannot assure that the storage remains unmodified because a prior post-execution hook may have reset the state. As long as the requirements checked by a plugin as part of an execution hook are only modifiable by the plugin itself, this can be considered safe. +Execution hooks have no awareness of other execution hooks being performed in the same function selector execution setting. Since execution hooks can perform state changes, this reveals an important security consideration: An execution hook can only assure that at the time of its own execution, certain conditions are met, but this can not be generalized to the entire pre execution context of potentially multiple pre execution hooks. For example, a pre execution hook cannot be assured that the storage it performed validation upon does not get further updated in subsequent pre execution hooks. Even a post execution hook potentially repeating the validation cannot assure that the storage remains unmodified because a prior post execution hook may have reset the state. As long as the requirements checked by a module as part of an execution hook are only modifiable by the module itself, this can be considered safe. ## Copyright diff --git a/ERCS/erc-7662.md b/ERCS/erc-7662.md new file mode 100644 index 0000000000..252044e184 --- /dev/null +++ b/ERCS/erc-7662.md @@ -0,0 +1,106 @@ +--- +eip: 7662 +title: AI Agent NFTs +description: A specification for NFTs that represent AI Agents. +author: Greg Marlin (@marleymarl) +discussions-to: https://ethereum-magicians.org/t/erc-7662-ai-agent-nfts/19371 +status: Draft +type: Standards Track +category: ERC +created: 2024-03-26 +requires: 721 +--- + +## Abstract + +This proposal introduces a standard for AI agent NFTs. When AI Agents are created and traded as NFTs, it doesn't make sense to put the prompts in the token metadata, therefore it requires a standard custom struct. It also doesn't make sense to store the prompts directly onchain as they can be quite large, therefore this standard proposes they be stored as decentralized storage URLs. This standard also proposes two options on how this data should be made private to the owner of the NFT, with the favored implementation option being encrypting the data using custom contract parameters for decryption that decrypt only to the owner of the NFT. + +## Motivation + +The creation and trading of AI Agent NFTs are a natural fit and offer the potential for an entirely new onchain market. This requires some custom data to be embedded in the NFT through a custom struct and this needs to be standardized so that any marketplace or AI Agent management product, among others, know how to create and parse AI Agent NFTs. The goal of this standard is to provide a new utility for NFTs in the field of AI and also to provide new liquidity, through the NFT market, for AI Agents. If widely adopted by marketplaces, and infrastructure and no-code providers this should open up a new market and community for AI Agent creators in different fields, AI Agent consumers and NFT marketplaces. + + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + + +All ERC-XXXX compliant contracts MUST implement the standard [ERC-721](./eip-721.md) functionality for minting and transferring NFTs, and MUST additionally implement this standard's Agent interface + +```solidity + +interface IERC7662 is IERC721 { + + function getAgentData(uint256 tokenId) external view returns ( + string memory name, + string memory description, + string memory model, + string memory userPromptURI, + string memory systemPromptURI, + bool promptsEncrypted + ); + + event AgentUpdated(uint256 indexed tokenId); +} +``` + +and MUST implement the mapping between NFT Token ID and its Agent information. + +It is RECOMMENDED that this mapping is public and that the URIs for User Prompt and System Prompt are made private through encryption with decryption logic set to the holder of the NFT via custom contract parameters set during encryption, and the method or platform used to provide this encryption SHOULD be retrievable as a data property of the NFT in order that platforms that should facilitate the use of these NFTs can set up a predictable way to handle this decryption, depending on the platform or method used. + +It is conceivable to also create an implementation whereby this mapping was set to private and accessed through a custom function that restricted access to the holder of the NFT. This approach would explose the prompts through their urls though, therefore the RECOMMENDED approach is a public mapping and encryption on the URLs. This also has the benefit of publicly exposing the data in the Agent struct to verify name, description and model and that encyrpted URIs for the User Prompt and System Prompt exist. + +All ERC-XXXX compliant contracts MUST implement a function to mint new Agent tokens. This function SHOULD: + +- Accept parameters for all Agent properties (name, description, model, userPromptURI, systemPromptURI, etc.) +- Mint a new token to the specified recipient +- Associate the provided Agent properties with the newly minted token +- Emit an event signaling the creation of a new Agent token + +It is RECOMMENDED that ERC-XXXX compliant contracts provide functionality to encrypt the user prompt and system prompt. This functionality SHOULD: + +- Allow only the token owner to encrypt the prompts +- Update the userPromptURI and systemPromptURI with encrypted versions +- Set a flag indicating that the prompts are encrypted + +It is RECOMMENDED to implement the following event: + +```solidity +event AgentCreated(string name, string description, string model, address recipient, uint256 tokenId) + +``` + +This event SHOULD be emitted when a new Agent token is minted, providing key information about the newly created Agent. + +To enable dynamic variables being injected into the User Prompt before being run, any such variables MUST be surrounded with ${} e.g. ${dynamicVariableName} in order that they can be recognized and handled appropriately by programs and systems that will enabled the injection, e.g. web forms and automation systems. + +It is RECOMMENDED to add a data to the [ERC-721](./eip-721.md) standard that makes it easy for e.g. NFT Marketplaces to display data about the AI Agent NFT, i.e. Model, which in turn reveals the platform that is used for the agent, e.g. OpenAI in the case of gpt-4-0125-preview or Anthropic in the case of claude-3-opus-20240229. The standard name and description can be used to display the Agent Name and Agent Description. + +## Rationale + +This standard provides a unified way to create and parse AI Agent NFTs. + +This standard codifies the necessary parameters of Name, Description, Model, User Prompt, and System Prompt for creating and using AI Agent NFTs. + +It doesn't make practical sense to store the user and system prompts in an existing [ERC-721](./eip-721.md) as the only place to put would be in the token metadata that is open for anyone to access the prompts without owning the NFT. By storing the prompts in a custom Agent struct and restricting access to the prompts to the holder of the NFT. One way to do this would be through restricing access to the struct info to the holder of the NFT through a custom function, however since that option still exposes the prompt URIs to the public and thus the data inside them, the recommended method is by encrypting the prompts onchain and tying the decryption of the URLs to the holder of the NFT, using onchain services that enable decryption to be tied to contract parameters such as ownerOf(tokenId). + + +## Backwards Compatibility + +The AI Agents NFT standard introduces additional features and data to the standard [ERC-721](./eip-721.md) protocol, aimed at addressing the practical requirements of using NFTs to store, trade and use AI Agents. It is designed to be fully backward-compatible with the original [ERC-721](./eip-721.md) standard. All existing [ERC-721](./eip-721.md) functions (such as transferFrom, approve, and balanceOf) retain their original functionality and interfaces. Our extension does not modify these core behaviors, ensuring that any [ERC-721](./eip-721.md) compliant wallet or service can interact with these tokens without modifications. + +### Reference Implementation + +This is being currently implemented in a product for creating, managing and using AI Agents Onchain through a DApp interface. In this implementation, an encryption platform is being used to encrypt the prompts using custom EVMContractParameters that only decrypt for the holder of the NFT and using a decentralized storage network to store the URLs of this encrypted data. To facilitate that and make DApp handling easier, some parameters were added to Agent and the addEncryptedPrompts function is added that enables adding the encrypted prompt URIs after first minting the NFT (as the tokenId of the NFT is needed for setting the encryption/decryption conditions). + +A reference smart contract is provided in the assets folder. + + + +## Security Considerations + + + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file diff --git a/ERCS/erc-7722.md b/ERCS/erc-7722.md new file mode 100644 index 0000000000..29756670dd --- /dev/null +++ b/ERCS/erc-7722.md @@ -0,0 +1,328 @@ +--- +eip: 7722 +title: Opaque Token +description: A token specification designed to enhance privacy by concealing balance information. +author: Ivica Aračić (@ivica7), SWIAT +discussions-to: https://ethereum-magicians.org/t/erc-7722-opaque-token/20249 +status: Draft +type: Standards Track +category: ERC +created: 2024-06-09 +--- + +## Abstract + +This ERC proposes a specification for an opaque token that enhances privacy by concealing balance information. Privacy is achieved by representing balances as off-chain data encapsulated in hashes, referred to as "baskets". These baskets can be reorganized, transferred, and managed through token functions on-chain. + +## Motivation + +Smart contract accounts serve as well-defined identities that can have reusable claims and attestations attached to them, making them highly useful for various applications. However, this strength also introduces a significant privacy challenge when these identities are used to hold tokens. Specifically, in the case of [ERC-20](./eip-20.html) compatible tokens, where balances are stored directly on-chain in plain text, the transparency of these balances can compromise the privacy of the account holder. This creates a dilemma: while the reuse of claims and attestations tied to a smart contract account can be advantageous, it also increases the risk of exposing sensitive financial information, particularly when these well-defined identities are associated with publicly visible token holdings. + +This proposal aims to conceal balances on-chain, allowing the use of smart contract accounts to hold tokens without compromising privacy or integrity. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +The concept revolves around representing token balances on-chain as hashed values, called baskets, which obscure the actual balance information. These baskets combine a random salt, a unique token ID, and the token's value, making it impossible to derive the token's value directly from the blockchain. + +The token interface allows for creating, transferring, minting, and reorganizing (splitting and joining) these baskets. To prevent unauthorized changes and maintain integrity, oracle services verify that the total value of baskets remains consistent during reorganizations. Additionally, differential privacy techniques, such as overlaying noise and empty transfers, further protect privacy by making it difficult to trace token movements and determine actual transaction details. + +### Baskets + +Balances are represented on-chain as hashes of the form: + +``` +keccak256(abi.encode(salt, tokenId, value)) + +// where salt (bytes32) - random 32bytes to increase the entropy and +// make brute-forcing the hash impossible +// tokenId (bytes32) - a unique tokenId within token's smart contract instance +// value (uint256) - the value of the position +``` + +For the remainder of this document, we refer to these hashes as "baskets" because they conceal the balance information in an opaque manner, similar to how a covered basket hides its contents. + +### Token Interface + +An opaque token MUST implement the following interface. + +``` +interface OpaqueToken { + // + // TYPES + // + + struct SIGNATURE { + uint8 v; bytes32 r; bytes32 s; + } + + struct ORACLECONFIG { + uint8 minNumberOfOracles; // min. number of oracle signatures required for reorg + address[] oracles; // valid oracles + } + + // + // EVENTS + // + + event CreateToken(address initiatedBy, bytes32 tokenId, bytes32 totalSupplyBasket, bytes32 ref); + event Mint(address initiatedBy, bytes32[] baskets, bytes32 ref); + event ReorgHolderBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref); + event ReorgSupplyBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref); + event Transfer(address initiatedBy, address receiver, bytes32[] baskets, bytes32 ref); + event Burn(address initiatedBy, bytes32[] baskets, bytes32 ref); + + // + // FUNCTIONS + // + + /** + * @dev returns the configuration for this token + */ + function oracleConfig() external view returns (ORACLECONFIG memory); + + /** + * @dev returns the address of the basket owner + */ + function owner(bytes32 basket) external view returns (address); + + /** + * @dev returns the total supply for a `tokenId`` + * All token investors are allowed to fetch this value from the token operator's off-chain storage. + */ + function totalSupply(bytes32 tokenId) external view returns (bytes32); + + /** + * @dev returns the operator of this token, who is also responsible for providing the main + * off-chain storage source. + */ + function operator() external view returns (address); + + /** + * @dev Allows the token operator to create a new token with the specified `tokenId` and an initial + * `totalSupplyBasket`. The `totalSupplyBasket` can be partitioned using {reorgSupplyBaskets} as needed + * when calling {mint}. The `ref` parameter can be used freely by the caller for any reference purpose. + */ + function createToken( + bytes32 tokenId, + bytes32 totalSupplyBasket, + bytes32 ref + ) external; + + /** + * @dev Allows the token operator to mint tokens by assigning `supplyBaskets` to a `receiver` which + * becomes the owner of these baskets. + */ + function mint( + bytes32[] calldata supplyBaskets, + address receiver, + bytes32 ref + ) external; + + /** + * @dev transfers `baskets` to a `receiver` who becomes the new owner of these baskets. + */ + function transfer( + bytes32[] calldata baskets, + address receiver, + bytes32 ref + ) external; + + /** + * @dev reorganizes a set of holder baskets (`basketsIn`) to a new set (`basketsOut`) having + * the same value, i.e., the sum of all values from input baskets equals the sum of values + * in output baskets. In order to ensure the integrity, external oracle service is required that + * will sign the reorg proposal requested by the basket owner, which is passed as `reorgOracleSignatures`. + * The minimum number of oracle signatures is defined in the oracle configuration. + */ + function reorgHolderBaskets( + SIGNATURE[] calldata reorgOracleSignatures, + bytes32[] calldata basketsIn, + bytes32[] calldata basketsOut, + bytes32 ref + ) external; + + /** + * @dev same as {reorgHolderBaskets}, but for the available supply baskets. + */ + function reorgSupplyBaskets( + SIGNATURE[] calldata reorgOracleSignatures, + bytes32[] calldata basketsIn, + bytes32[] calldata basketsOut, + bytes32 ref + ) external; + + /** + * @dev burns holder's `baskets` and returns them to available supply + */ + function burn( + bytes32[] calldata baskets, + bytes32 ref + ) external; +} +``` + +### Off-chain Data Endpoints + +* The operator of the token (e.g., issuer or registrar) MUST provide the off-chain storage that implements the `GET basket` and `PUT basket` REST endpoints as described in this section. +* The operator MUST ensure the availability of the basket data and will share it on need-to-know basis with all eligible holders, i.e., with all address that either were holding the basket in the past or are currently the holder of the basket. +* To ensure data is only shared with and can be written by eligible holders, the operator MUST implement authentication for both endpoints. The concrete authentication schema is not specified here and my depend on the environment of the token operator. +* The operator MUST allow an existing token holder to `PUT basket` +* The operator MUST allow the current or historical basket holder to `GET basket` +* Token holders SHOULD store a copy of the data about their own baskets in their own off-chain storage for the case that operator's service is unavailable. + +REST API Endpoints for creating and querying baskets: + +``` + Endpoint: PUT baskets + Description: will store baskets if the `basket` hash is matching `data`. + PostData: + [ + { + basket: keccak256(abi.encode(salt, tokenId, value)), + data: { + salt: , + tokenId: , + value: + } + }, + ... + ] + + Endpoint: GET baskets?basket-hash= + Description: will return the list of baskets depending on the query parameters. + Query Parameters: + - basket-hash (optional): returns one basket matching the requested hash + - if no query parameter is set, then the endpoint will return all baskets of the requestor + Response: + [ + { + basket: keccak256(abi.encode(salt, tokenId, value)), + data: { + salt: , + tokenId: , + value: + } + }, + ... + ] +``` + +### reorg Endpoint + +To ensure the integrity of a reorg and avoid accidental or fraudulent mints or burns, an oracle services is required. + +* Oracles MUST provide a `POST reorg` REST Endpoint as described in this section +* Oracles MUST sign any reorg proposal request where + * the sum of values in input baskets grouped by tokenId is equal the sum of values of the output baskets grouped by tokenId. + * `item.basket` hash matches `keccak256(abi.encode(data.salt, data.tokenId, data.value))` +* The reorg endpoint MUST be stateless +* Oracle MUST NOT persist data from the request for later analysis. +* The reorg endpoint SHOULD NOT require authentication and can be used by anyone without restrictions. + +``` +Endpoint: POST reorg +PostData: +{ + in: [ + { + basket: keccak256(abi.encode(salt, tokenId, value)), + data: { + salt: , + tokenId: , + value: + } + }, + ... + ], + out: [ + { + basket: keccak256(abi.encode(salt, tokenId, value)), + data: { + salt: , + tokenId: , + value: + } + }, + ... + ] +} + +Response: { + // hash is signed with oracles private key + // basketsIn and basketsIn are bytes32[] + signature: sign(keccak256(abi.encode(basketsIn, basketsOut))) +} +``` + +Example for valid reorg requests (salt and hashes are omitted for better readability): + +``` +in : (..., token1, 10), (..., token1, 30), (..., token2, 5), (..., token2, 95) +out: (..., token1, 40), (..., token2, 100) + +in : (..., token1, 40), (..., token2, 100) +out: (..., token1, 10), (..., token1, 30), (..., token2, 5), (..., token2, 95) +``` + +### Overlaying Noise (Differential Privacy) + +To further enhance privacy and obscure transaction details, an additional layer of noise need to be introduced through reorgs and empty transfers. For example, received baskets can be reorganized into new baskets to prevent information leakage to the previous owner. Additionally, null-value baskets can be sent to random receivers (empty transfers), making it difficult for observers to determine who is transferring to whom. + +Example with reorg and null-value basket transfers: +``` +A owns basket-a1{..., value:10} +B owns basket-b1{..., value:5}, basket-b2{..., value:15}, ... +A: transfer basket-a1 to B +B: reorg [basket-a1, basket-b1, basket-b2] + to [basket-b3{..., value:10}, basket-b4{..., value:10}, basket-b5:{..., value:10}, + basket-b6:{..., value:0}, basket-b7:{..., value:0}] + where sum of inputs is the sum of outputs +B: transfer basket-b5{value:10} to C +B: transfer basket-b6{value:0} to D +B: transfer basket-b7{value:0} to E +``` + +If B would directly send basket-a1 to C, A would know what C is receiving, however, now that B has reorg'ed the baskets, A can not know anymore what has been sent to C. + +Moreover, observers still see who is communicating with whom, but since there is noise introduced, they can not tell which of these transfers are actually transferring real values. + +## Rationale + +### Breaking the ERC-20 Compatibility + +The transparency inherent in ERC-20 tokens presents a significant issue for reusable blockchain identities. To address this, we prioritize privacy over ERC-20 compatibility, ensuring the confidentiality of token balances. + +### Reorg Oracles + +The trusted oracles and the minimum number of required signatures can be configured to achieve the desired level of decentralization. + +The basket holder proposes the input and output baskets for the reorg, while the oracles are responsible for verifying that the sums of the values on both sides (input and output) are equal. This system allows for mutual control, ensuring that no single party can manipulate the process. + +Fraudulent oracles can be tracked back on-chain, i.e., the system ensures weak-integrity at minimum. + +To further strengthen the integrity, it would also be possible to apply Zero-Knowledge Proofs (ZKP) to provide reorg proofs, however, we have chosen to use oracles for efficiency and simplicity reasons. + +### Off-chain Data Storage + +We have chosen the token operator, which in most cases will be the issuer or registrar, as the initial and main source for off-chain data. This is acceptable, since they must know anyway which investor holds which positions to manage lifecycle events on the token. While this approach may not be suitable for every use case within the broader Ethereum ecosystem, it fits well the financial instruments in the regulated environment of the financial industry, which rely on strict KYC and token operation procedures. + +## Backwards Compatibility + +* Opaque Token is not compatible with ERC-20 for reasons explained in the Rationale section. + +## Security Considerations + +### Fraudulent Oracles + + +### Oracles Collecting Confidential Data + + +### Confidential Data Loss + + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/ERCS/erc-7729.md b/ERCS/erc-7729.md new file mode 100644 index 0000000000..d839b5b4d3 --- /dev/null +++ b/ERCS/erc-7729.md @@ -0,0 +1,97 @@ +--- +eip: 7729 +title: Token with Metadata +description: An ERC-20 extension for tokens with metadata. +author: msfew (@fewwwww) +discussions-to: https://ethereum-magicians.org/t/erc-7729-token-with-metadata/20939 +status: Draft +type: Standards Track +category: ERC +created: 2023-06-24 +requires: 20 +--- + +## Abstract + +This standard extends the [ERC-20](./eip-20.md) standard to include a `metadata` function interface and a JSON schema for metadata. + +## Motivation + +Memecoins have demonstrated the value of associating tokens with visual metadata. By standardizing a way to include metadata in ERC-20 tokens, developers can create more engaging and interactive tokens, fostering community engagement. + +## Specification + +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +**Every compliant contract must implement the `IERC7729`, and [`ERC20`](./eip-20.md) interfaces.** + +This standard includes the following interface: + +```solidity +pragma solidity ^0.8.0; + +interface IERC20Metadata is IERC20 { + /// @dev Returns the metadata URI associated with the token. + /// The URI may point to a JSON file that conforms to the "ERCX Metadata JSON Schema". + function metadata() external view returns (string memory); +} +``` + +This is the "[ERC-7729](./eip-7729.md) Metadata JSON Schema" referenced above. + +```json +{ + "title": "Token Metadata", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Identifies the asset to which this token represents" + }, + "description": { + "type": "string", + "description": "Describes the asset to which this token represents" + }, + "image": { + "type": "string", + "description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents." + } + } +} +``` + +## Rationale + +The `metadata` function was chosen based on existing implementations in standards and applications. + +## Backwards Compatibility + +This standard is backward compatible with the [ERC-20](./eip-20.md) as it extends the existing functionality with new interfaces. + +## Reference Implementation + +```solidity +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +interface IERC7729 is IERC20 { + function metadata() external view returns (string memory); +} + +contract ERC7729 is ERC20, IERCX { + string _metadata = "ipfs://QmakTsyRRmvihYwiAstYPYAeHBfaPYz3v9z2mkA1tYLA4w"; + + function metadata() external view returns (string memory) { + return _metadata; + } +} +``` + +## Security Considerations + +The metadata URI could be manipulated to point to malicious content or phishing sites. Off-chain indexers should perform validation checks to ensure the security and integrity of the metadata URIs for users. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/ERCS/erc-7739.md b/ERCS/erc-7739.md new file mode 100644 index 0000000000..f4d4c86710 --- /dev/null +++ b/ERCS/erc-7739.md @@ -0,0 +1,359 @@ +--- +eip: 7739 +title: Readable Typed Signatures for Smart Accounts +description: A defensive rehashing scheme which prevents signature replays across smart accounts and preserves the readability of the signed contents +author: vectorized (@vectorized), Sihoon Lee (@push0ebp), Francisco Giordano (@frangio), Im Juno (@junomonster), howydev (@howydev), 0xcuriousapple (@0xcuriousapple) +discussions-to: https://ethereum-magicians.org/t/erc-7739-readable-typed-signatures-for-smart-accounts/20513 +status: Draft +type: Standards Track +category: ERC +created: 2024-05-28 +requires: 191, 712, 1271, 5267 +--- + +## Abstract + +This proposal defines a standard to prevent signature replays across multiple smart accounts when they are owned by a single Externally Owned Account (EOA). This is achieved through a defensive rehashing scheme for [ERC-1271](./eip-1271.md) verification using specific nested [EIP-712](./eip-712.md) typed structures, which preserves the readability of the signed contents during wallet client signature requests. + +## Motivation + +Smart accounts can verify signatures with via [ERC-1271](./eip-1271.md) using the `isValidSignature` function. + +A straightforward implementation as shown below, is vulnerable to signature replay attacks. + +```solidity +/// @dev This implementation is NOT safe. +function isValidSignature( + bytes32 hash, + bytes calldata signature +) external override view returns (bytes4) { + uint8 v = uint8(signature[64]); + (bytes32 r, bytes32 s) = abi.decode(signature, (bytes32, bytes32)); + // Reject malleable signatures. + require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0); + address signer = ecrecover(hash, v, r, s); + // Reject failed recovery. + require(signer != address(0)); + // `owner` is a storage variable containing the smart account's owner. + if (signer == owner) { + return 0x1626ba7e; + } else { + return 0xffffffff; + } +} +``` + +When multiple smart accounts are owned by a single EOA, the same signature can be replayed across the smart accounts if the `hash` does not include the smart account address. + +Unfortunately, this is the case for many popular applications (e.g. Permit2). As such, many smart account implementations perform some form of defensive rehashing. First, the smart account computes a final hash from minimally: (1) the hash, (2) its own address, (3) the chain ID. Then, the smart account verifies the final hash against the signature. Defensive rehashing can be implemented with [EIP-712](./eip-712.md), but a straightforward implementation will make the signed contents opaque. + +This standard provides a defensive rehashing scheme that makes the signed contents visible across all wallet clients that support [EIP-712](./eip-712.md). It is designed for minimal adoption friction. Even if wallet clients or application frontends are not updated, users can still inject client side JavaScript to enable the defensive rehashing. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +### Overview + +Compliant smart accounts MUST implement the following dependencies: + +- [EIP-712](./eip-712.md) Typed structured data hashing and signing. + Provides the relevant typed data hashing logic internally, which is required to construct the final hashes. + +- [ERC-1271](./eip-1271.md) Standard Signature Validation Method for Contracts. + Provides the `isValidSignature(bytes32 hash, bytes calldata signature)` function. + +- [ERC-5267](./eip-5267.md) Retrieval of EIP-712 domain. + Provides the `eip712Domain()` function which is required to compute the final hashes. + +This standard defines the behavior of the `isValidSignature` function for [ERC-1271](./eip-1271.md), which comprises of two workflows: (1) the `TypedDataSign` workflow, (2) the `PersonalSign` workflow. + +### `TypedDataSign` workflow + +The `TypedDataSign` workflow handles the case where the `hash` is originally computed with [EIP-712](./eip-712.md). + +#### `TypedDataSign` final hash + +The final hash for the `TypedDataSign` workflow is defined as: + +``` +keccak256(\x19\x01 ‖ APP_DOMAIN_SEPARATOR ‖ + hashStruct(TypedDataSign({ + contents: hashStruct(originalStruct), + name: eip712Domain().name, + version: eip712Domain().version, + chainId: eip712Domain().chainId, + verifyingContract: eip712Domain().verifyingContract, + salt: eip712Domain().salt, + extensions: eip712Domain().extensions + })) +) +``` + +where `‖` denotes the concatenation operator for bytes. + +In Solidity, this can be written as: + +```solidity +finalTypedDataSignHash = + keccak256( + abi.encodePacked( + hex"1901", + // Application specific domain separator. Passed via `signature`. + bytes32(APP_DOMAIN_SEPARATOR), + keccak256( + abi.encode( + // Computed on-the-fly with `contentsType`, which is passed via `signature`. + typedDataSignTypehash, + // This is the `contents` struct hash, which is passed via `signature`. + bytes32(hashStruct(originalStruct)), + // `eip712Domain()` is from ERC-5267. + keccak256(bytes(eip712Domain().name)), + keccak256(bytes(eip712Domain().version)), + uint256(eip712Domain().chainId), + uint256(uint160(eip712Domain().verifyingContract)), + bytes32(eip712Domain().salt), + keccak256(abi.encodePacked(eip712Domain().extensions)) + ) + ) + ) + ); +``` + +where `typedDataSignTypehash` is: + +```solidity +typedDataSignTypehash = + keccak256( + abi.encodePacked( + "TypedDataSign(", + contentsTypeName, " contents", + "bytes1 fields,", + "string name,", + "string version,", + "uint256 chainId,", + "address verifyingContract,", + "bytes32 salt,", + "uint256[] extensions", + ")", + contentsType + ) + ); +``` + +If `contentsType` is `"Mail(address from,address to,string message)"`, then `contentsTypeName` will be `"Mail"`. + +The `contentsTypeName` is the substring of `contentsType` up to (excluding) the first instance of `"("`: + +In Solidity, this can be written as: + +```solidity +// `slice(string memory subject, uint256 start, uint256 end)` +// returns a copy of `subject` sliced from `start` to `end` (exclusive). +// `start` and `end` are byte offsets. +// +// `indexOf(string memory subject, string memory search)` +// Returns the byte index of the first location of `search` in `subject`, +// searching from left to right. Returns `2**256 - 1` if `search` is not found. +contentsTypeName = + LibString.slice( + contentsType, + 0, // Start byte index. + LibString.indexOf(contentsType, "(") // End byte index (exclusive). + ); +``` + +A copy of the `LibString` Solidity library is provided for reference in [`/assets/eip-7739/contracts/utils/LibString.sol`]. + +For safety, smart accounts MUST treat the signature as invalid if any of the following is true: + +- `contentsTypeName` is the empty string (i.e. `bytes(contentsTypeName).length == 0`). +- `contentsTypeName` starts with any of the following bytes `abcdefghijklmnopqrstuvwxyz(`. +- `contentsTypeName` contains any of the following bytes `, )\x00`. + +#### `TypedDataSign` signature + +The `signature` passed into `isValidSignature` will be changed to: + +``` +originalSignature ‖ APP_DOMAIN_SEPARATOR ‖ contents ‖ contentsType ‖ uint16(contentsType.length) +``` + +where `contents` is the bytes32 struct hash of the original struct. + +In Solidity, this can be written as: + +```solidity +signature = + abi.encodePacked( + bytes(originalSignature), + bytes32(APP_DOMAIN_SEPARATOR), + bytes32(contents), + bytes(contentsType), + uint16(contentsType.length) + ); +``` + +The appended `APP_DOMAIN_SEPARATOR` and `contents` struct hash will be used to verify if the `hash` passed into `isValidSignature` is indeed correct via: + +```solidity +hash == keccak256( + abi.encodePacked( + hex"1901", + bytes32(APP_DOMAIN_SEPARATOR), + bytes32(contents) + ) +) +``` + +If the `hash` does not match the reconstructed hash, then the `hash` and `signature` are invalid under the `TypedDataSign` workflow. + +### `PersonalSign` workflow + +This `PersonalSign` workflow handles the case where the `hash` is originally computed with [EIP-191](./eip-191.md). + +#### `PersonalSign` final hash + +The final hash for the `PersonalSign` workflow is defined as: + +``` +keccak256(\x19\x01 ‖ ACCOUNT_DOMAIN_SEPARATOR ‖ + hashStruct(PersonalSign({ + prefixed: keccak256(bytes(\x19Ethereum Signed Message:\n ‖ + base10(bytes(someString).length) ‖ someString)) + })) +) +``` + +where `‖` denotes the concatenation operator for bytes. + +In Solidity, this can be written as: + +```solidity +finalPersonalSignHash = + keccak256( + abi.encodePacked( + hex"1901", + // Smart account domain separator. + // Can be computed via `eip712Domain()` from ERC-5267. + bytes32(ACCOUNT_DOMAIN_SEPARATOR), + keccak256( + abi.encode( + // `PERSONAL_SIGN_TYPEHASH`. + keccak256("PersonalSign(bytes prefixed)"), + // `hash` is from `isValidSignature(hash, signature)` + hash + ) + ) + ) + ); +``` + +Here, `hash` is computed in the application contract and passed into `isValidSignature`. + +The smart account does not need to know how `hash` is computed. For completeness, this is how it can be computed: + +```solidity +hash = + abi.encodePacked( + "\x19Ethereum Signed Message:\n", + // `toString` returns the base10 representation of a uint256. + LibString.toString(someString.length), + // This is the original message to be signed. + someString + ); +``` + +#### `PersonalSign` signature + +The `PersonalSign` workflow does not require additional data to be appended to the `signature` passed into `isValidSignature`. + +### `supportsNestedTypedDataSign` function for detection + +To facilitate automatic detection, smart accounts SHOULD implement the following function: + +```solidity +/// @dev For automatic detection that the smart account supports the nested EIP-712 workflow. +/// By default, it returns `bytes32(bytes4(keccak256("supportsNestedTypedDataSign()")))`, +/// denoting support for the default behavior, as implemented in +/// `_erc1271IsValidSignatureViaNestedEIP712`, which is called in `isValidSignature`. +/// Future extensions should return a different non-zero `result` to denote different behavior. +/// This method intentionally returns bytes32 to allow freedom for future extensions. +function supportsNestedTypedDataSign() public view virtual returns (bytes32 result) { + result = bytes4(0xd620c85a); +} +``` + +### Signature verification workflow deduction + +As the `isValidSignature` signature function signature is unchanged, the implementation MUST be able to deduce the type of workflow required to verify the signature. + +If the signature contains the correct data to reconstruct the `hash`, the `isValidSignature` function MUST perform the `TypedDataSign` workflow. +Otherwise, the `isValidSignature` function MUST perform the `PersonalSign` workflow. + +In Solidity, the check can be written as: + +```solidity +// If this is true, it means that the `signature` contains +// the correct `APP_DOMAIN_SEPARATOR` and `contents`, +// and the `TypedDataSign` workflow MUST be performed. +// Otherwise, the `PersonalSign` workflow MUST be performed. +hash == keccak256( + abi.encodePacked( + hex"1901", + bytes32(APP_DOMAIN_SEPARATOR), + bytes32(contents) + ) +) +``` + +### Conditional skipping of defensive rehashing + +Smart accounts MAY skip the defensive rehashing workflows if any of the following is true: + +- `isValidSignature` is called off-chain. +- The `hash` passed into `isValidSignature` has already included the address of the smart account. + +As many developers may not update their applications to support the nested EIP-712 workflow, smart account implementations SHOULD try to accommodate by skipping the defensive rehashing where it is safe to do so. + +## Rationale + +### `TypedDataSign` structure + +The `typedDataSignTypehash` must be constructed on-the-fly on-chain. This is to enforce that the signed contents will be visible in the signature request, by requiring that `contents` be a user defined type. + +The structure is intentionally made flat with the fields of `eip712Domain` to make implementation feasible. Otherwise, smart accounts must implement on-chain lexographical sorting of strings for the struct type names when constructing `typedDataSignTypehash`. + +### `supportsNestedTypedDataSign` for detection + +Without this function, this standard will not change the interface of the smart account, as it defines the behavior of `isValidSignature` without adding any new functions. As such, [ERC-165](./eip-165.md) cannot be used. + +For future extendability, `supportsNestedTypedDataSign` is defined to return a bytes32 as the first word of its returned data. For bytecode compactness and to leave space for bit packing, only the leftmost 4 bytes are set to the function selector of `supportsNestedTypedDataSign`. + +The `supportsNestedTypedDataSign` function may be extended to return multiple values (e.g. `bytes32 result, bytes memory data`), as long as the first word of the returned data is a bytes32 identifier. This will not change the function selector. + +## Backwards Compatibility + +No backwards compatibility issues. + +## Reference Implementation + +A production ready and optimized reference implementation is provided at [`/assets/eip-7739/contracts/accounts/ERC1271.sol`]. + +It includes relevant complementary features required for safety, flexibility, developer experience, and user experience. + +The reference implementation is intentionally not minimalistic. This is to avoid repeating the mistake of [ERC-1271](./eip-1271.md), where a minimalist reference implementation is wrongly assumed to be safe for production use. + +## Security Considerations + +### Rejecting invalid `contentsTypeName` + +Current major implementations of `eth_signTypedData` do not sanitize the names of custom types. + +A phishing website can craft a `contentsTypeName` with control characters to break out of the `PersonalSign` type encoding, resulting in the wallet client asking the user to sign an opaque hash. + +Requiring on-chain sanitization of `contentsTypeName` will block this phishing attack vector. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/_config.yml b/_config.yml index d93acf2d21..57536d8d80 100644 --- a/_config.yml +++ b/_config.yml @@ -18,7 +18,7 @@ description: >- Ethereum Improvement Proposals (EIPs) describe standards for the Ethereum platform, including core protocol specifications, client APIs, and contract standards. -url: "https://eips.ethereum.org" +url: "https://ercs.ethereum.org" github_username: ethereum repository: ethereum/EIPs diff --git a/assets/erc-6900/MSCA_Shared_Components_Diagram.svg b/assets/erc-6900/MSCA_Shared_Components_Diagram.svg index 836a8ad980..e443dd5681 100644 --- a/assets/erc-6900/MSCA_Shared_Components_Diagram.svg +++ b/assets/erc-6900/MSCA_Shared_Components_Diagram.svg @@ -14,4 +14,4 @@ - Alice's MSCAUO / TxPluginHooksValidationExecutionPluginHooksValidationExecutionUO / TxBob's MSCA \ No newline at end of file + Alice's MSCAUO / TxModuleHooksValidationExecutionModuleHooksValidationExecutionUO / TxBob's MSCA \ No newline at end of file diff --git a/assets/erc-6900/Modular_Account_Call_Flow.svg b/assets/erc-6900/Modular_Account_Call_Flow.svg index c4112c335c..8d7f7611a0 100644 --- a/assets/erc-6900/Modular_Account_Call_Flow.svg +++ b/assets/erc-6900/Modular_Account_Call_Flow.svg @@ -14,4 +14,4 @@ - Direct Call1 validation2. Execution Modular Account validateUserOpPre User Operation Validation Hook(s)User Operation Validation FunctionPre Execution Hook(s)Native Function / (Plugin) Execution FunctionPost Execution Hook(s)EntryPointEOA / SCRuntime Validation FunctionModular Account Call FlowPre Runtime Validation Hook(s) \ No newline at end of file + Direct Call1 validation2. Execution Modular Account validateUserOpPre User Operation Validation Hook(s)User Operation Validation FunctionPre Execution Hook(s)Native Function / (Module) Execution FunctionPost Execution Hook(s)EntryPointEOA / SCRuntime Validation FunctionModular Account Call FlowPre Runtime Validation Hook(s) \ No newline at end of file diff --git a/assets/erc-6900/Plugin_Execution_Flow.svg b/assets/erc-6900/Plugin_Execution_Flow.svg deleted file mode 100644 index 3a94b512ed..0000000000 --- a/assets/erc-6900/Plugin_Execution_Flow.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - Permitted External Contracts & Methods executeFromPluginPre Execution Hook(s)Permitted Plugin Execution FunctionPost Execution Hook(s)PluginsPlugin Permission Check executeFromPluginExternal(Plugin) Execution Function1 calls plugin 2.2 calls external contracts through executeFromPluginExternalPlugin Permission CheckModular AccountPlugin Execution Flow2.1 calls other installed plugin through executeFromPluginPre executeFromPluginExternal Hook(s)Post executeFromPluginExternal Hook(s) \ No newline at end of file diff --git a/assets/erc-7662/ERC7662.sol b/assets/erc-7662/ERC7662.sol new file mode 100644 index 0000000000..b6c0734e90 --- /dev/null +++ b/assets/erc-7662/ERC7662.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@openzeppelin/contracts/interfaces/IERC721.sol"; +import "./lib/FactoryOperatorable.sol"; + +contract ERC7662 is FactoryOperatorable, ERC721URIStorage { + + uint256 public tokenIds; + + //NFT Base URI + string public baseURI; + + + struct Agent { + string name; + string description; + string model; + string userPromptURI; + string systemPromptURI; + string imageURI; + string category; + bool promptsEncrypted; + } + + mapping(address => uint256[]) public collectionIds; + mapping(uint => Agent) public Agents; + + event AgentCreated(string name, string description, string model, string category, address recipient, uint256 tokenId); + + constructor( + string memory collectionBaseURI, + address admin, + address operator) ERC721("Agent NFTs", "AGENTS") FactoryOperatorable(admin, operator) { + + baseURI = collectionBaseURI; + + } + + /** + * @dev Override supportInterface. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) { + return super.supportsInterface(interfaceId); + } + + + /** + * @dev Mint an Agent NFT and attach its data to the token id + * + * @param _recipient address to receive NFT + * @param _name string Name of the Agent + * @param _description string Description of the Agent + * @param _model string AI Model of the Agent + * @param _userPromptURI string URI of the Agent's User Prompt + * @param _systemPromptURI string URI of the Agent's System Prompt + * @param _imageURI string URI of the NFT image + * @param _category string Category of Agent + * @param _tokenURI string URI of the NFT + * + * Emits an AgentCreated event. + */ + function mintAgent(address _recipient, string memory _name, string memory _description, string memory _model, string memory _userPromptURI, string memory _systemPromptURI, string memory _imageURI, string memory _category, string memory _tokenURI) public { + tokenIds++; + bool _promptsEncrypted = false; + Agents[tokenIds] = Agent(_name, _description, _model, _userPromptURI, _systemPromptURI, _imageURI, _category, _promptsEncrypted); + + _mint(_recipient, tokenIds); + collectionIds[_recipient].push(tokenIds); + _setTokenURI(tokenIds, _tokenURI); + emit AgentCreated(_name, _description, _model, _category, _recipient, tokenIds); + } + + /** + * @dev Update NFT with Encrypted Prompts as token id needed first for encryption params + * + * @param _tokenId uint256 Id of the NFT to update + * @param _encryptedUserPromptURI string Encrypted URI of the Agent's User Prompt + * @param _encryptedSystemPromptURI string Encrypted URI of the Agent's System Prompt + */ + function addEncryptedPrompts(uint256 _tokenId, string memory _encryptedUserPromptURI, string memory _encryptedSystemPromptURI) public { + require(ownerOf(_tokenId) == msg.sender, "Sender must be token owner"); + Agent storage agent = Agents[_tokenId]; + agent.userPromptURI = _encryptedUserPromptURI; + agent.systemPromptURI = _encryptedSystemPromptURI; + agent.promptsEncrypted = true; + } + + /** + * @dev Return base URI + * Override {ERC721:_baseURI} + */ + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + /** + * @dev Return all token ids owned by address + * @param _address address Address to check for + */ + function getCollectionIds(address _address) public view returns (uint256[] memory) { + return collectionIds[_address]; + } + + + /** + * @dev Remove the given token from collectionIds. + * + * @param from address from + * @param tokenId tokenId to remove + */ + function _popId(address from, uint256 tokenId) internal { + uint256[] storage _collectionIds = collectionIds[from]; + for (uint256 i = 0; i < _collectionIds.length; i++) { + if (_collectionIds[i] == tokenId) { + if (i != _collectionIds.length - 1) { + _collectionIds[i] = _collectionIds[_collectionIds.length - 1]; + } + _collectionIds.pop(); + break; + } + } + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * Requirements: + * + * - `tokenId` token must be owned by `from`. + * + * @param from address from + * @param to address to + * @param tokenId tokenId to transfer + */ + function _transfer( + address from, + address to, + uint256 tokenId + ) internal override { + super._transfer(from, to, tokenId); + _popId(from, tokenId); + collectionIds[to].push(tokenId); + } + + +} \ No newline at end of file diff --git a/assets/erc-7739/contracts/accounts/ERC1271.sol b/assets/erc-7739/contracts/accounts/ERC1271.sol new file mode 100644 index 0000000000..c28e00ca10 --- /dev/null +++ b/assets/erc-7739/contracts/accounts/ERC1271.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {EIP712} from "../utils/EIP712.sol"; +import {SignatureCheckerLib} from "../utils/SignatureCheckerLib.sol"; + +/// @notice ERC1271 mixin with nested EIP-712 approach. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/ERC1271.sol) +abstract contract ERC1271 is EIP712 { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev `keccak256("PersonalSign(bytes prefixed)")`. + bytes32 internal constant _PERSONAL_SIGN_TYPEHASH = + 0x983e65e5148e570cd828ead231ee759a8d7958721a768f93bc4483ba005c32de; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERC1271 OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Validates the signature with ERC1271 return, + /// so that this account can also be used as a signer. + function isValidSignature(bytes32 hash, bytes calldata signature) + public + view + virtual + returns (bytes4 result) + { + bool success = _erc1271IsValidSignature(hash, _erc1271UnwrapSignature(signature)); + /// @solidity memory-safe-assembly + assembly { + // `success ? bytes4(keccak256("isValidSignature(bytes32,bytes)")) : 0xffffffff`. + // We use `0xffffffff` for invalid, in convention with the reference implementation. + result := shl(224, or(0x1626ba7e, sub(0, iszero(success)))) + } + } + + /// @dev For automatic detection that the smart account supports the nested EIP-712 workflow. + /// By default, it returns `bytes32(bytes4(keccak256("supportsNestedTypedDataSign()")))`, + /// denoting support for the default behavior, as implemented in + /// `_erc1271IsValidSignatureViaNestedEIP712`, which is called in `isValidSignature`. + /// Future extensions should return a different non-zero `result` to denote different behavior. + /// This method intentionally returns bytes32 to allow freedom for future extensions. + function supportsNestedTypedDataSign() public view virtual returns (bytes32 result) { + result = bytes4(0xd620c85a); + } + + /// @dev Returns the ERC1271 signer. + /// Override to return the signer `isValidSignature` checks against. + function _erc1271Signer() internal view virtual returns (address); + + /// @dev Returns whether the `msg.sender` is considered safe, such + /// that we don't need to use the nested EIP-712 workflow. + /// Override to return true for more callers. + /// See: https://mirror.xyz/curiousapple.eth/pFqAdW2LiJ-6S4sg_u1z08k4vK6BCJ33LcyXpnNb8yU + function _erc1271CallerIsSafe() internal view virtual returns (bool) { + // The canonical `MulticallerWithSigner` at 0x000000000000D9ECebf3C23529de49815Dac1c4c + // is known to include the account in the hash to be signed. + return msg.sender == 0x000000000000D9ECebf3C23529de49815Dac1c4c; + } + + /// @dev Returns whether the `hash` and `signature` are valid. + /// Override if you need non-ECDSA logic. + function _erc1271IsValidSignatureNowCalldata(bytes32 hash, bytes calldata signature) + internal + view + virtual + returns (bool) + { + return SignatureCheckerLib.isValidSignatureNowCalldata(_erc1271Signer(), hash, signature); + } + + /// @dev Unwraps and returns the signature. + function _erc1271UnwrapSignature(bytes calldata signature) + internal + view + virtual + returns (bytes calldata result) + { + result = signature; + /// @solidity memory-safe-assembly + assembly { + // Unwraps the ERC6492 wrapper if it exists. + // See: https://eips.ethereum.org/EIPS/eip-6492 + if eq( + calldataload(add(result.offset, sub(result.length, 0x20))), + mul(0x6492, div(not(mload(0x60)), 0xffff)) // `0x6492...6492`. + ) { + let o := add(result.offset, calldataload(add(result.offset, 0x40))) + result.length := calldataload(o) + result.offset := add(o, 0x20) + } + } + } + + /// @dev Returns whether the `signature` is valid for the `hash. + function _erc1271IsValidSignature(bytes32 hash, bytes calldata signature) + internal + view + virtual + returns (bool) + { + return _erc1271IsValidSignatureViaSafeCaller(hash, signature) + || _erc1271IsValidSignatureViaNestedEIP712(hash, signature) + || _erc1271IsValidSignatureViaRPC(hash, signature); + } + + /// @dev Performs the signature validation without nested EIP-712 if the caller is + /// a safe caller. A safe caller must include the address of this account in the hash. + function _erc1271IsValidSignatureViaSafeCaller(bytes32 hash, bytes calldata signature) + internal + view + virtual + returns (bool result) + { + if (_erc1271CallerIsSafe()) result = _erc1271IsValidSignatureNowCalldata(hash, signature); + } + + /// @dev ERC1271 signature validation (Nested EIP-712 workflow). + /// + /// This uses ECDSA recovery by default (see: `_erc1271IsValidSignatureNowCalldata`). + /// It also uses a nested EIP-712 approach to prevent signature replays when a single EOA + /// owns multiple smart contract accounts, + /// while still enabling wallet UIs (e.g. Metamask) to show the EIP-712 values. + /// + /// Crafted for phishing resistance, efficiency, flexibility. + /// __________________________________________________________________________________________ + /// + /// Glossary: + /// + /// - `APP_DOMAIN_SEPARATOR`: The domain separator of the `hash` passed in by the application. + /// Provided by the front end. Intended to be the domain separator of the contract + /// that will call `isValidSignature` on this account. + /// + /// - `ACCOUNT_DOMAIN_SEPARATOR`: The domain separator of this account. + /// See: `EIP712._domainSeparator()`. + /// __________________________________________________________________________________________ + /// + /// For the `TypedDataSign` workflow, the final hash will be: + /// ``` + /// keccak256(\x19\x01 ‖ APP_DOMAIN_SEPARATOR ‖ + /// hashStruct(TypedDataSign({ + /// contents: hashStruct(originalStruct), + /// name: keccak256(bytes(eip712Domain().name)), + /// version: keccak256(bytes(eip712Domain().version)), + /// chainId: eip712Domain().chainId, + /// verifyingContract: eip712Domain().verifyingContract, + /// salt: eip712Domain().salt, + /// extensions: keccak256(abi.encodePacked(eip712Domain().extensions)) + /// })) + /// ) + /// ``` + /// where `‖` denotes the concatenation operator for bytes. + /// The order of the fields is important: `contents` comes before `name`. + /// + /// The signature will be `r ‖ s ‖ v ‖ + /// APP_DOMAIN_SEPARATOR ‖ contents ‖ contentsType ‖ uint16(contentsType.length)`, + /// where `contents` is the bytes32 struct hash of the original struct. + /// + /// The `APP_DOMAIN_SEPARATOR` and `contents` will be used to verify if `hash` is indeed correct. + /// __________________________________________________________________________________________ + /// + /// For the `PersonalSign` workflow, the final hash will be: + /// ``` + /// keccak256(\x19\x01 ‖ ACCOUNT_DOMAIN_SEPARATOR ‖ + /// hashStruct(PersonalSign({ + /// prefixed: keccak256(bytes(\x19Ethereum Signed Message:\n ‖ + /// base10(bytes(someString).length) ‖ someString)) + /// })) + /// ) + /// ``` + /// where `‖` denotes the concatenation operator for bytes. + /// + /// The `PersonalSign` type hash will be `keccak256("PersonalSign(bytes prefixed)")`. + /// The signature will be `r ‖ s ‖ v`. + /// __________________________________________________________________________________________ + /// + /// For demo and typescript code, see: + /// - https://github.com/junomonster/nested-eip-712 + /// - https://github.com/frangio/eip712-wrapper-for-eip1271 + /// + /// Their nomenclature may differ from ours, although the high-level idea is similar. + /// + /// Of course, if you have control over the codebase of the wallet client(s) too, + /// you can choose a more minimalistic signature scheme like + /// `keccak256(abi.encode(address(this), hash))` instead of all these acrobatics. + /// All these are just for widespread out-of-the-box compatibility with other wallet clients. + /// We want to create bazaars, not walled castles. + /// And we'll use push the Turing Completeness of the EVM to the limits to do so. + function _erc1271IsValidSignatureViaNestedEIP712(bytes32 hash, bytes calldata signature) + internal + view + virtual + returns (bool result) + { + bytes32 t = _typedDataSignFields(); + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Cache the free memory pointer. + // `c` is `contentsType.length`, which is stored in the last 2 bytes of the signature. + let c := shr(240, calldataload(add(signature.offset, sub(signature.length, 2)))) + for {} 1 {} { + let l := add(0x42, c) // Total length of appended data (32 + 32 + c + 2). + let o := add(signature.offset, sub(signature.length, l)) // Offset of appended data. + mstore(0x00, 0x1901) // Store the "\x19\x01" prefix. + calldatacopy(0x20, o, 0x40) // Copy the `APP_DOMAIN_SEPARATOR` and `contents` struct hash. + // Use the `PersonalSign` workflow if the reconstructed hash doesn't match, + // or if the appended data is invalid, i.e. + // `appendedData.length > signature.length || contentsType.length == 0`. + if or(xor(keccak256(0x1e, 0x42), hash), or(lt(signature.length, l), iszero(c))) { + t := 0 // Set `t` to 0, denoting that we need to `hash = _hashTypedData(hash)`. + mstore(t, _PERSONAL_SIGN_TYPEHASH) + mstore(0x20, hash) // Store the `prefixed`. + hash := keccak256(t, 0x40) // Compute the `PersonalSign` struct hash. + break + } + // Else, use the `TypedDataSign` workflow. + // `TypedDataSign({ContentsName} contents,bytes1 fields,...){ContentsType}`. + mstore(m, "TypedDataSign(") // Store the start of `TypedDataSign`'s type encoding. + let p := add(m, 0x0e) // Advance 14 bytes to skip "TypedDataSign(". + calldatacopy(p, add(o, 0x40), c) // Copy `contentsType` to extract `contentsName`. + // `d & 1 == 1` means that `contentsName` is invalid. + let d := shr(byte(0, mload(p)), 0x7fffffe000000000000010000000000) // Starts with `[a-z(]`. + // Store the end sentinel '(', and advance `p` until we encounter a '(' byte. + for { mstore(add(p, c), 40) } iszero(eq(byte(0, mload(p)), 40)) { p := add(p, 1) } { + d := or(shr(byte(0, mload(p)), 0x120100000001), d) // Has a byte in ", )\x00". + } + mstore(p, " contents,bytes1 fields,string n") // Store the rest of the encoding. + mstore(add(p, 0x20), "ame,string version,uint256 chain") + mstore(add(p, 0x40), "Id,address verifyingContract,byt") + mstore(add(p, 0x60), "es32 salt,uint256[] extensions)") + p := add(p, 0x7f) + calldatacopy(p, add(o, 0x40), c) // Copy `contentsType`. + // Fill in the missing fields of the `TypedDataSign`. + calldatacopy(t, o, 0x40) // Copy the `contents` struct hash to `add(t, 0x20)`. + mstore(t, keccak256(m, sub(add(p, c), m))) // Store `typedDataSignTypehash`. + // The "\x19\x01" prefix is already at 0x00. + // `APP_DOMAIN_SEPARATOR` is already at 0x20. + mstore(0x40, keccak256(t, 0x120)) // `hashStruct(typedDataSign)`. + // Compute the final hash, corrupted if `contentsName` is invalid. + hash := keccak256(0x1e, add(0x42, and(1, d))) + signature.length := sub(signature.length, l) // Truncate the signature. + break + } + mstore(0x40, m) // Restore the free memory pointer. + } + if (t == bytes32(0)) hash = _hashTypedData(hash); // `PersonalSign` workflow. + result = _erc1271IsValidSignatureNowCalldata(hash, signature); + } + + /// @dev For use in `_erc1271IsValidSignatureViaNestedEIP712`, + function _typedDataSignFields() private view returns (bytes32 m) { + ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) = eip712Domain(); + /// @solidity memory-safe-assembly + assembly { + m := mload(0x40) // Grab the free memory pointer. + mstore(0x40, add(m, 0x120)) // Allocate the memory. + // Skip 2 words for the `typedDataSignTypehash` and `contents` struct hash. + mstore(add(m, 0x40), shl(248, byte(0, fields))) + mstore(add(m, 0x60), keccak256(add(name, 0x20), mload(name))) + mstore(add(m, 0x80), keccak256(add(version, 0x20), mload(version))) + mstore(add(m, 0xa0), chainId) + mstore(add(m, 0xc0), shr(96, shl(96, verifyingContract))) + mstore(add(m, 0xe0), salt) + mstore(add(m, 0x100), keccak256(add(extensions, 0x20), shl(5, mload(extensions)))) + } + } + + /// @dev Performs the signature validation without nested EIP-712 to allow for easy sign ins. + /// This function must always return false or revert if called on-chain. + function _erc1271IsValidSignatureViaRPC(bytes32 hash, bytes calldata signature) + internal + view + virtual + returns (bool result) + { + // Non-zero gasprice is a heuristic to check if a call is on-chain, + // but we can't fully depend on it because it can be manipulated. + // See: https://x.com/NoahCitron/status/1580359718341484544 + if (tx.gasprice == uint256(0)) { + /// @solidity memory-safe-assembly + assembly { + mstore(gasprice(), gasprice()) // `mstore(0x00, 0x00)`. + // Basefee contract. + // See: https://gist.github.com/Vectorized/3c9b63524d57492b265454f62d895f71 + // We'll use an external contract to retrieve the basefee, + // because solc 0.8.4 and some chains do not support the basefee opcode. + // In absence of this contract, the basefee will be treated as zero. + let b := 0x000000000000378eDCD5B5B0A24f5342d8C10485 + pop(staticcall(0xffff, b, codesize(), gasprice(), gasprice(), 0x20)) + // If `gasprice < basefee`, the call cannot be on-chain, and we can skip the gas burn. + if iszero(mload(gasprice())) { + let m := mload(0x40) // Cache the free memory pointer. + mstore(gasprice(), 0x1626ba7e) // `isValidSignature(bytes32,bytes)`. + mstore(0x20, b) // Recycle `b` to denote if we need to burn gas. + mstore(0x40, 0x40) + let gasToBurn := or(add(0xffff, gaslimit()), gaslimit()) + // Burns gas computationally efficiently. Also, requires that `gas > gasToBurn`. + if or(eq(hash, b), lt(gas(), gasToBurn)) { invalid() } + // Make a call to this with `b`, efficiently burning the gas provided. + // No valid transaction can consume more than the gaslimit. + // See: https://ethereum.github.io/yellowpaper/paper.pdf + // Most RPCs perform calls with a gas budget greater than the gaslimit. + pop(staticcall(gasToBurn, address(), 0x1c, 0x64, gasprice(), gasprice())) + mstore(0x40, m) // Restore the free memory pointer. + } + } + result = _erc1271IsValidSignatureNowCalldata(hash, signature); + } + } +} diff --git a/assets/erc-7739/contracts/utils/EIP712.sol b/assets/erc-7739/contracts/utils/EIP712.sol new file mode 100644 index 0000000000..f77ee14983 --- /dev/null +++ b/assets/erc-7739/contracts/utils/EIP712.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Contract for EIP-712 typed structured data hashing and signing. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/EIP712.sol) +/// @author Modified from Solbase (https://github.com/Sol-DAO/solbase/blob/main/src/utils/EIP712.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol) +/// +/// @dev Note, this implementation: +/// - Uses `address(this)` for the `verifyingContract` field. +/// - Does NOT use the optional EIP-712 salt. +/// - Does NOT use any EIP-712 extensions. +/// This is for simplicity and to save gas. +/// If you need to customize, please fork / modify accordingly. +abstract contract EIP712 { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS AND IMMUTABLES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. + bytes32 internal constant _DOMAIN_TYPEHASH = + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + uint256 private immutable _cachedThis; + uint256 private immutable _cachedChainId; + bytes32 private immutable _cachedNameHash; + bytes32 private immutable _cachedVersionHash; + bytes32 private immutable _cachedDomainSeparator; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTRUCTOR */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Cache the hashes for cheaper runtime gas costs. + /// In the case of upgradeable contracts (i.e. proxies), + /// or if the chain id changes due to a hard fork, + /// the domain separator will be seamlessly calculated on-the-fly. + constructor() { + _cachedThis = uint256(uint160(address(this))); + _cachedChainId = block.chainid; + + string memory name; + string memory version; + if (!_domainNameAndVersionMayChange()) (name, version) = _domainNameAndVersion(); + bytes32 nameHash = _domainNameAndVersionMayChange() ? bytes32(0) : keccak256(bytes(name)); + bytes32 versionHash = + _domainNameAndVersionMayChange() ? bytes32(0) : keccak256(bytes(version)); + _cachedNameHash = nameHash; + _cachedVersionHash = versionHash; + + bytes32 separator; + if (!_domainNameAndVersionMayChange()) { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Load the free memory pointer. + mstore(m, _DOMAIN_TYPEHASH) + mstore(add(m, 0x20), nameHash) + mstore(add(m, 0x40), versionHash) + mstore(add(m, 0x60), chainid()) + mstore(add(m, 0x80), address()) + separator := keccak256(m, 0xa0) + } + } + _cachedDomainSeparator = separator; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* FUNCTIONS TO OVERRIDE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Please override this function to return the domain name and version. + /// ``` + /// function _domainNameAndVersion() + /// internal + /// pure + /// virtual + /// returns (string memory name, string memory version) + /// { + /// name = "Solady"; + /// version = "1"; + /// } + /// ``` + /// + /// Note: If the returned result may change after the contract has been deployed, + /// you must override `_domainNameAndVersionMayChange()` to return true. + function _domainNameAndVersion() + internal + view + virtual + returns (string memory name, string memory version); + + /// @dev Returns if `_domainNameAndVersion()` may change + /// after the contract has been deployed (i.e. after the constructor). + /// Default: false. + function _domainNameAndVersionMayChange() internal pure virtual returns (bool result) {} + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* HASHING OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the EIP-712 domain separator. + function _domainSeparator() internal view virtual returns (bytes32 separator) { + if (_domainNameAndVersionMayChange()) { + separator = _buildDomainSeparator(); + } else { + separator = _cachedDomainSeparator; + if (_cachedDomainSeparatorInvalidated()) separator = _buildDomainSeparator(); + } + } + + /// @dev Returns the hash of the fully encoded EIP-712 message for this domain, + /// given `structHash`, as defined in + /// https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct. + /// + /// The hash can be used together with {ECDSA-recover} to obtain the signer of a message: + /// ``` + /// bytes32 digest = _hashTypedData(keccak256(abi.encode( + /// keccak256("Mail(address to,string contents)"), + /// mailTo, + /// keccak256(bytes(mailContents)) + /// ))); + /// address signer = ECDSA.recover(digest, signature); + /// ``` + function _hashTypedData(bytes32 structHash) internal view virtual returns (bytes32 digest) { + // We will use `digest` to store the domain separator to save a bit of gas. + if (_domainNameAndVersionMayChange()) { + digest = _buildDomainSeparator(); + } else { + digest = _cachedDomainSeparator; + if (_cachedDomainSeparatorInvalidated()) digest = _buildDomainSeparator(); + } + /// @solidity memory-safe-assembly + assembly { + // Compute the digest. + mstore(0x00, 0x1901000000000000) // Store "\x19\x01". + mstore(0x1a, digest) // Store the domain separator. + mstore(0x3a, structHash) // Store the struct hash. + digest := keccak256(0x18, 0x42) + // Restore the part of the free memory slot that was overwritten. + mstore(0x3a, 0) + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EIP-5267 OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev See: https://eips.ethereum.org/EIPS/eip-5267 + function eip712Domain() + public + view + virtual + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + fields = hex"0f"; // `0b01111`. + (name, version) = _domainNameAndVersion(); + chainId = block.chainid; + verifyingContract = address(this); + salt = salt; // `bytes32(0)`. + extensions = extensions; // `new uint256[](0)`. + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* PRIVATE HELPERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the EIP-712 domain separator. + function _buildDomainSeparator() private view returns (bytes32 separator) { + // We will use `separator` to store the name hash to save a bit of gas. + bytes32 versionHash; + if (_domainNameAndVersionMayChange()) { + (string memory name, string memory version) = _domainNameAndVersion(); + separator = keccak256(bytes(name)); + versionHash = keccak256(bytes(version)); + } else { + separator = _cachedNameHash; + versionHash = _cachedVersionHash; + } + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Load the free memory pointer. + mstore(m, _DOMAIN_TYPEHASH) + mstore(add(m, 0x20), separator) // Name hash. + mstore(add(m, 0x40), versionHash) + mstore(add(m, 0x60), chainid()) + mstore(add(m, 0x80), address()) + separator := keccak256(m, 0xa0) + } + } + + /// @dev Returns if the cached domain separator has been invalidated. + function _cachedDomainSeparatorInvalidated() private view returns (bool result) { + uint256 cachedChainId = _cachedChainId; + uint256 cachedThis = _cachedThis; + /// @solidity memory-safe-assembly + assembly { + result := iszero(and(eq(chainid(), cachedChainId), eq(address(), cachedThis))) + } + } +} diff --git a/assets/erc-7739/contracts/utils/LibString.sol b/assets/erc-7739/contracts/utils/LibString.sol new file mode 100644 index 0000000000..cb67e372e3 --- /dev/null +++ b/assets/erc-7739/contracts/utils/LibString.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Library for converting numbers into strings and other string operations. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/LibString.sol) +/// +/// @dev Note: +/// For performance and bytecode compactness, most of the string operations are restricted to +/// byte strings (7-bit ASCII), except where otherwise specified. +/// Usage of byte string operations on charsets with runes spanning two or more bytes +/// can lead to undefined behavior. +library LibString { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The constant returned when the `search` is not found in the string. + uint256 internal constant NOT_FOUND = type(uint256).max; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* DECIMAL OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the base 10 decimal representation of `value`. + function toString(uint256 value) internal pure returns (string memory str) { + /// @solidity memory-safe-assembly + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but + // we allocate 0xa0 bytes to keep the free memory pointer 32-byte word aligned. + // We will need 1 word for the trailing zeros padding, 1 word for the length, + // and 3 words for a maximum of 78 digits. + str := add(mload(0x40), 0x80) + mstore(0x40, add(str, 0x20)) // Allocate the memory. + mstore(str, 0) // Zeroize the slot after the string. + + let end := str // Cache the end of the memory to calculate the length later. + let w := not(0) + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + for { let temp := value } 1 {} { + str := add(str, w) // `sub(str, 1)`. + // Store the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + temp := div(temp, 10) // Keep dividing `temp` until zero. + if iszero(temp) { break } + } + let length := sub(end, str) + str := sub(str, 0x20) // Move the pointer 32 bytes back to make room for the length. + mstore(str, length) // Store the length. + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* BYTE STRING OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + // For performance and bytecode compactness, byte string operations are restricted + // to 7-bit ASCII strings. All offsets are byte offsets, not UTF character offsets. + // Usage of byte string operations on charsets with runes spanning two or more bytes + // can lead to undefined behavior. + + /// @dev Returns the byte index of the first location of `search` in `subject`, + /// searching from left to right, starting from `from`. + /// Returns `NOT_FOUND` (i.e. `type(uint256).max`) if the `search` is not found. + function indexOf(string memory subject, string memory search, uint256 from) + internal + pure + returns (uint256 result) + { + /// @solidity memory-safe-assembly + assembly { + for { let subjectLength := mload(subject) } 1 {} { + if iszero(mload(search)) { + if iszero(gt(from, subjectLength)) { + result := from + break + } + result := subjectLength + break + } + let searchLength := mload(search) + let subjectStart := add(subject, 0x20) + + result := not(0) // Initialize to `NOT_FOUND`. + subject := add(subjectStart, from) + let end := add(sub(add(subjectStart, subjectLength), searchLength), 1) + + let m := shl(3, sub(0x20, and(searchLength, 0x1f))) + let s := mload(add(search, 0x20)) + + if iszero(and(lt(subject, end), lt(from, subjectLength))) { break } + + if iszero(lt(searchLength, 0x20)) { + for { let h := keccak256(add(search, 0x20), searchLength) } 1 {} { + if iszero(shr(m, xor(mload(subject), s))) { + if eq(keccak256(subject, searchLength), h) { + result := sub(subject, subjectStart) + break + } + } + subject := add(subject, 1) + if iszero(lt(subject, end)) { break } + } + break + } + for {} 1 {} { + if iszero(shr(m, xor(mload(subject), s))) { + result := sub(subject, subjectStart) + break + } + subject := add(subject, 1) + if iszero(lt(subject, end)) { break } + } + break + } + } + } + + /// @dev Returns the byte index of the first location of `search` in `subject`, + /// searching from left to right. + /// Returns `NOT_FOUND` (i.e. `type(uint256).max`) if the `search` is not found. + function indexOf(string memory subject, string memory search) + internal + pure + returns (uint256 result) + { + result = indexOf(subject, search, 0); + } + + /// @dev Returns a copy of `subject` sliced from `start` to `end` (exclusive). + /// `start` and `end` are byte offsets. + function slice(string memory subject, uint256 start, uint256 end) + internal + pure + returns (string memory result) + { + /// @solidity memory-safe-assembly + assembly { + let subjectLength := mload(subject) + if iszero(gt(subjectLength, end)) { end := subjectLength } + if iszero(gt(subjectLength, start)) { start := subjectLength } + if lt(start, end) { + result := mload(0x40) + let resultLength := sub(end, start) + mstore(result, resultLength) + subject := add(subject, start) + let w := not(0x1f) + // Copy the `subject` one word at a time, backwards. + for { let o := and(add(resultLength, 0x1f), w) } 1 {} { + mstore(add(result, o), mload(add(subject, o))) + o := add(o, w) // `sub(o, 0x20)`. + if iszero(o) { break } + } + // Zeroize the slot after the string. + mstore(add(add(result, 0x20), resultLength), 0) + mstore(0x40, add(result, add(resultLength, 0x40))) // Allocate the memory. + } + } + } + + /// @dev Returns a copy of `subject` sliced from `start` to the end of the string. + /// `start` is a byte offset. + function slice(string memory subject, uint256 start) + internal + pure + returns (string memory result) + { + result = slice(subject, start, uint256(int256(-1))); + } +} diff --git a/assets/erc-7739/contracts/utils/SignatureCheckerLib.sol b/assets/erc-7739/contracts/utils/SignatureCheckerLib.sol new file mode 100644 index 0000000000..cd790c74f8 --- /dev/null +++ b/assets/erc-7739/contracts/utils/SignatureCheckerLib.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Signature verification helper that supports both ECDSA signatures from EOAs +/// and ERC1271 signatures from smart contract wallets like Argent and Gnosis safe. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SignatureCheckerLib.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/SignatureChecker.sol) +/// +/// @dev Note: +/// - The signature checking functions use the ecrecover precompile (0x1). +/// - Unlike ECDSA signatures, contract signatures are revocable. +/// - All `bytes signature` variants accept both +/// regular 65-byte `(r, s, v)` and EIP-2098 `(r, vs)` short form signatures. +/// See: https://eips.ethereum.org/EIPS/eip-2098 +/// This is for calldata efficiency on smart accounts prevalent on L2s. +/// +/// WARNING! Do NOT use signatures as unique identifiers: +/// - Use a nonce in the digest to prevent replay attacks on the same contract. +/// - Use EIP-712 for the digest to prevent replay attacks across different chains and contracts. +/// EIP-712 also enables readable signing of typed data for better user safety. +/// This implementation does NOT check if a signature is non-malleable. +library SignatureCheckerLib { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* SIGNATURE CHECKING OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns whether `signature` is valid for `signer` and `hash`. + /// If `signer` is a smart contract, the signature is validated with ERC1271. + /// Otherwise, the signature is validated with `ECDSA.recover`. + function isValidSignatureNowCalldata(address signer, bytes32 hash, bytes calldata signature) + internal + view + returns (bool isValid) + { + /// @solidity memory-safe-assembly + assembly { + // Clean the upper 96 bits of `signer` in case they are dirty. + for { signer := shr(96, shl(96, signer)) } signer {} { + let m := mload(0x40) + mstore(0x00, hash) + if eq(signature.length, 64) { + let vs := calldataload(add(signature.offset, 0x20)) + mstore(0x20, add(shr(255, vs), 27)) // `v`. + mstore(0x40, calldataload(signature.offset)) // `r`. + mstore(0x60, shr(1, shl(1, vs))) // `s`. + let t := + staticcall( + gas(), // Amount of gas left for the transaction. + 1, // Address of `ecrecover`. + 0x00, // Start of input. + 0x80, // Size of input. + 0x01, // Start of output. + 0x20 // Size of output. + ) + // `returndatasize()` will be `0x20` upon success, and `0x00` otherwise. + if iszero(or(iszero(returndatasize()), xor(signer, mload(t)))) { + isValid := 1 + mstore(0x60, 0) // Restore the zero slot. + mstore(0x40, m) // Restore the free memory pointer. + break + } + } + if eq(signature.length, 65) { + mstore(0x20, byte(0, calldataload(add(signature.offset, 0x40)))) // `v`. + calldatacopy(0x40, signature.offset, 0x40) // `r`, `s`. + let t := + staticcall( + gas(), // Amount of gas left for the transaction. + 1, // Address of `ecrecover`. + 0x00, // Start of input. + 0x80, // Size of input. + 0x01, // Start of output. + 0x20 // Size of output. + ) + // `returndatasize()` will be `0x20` upon success, and `0x00` otherwise. + if iszero(or(iszero(returndatasize()), xor(signer, mload(t)))) { + isValid := 1 + mstore(0x60, 0) // Restore the zero slot. + mstore(0x40, m) // Restore the free memory pointer. + break + } + } + mstore(0x60, 0) // Restore the zero slot. + mstore(0x40, m) // Restore the free memory pointer. + + let f := shl(224, 0x1626ba7e) + mstore(m, f) // `bytes4(keccak256("isValidSignature(bytes32,bytes)"))`. + mstore(add(m, 0x04), hash) + let d := add(m, 0x24) + mstore(d, 0x40) // The offset of the `signature` in the calldata. + mstore(add(m, 0x44), signature.length) + // Copy the `signature` over. + calldatacopy(add(m, 0x64), signature.offset, signature.length) + // forgefmt: disable-next-item + isValid := and( + // Whether the returndata is the magic value `0x1626ba7e` (left-aligned). + eq(mload(d), f), + // Whether the staticcall does not revert. + // This must be placed at the end of the `and` clause, + // as the arguments are evaluated from right to left. + staticcall( + gas(), // Remaining gas. + signer, // The `signer` address. + m, // Offset of calldata in memory. + add(signature.length, 0x64), // Length of calldata in memory. + d, // Offset of returndata. + 0x20 // Length of returndata to write. + ) + ) + break + } + } + } +} diff --git a/config/eipw.toml b/config/eipw.toml index c195734cd8..90322beaf0 100644 --- a/config/eipw.toml +++ b/config/eipw.toml @@ -2,13 +2,13 @@ kind = "set-default-annotation" name = "status" value = "Stagnant" -annotation_type = "warning" +annotation_level = "warning" [[modifiers]] kind = "set-default-annotation" name = "status" value = "Withdrawn" -annotation_type = "warning" +annotation_level = "warning" [lints.markdown-re-eip-dash] kind = "markdown-regex" @@ -115,6 +115,7 @@ names = [ # "Final", # "Withdrawn", # "Living", +# "Moved", # ], # ] @@ -145,6 +146,8 @@ exceptions = [ '^https://(www\.)?github\.com/ethereum/yellowpaper/commit/[a-f0-9]{40}$', '^https://(www\.)?github\.com/ethereum/devp2p/(blob|tree)/[0-9a-f]{40}/.+$', '^https://(www\.)?github\.com/ethereum/devp2p/commit/[0-9a-f]{40}$', + '^https://(www\.)?github\.com/ethereum/portal-network-specs/(blob|tree)/[0-9a-f]{40}/.+$', + '^https://(www\.)?github\.com/ethereum/portal-network-specs/commit/[0-9a-f]{40}$', '^https://(www\.)?github\.com/bitcoin/bips/(blob|tree)/[0-9a-f]{40}/bip-[0-9]+\.mediawiki$', '^https://(www\.)?github\.com/ChainAgnostic/CAIPs/(blob|tree)/[a-f0-9]{40}/.+$', '^https://(www\.)?github\.com/ChainAgnostic/CAIPs/commit/[0-9a-f]{40}$', @@ -794,6 +797,7 @@ flow = [ "Final", "Withdrawn", "Living", + "Moved", ], ] @@ -842,7 +846,7 @@ min = 1 [lints.markdown-link-first] kind = "markdown-link-first" -pattern = "(?i)(?:eip|erc)-[0-9]+" +pattern = "(?i)(?:eip|erc)-([0-9])+" [lints.markdown-re-erc-dash] kind = "markdown-regex" @@ -925,4 +929,9 @@ name = "eip" prefix = "erc-" suffix = ".md" +[lints.markdown-no-backticks] +kind = "markdown-no-backticks" +pattern = "(?i)(eip|erc)-[0-9]+" +[lints.markdown-headings-space] +kind = "markdown-headings-space"