diff --git a/foundry.toml b/foundry.toml index 32fa163..c7a223c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -38,6 +38,9 @@ via-ir = true fuzz = { runs = 10_000 } verbosity = 4 +[profile.via-ir] +via_ir = true + [fuzz] runs = 1000 max_test_rejects = 1_000_000 diff --git a/src/StartaleSmartAccount.sol b/src/StartaleSmartAccount.sol index 5332603..8da7bf2 100644 --- a/src/StartaleSmartAccount.sol +++ b/src/StartaleSmartAccount.sol @@ -87,7 +87,7 @@ contract StartaleSmartAccount is IStartaleSmartAccount, BaseAccount, ExecutionHe /// @dev Features Module Enable Mode. /// This Module Enable Mode flow is intended for the module acting as the validator /// for the user operation that triggers the Module Enable Flow. Otherwise, a call to - /// `Nexus.installModule` should be included in `userOp.callData`. + /// `IERC7579Account.installModule` should be included in `userOp.callData`. function validateUserOp( PackedUserOperation calldata op, bytes32 userOpHash, @@ -192,6 +192,8 @@ contract StartaleSmartAccount is IStartaleSmartAccount, BaseAccount, ExecutionHe /// - 4 for Hook /// - 8 for 1271 Prevalidation Hook /// - 9 for 4337 Prevalidation Hook + /// @notice + /// If the module is malicious, it can prevent itself from being uninstalled by spending all gas in the onUninstall() method. /// @param module The address of the module to uninstall. /// @param deInitData De-initialization data for the module. /// @dev Ensures that the operation is authorized and valid before proceeding with the uninstallation. @@ -204,6 +206,7 @@ contract StartaleSmartAccount is IStartaleSmartAccount, BaseAccount, ExecutionHe if (moduleTypeId == MODULE_TYPE_VALIDATOR) { _uninstallValidator(module, deInitData); + _checkInitializedValidators(); } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) { _uninstallExecutor(module, deInitData); } else if (moduleTypeId == MODULE_TYPE_FALLBACK) { @@ -437,6 +440,26 @@ contract StartaleSmartAccount is IStartaleSmartAccount, BaseAccount, ExecutionHe } } + // checks if there's at least one validator initialized + function _checkInitializedValidators() internal view { + if (!_amIERC7702() && !IValidator(_DEFAULT_VALIDATOR).isInitialized(address(this))) { + unchecked { + SentinelListLib.SentinelList storage validators = _getAccountStorage().validators; + address next = validators.entries[SENTINEL]; + while (next != ZERO_ADDRESS && next != SENTINEL) { + if (IValidator(next).isInitialized(address(this))) { + break; + } + next = validators.getNext(next); + } + if (next == SENTINEL) { + //went through all validators and none was initialized + revert CanNotRemoveLastValidator(); + } + } + } + } + /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = 'Startale'; diff --git a/src/core/ExecutionHelper.sol b/src/core/ExecutionHelper.sol index f4bbba0..f794080 100644 --- a/src/core/ExecutionHelper.sol +++ b/src/core/ExecutionHelper.sol @@ -39,28 +39,6 @@ abstract contract ExecutionHelper is IExecutionHelper { } } - /// @notice Executes a call to a target address with specified value and data. - /// same as _execute, but callData can be in memory - function _executeMemory( - address target, - uint256 value, - bytes memory callData - ) internal virtual returns (bytes memory result) { - /// @solidity memory-safe-assembly - assembly { - result := mload(0x40) - if iszero(call(gas(), target, value, add(callData, 0x20), mload(callData), codesize(), 0x00)) { - // Bubble up the revert if the call reverts. - returndatacopy(result, 0x00, returndatasize()) - revert(result, returndatasize()) - } - mstore(result, returndatasize()) // Store the length. - let o := add(result, 0x20) - returndatacopy(o, 0x00, returndatasize()) // Copy the returndata. - mstore(0x40, add(o, returndatasize())) // Allocate the memory. - } - } - /// @notice Executes a call to a target address with specified value and data. /// Same as _execute but without return data for gas optimization. function _executeNoReturndata(address target, uint256 value, bytes calldata callData) internal virtual { diff --git a/src/core/ModuleManager.sol b/src/core/ModuleManager.sol index 59758ec..76611d5 100644 --- a/src/core/ModuleManager.sol +++ b/src/core/ModuleManager.sol @@ -47,11 +47,6 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr using ExcessivelySafeCall for address; using ECDSA for bytes32; - /// @dev The slot in the transient storage to store the hooking flag. - // keccak256(abi.encode(uint256(keccak256(bytes("startale.modulemanager.hooking"))) - 1)) & ~bytes32(uint256(0xff)) - bytes32 internal constant HOOKING_FLAG_TRANSIENT_STORAGE_SLOT = - 0x1085a7be7203eb252e321995729a7b29dd605712ca7b7b85cd33107663f41400; - /// @dev The default validator address. /// @notice To explicitly initialize the default validator, StartaleSmartAccount.execute(_DEFAULT_VALIDATOR.onInstall(...)) should be called. address internal immutable _DEFAULT_VALIDATOR; @@ -75,16 +70,9 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr /// @dev sender, msg.data and msg.value is passed to the hook to implement custom flows. modifier withHook() { address hook = _getHook(); - bool hooking; - assembly { - hooking := tload(HOOKING_FLAG_TRANSIENT_STORAGE_SLOT) - } - if (hook == address(0) || hooking) { + if (hook == address(0)) { _; } else { - assembly { - tstore(HOOKING_FLAG_TRANSIENT_STORAGE_SLOT, 1) - } bytes memory hookData = IHook(hook).preCheck(msg.sender, msg.value, msg.data); _; IHook(hook).postCheck(hookData); @@ -191,7 +179,7 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr /// @dev This function goes through hook checks via withHook modifier. /// @dev No need to check that the module is already installed, as this check is done /// when trying to sstore the module in an appropriate SentinelList - function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal withHook { + function _installModule(uint256 moduleTypeId, address module, bytes calldata initData) internal { if (!_areSentinelListsInitialized()) { _initSentinelLists(); } @@ -218,7 +206,7 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr /// @dev Installs a new validator module after checking if it matches the required module type. /// @param validator The address of the validator module to be installed. /// @param data Initialization data to configure the validator upon installation. - function _installValidator(address validator, bytes calldata data) internal virtual { + function _installValidator(address validator, bytes calldata data) internal virtual withHook { if (!IValidator(validator).isModuleType(MODULE_TYPE_VALIDATOR)) revert MismatchModuleTypeId(); if (validator == _DEFAULT_VALIDATOR) { revert DefaultValidatorAlreadyInstalled(); @@ -227,7 +215,7 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr IValidator(validator).onInstall(data); } - /// @dev Uninstalls a validator module /!\ ensuring the account retains at least one validator. + /// @dev Uninstalls a validator module /// @param validator The address of the validator to be uninstalled. /// @param data De-initialization data to configure the validator upon uninstallation. function _uninstallValidator(address validator, bytes calldata data) internal virtual { @@ -246,7 +234,7 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr /// @dev Installs a new executor module after checking if it matches the required module type. /// @param executor The address of the executor module to be installed. /// @param data Initialization data to configure the executor upon installation. - function _installExecutor(address executor, bytes calldata data) internal virtual { + function _installExecutor(address executor, bytes calldata data) internal virtual withHook { if (!IExecutor(executor).isModuleType(MODULE_TYPE_EXECUTOR)) revert MismatchModuleTypeId(); _getAccountStorage().executors.push(executor); IExecutor(executor).onInstall(data); @@ -266,7 +254,7 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr /// @dev Installs a hook module, ensuring no other hooks are installed before proceeding. /// @param hook The address of the hook to be installed. /// @param data Initialization data to configure the hook upon installation. - function _installHook(address hook, bytes calldata data) internal virtual { + function _installHook(address hook, bytes calldata data) internal virtual withHook { if (!IHook(hook).isModuleType(MODULE_TYPE_HOOK)) revert MismatchModuleTypeId(); address currentHook = _getHook(); require(currentHook == address(0), HookAlreadyInstalled(currentHook)); @@ -297,7 +285,7 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr /// @dev Installs a fallback handler for a given selector with initialization data. /// @param handler The address of the fallback handler to install. /// @param params The initialization parameters including the selector and call type. - function _installFallbackHandler(address handler, bytes calldata params) internal virtual { + function _installFallbackHandler(address handler, bytes calldata params) internal virtual withHook { if (!IFallback(handler).isModuleType(MODULE_TYPE_FALLBACK)) revert MismatchModuleTypeId(); // Extract the function selector from the provided parameters. bytes4 selector = bytes4(params[0:4]); @@ -350,7 +338,7 @@ abstract contract ModuleManager is AllStorage, EIP712, IModuleManagerEventsAndEr uint256 preValidationHookType, address preValidationHook, bytes calldata data - ) internal virtual { + ) internal virtual withHook { if (!IModule(preValidationHook).isModuleType(preValidationHookType)) revert MismatchModuleTypeId(); address currentPreValidationHook = _getPreValidationHook(preValidationHookType); if (currentPreValidationHook != address(0)) revert PrevalidationHookAlreadyInstalled(currentPreValidationHook); diff --git a/src/factory/EOAOnboardingFactory.sol b/src/factory/EOAOnboardingFactory.sol index 14a4435..7f410ae 100644 --- a/src/factory/EOAOnboardingFactory.sol +++ b/src/factory/EOAOnboardingFactory.sol @@ -13,7 +13,7 @@ import {Stakeable} from '../utils/Stakeable.sol'; /// Special thanks to the Biconomy team for https://github.com/bcnmy/nexus/ on which this factory implementation is highly based on. /// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady contract EOAOnboardingFactory is Stakeable { - /// @notice Stores the implementation contract address used to create new Nexus instances. + /// @notice Stores the implementation contract address used to create new account instances. /// @dev This address is set once upon deployment and cannot be changed afterwards. address public immutable ACCOUNT_IMPLEMENTATION; @@ -32,7 +32,7 @@ contract EOAOnboardingFactory is Stakeable { error ZeroAddressNotAllowed(); /// @notice Constructor to set the immutable variables. - /// @param implementation The address of the Nexus implementation to be used for all deployments. + /// @param implementation The address of the smart account implementation to be used for all deployments. /// @param factoryOwner The address of the factory owner. /// @param ecdsaValidator The address of the K1 Validator module to be used for all deployments. /// @param bootstrapper The address of the Bootstrapper module to be used for all deployments. @@ -62,9 +62,10 @@ contract EOAOnboardingFactory is Stakeable { // Compute the salt for deterministic deployment bytes32 salt = keccak256(abi.encodePacked(eoaOwner, index)); - // Create the validator configuration using the NexusBootstrap library - BootstrapConfig memory validator = BootstrapLib.createSingleConfig(ECDSA_VALIDATOR, abi.encodePacked(eoaOwner)); - bytes memory initData = BOOTSTRAPPER.getInitWithSingleValidatorCalldata(validator); + bytes memory initData = abi.encode( + address(BOOTSTRAPPER), + abi.encodeCall(BOOTSTRAPPER.initWithSingleValidator, (ECDSA_VALIDATOR, abi.encodePacked(eoaOwner))) + ); // Deploy the Smart account using the ProxyLib (bool alreadyDeployed, address payable account) = ProxyLib.deployProxy(ACCOUNT_IMPLEMENTATION, salt, initData); @@ -85,11 +86,10 @@ contract EOAOnboardingFactory is Stakeable { // Compute the salt for deterministic deployment bytes32 salt = keccak256(abi.encodePacked(eoaOwner, index)); - // Create the validator configuration using the NexusBootstrap library - BootstrapConfig memory validator = BootstrapLib.createSingleConfig(ECDSA_VALIDATOR, abi.encodePacked(eoaOwner)); - - // Get the initialization data for the Nexus account - bytes memory initData = BOOTSTRAPPER.getInitWithSingleValidatorCalldata(validator); + bytes memory initData = abi.encode( + address(BOOTSTRAPPER), + abi.encodeCall(BOOTSTRAPPER.initWithSingleValidator, (ECDSA_VALIDATOR, abi.encodePacked(eoaOwner))) + ); // Compute the predicted address using the ProxyLib return ProxyLib.predictProxyAddress(ACCOUNT_IMPLEMENTATION, salt, initData); diff --git a/src/lib/BootstrapLib.sol b/src/lib/BootstrapLib.sol index 4f4cd9c..1ef1d4e 100644 --- a/src/lib/BootstrapLib.sol +++ b/src/lib/BootstrapLib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BootstrapConfig} from '../utils/Bootstrap.sol'; +import {BootstrapConfig, BootstrapPreValidationHookConfig} from '../utils/Bootstrap.sol'; /// @title Bootstrap Configuration Library /// @notice Provides utility functions to create and manage BootstrapConfig structures. @@ -28,6 +28,22 @@ library BootstrapLib { config[0].data = data; } + /// @notice Creates an array with a single BootstrapPreValidationHookConfig structure. + /// @param hookType The type of the pre-validation hook. + /// @param module The address of the module. + /// @param data The initialization data for the module. + /// @return config An array containing a single BootstrapPreValidationHookConfig structure. + function createArrayPreValidationHookConfig( + uint256 hookType, + address module, + bytes memory data + ) internal pure returns (BootstrapPreValidationHookConfig[] memory config) { + config = new BootstrapPreValidationHookConfig[](1); + config[0].hookType = hookType; + config[0].module = module; + config[0].data = data; + } + /// @notice Creates an array of BootstrapConfig structures. /// @param modules An array of module addresses. /// @param datas An array of initialization data for each module. diff --git a/src/utils/Bootstrap.sol b/src/utils/Bootstrap.sol index d7de674..c3c74ff 100644 --- a/src/utils/Bootstrap.sol +++ b/src/utils/Bootstrap.sol @@ -16,6 +16,14 @@ struct BootstrapConfig { bytes data; } +/// @title Bootstrap Pre-Validation Hook Configuration +/// @notice Provides configuration for pre-validation hooks. +struct BootstrapPreValidationHookConfig { + uint256 hookType; + address module; + bytes data; +} + /// @title Bootstrap /// @notice Manages the installation of modules into Smart Account using delegate calls. contract Bootstrap is ModuleManager { @@ -43,20 +51,22 @@ contract Bootstrap is ModuleManager { /// @param executors The configuration array for executor modules. /// @param hook The configuration for the hook module. /// @param fallbacks The configuration array for fallback handler modules. - function initNexusWithDefaultValidatorAndOtherModules( + function initWithDefaultValidatorAndOtherModules( bytes calldata defaultValidatorInitData, BootstrapConfig[] calldata executors, BootstrapConfig calldata hook, - BootstrapConfig[] calldata fallbacks + BootstrapConfig[] calldata fallbacks, + BootstrapPreValidationHookConfig[] calldata preValidationHooks ) external payable { - _initWithDefaultValidatorAndOtherModules(defaultValidatorInitData, executors, hook, fallbacks); + _initWithDefaultValidatorAndOtherModules(defaultValidatorInitData, executors, hook, fallbacks, preValidationHooks); } function _initWithDefaultValidatorAndOtherModules( bytes calldata defaultValidatorInitData, BootstrapConfig[] calldata executors, BootstrapConfig calldata hook, - BootstrapConfig[] calldata fallbacks + BootstrapConfig[] calldata fallbacks, + BootstrapPreValidationHookConfig[] calldata preValidationHooks ) internal _withInitSentinelLists { IModule(_DEFAULT_VALIDATOR).onInstall(defaultValidatorInitData); @@ -66,17 +76,26 @@ contract Bootstrap is ModuleManager { emit ModuleInstalled(MODULE_TYPE_EXECUTOR, executors[i].module); } + // Initialize fallback handlers + for (uint256 i = 0; i < fallbacks.length; i++) { + if (fallbacks[i].module == address(0)) continue; + _installFallbackHandler(fallbacks[i].module, fallbacks[i].data); + emit ModuleInstalled(MODULE_TYPE_FALLBACK, fallbacks[i].module); + } + // Initialize hook if (hook.module != address(0)) { _installHook(hook.module, hook.data); emit ModuleInstalled(MODULE_TYPE_HOOK, hook.module); } - // Initialize fallback handlers - for (uint256 i = 0; i < fallbacks.length; i++) { - if (fallbacks[i].module == address(0)) continue; - _installFallbackHandler(fallbacks[i].module, fallbacks[i].data); - emit ModuleInstalled(MODULE_TYPE_FALLBACK, fallbacks[i].module); + // Initialize pre-validation hooks + for (uint256 i; i < preValidationHooks.length; i++) { + if (preValidationHooks[i].module == address(0)) continue; + _installPreValidationHook( + preValidationHooks[i].hookType, preValidationHooks[i].module, preValidationHooks[i].data + ); + emit ModuleInstalled(preValidationHooks[i].hookType, preValidationHooks[i].module); } } @@ -87,11 +106,11 @@ contract Bootstrap is ModuleManager { /// @dev Intended to be called by the starttale account with a delegatecall. /// @param validator The address of the validator module. /// @param data The initialization data for the validator module. - function initWithSingleValidator(IModule validator, bytes calldata data) external payable { + function initWithSingleValidator(address validator, bytes calldata data) external payable { _initWithSingleValidator(validator, data); } - function _initWithSingleValidator(IModule validator, bytes calldata data) internal _withInitSentinelLists { + function _initWithSingleValidator(address validator, bytes calldata data) internal _withInitSentinelLists { _installValidator(address(validator), data); emit ModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator)); } @@ -110,16 +129,18 @@ contract Bootstrap is ModuleManager { BootstrapConfig[] calldata validators, BootstrapConfig[] calldata executors, BootstrapConfig calldata hook, - BootstrapConfig[] calldata fallbacks + BootstrapConfig[] calldata fallbacks, + BootstrapPreValidationHookConfig[] calldata preValidationHooks ) external payable { - _init(validators, executors, hook, fallbacks); + _init(validators, executors, hook, fallbacks, preValidationHooks); } function _init( BootstrapConfig[] calldata validators, BootstrapConfig[] calldata executors, BootstrapConfig calldata hook, - BootstrapConfig[] calldata fallbacks + BootstrapConfig[] calldata fallbacks, + BootstrapPreValidationHookConfig[] calldata preValidationHooks ) internal _withInitSentinelLists { // Initialize validators for (uint256 i = 0; i < validators.length; i++) { @@ -146,6 +167,15 @@ contract Bootstrap is ModuleManager { _installHook(hook.module, hook.data); emit ModuleInstalled(MODULE_TYPE_HOOK, hook.module); } + + // Initialize pre-validation hooks + for (uint256 i = 0; i < preValidationHooks.length; i++) { + if (preValidationHooks[i].module == address(0)) continue; + _installPreValidationHook( + preValidationHooks[i].hookType, preValidationHooks[i].module, preValidationHooks[i].data + ); + emit ModuleInstalled(preValidationHooks[i].hookType, preValidationHooks[i].module); + } } // ================================================ @@ -181,49 +211,6 @@ contract Bootstrap is ModuleManager { } } - // ================================================ - // ===== EXTERNAL VIEW HELPERS ===== - // ================================================ - - /// @notice Prepares calldata for the init function. - /// @param validators The configuration array for validator modules. - /// @param executors The configuration array for executor modules. - /// @param hook The configuration for the hook module. - /// @param fallbacks The configuration array for fallback handler modules. - /// @return initData The prepared calldata for init(). - function getInitNexusCalldata( - BootstrapConfig[] calldata validators, - BootstrapConfig[] calldata executors, - BootstrapConfig calldata hook, - BootstrapConfig[] calldata fallbacks - ) external view returns (bytes memory initData) { - initData = abi.encode(address(this), abi.encodeCall(this.init, (validators, executors, hook, fallbacks))); - } - - /// @notice Prepares calldata for the initScoped function. - /// @param validators The configuration array for validator modules. - /// @param hook The configuration for the hook module. - /// @return initData The prepared calldata for initScoped. - function getInitScopedCalldata( - BootstrapConfig[] calldata validators, - BootstrapConfig calldata hook - ) external view returns (bytes memory initData) { - initData = abi.encode(address(this), abi.encodeCall(this.initScoped, (validators, hook))); - } - - /// @notice Prepares calldata for the initWithSingleValidator function. - /// @param validator The configuration for the validator module. - /// @return initData The prepared calldata for initWithSingleValidator. - function getInitWithSingleValidatorCalldata(BootstrapConfig calldata validator) - external - view - returns (bytes memory initData) - { - initData = abi.encode( - address(this), abi.encodeCall(this.initWithSingleValidator, (IModule(validator.module), validator.data)) - ); - } - /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = 'StartaleAccountBootstrap';