Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add call flow spec for validation and execution #166

Merged
merged 3 commits into from
Aug 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 34 additions & 30 deletions standard/ERCs/erc-6900.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ status: Draft
type: Standards Track
category: ERC
created: 2023-04-18
requires: 165, 4337
requires: 165, 1271, 4337
---

## Abstract
Expand Down Expand Up @@ -68,6 +68,7 @@ Each step is modular, supporting different implementations, that allows for open
- `IAccount.sol` from [ERC-4337](./eip-4337.md).
- `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)

**Modular Smart Contract Accounts** **MAY** implement

Expand Down Expand Up @@ -471,18 +472,6 @@ An account can have the same validation module installed more than once.
The entityId 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.

#### Responsibilties of `StandardExecutor` and `ModuleExecutor`

`StandardExecutor` functions are used for open-ended calls to external addresses.

`ModuleExecutor` functions are specifically used by modules to request the account to execute with account's context. Explicit permissions are required for modules to use `ModuleExecutor`.

The following behavior MUST be followed:

- `StandardExecutor` can NOT call module execution functions and/or `ModuleExecutor`. This is guaranteed by checking whether the call's target implements the `IModule` interface via ERC-165 as required.
- `StandardExecutor` can NOT be called by module execution functions and/or `ModuleExecutor`.
- Module execution functions MUST NOT request access to `StandardExecutor`, they MAY request access to `ModuleExecutor`.

#### Calls to `installModule`

The function `installModule` accepts 3 parameters: the address of the module to install, the Keccak-256 hash of the module's manifest, ABI-encoded data to pass to the module's `onInstall` callback.
Expand Down Expand Up @@ -538,36 +527,51 @@ Finally, the function MUST emit the event `ModuleUninstalled` with the module's

> **⚠️ Incorrectly uninstalled modules can prevent uninstalls of their dependencies. Therefore, some form of validation that the uninstall step completely and correctly removes the module and its usage of dependencies is required.**

#### Calls to `validateUserOp`
### Validation Call Flow

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 `executeWithAuthorization`, or when using direct call validation. Signature validation happens within the account's implementation of the function `isValidSignature`, defined in ERC-1271.

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.
Zer0dot marked this conversation as resolved.
Show resolved Hide resolved

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. Then, the account MUST execute all validation hooks of the corresponding type associated with the validation function in use. These hooks MUST be executed in the order specified during installation. After the execution of validation hooks, the account MUST invoke the validation function of the corresponding type. If any validation hooks or the validation function revert, the account MUST revert. It SHOULD include the module's revert data within its revert data.

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 account MUST define a way to pass data separately for each pre-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.

Is the second sentence necessary?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use pre validation hook(s) for all validation hook(s) references to be precise and consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the second sentence necessary?

Yes, if that is not standardized, then the modules may be written to expect the data in different locations, which would result in modules that are compatible with some 6900-compliant accounts, but not all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use pre validation hook(s) for all validation hook(s) references to be precise and consistent.

I thought about this, but I think the fact that there are not post validation hook(s) means that the "pre" wording doesn't differentiate it any further. It gives some insight into when to expect it to be run, but that's not really necessary for this section of the spec.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely get what you mean. However, I think there are benefits to have pre as it makes it a bit easier to understand how it works for new comers to (4337 and 6900) who does not know a ton how exactly validation works yet.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards just calling them validation hooks (adding "pre" might signal that there exists a "post", similar to execution hooks). But I see that we do reference "pre validation hooks" in the prior paragraph. We should be consistent everywhere.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point on signaling "post". Happy to go with "validation hooks" with consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to be consistent


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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, in other words, means that the result of UO validation should always be the longest possible time bound that satisfies all time bounds form hooks , right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Technically I think it is accurate to describe it as the "longest time bound that satisfies hooks", but intuitively, the operation of intersecting them is to take the min. E.g. if you have ascending timestamps $t_1$, $t_2$, $t_3$, $t_4$, and hook 1 returns the range $(t_1, t_3)$, and the validation function returns $(t_2, t_4)$, the result of validation should be the range $(t_2, t_3)$.


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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
The set of pre 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's been decided to just use "validation hooks" as the consistent term yeah?


#### Checking Validation Applicability

To enforce module permission isolation, the modular account MUST check validation function applicability as part of each validation function implementation.

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.

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.
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.

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 the selector has any pre execution hooks 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.
Installed validations have two flag variables indicating what they may be used for. If a validation is attempted to be used for user op validation and the flag `isUserOpValidation` is set to false, validation MUST revert. If the validation is attempted to be used for signature validation and the flag `isSignatureValidation` is set to false, validation MUST revert.

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`.
#### Direct Call Validation.

