diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol new file mode 100644 index 00000000..9dd7a78e --- /dev/null +++ b/contracts/account/extensions/AccountERC6900.sol @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {PackedUserOperation, IAccount} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; +import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Calldata} from "@openzeppelin/contracts/utils/Calldata.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {IERC6900ModularAccount, IERC6900Module, IERC6900ValidationModule, IERC6900ExecutionModule, ValidationConfig, ModuleEntity, ValidationFlags, HookConfig, ExecutionManifest, ManifestExecutionHook, ManifestExecutionFunction, Call} from "../../interfaces/IERC6900.sol"; +import {ERC6900Utils} from "../utils/ERC6900Utils.sol"; +import {AccountCore} from "../AccountCore.sol"; + +/** + * @dev Extension of {AccountCore} that implements support for ERC-6900 modules. + * + * To comply with the ERC-1271 support requirement, this contract implements {ERC7739} as an + * opinionated layer to avoid signature replayability across accounts controlled by the same key. + * + * This contract does not implement validation logic for user operations since these functionality + * is often delegated to self-contained validation modules. Developers must install a validator module + * upon initialization (or any other mechanism to enable execution from the account): + * + * ```solidity + * contract MyAccountERC6900is AccountERC6900, Initializable { + * constructor() EIP712("MyAccount", "1") {} + * + * function installValidation( + * ValidationConfig validationConfig, + * bytes4[] calldata selectors, + * bytes calldata installData, + * bytes[] calldata hooks + * ) public initializer { + * _installValidation(validationConfig, selectors, installData, hooks); + * } + * } + * ``` + * + */ + +abstract contract AccountERC6900 is AccountCore, IERC1271, IERC6900ModularAccount { + using Bytes for *; + using ERC6900Utils for *; + using EnumerableSet for *; + using Packing for bytes32; + + mapping(ModuleEntity moduleEntity => Validation) private _validations; + mapping(bytes4 selector => Execution) private _executions; + mapping(bytes4 interfaceId => uint256 supported) private _interfaceIds; + + error ERC6900ModuleInterfaceNotSupported(address module, bytes4 expectedInterface); + error ERC6900AlreadySetSelectorForValidation(ModuleEntity validationFunction, bytes4 selector); + error ERC6900AlreadySetValidationHookForValidation(); + error ERC6900AlreadySetExecutionHookForExecution(); + error ERC6900AlreadySetExecutionHookForValidation(); + error ERC6900AlreadyUsedModuleFunctionExecutionSelector(bytes4 selector); + error ERC6900ExecutionSelectorConflictingWithERC4337Function(address module, bytes4 selector); + error ERC6900ExecutionSelectorConflictingWithERC6900Function(address module, bytes4 selector); + error ERC6900BadUserOpValidation(ModuleEntity moduleEntity); + error ERC6900BadSignatureValidation(ModuleEntity moduleEntity); + error ERC6900MissingValidationForSelector(bytes4 selector); + error ERC6900ExecutionSelectorNotAllowedForGlobalValidation( + ModuleEntity validationModuleEntity, + bytes4 executionSelector + ); + error ERC6900InvalidExecuteTarget(); + error ERC6900MissingFallbackExecutionModule(address module); + + struct Validation { + EnumerableSet.Bytes32Set selectors; + ValidationFlags validationFlags; + EnumerableSet.Bytes32Set validationHooks; + EnumerableSet.Bytes32Set executionHooks; + } + + struct Execution { + address module; + bool skipRuntimeValidation; + bool allowGlobalValidation; + EnumerableSet.Bytes32Set executionHooks; + } + + /// @dev See {_fallback}. + fallback(bytes calldata) external payable virtual returns (bytes memory) { + return _fallback(); + } + + /// @inheritdoc IERC6900ModularAccount + function accountId() public view virtual returns (string memory) { + // vendorname.accountname.semver + return "@openzeppelin/community-contracts.AccountERC6900.v0.0.0"; + } + + /// @inheritdoc IERC6900ModularAccount + function installValidation( + ValidationConfig validationConfig, + bytes4[] calldata selectors, + bytes calldata installData, + bytes[] calldata hooks + ) public virtual onlyEntryPointOrSelf { + _installValidation(validationConfig, selectors, installData, hooks); + } + + /// @inheritdoc IERC6900ModularAccount + function uninstallValidation( + ModuleEntity validationFunction, + bytes calldata uninstallData, + bytes[] calldata hookUninstallData + ) public virtual onlyEntryPointOrSelf { + // _uninstallModule(moduleTypeId, module, deInitData); + } + + /// @inheritdoc IERC6900ModularAccount + function installExecution( + address module, + ExecutionManifest memory manifest, + bytes calldata installData + ) public virtual onlyEntryPointOrSelf { + _installExecution(module, manifest, installData); + } + + /// @inheritdoc IERC6900ModularAccount + function uninstallExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata uninstallData + ) public virtual onlyEntryPointOrSelf { + // _uninstallModule(moduleTypeId, module, deInitData); + } + + /// @inheritdoc IERC6900ModularAccount + function execute( + address target, + uint256 value, + bytes calldata data + ) public payable virtual onlyEntryPointOrSelf returns (bytes memory) { + return _execute(target, value, data); + } + + /// @inheritdoc IERC6900ModularAccount + function executeBatch(Call[] calldata calls) public payable virtual onlyEntryPointOrSelf returns (bytes[] memory) { + bytes[] memory returnedData = new bytes[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { + returnedData[i] = _execute(calls[i].target, calls[i].value, calls[i].data); + } + return returnedData; + } + + /// @inheritdoc IERC6900ModularAccount + function executeWithRuntimeValidation( + bytes calldata data, + bytes calldata authorization + ) public payable virtual onlyEntryPointOrSelf returns (bytes memory) { + ModuleEntity validationModuleEntity = ModuleEntity.wrap(bytes24(authorization[:24])); + bytes calldata validationAuth = authorization[24:]; + _checkValidationApplicability(data); + IERC6900ValidationModule(validationModuleEntity.module()).validateRuntime( + address(this), + validationModuleEntity.entityId(), + msg.sender, + msg.value, + data, + validationAuth + ); + return _execute(address(this), msg.value, data); + } + + /** + * @dev Validates a user operation with {_signableUserOpHash} and returns the validation data + * if the module specified by the first 20 bytes of the nonce key is installed. Falls back to + * {AccountCore-_validateUserOp} otherwise. + * + * See {_extractUserOpValidator} for the module extraction logic. + */ + function _validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256) { + ModuleEntity validationModuleEntity = _extractUserOpValidator(userOp); + (address module, uint32 entityId) = validationModuleEntity.unpack(); + Validation storage validation = _validations[validationModuleEntity]; + ValidationFlags _validationFlags = validation.validationFlags; + // If a validation function is attempted to be used for user op validation + // and the flag isUserOpValidation is set to false, validation MUST revert + require(!_validationFlags.isUserOpValidation(), ERC6900BadUserOpValidation(validationModuleEntity)); + bytes4 executionSelector = bytes4(userOp.callData[:4]); + // validation installation MAY specify the isGlobal flag as true + if (_validationFlags.isGlobalValidation()) { + Execution storage execution = _executions[executionSelector]; + // The account MUST consider the validation applicable to any module + // execution function with the allowGlobalValidation flag set to true + require( + !execution.allowGlobalValidation, + ERC6900ExecutionSelectorNotAllowedForGlobalValidation(validationModuleEntity, executionSelector) + ); + } else { + // validation functions have a configurable range of applicability. + // This can be configured with selectors installed to a validation + require( + !validation.selectors.contains(executionSelector), + ERC6900MissingValidationForSelector(executionSelector) + ); + } + _checkValidationApplicability(userOp.callData); + return + IERC6900ValidationModule(module).validateUserOp(entityId, userOp, _signableUserOpHash(userOp, userOpHash)); + } + + /** + * @dev If the selector being checked is execute or executeBatch, it MUST perform + * additional checking on target. + */ + function _checkValidationApplicability(bytes calldata callData) internal view { + bytes4 executionSelector = bytes4(callData[:4]); + bytes memory data = callData[4:]; + if (executionSelector == IERC6900ModularAccount.execute.selector) { + (address target, , ) = abi.decode(data, (address, uint256, bytes)); + require(target != address(this), ERC6900InvalidExecuteTarget()); + } + if (executionSelector == IERC6900ModularAccount.executeBatch.selector) { + Call[] memory calls = abi.decode(data, (Call[])); + for (uint256 i = 0; i < calls.length; i++) { + require(calls[i].target != address(this), ERC6900InvalidExecuteTarget()); + } + } + } + + /** + * Signature validation happens within the account's implementation of the function + * isValidSignature, defined in ERC-1271. + */ + function isValidSignature(bytes32 hash, bytes calldata signature) public view override returns (bytes4 magicValue) { + return _rawSignatureValidation(hash, signature) ? IERC1271.isValidSignature.selector : bytes4(0xffffffff); + } + + /** + * This function delegates the signature validation to a validation module if the + * first 24 bytes of the signature correspond to an installed validation module entity. + * + * See {_extractSignatureValidator} for the module extraction logic. + */ + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) internal view virtual override returns (bool) { + if (signature.length < 25) return false; + (ModuleEntity validationModuleEntity, bytes calldata innerSignature) = _extractSignatureValidator(signature); + (address module, uint32 entityId) = validationModuleEntity.unpack(); + // If the validation function is attempted to be used for signature validation + // and the flag isSignatureValidation is set to false, validation MUST revert + require( + !_validations[validationModuleEntity].validationFlags.isSignatureValidation(), + ERC6900BadSignatureValidation(validationModuleEntity) + ); + + return + IERC6900ValidationModule(module).validateSignature( + address(this), + entityId, + msg.sender, + hash, + innerSignature + ) == IERC1271.isValidSignature.selector; + } + + /** + * @dev ERC-6900 execution logic. + * + */ + function _execute(address target, uint256 value, bytes calldata data) internal virtual returns (bytes memory) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + return Address.verifyCallResult(success, returndata); + } + + /** + * @dev Installs a validation module of the given type with the given initialization data. + * + * + * Requirements: + * + * * TODO + * + * Emits a {ValidationInstalled} event. + */ + function _installValidation( + ValidationConfig validationConfig, + bytes4[] calldata selectors, + bytes calldata installData, + bytes[] calldata hooks + ) internal virtual { + (ModuleEntity moduleEntity, ValidationFlags _validationFlags) = validationConfig.unpack(); + (address module, uint32 entityId) = moduleEntity.unpack(); + // Modules MUST implement ERC-165 for IModule. + require( + ERC165Checker.supportsInterface(module, type(IERC6900Module).interfaceId), + ERC6900ModuleInterfaceNotSupported(module, type(IERC6900Module).interfaceId) + ); + Validation storage validation = _validations[moduleEntity]; + // The account MUST configure the validation function to validate all of the selectors specified by the user. + for (uint256 i = 0; i < selectors.length; i++) { + require( + validation.selectors.add(selectors[i]), + ERC6900AlreadySetSelectorForValidation(moduleEntity, selectors[i]) + ); + } + // - 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. + for (uint256 i = 0; i < hooks.length; i++) { + bytes calldata hook = hooks[i]; + HookConfig hookConfig = hook.extractHookConfig(); + address hookModule = hookConfig.module(); + bytes4 expectedInterface; + if (hookConfig.isValidationHook()) { + expectedInterface = type(IERC6900ValidationModule).interfaceId; + require( + validation.validationHooks.add(bytes32(hook[:24])), + ERC6900AlreadySetValidationHookForValidation() + ); + } else { + // Is execution hook + expectedInterface = type(IERC6900ExecutionModule).interfaceId; + require( + validation.executionHooks.add(bytes32(hook[:25])), + ERC6900AlreadySetExecutionHookForValidation() + ); + } + // TODO: Firstly check interface is supported + if (hookModule.code.length > 0) { + require( + ERC165Checker.supportsInterface(hookModule, expectedInterface), + ERC6900ModuleInterfaceNotSupported(hookModule, expectedInterface) + ); + //IERC6900Module(hookModule).onInstall(installData); //TODO Enable + } + } + // The account MUST set all flags as specified, like isGlobal, isSignatureValidation, and isUserOpValidation. + validation.validationFlags = _validationFlags; + // The account SHOULD call onInstall on the validation module to initialize state if specified by the user. + IERC6900Module(module).onInstall(installData); + // The account MUST emit ValidationInstalled as defined in the interface for all installed validation functions. + emit ValidationInstalled(module, entityId); + } + + function _installExecution( + address module, + ExecutionManifest memory manifest, // TODO: change to call data + bytes calldata installData + ) internal virtual { + // Modules MUST implement ERC-165 for IModule. + require( + IERC6900Module(module).supportsInterface(type(IERC6900Module).interfaceId), //TODO Use checker + ERC6900ModuleInterfaceNotSupported(module, type(IERC6900Module).interfaceId) + ); + // The account MUST install all execution functions and set flags and fields as specified in the manifest. + ManifestExecutionFunction[] memory executionFunctions = manifest.executionFunctions; + for (uint256 i = 0; i < executionFunctions.length; i++) { + ManifestExecutionFunction memory executionFunction = executionFunctions[i]; + bytes4 executionSelector = executionFunction.executionSelector; + Execution storage execution = _executions[executionSelector]; + // An execution function selector MUST be unique in the account. + require( + execution.module == address(0), + ERC6900AlreadyUsedModuleFunctionExecutionSelector(executionSelector) + ); + // An execution function selector MUST not conflict with native ERC-4337 and ERC-6900 functions. + require( + IAccount.validateUserOp.selector != executionSelector, // TODO Check other ERC-4337 functions + ERC6900ExecutionSelectorConflictingWithERC4337Function(module, executionSelector) + ); + require( + IERC6900ModularAccount.execute.selector != executionSelector, // TODO Check other ERC-6900 functions + ERC6900ExecutionSelectorConflictingWithERC6900Function(module, executionSelector) + ); + execution.module = module; + execution.skipRuntimeValidation = executionFunction.skipRuntimeValidation; + execution.allowGlobalValidation = executionFunction.allowGlobalValidation; + } + // The account MUST add all execution hooks as specified in the manifest. + ManifestExecutionHook[] memory executionHooks = manifest.executionHooks; + for (uint256 i = 0; i < executionHooks.length; i++) { + ManifestExecutionHook memory executionHook = executionHooks[i]; + bytes4 executionSelector = executionHook.executionSelector; + // module + uint32 entityId = executionHook.entityId; + bool isPreHook = executionHook.isPreHook; + bool isPostHook = executionHook.isPostHook; + Execution storage execution = _executions[executionSelector]; + require( + execution.executionHooks.add(bytes32(abi.encodePacked(module, entityId, isPreHook, isPostHook))), + ERC6900AlreadySetExecutionHookForExecution() + ); + } + // The account SHOULD add all supported interfaces as specified in the manifest. + bytes4[] memory interfaceIds = manifest.interfaceIds; + for (uint256 i = 0; i < interfaceIds.length; i++) { + _interfaceIds[interfaceIds[i]]++; + } + // The account SHOULD call onInstall on the execution module to initialize state if specified by the user. + IERC6900Module(module).onInstall(installData); + // The account MUST emit ExecutionInstalled as defined in the interface for all installed executions. + emit ExecutionInstalled(module, manifest); + } + + /** + * @dev Uninstalls a module. + * + * + * Requirements: + * + * * TODO + */ + function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual { + // TODO + // IERC6900Module(module).onUninstall(deInitData); + // emit ModuleUninstalled(moduleTypeId, module); + } + + /** + * @dev Fallback function that delegates the call to the installed execution module for the given selector. + * + */ + function _fallback() internal virtual returns (bytes memory) { + address module = _fallbackExecutionModule(msg.sig); + require(module != address(0), ERC6900MissingFallbackExecutionModule(module)); + (bool success, bytes memory returndata) = module.call{value: msg.value}(msg.data); + return Address.verifyCallResult(success, returndata); + } + + /// @dev Returns the fallback execution module for the given selector. Returns `address(0)` if not installed. + function _fallbackExecutionModule(bytes4 selector) internal view virtual returns (address) { + return _executions[selector].module; + } + + function _decodeValidationConfig( + ValidationConfig validationConfig + ) internal pure virtual returns (address module, uint32 entityId, bytes1 validationFlags) { + return ( + address(Packing.extract_32_20(ValidationConfig.unwrap(validationConfig), 0)), + uint32(Packing.extract_32_4(ValidationConfig.unwrap(validationConfig), 20)), + Packing.extract_32_1(ValidationConfig.unwrap(validationConfig), 21) + ); + } + + /** + * @dev Extracts the validator from the user operation. + * + */ + function _extractUserOpValidator(PackedUserOperation calldata userOp) internal pure virtual returns (ModuleEntity) { + return ModuleEntity.wrap(bytes24(userOp.signature[:24])); + } + + /** + * @dev Extracts the validator from the signature. + * + * To construct a signature, set the first 20 bytes as the module address, the next 4 bytes + * as the entityId and the remaining bytes as the signature data: + * + * ``` + * | | + * ``` + */ + function _extractSignatureValidator( + bytes calldata signature + ) internal pure virtual returns (ModuleEntity moduleEntity, bytes calldata innerSignature) { + return (ModuleEntity.wrap(bytes24(signature[0:24])), signature[24:]); + } + + // TODO: Remove flat function and update test + function installExecutionFlat( + address module, + bytes4 executionSelector, + // TODO: Add exec flags + bytes4 executionHookSelector, + uint32 executionHookEntityId, + // TODO: Add exec hook flags + bytes4[] memory manifestInterfaceIds, + bytes calldata installData + ) public virtual onlyEntryPointOrSelf { + ExecutionManifest memory manifest = ExecutionManifest({ + executionFunctions: new ManifestExecutionFunction[](1), + executionHooks: new ManifestExecutionHook[](1), + interfaceIds: manifestInterfaceIds + }); + manifest.executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: executionSelector, + skipRuntimeValidation: true, + allowGlobalValidation: true + }); + manifest.executionHooks[0] = ManifestExecutionHook({ + executionSelector: executionHookSelector, + entityId: executionHookEntityId, + isPreHook: true, + isPostHook: true + }); + + installExecution(module, manifest, installData); + } +} diff --git a/contracts/account/utils/ERC6900Utils.sol b/contracts/account/utils/ERC6900Utils.sol new file mode 100644 index 00000000..47795fa0 --- /dev/null +++ b/contracts/account/utils/ERC6900Utils.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {ValidationConfig, ModuleEntity, ValidationFlags, HookConfig, ExecutionManifest, ManifestExecutionHook, ManifestExecutionFunction, Call} from "../../interfaces/IERC6900.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; +import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @dev Library with common ERC-6900 utility functions. + * + * See https://eips.ethereum.org/EIPS/eip-6900[ERC-6900]. + */ +// slither-disable-next-line unused-state +library ERC6900Utils { + using Packing for *; + + // ModuleEntity + + function unpack(ModuleEntity moduleEntity) internal pure returns (address, uint32) { + return (module(moduleEntity), entityId(moduleEntity)); + } + + function module(ModuleEntity moduleEntity) internal pure returns (address) { + return address(Packing.extract_32_20(ModuleEntity.unwrap(moduleEntity), 0)); + } + + function entityId(ModuleEntity moduleEntity) internal pure returns (uint32) { + return uint32(Packing.extract_32_4(ModuleEntity.unwrap(moduleEntity), 20)); + } + + // ValidationFlags + + function isGlobalValidation(ValidationFlags validationFlags) internal pure returns (bool) { + return ValidationFlags.unwrap(validationFlags) & uint8(0x04) == 0x04; // 0b00000100 + } + + function isSignatureValidation(ValidationFlags validationFlags) internal pure returns (bool) { + return ValidationFlags.unwrap(validationFlags) & uint8(0x02) == 0x02; // 0b00000010 + } + + function isUserOpValidation(ValidationFlags validationFlags) internal pure returns (bool) { + return ValidationFlags.unwrap(validationFlags) & uint8(0x01) == 0x01; // 0b00000001 + } + + // ValidationConfig + + // function module(ValidationConfig validationConfig) internal pure returns (address) { + // return address(Packing.extract_32_20(ValidationConfig.unwrap(validationConfig), 0)); + // } + + // function module(bytes calldata hook) internal pure returns (address) { + // return address(Packing.extract_32_20(bytes32(hook), 0)); + // } + + // function entityId(ValidationConfig validationConfig) internal pure returns (uint32) { + // return uint32(Packing.extract_32_4(ValidationConfig.unwrap(validationConfig), 20)); + // } + + function unpack(ValidationConfig validationConfig) internal pure returns (ModuleEntity, ValidationFlags) { + return ( + ModuleEntity.wrap(Packing.extract_32_24(ValidationConfig.unwrap(validationConfig), 0)), + ValidationFlags.wrap(uint8(Packing.extract_32_1(ValidationConfig.unwrap(validationConfig), 21))) + ); + } + + // function moduleEntity(ValidationConfig validationConfig) internal pure returns (ModuleEntity) { + // return ModuleEntity.wrap(Packing.extract_32_24(ValidationConfig.unwrap(validationConfig), 0)); + // } + + // function getValidationFlags(ValidationConfig validationConfig) internal pure returns (ValidationFlags) { + // return ValidationFlags.wrap(uint8(Packing.extract_32_1(ValidationConfig.unwrap(validationConfig), 21))); + // } + + // HookConfig + + function extractHookConfig(bytes calldata hook) internal pure returns (HookConfig) { + return HookConfig.wrap(bytes25(hook[:25])); + } + + function isValidationHook(HookConfig hookConfig) internal pure returns (bool) { + return uint8(Packing.extract_32_1(HookConfig.unwrap(hookConfig), 24) & bytes1(0x01)) == 1; + } + + function module(HookConfig hookConfig) internal pure returns (address) { + return address(Packing.extract_32_20(HookConfig.unwrap(hookConfig), 0)); + } + + function entity(HookConfig hookConfig) internal pure returns (uint32) { + return uint32(Packing.extract_32_4(HookConfig.unwrap(hookConfig), 20)); + } + + // function pack( + // address module, + // uint32 entityId, + // bool isPreHook, + // bool isPostHook + // ) internal pure returns (bytes calldata) { + // return hook[:25]; + // } +} diff --git a/contracts/interfaces/IERC6900.sol b/contracts/interfaces/IERC6900.sol new file mode 100644 index 00000000..f04368d3 --- /dev/null +++ b/contracts/interfaces/IERC6900.sol @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol"; + +// TODO: Add & update comments + +/// @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 +/// ValidationFlags: 1 byte +type ValidationConfig is bytes25; + +// ValidationFlags layout: +// 0b00000___ // unused +// 0b_____A__ // isGlobal +// 0b______B_ // isSignatureValidation +// 0b_______C // isUserOpValidation +type ValidationFlags is uint8; + +/// @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; + // The value to send with the call. + uint256 value; + // The calldata for the call. + bytes data; +} + +interface IERC6900ModularAccount { + 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. + /// @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 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); + + /// @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); + + /// @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; + + /// @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; + + /// @notice Installs a validation function across a set of execution selectors, and optionally mark it as a + /// global validation function. + /// @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; + + /// @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 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); +} + +struct ExecutionDataView { + // The module that implements this execution function. + // If this is a native function, the address must be the address of the account. + 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 { + // ValidationFlags layout: + // 0b00000___ // unused + // 0b_____A__ // isGlobal + // 0b______B_ // isSignatureValidation + // 0b_______C // isUserOpValidation + ValidationFlags validationFlags; + // 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 IERC6900ModularAccountView { + /// @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 (ValidationDataView memory); +} + +interface IERC6900Module 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 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 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); +} + +interface IERC6900ValidationModule is IERC6900Module { + /// @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 validateUserOp( + uint32 entityId, + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) external returns (uint256); + + /// @notice Run the runtime validation function specified by the `entityId`. + /// @dev To indicate the entire call should revert, the function MUST revert. + /// @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. + /// @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 Validates a signature using ERC-1271. + /// @dev To indicate the entire call should revert, the function MUST revert. + /// @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); +} + +struct Execution { + address target; + uint256 value; + bytes callData; +} + +interface IERC6900ValidationHookModule is IERC6900Module { + /// @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 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. + /// @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 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; +} + +struct ManifestExecutionFunction { + // The selector to install. + bytes4 executionSelector; + // 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 executionSelector; + uint32 entityId; + bool isPreHook; + bool isPostHook; +} + +/// @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; +} + +interface IERC6900ExecutionModule is IERC6900Module { + /// @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); +} + +interface IERC6900ExecutionHookModule is IERC6900Module { + /// @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 of validation-associated hooks, hook modules + /// should receive the full calldata. + /// @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; +} diff --git a/contracts/mocks/account/AccountERC6900Mock.sol b/contracts/mocks/account/AccountERC6900Mock.sol new file mode 100644 index 00000000..77338e54 --- /dev/null +++ b/contracts/mocks/account/AccountERC6900Mock.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {AccountERC6900} from "../../account/extensions/AccountERC6900.sol"; + +abstract contract AccountERC6900Mock is EIP712, AccountERC6900 { + bytes32 internal constant _PACKED_USER_OPERATION = + keccak256( + "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)" + ); + + constructor(address validator, bytes memory initData) { + // _installModule(MODULE_TYPE_VALIDATOR, validator, initData); + } + + function _signableUserOpHash( + PackedUserOperation calldata userOp, + bytes32 /*userOpHash*/ + ) internal view virtual override returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + _PACKED_USER_OPERATION, + userOp.sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + userOp.preVerificationGas, + userOp.gasFees, + keccak256(userOp.paymasterAndData) + ) + ) + ); + } +} diff --git a/contracts/mocks/account/modules/ERC6900ExecutionMock.sol b/contracts/mocks/account/modules/ERC6900ExecutionMock.sol new file mode 100644 index 00000000..f4d6c643 --- /dev/null +++ b/contracts/mocks/account/modules/ERC6900ExecutionMock.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IERC6900ExecutionModule, IERC6900Module, ExecutionManifest, ManifestExecutionFunction, ManifestExecutionHook} from "../../../interfaces/IERC6900.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {ERC6900ModuleMock} from "./ERC6900ModuleMock.sol"; + +abstract contract ERC6900ExecutionMock is ERC6900ModuleMock, IERC6900ExecutionModule { + mapping(address sender => address signer) private _associatedSigners; + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC6900ModuleMock) returns (bool) { + return interfaceId == type(IERC6900ExecutionModule).interfaceId || super.supportsInterface(interfaceId); + } + + function onInstall(bytes calldata data) public virtual override(IERC6900Module, ERC6900ModuleMock) { + _associatedSigners[msg.sender] = address(bytes20(data[0:20])); + super.onInstall(data); + } + + function onUninstall(bytes calldata data) public virtual override(IERC6900Module, ERC6900ModuleMock) { + delete _associatedSigners[msg.sender]; + super.onUninstall(data); + } + + /// @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) { + bytes4 executionSelector = bytes4(0x99887766); //todo change them + bytes4 executionHookSelector = bytes4(0x99887766); + uint32 executionHookEntityId = uint32(0x99887766); + bytes4[] memory interfaceIds = new bytes4[](1); + interfaceIds[0] = bytes4(0x99887766); + + ExecutionManifest memory manifest = ExecutionManifest({ + executionFunctions: new ManifestExecutionFunction[](1), + executionHooks: new ManifestExecutionHook[](1), + interfaceIds: interfaceIds + }); + manifest.executionFunctions[0] = ManifestExecutionFunction({ + executionSelector: executionSelector, + skipRuntimeValidation: true, + allowGlobalValidation: true + }); + manifest.executionHooks[0] = ManifestExecutionHook({ + executionSelector: executionHookSelector, + entityId: executionHookEntityId, + isPreHook: true, + isPostHook: true + }); + return manifest; + } +} diff --git a/contracts/mocks/account/modules/ERC6900ModuleMock.sol b/contracts/mocks/account/modules/ERC6900ModuleMock.sol new file mode 100644 index 00000000..51357bf5 --- /dev/null +++ b/contracts/mocks/account/modules/ERC6900ModuleMock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC6900Module} from "../../../interfaces/IERC6900.sol"; + +abstract contract ERC6900ModuleMock is IERC6900Module, ERC165 { + event ModuleInstalledReceived(address account, bytes data); + event ModuleUninstalledReceived(address account, bytes data); + + constructor() {} + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC6900Module).interfaceId || super.supportsInterface(interfaceId); + } + + function onInstall(bytes calldata data) public virtual { + emit ModuleInstalledReceived(msg.sender, data); + } + + function onUninstall(bytes calldata data) public virtual { + emit ModuleUninstalledReceived(msg.sender, data); + } + + function moduleId() public view virtual returns (string memory) { + // vendor.module.semver + return "@openzeppelin/community-contracts.ModuleERC6900.v0.0.0"; + } +} diff --git a/contracts/mocks/account/modules/ERC6900ValidationMock.sol b/contracts/mocks/account/modules/ERC6900ValidationMock.sol new file mode 100644 index 00000000..ca83ba69 --- /dev/null +++ b/contracts/mocks/account/modules/ERC6900ValidationMock.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IERC6900ValidationModule, IERC6900Module} from "../../../interfaces/IERC6900.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {ERC6900ModuleMock} from "./ERC6900ModuleMock.sol"; + +abstract contract ERC6900ValidationMock is ERC6900ModuleMock, IERC6900ValidationModule { + mapping(address sender => address signer) private _associatedSigners; + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC6900ModuleMock) returns (bool) { + return interfaceId == type(IERC6900ValidationModule).interfaceId || super.supportsInterface(interfaceId); + } + + function onInstall(bytes calldata data) public virtual override(IERC6900Module, ERC6900ModuleMock) { + _associatedSigners[msg.sender] = address(bytes20(data[0:20])); + super.onInstall(data); + } + + function onUninstall(bytes calldata data) public virtual override(IERC6900Module, ERC6900ModuleMock) { + delete _associatedSigners[msg.sender]; + super.onUninstall(data); + } + + // le'ts no override moduleId() + + function validateUserOp( + uint32 entityId, + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) public view virtual returns (uint256) { + entityId; // silence warning + return + SignatureChecker.isValidSignatureNow(_associatedSigners[msg.sender], userOpHash, userOp.signature) + ? ERC4337Utils.SIG_VALIDATION_SUCCESS + : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + function validateRuntime( + address account, + uint32 entityId, + address sender, + uint256 value, + bytes calldata data, + bytes calldata authorization + ) public view virtual { + // return + // SignatureChecker.isValidSignatureNow(_associatedSigners[msg.sender], userOpHash, userOp.signature) + // ? ERC4337Utils.SIG_VALIDATION_SUCCESS + // : ERC4337Utils.SIG_VALIDATION_FAILED; + } + + function validateSignature( + address account, + uint32 entityId, + address sender, + bytes32 hash, + bytes calldata signature + ) public view virtual returns (bytes4) { + account; // silence warning + entityId; // silence warning + return + SignatureChecker.isValidSignatureNow(_associatedSigners[sender], hash, signature) + ? IERC1271.isValidSignature.selector + : bytes4(0xffffffff); + } +} diff --git a/test/account/extensions/AccountERC6900.behavior.js b/test/account/extensions/AccountERC6900.behavior.js new file mode 100644 index 00000000..f1a16e09 --- /dev/null +++ b/test/account/extensions/AccountERC6900.behavior.js @@ -0,0 +1,150 @@ +const { ethers, entrypoint } = require('hardhat'); +const { expect } = require('chai'); +const { impersonate } = require('@openzeppelin/contracts/test/helpers/account'); + +function shouldBehaveLikeAccountERC6900() { + describe('AccountERC6900', function () { + beforeEach(async function () { + await this.mock.deploy(); + await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') }); + + this.modules = {}; + this.validationModule = await ethers.deployContract('$ERC6900ValidationMock'); + this.executionModule = await ethers.deployContract('$ERC6900ExecutionMock'); + this.randomContract = await ethers.deployContract('CallReceiverMock'); + + this.mockFromEntrypoint = this.mock.connect(await impersonate(entrypoint.target)); + }); + + describe('accountId', function () { + it('should return the account ID', async function () { + await expect(this.mock.accountId()).to.eventually.equal( + '@openzeppelin/community-contracts.AccountERC6900.v0.0.0', + ); + }); + }); + + describe('module installation', function () { + it('should revert if module has not ERC-6900 module interface', async function () { + const moduleId = this.randomContract.target; // not a validation module + const installData = ethers.hexlify(ethers.randomBytes(256)); + const entityId = ethers.hexlify('0x11223344'); + const validationFlags = ethers.hexlify('0x11'); + const validationConfig = ethers.concat([moduleId, entityId, validationFlags]); + const selectors = [ethers.hexlify('0x11223344')]; + const hooks = [ethers.hexlify(ethers.randomBytes(32))]; + await expect(this.mockFromEntrypoint.installValidation(validationConfig, selectors, installData, hooks)) + .to.be.revertedWithCustomError(this.mock, 'ERC6900ModuleInterfaceNotSupported') + .withArgs(moduleId, '0x46c0c1b4'); + }); + + it('should revert if selector is already set', async function () { + const moduleId = this.validationModule.target; + const installData = ethers.hexlify(ethers.randomBytes(256)); + const entityId = ethers.hexlify('0x11223344'); + const validationFlags = ethers.hexlify('0x11'); + const validationConfig = ethers.concat([moduleId, entityId, validationFlags]); + const selectors = [ethers.hexlify('0x11223344'), ethers.hexlify('0x11223344')]; // same selector twice + const hooks = [ethers.hexlify(ethers.randomBytes(32))]; + await expect( + this.mockFromEntrypoint.installValidation(validationConfig, selectors, installData, hooks), + ).to.be.revertedWithCustomError(this.mock, 'ERC6900AlreadySetSelectorForValidation'); + // .withArgs(moduleId, "0x46c0c1b4"); + }); + + it('should revert if validation hook already set', async function () { + const moduleId = this.validationModule.target; + const installData = ethers.hexlify(ethers.randomBytes(256)); + const entityId = ethers.hexlify('0x11223344'); + const validationFlags = ethers.hexlify('0x11'); + const validationConfig = ethers.concat([moduleId, entityId, validationFlags]); + const selectors = [ethers.hexlify('0x11223344')]; + const hook = ethers.concat([ethers.hexlify(ethers.randomBytes(24)), ethers.hexlify('0x01')]); + const hooks = [hook, hook]; // same validation hook twice + await expect( + this.mockFromEntrypoint.installValidation(validationConfig, selectors, installData, hooks), + ).to.be.revertedWithCustomError(this.mock, 'ERC6900AlreadySetValidationHookForValidation'); + }); + + it('should revert if execution hook already set', async function () { + const moduleId = this.validationModule.target; + const installData = ethers.hexlify(ethers.randomBytes(256)); + const entityId = ethers.hexlify('0x11223344'); + const validationFlags = ethers.hexlify('0x11'); + const validationConfig = ethers.concat([moduleId, entityId, validationFlags]); + const selectors = [ethers.hexlify('0x11223344')]; + const hook = ethers.concat([ethers.hexlify(ethers.randomBytes(24)), ethers.hexlify('0x00')]); + const hooks = [hook, hook]; // same execution hook twice + await expect( + this.mockFromEntrypoint.installValidation(validationConfig, selectors, installData, hooks), + ).to.be.revertedWithCustomError(this.mock, 'ERC6900AlreadySetExecutionHookForValidation'); + }); + + it(`should install validation`, async function () { + const moduleId = this.validationModule.target; + const installData = ethers.hexlify(ethers.randomBytes(256)); + const entityId = ethers.hexlify('0x11223344'); + const validationFlags = ethers.hexlify('0x11'); + const validationConfig = ethers.concat([moduleId, entityId, validationFlags]); + const selectors = [ethers.hexlify('0x11223344')]; + const hooks = [ethers.hexlify(ethers.randomBytes(32))]; + + await expect(this.mockFromEntrypoint.installValidation(validationConfig, selectors, installData, hooks)) + .to.emit(this.validationModule, 'ModuleInstalledReceived') + .withArgs(this.mock, installData) + .to.emit(this.mock, 'ValidationInstalled') + .withArgs(moduleId, entityId); + }); + }); + + describe('module execution', function () { + it(`should install execution`, async function () { + const moduleId = this.executionModule.target; + const installData = ethers.hexlify(ethers.randomBytes(256)); + const executionSelector = ethers.hexlify('0x11223344'); + const skipRuntimeValidation = true; + const allowGlobalValidation = true; + const executionHookSelector = ethers.hexlify('0x11223355'); + const entityId = ethers.hexlify('0x11223366'); + //const executionHookFlags = ethers.hexlify("0x02"); // isPreHook && isPostHook + const isPreHook = true; + const isPostHook = true; + const interfaceId = ethers.hexlify('0x11223377'); + /* + const executionManifest = ethers.AbiCoder.defaultAbiCoder().encode( + ['tuple(tuple(bytes4,bool,bool)[],tuple(bytes4,uint32,bool,bool)[],bytes4[])'], + [ + [ + [[executionSelector, skipRuntimeValidation, allowGlobalValidation]], + [[executionSelector, entityId, isPreHook, isPostHook]], + [interfaceId], + ], + ], + ); + */ + await expect( + this.mockFromEntrypoint.installExecutionFlat( + moduleId, + executionSelector, + executionHookSelector, + entityId, + [interfaceId], + installData, + ), + ) + .to.emit(this.executionModule, 'ModuleInstalledReceived') + .withArgs(this.mock, installData) + .to.emit(this.mock, 'ExecutionInstalled') + .withArgs(moduleId, [ + [[executionSelector, skipRuntimeValidation, allowGlobalValidation]], + [[executionHookSelector, entityId, isPreHook, isPostHook]], + [interfaceId], + ]); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeAccountERC6900, +}; diff --git a/test/account/extensions/AccountERC6900.test.js b/test/account/extensions/AccountERC6900.test.js new file mode 100644 index 00000000..513b5b4f --- /dev/null +++ b/test/account/extensions/AccountERC6900.test.js @@ -0,0 +1,59 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ERC4337Helper } = require('../../helpers/erc4337'); +const { PackedUserOperation } = require('../../helpers/eip712-types'); + +// const { shouldBehaveLikeAccountCore } = require('../Account.behavior'); +const { shouldBehaveLikeAccountERC6900 } = require('./AccountERC6900.behavior'); + +async function fixture() { + // EOAs and environment + const [other] = await ethers.getSigners(); + const target = await ethers.deployContract('CallReceiverMockExtended'); + const anotherTarget = await ethers.deployContract('CallReceiverMockExtended'); + + // ERC-6900 validator + const validatorMock = await ethers.deployContract('$ERC6900ValidationMock'); + + // ERC-4337 signer + const signer = ethers.Wallet.createRandom(); + + // ERC-4337 account + const helper = new ERC4337Helper(); + const env = await helper.wait(); + const mock = await helper.newAccount('$AccountERC6900Mock', [ + 'AccountERC6900', + '1', + validatorMock.target, + ethers.solidityPacked(['address'], [signer.address]), + ]); + + // domain cannot be fetched using getDomain(mock) before the mock is deployed + const domain = { + name: 'AccountERC6900', + version: '1', + chainId: env.chainId, + verifyingContract: mock.address, + }; + + const signUserOp = userOp => + signer + .signTypedData(domain, { PackedUserOperation }, userOp.packed) + .then(signature => Object.assign(userOp, { signature })); + + const userOp = { + // Use the first 20 bytes from the nonce key (24 bytes) to identify the validator module + nonce: ethers.zeroPadBytes(ethers.hexlify(validatorMock.target), 32), + }; + + return { ...env, validatorMock, mock, domain, signer, target, anotherTarget, other, signUserOp, userOp }; +} + +describe('AccountERC6900', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + // shouldBehaveLikeAccountCore(); + shouldBehaveLikeAccountERC6900(); +});