From a0c58324195ab1bff1f4b8d8d9c4e655c6a8ec8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:01:27 +0100 Subject: [PATCH 01/14] Init ERC-6900 account extension --- .../account/extensions/AccountERC6900.sol | 494 ++++++++++++++++++ contracts/account/utils/ERC6900Utils.sol | 388 ++++++++++++++ contracts/interfaces/IERC6900.sol | 351 +++++++++++++ .../mocks/account/AccountERC6900Mock.sol | 40 ++ .../account/modules/ERC6900ExecutionMock.sol | 60 +++ .../account/modules/ERC6900ModuleMock.sol | 31 ++ .../account/modules/ERC6900ValidationMock.sol | 74 +++ package.json | 3 +- .../extensions/AccountERC6900.behavior.js | 148 ++++++ .../account/extensions/AccountERC6900.test.js | 60 +++ 10 files changed, 1647 insertions(+), 2 deletions(-) create mode 100644 contracts/account/extensions/AccountERC6900.sol create mode 100644 contracts/account/utils/ERC6900Utils.sol create mode 100644 contracts/interfaces/IERC6900.sol create mode 100644 contracts/mocks/account/AccountERC6900Mock.sol create mode 100644 contracts/mocks/account/modules/ERC6900ExecutionMock.sol create mode 100644 contracts/mocks/account/modules/ERC6900ModuleMock.sol create mode 100644 contracts/mocks/account/modules/ERC6900ValidationMock.sol create mode 100644 test/account/extensions/AccountERC6900.behavior.js create mode 100644 test/account/extensions/AccountERC6900.test.js diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol new file mode 100644 index 00000000..f34b0114 --- /dev/null +++ b/contracts/account/extensions/AccountERC6900.sol @@ -0,0 +1,494 @@ +// 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 {ERC7739} from "../../utils/cryptography/ERC7739.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, ERC7739, IERC6900ModularAccount { + using Bytes for *; + using ERC6900Utils for *; + using ERC6900Utils for ModuleEntity; + using EnumerableSet for *; + using Packing for bytes32; + + mapping(ModuleEntity moduleEntity => Validation) private _validations; + mapping(bytes4 selector => Execution) private _executions; + mapping(bytes4 interfaceId => bool supported) private _interfaceIds; + mapping(bytes4 selector => address) private _fallbacks; + + 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); + } + + /// @dev Executes a transaction from the entry point or the account itself. See {_execute}. + function execute( + address target, + uint256 value, + bytes calldata data + ) public payable virtual onlyEntryPointOrSelf returns (bytes memory) { + return _execute(target, value, data); + } + + 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; + } + + 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:]; + 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(), + ERC6900Utils.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, + ERC6900Utils.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), + ERC6900Utils.ERC6900MissingValidationForSelector(executionSelector) + ); + } + // If the selector being checked is execute or executeBatch, + // it MUST perform additional checking on target. + if (executionSelector == IERC6900ModularAccount.execute.selector) { + (address target, , ) = abi.decode(userOp.callData[4:], (address, uint256, bytes)); + require(target != address(this), ERC6900Utils.ERC6900InvalidExecuteTarget()); + } + if (executionSelector == IERC6900ModularAccount.executeBatch.selector) { + Call[] memory calls = abi.decode(userOp.callData[4:], (Call[])); + for (uint256 i = 0; i < calls.length; i++) { + require(calls[i].target != address(this), ERC6900Utils.ERC6900InvalidExecuteTarget()); + } + } + return + IERC6900ValidationModule(module).validateUserOp(entityId, userOp, _signableUserOpHash(userOp, userOpHash)); + } + + /** + * @dev Lowest-level signature validation function. See {ERC7739-_rawSignatureValidation}. + * + * This function delegates the signature validation to a validation module if the first 20 bytes of the + * signature correspond to an installed validator module. + * + * 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(), + ERC6900Utils.ERC6900BadSignatureValidation(validationModuleEntity) + ); + + return + IERC6900ValidationModule(module).validateSignature( + address(this), + entityId, + msg.sender, + hash, + innerSignature + ) == IERC1271.isValidSignature.selector; + } + + /** + * @dev ERC-6900 execution logic. + * + * Reverts if the call type is not supported. + */ + function _execute(address target, uint256 value, bytes calldata data) internal virtual returns (bytes memory) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + + require(success, ERC6900Utils.ERC6900ExecutionFailed(target, value, data)); + return 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), + ERC6900Utils.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]), + ERC6900Utils.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])), + ERC6900Utils.ERC6900AlreadySetValidationHookForValidation() + ); + } else { + // Is execution hook + expectedInterface = type(IERC6900ExecutionModule).interfaceId; + require( + validation.executionHooks.add(bytes32(hook[:25])), + ERC6900Utils.ERC6900AlreadySetExecutionHookForValidation() + ); + } + // TODO: Firstly check interface is supported + if (hookModule.code.length > 0) { + require( + ERC165Checker.supportsInterface(hookModule, expectedInterface), + ERC6900Utils.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 + ERC6900Utils.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), + ERC6900Utils.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 + ERC6900Utils.ERC6900ExecutionSelectorConflictingWithERC4337Function(module, executionSelector) + ); + require( + IERC6900ModularAccount.execute.selector != executionSelector, // TODO Check other ERC-6900 functions + ERC6900Utils.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))), + ERC6900Utils.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++) { + bytes4 interfaceId = interfaceIds[i]; + require(!_interfaceIds[interfaceId], "Interface already set"); // TODO use custom error + _interfaceIds[interfaceId] = true; + } + // 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 handler for the given selector. + * + */ + function _fallback() internal virtual returns (bytes memory) { + // TODO + } + + /// @dev Returns the fallback handler for the given selector. Returns `address(0)` if not installed. + function _fallbackHandler(bytes4 selector) internal view virtual returns (address) { + return _fallbacks[selector]; + } + + 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:]); + } + + /** + * @dev Extract the function selector from initData/deInitData for MODULE_TYPE_FALLBACK + * + * NOTE: If we had calldata here, we would could use calldata slice which are cheaper to manipulate and don't + * require actual copy. However, this would require `_installModule` to get a calldata bytes object instead of a + * memory bytes object. This would prevent calling `_installModule` from a contract constructor and would force + * the use of external initializers. That may change in the future, as most accounts will probably be deployed as + * clones/proxy/ERC-7702 delegates and therefore rely on initializers anyway. + */ + function _decodeFallbackData( + bytes memory data + ) internal pure virtual returns (bytes4 selector, bytes memory remaining) { + return (bytes4(data), data.slice(4)); + } + + // 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..90bced8d --- /dev/null +++ b/contracts/account/utils/ERC6900Utils.sol @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (account/utils/draft-ERC7579Utils.sol) + +pragma solidity ^0.8.20; + +// import {Execution} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol"; +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"; + +type Mode is bytes32; +type CallType is bytes1; +type ExecType is bytes1; +type ModeSelector is bytes4; +type ModePayload is bytes22; + +/** + * @dev Library with common ERC-7579 utility functions. + * + * See https://eips.ethereum.org/EIPS/eip-7579[ERC-7579]. + */ +// slither-disable-next-line unused-state +library ERC6900Utils { + using Packing for *; + + /// @dev A single `call` execution. + CallType internal constant CALLTYPE_SINGLE = CallType.wrap(0x00); + + /// @dev A batch of `call` executions. + CallType internal constant CALLTYPE_BATCH = CallType.wrap(0x01); + + /// @dev A `delegatecall` execution. + CallType internal constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); + + /// @dev Default execution type that reverts on failure. + ExecType internal constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); + + /// @dev Execution type that does not revert on failure. + ExecType internal constant EXECTYPE_TRY = ExecType.wrap(0x01); + + /** + * @dev Emits when an {EXECTYPE_TRY} execution fails. + * @param batchExecutionIndex The index of the failed call in the execution batch. + * @param returndata The returned data from the failed call. + */ + event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes returndata); + + 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 ERC6900ExecutionFailed(address target, uint256 value, bytes data); + + /// @dev The provided {CallType} is not supported. + error ERC7579UnsupportedCallType(CallType callType); + + /// @dev The provided {ExecType} is not supported. + error ERC7579UnsupportedExecType(ExecType execType); + + /// @dev The provided module doesn't match the provided module type. + error ERC7579MismatchedModuleTypeId(uint256 moduleTypeId, address module); + + /// @dev The module is not installed. + error ERC7579UninstalledModule(uint256 moduleTypeId, address module); + + /// @dev The module is already installed. + error ERC7579AlreadyInstalledModule(uint256 moduleTypeId, address module); + + /// @dev The module type is not supported. + error ERC7579UnsupportedModuleType(uint256 moduleTypeId); + + /// @dev Input calldata not properly formatted and possibly malicious. + error ERC7579DecodingError(); + + // 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 getHookData(bytes calldata hook) internal pure returns (bytes calldata) { + return hook[:25]; + } + + // function pack( + // address module, + // uint32 entityId, + // bool isPreHook, + // bool isPostHook + // ) internal pure returns (bytes calldata) { + // return hook[:25]; + // } + + /* + /// @dev Executes a single call. + function execSingle( + bytes calldata executionCalldata, + ExecType execType + ) internal returns (bytes[] memory returnData) { + (address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata); + returnData = new bytes[](1); + returnData[0] = _call(0, execType, target, value, callData); + } + + /// @dev Executes a batch of calls. + function execBatch( + bytes calldata executionCalldata, + ExecType execType + ) internal returns (bytes[] memory returnData) { + Execution[] calldata executionBatch = decodeBatch(executionCalldata); + returnData = new bytes[](executionBatch.length); + for (uint256 i = 0; i < executionBatch.length; ++i) { + returnData[i] = _call( + i, + execType, + executionBatch[i].target, + executionBatch[i].value, + executionBatch[i].callData + ); + } + } + + /// @dev Executes a delegate call. + function execDelegateCall( + bytes calldata executionCalldata, + ExecType execType + ) internal returns (bytes[] memory returnData) { + (address target, bytes calldata callData) = decodeDelegate(executionCalldata); + returnData = new bytes[](1); + returnData[0] = _delegatecall(0, execType, target, callData); + } + + /// @dev Encodes the mode with the provided parameters. See {decodeMode}. + function encodeMode( + CallType callType, + ExecType execType, + ModeSelector selector, + ModePayload payload + ) internal pure returns (Mode mode) { + return + Mode.wrap( + CallType + .unwrap(callType) + .pack_1_1(ExecType.unwrap(execType)) + .pack_2_4(bytes4(0)) + .pack_6_4(ModeSelector.unwrap(selector)) + .pack_10_22(ModePayload.unwrap(payload)) + ); + } + + /// @dev Decodes the mode into its parameters. See {encodeMode}. + function decodeMode( + Mode mode + ) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) { + return ( + CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)), + ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)), + ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)), + ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10)) + ); + } + + /// @dev Encodes a single call execution. See {decodeSingle}. + function encodeSingle( + address target, + uint256 value, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, value, callData); + } + + /// @dev Decodes a single call execution. See {encodeSingle}. + function decodeSingle( + bytes calldata executionCalldata + ) internal pure returns (address target, uint256 value, bytes calldata callData) { + target = address(bytes20(executionCalldata[0:20])); + value = uint256(bytes32(executionCalldata[20:52])); + callData = executionCalldata[52:]; + } + + /// @dev Encodes a delegate call execution. See {decodeDelegate}. + function encodeDelegate( + address target, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, callData); + } + + /// @dev Decodes a delegate call execution. See {encodeDelegate}. + function decodeDelegate( + bytes calldata executionCalldata + ) internal pure returns (address target, bytes calldata callData) { + target = address(bytes20(executionCalldata[0:20])); + callData = executionCalldata[20:]; + } + + /// @dev Encodes a batch of executions. See {decodeBatch}. + function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) { + return abi.encode(executionBatch); + } + + /// @dev Decodes a batch of executions. See {encodeBatch}. + /// + /// NOTE: This function runs some checks and will throw a {ERC7579DecodingError} if the input is not properly formatted. + function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { + unchecked { + uint256 bufferLength = executionCalldata.length; + + // Check executionCalldata is not empty. + if (bufferLength < 32) revert ERC7579DecodingError(); + + // Get the offset of the array (pointer to the array length). + uint256 arrayLengthOffset = uint256(bytes32(executionCalldata[0:32])); + + // The array length (at arrayLengthOffset) should be 32 bytes long. We check that this is within the + // buffer bounds. Since we know bufferLength is at least 32, we can subtract with no overflow risk. + if (arrayLengthOffset > bufferLength - 32) revert ERC7579DecodingError(); + + // Get the array length. arrayLengthOffset + 32 is bounded by bufferLength so it does not overflow. + uint256 arrayLength = uint256(bytes32(executionCalldata[arrayLengthOffset:arrayLengthOffset + 32])); + + // Check that the buffer is long enough to store the array elements as "offset pointer": + // - each element of the array is an "offset pointer" to the data. + // - each "offset pointer" (to an array element) takes 32 bytes. + // - validity of the calldata at that location is checked when the array element is accessed, so we only + // need to check that the buffer is large enough to hold the pointers. + // + // Since we know bufferLength is at least arrayLengthOffset + 32, we can subtract with no overflow risk. + // Solidity limits length of such arrays to 2**64-1, this guarantees `arrayLength * 32` does not overflow. + if (arrayLength > type(uint64).max || bufferLength - arrayLengthOffset - 32 < arrayLength * 32) + revert ERC7579DecodingError(); + + assembly ("memory-safe") { + executionBatch.offset := add(add(executionCalldata.offset, arrayLengthOffset), 32) + executionBatch.length := arrayLength + } + } + } + + /// @dev Executes a `call` to the target with the provided {ExecType}. + function _call( + uint256 index, + ExecType execType, + address target, + uint256 value, + bytes calldata data + ) private returns (bytes memory) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + return _validateExecutionMode(index, execType, success, returndata); + } + + /// @dev Executes a `delegatecall` to the target with the provided {ExecType}. + function _delegatecall( + uint256 index, + ExecType execType, + address target, + bytes calldata data + ) private returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return _validateExecutionMode(index, execType, success, returndata); + } + + /// @dev Validates the execution mode and returns the returndata. + function _validateExecutionMode( + uint256 index, + ExecType execType, + bool success, + bytes memory returndata + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { + Address.verifyCallResult(success, returndata); + } else if (execType == ERC7579Utils.EXECTYPE_TRY) { + if (!success) emit ERC7579TryExecuteFail(index, returndata); + } else { + revert ERC7579UnsupportedExecType(execType); + } + return returndata; + } + */ +} + +// Operators +using {eqCallType as ==} for CallType global; +using {eqExecType as ==} for ExecType global; +using {eqModeSelector as ==} for ModeSelector global; +using {eqModePayload as ==} for ModePayload global; + +/// @dev Compares two `CallType` values for equality. +function eqCallType(CallType a, CallType b) pure returns (bool) { + return CallType.unwrap(a) == CallType.unwrap(b); +} + +/// @dev Compares two `ExecType` values for equality. +function eqExecType(ExecType a, ExecType b) pure returns (bool) { + return ExecType.unwrap(a) == ExecType.unwrap(b); +} + +/// @dev Compares two `ModeSelector` values for equality. +function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) { + return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); +} + +/// @dev Compares two `ModePayload` values for equality. +function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) { + return ModePayload.unwrap(a) == ModePayload.unwrap(b); +} diff --git a/contracts/interfaces/IERC6900.sol b/contracts/interfaces/IERC6900.sol new file mode 100644 index 00000000..e1866b52 --- /dev/null +++ b/contracts/interfaces/IERC6900.sol @@ -0,0 +1,351 @@ +// 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/package.json b/package.json index a0a04a8b..90231584 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,7 @@ }, "lint-staged": { "*.{js,ts}": [ - "prettier --log-level warn --ignore-path .gitignore --check", - "eslint" + "prettier --log-level warn --ignore-path .gitignore --check" ], "*.sol": [ "prettier --log-level warn --ignore-path .gitignore --check", diff --git a/test/account/extensions/AccountERC6900.behavior.js b/test/account/extensions/AccountERC6900.behavior.js new file mode 100644 index 00000000..c632dec3 --- /dev/null +++ b/test/account/extensions/AccountERC6900.behavior.js @@ -0,0 +1,148 @@ +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..14ca93cf --- /dev/null +++ b/test/account/extensions/AccountERC6900.test.js @@ -0,0 +1,60 @@ +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'); +// const { shouldBehaveLikeERC7739 } = require('../../utils/cryptography/ERC7739.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(); +}); From 0025f7cf61b3f8eea4ebace8bdc48a336378742e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:07:27 +0100 Subject: [PATCH 02/14] Remove unused errors --- contracts/account/utils/ERC6900Utils.sol | 268 +---------------------- 1 file changed, 2 insertions(+), 266 deletions(-) diff --git a/contracts/account/utils/ERC6900Utils.sol b/contracts/account/utils/ERC6900Utils.sol index 90bced8d..af3b38a5 100644 --- a/contracts/account/utils/ERC6900Utils.sol +++ b/contracts/account/utils/ERC6900Utils.sol @@ -1,51 +1,21 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.2.0) (account/utils/draft-ERC7579Utils.sol) pragma solidity ^0.8.20; -// import {Execution} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol"; 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"; -type Mode is bytes32; -type CallType is bytes1; -type ExecType is bytes1; -type ModeSelector is bytes4; -type ModePayload is bytes22; - /** - * @dev Library with common ERC-7579 utility functions. + * @dev Library with common ERC-6900 utility functions. * - * See https://eips.ethereum.org/EIPS/eip-7579[ERC-7579]. + * See https://eips.ethereum.org/EIPS/eip-6900[ERC-6900]. */ // slither-disable-next-line unused-state library ERC6900Utils { using Packing for *; - /// @dev A single `call` execution. - CallType internal constant CALLTYPE_SINGLE = CallType.wrap(0x00); - - /// @dev A batch of `call` executions. - CallType internal constant CALLTYPE_BATCH = CallType.wrap(0x01); - - /// @dev A `delegatecall` execution. - CallType internal constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); - - /// @dev Default execution type that reverts on failure. - ExecType internal constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); - - /// @dev Execution type that does not revert on failure. - ExecType internal constant EXECTYPE_TRY = ExecType.wrap(0x01); - - /** - * @dev Emits when an {EXECTYPE_TRY} execution fails. - * @param batchExecutionIndex The index of the failed call in the execution batch. - * @param returndata The returned data from the failed call. - */ - event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes returndata); - error ERC6900ModuleInterfaceNotSupported(address module, bytes4 expectedInterface); error ERC6900AlreadySetSelectorForValidation(ModuleEntity validationFunction, bytes4 selector); error ERC6900AlreadySetValidationHookForValidation(); @@ -64,27 +34,6 @@ library ERC6900Utils { error ERC6900InvalidExecuteTarget(); error ERC6900ExecutionFailed(address target, uint256 value, bytes data); - /// @dev The provided {CallType} is not supported. - error ERC7579UnsupportedCallType(CallType callType); - - /// @dev The provided {ExecType} is not supported. - error ERC7579UnsupportedExecType(ExecType execType); - - /// @dev The provided module doesn't match the provided module type. - error ERC7579MismatchedModuleTypeId(uint256 moduleTypeId, address module); - - /// @dev The module is not installed. - error ERC7579UninstalledModule(uint256 moduleTypeId, address module); - - /// @dev The module is already installed. - error ERC7579AlreadyInstalledModule(uint256 moduleTypeId, address module); - - /// @dev The module type is not supported. - error ERC7579UnsupportedModuleType(uint256 moduleTypeId); - - /// @dev Input calldata not properly formatted and possibly malicious. - error ERC7579DecodingError(); - // ModuleEntity function unpack(ModuleEntity moduleEntity) internal pure returns (address, uint32) { @@ -172,217 +121,4 @@ library ERC6900Utils { // ) internal pure returns (bytes calldata) { // return hook[:25]; // } - - /* - /// @dev Executes a single call. - function execSingle( - bytes calldata executionCalldata, - ExecType execType - ) internal returns (bytes[] memory returnData) { - (address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata); - returnData = new bytes[](1); - returnData[0] = _call(0, execType, target, value, callData); - } - - /// @dev Executes a batch of calls. - function execBatch( - bytes calldata executionCalldata, - ExecType execType - ) internal returns (bytes[] memory returnData) { - Execution[] calldata executionBatch = decodeBatch(executionCalldata); - returnData = new bytes[](executionBatch.length); - for (uint256 i = 0; i < executionBatch.length; ++i) { - returnData[i] = _call( - i, - execType, - executionBatch[i].target, - executionBatch[i].value, - executionBatch[i].callData - ); - } - } - - /// @dev Executes a delegate call. - function execDelegateCall( - bytes calldata executionCalldata, - ExecType execType - ) internal returns (bytes[] memory returnData) { - (address target, bytes calldata callData) = decodeDelegate(executionCalldata); - returnData = new bytes[](1); - returnData[0] = _delegatecall(0, execType, target, callData); - } - - /// @dev Encodes the mode with the provided parameters. See {decodeMode}. - function encodeMode( - CallType callType, - ExecType execType, - ModeSelector selector, - ModePayload payload - ) internal pure returns (Mode mode) { - return - Mode.wrap( - CallType - .unwrap(callType) - .pack_1_1(ExecType.unwrap(execType)) - .pack_2_4(bytes4(0)) - .pack_6_4(ModeSelector.unwrap(selector)) - .pack_10_22(ModePayload.unwrap(payload)) - ); - } - - /// @dev Decodes the mode into its parameters. See {encodeMode}. - function decodeMode( - Mode mode - ) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) { - return ( - CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)), - ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)), - ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)), - ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10)) - ); - } - - /// @dev Encodes a single call execution. See {decodeSingle}. - function encodeSingle( - address target, - uint256 value, - bytes calldata callData - ) internal pure returns (bytes memory executionCalldata) { - return abi.encodePacked(target, value, callData); - } - - /// @dev Decodes a single call execution. See {encodeSingle}. - function decodeSingle( - bytes calldata executionCalldata - ) internal pure returns (address target, uint256 value, bytes calldata callData) { - target = address(bytes20(executionCalldata[0:20])); - value = uint256(bytes32(executionCalldata[20:52])); - callData = executionCalldata[52:]; - } - - /// @dev Encodes a delegate call execution. See {decodeDelegate}. - function encodeDelegate( - address target, - bytes calldata callData - ) internal pure returns (bytes memory executionCalldata) { - return abi.encodePacked(target, callData); - } - - /// @dev Decodes a delegate call execution. See {encodeDelegate}. - function decodeDelegate( - bytes calldata executionCalldata - ) internal pure returns (address target, bytes calldata callData) { - target = address(bytes20(executionCalldata[0:20])); - callData = executionCalldata[20:]; - } - - /// @dev Encodes a batch of executions. See {decodeBatch}. - function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) { - return abi.encode(executionBatch); - } - - /// @dev Decodes a batch of executions. See {encodeBatch}. - /// - /// NOTE: This function runs some checks and will throw a {ERC7579DecodingError} if the input is not properly formatted. - function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { - unchecked { - uint256 bufferLength = executionCalldata.length; - - // Check executionCalldata is not empty. - if (bufferLength < 32) revert ERC7579DecodingError(); - - // Get the offset of the array (pointer to the array length). - uint256 arrayLengthOffset = uint256(bytes32(executionCalldata[0:32])); - - // The array length (at arrayLengthOffset) should be 32 bytes long. We check that this is within the - // buffer bounds. Since we know bufferLength is at least 32, we can subtract with no overflow risk. - if (arrayLengthOffset > bufferLength - 32) revert ERC7579DecodingError(); - - // Get the array length. arrayLengthOffset + 32 is bounded by bufferLength so it does not overflow. - uint256 arrayLength = uint256(bytes32(executionCalldata[arrayLengthOffset:arrayLengthOffset + 32])); - - // Check that the buffer is long enough to store the array elements as "offset pointer": - // - each element of the array is an "offset pointer" to the data. - // - each "offset pointer" (to an array element) takes 32 bytes. - // - validity of the calldata at that location is checked when the array element is accessed, so we only - // need to check that the buffer is large enough to hold the pointers. - // - // Since we know bufferLength is at least arrayLengthOffset + 32, we can subtract with no overflow risk. - // Solidity limits length of such arrays to 2**64-1, this guarantees `arrayLength * 32` does not overflow. - if (arrayLength > type(uint64).max || bufferLength - arrayLengthOffset - 32 < arrayLength * 32) - revert ERC7579DecodingError(); - - assembly ("memory-safe") { - executionBatch.offset := add(add(executionCalldata.offset, arrayLengthOffset), 32) - executionBatch.length := arrayLength - } - } - } - - /// @dev Executes a `call` to the target with the provided {ExecType}. - function _call( - uint256 index, - ExecType execType, - address target, - uint256 value, - bytes calldata data - ) private returns (bytes memory) { - (bool success, bytes memory returndata) = target.call{value: value}(data); - return _validateExecutionMode(index, execType, success, returndata); - } - - /// @dev Executes a `delegatecall` to the target with the provided {ExecType}. - function _delegatecall( - uint256 index, - ExecType execType, - address target, - bytes calldata data - ) private returns (bytes memory) { - (bool success, bytes memory returndata) = target.delegatecall(data); - return _validateExecutionMode(index, execType, success, returndata); - } - - /// @dev Validates the execution mode and returns the returndata. - function _validateExecutionMode( - uint256 index, - ExecType execType, - bool success, - bytes memory returndata - ) private returns (bytes memory) { - if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { - Address.verifyCallResult(success, returndata); - } else if (execType == ERC7579Utils.EXECTYPE_TRY) { - if (!success) emit ERC7579TryExecuteFail(index, returndata); - } else { - revert ERC7579UnsupportedExecType(execType); - } - return returndata; - } - */ -} - -// Operators -using {eqCallType as ==} for CallType global; -using {eqExecType as ==} for ExecType global; -using {eqModeSelector as ==} for ModeSelector global; -using {eqModePayload as ==} for ModePayload global; - -/// @dev Compares two `CallType` values for equality. -function eqCallType(CallType a, CallType b) pure returns (bool) { - return CallType.unwrap(a) == CallType.unwrap(b); -} - -/// @dev Compares two `ExecType` values for equality. -function eqExecType(ExecType a, ExecType b) pure returns (bool) { - return ExecType.unwrap(a) == ExecType.unwrap(b); -} - -/// @dev Compares two `ModeSelector` values for equality. -function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) { - return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); -} - -/// @dev Compares two `ModePayload` values for equality. -function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) { - return ModePayload.unwrap(a) == ModePayload.unwrap(b); } From 5f5e785d9c043e13b091cceb44a72a16e56ef05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:16:50 +0100 Subject: [PATCH 03/14] Bump version pragma of ERC6900Utils for consistency --- contracts/account/utils/ERC6900Utils.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/account/utils/ERC6900Utils.sol b/contracts/account/utils/ERC6900Utils.sol index af3b38a5..8ca55dfa 100644 --- a/contracts/account/utils/ERC6900Utils.sol +++ b/contracts/account/utils/ERC6900Utils.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +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"; From a4a5d2341f44e28484c75a278183b8dec4577483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:36:27 +0100 Subject: [PATCH 04/14] Lint AccountERC6900 test files --- package.json | 3 ++- test/account/extensions/AccountERC6900.behavior.js | 2 ++ test/account/extensions/AccountERC6900.test.js | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 90231584..a0a04a8b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ }, "lint-staged": { "*.{js,ts}": [ - "prettier --log-level warn --ignore-path .gitignore --check" + "prettier --log-level warn --ignore-path .gitignore --check", + "eslint" ], "*.sol": [ "prettier --log-level warn --ignore-path .gitignore --check", diff --git a/test/account/extensions/AccountERC6900.behavior.js b/test/account/extensions/AccountERC6900.behavior.js index c632dec3..f1a16e09 100644 --- a/test/account/extensions/AccountERC6900.behavior.js +++ b/test/account/extensions/AccountERC6900.behavior.js @@ -110,6 +110,7 @@ function shouldBehaveLikeAccountERC6900() { 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[])'], [ @@ -120,6 +121,7 @@ function shouldBehaveLikeAccountERC6900() { ], ], ); + */ await expect( this.mockFromEntrypoint.installExecutionFlat( moduleId, diff --git a/test/account/extensions/AccountERC6900.test.js b/test/account/extensions/AccountERC6900.test.js index 14ca93cf..513b5b4f 100644 --- a/test/account/extensions/AccountERC6900.test.js +++ b/test/account/extensions/AccountERC6900.test.js @@ -3,9 +3,8 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ERC4337Helper } = require('../../helpers/erc4337'); const { PackedUserOperation } = require('../../helpers/eip712-types'); -const { shouldBehaveLikeAccountCore } = require('../Account.behavior'); +// const { shouldBehaveLikeAccountCore } = require('../Account.behavior'); const { shouldBehaveLikeAccountERC6900 } = require('./AccountERC6900.behavior'); -// const { shouldBehaveLikeERC7739 } = require('../../utils/cryptography/ERC7739.behavior'); async function fixture() { // EOAs and environment From 65c96c0901e13193e0f2f6d4d1673b6ecef0c43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20=28James=29=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Tue, 11 Feb 2025 21:56:06 +0100 Subject: [PATCH 05/14] Add missings inheritdoc Co-authored-by: Hadrien Croubois --- contracts/account/extensions/AccountERC6900.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index f34b0114..b9f306e3 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -45,7 +45,6 @@ import {AccountCore} from "../AccountCore.sol"; abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount { using Bytes for *; using ERC6900Utils for *; - using ERC6900Utils for ModuleEntity; using EnumerableSet for *; using Packing for bytes32; @@ -116,7 +115,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount // _uninstallModule(moduleTypeId, module, deInitData); } - /// @dev Executes a transaction from the entry point or the account itself. See {_execute}. + /// @inheritdoc IERC6900ModularAccount function execute( address target, uint256 value, @@ -125,6 +124,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount 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++) { @@ -133,6 +133,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount return returnedData; } + /// @inheritdoc IERC6900ModularAccount function executeWithRuntimeValidation( bytes calldata data, bytes calldata authorization From 078c232c8f5437fc0d864810ffad72a391de5444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:00:56 +0100 Subject: [PATCH 06/14] Remove duplicated HookConfig extractor --- contracts/account/utils/ERC6900Utils.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/account/utils/ERC6900Utils.sol b/contracts/account/utils/ERC6900Utils.sol index 8ca55dfa..6f1cbe99 100644 --- a/contracts/account/utils/ERC6900Utils.sol +++ b/contracts/account/utils/ERC6900Utils.sol @@ -109,10 +109,6 @@ library ERC6900Utils { return uint32(Packing.extract_32_4(HookConfig.unwrap(hookConfig), 20)); } - function getHookData(bytes calldata hook) internal pure returns (bytes calldata) { - return hook[:25]; - } - // function pack( // address module, // uint32 entityId, From 2c133a665c03c068dcf5324750606075a73dc9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20=28James=29=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:33:14 +0100 Subject: [PATCH 07/14] Add missing line break Co-authored-by: Hadrien Croubois --- contracts/interfaces/IERC6900.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/interfaces/IERC6900.sol b/contracts/interfaces/IERC6900.sol index e1866b52..f04368d3 100644 --- a/contracts/interfaces/IERC6900.sol +++ b/contracts/interfaces/IERC6900.sol @@ -47,6 +47,7 @@ struct Call { // 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); From ace9b0300545008a3d312bf6cee0cba999d88d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Tue, 11 Feb 2025 23:02:00 +0100 Subject: [PATCH 08/14] Bubble revert reason when execute fails --- contracts/account/extensions/AccountERC6900.sol | 5 +---- contracts/account/utils/ERC6900Utils.sol | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index b9f306e3..3f3edfc4 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -244,13 +244,10 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount /** * @dev ERC-6900 execution logic. * - * Reverts if the call type is not supported. */ function _execute(address target, uint256 value, bytes calldata data) internal virtual returns (bytes memory) { (bool success, bytes memory returndata) = target.call{value: value}(data); - - require(success, ERC6900Utils.ERC6900ExecutionFailed(target, value, data)); - return returndata; + return Address.verifyCallResult(success, returndata); } /** diff --git a/contracts/account/utils/ERC6900Utils.sol b/contracts/account/utils/ERC6900Utils.sol index 6f1cbe99..114c1d42 100644 --- a/contracts/account/utils/ERC6900Utils.sol +++ b/contracts/account/utils/ERC6900Utils.sol @@ -32,7 +32,6 @@ library ERC6900Utils { bytes4 executionSelector ); error ERC6900InvalidExecuteTarget(); - error ERC6900ExecutionFailed(address target, uint256 value, bytes data); // ModuleEntity From dbb5521286bcd9ff10343c93be7109a6940674a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Tue, 11 Feb 2025 23:09:53 +0100 Subject: [PATCH 09/14] Move errors to AccountERC6900 --- .../account/extensions/AccountERC6900.sol | 55 +++++++++++-------- contracts/account/utils/ERC6900Utils.sol | 17 ------ 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index 3f3edfc4..e3eab4f1 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -53,6 +53,23 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount mapping(bytes4 interfaceId => bool supported) private _interfaceIds; mapping(bytes4 selector => address) private _fallbacks; + 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(); + struct Validation { EnumerableSet.Bytes32Set selectors; ValidationFlags validationFlags; @@ -168,10 +185,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount 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(), - ERC6900Utils.ERC6900BadUserOpValidation(validationModuleEntity) - ); + require(!_validationFlags.isUserOpValidation(), ERC6900BadUserOpValidation(validationModuleEntity)); bytes4 executionSelector = bytes4(userOp.callData[:4]); // validation installation MAY specify the isGlobal flag as true if (_validationFlags.isGlobalValidation()) { @@ -180,29 +194,26 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount // execution function with the allowGlobalValidation flag set to true require( !execution.allowGlobalValidation, - ERC6900Utils.ERC6900ExecutionSelectorNotAllowedForGlobalValidation( - validationModuleEntity, - executionSelector - ) + 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), - ERC6900Utils.ERC6900MissingValidationForSelector(executionSelector) + ERC6900MissingValidationForSelector(executionSelector) ); } // If the selector being checked is execute or executeBatch, // it MUST perform additional checking on target. if (executionSelector == IERC6900ModularAccount.execute.selector) { (address target, , ) = abi.decode(userOp.callData[4:], (address, uint256, bytes)); - require(target != address(this), ERC6900Utils.ERC6900InvalidExecuteTarget()); + require(target != address(this), ERC6900InvalidExecuteTarget()); } if (executionSelector == IERC6900ModularAccount.executeBatch.selector) { Call[] memory calls = abi.decode(userOp.callData[4:], (Call[])); for (uint256 i = 0; i < calls.length; i++) { - require(calls[i].target != address(this), ERC6900Utils.ERC6900InvalidExecuteTarget()); + require(calls[i].target != address(this), ERC6900InvalidExecuteTarget()); } } return @@ -228,7 +239,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount // and the flag isSignatureValidation is set to false, validation MUST revert require( !_validations[validationModuleEntity].validationFlags.isSignatureValidation(), - ERC6900Utils.ERC6900BadSignatureValidation(validationModuleEntity) + ERC6900BadSignatureValidation(validationModuleEntity) ); return @@ -271,14 +282,14 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount // Modules MUST implement ERC-165 for IModule. require( ERC165Checker.supportsInterface(module, type(IERC6900Module).interfaceId), - ERC6900Utils.ERC6900ModuleInterfaceNotSupported(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]), - ERC6900Utils.ERC6900AlreadySetSelectorForValidation(moduleEntity, selectors[i]) + ERC6900AlreadySetSelectorForValidation(moduleEntity, selectors[i]) ); } // - The account MUST install all validation hooks specified by the user and SHOULD call onInstall @@ -294,21 +305,21 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount expectedInterface = type(IERC6900ValidationModule).interfaceId; require( validation.validationHooks.add(bytes32(hook[:24])), - ERC6900Utils.ERC6900AlreadySetValidationHookForValidation() + ERC6900AlreadySetValidationHookForValidation() ); } else { // Is execution hook expectedInterface = type(IERC6900ExecutionModule).interfaceId; require( validation.executionHooks.add(bytes32(hook[:25])), - ERC6900Utils.ERC6900AlreadySetExecutionHookForValidation() + ERC6900AlreadySetExecutionHookForValidation() ); } // TODO: Firstly check interface is supported if (hookModule.code.length > 0) { require( ERC165Checker.supportsInterface(hookModule, expectedInterface), - ERC6900Utils.ERC6900ModuleInterfaceNotSupported(hookModule, expectedInterface) + ERC6900ModuleInterfaceNotSupported(hookModule, expectedInterface) ); //IERC6900Module(hookModule).onInstall(installData); //TODO Enable } @@ -329,7 +340,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount // Modules MUST implement ERC-165 for IModule. require( IERC6900Module(module).supportsInterface(type(IERC6900Module).interfaceId), //TODO Use checker - ERC6900Utils.ERC6900ModuleInterfaceNotSupported(module, type(IERC6900Module).interfaceId) + 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; @@ -340,16 +351,16 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount // An execution function selector MUST be unique in the account. require( execution.module == address(0), - ERC6900Utils.ERC6900AlreadyUsedModuleFunctionExecutionSelector(executionSelector) + 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 - ERC6900Utils.ERC6900ExecutionSelectorConflictingWithERC4337Function(module, executionSelector) + ERC6900ExecutionSelectorConflictingWithERC4337Function(module, executionSelector) ); require( IERC6900ModularAccount.execute.selector != executionSelector, // TODO Check other ERC-6900 functions - ERC6900Utils.ERC6900ExecutionSelectorConflictingWithERC6900Function(module, executionSelector) + ERC6900ExecutionSelectorConflictingWithERC6900Function(module, executionSelector) ); execution.module = module; execution.skipRuntimeValidation = executionFunction.skipRuntimeValidation; @@ -367,7 +378,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount Execution storage execution = _executions[executionSelector]; require( execution.executionHooks.add(bytes32(abi.encodePacked(module, entityId, isPreHook, isPostHook))), - ERC6900Utils.ERC6900AlreadySetExecutionHookForExecution() + ERC6900AlreadySetExecutionHookForExecution() ); } // The account SHOULD add all supported interfaces as specified in the manifest. diff --git a/contracts/account/utils/ERC6900Utils.sol b/contracts/account/utils/ERC6900Utils.sol index 114c1d42..47795fa0 100644 --- a/contracts/account/utils/ERC6900Utils.sol +++ b/contracts/account/utils/ERC6900Utils.sol @@ -16,23 +16,6 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; library ERC6900Utils { using Packing for *; - 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(); - // ModuleEntity function unpack(ModuleEntity moduleEntity) internal pure returns (address, uint32) { From 19df12ef2a73862cfc9eebf04525f02e0a617034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Wed, 12 Feb 2025 22:11:34 +0100 Subject: [PATCH 10/14] Init fallback --- .../account/extensions/AccountERC6900.sol | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index e3eab4f1..8457b4f3 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -51,7 +51,6 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount mapping(ModuleEntity moduleEntity => Validation) private _validations; mapping(bytes4 selector => Execution) private _executions; mapping(bytes4 interfaceId => bool supported) private _interfaceIds; - mapping(bytes4 selector => address) private _fallbacks; error ERC6900ModuleInterfaceNotSupported(address module, bytes4 expectedInterface); error ERC6900AlreadySetSelectorForValidation(ModuleEntity validationFunction, bytes4 selector); @@ -69,6 +68,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount bytes4 executionSelector ); error ERC6900InvalidExecuteTarget(); + error ERC6900MissingFallbackExecutionModule(address module); struct Validation { EnumerableSet.Bytes32Set selectors; @@ -409,16 +409,19 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount } /** - * @dev Fallback function that delegates the call to the installed handler for the given selector. + * @dev Fallback function that delegates the call to the installed execution module for the given selector. * */ function _fallback() internal virtual returns (bytes memory) { - // TODO + 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 handler for the given selector. Returns `address(0)` if not installed. - function _fallbackHandler(bytes4 selector) internal view virtual returns (address) { - return _fallbacks[selector]; + /// @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( @@ -455,21 +458,6 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount return (ModuleEntity.wrap(bytes24(signature[0:24])), signature[24:]); } - /** - * @dev Extract the function selector from initData/deInitData for MODULE_TYPE_FALLBACK - * - * NOTE: If we had calldata here, we would could use calldata slice which are cheaper to manipulate and don't - * require actual copy. However, this would require `_installModule` to get a calldata bytes object instead of a - * memory bytes object. This would prevent calling `_installModule` from a contract constructor and would force - * the use of external initializers. That may change in the future, as most accounts will probably be deployed as - * clones/proxy/ERC-7702 delegates and therefore rely on initializers anyway. - */ - function _decodeFallbackData( - bytes memory data - ) internal pure virtual returns (bytes4 selector, bytes memory remaining) { - return (bytes4(data), data.slice(4)); - } - // TODO: Remove flat function and update test function installExecutionFlat( address module, From 1490ca9418a7cb8eb86e3c9e871bd7013a5767e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Wed, 12 Feb 2025 22:41:01 +0100 Subject: [PATCH 11/14] Update signature validation comment --- contracts/account/extensions/AccountERC6900.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index 8457b4f3..e8d55864 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -223,8 +223,8 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount /** * @dev Lowest-level signature validation function. See {ERC7739-_rawSignatureValidation}. * - * This function delegates the signature validation to a validation module if the first 20 bytes of the - * signature correspond to an installed validator module. + * 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. */ From 6e39db13e8a61945a675f57c83676424d0cfee4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Wed, 12 Feb 2025 23:12:22 +0100 Subject: [PATCH 12/14] Check validation applicability --- .../account/extensions/AccountERC6900.sol | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index e8d55864..47414282 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -157,6 +157,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount ) 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(), @@ -204,20 +205,28 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount ERC6900MissingValidationForSelector(executionSelector) ); } - // If the selector being checked is execute or executeBatch, - // it MUST perform additional checking on target. + _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(userOp.callData[4:], (address, uint256, bytes)); + (address target, , ) = abi.decode(data, (address, uint256, bytes)); require(target != address(this), ERC6900InvalidExecuteTarget()); } if (executionSelector == IERC6900ModularAccount.executeBatch.selector) { - Call[] memory calls = abi.decode(userOp.callData[4:], (Call[])); + Call[] memory calls = abi.decode(data, (Call[])); for (uint256 i = 0; i < calls.length; i++) { require(calls[i].target != address(this), ERC6900InvalidExecuteTarget()); } } - return - IERC6900ValidationModule(module).validateUserOp(entityId, userOp, _signableUserOpHash(userOp, userOpHash)); } /** From 21949b099c4c2a5c564a2a934cd6062599d554a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Wed, 12 Feb 2025 23:26:43 +0100 Subject: [PATCH 13/14] Count supported interfaces --- contracts/account/extensions/AccountERC6900.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index 47414282..9eba8392 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -50,7 +50,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount mapping(ModuleEntity moduleEntity => Validation) private _validations; mapping(bytes4 selector => Execution) private _executions; - mapping(bytes4 interfaceId => bool supported) private _interfaceIds; + mapping(bytes4 interfaceId => uint256 supported) private _interfaceIds; error ERC6900ModuleInterfaceNotSupported(address module, bytes4 expectedInterface); error ERC6900AlreadySetSelectorForValidation(ModuleEntity validationFunction, bytes4 selector); @@ -393,9 +393,7 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount // 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++) { - bytes4 interfaceId = interfaceIds[i]; - require(!_interfaceIds[interfaceId], "Interface already set"); // TODO use custom error - _interfaceIds[interfaceId] = true; + _interfaceIds[interfaceIds[i]]++; } // The account SHOULD call onInstall on the execution module to initialize state if specified by the user. IERC6900Module(module).onInstall(installData); From a5668399f58053401b6a00b31ccb4cc0d0ab01e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20James=20Toussaint?= <33313130+jeremyjams@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:34:58 +0100 Subject: [PATCH 14/14] Expose ERC-1271 signature validation --- contracts/account/extensions/AccountERC6900.sol | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/account/extensions/AccountERC6900.sol b/contracts/account/extensions/AccountERC6900.sol index 9eba8392..9dd7a78e 100644 --- a/contracts/account/extensions/AccountERC6900.sol +++ b/contracts/account/extensions/AccountERC6900.sol @@ -12,7 +12,6 @@ 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 {ERC7739} from "../../utils/cryptography/ERC7739.sol"; import {AccountCore} from "../AccountCore.sol"; /** @@ -42,7 +41,7 @@ import {AccountCore} from "../AccountCore.sol"; * */ -abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount { +abstract contract AccountERC6900 is AccountCore, IERC1271, IERC6900ModularAccount { using Bytes for *; using ERC6900Utils for *; using EnumerableSet for *; @@ -230,10 +229,16 @@ abstract contract AccountERC6900 is AccountCore, ERC7739, IERC6900ModularAccount } /** - * @dev Lowest-level signature validation function. See {ERC7739-_rawSignatureValidation}. - * - * 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. + * 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. */