#### Calls to execution functions
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 `executeWithAuthorization` to use as a selection mechanism for a runtime validation function.

When a function other than a native function is called on an modular account, it MUST find the module configuration for the corresponding selector added via module installation. If no corresponding module is found, the modular account MUST revert. Otherwise, the following steps MUST be performed.
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 pre validation hooks and execution hooks installed to this validation function MUST still run.

Additionally, when the modular account natively implements functions in `IModuleManager` and `IStandardExecutor`, the same following steps MUST be performed for those functions. Other native functions MAY perform these steps.
### Execution Call Flow

The steps to perform are:
For all non-view functions within `IModularAccount` except `executeWithAuthorization`, 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:

- 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 execution hooks are set to `PRE_HOOK_ALWAYS_DENY`, execution MUST revert. If the validation function is set to `RUNTIME_VALIDATION_ALWAYS_ALLOW`, the runtime 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 `ModuleEntity`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.
If the caller is not the EntryPoint or the account, the account MUST check access control for direct call validation.

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 `installModule` and `uninstallModule`, which modify the account state, and possibly other execution or native functions as well.
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 a permission hook to the validation function that was used for the current execution. Pre execution hooks MUST run validation-associated hooks first, in the order they were installed, then selector-associated hooks second, in the order they were installed.

#### Calls made from modules
Next, the modular account MUST run the target function, either an account native function or a module-defined execution function.

Modules MAY interact with other modules and external addresses through the modular account using the functions defined in the `IModuleExecutor` interface. These functions MAY be called without a defined validation function, but the modular account MUST enforce these checks and behaviors:
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.

The `executeFromModule` function MUST allow modules to call execution functions installed by modules on the modular account. Hooks matching the function selector provided in `data` MUST be called. If the calling module's manifest did not include the provided function selector within `permittedExecutionSelectors` at the time of installation, execution MUST revert.
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 `executeFromModuleExternal` function MUST allow modules to call external addresses as specified by its parameters on behalf of the modular account. If the calling module's manifest did not explicitly allow the external call within `permittedExternalCalls` at the time of installation, execution MUST revert.
Module execution functions where the field `isPublic` is set to true, or 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should state the risk where if the isPublic function changes states, omitting validation can be dangerous.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need that extra detail? I think that is obvious from the spec stating that validation can be skipped if the function is marked public.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code comment on isPublic is Whether or not the function needs runtime validation, or can be called by anyone. .
Why didn't we name it skipRuntimeValidation flag?

My concern of the name isPublic is that it does not indicate that it can potentially be state changing. We only skip runtime by using the old ALWAYS_ALLOW.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why didn't we name it skipRuntimeValidation flag?

That does seem like a good idea, makes it more literal. Maybe less obvious to connect it to view functions, but it would match the naming of other flags.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe less obvious to connect it to view functions

They don't have to be view functions is the whole point (to allow state changing though I am not sure what the use cases are, maybe exec timelock tx after trigger time or sth), right?
Otherwise, it can just be isPublic or isPublicView or isView, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Let's update that in a different PR, because it also requires code changes.


### Extension

Expand Down
Loading