diff --git a/src/account/ReferenceModularAccount.sol b/src/account/ReferenceModularAccount.sol index 84459949..cefc1d46 100644 --- a/src/account/ReferenceModularAccount.sol +++ b/src/account/ReferenceModularAccount.sol @@ -281,7 +281,7 @@ contract ReferenceModularAccount is /// @inheritdoc IModularAccount function accountId() external pure virtual returns (string memory) { - return "erc6900/reference-modular-account/0.8.0"; + return "erc6900.reference-modular-account.0.8.0"; } /// @inheritdoc UUPSUpgradeable diff --git a/src/account/SemiModularAccount.sol b/src/account/SemiModularAccount.sol index 8bde2316..906349fa 100644 --- a/src/account/SemiModularAccount.sol +++ b/src/account/SemiModularAccount.sol @@ -93,7 +93,7 @@ contract SemiModularAccount is ReferenceModularAccount { /// @inheritdoc IModularAccount function accountId() external pure override returns (string memory) { - return "erc6900/reference-semi-modular-account/0.8.0"; + return "erc6900.reference-semi-modular-account.0.8.0"; } function replaySafeHash(bytes32 hash) public view virtual returns (bytes32) { diff --git a/src/account/UpgradeableModularAccount.sol b/src/account/UpgradeableModularAccount.sol new file mode 100644 index 00000000..f61f2ea2 --- /dev/null +++ b/src/account/UpgradeableModularAccount.sol @@ -0,0 +1,711 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {BaseAccount} from "@eth-infinitism/account-abstraction/core/BaseAccount.sol"; +import {IAccountExecute} from "@eth-infinitism/account-abstraction/interfaces/IAccountExecute.sol"; +import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol"; +import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol"; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {collectReturnData} from "../helpers/CollectReturnData.sol"; +import {DIRECT_CALL_VALIDATION_ENTITYID} from "../helpers/Constants.sol"; +import {HookConfig, HookConfigLib} from "../helpers/HookConfigLib.sol"; +import {ModuleEntityLib} from "../helpers/ModuleEntityLib.sol"; +import {SparseCalldataSegmentLib} from "../helpers/SparseCalldataSegmentLib.sol"; +import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol"; +import {_coalescePreValidation, _coalesceValidation} from "../helpers/ValidationResHelpers.sol"; +import {IExecutionHookModule} from "../interfaces/IExecutionHookModule.sol"; +import {ExecutionManifest} from "../interfaces/IExecutionModule.sol"; +import {Call, IModularAccount, ModuleEntity, ValidationConfig} from "../interfaces/IModularAccount.sol"; +import {IValidationHookModule} from "../interfaces/IValidationHookModule.sol"; +import {IValidationModule} from "../interfaces/IValidationModule.sol"; +import {AccountExecutor} from "./AccountExecutor.sol"; +import {AccountLoupe} from "./AccountLoupe.sol"; +import {AccountStorage, getAccountStorage, toHookConfig, toSetValue} from "./AccountStorage.sol"; +import {AccountStorageInitializable} from "./AccountStorageInitializable.sol"; +import {ModuleManagerInternals} from "./ModuleManagerInternals.sol"; + +contract UpgradeableModularAccount is + IModularAccount, + AccountExecutor, + AccountLoupe, + AccountStorageInitializable, + BaseAccount, + IERC165, + IERC1271, + IAccountExecute, + ModuleManagerInternals, + UUPSUpgradeable +{ + using EnumerableSet for EnumerableSet.Bytes32Set; + using ModuleEntityLib for ModuleEntity; + using ValidationConfigLib for ValidationConfig; + using HookConfigLib for HookConfig; + using SparseCalldataSegmentLib for bytes; + + struct PostExecToRun { + bytes preExecHookReturnData; + ModuleEntity postExecHook; + } + + IEntryPoint private immutable _ENTRY_POINT; + + // As per the EIP-165 spec, no interface should ever match 0xffffffff + bytes4 internal constant _INTERFACE_ID_INVALID = 0xffffffff; + bytes4 internal constant _IERC165_INTERFACE_ID = 0x01ffc9a7; + + // bytes4(keccak256("isValidSignature(bytes32,bytes)")) + bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e; + bytes4 internal constant _1271_INVALID = 0xffffffff; + + error NotEntryPoint(); + error PostExecHookReverted(address module, uint32 entityId, bytes revertReason); + error PreExecHookReverted(address module, uint32 entityId, bytes revertReason); + error PreRuntimeValidationHookFailed(address module, uint32 entityId, bytes revertReason); + error RequireUserOperationContext(); + error RuntimeValidationFunctionReverted(address module, uint32 entityId, bytes revertReason); + error SelfCallRecursionDepthExceeded(); + error SignatureValidationInvalid(address module, uint32 entityId); + error UnexpectedAggregator(address module, uint32 entityId, address aggregator); + error UnrecognizedFunction(bytes4 selector); + error ValidationFunctionMissing(bytes4 selector); + + // Wraps execution of a native function with runtime validation and hooks + // Used for upgradeTo, upgradeToAndCall, execute, executeBatch, installExecution, uninstallExecution + modifier wrapNativeFunction() { + (PostExecToRun[] memory postPermissionHooks, PostExecToRun[] memory postExecHooks) = + _checkPermittedCallerAndAssociatedHooks(); + + _; + + _doCachedPostExecHooks(postExecHooks); + _doCachedPostExecHooks(postPermissionHooks); + } + + constructor(IEntryPoint anEntryPoint) { + _ENTRY_POINT = anEntryPoint; + _disableInitializers(); + } + + // EXTERNAL FUNCTIONS + + receive() external payable {} + + /// @notice Fallback function + /// @dev We route calls to execution functions based on incoming msg.sig + /// @dev If there's no module associated with this function selector, revert + fallback(bytes calldata) external payable returns (bytes memory) { + address execModule = getAccountStorage().executionData[msg.sig].module; + if (execModule == address(0)) { + revert UnrecognizedFunction(msg.sig); + } + (PostExecToRun[] memory postPermissionHooks, PostExecToRun[] memory postExecHooks) = + _checkPermittedCallerAndAssociatedHooks(); + + // execute the function, bubbling up any reverts + (bool execSuccess, bytes memory execReturnData) = execModule.call(msg.data); + + if (!execSuccess) { + // Bubble up revert reasons from modules + assembly ("memory-safe") { + revert(add(execReturnData, 32), mload(execReturnData)) + } + } + + _doCachedPostExecHooks(postExecHooks); + _doCachedPostExecHooks(postPermissionHooks); + + return execReturnData; + } + + /// @inheritdoc IAccountExecute + /// @notice Execution function that allows UO context to be passed to execution hooks + /// @dev This function is only callable by the EntryPoint + function executeUserOp(PackedUserOperation calldata userOp, bytes32) external override { + if (msg.sender != address(_ENTRY_POINT)) { + revert NotEntryPoint(); + } + + ModuleEntity userOpValidationFunction = ModuleEntity.wrap(bytes24(userOp.signature[:24])); + + PostExecToRun[] memory postPermissionHooks = + _doPreHooks(getAccountStorage().validationData[userOpValidationFunction].permissionHooks, msg.data); + + (bool success, bytes memory result) = address(this).call(userOp.callData[4:]); + + if (!success) { + // Directly bubble up revert messages + assembly ("memory-safe") { + revert(add(result, 32), mload(result)) + } + } + + _doCachedPostExecHooks(postPermissionHooks); + } + + /// @inheritdoc IModularAccount + /// @notice May be validated by a global validation. + function execute(address target, uint256 value, bytes calldata data) + external + payable + override + wrapNativeFunction + returns (bytes memory result) + { + result = _exec(target, value, data); + } + + /// @inheritdoc IModularAccount + /// @notice May be validated by a global validation function. + function executeBatch(Call[] calldata calls) + external + payable + override + wrapNativeFunction + returns (bytes[] memory results) + { + uint256 callsLength = calls.length; + results = new bytes[](callsLength); + + for (uint256 i = 0; i < callsLength; ++i) { + results[i] = _exec(calls[i].target, calls[i].value, calls[i].data); + } + } + + /// @inheritdoc IModularAccount + function executeWithAuthorization(bytes calldata data, bytes calldata authorization) + external + payable + returns (bytes memory) + { + // Revert if the provided `authorization` less than 21 bytes long, rather than right-padding. + ModuleEntity runtimeValidationFunction = ModuleEntity.wrap(bytes24(authorization[:24])); + + // Check if the runtime validation function is allowed to be called + bool isGlobalValidation = uint8(authorization[24]) == 1; + _checkIfValidationAppliesCallData(data, runtimeValidationFunction, isGlobalValidation); + + _doRuntimeValidation(runtimeValidationFunction, data, authorization[25:]); + + // If runtime validation passes, do runtime permission checks + PostExecToRun[] memory postPermissionHooks = + _doPreHooks(getAccountStorage().validationData[runtimeValidationFunction].permissionHooks, data); + + // Execute the call + (bool success, bytes memory returnData) = address(this).call(data); + + if (!success) { + assembly ("memory-safe") { + revert(add(returnData, 32), mload(returnData)) + } + } + + _doCachedPostExecHooks(postPermissionHooks); + + return returnData; + } + + /// @inheritdoc IModularAccount + /// @notice May be validated by a global validation. + function installExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata moduleInstallData + ) external override wrapNativeFunction { + _installExecution(module, manifest, moduleInstallData); + } + + /// @inheritdoc IModularAccount + /// @notice May be validated by a global validation. + function uninstallExecution( + address module, + ExecutionManifest calldata manifest, + bytes calldata moduleUninstallData + ) external override wrapNativeFunction { + _uninstallExecution(module, manifest, moduleUninstallData); + } + + /// @notice Initializes the account with a validation function added to the global pool. + /// @dev This function is only callable once. + function initializeWithValidation( + ValidationConfig validationConfig, + bytes4[] calldata selectors, + bytes calldata installData, + bytes[] calldata hooks + ) external virtual initializer { + _installValidation(validationConfig, selectors, installData, hooks); + } + + /// @inheritdoc IModularAccount + /// @notice May be validated by a global validation. + function installValidation( + ValidationConfig validationConfig, + bytes4[] calldata selectors, + bytes calldata installData, + bytes[] calldata hooks + ) external wrapNativeFunction { + _installValidation(validationConfig, selectors, installData, hooks); + } + + /// @inheritdoc IModularAccount + /// @notice May be validated by a global validation. + function uninstallValidation( + ModuleEntity validationFunction, + bytes calldata uninstallData, + bytes[] calldata hookUninstallData + ) external wrapNativeFunction { + _uninstallValidation(validationFunction, uninstallData, hookUninstallData); + } + + /// @notice ERC165 introspection + /// @dev returns true for `IERC165.interfaceId` and false for `0xFFFFFFFF` + /// @param interfaceId interface id to check against + /// @return bool support for specific interface + function supportsInterface(bytes4 interfaceId) external view override returns (bool) { + if (interfaceId == _INTERFACE_ID_INVALID) { + return false; + } + if (interfaceId == _IERC165_INTERFACE_ID) { + return true; + } + + return getAccountStorage().supportedIfaces[interfaceId] > 0; + } + + /// @inheritdoc IModularAccount + function accountId() external pure virtual returns (string memory) { + return "erc6900.reference-modular-account.0.8.0"; + } + + /// @inheritdoc UUPSUpgradeable + /// @notice May be validated by a global validation. + function upgradeToAndCall(address newImplementation, bytes memory data) + public + payable + override + onlyProxy + wrapNativeFunction + { + super.upgradeToAndCall(newImplementation, data); + } + + function isValidSignature(bytes32 hash, bytes calldata signature) public view override returns (bytes4) { + ModuleEntity sigValidation = ModuleEntity.wrap(bytes24(signature)); + signature = signature[24:]; + + ModuleEntity[] memory preSignatureValidationHooks = + getAccountStorage().validationData[sigValidation].preValidationHooks; + + for (uint256 i = 0; i < preSignatureValidationHooks.length; ++i) { + (address hookModule, uint32 hookEntityId) = preSignatureValidationHooks[i].unpack(); + + bytes memory currentSignatureSegment; + + (currentSignatureSegment, signature) = signature.advanceSegmentIfAtIndex(uint8(i)); + + // If this reverts, bubble up revert reason. + IValidationHookModule(hookModule).preSignatureValidationHook( + hookEntityId, msg.sender, hash, currentSignatureSegment + ); + } + + signature = signature.getFinalSegment(); + + return _exec1271Validation(sigValidation, hash, signature); + } + + /// @notice Gets the entry point for this account + /// @return entryPoint The entry point for this account + function entryPoint() public view override returns (IEntryPoint) { + return _ENTRY_POINT; + } + + // INTERNAL FUNCTIONS + + // Parent function validateUserOp enforces that this call can only be made by the EntryPoint + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) + internal + override + returns (uint256 validationData) + { + if (userOp.callData.length < 4) { + revert UnrecognizedFunction(bytes4(userOp.callData)); + } + + // Revert if the provided `authorization` less than 21 bytes long, rather than right-padding. + ModuleEntity userOpValidationFunction = ModuleEntity.wrap(bytes24(userOp.signature[:24])); + bool isGlobalValidation = uint8(userOp.signature[24]) == 1; + + _checkIfValidationAppliesCallData(userOp.callData, userOpValidationFunction, isGlobalValidation); + + // Check if there are permission hooks associated with the validator, and revert if the call isn't to + // `executeUserOp` + // This check must be here because if context isn't passed, we can't tell in execution which hooks should + // have ran + if ( + getAccountStorage().validationData[userOpValidationFunction].permissionHooks.length() > 0 + && bytes4(userOp.callData[:4]) != this.executeUserOp.selector + ) { + revert RequireUserOperationContext(); + } + + validationData = _doUserOpValidation(userOpValidationFunction, userOp, userOp.signature[25:], userOpHash); + } + + // To support gas estimation, we don't fail early when the failure is caused by a signature failure + function _doUserOpValidation( + ModuleEntity userOpValidationFunction, + PackedUserOperation memory userOp, + bytes calldata signature, + bytes32 userOpHash + ) internal returns (uint256) { + uint256 validationRes; + + // Do preUserOpValidation hooks + ModuleEntity[] memory preUserOpValidationHooks = + getAccountStorage().validationData[userOpValidationFunction].preValidationHooks; + + for (uint256 i = 0; i < preUserOpValidationHooks.length; ++i) { + (userOp.signature, signature) = signature.advanceSegmentIfAtIndex(uint8(i)); + + (address module, uint32 entityId) = preUserOpValidationHooks[i].unpack(); + uint256 currentValidationRes = + IValidationHookModule(module).preUserOpValidationHook(entityId, userOp, userOpHash); + + if (uint160(currentValidationRes) > 1) { + // If the aggregator is not 0 or 1, it is an unexpected value + revert UnexpectedAggregator(module, entityId, address(uint160(currentValidationRes))); + } + validationRes = _coalescePreValidation(validationRes, currentValidationRes); + } + + // Run the user op validation function + { + userOp.signature = signature.getFinalSegment(); + + uint256 currentValidationRes = _execUserOpValidation(userOpValidationFunction, userOp, userOpHash); + + if (preUserOpValidationHooks.length != 0) { + // If we have other validation data we need to coalesce with + validationRes = _coalesceValidation(validationRes, currentValidationRes); + } else { + validationRes = currentValidationRes; + } + } + + return validationRes; + } + + function _doRuntimeValidation( + ModuleEntity runtimeValidationFunction, + bytes calldata callData, + bytes calldata authorizationData + ) internal { + // run all preRuntimeValidation hooks + ModuleEntity[] memory preRuntimeValidationHooks = + getAccountStorage().validationData[runtimeValidationFunction].preValidationHooks; + + for (uint256 i = 0; i < preRuntimeValidationHooks.length; ++i) { + bytes memory currentAuthSegment; + + (currentAuthSegment, authorizationData) = authorizationData.advanceSegmentIfAtIndex(uint8(i)); + + _doPreRuntimeValidationHook(preRuntimeValidationHooks[i], callData, currentAuthSegment); + } + + authorizationData = authorizationData.getFinalSegment(); + + _execRuntimeValidation(runtimeValidationFunction, callData, authorizationData); + } + + function _doPreHooks(EnumerableSet.Bytes32Set storage executionHooks, bytes memory data) + internal + returns (PostExecToRun[] memory postHooksToRun) + { + uint256 hooksLength = executionHooks.length(); + // Overallocate on length - not all of this may get filled up. We set the correct length later. + postHooksToRun = new PostExecToRun[](hooksLength); + + // Copy all post hooks to the array. This happens before any pre hooks are run, so we can + // be sure that the set of hooks to run will not be affected by state changes mid-execution. + for (uint256 i = 0; i < hooksLength; ++i) { + HookConfig hookConfig = toHookConfig(executionHooks.at(i)); + if (hookConfig.hasPostHook()) { + postHooksToRun[i].postExecHook = hookConfig.moduleEntity(); + } + } + + // Run the pre hooks and copy their return data to the post hooks array, if an associated post-exec hook + // exists. + for (uint256 i = 0; i < hooksLength; ++i) { + HookConfig hookConfig = toHookConfig(executionHooks.at(i)); + + if (hookConfig.hasPreHook()) { + bytes memory preExecHookReturnData; + + preExecHookReturnData = _runPreExecHook(hookConfig.moduleEntity(), data); + + // If there is an associated post-exec hook, save the return data. + if (hookConfig.hasPostHook()) { + postHooksToRun[i].preExecHookReturnData = preExecHookReturnData; + } + } + } + } + + function _runPreExecHook(ModuleEntity preExecHook, bytes memory data) + internal + returns (bytes memory preExecHookReturnData) + { + (address module, uint32 entityId) = preExecHook.unpack(); + try IExecutionHookModule(module).preExecutionHook(entityId, msg.sender, msg.value, data) returns ( + bytes memory returnData + ) { + preExecHookReturnData = returnData; + } catch { + bytes memory revertReason = collectReturnData(); + revert PreExecHookReverted(module, entityId, revertReason); + } + } + + /// @dev Associated post hooks are run in reverse order of their pre hooks. + function _doCachedPostExecHooks(PostExecToRun[] memory postHooksToRun) internal { + uint256 postHooksToRunLength = postHooksToRun.length; + for (uint256 i = postHooksToRunLength; i > 0;) { + // Decrement here, instead of in the loop body, to handle the case where length is 0. + --i; + + PostExecToRun memory postHookToRun = postHooksToRun[i]; + + if (postHookToRun.postExecHook.isEmpty()) { + // This is an empty post hook, from a pre-only hook, so we skip it. + continue; + } + + (address module, uint32 entityId) = postHookToRun.postExecHook.unpack(); + // solhint-disable-next-line no-empty-blocks + try IExecutionHookModule(module).postExecutionHook(entityId, postHookToRun.preExecHookReturnData) {} + catch { + bytes memory revertReason = collectReturnData(); + revert PostExecHookReverted(module, entityId, revertReason); + } + } + } + + function _doPreRuntimeValidationHook( + ModuleEntity validationHook, + bytes memory callData, + bytes memory currentAuthData + ) internal { + (address hookModule, uint32 hookEntityId) = validationHook.unpack(); + try IValidationHookModule(hookModule).preRuntimeValidationHook( + hookEntityId, msg.sender, msg.value, callData, currentAuthData + ) + // forgefmt: disable-start + // solhint-disable-next-line no-empty-blocks + {} catch{ + // forgefmt: disable-end + bytes memory revertReason = collectReturnData(); + revert PreRuntimeValidationHookFailed(hookModule, hookEntityId, revertReason); + } + } + + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address newImplementation) internal override {} + + /** + * Order of operations: + * 1. Check if the sender is the entry point, the account itself, or the selector called is public. + * - Yes: Return an empty array, there are no post-permissionHooks. + * - No: Continue + * 2. Check if the called selector (msg.sig) is included in the set of selectors the msg.sender can + * directly call. + * - Yes: Continue + * - No: Revert, the caller is not allowed to call this selector + * 3. If there are runtime validation hooks associated with this caller-sig combination, run them. + * 4. Run the pre-permissionHooks associated with this caller-sig combination, and return the + * post-permissionHooks to run later. + */ + function _checkPermittedCallerAndAssociatedHooks() + internal + returns (PostExecToRun[] memory, PostExecToRun[] memory) + { + AccountStorage storage _storage = getAccountStorage(); + PostExecToRun[] memory postPermissionHooks; + + // We only need to handle permission hooks when the sender is not the entry point or the account itself, + // and the selector isn't public. + if ( + msg.sender != address(_ENTRY_POINT) && msg.sender != address(this) + && !_storage.executionData[msg.sig].isPublic + ) { + ModuleEntity directCallValidationKey = + ModuleEntityLib.pack(msg.sender, DIRECT_CALL_VALIDATION_ENTITYID); + + _checkIfValidationAppliesCallData(msg.data, directCallValidationKey, false); + + // Direct call is allowed, run associated permission & validation hooks + + // Validation hooks + ModuleEntity[] memory preRuntimeValidationHooks = + _storage.validationData[directCallValidationKey].preValidationHooks; + + uint256 hookLen = preRuntimeValidationHooks.length; + for (uint256 i = 0; i < hookLen; ++i) { + _doPreRuntimeValidationHook(preRuntimeValidationHooks[i], msg.data, ""); + } + + // Permission hooks + postPermissionHooks = + _doPreHooks(_storage.validationData[directCallValidationKey].permissionHooks, msg.data); + } + + // Exec hooks + PostExecToRun[] memory postExecutionHooks = + _doPreHooks(_storage.executionData[msg.sig].executionHooks, msg.data); + + return (postPermissionHooks, postExecutionHooks); + } + + function _execUserOpValidation( + ModuleEntity userOpValidationFunction, + PackedUserOperation memory userOp, + bytes32 userOpHash + ) internal virtual returns (uint256) { + (address module, uint32 entityId) = userOpValidationFunction.unpack(); + + return IValidationModule(module).validateUserOp(entityId, userOp, userOpHash); + } + + function _execRuntimeValidation( + ModuleEntity runtimeValidationFunction, + bytes calldata callData, + bytes calldata authorization + ) internal virtual { + (address module, uint32 entityId) = runtimeValidationFunction.unpack(); + + try IValidationModule(module).validateRuntime( + address(this), entityId, msg.sender, msg.value, callData, authorization + ) + // forgefmt: disable-start + // solhint-disable-next-line no-empty-blocks + {} catch{ + // forgefmt: disable-end + bytes memory revertReason = collectReturnData(); + revert RuntimeValidationFunctionReverted(module, entityId, revertReason); + } + } + + function _exec1271Validation(ModuleEntity sigValidation, bytes32 hash, bytes calldata signature) + internal + view + virtual + returns (bytes4) + { + AccountStorage storage _storage = getAccountStorage(); + + (address module, uint32 entityId) = sigValidation.unpack(); + if (!_storage.validationData[sigValidation].isSignatureValidation) { + revert SignatureValidationInvalid(module, entityId); + } + + if ( + IValidationModule(module).validateSignature(address(this), entityId, msg.sender, hash, signature) + == _1271_MAGIC_VALUE + ) { + return _1271_MAGIC_VALUE; + } + return _1271_INVALID; + } + + function _globalValidationAllowed(bytes4 selector) internal view virtual returns (bool) { + if ( + selector == this.execute.selector || selector == this.executeBatch.selector + || selector == this.installExecution.selector || selector == this.uninstallExecution.selector + || selector == this.installValidation.selector || selector == this.uninstallValidation.selector + || selector == this.upgradeToAndCall.selector + ) { + return true; + } + + return getAccountStorage().executionData[selector].allowGlobalValidation; + } + + function _isValidationGlobal(ModuleEntity validationFunction) internal view virtual returns (bool) { + return getAccountStorage().validationData[validationFunction].isGlobal; + } + + function _checkIfValidationAppliesCallData( + bytes calldata callData, + ModuleEntity validationFunction, + bool isGlobal + ) internal view { + bytes4 outerSelector = bytes4(callData[:4]); + if (outerSelector == this.executeUserOp.selector) { + // If the selector is executeUserOp, pull the actual selector from the following data, + // and trim the calldata to ensure the self-call decoding is still accurate. + callData = callData[4:]; + outerSelector = bytes4(callData[:4]); + } + + _checkIfValidationAppliesSelector(outerSelector, validationFunction, isGlobal); + + if (outerSelector == IModularAccount.execute.selector) { + (address target,,) = abi.decode(callData[4:], (address, uint256, bytes)); + + if (target == address(this)) { + // There is no point to call `execute` to recurse exactly once - this is equivalent to just having + // the calldata as a top-level call. + revert SelfCallRecursionDepthExceeded(); + } + } else if (outerSelector == IModularAccount.executeBatch.selector) { + // executeBatch may be used to batch account actions together, by targetting the account itself. + // If this is done, we must ensure all of the inner calls are allowed by the provided validation + // function. + + (Call[] memory calls) = abi.decode(callData[4:], (Call[])); + + for (uint256 i = 0; i < calls.length; ++i) { + if (calls[i].target == address(this)) { + bytes4 nestedSelector = bytes4(calls[i].data); + + if ( + nestedSelector == IModularAccount.execute.selector + || nestedSelector == IModularAccount.executeBatch.selector + ) { + // To prevent arbitrarily-deep recursive checking, we limit the depth of self-calls to one + // for the purposes of batching. + // This means that all self-calls must occur at the top level of the batch. + // Note that modules of other contracts using `executeWithAuthorization` may still + // independently call into this account with a different validation function, allowing + // composition of multiple batches. + revert SelfCallRecursionDepthExceeded(); + } + + _checkIfValidationAppliesSelector(nestedSelector, validationFunction, isGlobal); + } + } + } + } + + function _checkIfValidationAppliesSelector(bytes4 selector, ModuleEntity validationFunction, bool isGlobal) + internal + view + { + // Check that the provided validation function is applicable to the selector + if (isGlobal) { + if (!_globalValidationAllowed(selector) || !_isValidationGlobal(validationFunction)) { + revert ValidationFunctionMissing(selector); + } + } else { + // Not global validation, but per-selector + if (!getAccountStorage().validationData[validationFunction].selectors.contains(toSetValue(selector))) { + revert ValidationFunctionMissing(selector); + } + } + } +} diff --git a/src/interfaces/IModularAccount.sol b/src/interfaces/IModularAccount.sol index dcad3ee8..8ddebb75 100644 --- a/src/interfaces/IModularAccount.sol +++ b/src/interfaces/IModularAccount.sol @@ -99,7 +99,8 @@ interface IModularAccount { ) external; /// @notice Return a unique identifier for the account implementation. - /// @dev This function MUST return a string in the format "vendor/account/semver". + /// @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); } diff --git a/src/interfaces/IModule.sol b/src/interfaces/IModule.sol index df070f6f..523e7265 100644 --- a/src/interfaces/IModule.sol +++ b/src/interfaces/IModule.sol @@ -17,7 +17,8 @@ interface IModule is IERC165 { 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". + /// @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); } diff --git a/src/modules/ERC20TokenLimitModule.sol b/src/modules/ERC20TokenLimitModule.sol index c72bace3..eda85d15 100644 --- a/src/modules/ERC20TokenLimitModule.sol +++ b/src/modules/ERC20TokenLimitModule.sol @@ -115,7 +115,7 @@ contract ERC20TokenLimitModule is BaseModule, IExecutionHookModule { /// @inheritdoc IModule function moduleId() external pure returns (string memory) { - return "erc6900/erc20-token-limit-module/1.0.0"; + return "erc6900.erc20-token-limit-module.1.0.0"; } /// @inheritdoc BaseModule diff --git a/src/modules/NativeTokenLimitModule.sol b/src/modules/NativeTokenLimitModule.sol index 4d0a076b..b9244145 100644 --- a/src/modules/NativeTokenLimitModule.sol +++ b/src/modules/NativeTokenLimitModule.sol @@ -116,7 +116,7 @@ contract NativeTokenLimitModule is BaseModule, IExecutionHookModule, IValidation /// @inheritdoc IModule function moduleId() external pure returns (string memory) { - return "erc6900/native-token-limit-module/1.0.0"; + return "erc6900.native-token-limit-module.1.0.0"; } // ┏━━━━━━━━━━━━━━━┓ diff --git a/src/modules/TokenReceiverModule.sol b/src/modules/TokenReceiverModule.sol index 51f06fcb..df02cd1f 100644 --- a/src/modules/TokenReceiverModule.sol +++ b/src/modules/TokenReceiverModule.sol @@ -82,6 +82,6 @@ contract TokenReceiverModule is BaseModule, IExecutionModule, IERC721Receiver, I /// @inheritdoc IModule function moduleId() external pure returns (string memory) { - return "erc6900/token-receiver-module/1.0.0"; + return "erc6900.token-receiver-module.1.0.0"; } } diff --git a/src/modules/permissionhooks/AllowlistModule.sol b/src/modules/permissionhooks/AllowlistModule.sol index 4d82bcf6..6b693244 100644 --- a/src/modules/permissionhooks/AllowlistModule.sol +++ b/src/modules/permissionhooks/AllowlistModule.sol @@ -91,7 +91,7 @@ contract AllowlistModule is IValidationHookModule, BaseModule { /// @inheritdoc IModule function moduleId() external pure returns (string memory) { - return "erc6900/allowlist-module/0.0.1"; + return "erc6900.allowlist-module.0.0.1"; } function setAllowlistTarget(uint32 entityId, address target, bool allowed, bool hasSelectorAllowlist) public { diff --git a/src/modules/validation/SingleSignerValidationModule.sol b/src/modules/validation/SingleSignerValidationModule.sol index 2dd0b28d..a750f4d6 100644 --- a/src/modules/validation/SingleSignerValidationModule.sol +++ b/src/modules/validation/SingleSignerValidationModule.sol @@ -114,7 +114,7 @@ contract SingleSignerValidationModule is ISingleSignerValidationModule, ReplaySa /// @inheritdoc IModule function moduleId() external pure returns (string memory) { - return "erc6900/single-signer-validation-module/1.0.0"; + return "erc6900.single-signer-validation-module.1.0.0"; } function supportsInterface(bytes4 interfaceId) diff --git a/test/account/ReferenceModularAccount.t.sol b/test/account/ReferenceModularAccount.t.sol index 5ffa8fa7..85b06c93 100644 --- a/test/account/ReferenceModularAccount.t.sol +++ b/test/account/ReferenceModularAccount.t.sol @@ -189,8 +189,8 @@ contract ReferenceModularAccountTest is AccountTestBase { assertEq( accountId, vm.envOr("SMA_TEST", false) - ? "erc6900/reference-semi-modular-account/0.8.0" - : "erc6900/reference-modular-account/0.8.0" + ? "erc6900.reference-semi-modular-account.0.8.0" + : "erc6900.reference-modular-account.0.8.0" ); } diff --git a/test/mocks/MockModule.sol b/test/mocks/MockModule.sol index 354373e1..260c515c 100644 --- a/test/mocks/MockModule.sol +++ b/test/mocks/MockModule.sol @@ -48,7 +48,7 @@ contract MockModule is ERC165 { } function moduleId() external pure returns (string memory) { - return "erc6900/mock-module/1.0.0"; + return "erc6900.mock-module.1.0.0"; } /// @dev Returns true if this contract implements the interface defined by diff --git a/test/mocks/modules/ComprehensiveModule.sol b/test/mocks/modules/ComprehensiveModule.sol index 623c4cf9..cfaead0e 100644 --- a/test/mocks/modules/ComprehensiveModule.sol +++ b/test/mocks/modules/ComprehensiveModule.sol @@ -175,6 +175,6 @@ contract ComprehensiveModule is } function moduleId() external pure returns (string memory) { - return "erc6900/comprehensive-module/1.0.0"; + return "erc6900.comprehensive-module.1.0.0"; } } diff --git a/test/mocks/modules/DirectCallModule.sol b/test/mocks/modules/DirectCallModule.sol index 35adde84..c81c9a18 100644 --- a/test/mocks/modules/DirectCallModule.sol +++ b/test/mocks/modules/DirectCallModule.sol @@ -24,7 +24,7 @@ contract DirectCallModule is BaseModule, IExecutionHookModule { } function moduleId() external pure returns (string memory) { - return "erc6900/direct-call-module/1.0.0"; + return "erc6900.direct-call-module.1.0.0"; } function preExecutionHook(uint32, address sender, uint256, bytes calldata) diff --git a/test/mocks/modules/MockAccessControlHookModule.sol b/test/mocks/modules/MockAccessControlHookModule.sol index 74a92b10..bad9fcb4 100644 --- a/test/mocks/modules/MockAccessControlHookModule.sol +++ b/test/mocks/modules/MockAccessControlHookModule.sol @@ -79,7 +79,7 @@ contract MockAccessControlHookModule is IValidationHookModule, BaseModule { } function moduleId() external pure returns (string memory) { - return "erc6900/mock-access-control-hook-module/1.0.0"; + return "erc6900.mock-access-control-hook-module.1.0.0"; } function supportsInterface(bytes4 interfaceId) diff --git a/test/mocks/modules/PermittedCallMocks.sol b/test/mocks/modules/PermittedCallMocks.sol index 0981160f..41931d4f 100644 --- a/test/mocks/modules/PermittedCallMocks.sol +++ b/test/mocks/modules/PermittedCallMocks.sol @@ -30,7 +30,7 @@ contract PermittedCallerModule is IExecutionModule, BaseModule { } function moduleId() external pure returns (string memory) { - return "erc6900/permitted-caller-module/1.0.0"; + return "erc6900.permitted-caller-module.1.0.0"; } // The manifest requested access to use the module-defined method "foo" diff --git a/test/mocks/modules/ReturnDataModuleMocks.sol b/test/mocks/modules/ReturnDataModuleMocks.sol index 3eb81fa7..1c7ff6a2 100644 --- a/test/mocks/modules/ReturnDataModuleMocks.sol +++ b/test/mocks/modules/ReturnDataModuleMocks.sol @@ -58,7 +58,7 @@ contract ResultCreatorModule is IExecutionModule, BaseModule { } function moduleId() external pure returns (string memory) { - return "erc6900/result-creator-module/1.0.0"; + return "erc6900.result-creator-module.1.0.0"; } } @@ -139,6 +139,6 @@ contract ResultConsumerModule is IExecutionModule, BaseModule, IValidationModule } function moduleId() external pure returns (string memory) { - return "erc6900/result-consumer-module/1.0.0"; + return "erc6900.result-consumer-module.1.0.0"; } } diff --git a/test/mocks/modules/ValidationModuleMocks.sol b/test/mocks/modules/ValidationModuleMocks.sol index 3521e3fc..40e6ad26 100644 --- a/test/mocks/modules/ValidationModuleMocks.sol +++ b/test/mocks/modules/ValidationModuleMocks.sol @@ -75,7 +75,7 @@ abstract contract MockBaseUserOpValidationModule is } function moduleId() external pure returns (string memory) { - return "erc6900/mock-user-op-validation-module/1.0.0"; + return "erc6900.mock-user-op-validation-module.1.0.0"; } // Empty stubs