diff --git a/.solhint.json b/.solhint.json index f2f7c9ac1..a706ea854 100644 --- a/.solhint.json +++ b/.solhint.json @@ -31,7 +31,8 @@ "reason-string": "error", "avoid-low-level-calls": "off", "no-inline-assembly": "off", - "no-complex-fallback": "off" + "no-complex-fallback": "off", + "gas-custom-errors": "off" }, "plugins": ["prettier"] } diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 265dc1de3..2b32ef3a4 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -21,10 +21,33 @@ import { IERC7484 } from "./interfaces/IERC7484.sol"; import { ModuleManager } from "./base/ModuleManager.sol"; import { ExecutionHelper } from "./base/ExecutionHelper.sol"; import { IValidator } from "./interfaces/modules/IValidator.sol"; -import { MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_FALLBACK, MODULE_TYPE_HOOK, MODULE_TYPE_MULTI, SUPPORTS_ERC7739 } from "./types/Constants.sol"; -import { ModeLib, ExecutionMode, ExecType, CallType, CALLTYPE_BATCH, CALLTYPE_SINGLE, CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, EXECTYPE_TRY } from "./lib/ModeLib.sol"; +import { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK, + MODULE_TYPE_MULTI, + MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, + MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, + SUPPORTS_ERC7739, + VALIDATION_SUCCESS, + VALIDATION_FAILED +} from "./types/Constants.sol"; +import { + ModeLib, + ExecutionMode, + ExecType, + CallType, + CALLTYPE_BATCH, + CALLTYPE_SINGLE, + CALLTYPE_DELEGATECALL, + EXECTYPE_DEFAULT, + EXECTYPE_TRY +} from "./lib/ModeLib.sol"; import { NonceLib } from "./lib/NonceLib.sol"; import { SentinelListLib, SENTINEL, ZERO_ADDRESS } from "sentinellist/SentinelList.sol"; +import { Initializable } from "./lib/Initializable.sol"; +import { EmergencyUninstall } from "./types/DataTypes.sol"; /// @title Nexus - Smart Account /// @notice This contract integrates various functionalities to handle modular smart accounts compliant with ERC-7579 and ERC-4337 standards. @@ -50,10 +73,15 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra event EmergencyHookUninstallRequestReset(address hook, uint256 timestamp); /// @notice Initializes the smart account with the specified entry point. - constructor(address anEntryPoint) { + constructor( + address anEntryPoint, + address defaultValidator, + bytes memory initData + ) + ModuleManager(defaultValidator, initData) + { require(address(anEntryPoint) != address(0), EntryPointCanNotBeZero()); _ENTRYPOINT = anEntryPoint; - _initModuleManager(); } /// @notice Validates a user operation against a specified validator, extracted from the operation's nonce. @@ -78,17 +106,29 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra PackedUserOperation calldata op, bytes32 userOpHash, uint256 missingAccountFunds - ) external virtual payPrefund(missingAccountFunds) onlyEntryPoint returns (uint256 validationData) { - address validator = op.nonce.getValidator(); - if (op.nonce.isModuleEnableMode()) { - PackedUserOperation memory userOp = op; - userOp.signature = _enableMode(userOpHash, op.signature); - require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); - validationData = IValidator(validator).validateUserOp(userOp, userOpHash); + ) + external + virtual + payPrefund(missingAccountFunds) + onlyEntryPoint + returns (uint256 validationData) + { + address validator; + PackedUserOperation memory userOp = op; + if (op.nonce.isDefaultValidatorMode()) { + validator = _DEFAULT_VALIDATOR; } else { + // if it is module enable mode, we need to enable the module first + // and get the cleaned signature + if (op.nonce.isModuleEnableMode()) { + userOp.signature = _enableMode(userOpHash, op.signature); + } + validator = op.nonce.getValidator(); require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); - validationData = IValidator(validator).validateUserOp(op, userOpHash); } + + (userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, userOp, missingAccountFunds); + validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } /// @notice Executes transactions in single or batch modes as specified by the execution mode. @@ -117,7 +157,14 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra function executeFromExecutor( ExecutionMode mode, bytes calldata executionCalldata - ) external payable onlyExecutorModule withHook withRegistry(msg.sender, MODULE_TYPE_EXECUTOR) returns (bytes[] memory returnData) { + ) + external + payable + onlyExecutorModule + withHook + withRegistry(msg.sender, MODULE_TYPE_EXECUTOR) + returns (bytes[] memory returnData) + { (CallType callType, ExecType execType) = mode.decodeBasic(); // check if calltype is batch or single or delegate call if (callType == CALLTYPE_SINGLE) { @@ -140,7 +187,9 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra (bool success, bytes memory innerCallRet) = address(this).delegatecall(callData); if (success) { emit Executed(userOp, innerCallRet); - } else revert ExecutionFailed(); + } else { + revert ExecutionFailed(); + } } /// @notice Installs a new module to the smart account. @@ -149,6 +198,8 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// - 2 for Executor /// - 3 for Fallback /// - 4 for Hook + /// - 8 for 1271 Prevalidation Hook + /// - 9 for 4337 Prevalidation Hook /// @param module The address of the module to install. /// @param initData Initialization data for the module. /// @dev This function can only be called by the EntryPoint or the account itself for security reasons. @@ -164,12 +215,13 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// - 2 for Executor /// - 3 for Fallback /// - 4 for Hook + /// - 8 for 1271 Prevalidation Hook + /// - 9 for 4337 Prevalidation Hook /// @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. function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external payable onlyEntryPointOrSelf withHook { require(_isModuleInstalled(moduleTypeId, module, deInitData), ModuleNotInstalled(moduleTypeId, module)); - emit ModuleUninstalled(moduleTypeId, module); if (moduleTypeId == MODULE_TYPE_VALIDATOR) { _uninstallValidator(module, deInitData); @@ -177,13 +229,28 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra _uninstallExecutor(module, deInitData); } else if (moduleTypeId == MODULE_TYPE_FALLBACK) { _uninstallFallbackHandler(module, deInitData); - } else if (moduleTypeId == MODULE_TYPE_HOOK) { - _uninstallHook(module, deInitData); + } else if ( + moduleTypeId == MODULE_TYPE_HOOK || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 + ) { + _uninstallHook(module, moduleTypeId, deInitData); } + emit ModuleUninstalled(moduleTypeId, module); } - function emergencyUninstallHook(address hook, bytes calldata deInitData) external payable onlyEntryPoint { - require(_isModuleInstalled(MODULE_TYPE_HOOK, hook, deInitData), ModuleNotInstalled(MODULE_TYPE_HOOK, hook)); + function emergencyUninstallHook(EmergencyUninstall calldata data, bytes calldata signature) external payable { + // Validate the signature + _checkEmergencyUninstallSignature(data, signature); + // Parse uninstall data + (uint256 hookType, address hook, bytes calldata deInitData) = (data.hookType, data.hook, data.deInitData); + + // Validate the hook is of a supported type and is installed + require( + hookType == MODULE_TYPE_HOOK || hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, + UnsupportedModuleType(hookType) + ); + require(_isModuleInstalled(hookType, hook, deInitData), ModuleNotInstalled(hookType, hook)); + + // Get the account storage AccountStorage storage accountStorage = _getAccountStorage(); uint256 hookTimelock = accountStorage.emergencyUninstallTimelock[hook]; @@ -198,23 +265,50 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra } else if (block.timestamp >= hookTimelock + _EMERGENCY_TIMELOCK) { // if the timelock expired, clear it and uninstall the hook accountStorage.emergencyUninstallTimelock[hook] = 0; - _uninstallHook(hook, deInitData); - emit ModuleUninstalled(MODULE_TYPE_HOOK, hook); + _uninstallHook(hook, hookType, deInitData); + emit ModuleUninstalled(hookType, hook); } else { // if the timelock is initiated but not expired, revert revert EmergencyTimeLockNotExpired(); } } + /// @notice Initializes the smart account with the specified initialization data. + /// @param initData The initialization data for the smart account. + /// @dev This function can only be called by the account itself or the proxy factory. + /// When a 7702 account is created, the first userOp should contain self-call to initialize the account. function initializeAccount(bytes calldata initData) external payable virtual { - _initModuleManager(); - (address bootstrap, bytes memory bootstrapCall) = abi.decode(initData, (address, bytes)); + require(initData.length >= 24, InvalidInitData()); + + // Protect this function to only be callable when used with the proxy factory or when + // account calls itself + if (msg.sender != address(this)) { + Initializable.requireInitializable(); + } + + address bootstrap; + bytes calldata bootstrapCall; + assembly { + bootstrap := calldataload(initData.offset) + let s := calldataload(add(initData.offset, 0x20)) + let u := add(initData.offset, s) + bootstrapCall.offset := add(u, 0x20) + bootstrapCall.length := calldataload(u) + } (bool success, ) = bootstrap.delegatecall(bootstrapCall); require(success, NexusInitializationFailed()); - require(_hasValidators(), NoValidatorInstalled()); + // _hasValidators check removed as with 7702 even if there's no validator installed, + // the account is still initializeable. + // Checking all the possible cases of whether account is initializeable or initialized + // is too gas heavy, so it's initializing party responsibility to provide valid initData. } + /// @notice Sets the registry for the smart account. + /// @param newRegistry The new registry to set. + /// @param attesters The attesters to set. + /// @param threshold The threshold to set. + /// @dev This function can only be called by the EntryPoint or the account itself. function setRegistry(IERC7484 newRegistry, address[] calldata attesters, uint8 threshold) external payable onlyEntryPointOrSelf { _configureRegistry(newRegistry, attesters, threshold); } @@ -234,11 +328,11 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra } } // else proceed with normal signature verification - // First 20 bytes of data will be validator address and rest of the bytes is complete signature. - address validator = address(bytes20(signature[0:20])); - require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); - try IValidator(validator).isValidSignatureWithSender(msg.sender, hash, signature[20:]) returns (bytes4 res) { + address validator = _handleSigValidator(address(bytes20(signature[0:20]))); + bytes memory signature_; + (hash, signature_) = _withPreValidationHook(hash, signature[20:]); + try IValidator(validator).isValidSignatureWithSender(msg.sender, hash, signature_) returns (bytes4 res) { return res; } catch { return bytes4(0xffffffff); @@ -263,12 +357,17 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @param moduleTypeId The identifier of the module type to check. /// @return True if the module type is supported, false otherwise. function supportsModule(uint256 moduleTypeId) external view virtual returns (bool) { - if (moduleTypeId == MODULE_TYPE_VALIDATOR) return true; - else if (moduleTypeId == MODULE_TYPE_EXECUTOR) return true; - else if (moduleTypeId == MODULE_TYPE_FALLBACK) return true; - else if (moduleTypeId == MODULE_TYPE_HOOK) return true; - else if (moduleTypeId == MODULE_TYPE_MULTI) return true; - else return false; + if (moduleTypeId == MODULE_TYPE_VALIDATOR || + moduleTypeId == MODULE_TYPE_EXECUTOR || + moduleTypeId == MODULE_TYPE_FALLBACK || + moduleTypeId == MODULE_TYPE_HOOK || + moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || + moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 || + moduleTypeId == MODULE_TYPE_MULTI) + { + return true; + } + return false; } /// @notice Determines if a specific execution mode is supported. @@ -278,9 +377,8 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra (CallType callType, ExecType execType) = mode.decodeBasic(); // Return true if both the call type and execution type are supported. - return - (callType == CALLTYPE_SINGLE || callType == CALLTYPE_BATCH || callType == CALLTYPE_DELEGATECALL) && - (execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY); + return (callType == CALLTYPE_SINGLE || callType == CALLTYPE_BATCH || callType == CALLTYPE_DELEGATECALL) + && (execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY); } /// @notice Determines whether a module is installed on the smart account. @@ -292,15 +390,15 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra return _isModuleInstalled(moduleTypeId, module, additionalContext); } - /// @dev EIP712 hashTypedData method. - function hashTypedData(bytes32 structHash) external view returns (bytes32) { - return _hashTypedData(structHash); - } - - /// @dev EIP712 domain separator. - // solhint-disable func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparator(); + /// @notice Checks if the smart account is initialized. + /// @return True if the smart account is initialized, false otherwise. + /// @dev In case default validator is initialized, two other SLOADS from _areSentinelListsInitialized() are not checked, + /// this method should not introduce huge gas overhead. + function isInitialized() public view returns (bool) { + return ( + IValidator(_DEFAULT_VALIDATOR).isInitialized(address(this)) || + _areSentinelListsInitialized() + ); } /// Returns the account's implementation ID. @@ -340,7 +438,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// thus the account will proceed with normal signature verification /// and return 0xffffffff as a result. function checkERC7739Support(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) { - bytes4 result; + bytes4 result; unchecked { SentinelListLib.SentinelList storage validators = _getAccountStorage().validators; address next = validators.entries[SENTINEL]; @@ -358,11 +456,15 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @dev Ensures that only authorized callers can upgrade the smart contract implementation. /// This is part of the UUPS (Universal Upgradeable Proxy Standard) pattern. /// @param newImplementation The address of the new implementation to upgrade to. - function _authorizeUpgrade(address newImplementation) internal virtual override(UUPSUpgradeable) onlyEntryPointOrSelf {} + function _authorizeUpgrade(address newImplementation) internal virtual override(UUPSUpgradeable) onlyEntryPointOrSelf { + if(_amIERC7702()) { + revert ERC7702AccountCannotBeUpgradedThisWay(); + } + } /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = "Nexus"; - version = "1.0.1"; + version = "1.2.0"; } } diff --git a/contracts/base/BaseAccount.sol b/contracts/base/BaseAccount.sol index 1f15e9892..3a19e9277 100644 --- a/contracts/base/BaseAccount.sol +++ b/contracts/base/BaseAccount.sol @@ -25,7 +25,7 @@ import { IBaseAccount } from "../interfaces/base/IBaseAccount.sol"; /// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady contract BaseAccount is IBaseAccount { /// @notice Identifier for this implementation on the network - string internal constant _ACCOUNT_IMPLEMENTATION_ID = "biconomy.nexus.1.0.0"; + string internal constant _ACCOUNT_IMPLEMENTATION_ID = "biconomy.nexus.1.2.0"; /// @notice The canonical address for the ERC4337 EntryPoint contract, version 0.7. /// This address is consistent across all supported networks. diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index a59ca9b38..022058363 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -16,6 +16,7 @@ import { SentinelListLib } from "sentinellist/SentinelList.sol"; import { Storage } from "./Storage.sol"; import { IHook } from "../interfaces/modules/IHook.sol"; import { IModule } from "../interfaces/modules/IModule.sol"; +import { IPreValidationHookERC1271, IPreValidationHookERC4337 } from "../interfaces/modules/IPreValidationHook.sol"; import { IExecutor } from "../interfaces/modules/IExecutor.sol"; import { IFallback } from "../interfaces/modules/IFallback.sol"; import { IValidator } from "../interfaces/modules/IValidator.sol"; @@ -23,10 +24,25 @@ import { CallType, CALLTYPE_SINGLE, CALLTYPE_STATIC } from "../lib/ModeLib.sol"; import { ExecLib } from "../lib/ExecLib.sol"; import { LocalCallDataParserLib } from "../lib/local/LocalCallDataParserLib.sol"; import { IModuleManagerEventsAndErrors } from "../interfaces/base/IModuleManagerEventsAndErrors.sol"; -import { MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_FALLBACK, MODULE_TYPE_HOOK, MODULE_TYPE_MULTI, MODULE_ENABLE_MODE_TYPE_HASH, ERC1271_MAGICVALUE } from "../types/Constants.sol"; +import { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK, + MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, + MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, + MODULE_TYPE_MULTI, + MODULE_ENABLE_MODE_TYPE_HASH, + EMERGENCY_UNINSTALL_TYPE_HASH, + ERC1271_MAGICVALUE, + DEFAULT_VALIDATOR_FLAG +} from "../types/Constants.sol"; import { EIP712 } from "solady/utils/EIP712.sol"; import { ExcessivelySafeCall } from "excessively-safe-call/ExcessivelySafeCall.sol"; +import { PackedUserOperation } from "account-abstraction/interfaces/PackedUserOperation.sol"; import { RegistryAdapter } from "./RegistryAdapter.sol"; +import { EmergencyUninstall } from "../types/DataTypes.sol"; +import { ECDSA } from "solady/utils/ECDSA.sol"; /// @title Nexus - ModuleManager /// @notice Manages Validator, Executor, Hook, and Fallback modules within the Nexus suite, supporting @@ -41,6 +57,19 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError using LocalCallDataParserLib for bytes; using ExecLib for address; using ExcessivelySafeCall for address; + using ECDSA for bytes32; + + /// @dev The default validator address. + /// @notice To initialize the default validator, Nexus.execute(_DEFAULT_VALIDATOR.onInstall(...)) should be called. + address internal immutable _DEFAULT_VALIDATOR; + + /// @dev initData should block the implementation from being used as a Smart Account + constructor(address _defaultValidator, bytes memory _initData) { + if (!IValidator(_defaultValidator).isModuleType(MODULE_TYPE_VALIDATOR)) + revert MismatchModuleTypeId(MODULE_TYPE_VALIDATOR); + IValidator(_defaultValidator).onInstall(_initData); + _DEFAULT_VALIDATOR = _defaultValidator; + } /// @notice Ensures the message sender is a registered executor module. modifier onlyExecutorModule() virtual { @@ -61,7 +90,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } - receive() external payable {} + receive() external payable { } /// @dev Fallback function to manage incoming calls using designated handlers based on the call type. fallback(bytes calldata callData) external payable withHook returns (bytes memory) { @@ -106,7 +135,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } /// @dev Initializes the module manager by setting up default states for validators and executors. - function _initModuleManager() internal virtual { + function _initSentinelLists() internal virtual { // account module storage AccountStorage storage ams = _getAccountStorage(); ams.executors.init(); @@ -124,9 +153,17 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError (module, moduleType, moduleInitData, enableModeSignature, userOpSignature) = packedData.parseEnableModeData(); - if (!_checkEnableModeSignature(_getEnableModeDataHash(module, moduleType, userOpHash, moduleInitData), enableModeSignature)) + address enableModeSigValidator = _handleSigValidator(address(bytes20(enableModeSignature[0:20]))); + + enableModeSignature = enableModeSignature[20:]; + + if (!_checkEnableModeSignature({ + structHash: _getEnableModeDataHash(module, moduleType, userOpHash, moduleInitData), + sig: enableModeSignature, + validator: enableModeSigValidator + })) { revert EnableModeSigError(); - + } _installModule(moduleType, module, moduleInitData); } @@ -143,6 +180,9 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @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 { + if (!_areSentinelListsInitialized()) { + _initSentinelLists(); + } if (module == address(0)) revert ModuleAddressCanNotBeZero(); if (moduleTypeId == MODULE_TYPE_VALIDATOR) { _installValidator(module, initData); @@ -152,6 +192,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError _installFallbackHandler(module, initData); } else if (moduleTypeId == MODULE_TYPE_HOOK) { _installHook(module, initData); + } else if (moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337) { + _installPreValidationHook(moduleTypeId, module, initData); } else if (moduleTypeId == MODULE_TYPE_MULTI) { _multiTypeInstall(module, initData); } else { @@ -164,6 +206,9 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @param data Initialization data to configure the validator upon installation. function _installValidator(address validator, bytes calldata data) internal virtual withRegistry(validator, MODULE_TYPE_VALIDATOR) { if (!IValidator(validator).isModuleType(MODULE_TYPE_VALIDATOR)) revert MismatchModuleTypeId(MODULE_TYPE_VALIDATOR); + if (validator == _DEFAULT_VALIDATOR) { + revert DefaultValidatorAlreadyInstalled(); + } _getAccountStorage().validators.push(validator); IValidator(validator).onInstall(data); } @@ -179,9 +224,6 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // Perform the removal first validators.pop(prev, validator); - // Sentinel pointing to itself / zero means the list is empty / uninitialized, so check this after removal - // Below error is very specific to uninstalling validators. - require(_hasValidators(), CanNotRemoveLastValidator()); validator.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, disableModuleData)); } @@ -216,9 +258,14 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @dev Uninstalls a hook module, ensuring the current hook matches the one intended for uninstallation. /// @param hook The address of the hook to be uninstalled. + /// @param hookType The type of the hook to be uninstalled. /// @param data De-initialization data to configure the hook upon uninstallation. - function _uninstallHook(address hook, bytes calldata data) internal virtual { - _setHook(address(0)); + function _uninstallHook(address hook, uint256 hookType, bytes calldata data) internal virtual { + if (hookType == MODULE_TYPE_HOOK) { + _setHook(address(0)); + } else if (hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337) { + _uninstallPreValidationHook(hook, hookType, data); + } hook.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, data)); } @@ -273,6 +320,46 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError fallbackHandler.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, data[4:])); } + /// @dev Installs a pre-validation hook module, ensuring no other pre-validation hooks are installed before proceeding. + /// @param preValidationHookType The type of the pre-validation hook. + /// @param preValidationHook The address of the pre-validation hook to be installed. + /// @param data Initialization data to configure the hook upon installation. + function _installPreValidationHook( + uint256 preValidationHookType, + address preValidationHook, + bytes calldata data + ) + internal + virtual + withRegistry(preValidationHook, preValidationHookType) + { + if (!IModule(preValidationHook).isModuleType(preValidationHookType)) revert MismatchModuleTypeId(MODULE_TYPE_HOOK); + address currentPreValidationHook = _getPreValidationHook(preValidationHookType); + require(currentPreValidationHook == address(0), PrevalidationHookAlreadyInstalled(currentPreValidationHook)); + _setPreValidationHook(preValidationHookType, preValidationHook); + IModule(preValidationHook).onInstall(data); + } + + /// @dev Uninstalls a pre-validation hook module + /// @param preValidationHook The address of the pre-validation hook to be uninstalled. + /// @param hookType The type of the pre-validation hook. + /// @param data De-initialization data to configure the hook upon uninstallation. + function _uninstallPreValidationHook(address preValidationHook, uint256 hookType, bytes calldata data) internal virtual { + _setPreValidationHook(hookType, address(0)); + preValidationHook.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, data)); + } + + /// @dev Sets the current pre-validation hook in the storage to the specified address, based on the hook type. + /// @param hookType The type of the pre-validation hook. + /// @param hook The new hook address. + function _setPreValidationHook(uint256 hookType, address hook) internal virtual { + if (hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271) { + _getAccountStorage().preValidationHookERC1271 = IPreValidationHookERC1271(hook); + } else if (hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337) { + _getAccountStorage().preValidationHookERC4337 = IPreValidationHookERC4337(hook); + } + } + /// @notice Installs a module with multiple types in a single operation. /// @dev This function handles installing a multi-type module by iterating through each type and initializing it. /// The initData should include an ABI-encoded tuple of (uint[] types, bytes[] initDatas). @@ -312,25 +399,93 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError else if (theType == MODULE_TYPE_HOOK) { _installHook(module, initDatas[i]); } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INSTALL PRE-VALIDATION HOOK */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + else if (theType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || theType == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337) { + _installPreValidationHook(theType, module, initDatas[i]); + } } } + /// @notice Checks if an emergency uninstall signature is valid. + /// @param data The emergency uninstall data. + /// @param signature The signature to validate. + function _checkEmergencyUninstallSignature(EmergencyUninstall calldata data, bytes calldata signature) internal { + address validator = address(bytes20(signature[0:20])); + require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); + // Hash the data + bytes32 hash = _getEmergencyUninstallDataHash(data.hook, data.hookType, data.deInitData, data.nonce); + // Check if nonce is valid + require(!_getAccountStorage().nonces[data.nonce], InvalidNonce()); + // Mark nonce as used + _getAccountStorage().nonces[data.nonce] = true; + // Check if the signature is valid + require((IValidator(validator).isValidSignatureWithSender(address(this), hash, signature[20:]) == ERC1271_MAGICVALUE), EmergencyUninstallSigError()); + } + + /// @dev Retrieves the pre-validation hook from the storage based on the hook type. + /// @param preValidationHookType The type of the pre-validation hook. + /// @return preValidationHook The address of the pre-validation hook. + function _getPreValidationHook(uint256 preValidationHookType) internal view returns (address preValidationHook) { + preValidationHook = preValidationHookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 + ? address(_getAccountStorage().preValidationHookERC1271) + : address(_getAccountStorage().preValidationHookERC4337); + } + + /// @dev Calls the pre-validation hook for ERC-1271. + /// @param hash The hash of the user operation. + /// @param signature The signature to validate. + /// @return postHash The updated hash after the pre-validation hook. + /// @return postSig The updated signature after the pre-validation hook. + function _withPreValidationHook(bytes32 hash, bytes calldata signature) internal view virtual returns (bytes32 postHash, bytes memory postSig) { + // Get the pre-validation hook for ERC-1271 + address preValidationHook = _getPreValidationHook(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271); + // If no pre-validation hook is installed, return the original hash and signature + if (preValidationHook == address(0)) return (hash, signature); + // Otherwise, call the pre-validation hook and return the updated hash and signature + else return IPreValidationHookERC1271(preValidationHook).preValidationHookERC1271(msg.sender, hash, signature); + } + + /// @dev Calls the pre-validation hook for ERC-4337. + /// @param hash The hash of the user operation. + /// @param userOp The user operation data. + /// @param missingAccountFunds The amount of missing account funds. + /// @return postHash The updated hash after the pre-validation hook. + /// @return postSig The updated signature after the pre-validation hook. + function _withPreValidationHook( + bytes32 hash, + PackedUserOperation memory userOp, + uint256 missingAccountFunds + ) + internal + virtual + returns (bytes32 postHash, bytes memory postSig) + { + // Get the pre-validation hook for ERC-4337 + address preValidationHook = _getPreValidationHook(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337); + // If no pre-validation hook is installed, return the original hash and signature + if (preValidationHook == address(0)) return (hash, userOp.signature); + // Otherwise, call the pre-validation hook and return the updated hash and signature + else return IPreValidationHookERC4337(preValidationHook).preValidationHookERC4337(userOp, missingAccountFunds, hash); + } + /// @notice Checks if an enable mode signature is valid. /// @param structHash data hash. /// @param sig Signature. - function _checkEnableModeSignature(bytes32 structHash, bytes calldata sig) internal view returns (bool) { - address enableModeSigValidator = address(bytes20(sig[0:20])); - if (!_isValidatorInstalled(enableModeSigValidator)) { - revert ValidatorNotInstalled(enableModeSigValidator); - } + /// @param validator Validator address. + function _checkEnableModeSignature( + bytes32 structHash, + bytes calldata sig, + address validator + ) internal view returns (bool) { bytes32 eip712Digest = _hashTypedData(structHash); - // Use standard IERC-1271/ERC-7739 interface. // Even if the validator doesn't support 7739 under the hood, it is still secure, // as eip712digest is already built based on 712Domain of this Smart Account // This interface should always be exposed by validators as per ERC-7579 - try IValidator(enableModeSigValidator).isValidSignatureWithSender(address(this), eip712Digest, sig[20:]) returns (bytes4 res) { - return res == ERC1271_MAGICVALUE; + try IValidator(validator).isValidSignatureWithSender(address(this), eip712Digest, sig) returns (bytes4 res) { + return res == ERC1271_MAGICVALUE; } catch { return false; } @@ -346,6 +501,16 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError return keccak256(abi.encode(MODULE_ENABLE_MODE_TYPE_HASH, module, moduleType, userOpHash, keccak256(initData))); } + /// @notice Builds the emergency uninstall data hash as per eip712 + /// @param hookType Type of the hook (4 for Hook, 8 for ERC-1271 Prevalidation Hook, 9 for ERC-4337 Prevalidation Hook) + /// @param hook address of the hook being uninstalled + /// @param data De-initialization data to configure the hook upon uninstallation. + /// @param nonce Unique nonce for the operation + /// @return structHash data hash + function _getEmergencyUninstallDataHash(address hook, uint256 hookType, bytes calldata data, uint256 nonce) internal view returns (bytes32) { + return _hashTypedData(keccak256(abi.encode(EMERGENCY_UNINSTALL_TYPE_HASH, hook, hookType, keccak256(data), nonce))); + } + /// @notice Checks if a module is installed on the smart account. /// @param moduleTypeId The module type ID. /// @param module The module address. @@ -367,11 +532,22 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError return _isFallbackHandlerInstalled(selector, module); } else if (moduleTypeId == MODULE_TYPE_HOOK) { return _isHookInstalled(module); + } else if (moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337) { + return _getPreValidationHook(moduleTypeId) == module; } else { return false; } } + /// @dev Checks if the validator list is already initialized. + /// In theory it doesn't 100% mean there is a validator or executor installed. + /// Use below functions to check for validators and executors. + function _areSentinelListsInitialized() internal view virtual returns (bool) { + // account module storage + AccountStorage storage ams = _getAccountStorage(); + return ams.validators.alreadyInitialized() && ams.executors.alreadyInitialized(); + } + /// @dev Checks if a fallback handler is set for a given selector. /// @param selector The function selector to check. /// @return True if a fallback handler is set, otherwise false. @@ -396,14 +572,6 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError return _getAccountStorage().validators.contains(validator); } - /// @dev Checks if there is at least one validator installed. - /// @return True if there is at least one validator, otherwise false. - function _hasValidators() internal view returns (bool) { - return - _getAccountStorage().validators.getNext(address(0x01)) != address(0x01) && - _getAccountStorage().validators.getNext(address(0x01)) != address(0x00); - } - /// @dev Checks if an executor is currently installed. /// @param executor The address of the executor to check. /// @return True if the executor is installed, otherwise false. @@ -424,6 +592,32 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError hook = address(_getAccountStorage().hook); } + /// @dev Checks if the account is an ERC7702 account + function _amIERC7702() internal view returns (bool) { + bytes32 c; + assembly { + // use extcodesize as the first cheapest check + if eq(extcodesize(address()), 23) { + // use extcodecopy to copy first 3 bytes of this contract and compare with 0xef0100 + let ptr := mload(0x40) + extcodecopy(address(),ptr, 0, 3) + c := mload(ptr) + } + // if it is not 23, we do not even check the first 3 bytes + } + return bytes3(c) == bytes3(0xef0100); + } + + /// @dev Returns the validator address to use + function _handleSigValidator(address validator) internal view returns (address) { + if (validator == DEFAULT_VALIDATOR_FLAG) { + return _DEFAULT_VALIDATOR; + } else { + require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); + return validator; + } + } + function _fallback(bytes calldata callData) private returns (bytes memory result) { bool success; FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig]; @@ -478,7 +672,11 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError SentinelListLib.SentinelList storage list, address cursor, uint256 size - ) private view returns (address[] memory array, address nextCursor) { + ) + private + view + returns (address[] memory array, address nextCursor) + { (array, nextCursor) = list.getEntriesPaginated(cursor, size); } } diff --git a/contracts/factory/K1ValidatorFactory.sol b/contracts/factory/K1ValidatorFactory.sol index 2e6a237a4..b9d6713ec 100644 --- a/contracts/factory/K1ValidatorFactory.sol +++ b/contracts/factory/K1ValidatorFactory.sol @@ -12,12 +12,11 @@ pragma solidity ^0.8.27; // Nexus: A suite of contracts for Modular Smart Accounts compliant with ERC-7579 and ERC-4337, developed by Biconomy. // Learn more at https://biconomy.io. For security issues, contact: security@biconomy.io -import { LibClone } from "solady/utils/LibClone.sol"; -import { INexus } from "../interfaces/INexus.sol"; import { BootstrapLib } from "../lib/BootstrapLib.sol"; import { NexusBootstrap, BootstrapConfig } from "../utils/NexusBootstrap.sol"; import { Stakeable } from "../common/Stakeable.sol"; import { IERC7484 } from "../interfaces/IERC7484.sol"; +import { ProxyLib } from "../lib/ProxyLib.sol"; /// @title K1ValidatorFactory for Nexus Account /// @notice Manages the creation of Modular Smart Accounts compliant with ERC-7579 and ERC-4337 using a K1 validator. @@ -47,21 +46,12 @@ contract K1ValidatorFactory is Stakeable { /// @notice Error thrown when a zero address is provided for the implementation, K1 validator, or bootstrapper. error ZeroAddressNotAllowed(); - /// @notice Error thrown when an inner call fails. - error InnerCallFailed(); - /// @notice Constructor to set the immutable variables. /// @param implementation The address of the Nexus implementation to be used for all deployments. /// @param factoryOwner The address of the factory owner. /// @param k1Validator 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. - constructor( - address implementation, - address factoryOwner, - address k1Validator, - NexusBootstrap bootstrapper, - IERC7484 registry - ) Stakeable(factoryOwner) { + constructor(address implementation, address factoryOwner, address k1Validator, NexusBootstrap bootstrapper, IERC7484 registry) Stakeable(factoryOwner) { require( !(implementation == address(0) || k1Validator == address(0) || address(bootstrapper) == address(0) || factoryOwner == address(0)), ZeroAddressNotAllowed() @@ -78,28 +68,20 @@ contract K1ValidatorFactory is Stakeable { /// @param attesters The list of attesters for the Nexus. /// @param threshold The threshold for the Nexus. /// @return The address of the newly created Nexus. - function createAccount( - address eoaOwner, - uint256 index, - address[] calldata attesters, - uint8 threshold - ) external payable returns (address payable) { - // Compute the actual salt for deterministic deployment - bytes32 actualSalt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold)); - - // Deploy the Nexus contract using the computed salt - (bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt); + function createAccount(address eoaOwner, uint256 index, address[] calldata attesters, uint8 threshold) external payable returns (address payable) { + // Compute the salt for deterministic deployment + bytes32 salt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold)); // Create the validator configuration using the NexusBootstrap library BootstrapConfig memory validator = BootstrapLib.createSingleConfig(K1_VALIDATOR, abi.encodePacked(eoaOwner)); bytes memory initData = BOOTSTRAPPER.getInitNexusWithSingleValidatorCalldata(validator, REGISTRY, attesters, threshold); - // Initialize the account if it was not already deployed + // Deploy the Nexus account using the ProxyLib + (bool alreadyDeployed, address payable account) = ProxyLib.deployProxy(ACCOUNT_IMPLEMENTATION, salt, initData); if (!alreadyDeployed) { - INexus(account).initializeAccount(initData); emit AccountCreated(account, eoaOwner, index); } - return payable(account); + return account; } /// @notice Computes the expected address of a Nexus contract using the factory's deterministic deployment algorithm. @@ -113,11 +95,21 @@ contract K1ValidatorFactory is Stakeable { uint256 index, address[] calldata attesters, uint8 threshold - ) external view returns (address payable expectedAddress) { - // Compute the actual salt for deterministic deployment - bytes32 actualSalt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold)); + ) + external + view + returns (address payable expectedAddress) + { + // Compute the salt for deterministic deployment + bytes32 salt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold)); + + // Create the validator configuration using the NexusBootstrap library + BootstrapConfig memory validator = BootstrapLib.createSingleConfig(K1_VALIDATOR, abi.encodePacked(eoaOwner)); + + // Get the initialization data for the Nexus account + bytes memory initData = BOOTSTRAPPER.getInitNexusWithSingleValidatorCalldata(validator, REGISTRY, attesters, threshold); - // Predict the deterministic address using the LibClone library - expectedAddress = payable(LibClone.predictDeterministicAddressERC1967(ACCOUNT_IMPLEMENTATION, actualSalt, address(this))); + // Compute the predicted address using the ProxyLib + return ProxyLib.predictProxyAddress(ACCOUNT_IMPLEMENTATION, salt, initData); } } diff --git a/contracts/factory/NexusAccountFactory.sol b/contracts/factory/NexusAccountFactory.sol index a968f078d..980ea7f77 100644 --- a/contracts/factory/NexusAccountFactory.sol +++ b/contracts/factory/NexusAccountFactory.sol @@ -11,10 +11,10 @@ pragma solidity ^0.8.27; // ────────────────────────────────────────────────────────────────────────────── // Nexus: A suite of contracts for Modular Smart Accounts compliant with ERC-7579 and ERC-4337, developed by Biconomy. // Learn more at https://biconomy.io. To report security issues, please contact us at: security@biconomy.io -import { LibClone } from "solady/utils/LibClone.sol"; -import { INexus } from "../interfaces/INexus.sol"; + import { Stakeable } from "../common/Stakeable.sol"; import { INexusFactory } from "../interfaces/factory/INexusFactory.sol"; +import { ProxyLib } from "../lib/ProxyLib.sol"; /// @title Nexus Account Factory /// @notice Manages the creation of Modular Smart Accounts compliant with ERC-7579 and ERC-4337 using a factory pattern. @@ -42,17 +42,12 @@ contract NexusAccountFactory is Stakeable, INexusFactory { /// @param salt Unique salt for the Smart Account creation. /// @return The address of the newly created Nexus account. function createAccount(bytes calldata initData, bytes32 salt) external payable override returns (address payable) { - // Compute the actual salt for deterministic deployment - bytes32 actualSalt = keccak256(abi.encodePacked(initData, salt)); - - // Deploy the account using the deterministic address - (bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt); - + // Deploy the Nexus account using the ProxyLib + (bool alreadyDeployed, address payable account) = ProxyLib.deployProxy(ACCOUNT_IMPLEMENTATION, salt, initData); if (!alreadyDeployed) { - INexus(account).initializeAccount(initData); emit AccountCreated(account, initData, salt); } - return payable(account); + return account; } /// @notice Computes the expected address of a Nexus contract using the factory's deterministic deployment algorithm. @@ -60,8 +55,7 @@ contract NexusAccountFactory is Stakeable, INexusFactory { /// @param salt - Unique salt for the Smart Account creation. /// @return expectedAddress The expected address at which the Nexus contract will be deployed if the provided parameters are used. function computeAccountAddress(bytes calldata initData, bytes32 salt) external view override returns (address payable expectedAddress) { - // Compute the actual salt for deterministic deployment - bytes32 actualSalt = keccak256(abi.encodePacked(initData, salt)); - expectedAddress = payable(LibClone.predictDeterministicAddressERC1967(ACCOUNT_IMPLEMENTATION, actualSalt, address(this))); + // Return the expected address of the Nexus account using the provided initialization data and salt + return ProxyLib.predictProxyAddress(ACCOUNT_IMPLEMENTATION, salt, initData); } } diff --git a/contracts/factory/RegistryFactory.sol b/contracts/factory/RegistryFactory.sol index f4e8b606c..b22a16b24 100644 --- a/contracts/factory/RegistryFactory.sol +++ b/contracts/factory/RegistryFactory.sol @@ -12,15 +12,14 @@ pragma solidity ^0.8.27; // Nexus: A suite of contracts for Modular Smart Accounts compliant with ERC-7579 and ERC-4337, developed by Biconomy. // Learn more at https://biconomy.io. To report security issues, please contact us at: security@biconomy.io -import { LibClone } from "solady/utils/LibClone.sol"; import { LibSort } from "solady/utils/LibSort.sol"; import { BytesLib } from "../lib/BytesLib.sol"; -import { INexus } from "../interfaces/INexus.sol"; import { BootstrapConfig } from "../utils/NexusBootstrap.sol"; import { Stakeable } from "../common/Stakeable.sol"; import { IERC7484 } from "../interfaces/IERC7484.sol"; import { INexusFactory } from "../interfaces/factory/INexusFactory.sol"; import { MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_FALLBACK, MODULE_TYPE_HOOK } from "../types/Constants.sol"; +import { ProxyLib } from "../lib/ProxyLib.sol"; /// @title RegistryFactory /// @notice Factory for creating Nexus accounts with whitelisted modules. Ensures compliance with ERC-7579 and ERC-4337 standards. @@ -113,15 +112,8 @@ contract RegistryFactory is Stakeable, INexusFactory { // Ensure that the initData is structured for the expected NexusBootstrap.initNexus or similar method. // This step is crucial for ensuring the proper initialization of the Nexus smart account. bytes memory innerData = BytesLib.slice(callData, 4, callData.length - 4); - ( - BootstrapConfig[] memory validators, - BootstrapConfig[] memory executors, - BootstrapConfig memory hook, - BootstrapConfig[] memory fallbacks, - , - , - - ) = abi.decode(innerData, (BootstrapConfig[], BootstrapConfig[], BootstrapConfig, BootstrapConfig[], address, address[], uint8)); + (BootstrapConfig[] memory validators, BootstrapConfig[] memory executors, BootstrapConfig memory hook, BootstrapConfig[] memory fallbacks,,,) = + abi.decode(innerData, (BootstrapConfig[], BootstrapConfig[], BootstrapConfig, BootstrapConfig[], address, address[], uint8)); // Ensure that all specified modules are whitelisted and allowed for the account. for (uint256 i = 0; i < validators.length; i++) { @@ -138,19 +130,12 @@ contract RegistryFactory is Stakeable, INexusFactory { require(_isModuleAllowed(fallbacks[i].module, MODULE_TYPE_FALLBACK), ModuleNotWhitelisted(fallbacks[i].module)); } - // Compute the actual salt for deterministic deployment - bytes32 actualSalt = keccak256(abi.encodePacked(initData, salt)); - - // Deploy the account using the deterministic address - (bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt); - + // Deploy the Nexus account using the ProxyLib + (bool alreadyDeployed, address payable account) = ProxyLib.deployProxy(ACCOUNT_IMPLEMENTATION, salt, initData); if (!alreadyDeployed) { - // Initialize the Nexus account using the provided initialization data - INexus(account).initializeAccount(initData); emit AccountCreated(account, initData, salt); } - - return payable(account); + return account; } /// @notice Computes the expected address of a Nexus contract using the factory's deterministic deployment algorithm. @@ -158,9 +143,7 @@ contract RegistryFactory is Stakeable, INexusFactory { /// @param salt - Unique salt for the Smart Account creation. /// @return expectedAddress The expected address at which the Nexus contract will be deployed if the provided parameters are used. function computeAccountAddress(bytes calldata initData, bytes32 salt) external view override returns (address payable expectedAddress) { - // Compute the actual salt for deterministic deployment - bytes32 actualSalt = keccak256(abi.encodePacked(initData, salt)); - expectedAddress = payable(LibClone.predictDeterministicAddressERC1967(ACCOUNT_IMPLEMENTATION, actualSalt, address(this))); + return ProxyLib.predictProxyAddress(ACCOUNT_IMPLEMENTATION, salt, initData); } function getAttesters() public view returns (address[] memory) { diff --git a/contracts/interfaces/INexusEventsAndErrors.sol b/contracts/interfaces/INexusEventsAndErrors.sol index d44beb968..23d8657bd 100644 --- a/contracts/interfaces/INexusEventsAndErrors.sol +++ b/contracts/interfaces/INexusEventsAndErrors.sol @@ -51,4 +51,10 @@ interface INexusEventsAndErrors { /// @notice Error thrown when attempted to emergency-uninstall a hook error EmergencyTimeLockNotExpired(); + + /// @notice Error thrown when attempted to upgrade an ERC7702 account via UUPS proxy upgrade mechanism + error ERC7702AccountCannotBeUpgradedThisWay(); + + /// @notice Error thrown when the provided initData is invalid. + error InvalidInitData(); } diff --git a/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol b/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol index 77ddc271f..5523f56ff 100644 --- a/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol +++ b/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol @@ -66,6 +66,9 @@ interface IModuleManagerEventsAndErrors { /// @dev Thrown when there is an attempt to install a hook while another is already installed. error HookAlreadyInstalled(address currentHook); + /// @dev Thrown when there is an attempt to install a PreValidationHook while another is already installed. + error PrevalidationHookAlreadyInstalled(address currentPreValidationHook); + /// @dev Thrown when there is an attempt to install a fallback handler for a selector already having one. error FallbackAlreadyInstalledForSelector(bytes4 selector); @@ -84,6 +87,12 @@ interface IModuleManagerEventsAndErrors { /// @dev Thrown when unable to validate Module Enable Mode signature error EnableModeSigError(); + /// @dev Thrown when unable to validate Emergency Uninstall signature + error EmergencyUninstallSigError(); + + /// @notice Error thrown when an invalid nonce is used + error InvalidNonce(); + /// Error thrown when account installs/uninstalls module with mismatched input `moduleTypeId` error MismatchModuleTypeId(uint256 moduleTypeId); @@ -96,4 +105,7 @@ interface IModuleManagerEventsAndErrors { /// @notice Error thrown when an execution with an unsupported CallType was made. /// @param callType The unsupported call type. error UnsupportedCallType(CallType callType); + + /// @notice Error thrown when the default validator is already installed. + error DefaultValidatorAlreadyInstalled(); } diff --git a/contracts/interfaces/base/IStorage.sol b/contracts/interfaces/base/IStorage.sol index b5ccb8c22..5b6808855 100644 --- a/contracts/interfaces/base/IStorage.sol +++ b/contracts/interfaces/base/IStorage.sol @@ -13,7 +13,7 @@ pragma solidity ^0.8.27; // Learn more at https://biconomy.io. To report security issues, please contact us at: security@biconomy.io import { SentinelListLib } from "sentinellist/SentinelList.sol"; - +import { IPreValidationHookERC1271, IPreValidationHookERC4337 } from "../modules/IPreValidationHook.sol"; import { IHook } from "../modules/IHook.sol"; import { CallType } from "../../lib/ModeLib.sol"; @@ -31,16 +31,29 @@ import { CallType } from "../../lib/ModeLib.sol"; interface IStorage { /// @notice Struct storing validators and executors using Sentinel lists, and fallback handlers via mapping. struct AccountStorage { - SentinelListLib.SentinelList validators; ///< List of validators, initialized upon contract deployment. - SentinelListLib.SentinelList executors; ///< List of executors, similarly initialized. - mapping(bytes4 => FallbackHandler) fallbacks; ///< Mapping of selectors to their respective fallback handlers. - IHook hook; ///< Current hook module associated with this account. - mapping(address hook => uint256) emergencyUninstallTimelock; ///< Mapping of hooks to requested timelocks. + ///< List of validators, initialized upon contract deployment. + SentinelListLib.SentinelList validators; + ///< List of executors, similarly initialized. + SentinelListLib.SentinelList executors; + ///< Mapping of selectors to their respective fallback handlers. + mapping(bytes4 => FallbackHandler) fallbacks; + ///< Current hook module associated with this account. + IHook hook; + ///< Mapping of hooks to requested timelocks. + mapping(address hook => uint256) emergencyUninstallTimelock; + ///< PreValidation hook for validateUserOp + IPreValidationHookERC4337 preValidationHookERC4337; + ///< PreValidation hook for isValidSignature + IPreValidationHookERC1271 preValidationHookERC1271; + ///< Mapping of used nonces for replay protection. + mapping(uint256 => bool) nonces; } /// @notice Defines a fallback handler with an associated handler address and a call type. struct FallbackHandler { - address handler; ///< The address of the fallback function handler. - CallType calltype; ///< The type of call this handler supports (e.g., static or call). + ///< The address of the fallback function handler. + address handler; + ///< The type of call this handler supports (e.g., static or call). + CallType calltype; } } diff --git a/contracts/interfaces/modules/IPreValidationHook.sol b/contracts/interfaces/modules/IPreValidationHook.sol new file mode 100644 index 000000000..b9b1b6b6d --- /dev/null +++ b/contracts/interfaces/modules/IPreValidationHook.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { PackedUserOperation } from "account-abstraction/interfaces/PackedUserOperation.sol"; +import { IModule } from "./IModule.sol"; + +/// @title Nexus - IPreValidationHookERC1271 Interface +/// @notice Defines the interface for ERC-1271 pre-validation hooks +interface IPreValidationHookERC1271 is IModule { + /// @notice Performs pre-validation checks for isValidSignature + /// @dev This method is called before the validation of a signature on a validator within isValidSignature + /// @param sender The original sender of the request + /// @param hash The hash of signed data + /// @param data The signature data to validate + /// @return hookHash The hash after applying the pre-validation hook + /// @return hookSignature The signature after applying the pre-validation hook + function preValidationHookERC1271(address sender, bytes32 hash, bytes calldata data) external view returns (bytes32 hookHash, bytes memory hookSignature); +} + +/// @title Nexus - IPreValidationHookERC4337 Interface +/// @notice Defines the interface for ERC-4337 pre-validation hooks +interface IPreValidationHookERC4337 is IModule { + /// @notice Performs pre-validation checks for user operations + /// @dev This method is called before the validation of a user operation + /// @param userOp The user operation to be validated + /// @param missingAccountFunds The amount of funds missing in the account + /// @param userOpHash The hash of the user operation data + /// @return hookHash The hash after applying the pre-validation hook + /// @return hookSignature The signature after applying the pre-validation hook + function preValidationHookERC4337( + PackedUserOperation calldata userOp, + uint256 missingAccountFunds, + bytes32 userOpHash + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature); +} diff --git a/contracts/lib/ExecLib.sol b/contracts/lib/ExecLib.sol index 1d4b4630f..16c84df46 100644 --- a/contracts/lib/ExecLib.sol +++ b/contracts/lib/ExecLib.sol @@ -29,20 +29,42 @@ library ExecLib { } } - function decodeBatch(bytes calldata callData) internal pure returns (Execution[] calldata executionBatch) { - /* - * Batch Call Calldata Layout - * Offset (in bytes) | Length (in bytes) | Contents - * 0x0 | 0x4 | bytes4 function selector - * 0x4 | - | - abi.encode(IERC7579Execution.Execution[]) - */ - assembly ("memory-safe") { - let dataPointer := add(callData.offset, calldataload(callData.offset)) - - // Extract the ERC7579 Executions - executionBatch.offset := add(dataPointer, 32) - executionBatch.length := calldataload(dataPointer) + /** + * @notice Decode a batch of `Execution` executionBatch from a `bytes` calldata. + * @dev code is copied from solady's LibERC7579.sol + * https://github.com/Vectorized/solady/blob/740812cedc9a1fc11e17cb3d4569744367dedf19/src/accounts/LibERC7579.sol#L146 + * Credits to Vectorized and the Solady Team + */ + function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { + /// @solidity memory-safe-assembly + assembly { + let u := calldataload(executionCalldata.offset) + let s := add(executionCalldata.offset, u) + let e := sub(add(executionCalldata.offset, executionCalldata.length), 0x20) + executionBatch.offset := add(s, 0x20) + executionBatch.length := calldataload(s) + if or(shr(64, u), gt(add(s, shl(5, executionBatch.length)), e)) { + mstore(0x00, 0xba597e7e) // `DecodingError()`. + revert(0x1c, 0x04) + } + if executionBatch.length { + // Perform bounds checks on the decoded `executionBatch`. + // Loop runs out-of-gas if `executionBatch.length` is big enough to cause overflows. + for { let i := executionBatch.length } 1 { } { + i := sub(i, 1) + let p := calldataload(add(executionBatch.offset, shl(5, i))) + let c := add(executionBatch.offset, p) + let q := calldataload(add(c, 0x40)) + let o := add(c, q) + // forgefmt: disable-next-item + if or(shr(64, or(calldataload(o), or(p, q))), + or(gt(add(c, 0x40), e), gt(add(o, calldataload(o)), e))) { + mstore(0x00, 0xba597e7e) // `DecodingError()`. + revert(0x1c, 0x04) + } + if iszero(i) { break } + } + } } } diff --git a/contracts/lib/Initializable.sol b/contracts/lib/Initializable.sol new file mode 100644 index 000000000..6de11825b --- /dev/null +++ b/contracts/lib/Initializable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +// keccak256(abi.encode(uint256(keccak256("initializable.transient.Nexus")) - 1)) & ~bytes32(uint256(0xff)); +bytes32 constant INIT_SLOT = 0x90b772c2cb8a51aa7a8a65fc23543c6d022d5b3f8e2b92eed79fba7eef829300; + +/// @title Initializable +/// @dev This library provides a way to set a transient flag on a contract to ensure that it is only initialized during the +/// constructor execution. This is useful to prevent a contract from being initialized multiple times. +library Initializable { + /// @dev Thrown when an attempt to initialize an already initialized contract is made + error NotInitializable(); + + /// @dev Sets the initializable flag in the transient storage slot to true + function setInitializable() internal { + bytes32 slot = INIT_SLOT; + assembly { + tstore(slot, 0x01) + } + } + + /// @dev Checks if the initializable flag is set in the transient storage slot, reverts with NotInitializable if not + function requireInitializable() internal view { + bytes32 slot = INIT_SLOT; + // Load the current value from the slot, revert if 0 + assembly { + let isInitializable := tload(slot) + if iszero(isInitializable) { + mstore(0x0, 0xaed59595) // NotInitializable() + revert(0x1c, 0x04) + } + } + } +} diff --git a/contracts/lib/NonceLib.sol b/contracts/lib/NonceLib.sol index bbff75092..faf48df6d 100644 --- a/contracts/lib/NonceLib.sol +++ b/contracts/lib/NonceLib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.27; -import { MODE_MODULE_ENABLE } from "../types/Constants.sol"; +import { MODE_MODULE_ENABLE, MODE_DEFAULT_VALIDATOR } from "../types/Constants.sol"; /** Nonce structure @@ -27,4 +27,14 @@ library NonceLib { res := eq(shl(248, vmode), MODE_MODULE_ENABLE) } } + + /// @dev Detects if Validaton Mode is Default Validator Mode + /// @param nonce The nonce + /// @return res boolean result, true if it is the Default Validator Mode + function isDefaultValidatorMode(uint256 nonce) internal pure returns (bool res) { + assembly { + let vmode := byte(3, nonce) + res := eq(shl(248, vmode), MODE_DEFAULT_VALIDATOR) + } + } } diff --git a/contracts/lib/ProxyLib.sol b/contracts/lib/ProxyLib.sol new file mode 100644 index 000000000..7889d1ef6 --- /dev/null +++ b/contracts/lib/ProxyLib.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { NexusProxy } from "../utils/NexusProxy.sol"; +import { INexus } from "../interfaces/INexus.sol"; + +/// @title ProxyLib +/// @notice A library for deploying NexusProxy contracts +library ProxyLib { + /// @notice Error thrown when ETH transfer fails. + error EthTransferFailed(); + + /// @notice Deploys a new NexusProxy contract, returning the address of the new contract, if the contract is already deployed, + /// the msg.value will be forwarded to the existing contract. + /// @param implementation The address of the implementation contract. + /// @param salt The salt used for the contract creation. + /// @param initData The initialization data for the implementation contract. + /// @return alreadyDeployed A boolean indicating if the contract was already deployed. + /// @return account The address of the new contract or the existing contract. + function deployProxy(address implementation, bytes32 salt, bytes memory initData) internal returns (bool alreadyDeployed, address payable account) { + // Check if the contract is already deployed + account = predictProxyAddress(implementation, salt, initData); + alreadyDeployed = account.code.length > 0; + // Deploy a new contract if it is not already deployed + if (!alreadyDeployed) { + // Deploy the contract + new NexusProxy{ salt: salt, value: msg.value }(implementation, abi.encodeCall(INexus.initializeAccount, initData)); + } else { + // Forward the value to the existing contract + (bool success,) = account.call{ value: msg.value }(""); + require(success, EthTransferFailed()); + } + } + + /// @notice Predicts the address of a NexusProxy contract. + /// @param implementation The address of the implementation contract. + /// @param salt The salt used for the contract creation. + /// @param initData The initialization data for the implementation contract. + /// @return predictedAddress The predicted address of the new contract. + function predictProxyAddress(address implementation, bytes32 salt, bytes memory initData) internal view returns (address payable predictedAddress) { + // Get the init code hash + bytes32 initCodeHash = + keccak256(abi.encodePacked(type(NexusProxy).creationCode, abi.encode(implementation, abi.encodeCall(INexus.initializeAccount, initData)))); + + // Compute the predicted address + predictedAddress = payable(address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash)))))); + } +} diff --git a/contracts/mocks/ExposedNexus.sol b/contracts/mocks/ExposedNexus.sol new file mode 100644 index 000000000..60d009958 --- /dev/null +++ b/contracts/mocks/ExposedNexus.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { Nexus } from "contracts/Nexus.sol"; +import { INexus } from "contracts/interfaces/INexus.sol"; +interface IExposedNexus is INexus { + function amIERC7702() external view returns (bool); +} + +contract ExposedNexus is Nexus, IExposedNexus { + + constructor(address anEntryPoint, address defaultValidator, bytes memory initData) + Nexus(anEntryPoint, defaultValidator, initData) {} + + function amIERC7702() external view returns (bool) { + return _amIERC7702(); + } +} \ No newline at end of file diff --git a/contracts/mocks/Mock7739PreValidationHook.sol b/contracts/mocks/Mock7739PreValidationHook.sol new file mode 100644 index 000000000..08a27004f --- /dev/null +++ b/contracts/mocks/Mock7739PreValidationHook.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IPreValidationHookERC1271 } from "../interfaces/modules/IPreValidationHook.sol"; +import { MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 } from "../types/Constants.sol"; +import { EIP712 } from "solady/utils/EIP712.sol"; +import { PackedUserOperation } from "account-abstraction/interfaces/PackedUserOperation.sol"; + +contract Mock7739PreValidationHook is IPreValidationHookERC1271 { + bytes32 internal constant _PERSONAL_SIGN_TYPEHASH = 0x983e65e5148e570cd828ead231ee759a8d7958721a768f93bc4483ba005c32de; + bytes32 internal constant _DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + address public immutable prevalidationHookMultiplexer; + + constructor(address _prevalidationHookMultiplexer) { + prevalidationHookMultiplexer = _prevalidationHookMultiplexer; + } + + function _msgSender() internal view returns (address sender) { + if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) { + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function isTrustedForwarder(address forwarder) public view returns (bool) { + return forwarder == prevalidationHookMultiplexer; + } + + function preValidationHookERC1271(address, bytes32 hash, bytes calldata data) external view returns (bytes32 hookHash, bytes memory hookSignature) { + address account = _msgSender(); + // Check flag in first byte + if (data[0] == 0x00) { + return wrapFor7739Validation(account, hash, _erc1271UnwrapSignature(data[1:])); + } + return (hash, data[1:]); + } + + function wrapFor7739Validation(address account, bytes32 hash, bytes calldata signature) internal view virtual returns (bytes32, bytes calldata) { + bytes32 t = _typedDataSignFieldsForAccount(account); + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Cache the free memory pointer. + // `c` is `contentsType.length`, which is stored in the last 2 bytes of the signature. + let c := shr(240, calldataload(add(signature.offset, sub(signature.length, 2)))) + for { } 1 { } { + let l := add(0x42, c) // Total length of appended data (32 + 32 + c + 2). + let o := add(signature.offset, sub(signature.length, l)) // Offset of appended data. + mstore(0x00, 0x1901) // Store the "\x19\x01" prefix. + calldatacopy(0x20, o, 0x40) // Copy the `APP_DOMAIN_SEPARATOR` and `contents` struct hash. + // Use the `PersonalSign` workflow if the reconstructed hash doesn't match, + // or if the appended data is invalid, i.e. + // `appendedData.length > signature.length || contentsType.length == 0`. + if or(xor(keccak256(0x1e, 0x42), hash), or(lt(signature.length, l), iszero(c))) { + t := 0 // Set `t` to 0, denoting that we need to `hash = _hashTypedData(hash)`. + mstore(t, _PERSONAL_SIGN_TYPEHASH) + mstore(0x20, hash) // Store the `prefixed`. + hash := keccak256(t, 0x40) // Compute the `PersonalSign` struct hash. + break + } + // Else, use the `TypedDataSign` workflow. + // `TypedDataSign({ContentsName} contents,bytes1 fields,...){ContentsType}`. + mstore(m, "TypedDataSign(") // Store the start of `TypedDataSign`'s type encoding. + let p := add(m, 0x0e) // Advance 14 bytes to skip "TypedDataSign(". + calldatacopy(p, add(o, 0x40), c) // Copy `contentsType` to extract `contentsName`. + // `d & 1 == 1` means that `contentsName` is invalid. + let d := shr(byte(0, mload(p)), 0x7fffffe000000000000010000000000) // Starts with `[a-z(]`. + // Store the end sentinel '(', and advance `p` until we encounter a '(' byte. + for { mstore(add(p, c), 40) } iszero(eq(byte(0, mload(p)), 40)) { p := add(p, 1) } { d := or(shr(byte(0, mload(p)), 0x120100000001), d) } // Has + // a byte in ", )\x00". + + mstore(p, " contents,bytes1 fields,string n") // Store the rest of the encoding. + mstore(add(p, 0x20), "ame,string version,uint256 chain") + mstore(add(p, 0x40), "Id,address verifyingContract,byt") + mstore(add(p, 0x60), "es32 salt,uint256[] extensions)") + p := add(p, 0x7f) + calldatacopy(p, add(o, 0x40), c) // Copy `contentsType`. + // Fill in the missing fields of the `TypedDataSign`. + calldatacopy(t, o, 0x40) // Copy the `contents` struct hash to `add(t, 0x20)`. + mstore(t, keccak256(m, sub(add(p, c), m))) // Store `typedDataSignTypehash`. + // The "\x19\x01" prefix is already at 0x00. + // `APP_DOMAIN_SEPARATOR` is already at 0x20. + mstore(0x40, keccak256(t, 0x120)) // `hashStruct(typedDataSign)`. + // Compute the final hash, corrupted if `contentsName` is invalid. + hash := keccak256(0x1e, add(0x42, and(1, d))) + signature.length := sub(signature.length, l) // Truncate the signature. + break + } + mstore(0x40, m) // Restore the free memory pointer. + } + if (t == bytes32(0)) hash = _hashTypedDataForAccount(account, hash); // `PersonalSign` workflow. + return (hash, signature); + } + + /// @dev Unwraps and returns the signature. + function _erc1271UnwrapSignature(bytes calldata signature) internal view virtual returns (bytes calldata result) { + result = signature; + /// @solidity memory-safe-assembly + assembly { + // Unwraps the ERC6492 wrapper if it exists. + // See: https://eips.ethereum.org/EIPS/eip-6492 + if eq( + calldataload(add(result.offset, sub(result.length, 0x20))), + mul(0x6492, div(not(mload(0x60)), 0xffff)) // `0x6492...6492`. + ) { + let o := add(result.offset, calldataload(add(result.offset, 0x40))) + result.length := calldataload(o) + result.offset := add(o, 0x20) + } + } + } + + /// @dev For use in `_erc1271IsValidSignatureViaNestedEIP712`, + function _typedDataSignFieldsForAccount(address account) private view returns (bytes32 m) { + (bytes1 fields, string memory name, string memory version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] memory extensions) = + EIP712(account).eip712Domain(); + /// @solidity memory-safe-assembly + assembly { + m := mload(0x40) // Grab the free memory pointer. + mstore(0x40, add(m, 0x120)) // Allocate the memory. + // Skip 2 words for the `typedDataSignTypehash` and `contents` struct hash. + mstore(add(m, 0x40), shl(248, byte(0, fields))) + mstore(add(m, 0x60), keccak256(add(name, 0x20), mload(name))) + mstore(add(m, 0x80), keccak256(add(version, 0x20), mload(version))) + mstore(add(m, 0xa0), chainId) + mstore(add(m, 0xc0), shr(96, shl(96, verifyingContract))) + mstore(add(m, 0xe0), salt) + mstore(add(m, 0x100), keccak256(add(extensions, 0x20), shl(5, mload(extensions)))) + } + } + + /// @notice Hashes typed data according to eip-712 + /// Uses account's domain separator + /// @param account the smart account, who's domain separator will be used + /// @param structHash the typed data struct hash + function _hashTypedDataForAccount(address account, bytes32 structHash) private view returns (bytes32 digest) { + ( + , + /*bytes1 fields*/ + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, /*bytes32 salt*/ /*uint256[] memory extensions*/ + , + ) = EIP712(account).eip712Domain(); + + /// @solidity memory-safe-assembly + assembly { + //Rebuild domain separator out of 712 domain + let m := mload(0x40) // Load the free memory pointer. + mstore(m, _DOMAIN_TYPEHASH) + mstore(add(m, 0x20), keccak256(add(name, 0x20), mload(name))) // Name hash. + mstore(add(m, 0x40), keccak256(add(version, 0x20), mload(version))) // Version hash. + mstore(add(m, 0x60), chainId) + mstore(add(m, 0x80), verifyingContract) + digest := keccak256(m, 0xa0) //domain separator + + // Hash typed data + mstore(0x00, 0x1901000000000000) // Store "\x19\x01". + mstore(0x1a, digest) // Store the domain separator. + mstore(0x3a, structHash) // Store the struct hash. + digest := keccak256(0x18, 0x42) + // Restore the part of the free memory slot that was overwritten. + mstore(0x3a, 0) + } + } + + function onInstall(bytes calldata data) external override { } + + function onUninstall(bytes calldata data) external override { } + + function isModuleType(uint256 moduleTypeId) external pure returns (bool) { + return moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337; + } + + function isInitialized(address) external pure returns (bool) { + return true; + } +} diff --git a/contracts/mocks/MockAccountLocker.sol b/contracts/mocks/MockAccountLocker.sol new file mode 100644 index 000000000..6c091549f --- /dev/null +++ b/contracts/mocks/MockAccountLocker.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IHook } from "../interfaces/modules/IHook.sol"; +import { MODULE_TYPE_HOOK } from "../types/Constants.sol"; + +contract MockAccountLocker is IHook { + mapping(address => mapping(address => uint256)) lockedAmount; + + function getLockedAmount(address account, address token) external view returns (uint256) { + return lockedAmount[token][account]; + } + + function setLockedAmount(address account, address token, uint256 amount) external { + lockedAmount[token][account] = amount; + } + + function onInstall(bytes calldata data) external override { } + + function onUninstall(bytes calldata data) external override { } + + function isModuleType(uint256 moduleTypeId) external pure override returns (bool) { + return moduleTypeId == MODULE_TYPE_HOOK; + } + + function isInitialized(address smartAccount) external view override returns (bool) { } + + function preCheck(address msgSender, uint256 msgValue, bytes calldata msgData) external override returns (bytes memory hookData) { } + + function postCheck(bytes calldata hookData) external override { } +} diff --git a/contracts/mocks/MockExecutor.sol b/contracts/mocks/MockExecutor.sol index 7efcb0250..898a974c6 100644 --- a/contracts/mocks/MockExecutor.sol +++ b/contracts/mocks/MockExecutor.sol @@ -54,15 +54,18 @@ contract MockExecutor is IExecutor { address target, uint256 value, bytes calldata callData - ) external returns (bytes[] memory returnData) { + ) external returns (bytes[] memory) { (CallType callType, ) = ModeLib.decodeBasic(mode); bytes memory executionCallData; if (callType == CALLTYPE_SINGLE) { executionCallData = ExecLib.encodeSingle(target, value, callData); + return account.executeFromExecutor(mode, executionCallData); } else if (callType == CALLTYPE_BATCH) { - Execution[] memory execution = new Execution[](1); + Execution[] memory execution = new Execution[](2); execution[0] = Execution(target, 0, callData); + execution[1] = Execution(address(this), 0, executionCallData); executionCallData = ExecLib.encodeBatch(execution); + return account.executeFromExecutor(mode, executionCallData); } return account.executeFromExecutor(mode, ExecLib.encodeSingle(target, value, callData)); } diff --git a/contracts/mocks/MockPreValidationHook.sol b/contracts/mocks/MockPreValidationHook.sol new file mode 100644 index 000000000..48f4cab12 --- /dev/null +++ b/contracts/mocks/MockPreValidationHook.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IPreValidationHookERC1271, IPreValidationHookERC4337, PackedUserOperation } from "../interfaces/modules/IPreValidationHook.sol"; +import { EncodedModuleTypes } from "../lib/ModuleTypeLib.sol"; +import "../types/Constants.sol"; + +contract MockPreValidationHook is IPreValidationHookERC1271, IPreValidationHookERC4337 { + event PreCheckCalled(); + event HookOnInstallCalled(bytes32 dataFirstWord); + + function onInstall(bytes calldata data) external override { + if (data.length >= 0x20) { + emit HookOnInstallCalled(bytes32(data[0:32])); + } + } + + function onUninstall(bytes calldata) external override { } + + function isModuleType(uint256 moduleTypeId) external pure returns (bool) { + return moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271; + } + + function isInitialized(address) external pure returns (bool) { + return true; + } + + function preValidationHookERC1271(address, bytes32 hash, bytes calldata data) external pure returns (bytes32 hookHash, bytes memory hookSignature) { + return (hash, data); + } + + function preValidationHookERC4337( + PackedUserOperation calldata userOp, + uint256, + bytes32 userOpHash + ) + external + pure + returns (bytes32 hookHash, bytes memory hookSignature) + { + return (userOpHash, userOp.signature); + } +} diff --git a/contracts/mocks/MockPreValidationHookMultiplexer.sol b/contracts/mocks/MockPreValidationHookMultiplexer.sol new file mode 100644 index 000000000..b5d6eff2d --- /dev/null +++ b/contracts/mocks/MockPreValidationHookMultiplexer.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IPreValidationHookERC1271, IPreValidationHookERC4337, PackedUserOperation, IModule } from "../interfaces/modules/IPreValidationHook.sol"; +import { MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 } from "../types/Constants.sol"; + +contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreValidationHookERC4337 { + struct HookConfig { + address[] hooks; + bool initialized; + } + + // Separate configurations for each hook type + mapping(uint256 hookType => mapping(address account => HookConfig)) internal accountConfig; + + error AlreadyInitialized(uint256 hookType); + error NotInitialized(uint256 hookType); + error InvalidHookType(uint256 hookType); + error OnInstallFailed(address hook); + error OnUninstallFailed(address hook); + error SubHookFailed(address hook); + + function onInstall(bytes calldata data) external { + (uint256 moduleType, address[] memory hooks, bytes[] memory hookData) = abi.decode(data, (uint256, address[], bytes[])); + + if (!isValidModuleType(moduleType)) { + revert InvalidHookType(moduleType); + } + + if (accountConfig[moduleType][msg.sender].initialized) { + revert AlreadyInitialized(moduleType); + } + + accountConfig[moduleType][msg.sender].hooks = hooks; + accountConfig[moduleType][msg.sender].initialized = true; + + for (uint256 i = 0; i < hooks.length; i++) { + bytes memory subHookOnInstallCalldata = abi.encodeCall(IModule.onInstall, hookData[i]); + (bool success,) = hooks[i].call(abi.encodePacked(subHookOnInstallCalldata, msg.sender)); + require(success, OnInstallFailed(hooks[i])); + } + } + + function onUninstall(bytes calldata data) external { + (uint256 moduleType, bytes[] memory hookData) = abi.decode(data, (uint256, bytes[])); + + if (!isValidModuleType(moduleType)) { + revert InvalidHookType(moduleType); + } + + address[] memory hooks = accountConfig[moduleType][msg.sender].hooks; + + delete accountConfig[moduleType][msg.sender]; + + for (uint256 i = 0; i < hooks.length; i++) { + bytes memory subHookOnUninstallCalldata = abi.encodeCall(IModule.onUninstall, hookData[i]); + (bool success,) = hooks[i].call(abi.encodePacked(subHookOnUninstallCalldata, msg.sender)); + require(success, OnUninstallFailed(hooks[i])); + } + } + + function preValidationHookERC4337( + PackedUserOperation calldata userOp, + uint256 missingAccountFunds, + bytes32 userOpHash + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature) + { + HookConfig storage config = accountConfig[MODULE_TYPE_PREVALIDATION_HOOK_ERC4337][msg.sender]; + + if (!config.initialized) { + revert NotInitialized(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337); + } + + hookHash = userOpHash; + hookSignature = userOp.signature; + PackedUserOperation memory op = userOp; + + for (uint256 i = 0; i < config.hooks.length; i++) { + bytes memory subHookData = abi.encodeWithSelector(IPreValidationHookERC4337.preValidationHookERC4337.selector, op, missingAccountFunds, hookHash); + (bool success, bytes memory result) = config.hooks[i].staticcall(abi.encodePacked(subHookData, msg.sender)); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + (hookHash, hookSignature) = abi.decode(result, (bytes32, bytes)); + op.signature = hookSignature; + } + + return (hookHash, hookSignature); + } + + function preValidationHookERC1271( + address sender, + bytes32 hash, + bytes calldata signature + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature) + { + HookConfig storage config = accountConfig[MODULE_TYPE_PREVALIDATION_HOOK_ERC1271][msg.sender]; + + if (!config.initialized) { + revert NotInitialized(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271); + } + + hookHash = hash; + hookSignature = signature; + + for (uint256 i = 0; i < config.hooks.length; i++) { + bytes memory subHookData = abi.encodeWithSelector(IPreValidationHookERC1271.preValidationHookERC1271.selector, sender, hookHash, hookSignature); + (bool success, bytes memory result) = config.hooks[i].staticcall(abi.encodePacked(subHookData, msg.sender)); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + (hookHash, hookSignature) = abi.decode(result, (bytes32, bytes)); + } + + return (hookHash, hookSignature); + } + + function isModuleType(uint256 moduleTypeId) external pure returns (bool) { + return isValidModuleType(moduleTypeId); + } + + function isInitialized(address smartAccount) external view returns (bool) { + // Account is initialized if either hook type is initialized + return accountConfig[MODULE_TYPE_PREVALIDATION_HOOK_ERC4337][smartAccount].initialized + || accountConfig[MODULE_TYPE_PREVALIDATION_HOOK_ERC1271][smartAccount].initialized; + } + + function isHookTypeInitialized(address smartAccount, uint256 hookType) external view returns (bool) { + return accountConfig[hookType][smartAccount].initialized; + } + + function isValidModuleType(uint256 moduleTypeId) internal pure returns (bool) { + return moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271; + } +} diff --git a/contracts/mocks/MockResourceLockPreValidationHook.sol b/contracts/mocks/MockResourceLockPreValidationHook.sol new file mode 100644 index 000000000..ebc47c15e --- /dev/null +++ b/contracts/mocks/MockResourceLockPreValidationHook.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IPreValidationHookERC1271, IPreValidationHookERC4337, PackedUserOperation } from "../interfaces/modules/IPreValidationHook.sol"; +import { MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, MODULE_TYPE_HOOK, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 } from "../types/Constants.sol"; +import { EIP712 } from "solady/utils/EIP712.sol"; + +interface IAccountLocker { + function getLockedAmount(address account, address token) external view returns (uint256); +} + +interface IAccount { + function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) external view returns (bool installed); +} + +contract MockResourceLockPreValidationHook is IPreValidationHookERC4337, IPreValidationHookERC1271 { + address constant NATIVE_TOKEN = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + + /// @dev `keccak256("PersonalSign(bytes prefixed)")`. + bytes32 internal constant _PERSONAL_SIGN_TYPEHASH = 0x983e65e5148e570cd828ead231ee759a8d7958721a768f93bc4483ba005c32de; + bytes32 internal constant _DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + IAccountLocker public immutable resourceLocker; + address public immutable prevalidationHookMultiplexer; + + error InsufficientUnlockedETH(uint256 required); + error ResourceLockerNotInstalled(); + error ResourceLockerInstalled(); + error SenderIsResourceLocked(); + + constructor(address _resourceLocker, address _prevalidationHookMultiplexer) { + resourceLocker = IAccountLocker(_resourceLocker); + prevalidationHookMultiplexer = _prevalidationHookMultiplexer; + } + + function isTrustedForwarder(address forwarder) public view returns (bool) { + return forwarder == prevalidationHookMultiplexer; + } + + function _msgSender() internal view returns (address sender) { + if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) { + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + } else { + return msg.sender; + } + } + + function onInstall(bytes calldata) external view override { + address sender = _msgSender(); + require(IAccount(sender).isModuleInstalled(MODULE_TYPE_HOOK, address(resourceLocker), ""), ResourceLockerNotInstalled()); + } + + function onUninstall(bytes calldata) external view override { + address sender = _msgSender(); + require(!IAccount(sender).isModuleInstalled(MODULE_TYPE_HOOK, address(resourceLocker), ""), ResourceLockerInstalled()); + } + + function isModuleType(uint256 moduleTypeId) external pure returns (bool) { + return moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 || moduleTypeId == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271; + } + + function isInitialized(address) external pure returns (bool) { + return true; + } + + function preValidationHookERC4337( + PackedUserOperation calldata userOp, + uint256 missingAccountFunds, + bytes32 userOpHash + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature) + { + address account = _msgSender(); + require(enoughETHAvailable(account, missingAccountFunds), InsufficientUnlockedETH(missingAccountFunds)); + return (userOpHash, userOp.signature); + } + + function enoughETHAvailable(address account, uint256 requiredAmount) internal view returns (bool) { + if (requiredAmount == 0) { + return true; + } + + uint256 lockedAmount = resourceLocker.getLockedAmount(account, NATIVE_TOKEN); + uint256 unlockedAmount = address(account).balance - lockedAmount; + + return unlockedAmount >= requiredAmount; + } + + function preValidationHookERC1271( + address sender, + bytes32 hash, + bytes calldata data + ) + external + view + override + returns (bytes32 hookHash, bytes memory hookSignature) + { + address account = _msgSender(); + require(notResourceLocked(account, sender), SenderIsResourceLocked()); + return (hash, data); + } + + function notResourceLocked(address account, address sender) internal view returns (bool) { + uint256 lockedAmount = resourceLocker.getLockedAmount(account, sender); + return lockedAmount == 0; + } +} diff --git a/contracts/mocks/MockSimpleValidator.sol b/contracts/mocks/MockSimpleValidator.sol new file mode 100644 index 000000000..f12003ad1 --- /dev/null +++ b/contracts/mocks/MockSimpleValidator.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IValidator } from "../interfaces/modules/IValidator.sol"; +import { VALIDATION_SUCCESS, VALIDATION_FAILED, MODULE_TYPE_VALIDATOR } from "../types/Constants.sol"; +import { PackedUserOperation } from "account-abstraction/interfaces/PackedUserOperation.sol"; +import { ECDSA } from "solady/utils/ECDSA.sol"; + +contract MockSimpleValidator is IValidator { + using ECDSA for bytes32; + + mapping(address => address) public smartAccountOwners; + + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external view returns (uint256) { + address owner = smartAccountOwners[msg.sender]; + return verify(owner, userOpHash, userOp.signature) ? VALIDATION_SUCCESS : VALIDATION_FAILED; + } + + function isValidSignatureWithSender(address, bytes32 hash, bytes calldata signature) external view returns (bytes4) { + address owner = smartAccountOwners[msg.sender]; + return verify(owner, hash, signature) ? bytes4(0x1626ba7e) : bytes4(0xffffffff); + } + + function verify(address signer, bytes32 hash, bytes calldata signature) internal view returns (bool) { + if (signer == hash.recover(signature)) { + return true; + } + if (signer == hash.toEthSignedMessageHash().recover(signature)) { + return true; + } + return false; + } + + function onInstall(bytes calldata data) external { + smartAccountOwners[msg.sender] = address(bytes20(data)); + } + + function onUninstall(bytes calldata) external { + delete smartAccountOwners[msg.sender]; + } + + function isModuleType(uint256 moduleTypeId) external pure returns (bool) { + return moduleTypeId == MODULE_TYPE_VALIDATOR; + } + + function isInitialized(address) external pure returns (bool) { + return false; + } +} diff --git a/contracts/mocks/MockTarget.sol b/contracts/mocks/MockTarget.sol new file mode 100644 index 000000000..6e8f4b77c --- /dev/null +++ b/contracts/mocks/MockTarget.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +contract MockTarget { + uint256 public value; + + function setValue(uint256 _value) public returns (uint256) { + value = _value; + return _value; + } +} diff --git a/contracts/mocks/MockValidator.sol b/contracts/mocks/MockValidator.sol index 5a216f4f6..c9718ff51 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -14,15 +14,11 @@ contract MockValidator is ERC7739Validator { mapping(address => address) public smartAccountOwners; function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external view returns (uint256 validation) { - address owner = smartAccountOwners[msg.sender]; + address owner = getOwner(msg.sender); return _validateSignatureForOwner(owner, userOpHash, userOp.signature) ? VALIDATION_SUCCESS : VALIDATION_FAILED; } - function isValidSignatureWithSender( - address sender, - bytes32 hash, - bytes calldata signature - ) external view virtual returns (bytes4 sigValidationResult) { + function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) external view virtual returns (bytes4 sigValidationResult) { // can put additional checks based on sender here return _erc1271IsValidSignatureWithSender(sender, hash, _erc1271UnwrapSignature(signature)); } @@ -48,11 +44,9 @@ contract MockValidator is ERC7739Validator { /// module's specific internal function to validate the signature /// against credentials. function _erc1271IsValidSignatureNowCalldata(bytes32 hash, bytes calldata signature) internal view override returns (bool) { - // obtain credentials - address owner = smartAccountOwners[msg.sender]; // call custom internal function to validate the signature against credentials - return _validateSignatureForOwner(owner, hash, signature); + return _validateSignatureForOwner(getOwner(msg.sender), hash, signature); } /// @dev Returns whether the `sender` is considered safe, such @@ -63,18 +57,29 @@ contract MockValidator is ERC7739Validator { // msg.sender = Smart Account // sender = 1271 og request sender function _erc1271CallerIsSafe(address sender) internal view virtual override returns (bool) { - return (sender == 0x000000000000D9ECebf3C23529de49815Dac1c4c || // MulticallerWithSigner - sender == msg.sender); + return ( + sender == 0x000000000000D9ECebf3C23529de49815Dac1c4c // MulticallerWithSigner + || sender == msg.sender + ); + } + + /** + * Get the owner of the smart account + * @param smartAccount The address of the smart account + * @return The owner of the smart account + */ + function getOwner(address smartAccount) public view returns (address) { + address owner = smartAccountOwners[smartAccount]; + return owner == address(0) ? smartAccount : owner; } function onInstall(bytes calldata data) external { - require(IModuleManager(msg.sender).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(this), ""), "Validator is still installed"); smartAccountOwners[msg.sender] = address(bytes20(data)); } function onUninstall(bytes calldata data) external { - data; require(!IModuleManager(msg.sender).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(this), ""), "Validator is still installed"); + data; delete smartAccountOwners[msg.sender]; } @@ -90,7 +95,4 @@ contract MockValidator is ERC7739Validator { return false; } - function getOwner(address account) external view returns (address) { - return smartAccountOwners[account]; - } } diff --git a/contracts/modules/validators/K1Validator.sol b/contracts/modules/validators/K1Validator.sol index 110fcfc27..506129ca5 100644 --- a/contracts/modules/validators/K1Validator.sol +++ b/contracts/modules/validators/K1Validator.sol @@ -40,7 +40,7 @@ contract K1Validator is IValidator, ERC7739Validator { //////////////////////////////////////////////////////////////////////////*/ /// @notice Mapping of smart account addresses to their respective owner addresses - mapping(address => address) public smartAccountOwners; + mapping(address => address) internal smartAccountOwners; EnumerableSet.AddressSet private _safeSenders; @@ -53,15 +53,15 @@ contract K1Validator is IValidator, ERC7739Validator { /// @notice Error to indicate the module is already initialized error ModuleAlreadyInitialized(); - /// @notice Error to indicate that the new owner cannot be a contract address - error NewOwnerIsContract(); - /// @notice Error to indicate that the owner cannot be the zero address error OwnerCannotBeZeroAddress(); /// @notice Error to indicate that the data length is invalid error InvalidDataLength(); + /// @notice Error to indicate that the safe senders data length is invalid + error InvalidSafeSendersLength(); + /*////////////////////////////////////////////////////////////////////////// CONFIG //////////////////////////////////////////////////////////////////////////*/ @@ -76,7 +76,6 @@ contract K1Validator is IValidator, ERC7739Validator { require(!_isInitialized(msg.sender), ModuleAlreadyInitialized()); address newOwner = address(bytes20(data[:20])); require(newOwner != address(0), OwnerCannotBeZeroAddress()); - require(!_isContract(newOwner), NewOwnerIsContract()); smartAccountOwners[msg.sender] = newOwner; if (data.length > 20) { _fillSafeSenders(data[20:]); @@ -95,7 +94,6 @@ contract K1Validator is IValidator, ERC7739Validator { /// @param newOwner The address of the new owner function transferOwnership(address newOwner) external { require(newOwner != address(0), ZeroAddressNotAllowed()); - require(!_isContract(newOwner), NewOwnerIsContract()); smartAccountOwners[msg.sender] = newOwner; } @@ -179,6 +177,16 @@ contract K1Validator is IValidator, ERC7739Validator { return _validateSignatureForOwner(owner, hash, sig); } + /** + * Get the owner of the smart account + * @param smartAccount The address of the smart account + * @return The owner of the smart account + */ + function getOwner(address smartAccount) public view returns (address) { + address owner = smartAccountOwners[smartAccount]; + return owner == address(0) ? smartAccount : owner; + } + /*////////////////////////////////////////////////////////////////////////// METADATA //////////////////////////////////////////////////////////////////////////*/ @@ -212,7 +220,7 @@ contract K1Validator is IValidator, ERC7739Validator { /// @return The recovered signer address /// @notice tryRecover returns address(0) on invalid signature function _recoverSigner(bytes32 hash, bytes calldata signature) internal view returns (address) { - return hash.tryRecover(signature); + return hash.tryRecoverCalldata(signature); } /// @dev Returns whether the `hash` and `signature` are valid. @@ -221,7 +229,7 @@ contract K1Validator is IValidator, ERC7739Validator { /// against credentials. function _erc1271IsValidSignatureNowCalldata(bytes32 hash, bytes calldata signature) internal view override returns (bool) { // call custom internal function to validate the signature against credentials - return _validateSignatureForOwner(smartAccountOwners[msg.sender], hash, signature); + return _validateSignatureForOwner(getOwner(msg.sender), hash, signature); } /// @dev Returns whether the `sender` is considered safe, such @@ -251,6 +259,7 @@ contract K1Validator is IValidator, ERC7739Validator { // @notice Fills the _safeSenders list from the given data function _fillSafeSenders(bytes calldata data) private { + require(data.length % 20 == 0, InvalidSafeSendersLength()); for (uint256 i; i < data.length / 20; i++) { _safeSenders.add(msg.sender, address(bytes20(data[20 * i:20 * (i + 1)]))); } @@ -262,15 +271,4 @@ contract K1Validator is IValidator, ERC7739Validator { function _isInitialized(address smartAccount) private view returns (bool) { return smartAccountOwners[smartAccount] != address(0); } - - /// @notice Checks if the address is a contract - /// @param account The address to check - /// @return True if the address is a contract, false otherwise - function _isContract(address account) private view returns (bool) { - uint256 size; - assembly { - size := extcodesize(account) - } - return size > 0; - } } diff --git a/contracts/types/Constants.sol b/contracts/types/Constants.sol index 22e00ba06..9cbb29195 100644 --- a/contracts/types/Constants.sol +++ b/contracts/types/Constants.sol @@ -39,12 +39,24 @@ uint256 constant MODULE_TYPE_FALLBACK = 3; // Module type identifier for hooks uint256 constant MODULE_TYPE_HOOK = 4; -string constant MODULE_ENABLE_MODE_NOTATION = "ModuleEnableMode(address module,uint256 moduleType,bytes32 userOpHash,bytes32 initDataHash)"; -bytes32 constant MODULE_ENABLE_MODE_TYPE_HASH = keccak256(bytes(MODULE_ENABLE_MODE_NOTATION)); +// Module type identifiers for pre-validation hooks +uint256 constant MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 = 8; +uint256 constant MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 = 9; + + +// keccak256("ModuleEnableMode(address module,uint256 moduleType,bytes32 userOpHash,bytes32 initDataHash)") +bytes32 constant MODULE_ENABLE_MODE_TYPE_HASH = 0xbe844ccefa05559a48680cb7fe805b2ec58df122784191aed18f9f315c763e1b; + +// keccak256("EmergencyUninstall(address hook,uint256 hookType,bytes deInitData,uint256 nonce)") +bytes32 constant EMERGENCY_UNINSTALL_TYPE_HASH = 0xd3ddfc12654178cc44d4a7b6b969cfdce7ffe6342326ba37825314cffa0fba9c; // Validation modes bytes1 constant MODE_VALIDATION = 0x00; bytes1 constant MODE_MODULE_ENABLE = 0x01; +bytes1 constant MODE_DEFAULT_VALIDATOR = 0x02; + +// The flag to indicate the default validator mode for enable mode signature +address constant DEFAULT_VALIDATOR_FLAG = 0x0000000000000000000000000000000000000088; bytes4 constant SUPPORTS_ERC7739 = 0x77390000; -bytes4 constant SUPPORTS_ERC7739_V1 = 0x77390001; \ No newline at end of file +bytes4 constant SUPPORTS_ERC7739_V1 = 0x77390001; diff --git a/contracts/types/DataTypes.sol b/contracts/types/DataTypes.sol index 021573231..b3e51a5cd 100644 --- a/contracts/types/DataTypes.sol +++ b/contracts/types/DataTypes.sol @@ -22,3 +22,16 @@ struct Execution { /// @notice The calldata for the transaction bytes callData; } + +/// @title Emergency Uninstall +/// @notice Struct to encapsulate emergency uninstall data for a hook +struct EmergencyUninstall { + /// @notice The address of the hook to be uninstalled + address hook; + /// @notice The hook type identifier + uint256 hookType; + /// @notice Data used to uninstall the hook + bytes deInitData; + /// @notice Nonce used to prevent replay attacks + uint256 nonce; +} diff --git a/contracts/utils/NexusBootstrap.sol b/contracts/utils/NexusBootstrap.sol index 3cebd3b26..af14c2669 100644 --- a/contracts/utils/NexusBootstrap.sol +++ b/contracts/utils/NexusBootstrap.sol @@ -15,6 +15,12 @@ pragma solidity ^0.8.27; import { ModuleManager } from "../base/ModuleManager.sol"; import { IModule } from "../interfaces/modules/IModule.sol"; import { IERC7484 } from "../interfaces/IERC7484.sol"; +import { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK +} from "../types/Constants.sol"; /// @title NexusBootstrap Configuration for Nexus /// @notice Provides configuration and initialization for Nexus smart accounts. @@ -31,6 +37,29 @@ struct BootstrapConfig { /// @title NexusBootstrap /// @notice Manages the installation of modules into Nexus smart accounts using delegatecalls. contract NexusBootstrap is ModuleManager { + + constructor(address defaultValidator, bytes memory initData) ModuleManager(defaultValidator, initData) {} + + modifier _withInitSentinelLists() { + _initSentinelLists(); + _; + } + + /// @notice Initializes the Nexus account with the default validator. + /// @dev Intended to be called by the Nexus with a delegatecall. + /// @dev For gas savings purposes this method does not initialize the registry. + /// @dev The registry should be initialized via the `setRegistry` function on the Nexus contract later if needed. + /// @param data The initialization data for the default validator module. + function initNexusWithDefaultValidator( + bytes calldata data + ) + external + payable + { + IModule(_DEFAULT_VALIDATOR).onInstall(data); + } + + /// @notice Initializes the Nexus account with a single validator. /// @dev Intended to be called by the Nexus with a delegatecall. /// @param validator The address of the validator module. @@ -41,9 +70,14 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external { + ) + external + payable + _withInitSentinelLists + { _configureRegistry(registry, attesters, threshold); _installValidator(address(validator), data); + emit ModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator)); } /// @notice Initializes the Nexus account with multiple modules. @@ -60,29 +94,37 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external { + ) + external + payable + _withInitSentinelLists + { _configureRegistry(registry, attesters, threshold); // Initialize validators for (uint256 i = 0; i < validators.length; i++) { _installValidator(validators[i].module, validators[i].data); + emit ModuleInstalled(MODULE_TYPE_VALIDATOR, validators[i].module); } // Initialize executors for (uint256 i = 0; i < executors.length; i++) { if (executors[i].module == address(0)) continue; _installExecutor(executors[i].module, executors[i].data); + emit ModuleInstalled(MODULE_TYPE_EXECUTOR, executors[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); } } @@ -96,17 +138,23 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external { + ) + external + payable + _withInitSentinelLists + { _configureRegistry(registry, attesters, threshold); // Initialize validators for (uint256 i = 0; i < validators.length; i++) { _installValidator(validators[i].module, validators[i].data); + emit ModuleInstalled(MODULE_TYPE_VALIDATOR, validators[i].module); } // Initialize hook if (hook.module != address(0)) { _installHook(hook.module, hook.data); + emit ModuleInstalled(MODULE_TYPE_HOOK, hook.module); } } @@ -124,7 +172,11 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external view returns (bytes memory init) { + ) + external + view + returns (bytes memory init) + { init = abi.encode(address(this), abi.encodeCall(this.initNexus, (validators, executors, hook, fallbacks, registry, attesters, threshold))); } @@ -138,7 +190,11 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external view returns (bytes memory init) { + ) + external + view + returns (bytes memory init) + { init = abi.encode(address(this), abi.encodeCall(this.initNexusScoped, (validators, hook, registry, attesters, threshold))); } @@ -150,16 +206,19 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external view returns (bytes memory init) { + ) + external + view + returns (bytes memory init) + { init = abi.encode( - address(this), - abi.encodeCall(this.initNexusWithSingleValidator, (IModule(validator.module), validator.data, registry, attesters, threshold)) + address(this), abi.encodeCall(this.initNexusWithSingleValidator, (IModule(validator.module), validator.data, registry, attesters, threshold)) ); } /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = "NexusBootstrap"; - version = "1.0.0"; + version = "1.2.0"; } } diff --git a/contracts/utils/NexusProxy.sol b/contracts/utils/NexusProxy.sol new file mode 100644 index 000000000..6c05cac31 --- /dev/null +++ b/contracts/utils/NexusProxy.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol"; +import { ERC1967Utils } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import { Initializable } from "../lib/Initializable.sol"; + +/// @title NexusProxy +/// @dev A proxy contract that uses the ERC1967 upgrade pattern and sets the initializable flag +/// in the constructor to prevent reinitialization +contract NexusProxy is Proxy { + constructor(address implementation, bytes memory data) payable { + Initializable.setInitializable(); + ERC1967Utils.upgradeToAndCall(implementation, data); + } + + function _implementation() internal view virtual override returns (address) { + return ERC1967Utils.getImplementation(); + } +} diff --git a/foundry.toml b/foundry.toml index 1771d8e22..80e0061fa 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ auto_detect_solc = false block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT bytecode_hash = "none" - evm_version = "cancun" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode + evm_version = "prague" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode fuzz = { runs = 1_000 } via-ir = false gas_reports = ["*"] diff --git a/scripts/foundry/Deploy.s.sol b/scripts/foundry/Deploy.s.sol deleted file mode 100644 index 9388d4379..000000000 --- a/scripts/foundry/Deploy.s.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.0 <0.9.0; -pragma solidity >=0.8.0 <0.9.0; - -import { Nexus } from "../../contracts/Nexus.sol"; - -import { BaseScript } from "./Base.s.sol"; -import { K1ValidatorFactory } from "../../contracts/factory/K1ValidatorFactory.sol"; -import { K1Validator } from "../../contracts/modules/validators/K1Validator.sol"; -import { BootstrapLib } from "../../contracts/lib/BootstrapLib.sol"; -import { NexusBootstrap } from "../../contracts/utils/NexusBootstrap.sol"; -import { MockRegistry } from "../../contracts/mocks/MockRegistry.sol"; -import { HelperConfig } from "./HelperConfig.s.sol"; - -contract Deploy is BaseScript { - K1ValidatorFactory private k1ValidatorFactory; - K1Validator private k1Validator; - NexusBootstrap private bootstrapper; - MockRegistry private registry; - HelperConfig private helperConfig; - - function run() public broadcast returns (Nexus smartAccount) { - helperConfig = new HelperConfig(); - require(address(helperConfig.ENTRYPOINT()) != address(0), "ENTRYPOINT is not set"); - smartAccount = new Nexus(address(helperConfig.ENTRYPOINT())); - k1Validator = new K1Validator(); - bootstrapper = new NexusBootstrap(); - registry = new MockRegistry(); - k1ValidatorFactory = new K1ValidatorFactory( - address(smartAccount), - msg.sender, - address(k1Validator), - bootstrapper, - registry - ); - } -} diff --git a/scripts/foundry/HelperConfig.s.sol b/scripts/foundry/HelperConfig.s.sol index 7ed3d9996..bcb18fbd2 100644 --- a/scripts/foundry/HelperConfig.s.sol +++ b/scripts/foundry/HelperConfig.s.sol @@ -9,7 +9,7 @@ import {Script} from "forge-std/Script.sol"; contract HelperConfig is Script { IEntryPoint public ENTRYPOINT; - address private constant MAINNET_ENTRYPOINT_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + address private constant MAINNET_ENTRYPOINT_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; constructor() { if (block.chainid == 31337) { diff --git a/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol index 06b9584fb..77770adf2 100644 --- a/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol +++ b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol @@ -28,7 +28,7 @@ contract ArbitrumSmartAccountUpgradeTest is NexusTest_Base, ArbitrumSettings { smartAccountV2 = IBiconomySmartAccountV2(SMART_ACCOUNT_V2_ADDRESS); ENTRYPOINT_V_0_6 = IEntryPointV_0_6(ENTRYPOINT_ADDRESS); ENTRYPOINT_V_0_7 = ENTRYPOINT; - newImplementation = new Nexus(_ENTRYPOINT); + newImplementation = new Nexus(_ENTRYPOINT, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); // /!\ The private key is for testing purposes only and should not be used in production. signerPrivateKey = 0x2924d554c046e633f658427df4d0e7726487b1322bd16caaf24a53099f1cda85; signer = vm.createWallet(signerPrivateKey); @@ -45,7 +45,7 @@ contract ArbitrumSmartAccountUpgradeTest is NexusTest_Base, ArbitrumSettings { /// @notice Validates the account ID after the upgrade process. function test_AccountIdValidationAfterUpgrade() public { test_UpgradeV2ToV3AndInitialize(); - string memory expectedAccountId = "biconomy.nexus.1.0.0"; + string memory expectedAccountId = "biconomy.nexus.1.2.0"; string memory actualAccountId = IAccountConfig(payable(address(smartAccountV2))).accountId(); assertEq(actualAccountId, expectedAccountId, "Account ID does not match after upgrade."); } diff --git a/test/foundry/fork/base/BaseSettings.t.sol b/test/foundry/fork/base/BaseSettings.t.sol index 882f1bdc9..ccffd9757 100644 --- a/test/foundry/fork/base/BaseSettings.t.sol +++ b/test/foundry/fork/base/BaseSettings.t.sol @@ -8,8 +8,8 @@ import "../../utils/NexusTest_Base.t.sol"; contract BaseSettings is NexusTest_Base { address public constant UNISWAP_V2_ROUTER02 = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24; address public constant USDC_ADDRESS = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; - string public constant DEFAULT_BASE_RPC_URL = "https://mainnet.base.org"; - //string public constant DEFAULT_BASE_RPC_URL = "https://base.llamarpc.com"; + //string public constant DEFAULT_BASE_RPC_URL = "https://mainnet.base.org"; + string public constant DEFAULT_BASE_RPC_URL = "https://base.llamarpc.com"; //string public constant DEFAULT_BASE_RPC_URL = "https://developer-access-mainnet.base.org"; uint constant BLOCK_NUMBER = 15000000; diff --git a/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol new file mode 100644 index 000000000..9fb58a62f --- /dev/null +++ b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "../shared/TestModuleManagement_Base.t.sol"; +import { MockPreValidationHookMultiplexer } from "../../../contracts/mocks/MockPreValidationHookMultiplexer.sol"; +import { MockResourceLockPreValidationHook } from "../../../contracts/mocks/MockResourceLockPreValidationHook.sol"; +import { Mock7739PreValidationHook } from "../../../contracts/mocks/Mock7739PreValidationHook.sol"; +import { MockAccountLocker } from "../../../contracts/mocks/MockAccountLocker.sol"; +import { MockSimpleValidator } from "../../../contracts/mocks/MockSimpleValidator.sol"; +import { K1Validator } from "../../../contracts/modules/validators/K1Validator.sol"; + +/// @title TestNexusPreValidation_Integration_HookMultiplexer +/// @notice This contract tests the integration of the PreValidation hook multiplexer with the PreValidation resource lock hooks + +contract TestNexusPreValidation_Integration_HookMultiplexer is TestModuleManagement_Base { + MockPreValidationHookMultiplexer private hookMultiplexer; + MockResourceLockPreValidationHook private resourceLockHook; + Mock7739PreValidationHook private erc7739Hook; + MockAccountLocker private accountLocker; + MockSimpleValidator private SIMPLE_VALIDATOR; + K1Validator private K1_VALIDATOR; + + struct TestTemps { + bytes32 contents; + uint8 v; + bytes32 r; + bytes32 s; + } + + bytes32 internal constant APP_DOMAIN_SEPARATOR = 0xa1a044077d7677adbbfa892ded5390979b33993e0e2a457e3f974bbcda53821b; + + function setUp() public { + setUpModuleManagement_Base(); + + // Deploy supporting contracts + accountLocker = new MockAccountLocker(); + hookMultiplexer = new MockPreValidationHookMultiplexer(); + erc7739Hook = new Mock7739PreValidationHook(address(hookMultiplexer)); + resourceLockHook = new MockResourceLockPreValidationHook(address(accountLocker), address(hookMultiplexer)); + K1_VALIDATOR = new K1Validator(); + // Deploy the simple validator + SIMPLE_VALIDATOR = new MockSimpleValidator(); + // Format install data with owner + bytes memory validatorSetupData = abi.encodePacked(BOB_ADDRESS); // Set BOB as owner + // Prepare the call data for installing the validator module + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_VALIDATOR, address(SIMPLE_VALIDATOR), validatorSetupData); + // Install validator module using execution + installModule(callData, MODULE_TYPE_VALIDATOR, address(SIMPLE_VALIDATOR), EXECTYPE_DEFAULT); + // Prepare calldata for installing the account locker + bytes memory accountLockerInstallCallData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(accountLocker), ""); + // Install account locker + installModule(accountLockerInstallCallData, MODULE_TYPE_HOOK, address(accountLocker), EXECTYPE_DEFAULT); + // Install the K1 validator + bytes memory k1ValidatorInstallData = abi.encodePacked(BOB_ADDRESS); + bytes memory k1ValidatorInstallCallData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_VALIDATOR, address(K1_VALIDATOR), k1ValidatorInstallData); + installModule(k1ValidatorInstallCallData, MODULE_TYPE_VALIDATOR, address(K1_VALIDATOR), EXECTYPE_DEFAULT); + } + + function test_installMultiplePreValidationHooks() public { + // Install hooks for 4337 + address[] memory hooks4337 = new address[](1); + hooks4337[0] = address(resourceLockHook); + bytes[] memory hookData4337 = new bytes[](1); + hookData4337[0] = "foo"; + + // Install hooks for 1271 + address[] memory hooks1271 = new address[](2); + hooks1271[0] = address(resourceLockHook); + hooks1271[1] = address(erc7739Hook); + bytes[] memory hookData1271 = new bytes[](2); + hookData1271[0] = "foo"; + hookData1271[1] = "bar"; + + // Install 4337 hooks + bytes memory installData4337 = abi.encode(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, hooks4337, hookData4337); + bytes memory installCallData4337 = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(hookMultiplexer), installData4337); + installModule(installCallData4337, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(hookMultiplexer), EXECTYPE_DEFAULT); + + // Install 1271 hooks + bytes memory installData1271 = abi.encode(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, hooks1271, hookData1271); + bytes memory installCallData1271 = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(hookMultiplexer), installData1271); + installModule(installCallData1271, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(hookMultiplexer), EXECTYPE_DEFAULT); + + // Verify multiplexer is installed for both types + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(hookMultiplexer), ""), "4337 multiplexer should be installed"); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(hookMultiplexer), ""), "1271 multiplexer should be installed"); + } + + function test_1271_HookChaining_MockValidator_Success() public { + // Install hooks and multiplexer + test_installMultiplePreValidationHooks(); + + // Prepare test data + TestTemps memory t; + t.contents = keccak256("test message"); + + // Create signature data for personal sign + bytes32 hashToSign = toERC1271HashPersonalSign(t.contents, address(BOB_ACCOUNT)); + (t.v, t.r, t.s) = vm.sign(BOB.privateKey, hashToSign); + + // Prepare signature with validator prefix and triggering both hooks + bytes memory signature = abi.encodePacked(t.r, t.s, t.v); + bytes memory validatorSignature = abi.encodePacked( + address(VALIDATOR_MODULE), + bytes1(0x01), // Skip 7739 wrap + signature + ); + + // Validate signature through hook chain + bytes4 result = BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature); + assertEq(result, bytes4(0x1626ba7e), "Signature should be valid after hook chaining"); + } + + function test_1271_HookChaining_MockSimpleValidator_Success() public { + // Install hooks and multiplexer + test_installMultiplePreValidationHooks(); + + // Prepare test data + TestTemps memory t; + t.contents = keccak256("test message"); + + // Create signature data for personal sign + bytes32 hashToSign = toERC1271HashPersonalSign(t.contents, address(BOB_ACCOUNT)); + (t.v, t.r, t.s) = vm.sign(BOB.privateKey, hashToSign); + + // Prepare signature with validator prefix and triggering both hooks + bytes memory signature = abi.encodePacked(t.r, t.s, t.v); + bytes memory validatorSignature = abi.encodePacked(address(SIMPLE_VALIDATOR), bytes1(0x00), signature); + + // Validate signature through hook chain + bytes4 result = BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature); + assertEq(result, bytes4(0x1626ba7e), "Signature should be valid after hook chaining"); + } + + function test_1271_HookChaining_K1Validator_Success() public { + // Install hooks and multiplexer + test_installMultiplePreValidationHooks(); + + // Prepare test data + TestTemps memory t; + t.contents = keccak256("test message"); + + // Create signature data for personal sign + bytes32 hashToSign = toERC1271HashPersonalSign(t.contents, address(BOB_ACCOUNT)); + (t.v, t.r, t.s) = vm.sign(BOB.privateKey, hashToSign); + + // Prepare signature with validator prefix and triggering both hooks + bytes memory signature = abi.encodePacked(t.r, t.s, t.v); + bytes memory validatorSignature = abi.encodePacked(address(K1_VALIDATOR), bytes1(0x01), signature); + + // Validate signature through hook chain + bytes4 result = BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature); + assertEq(result, bytes4(0x1626ba7e), "Signature should be valid after hook chaining"); + } + + function test_1271_HookChaining_MockSimpleValidator_K1Validator_SameSignature_Success() public { + // Install hooks and multiplexer + test_installMultiplePreValidationHooks(); + + // Prepare test data + TestTemps memory t; + t.contents = keccak256("test message"); + + // Create signature data for personal sign + bytes32 hashToSign = toERC1271HashPersonalSign(t.contents, address(BOB_ACCOUNT)); + (t.v, t.r, t.s) = vm.sign(BOB.privateKey, hashToSign); + + // Prepare signature with validator prefix and triggering both hooks + bytes memory signature = abi.encodePacked(t.r, t.s, t.v); + bytes memory validatorSignature = abi.encodePacked(address(SIMPLE_VALIDATOR), bytes1(0x00), signature); + + // Validate signature through hook chain + bytes4 result = BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature); + assertEq(result, bytes4(0x1626ba7e), "Signature should be valid after hook chaining"); + + // Prepare signature with validator prefix and triggering both hooks + bytes memory validatorSignature2 = abi.encodePacked(address(K1_VALIDATOR), bytes1(0x01), signature); // Skip 7739 wrap + + // Validate signature through hook chain + bytes4 result2 = BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature2); + assertEq(result2, bytes4(0x1626ba7e), "Signature should be valid after hook chaining"); + } + + function test_1271_HookChaining_Fails_WhenResourceLocked() public { + // Install hooks and multiplexer + test_installMultiplePreValidationHooks(); + + // Lock resources + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), address(this), 1); + + // Prepare test data + TestTemps memory t; + t.contents = keccak256("test message"); + + // Create signature data + bytes32 hashToSign = toERC1271HashPersonalSign(t.contents, address(BOB_ACCOUNT)); + (t.v, t.r, t.s) = vm.sign(BOB.privateKey, hashToSign); + + // Prepare signature with validator prefix + bytes memory signature = abi.encodePacked(t.r, t.s, t.v); + bytes memory validatorSignature = abi.encodePacked( + address(VALIDATOR_MODULE), + bytes1(0x00), // Trigger 7739 wrap + signature + ); + + // Expect revert due to resource lock + vm.expectRevert(abi.encodeWithSelector(MockResourceLockPreValidationHook.SenderIsResourceLocked.selector)); + BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature); + } + + // Helper function to generate ERC-1271 hash for personal sign + function toERC1271HashPersonalSign(bytes32 childHash, address account) internal view returns (bytes32) { + AccountDomainStruct memory t; + (t.fields, t.name, t.version, t.chainId, t.verifyingContract, t.salt, t.extensions) = EIP712(account).eip712Domain(); + bytes32 domainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(t.name)), + keccak256(bytes(t.version)), + t.chainId, + t.verifyingContract + ) + ); + bytes32 parentStructHash = keccak256(abi.encode(keccak256("PersonalSign(bytes prefixed)"), childHash)); + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, parentStructHash)); + } + + struct AccountDomainStruct { + bytes1 fields; + string name; + string version; + uint256 chainId; + address verifyingContract; + bytes32 salt; + uint256[] extensions; + } +} diff --git a/test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol b/test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol new file mode 100644 index 000000000..8d1182ec6 --- /dev/null +++ b/test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "../shared/TestModuleManagement_Base.t.sol"; +import { MockResourceLockPreValidationHook } from "../../../contracts/mocks/MockResourceLockPreValidationHook.sol"; +import { MockAccountLocker } from "../../../contracts/mocks/MockAccountLocker.sol"; + +/// @title TestNexusPreValidation_Integration_ResourceLockHooks +/// @notice This contract tests the integration of ResourceLock hook with the PreValidation resource lock hooks +contract TestNexusPreValidation_Integration_ResourceLockHooks is TestModuleManagement_Base { + MockResourceLockPreValidationHook private resourceLockHook; + MockAccountLocker private accountLocker; + + address internal constant NATIVE_TOKEN = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + bytes32 internal constant APP_DOMAIN_SEPARATOR = 0xa1a044077d7677adbbfa892ded5390979b33993e0e2a457e3f974bbcda53821b; + + struct TestTemps { + bytes32 contents; + address signer; + uint256 privateKey; + uint8 v; + bytes32 r; + bytes32 s; + } + + function setUp() public { + setUpModuleManagement_Base(); + accountLocker = new MockAccountLocker(); + resourceLockHook = new MockResourceLockPreValidationHook(address(accountLocker), address(0)); + } + + /// @notice Tests installing the account locker and resource lock hook + function test_InstallResourceLockHooks() public { + installResourceLockHooks(); + // Verify hooks are installed + assertTrue( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(resourceLockHook), ""), "Resource lock 4337 hook should be installed" + ); + assertTrue( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(resourceLockHook), ""), "Resource lock 1271 hook should be installed" + ); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(accountLocker), ""), "Account locker should be installed"); + } + + /// @notice Fuzz test for pre-validation hook when ETH is locked + /// @param lockedAmount Amount of ETH to lock + /// @param missingAccountFunds Funds missing from the account + function testFuzz_4337_PreValidationHook_RevertsWhen_InsufficientUnlockedETH(uint256 lockedAmount, uint256 missingAccountFunds) public { + // Constrain inputs to reasonable ranges + vm.assume(lockedAmount > 0); + vm.assume(missingAccountFunds > 0); + + // Install resource lock hooks + installResourceLockHooks(); + + // Prepare user operation + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = buildUserOpWithCalldata(BOB, "", address(VALIDATOR_MODULE)); + + // Set locked amount to block ETH transactions + + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), NATIVE_TOKEN, lockedAmount); + + // Ensure account has enough total balance + vm.deal(address(BOB_ACCOUNT), lockedAmount); + assertTrue(address(BOB_ACCOUNT).balance == lockedAmount, "Account should have correct balance"); + + // Calculate user op hash + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + + // Sign the user operation + userOps[0].signature = signMessage(BOB, userOpHash); + + // Expect revert due to insufficient unlocked ETH + vm.expectRevert(abi.encodeWithSelector(MockResourceLockPreValidationHook.InsufficientUnlockedETH.selector, missingAccountFunds)); + + // Attempt to validate the user operation + startPrank(address(ENTRYPOINT)); + BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, missingAccountFunds); + stopPrank(); + } + + /// @notice Fuzz test for pre-validation hook when sufficient ETH is unlocked + /// @param lockedAmount Amount of ETH to lock + /// @param totalBalance Total balance of the account + function testFuzz_4337_PreValidationHook_Success(uint256 lockedAmount, uint256 totalBalance) public { + // Constrain inputs to reasonable ranges + vm.assume(lockedAmount > 0); + vm.assume(totalBalance > lockedAmount); + + // Install resource lock hooks + installResourceLockHooks(); + + // Prepare user operation + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = buildUserOpWithCalldata(BOB, "", address(VALIDATOR_MODULE)); + + // Set locked amount + + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), NATIVE_TOKEN, lockedAmount); + + // Ensure account has enough total balance + vm.deal(address(BOB_ACCOUNT), totalBalance); + assertTrue(address(BOB_ACCOUNT).balance == totalBalance, "Account should have correct balance"); + + // Calculate user op hash + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + + // Sign the user operation + userOps[0].signature = signMessage(BOB, userOpHash); + + // Attempt to validate the user operation when unlocked balance is sufficient + vm.assume(totalBalance - lockedAmount >= 0); + startPrank(address(ENTRYPOINT)); + uint256 result = BOB_ACCOUNT.validateUserOp(userOps[0], userOpHash, 0); + assertTrue(result == 0, "Validation should succeed"); + stopPrank(); + } + + /// @notice Tests signature validation succeeds when resource is not locked + function test_1271_PreValidationHook_Success() public { + // Install resource lock hooks + installResourceLockHooks(); + + // Prepare signature + TestTemps memory t; + t.contents = keccak256("123"); + bytes32 hashToSign = toERC1271HashPersonalSign(t.contents, address(BOB_ACCOUNT)); + (t.v, t.r, t.s) = vm.sign(BOB.privateKey, hashToSign); + + // Prepare signature with validator prefix + bytes memory signature = abi.encodePacked(t.r, t.s, t.v); + bytes memory validatorSignature = abi.encodePacked(address(VALIDATOR_MODULE), signature); + + // Validate signature + bytes4 result = BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature); + assertEq(result, bytes4(0x1626ba7e), "Signature should be valid"); + } + + /// @notice Tests signature validation fails when resource is locked + function test_1271_PreValidationHook_RevertsWhen_ResourceLocked() public { + // Install resource lock hooks + installResourceLockHooks(); + + // Prepare signature + TestTemps memory t; + t.contents = keccak256("123"); + bytes32 hashToSign = toERC1271HashPersonalSign(t.contents, address(BOB_ACCOUNT)); + (t.v, t.r, t.s) = vm.sign(BOB.privateKey, hashToSign); + + // Prepare signature with validator prefix + bytes memory signature = abi.encodePacked(t.r, t.s, t.v); + bytes memory validatorSignature = abi.encodePacked(address(VALIDATOR_MODULE), signature); + + // Set locked amount to block signature validation + + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), address(this), 1); + + // Expect revert due to resource lock + vm.expectRevert(abi.encodeWithSelector(MockResourceLockPreValidationHook.SenderIsResourceLocked.selector)); + BOB_ACCOUNT.isValidSignature(t.contents, validatorSignature); + } + + function installResourceLockHooks() internal { + // Install account locker first + bytes memory accountLockerInstallCallData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(accountLocker), ""); + installModule(accountLockerInstallCallData, MODULE_TYPE_HOOK, address(accountLocker), EXECTYPE_DEFAULT); + + // Install resource lock pre-validation 4337 hook + bytes memory resourceLockHook4337InstallCallData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(resourceLockHook), ""); + installModule(resourceLockHook4337InstallCallData, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(resourceLockHook), EXECTYPE_DEFAULT); + + // Install resource lock pre-validation 1271 hook + bytes memory resourceLockHook1271InstallCallData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(resourceLockHook), ""); + installModule(resourceLockHook1271InstallCallData, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(resourceLockHook), EXECTYPE_DEFAULT); + } + + /// @notice Generates an ERC-1271 hash for personal sign. + /// @param childHash The child hash. + /// @return The ERC-1271 hash for personal sign. + function toERC1271HashPersonalSign(bytes32 childHash, address account) internal view returns (bytes32) { + AccountDomainStruct memory t; + (t.fields, t.name, t.version, t.chainId, t.verifyingContract, t.salt, t.extensions) = EIP712(account).eip712Domain(); + bytes32 domainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(t.name)), + keccak256(bytes(t.version)), + t.chainId, + t.verifyingContract // veryfingContract should be the account address. + ) + ); + bytes32 parentStructHash = keccak256(abi.encode(keccak256("PersonalSign(bytes prefixed)"), childHash)); + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, parentStructHash)); + } + + struct AccountDomainStruct { + bytes1 fields; + string name; + string version; + uint256 chainId; + address verifyingContract; + bytes32 salt; + uint256[] extensions; + } +} diff --git a/test/foundry/integration/UpgradeSmartAccountTest.t.sol b/test/foundry/integration/UpgradeSmartAccountTest.t.sol index 09a15c962..55463ee4a 100644 --- a/test/foundry/integration/UpgradeSmartAccountTest.t.sol +++ b/test/foundry/integration/UpgradeSmartAccountTest.t.sol @@ -24,7 +24,7 @@ contract UpgradeSmartAccountTest is NexusTest_Base { /// @notice Tests the upgrade of the smart account implementation function test_upgradeImplementation() public { address _ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - Nexus newSmartAccount = new Nexus(_ENTRYPOINT); + Nexus newSmartAccount = new Nexus(_ENTRYPOINT, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); bytes memory callData = abi.encodeWithSelector(Nexus.upgradeToAndCall.selector, address(newSmartAccount), ""); Execution[] memory execution = new Execution[](1); @@ -39,7 +39,7 @@ contract UpgradeSmartAccountTest is NexusTest_Base { /// @notice Tests the upgrade of the smart account implementation with invalid call data function test_upgradeImplementation_invalidCallData() public { address _ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - Nexus newSmartAccount = new Nexus(_ENTRYPOINT); + Nexus newSmartAccount = new Nexus(_ENTRYPOINT, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); bytes memory callData = abi.encodeWithSelector(Nexus.upgradeToAndCall.selector, address(newSmartAccount), bytes(hex"1234")); Execution[] memory execution = new Execution[](1); execution[0] = Execution(address(BOB_ACCOUNT), 0, callData); @@ -59,7 +59,7 @@ contract UpgradeSmartAccountTest is NexusTest_Base { function test_upgradeImplementation_invalidCaller() public { address _ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - Nexus newSmartAccount = new Nexus(_ENTRYPOINT); + Nexus newSmartAccount = new Nexus(_ENTRYPOINT, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); vm.expectRevert(abi.encodeWithSelector(AccountAccessUnauthorized.selector)); BOB_ACCOUNT.upgradeToAndCall(address(newSmartAccount), ""); } @@ -120,7 +120,7 @@ contract UpgradeSmartAccountTest is NexusTest_Base { test_proxiableUUIDSlot(); test_currentImplementationAddress(); address _ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - Nexus newSmartAccount = new Nexus(_ENTRYPOINT); + Nexus newSmartAccount = new Nexus(_ENTRYPOINT, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); vm.expectRevert(abi.encodeWithSelector(AccountAccessUnauthorized.selector)); BOB_ACCOUNT.upgradeToAndCall(address(newSmartAccount), ""); } diff --git a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol index c8e137971..1e51d7667 100644 --- a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol +++ b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol @@ -1,25 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import "../../../utils/Imports.sol"; +import { NexusTest_Base } from "../../../utils/NexusTest_Base.t.sol"; /// @title Test suite for checking account ID in AccountConfig -contract TestAccountConfig_AccountId is Test { - Nexus internal accountConfig; - address _ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; - - modifier givenTheAccountConfiguration() { - _; - } - +contract TestAccountConfig_AccountId is NexusTest_Base { + /// @notice Initialize the testing environment /// @notice Initialize the testing environment function setUp() public { - accountConfig = new Nexus(_ENTRYPOINT); + setupPredefinedWallets(); + deployTestContracts(); } /// @notice Tests if the account ID returns the expected value - function test_WhenCheckingTheAccountID() external givenTheAccountConfiguration { - string memory expected = "biconomy.nexus.1.0.0"; - assertEq(accountConfig.accountId(), expected, "AccountConfig should return the expected account ID."); + function test_WhenCheckingTheAccountID() external { + string memory expected = "biconomy.nexus.1.2.0"; + assertEq(ACCOUNT_IMPLEMENTATION.accountId(), expected, "AccountConfig should return the expected account ID."); } } diff --git a/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol b/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol index 9bac78537..c6a0974f4 100644 --- a/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol +++ b/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol @@ -61,11 +61,13 @@ contract TestAccountExecution_ExecuteFromExecutor is TestAccountExecution_Base { address valueTarget = makeAddr("valueTarget"); uint256 value = 1 ether; - bytes memory sendValueCallData = - abi.encodeWithSelector(MockDelegateTarget.sendValue.selector, valueTarget, value); + bytes memory sendValueCallData = abi.encodePacked( + address(delegateTarget), + abi.encodeWithSelector(MockDelegateTarget.sendValue.selector, valueTarget, value) + ); mockExecutor.execDelegatecall(BOB_ACCOUNT, sendValueCallData); // Assert that the value was set ie that execution was successful - // assertTrue(valueTarget.balance == value); + assertTrue(valueTarget.balance == value); } /// @notice Tests batch execution via MockExecutor @@ -196,11 +198,8 @@ contract TestAccountExecution_ExecuteFromExecutor is TestAccountExecution_Base { /// @notice Tests execution with an unsupported call type via MockExecutor function test_RevertIf_ExecuteFromExecutor_UnsupportedCallType() public { ExecutionMode unsupportedMode = ExecutionMode.wrap(bytes32(abi.encodePacked(bytes1(0xee), bytes1(0x00), bytes4(0), bytes22(0)))); - bytes memory executionCalldata = abi.encodePacked(address(counter), uint256(0), abi.encodeWithSelector(Counter.incrementNumber.selector)); (CallType callType, , , ) = ModeLib.decode(unsupportedMode); - Execution[] memory execution = new Execution[](1); - execution[0] = Execution(address(mockExecutor), 0, executionCalldata); vm.expectRevert(abi.encodeWithSelector(UnsupportedCallType.selector, callType)); @@ -217,13 +216,10 @@ contract TestAccountExecution_ExecuteFromExecutor is TestAccountExecution_Base { function test_RevertIf_ExecuteFromExecutor_UnsupportedExecType_Batch() public { // Create an unsupported execution mode with an invalid execution type ExecutionMode unsupportedMode = ExecutionMode.wrap(bytes32(abi.encodePacked(CALLTYPE_BATCH, bytes1(0xff), bytes4(0), bytes22(0)))); - bytes memory executionCalldata = abi.encodePacked(address(counter), uint256(0), abi.encodeWithSelector(Counter.incrementNumber.selector)); - + // Decode the mode to extract the execution type for the expected revert (, ExecType execType, , ) = ModeLib.decode(unsupportedMode); - Execution[] memory execution = new Execution[](1); - execution[0] = Execution(address(mockExecutor), 0, executionCalldata); - + // Expect the revert with UnsupportedExecType error vm.expectRevert(abi.encodeWithSelector(UnsupportedExecType.selector, execType)); diff --git a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol new file mode 100644 index 000000000..fbb2f7df0 --- /dev/null +++ b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { NexusTest_Base } from "../../../utils/NexusTest_Base.t.sol"; +import "../../../utils/Imports.sol"; +import { MockTarget } from "contracts/mocks/MockTarget.sol"; +import { IExecutionHelper } from "contracts/interfaces/base/IExecutionHelper.sol"; +import { IHook } from "contracts/interfaces/modules/IHook.sol"; +import { IPreValidationHookERC1271, IPreValidationHookERC4337 } from "contracts/interfaces/modules/IPreValidationHook.sol"; +import { MockPreValidationHook } from "contracts/mocks/MockPreValidationHook.sol"; + +contract TestEIP7702 is NexusTest_Base { + using ECDSA for bytes32; + + MockDelegateTarget delegateTarget; + MockTarget target; + MockValidator public mockValidator; + MockExecutor public mockExecutor; + + function setUp() public { + setupTestEnvironment(); + delegateTarget = new MockDelegateTarget(); + target = new MockTarget(); + mockValidator = new MockValidator(); + mockExecutor = new MockExecutor(); + } + + function _doEIP7702(address account) internal { + vm.etch(account, abi.encodePacked(bytes3(0xef0100), bytes20(address(ACCOUNT_IMPLEMENTATION)))); + } + + function _getInitData() internal view returns (bytes memory) { + // Create config for initial modules + BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(mockValidator), ""); + BootstrapConfig[] memory executors = BootstrapLib.createArrayConfig(address(mockExecutor), ""); + BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); + BootstrapConfig[] memory fallbacks = BootstrapLib.createArrayConfig(address(0), ""); + + return BOOTSTRAPPER.getInitNexusCalldata(validators, executors, hook, fallbacks, REGISTRY, ATTESTERS, THRESHOLD); + } + + function _getSignature(uint256 eoaKey, PackedUserOperation memory userOp) internal view returns (bytes memory) { + bytes32 hash = ENTRYPOINT.getUserOpHash(userOp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(eoaKey, hash.toEthSignedMessageHash()); + return abi.encodePacked(r, s, v); + } + + function test_execSingle() public returns (address) { + // Create calldata for the account to execute + bytes memory setValueOnTarget = abi.encodeCall(MockTarget.setValue, 1337); + + // Encode the call into the calldata for the userOp + bytes memory userOpCalldata = + abi.encodeCall(IExecutionHelper.execute, (ModeLib.encodeSimpleSingle(), ExecLib.encodeSingle(address(target), uint256(0), setValueOnTarget))); + + // Get the account, initcode and nonce + uint256 eoaKey = uint256(8); + address account = vm.addr(eoaKey); + vm.deal(account, 100 ether); + + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); + + // Create the userOp and add the data + PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); + userOp.callData = userOpCalldata; + userOp.callData = userOpCalldata; + + userOp.signature = _getSignature(eoaKey, userOp); + _doEIP7702(account); + + // Create userOps array + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + // Send the userOp to the entrypoint + ENTRYPOINT.handleOps(userOps, payable(address(0x69))); + + // Assert that the value was set ie that execution was successful + assertTrue(target.value() == 1337); + return account; + } + + function test_initializeAndExecSingle() public returns (address) { + // Get the account, initcode and nonce + uint256 eoaKey = uint256(8); + address account = vm.addr(eoaKey); + vm.deal(account, 100 ether); + + // Create calldata for the account to execute + bytes memory setValueOnTarget = abi.encodeCall(MockTarget.setValue, 1337); + + bytes memory initData = _getInitData(); + + Execution[] memory executions = new Execution[](2); + executions[0] = Execution({ target: account, value: 0, callData: abi.encodeCall(INexus.initializeAccount, initData) }); + executions[1] = Execution({ target: address(target), value: 0, callData: setValueOnTarget }); + + // Encode the call into the calldata for the userOp + bytes memory userOpCalldata = abi.encodeCall(IExecutionHelper.execute, (ModeLib.encodeSimpleBatch(), ExecLib.encodeBatch(executions))); + + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); + + // Create the userOp and add the data + PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); + userOp.callData = userOpCalldata; + + userOp.signature = _getSignature(eoaKey, userOp); + _doEIP7702(account); + + // Create userOps array + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + // Send the userOp to the entrypoint + ENTRYPOINT.handleOps(userOps, payable(address(0x69))); + + // Assert that the value was set ie that execution was successful + assertTrue(target.value() == 1337); + return account; + } + + function test_execBatch() public { + // Create calldata for the account to execute + bytes memory setValueOnTarget = abi.encodeCall(MockTarget.setValue, 1337); + address target2 = address(0x420); + uint256 target2Amount = 1 wei; + + // Create the executions + Execution[] memory executions = new Execution[](2); + executions[0] = Execution({ target: address(target), value: 0, callData: setValueOnTarget }); + executions[1] = Execution({ target: target2, value: target2Amount, callData: "" }); + + // Encode the call into the calldata for the userOp + bytes memory userOpCalldata = abi.encodeCall(IExecutionHelper.execute, (ModeLib.encodeSimpleBatch(), ExecLib.encodeBatch(executions))); + + // Get the account, initcode and nonce + uint256 eoaKey = uint256(8); + address account = vm.addr(eoaKey); + vm.deal(account, 100 ether); + + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); + + // Create the userOp and add the data + PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); + userOp.callData = userOpCalldata; + userOp.callData = userOpCalldata; + + userOp.signature = _getSignature(eoaKey, userOp); + _doEIP7702(account); + + // Create userOps array + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + // Send the userOp to the entrypoint + ENTRYPOINT.handleOps(userOps, payable(address(0x69))); + + // Assert that the value was set ie that execution was successful + assertTrue(target.value() == 1337); + assertTrue(target2.balance == target2Amount); + } + + function test_execSingleFromExecutor() public { + address account = test_initializeAndExecSingle(); + + bytes[] memory ret = + mockExecutor.executeViaAccount(INexus(address(account)), address(target), 0, abi.encodePacked(MockTarget.setValue.selector, uint256(1338))); + + assertEq(ret.length, 1); + assertEq(abi.decode(ret[0], (uint256)), 1338); + } + + function test_execBatchFromExecutor() public { + address account = test_initializeAndExecSingle(); + + bytes memory setValueOnTarget = abi.encodeCall(MockTarget.setValue, 1338); + Execution[] memory executions = new Execution[](2); + executions[0] = Execution({ target: address(target), value: 0, callData: setValueOnTarget }); + executions[1] = Execution({ target: address(target), value: 0, callData: setValueOnTarget }); + bytes[] memory ret = mockExecutor.executeBatchViaAccount({ account: INexus(address(account)), execs: executions }); + + assertEq(ret.length, 2); + assertEq(abi.decode(ret[0], (uint256)), 1338); + } + + function test_delegateCall() public { + // Create calldata for the account to execute + address valueTarget = makeAddr("valueTarget"); + uint256 value = 1 ether; + bytes memory sendValue = abi.encodeWithSelector(MockDelegateTarget.sendValue.selector, valueTarget, value); + + // Encode the call into the calldata for the userOp + bytes memory userOpCalldata = abi.encodeCall( + IExecutionHelper.execute, + ( + ModeLib.encode(CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)), + abi.encodePacked(address(delegateTarget), sendValue) + ) + ); + + // Get the account, initcode and nonce + uint256 eoaKey = uint256(8); + address account = vm.addr(eoaKey); + vm.deal(account, 100 ether); + + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); + + // Create the userOp and add the data + PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); + userOp.callData = userOpCalldata; + userOp.callData = userOpCalldata; + + userOp.signature = _getSignature(eoaKey, userOp); + _doEIP7702(account); + + // Create userOps array + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + + // Send the userOp to the entrypoint + ENTRYPOINT.handleOps(userOps, payable(address(0x69))); + + // Assert that the value was set ie that execution was successful + assertTrue(valueTarget.balance == value); + } + + function test_delegateCall_fromExecutor() public { + address account = test_initializeAndExecSingle(); + + // Create calldata for the account to execute + address valueTarget = makeAddr("valueTarget"); + uint256 value = 1 ether; + bytes memory sendValue = abi.encodeWithSelector(MockDelegateTarget.sendValue.selector, valueTarget, value); + + // Execute the delegatecall via the executor + mockExecutor.execDelegatecall(INexus(address(account)), abi.encodePacked(address(delegateTarget), sendValue)); + + // Assert that the value was set ie that execution was successful + assertTrue(valueTarget.balance == value); + } + + function test_amIERC7702_success()public { + ExposedNexus exposedNexus = new ExposedNexus(address(ENTRYPOINT), address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xEeEe))); + address eip7702account = address(0x7702acc7702acc7702acc7702acc); + vm.etch(eip7702account, abi.encodePacked(bytes3(0xef0100), bytes20(address(exposedNexus)))); + assertTrue(IExposedNexus(eip7702account).amIERC7702()); + } +} diff --git a/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol b/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol index 9d863f1e6..27fa26526 100644 --- a/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol +++ b/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol @@ -4,12 +4,14 @@ pragma solidity ^0.8.27; import "../../../utils/Imports.sol"; import "../../../utils/NexusTest_Base.t.sol"; import { TokenWithPermit } from "../../../../../contracts/mocks/TokenWithPermit.sol"; +import { MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 } from "contracts/types/Constants.sol"; +import { MockPreValidationHook } from "contracts/mocks/MockPreValidationHook.sol"; /// @title TestERC1271Account_MockProtocol /// @notice This contract tests the ERC1271 signature validation with a mock protocol and mock validator. contract TestERC1271Account_MockProtocol is NexusTest_Base { - K1Validator private validator; + struct TestTemps { bytes32 userOpHash; bytes32 contents; @@ -24,15 +26,18 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { bytes32 internal constant PARENT_TYPEHASH = 0xd61db970ec8a2edc5f9fd31d876abe01b785909acb16dcd4baaf3b434b4c439b; bytes32 internal domainSepB; TokenWithPermit public permitToken; + MockPreValidationHook preValidationHook; /// @notice Sets up the testing environment and initializes the permit token. function setUp() public { init(); validator = new K1Validator(); + preValidationHook = new MockPreValidationHook(); installK1Validator(BOB_ACCOUNT, BOB); - installK1Validator(ALICE_ACCOUNT, ALICE); + installPrevalidationHook(BOB_ACCOUNT, BOB); + installPrevalidationHook(ALICE_ACCOUNT, ALICE); permitToken = new TokenWithPermit("TestToken", "TST"); domainSepB = permitToken.DOMAIN_SEPARATOR(); } @@ -42,12 +47,7 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { TestTemps memory t; t.contents = keccak256( abi.encode( - permitToken.PERMIT_TYPEHASH_LOCAL(), - address(ALICE_ACCOUNT), - address(0x69), - 1e18, - permitToken.nonces(address(ALICE_ACCOUNT)), - block.timestamp + permitToken.PERMIT_TYPEHASH_LOCAL(), address(ALICE_ACCOUNT), address(0x69), 1e18, permitToken.nonces(address(ALICE_ACCOUNT)), block.timestamp ) ); (t.v, t.r, t.s) = vm.sign(ALICE.privateKey, toERC1271Hash(t.contents, address(ALICE_ACCOUNT))); @@ -60,45 +60,12 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { assertEq(permitToken.allowance(address(ALICE_ACCOUNT), address(0x69)), 1e18); } - function testHashTypedData() public { - bytes32 structHash = keccak256(abi.encodePacked("testStruct")); - bytes32 expectedHash = BOB_ACCOUNT.hashTypedData(structHash); - - bytes32 domainSeparator = BOB_ACCOUNT.DOMAIN_SEPARATOR(); - bytes32 actualHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - assertEq(expectedHash, actualHash); - } - - function testDomainSeparator() public { - bytes32 expectedDomainSeparator = BOB_ACCOUNT.DOMAIN_SEPARATOR(); - - AccountDomainStruct memory t; - (/*t.fields*/, t.name, t.version, t.chainId, t.verifyingContract, t.salt, /*t.extensions*/) = BOB_ACCOUNT.eip712Domain(); - - bytes32 calculatedDomainSeparator = keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(t.name)), - keccak256(bytes(t.version)), - t.chainId, - t.verifyingContract - ) - ); - assertEq(expectedDomainSeparator, calculatedDomainSeparator); - } - /// @notice Tests the failure of signature validation due to an incorrect signer. function test_RevertWhen_SignatureIsInvalidDueToWrongSigner() public { TestTemps memory t; t.contents = keccak256( abi.encode( - permitToken.PERMIT_TYPEHASH_LOCAL(), - address(ALICE_ACCOUNT), - address(0x69), - 1e18, - permitToken.nonces(address(ALICE_ACCOUNT)), - block.timestamp + permitToken.PERMIT_TYPEHASH_LOCAL(), address(ALICE_ACCOUNT), address(0x69), 1e18, permitToken.nonces(address(ALICE_ACCOUNT)), block.timestamp ) ); (t.v, t.r, t.s) = vm.sign(BOB.privateKey, toERC1271Hash(t.contents, address(ALICE_ACCOUNT))); @@ -117,12 +84,7 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { TestTemps memory t; t.contents = keccak256( abi.encode( - permitToken.PERMIT_TYPEHASH_LOCAL(), - address(ALICE_ACCOUNT), - address(0x69), - 1e6, - permitToken.nonces(address(ALICE_ACCOUNT)), - block.timestamp + permitToken.PERMIT_TYPEHASH_LOCAL(), address(ALICE_ACCOUNT), address(0x69), 1e6, permitToken.nonces(address(ALICE_ACCOUNT)), block.timestamp ) ); (t.v, t.r, t.s) = vm.sign(BOB.privateKey, toERC1271Hash(t.contents, address(ALICE_ACCOUNT))); @@ -175,16 +137,15 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { /// @return The EIP-712 domain struct fields encoded. function accountDomainStructFields(address account) internal view returns (bytes memory) { AccountDomainStruct memory t; - (/*t.fields*/, t.name, t.version, t.chainId, t.verifyingContract, t.salt, /*t.extensions*/) = EIP712(account).eip712Domain(); - - return - abi.encode( - keccak256(bytes(t.name)), - keccak256(bytes(t.version)), - t.chainId, - t.verifyingContract, // Use the account address as the verifying contract. - t.salt - ); + ( /*t.fields*/ , t.name, t.version, t.chainId, t.verifyingContract, t.salt, /*t.extensions*/ ) = EIP712(account).eip712Domain(); + + return abi.encode( + keccak256(bytes(t.name)), + keccak256(bytes(t.version)), + t.chainId, + t.verifyingContract, // Use the account address as the verifying contract. + t.salt + ); } /// @notice Helper function to install a validator module to a specific deployed Smart Account. @@ -192,12 +153,7 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { /// @param user The wallet executing the operation. function installK1Validator(Nexus account, Vm.Wallet memory user) internal { // Prepare call data for installing the validator module - bytes memory callData = abi.encodeWithSelector( - IModuleManager.installModule.selector, - MODULE_TYPE_VALIDATOR, - validator, - abi.encodePacked(user.addr) - ); + bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_VALIDATOR, validator, abi.encodePacked(user.addr)); // Prepare execution array Execution[] memory execution = new Execution[](1); @@ -212,4 +168,25 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { // Assert that the validator module is installed assertTrue(account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), ""), "Validator module should be installed"); } + + function installPrevalidationHook(Nexus account, Vm.Wallet memory user) internal { + // Prepare call data for installing the validator module + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), ""); + + // Prepare execution array + Execution[] memory execution = new Execution[](1); + execution[0] = Execution(address(account), 0, callData); + + // Build the packed user operation + PackedUserOperation[] memory userOps = buildPackedUserOperation(user, account, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); + + // Handle the user operation through the entry point + ENTRYPOINT.handleOps(userOps, payable(user.addr)); + + // Assert that the validator module is installed + assertTrue( + account.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), ""), "PreValidationHook module should be installed" + ); + } } diff --git a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol deleted file mode 100644 index 6b97f1e40..000000000 --- a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol +++ /dev/null @@ -1,252 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import "../../../utils/NexusTest_Base.t.sol"; - -/// @title TestAccountFactory_Deployments -/// @notice Tests for deploying accounts using the AccountFactory and various methods. -contract TestAccountFactory_Deployments is NexusTest_Base { - Vm.Wallet public user; - bytes initData; - - /// @notice Sets up the testing environment. - function setUp() public { - init(); - user = newWallet("user"); - vm.deal(user.addr, 1 ether); - initData = abi.encodePacked(user.addr); - } - - /// @notice Tests deploying an account using the factory directly. - function test_DeployAccount_CreateAccount() public { - // Prepare bootstrap configuration for validators - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - // Create initcode and salt to be sent to Factory - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt); - - vm.expectEmit(true, true, true, true); - emit AccountCreated(expectedAddress, _initData, salt); - - bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); - - address payable deployedAccountAddress = META_FACTORY.deployWithFactory(address(FACTORY), factoryData); - - // Validate that the account was deployed correctly - assertEq(deployedAccountAddress, expectedAddress, "Deployed account address mismatch"); - } - - /// @notice Tests that deploying an account returns the same address with the same arguments. - function test_DeployAccount_CreateAccount_SameAddress() public { - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - // Create initcode and salt to be sent to Factory - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt); - - vm.expectEmit(true, true, true, true); - emit AccountCreated(expectedAddress, _initData, salt); - - bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); - - address payable deployedAccountAddress = META_FACTORY.deployWithFactory(address(FACTORY), factoryData); - - address payable deployedAccountAddress2 = META_FACTORY.deployWithFactory(address(FACTORY), factoryData); - assertEq(deployedAccountAddress, deployedAccountAddress2, "Deployed account address mismatch"); - } - - /// @notice Tests deploying an account using handleOps method. - function test_DeployAccount_HandleOps_Success() public { - address payable accountAddress = calculateAccountAddress(user.addr, address(VALIDATOR_MODULE)); - bytes memory initCode = buildInitCode(user.addr, address(VALIDATOR_MODULE)); - PackedUserOperation[] memory userOps = new PackedUserOperation[](1); - userOps[0] = buildUserOpWithInitAndCalldata(user, initCode, "", address(VALIDATOR_MODULE)); - ENTRYPOINT.depositTo{ value: 1 ether }(address(accountAddress)); - ENTRYPOINT.handleOps(userOps, payable(user.addr)); - assertEq(IAccountConfig(accountAddress).accountId(), "biconomy.nexus.1.0.0", "Not deployed properly"); - } - - /// @notice Tests that deploying an account fails if it already exists. - function test_RevertIf_HandleOps_AccountExists() public { - address payable accountAddress = calculateAccountAddress(user.addr, address(VALIDATOR_MODULE)); - bytes memory initCode = buildInitCode(user.addr, address(VALIDATOR_MODULE)); - PackedUserOperation[] memory userOps = new PackedUserOperation[](1); - userOps[0] = buildUserOpWithInitAndCalldata(user, initCode, "", address(VALIDATOR_MODULE)); - ENTRYPOINT.depositTo{ value: 1 ether }(address(accountAddress)); - ENTRYPOINT.handleOps(userOps, payable(user.addr)); - vm.expectRevert(abi.encodeWithSelector(FailedOp.selector, 0, "AA10 sender already constructed")); - ENTRYPOINT.handleOps(userOps, payable(user.addr)); - } - - /// @notice Tests that a deployed account is initialized and cannot be reinitialized. - function test_DeployAccount_CannotReinitialize() public { - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - // Create initcode and salt to be sent to Factory - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); - - address payable firstAccountAddress = FACTORY.createAccount(_initData, salt); - - vm.prank(user.addr); // Even owner cannot reinitialize the account - vm.expectRevert(LinkedList_AlreadyInitialized.selector); - INexus(firstAccountAddress).initializeAccount(factoryData); - } - - /// @notice Tests creating accounts with different indexes. - function test_DeployAccount_DifferentIndexes() public { - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - bytes memory factoryData1 = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); - bytes memory factoryData2 = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, keccak256("1")); - - address payable accountAddress1 = META_FACTORY.deployWithFactory(address(FACTORY), factoryData1); - address payable accountAddress2 = META_FACTORY.deployWithFactory(address(FACTORY), factoryData2); - - // Validate that the deployed addresses are different - assertTrue(accountAddress1 != accountAddress2, "Accounts with different indexes should have different addresses"); - } - - /// @notice Tests creating accounts with an invalid validator module. - function test_DeployAccount_InvalidValidatorModule() public { - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt); - - // Should revert if the validator module is invalid - BootstrapConfig[] memory validatorsInvalid = BootstrapLib.createArrayConfig(address(0), initData); - bytes memory _initDataInvalidModule = BOOTSTRAPPER.getInitNexusScopedCalldata(validatorsInvalid, hook, REGISTRY, ATTESTERS, THRESHOLD); - - vm.expectRevert(); - address payable accountAddress = FACTORY.createAccount(_initDataInvalidModule, salt); - assertTrue(expectedAddress != accountAddress, "Account address should be different for invalid module"); - } - - /// @notice Tests creating accounts without enough gas. - function test_RevertIf_DeployAccount_InsufficientGas() public { - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - vm.expectRevert(); - // Should revert if there is not enough gas - FACTORY.createAccount{ gas: 1000 }(_initData, salt); - } - - /// @notice Tests creating accounts with multiple modules and data using BootstrapLib. - function test_createArrayConfig_MultipleModules_DeployAccount() public { - address[] memory modules = new address[](2); - bytes[] memory datas = new bytes[](2); - - modules[0] = address(VALIDATOR_MODULE); - modules[1] = address(MULTI_MODULE); - datas[0] = abi.encodePacked(user.addr); - datas[1] = abi.encodePacked(bytes1(uint8(MODULE_TYPE_VALIDATOR)), bytes32(bytes20(user.addr))); - - BootstrapConfig[] memory configArray = BootstrapLib.createMultipleConfigs(modules, datas); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(configArray, hook, REGISTRY, ATTESTERS, THRESHOLD); - - address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt); - - bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); - - address payable deployedAccountAddress = META_FACTORY.deployWithFactory(address(FACTORY), factoryData); - // Validate that the account was deployed correctly - assertEq(deployedAccountAddress, expectedAddress, "Deployed account address mismatch"); - } - - /// @notice Tests initNexusScoped function in NexusBootstrap and uses it to deploy an account with a hook module. - function test_initNexusScoped_WithHook_DeployAccount() public { - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(HOOK_MODULE), abi.encodePacked(user.addr)); - - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt); - - bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); - - address payable deployedAccountAddress = META_FACTORY.deployWithFactory(address(FACTORY), factoryData); - - // Validate that the account was deployed correctly - assertEq(deployedAccountAddress, expectedAddress, "Deployed account address mismatch"); - - // Verify that the validators and hook were installed - assertTrue( - INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), - "Validator should be installed" - ); - assertTrue( - INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), abi.encodePacked(user.addr)), - "Hook should be installed" - ); - } - - /// @notice Tests that the manually computed address matches the one from computeAccountAddress. - function test_ComputeAccountAddress_ManualComparison() public { - // Prepare the initial data and salt - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - // Create initcode and salt to be sent to Factory - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - // Manually compute the actual salt - bytes32 actualSalt = keccak256(abi.encodePacked(_initData, salt)); - // Compute the expected address using the factory's function - address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt); - - // Manually compute the expected address - address payable manualExpectedAddress = payable( - LibClone.predictDeterministicAddressERC1967(FACTORY.ACCOUNT_IMPLEMENTATION(), actualSalt, address(FACTORY)) - ); - - // Validate that both addresses match - assertEq(expectedAddress, manualExpectedAddress, "Manually computed address mismatch"); - } - - /// @notice Tests that the Nexus contract constructor reverts if the entry point address is zero. - function test_Constructor_RevertIf_EntryPointIsZero() public { - address zeroAddress = address(0); - - // Expect the contract deployment to revert with the correct error message - vm.expectRevert(EntryPointCanNotBeZero.selector); - - // Try deploying the Nexus contract with an entry point address of zero - new Nexus(zeroAddress); - } -} diff --git a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.tree b/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.tree deleted file mode 100644 index 9866d9b53..000000000 --- a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.tree +++ /dev/null @@ -1,26 +0,0 @@ -TestAccountFactory_Deployments -└── given the testing environment is initialized - ├── when deploying an account using the factory directly - │ └── it should deploy the account correctly - ├── when deploying an account with the same arguments - │ └── it should return the same address - ├── when deploying an account using handleOps method - │ └── it should deploy the account successfully - ├── when deploying an account that already exists using handleOps - │ └── it should revert - ├── when deploying an account that is already initialized - │ └── it should not allow reinitialization - ├── when deploying accounts with different indexes - │ └── it should deploy to different addresses - ├── when deploying an account with an invalid validator module - │ └── it should revert - ├── when deploying an account with insufficient gas - │ └── it should revert - ├── when creating accounts with multiple modules and data using BootstrapLib - │ └── it should deploy the account correctly - ├── when initializing Nexus with a hook module and deploying an account - │ └── it should deploy the account and install the modules correctly - ├── when the Nexus contract constructor is called with a zero entry point address - │ └── it should revert - └── when manually computing the address using keccak256 - └── it should match the address computed by computeAccountAddress diff --git a/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol index 52dfba2f9..5ba9d5be9 100644 --- a/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol @@ -20,7 +20,13 @@ contract TestBiconomyMetaFactory_Deployments is NexusTest_Base { vm.deal(user.addr, 1 ether); metaFactory = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); mockFactory = address( - new K1ValidatorFactory(address(FACTORY_OWNER.addr), address(ACCOUNT_IMPLEMENTATION), address(VALIDATOR_MODULE), new NexusBootstrap(), REGISTRY) + new K1ValidatorFactory( + address(ACCOUNT_IMPLEMENTATION), + address(FACTORY_OWNER.addr), + address(VALIDATOR_MODULE), + new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))), + REGISTRY + ) ); } diff --git a/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol index 95e0dd623..9170b4e08 100644 --- a/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol @@ -5,6 +5,7 @@ import "../../../utils/NexusTest_Base.t.sol"; import "../../../../../contracts/factory/K1ValidatorFactory.sol"; import "../../../../../contracts/utils/NexusBootstrap.sol"; import "../../../../../contracts/interfaces/INexus.sol"; +import { NexusProxy } from "../../../../../contracts/utils/NexusProxy.sol"; /// @title TestK1ValidatorFactory_Deployments /// @notice Tests for deploying accounts using the K1ValidatorFactory and various methods. @@ -20,21 +21,16 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { user = newWallet("user"); vm.deal(user.addr, 1 ether); initData = abi.encodePacked(user.addr); - bootstrapper = new NexusBootstrap(); - validatorFactory = new K1ValidatorFactory( - address(ACCOUNT_IMPLEMENTATION), - address(FACTORY_OWNER.addr), - address(VALIDATOR_MODULE), - bootstrapper, - REGISTRY - ); + bootstrapper = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); + validatorFactory = + new K1ValidatorFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr), address(VALIDATOR_MODULE), bootstrapper, REGISTRY); } /// @notice Tests if the constructor correctly initializes the factory with the given implementation, K1 Validator, and Bootstrapper addresses. function test_ConstructorInitializesFactory() public { address implementation = address(0x123); address k1Validator = address(0x456); - NexusBootstrap bootstrapperInstance = new NexusBootstrap(); + NexusBootstrap bootstrapperInstance = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); K1ValidatorFactory factory = new K1ValidatorFactory(implementation, FACTORY_OWNER.addr, k1Validator, bootstrapperInstance, REGISTRY); // Verify the implementation address is set correctly @@ -54,7 +50,7 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { function test_ConstructorInitializesWithRegistryAddressZero() public { IERC7484 registry = IERC7484(address(0)); address k1Validator = address(0x456); - NexusBootstrap bootstrapperInstance = new NexusBootstrap(); + NexusBootstrap bootstrapperInstance = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); K1ValidatorFactory factory = new K1ValidatorFactory(address(ACCOUNT_IMPLEMENTATION), FACTORY_OWNER.addr, k1Validator, bootstrapperInstance, registry); // Verify the registry address 0 @@ -129,11 +125,7 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { // Validate that the account was deployed correctly assertEq(deployedAccountAddress, expectedAddress, "Deployed account address mismatch"); - assertEq( - INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), - true, - "Validator should be installed" - ); + assertEq(INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), true, "Validator should be installed"); } /// @notice Tests that computing the account address returns the expected address. @@ -187,10 +179,33 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { // Compute the actual salt manually using keccak256 bytes32 manualSalt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold)); - address expectedAddress = LibClone.predictDeterministicAddressERC1967( - address(validatorFactory.ACCOUNT_IMPLEMENTATION()), - manualSalt, - address(validatorFactory) + // Create the validator configuration using the NexusBootstrap library + BootstrapConfig memory validator = BootstrapLib.createSingleConfig(validatorFactory.K1_VALIDATOR(), abi.encodePacked(eoaOwner)); + + // Get the initialization data for the Nexus account + bytes memory _initData = + validatorFactory.BOOTSTRAPPER().getInitNexusWithSingleValidatorCalldata(validator, validatorFactory.REGISTRY(), attesters, threshold); + + address expectedAddress = payable( + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(validatorFactory), + manualSalt, + keccak256( + abi.encodePacked( + type(NexusProxy).creationCode, + abi.encode(validatorFactory.ACCOUNT_IMPLEMENTATION(), abi.encodeCall(INexus.initializeAccount, _initData)) + ) + ) + ) + ) + ) + ) + ) ); address computedAddress = validatorFactory.computeAccountAddress(eoaOwner, index, attesters, threshold); diff --git a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol index ce4146d9b..1d20b5484 100644 --- a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import "../../../utils/NexusTest_Base.t.sol"; +import { NexusProxy } from "../../../../../contracts/utils/NexusProxy.sol"; /// @title TestNexusAccountFactory_Deployments /// @notice Tests for deploying accounts using the NexusAccountFactory. @@ -83,7 +84,7 @@ contract TestNexusAccountFactory_Deployments is NexusTest_Base { userOps[0] = buildUserOpWithInitAndCalldata(user, initCode, "", address(VALIDATOR_MODULE)); ENTRYPOINT.depositTo{ value: 1 ether }(address(accountAddress)); ENTRYPOINT.handleOps(userOps, payable(user.addr)); - assertEq(IAccountConfig(accountAddress).accountId(), "biconomy.nexus.1.0.0", "Not deployed properly"); + assertEq(IAccountConfig(accountAddress).accountId(), "biconomy.nexus.1.2.0", "Not deployed properly"); } /// @notice Tests that deploying an account fails if it already exists. @@ -108,27 +109,11 @@ contract TestNexusAccountFactory_Deployments is NexusTest_Base { // Create initcode and salt to be sent to Factory bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); - bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); - address payable firstAccountAddress = FACTORY.createAccount(_initData, salt); vm.prank(user.addr); // Even owner cannot reinitialize the account - vm.expectRevert(LinkedList_AlreadyInitialized.selector); - INexus(firstAccountAddress).initializeAccount(factoryData); - } - - /// @notice Tests that account initialization reverts if no validator is installed. - function test_RevertIf_NoValidatorDuringInitialization() public { - BootstrapConfig[] memory emptyValidators; // Empty validators array - BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); - bytes memory saDeploymentIndex = "0"; - bytes32 salt = keccak256(saDeploymentIndex); - - // Create initcode with no validator configuration - bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(emptyValidators, hook, REGISTRY, ATTESTERS, THRESHOLD); - - vm.expectRevert(NoValidatorInstalled.selector); - FACTORY.createAccount(_initData, salt); + vm.expectRevert(NexusInitializationFailed.selector); + INexus(firstAccountAddress).initializeAccount(_initData); } /// @notice Tests creating accounts with different indexes. @@ -192,7 +177,7 @@ contract TestNexusAccountFactory_Deployments is NexusTest_Base { vm.expectRevert(EntryPointCanNotBeZero.selector); // Try deploying the Nexus contract with an entry point address of zero - new Nexus(zeroAddress); + new Nexus(zeroAddress, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); } /// @notice Tests BootstrapLib.createArrayConfig function for multiple modules and data in BootstrapLib and uses it to deploy an account. @@ -249,4 +234,45 @@ contract TestNexusAccountFactory_Deployments is NexusTest_Base { "Hook should be installed" ); } + + /// @notice Tests that the manually computed address matches the one from computeAccountAddress. + function test_ComputeAccountAddress_ManualComparison() public view { + // Prepare the initial data and salt + BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData); + BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); + bytes memory saDeploymentIndex = "0"; + bytes32 salt = keccak256(saDeploymentIndex); + + // Create initcode and salt to be sent to Factory + bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD); + + // Compute the expected address using the factory's function + address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt); + + // Manually compute the expected address + address payable manualExpectedAddress = payable( + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(FACTORY), + salt, + keccak256( + abi.encodePacked( + type(NexusProxy).creationCode, + abi.encode(FACTORY.ACCOUNT_IMPLEMENTATION(), abi.encodeCall(INexus.initializeAccount, _initData)) + ) + ) + ) + ) + ) + ) + ) + ); + + // Validate that both addresses match + assertEq(expectedAddress, manualExpectedAddress, "Manually computed address mismatch"); + } } diff --git a/test/foundry/unit/concrete/gas/TestGas_NexusAccountFactory.t.sol b/test/foundry/unit/concrete/gas/TestGas_NexusAccountFactory.t.sol index 628adbd50..11a34bacc 100644 --- a/test/foundry/unit/concrete/gas/TestGas_NexusAccountFactory.t.sol +++ b/test/foundry/unit/concrete/gas/TestGas_NexusAccountFactory.t.sol @@ -71,7 +71,7 @@ contract TestGas_NexusAccountFactory is TestModuleManagement_Base { /// @notice Validates the creation of a new account. /// @param _account The new account address. function assertValidCreation(Nexus _account) internal { - string memory expected = "biconomy.nexus.1.0.0"; + string memory expected = "biconomy.nexus.1.2.0"; assertEq(_account.accountId(), expected, "AccountConfig should return the expected account ID."); assertTrue( _account.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), "Account should have the validation module installed" diff --git a/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol b/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol index f5853c6ef..8015f77cb 100644 --- a/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol +++ b/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol @@ -3,212 +3,653 @@ pragma solidity ^0.8.27; import "../../../shared/TestModuleManagement_Base.t.sol"; import "../../../../../contracts/mocks/MockHook.sol"; +import { MockSimpleValidator } from "../../../../../contracts/mocks/MockSimpleValidator.sol"; +import { MockPreValidationHook } from "../../../../../contracts/mocks/MockPreValidationHook.sol"; +import { EMERGENCY_UNINSTALL_TYPE_HASH } from "../../../../../contracts/types/Constants.sol"; +import { EmergencyUninstall } from "../../../../../contracts/types/DataTypes.sol"; /// @title TestNexus_Hook_Uninstall /// @notice Tests for handling hooks emergency uninstall contract TestNexus_Hook_Emergency_Uninstall is TestModuleManagement_Base { + MockSimpleValidator SIMPLE_VALIDATOR_MODULE; + /// @notice Sets up the base module management environment. function setUp() public { setUpModuleManagement_Base(); + // Deploy simple validator + SIMPLE_VALIDATOR_MODULE = new MockSimpleValidator(); + + // Format install data with owner + bytes memory validatorSetupData = abi.encodePacked(BOB_ADDRESS); // Set BOB as owner + + // Prepare the call data for installing the validator module + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_VALIDATOR, address(SIMPLE_VALIDATOR_MODULE), validatorSetupData); + + // Install validator module using execution + installModule(callData, MODULE_TYPE_VALIDATOR, address(SIMPLE_VALIDATOR_MODULE), EXECTYPE_DEFAULT); + + // Assert that bob is the owner + assertTrue(SIMPLE_VALIDATOR_MODULE.smartAccountOwners(address(BOB_ACCOUNT)) == BOB_ADDRESS, "Bob should be the owner"); } /// @notice Tests the successful installation of the hook module, then tests initiate emergency uninstall. function test_EmergencyUninstallHook_Initiate_Success() public { // 1. Install the hook - - // Ensure the hook module is not installed initially - assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed initially"); - - // Prepare call data for installing the hook module + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), ""); - - // Install the hook module installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = EmergencyUninstall(address(HOOK_MODULE), MODULE_TYPE_HOOK, "", 0); + // Get the hash of the emergency uninstall data + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); - // Assert that the hook module is now installed - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should be installed"); - - uint256 prevTimeStamp = block.timestamp; - - + // Format signature with validator address prefix + bytes memory signature = abi.encodePacked( + address(SIMPLE_VALIDATOR_MODULE), // First 20 bytes is validator + sign(BOB, hash) // Rest is signature + ); - // 2. Request to uninstall the hook - bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, address(HOOK_MODULE), ""); + bytes memory emergencyUninstallCalldata = abi.encodeWithSelector( + Nexus.emergencyUninstallHook.selector, + emergencyUninstall, // EmergencyUninstall struct + signature + ); - // Initialize the userOps array with one operation PackedUserOperation[] memory userOps = new PackedUserOperation[](1); - userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE), bytes3(0))); + userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(SIMPLE_VALIDATOR_MODULE), bytes3(0))); userOps[0].callData = emergencyUninstallCalldata; bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); - userOps[0].signature = signMessage(BOB, userOpHash); + userOps[0].signature = sign(BOB, userOpHash); vm.expectEmit(true, true, true, true); emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp); ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook MUST still be installed"); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); } function test_EmergencyUninstallHook_Fail_AfterInitiated() public { // 1. Install the hook - - // Ensure the hook module is not installed initially - assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed initially"); - - // Prepare call data for installing the hook module + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), ""); - - // Install the hook module installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = EmergencyUninstall({ hook: address(HOOK_MODULE), hookType: MODULE_TYPE_HOOK, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); - // Assert that the hook module is now installed - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should be installed"); - - uint256 prevTimeStamp = block.timestamp; - - + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); - // 2. Request to uninstall the hook - bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, address(HOOK_MODULE), ""); + bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, emergencyUninstall, signature); - // Initialize the userOps array with one operation PackedUserOperation[] memory userOps = new PackedUserOperation[](1); - userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE), bytes3(0))); + userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(SIMPLE_VALIDATOR_MODULE), bytes3(0))); userOps[0].callData = emergencyUninstallCalldata; bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); - userOps[0].signature = signMessage(BOB, userOpHash); + userOps[0].signature = sign(BOB, userOpHash); vm.expectEmit(true, true, true, true); emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp); ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); - // 3. Try without waiting for time to pass + + // Rebuild the user operation + emergencyUninstall.nonce = 1; + hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, emergencyUninstall, signature); + PackedUserOperation[] memory newUserOps = new PackedUserOperation[](1); - newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE), bytes3(0))); + newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(SIMPLE_VALIDATOR_MODULE), bytes3(0))); newUserOps[0].callData = emergencyUninstallCalldata; bytes32 newUserOpHash = ENTRYPOINT.getUserOpHash(newUserOps[0]); - newUserOps[0].signature = signMessage(BOB, newUserOpHash); + newUserOps[0].signature = sign(BOB, newUserOpHash); bytes memory expectedRevertReason = abi.encodeWithSelector(EmergencyTimeLockNotExpired.selector); // Expect the UserOperationRevertReason event vm.expectEmit(true, true, true, true); - emit UserOperationRevertReason( - newUserOpHash, // userOpHash - address(BOB_ACCOUNT), // sender - newUserOps[0].nonce, // nonce - expectedRevertReason - ); + emit UserOperationRevertReason(newUserOpHash, address(BOB_ACCOUNT), newUserOps[0].nonce, expectedRevertReason); ENTRYPOINT.handleOps(newUserOps, payable(BOB.addr)); - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook MUST still be installed"); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); } function test_EmergencyUninstallHook_Success_LongAfterInitiated() public { // 1. Install the hook - - // Ensure the hook module is not installed initially - assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed initially"); - - // Prepare call data for installing the hook module + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), ""); - - // Install the hook module installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT); - - // Assert that the hook module is now installed - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should be installed"); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); uint256 prevTimeStamp = block.timestamp; + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = EmergencyUninstall({ hook: address(HOOK_MODULE), hookType: MODULE_TYPE_HOOK, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); - // 2. Request to uninstall the hook - bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, address(HOOK_MODULE), ""); + bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, emergencyUninstall, signature); - // Initialize the userOps array with one operation PackedUserOperation[] memory userOps = new PackedUserOperation[](1); - userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE), bytes3(0))); + userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(SIMPLE_VALIDATOR_MODULE), bytes3(0))); userOps[0].callData = emergencyUninstallCalldata; bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); - userOps[0].signature = signMessage(BOB, userOpHash); + userOps[0].signature = sign(BOB, userOpHash); vm.expectEmit(true, true, true, true); emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp); ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); - // 3. Wait for time to pass + // not more than 3 days vm.warp(prevTimeStamp + 2 days); + // Rebuild the user operation + emergencyUninstall.nonce = 1; + hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, emergencyUninstall, signature); + PackedUserOperation[] memory newUserOps = new PackedUserOperation[](1); - newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE), bytes3(0))); + newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(SIMPLE_VALIDATOR_MODULE), bytes3(0))); newUserOps[0].callData = emergencyUninstallCalldata; bytes32 newUserOpHash = ENTRYPOINT.getUserOpHash(newUserOps[0]); - newUserOps[0].signature = signMessage(BOB, newUserOpHash); - // Expect the UserOperationRevertReason event + newUserOps[0].signature = sign(BOB, newUserOpHash); + vm.expectEmit(true, true, true, true); emit ModuleUninstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE)); ENTRYPOINT.handleOps(newUserOps, payable(BOB.addr)); - assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed anymore"); + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); } function test_EmergencyUninstallHook_Success_Reset_SuperLongAfterInitiated() public { // 1. Install the hook - - // Ensure the hook module is not installed initially - assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should not be installed initially"); - - // Prepare call data for installing the hook module + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), ""); - - // Install the hook module installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT); - - // Assert that the hook module is now installed - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should be installed"); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); uint256 prevTimeStamp = block.timestamp; + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = EmergencyUninstall({ hook: address(HOOK_MODULE), hookType: MODULE_TYPE_HOOK, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); - // 2. Request to uninstall the hook - bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, address(HOOK_MODULE), ""); + bytes memory emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, emergencyUninstall, signature); - // Initialize the userOps array with one operation PackedUserOperation[] memory userOps = new PackedUserOperation[](1); - userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE), 0)); + userOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(SIMPLE_VALIDATOR_MODULE), bytes3(0))); userOps[0].callData = emergencyUninstallCalldata; bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); - userOps[0].signature = signMessage(BOB, userOpHash); + userOps[0].signature = sign(BOB, userOpHash); vm.expectEmit(true, true, true, true); emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp); ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); - // 3. Wait for time to pass + // more than 3 days vm.warp(prevTimeStamp + 4 days); + // Rebuild the user operation + emergencyUninstall.nonce = 1; + hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + emergencyUninstallCalldata = abi.encodeWithSelector(Nexus.emergencyUninstallHook.selector, emergencyUninstall, signature); + PackedUserOperation[] memory newUserOps = new PackedUserOperation[](1); - newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(VALIDATOR_MODULE), 0)); + newUserOps[0] = buildPackedUserOp(address(BOB_ACCOUNT), getNonce(address(BOB_ACCOUNT), MODE_VALIDATION, address(SIMPLE_VALIDATOR_MODULE), bytes3(0))); newUserOps[0].callData = emergencyUninstallCalldata; bytes32 newUserOpHash = ENTRYPOINT.getUserOpHash(newUserOps[0]); - newUserOps[0].signature = signMessage(BOB, newUserOpHash); + newUserOps[0].signature = sign(BOB, newUserOpHash); - // Expect the UserOperationRevertReason event vm.expectEmit(true, true, true, true); emit EmergencyHookUninstallRequestReset(address(HOOK_MODULE), block.timestamp); ENTRYPOINT.handleOps(newUserOps, payable(BOB.addr)); - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), ""), "Hook module should still be installed"); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + } + + function test_EmergencyUninstallHook_DirectCall_Success() public { + // 1. Install the hook + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), ""); + installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = EmergencyUninstall({ hook: address(HOOK_MODULE), hookType: MODULE_TYPE_HOOK, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + + vm.prank(address(BOB_ACCOUNT)); + vm.expectEmit(true, true, true, true); + emit EmergencyHookUninstallRequest(address(HOOK_MODULE), block.timestamp); + + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + } + + function test_EmergencyUninstallHook_DirectCall_Fail_WrongSigner() public { + // 1. Install the hook + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(HOOK_MODULE), ""); + installModule(callData, MODULE_TYPE_HOOK, address(HOOK_MODULE), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); + + // 2. Sign with wrong signer (ALICE instead of BOB) + EmergencyUninstall memory emergencyUninstall = EmergencyUninstall({ hook: address(HOOK_MODULE), hookType: MODULE_TYPE_HOOK, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked( + address(SIMPLE_VALIDATOR_MODULE), + sign(ALICE, hash) // ALICE signs instead of BOB + ); + + vm.prank(address(BOB_ACCOUNT)); + vm.expectRevert(EmergencyUninstallSigError.selector); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), "")); } + function test_EmergencyUninstallHook_1271_DirectCall_Success() public { + // 1. Install the 1271 hook + MockPreValidationHook preValidationHook = new MockPreValidationHook(); + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), "")); + + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), ""); + installModule(callData, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), "")); + + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = + EmergencyUninstall({ hook: address(preValidationHook), hookType: MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + + vm.prank(address(BOB_ACCOUNT)); + vm.expectEmit(true, true, true, true); + emit EmergencyHookUninstallRequest(address(preValidationHook), block.timestamp); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), "")); + } + + function test_EmergencyUninstallHook_4337_DirectCall_Success() public { + // 1. Install the 4337 hook + MockPreValidationHook preValidationHook = new MockPreValidationHook(); + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), "")); + + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), ""); + installModule(callData, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), "")); + + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = + EmergencyUninstall({ hook: address(preValidationHook), hookType: MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + + vm.prank(address(BOB_ACCOUNT)); + vm.expectEmit(true, true, true, true); + emit EmergencyHookUninstallRequest(address(preValidationHook), block.timestamp); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), "")); + } + + function test_EmergencyUninstallHook_1271_DirectCall_Fail_WrongSigner() public { + // 1. Install the 1271 hook + MockPreValidationHook preValidationHook = new MockPreValidationHook(); + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), ""); + installModule(callData, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), EXECTYPE_DEFAULT); + + // 2. Sign with wrong signer (ALICE instead of BOB) + EmergencyUninstall memory emergencyUninstall = + EmergencyUninstall({ hook: address(preValidationHook), hookType: MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked( + address(SIMPLE_VALIDATOR_MODULE), + sign(ALICE, hash) // ALICE signs instead of BOB + ); + + vm.prank(address(BOB_ACCOUNT)); + vm.expectRevert(EmergencyUninstallSigError.selector); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + } + + function test_EmergencyUninstallHook_4337_DirectCall_Fail_WrongSigner() public { + // 1. Install the 4337 hook + MockPreValidationHook preValidationHook = new MockPreValidationHook(); + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), ""); + installModule(callData, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), EXECTYPE_DEFAULT); + + // 2. Sign with wrong signer (ALICE instead of BOB) + EmergencyUninstall memory emergencyUninstall = + EmergencyUninstall({ hook: address(preValidationHook), hookType: MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked( + address(SIMPLE_VALIDATOR_MODULE), + sign(ALICE, hash) // ALICE signs instead of BOB + ); + + vm.prank(address(BOB_ACCOUNT)); + vm.expectRevert(EmergencyUninstallSigError.selector); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + } + + function test_EmergencyUninstallHook_PreValidation1271_Uninstall() public { + // 1. Install the 1271 hook + MockPreValidationHook preValidationHook = new MockPreValidationHook(); + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), "")); + + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), ""); + installModule(callData, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), "")); + + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = + EmergencyUninstall({ hook: address(preValidationHook), hookType: MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + + uint256 prevTimeStamp = block.timestamp; + + // Direct call to emergency uninstall + vm.expectEmit(true, true, true, true); + emit EmergencyHookUninstallRequest(address(preValidationHook), block.timestamp); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + // Wait for time to pass + vm.warp(prevTimeStamp + 2 days); + + // Rebuild the request + emergencyUninstall.nonce = 1; + hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + + vm.expectEmit(true, true, true, true); + emit ModuleUninstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook)); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + assertFalse( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(preValidationHook), ""), + "PreValidation 1271 hook should be uninstalled" + ); + } + + function test_EmergencyUninstallHook_PreValidation4337_Uninstall() public { + // 1. Install the 4337 hook + MockPreValidationHook preValidationHook = new MockPreValidationHook(); + assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), "")); + + bytes memory callData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), ""); + installModule(callData, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), EXECTYPE_DEFAULT); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), "")); + + // 2. Sign and request emergency uninstall + EmergencyUninstall memory emergencyUninstall = + EmergencyUninstall({ hook: address(preValidationHook), hookType: MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, deInitData: "", nonce: 0 }); + + bytes32 hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + + bytes memory signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + + uint256 prevTimeStamp = block.timestamp; + + // Initiate uninstall request + vm.expectEmit(true, true, true, true); + emit EmergencyHookUninstallRequest(address(preValidationHook), block.timestamp); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + // Wait for time to pass + vm.warp(prevTimeStamp + 2 days); + + // Perform uninstall + + // Rebuild the request + emergencyUninstall.nonce = 1; + hash = _hashTypedData( + keccak256( + abi.encode( + EMERGENCY_UNINSTALL_TYPE_HASH, + emergencyUninstall.hook, + emergencyUninstall.hookType, + keccak256(emergencyUninstall.deInitData), + emergencyUninstall.nonce + ) + ), + address(BOB_ACCOUNT) + ); + signature = abi.encodePacked(address(SIMPLE_VALIDATOR_MODULE), sign(BOB, hash)); + + vm.expectEmit(true, true, true, true); + emit ModuleUninstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook)); + BOB_ACCOUNT.emergencyUninstallHook(emergencyUninstall, signature); + + assertFalse( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(preValidationHook), ""), + "PreValidation 4337 hook should be uninstalled" + ); + } + + function sign(Vm.Wallet memory wallet, bytes32 hash) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, hash); + return abi.encodePacked(r, s, v); + } } diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol index c6fbd907d..e0081161f 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol @@ -6,8 +6,9 @@ import "../../../utils/NexusTest_Base.t.sol"; import "../../../shared/TestModuleManagement_Base.t.sol"; import "contracts/mocks/Counter.sol"; import { Solarray } from "solarray/Solarray.sol"; -import { MODE_VALIDATION, MODE_MODULE_ENABLE, MODULE_TYPE_MULTI, MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_ENABLE_MODE_TYPE_HASH, MODULE_ENABLE_MODE_NOTATION } from "contracts/types/Constants.sol"; -import "solady/utils/EIP712.sol"; +import { MODE_VALIDATION, MODE_MODULE_ENABLE, MODULE_TYPE_MULTI, MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_ENABLE_MODE_TYPE_HASH } from "contracts/types/Constants.sol"; +import { MockResourceLockPreValidationHook } from "contracts/mocks/MockResourceLockPreValidationHook.sol"; +import { MockAccountLocker } from "contracts/mocks/MockAccountLocker.sol"; contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { @@ -24,13 +25,17 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { MockMultiModule mockMultiModule; Counter public counter; - bytes32 internal constant _DOMAIN_TYPEHASH = - 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + MockResourceLockPreValidationHook private resourceLockHook; + MockAccountLocker private accountLocker; + + string constant MODULE_ENABLE_MODE_NOTATION = "ModuleEnableMode(address module,uint256 moduleType,bytes32 userOpHash,bytes32 initDataHash)"; function setUp() public { setUpModuleManagement_Base(); mockMultiModule = new MockMultiModule(); counter = new Counter(); + accountLocker = new MockAccountLocker(); + resourceLockHook = new MockResourceLockPreValidationHook(address(accountLocker), address(0)); } function test_EnableMode_Success_No7739() public { @@ -79,6 +84,125 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { ); } + function test_EnableMode_Success_No7739_With_PreValidationHooksInstalled() public { + // Install account locker first + bytes memory accountLockerInstallCallData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(accountLocker), ""); + installModule(accountLockerInstallCallData, MODULE_TYPE_HOOK, address(accountLocker), EXECTYPE_DEFAULT); + + // Install resource lock pre-validation 4337 hook + bytes memory resourceLockHook4337InstallCallData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(resourceLockHook), ""); + installModule(resourceLockHook4337InstallCallData, MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(resourceLockHook), EXECTYPE_DEFAULT); + + // Install resource lock pre-validation 1271 hook + bytes memory resourceLockHook1271InstallCallData = + abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(resourceLockHook), ""); + installModule(resourceLockHook1271InstallCallData, MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(resourceLockHook), EXECTYPE_DEFAULT); + + assertTrue( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337, address(resourceLockHook), ""), "Resource lock 4337 hook should be installed" + ); + assertTrue( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271, address(resourceLockHook), ""), "Resource lock 1271 hook should be installed" + ); + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(accountLocker), ""), "Account locker should be installed"); + + address moduleToEnable = address(mockMultiModule); + address opValidator = address(mockMultiModule); + + PackedUserOperation memory op = makeDraftOp(opValidator); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(op); + op.signature = signMessage(ALICE, userOpHash); // SIGN THE USEROP WITH SIGNER THAT IS ABOUT TO BE USED + + (bytes memory multiInstallData, bytes32 hashToSign, ) = makeInstallDataAndHash(address(BOB_ACCOUNT), MODULE_TYPE_MULTI, userOpHash); + + bytes memory enableModeSig = signMessage(BOB, hashToSign); //should be signed by current owner + enableModeSig = abi.encodePacked(address(VALIDATOR_MODULE), enableModeSig); //append validator address + // Enable Mode Sig Prefix + // address moduleToEnable + // uint256 moduleTypeId + // bytes4 initDataLength + // initData + // bytes4 enableModeSig length + // enableModeSig + bytes memory enableModeSigPrefix = abi.encodePacked( + moduleToEnable, + MODULE_TYPE_MULTI, + bytes4(uint32(multiInstallData.length)), + multiInstallData, + bytes4(uint32(enableModeSig.length)), + enableModeSig + ); + + op.signature = abi.encodePacked(enableModeSigPrefix, op.signature); + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = op; + + uint256 counterBefore = counter.getNumber(); + ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); + assertEq(counter.getNumber(), counterBefore+1, "Counter should have been incremented after single execution"); + assertTrue( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(mockMultiModule), ""), + "Module should be installed as validator" + ); + assertTrue( + BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(mockMultiModule), ""), + "Module should be installed as executor" + ); + } + + function test_EnableMode_Uninitialized_7702_Account() public { + address moduleToEnable = address(mockMultiModule); + address opValidator = address(mockMultiModule); + + // make the account out of BOB itself + uint256 nonce = getNonce(BOB_ADDRESS, MODE_MODULE_ENABLE, moduleToEnable, bytes3(0)); + + PackedUserOperation memory op = buildPackedUserOp(BOB_ADDRESS, nonce); + + op.callData = prepareERC7579SingleExecuteCallData( + EXECTYPE_DEFAULT, + address(counter), 0, abi.encodeWithSelector(Counter.incrementNumber.selector) + ); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(op); + op.signature = signMessage(ALICE, userOpHash); // SIGN THE USEROP WITH SIGNER THAT IS ABOUT TO BE USED + + // simulate uninitialized 7702 account + vm.etch(BOB_ADDRESS, address(ACCOUNT_IMPLEMENTATION).code); + + (bytes memory multiInstallData, bytes32 hashToSign, ) = makeInstallDataAndHash(BOB_ADDRESS, MODULE_TYPE_MULTI, userOpHash); + + bytes memory enableModeSig = signMessage(BOB, hashToSign); //should be signed by current owner + enableModeSig = abi.encodePacked(DEFAULT_VALIDATOR_FLAG, enableModeSig); //append validator address + + bytes memory enableModeSigPrefix = abi.encodePacked( + moduleToEnable, + MODULE_TYPE_MULTI, + bytes4(uint32(multiInstallData.length)), + multiInstallData, + bytes4(uint32(enableModeSig.length)), + enableModeSig + ); + + op.signature = abi.encodePacked(enableModeSigPrefix, op.signature); + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = op; + + uint256 counterBefore = counter.getNumber(); + ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); + assertEq(counter.getNumber(), counterBefore+1, "Counter should have been incremented after single execution"); + assertTrue( + INexus(BOB_ADDRESS).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(mockMultiModule), ""), + "Module should be installed as validator" + ); + assertTrue( + INexus(BOB_ADDRESS).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(mockMultiModule), ""), + "Module should be installed as executor" + ); + } + // we do not test 7739 personal sign, as with personal sign makes enable data hash is unreadable function test_EnableMode_Success_7739_Nested_712() public { address moduleToEnable = address(mockMultiModule); @@ -92,7 +216,7 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { (bytes memory multiInstallData, /*bytes32 eip712ChildHash*/, bytes32 structHash) = makeInstallDataAndHash(address(BOB_ACCOUNT), MODULE_TYPE_MULTI, userOpHash); // app is just account itself in this case - bytes32 appDomainSeparator = _buildDomainSeparator(address(BOB_ACCOUNT)); + bytes32 appDomainSeparator = _getDomainSeparator(address(BOB_ACCOUNT)); bytes32 hashToSign = toERC1271Hash(structHash, address(BOB_ACCOUNT), appDomainSeparator); @@ -438,40 +562,6 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { eip712Hash = _hashTypedData(structHash, account); } - function _hashTypedData( - bytes32 structHash, - address account - ) internal view virtual returns (bytes32 digest) { - digest = _buildDomainSeparator(account); - /// @solidity memory-safe-assembly - assembly { - // Compute the digest. - mstore(0x00, 0x1901000000000000) // Store "\x19\x01". - mstore(0x1a, digest) // Store the domain separator. - mstore(0x3a, structHash) // Store the struct hash. - digest := keccak256(0x18, 0x42) - // Restore the part of the free memory slot that was overwritten. - mstore(0x3a, 0) - } - } - - /// @dev Returns the EIP-712 domain separator. - function _buildDomainSeparator(address account) private view returns (bytes32 separator) { - (,string memory name,string memory version,,address verifyingContract,,) = EIP712(address(account)).eip712Domain(); - bytes32 nameHash = keccak256(bytes(name)); - bytes32 versionHash = keccak256(bytes(version)); - /// @solidity memory-safe-assembly - assembly { - let m := mload(0x40) // Load the free memory pointer. - mstore(m, _DOMAIN_TYPEHASH) - mstore(add(m, 0x20), nameHash) // Name hash. - mstore(add(m, 0x40), versionHash) - mstore(add(m, 0x60), chainid()) - mstore(add(m, 0x80), verifyingContract) - separator := keccak256(m, 0xa0) - } - } - /// @notice Generates an ERC-1271 hash for the given contents and account. /// @param contents The contents hash. /// @param account The account address. diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_UninstallModule.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_UninstallModule.t.sol index 468cccbfe..a0169405e 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_UninstallModule.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_UninstallModule.t.sol @@ -146,46 +146,6 @@ contract TestModuleManager_UninstallModule is TestModuleManagement_Base { assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(newMockExecutor), ""), "Module should be installed"); } - /// @notice Tests failure to uninstall the last validator module - function test_RevertIf_UninstallingLastValidator() public { - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), "Module should not be installed initially"); - - // Find the previous module for uninstallation - (address[] memory array, ) = BOB_ACCOUNT.getValidatorsPaginated(address(0x1), 100); - address remove = address(mockValidator); - address prev = SentinelListHelper.findPrevious(array, remove); - if (prev == address(0)) prev = address(0x01); // Default to sentinel address if not found - - // Prepare call data for uninstalling the module - bytes memory callData = abi.encodeWithSelector( - IModuleManager.uninstallModule.selector, - MODULE_TYPE_VALIDATOR, - address(VALIDATOR_MODULE), - abi.encode(prev, "") - ); - - bytes memory expectedRevertReason = abi.encodeWithSignature("CanNotRemoveLastValidator()"); - - Execution[] memory execution = new Execution[](1); - execution[0] = Execution(address(BOB_ACCOUNT), 0, callData); - - // Prepare the user operation for uninstalling the module - PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); - bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); - - // Expect the UserOperationRevertReason event - vm.expectEmit(true, true, true, true); - emit UserOperationRevertReason( - userOpHash, // userOpHash - address(BOB_ACCOUNT), // sender - userOps[0].nonce, // nonce - expectedRevertReason - ); - - // Execute the user operation - ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); - } - /// @notice Tests uninstallation with incorrect module type function test_RevertIf_IncorrectModuleTypeUninstallation() public { assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), "Module should not be installed initially"); @@ -361,54 +321,6 @@ contract TestModuleManager_UninstallModule is TestModuleManagement_Base { assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(mockValidator), ""), "Module should not be installed"); } - /// @notice Tests reverting when uninstalling the last validator - function test_RevertIf_UninstallingLastValidatorModule() public { - bytes memory customData = abi.encode(GENERIC_FALLBACK_SELECTOR); - - assertTrue( - BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), customData), - "Module should not be installed initially" - ); - - // Find the previous module for uninstallation - (address[] memory array, ) = BOB_ACCOUNT.getValidatorsPaginated(address(0x1), 100); - address remove = address(VALIDATOR_MODULE); - address prev = SentinelListHelper.findPrevious(array, remove); - - // Prepare call data for uninstalling the last validator module - bytes memory callData = abi.encodeWithSelector( - IModuleManager.uninstallModule.selector, - MODULE_TYPE_VALIDATOR, - remove, - abi.encode(prev, customData) - ); - - Execution[] memory execution = new Execution[](1); - execution[0] = Execution(address(BOB_ACCOUNT), 0, callData); - - // Prepare the user operation for uninstalling the module - PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); - - bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); - - // Define expected revert reason - bytes memory expectedRevertReason = abi.encodeWithSignature("CanNotRemoveLastValidator()"); - - // Expect the UserOperationRevertReason event - vm.expectEmit(true, true, true, true); - emit UserOperationRevertReason( - userOpHash, // userOpHash - address(BOB_ACCOUNT), // sender - userOps[0].nonce, // nonce - expectedRevertReason - ); - - // Execute the user operation - ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); - - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), customData), "Module should be installed"); - } - /// @notice Tests successful uninstallation of the fallback handler module function test_SuccessfulUninstallationOfFallbackHandler() public { bytes memory customData = abi.encode(bytes4(GENERIC_FALLBACK_SELECTOR)); diff --git a/test/foundry/unit/concrete/modules/TestK1Validator.t.sol b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol index 47e5dad62..982ce7acf 100644 --- a/test/foundry/unit/concrete/modules/TestK1Validator.t.sol +++ b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol @@ -64,7 +64,7 @@ contract TestK1Validator is NexusTest_Base { validator.onInstall(abi.encodePacked(ALICE_ADDRESS)); - assertEq(validator.smartAccountOwners(address(ALICE_ACCOUNT)), ALICE_ADDRESS, "Owner should be correctly set"); + assertEq(validator.getOwner(address(ALICE_ACCOUNT)), ALICE_ADDRESS, "Owner should be correctly set"); } /// @notice Tests the onInstall function with no initialization data, expecting a revert @@ -91,7 +91,7 @@ contract TestK1Validator is NexusTest_Base { ENTRYPOINT.handleOps(userOps, payable(BOB.addr)); - assertEq(validator.smartAccountOwners(address(BOB_ACCOUNT)), address(0), "Owner should be removed"); + assertEq(validator.getOwner(address(BOB_ACCOUNT)), address(BOB_ACCOUNT), "Owner should be removed"); assertFalse(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator), "")); } @@ -158,7 +158,7 @@ contract TestK1Validator is NexusTest_Base { validator.transferOwnership(ALICE_ADDRESS); // Verify that the ownership is transferred - assertEq(validator.smartAccountOwners(address(BOB_ACCOUNT)), ALICE_ADDRESS, "Ownership should be transferred to ALICE"); + assertEq(validator.getOwner(address(BOB_ACCOUNT)), ALICE_ADDRESS, "Ownership should be transferred to ALICE"); stopPrank(); } @@ -201,20 +201,11 @@ contract TestK1Validator is NexusTest_Base { assertFalse(result, "Module type should be invalid"); } - /// @notice Ensures the transferOwnership function reverts when transferring to a contract address - function test_RevertWhen_TransferOwnership_ToContract() public { - startPrank(address(BOB_ACCOUNT)); - - // Deploy a dummy contract to use as the new owner - address dummyContract = address(new K1Validator()); - - // Expect the NewOwnerIsContract error to be thrown - vm.expectRevert(K1Validator.NewOwnerIsContract.selector); - - // Attempt to transfer ownership to the dummy contract address - validator.transferOwnership(dummyContract); - - stopPrank(); + /// @notice Tests that the account address is returned as owner if no owner is set for the account + function test_returns_AccountAddress_as_owner_if_owner_not_set_for_Account() public { + address account = address(0x7702770277027702770277027702770277027702); + address owner = validator.getOwner(account); + assertEq(owner, account, "Owner should be the account address"); } /// @notice Tests that a valid signature with a valid 's' value is accepted @@ -383,7 +374,7 @@ contract TestK1Validator is NexusTest_Base { abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("Nexus"), - keccak256("1.0.1"), + keccak256("1.2.0"), block.chainid, address(BOB_ACCOUNT) ) diff --git a/test/foundry/unit/fuzz/TestFuzz_ExecuteFromExecutor.t.sol b/test/foundry/unit/fuzz/TestFuzz_ExecuteFromExecutor.t.sol index 78c0696b0..c7ae9796a 100644 --- a/test/foundry/unit/fuzz/TestFuzz_ExecuteFromExecutor.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_ExecuteFromExecutor.t.sol @@ -45,7 +45,7 @@ contract TestFuzz_ExecuteFromExecutor is NexusTest_Base { /// @param target The target address for the execution /// @param value The value to be transferred in the execution function testFuzz_ExecuteSingleFromExecutor(address target, uint256 value) public { - vm.assume(uint160(address(target)) > 10); + vm.assume(uint160(address(target)) > 255); vm.assume(!isContract(target)); vm.assume(value < 1_000_000_000 ether); vm.deal(address(BOB_ACCOUNT), value); diff --git a/test/foundry/utils/EventsAndErrors.sol b/test/foundry/utils/EventsAndErrors.sol index a35390c89..50d8d3e6a 100644 --- a/test/foundry/utils/EventsAndErrors.sol +++ b/test/foundry/utils/EventsAndErrors.sol @@ -43,11 +43,11 @@ contract EventsAndErrors { error InvalidFactoryAddress(); error InvalidEntryPointAddress(); error InnerCallFailed(); + error EmergencyUninstallSigError(); error CallToDeployWithFactoryFailed(); error NexusInitializationFailed(); error InvalidThreshold(uint8 providedThreshold, uint256 attestersCount); - // ========================== // Operation Errors // ========================== diff --git a/test/foundry/utils/Imports.sol b/test/foundry/utils/Imports.sol index 7b6ef3382..394b42d2b 100644 --- a/test/foundry/utils/Imports.sol +++ b/test/foundry/utils/Imports.sol @@ -47,6 +47,7 @@ import "../../../contracts/factory/NexusAccountFactory.sol"; import "../../../contracts/factory/RegistryFactory.sol"; import "./../../../contracts/modules/validators/K1Validator.sol"; import "../../../contracts/common/Stakeable.sol"; +import "../../../contracts/mocks/ExposedNexus.sol"; // ========================== // Mock Contracts for Testing diff --git a/test/foundry/utils/TestHelper.t.sol b/test/foundry/utils/TestHelper.t.sol index e6154aa6b..9c250e30d 100644 --- a/test/foundry/utils/TestHelper.t.sol +++ b/test/foundry/utils/TestHelper.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + +import "forge-std/console2.sol"; import "solady/utils/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { EntryPoint } from "account-abstraction/core/EntryPoint.sol"; @@ -19,15 +21,22 @@ import { MockDelegateTarget } from "../../../contracts/mocks/MockDelegateTarget. import { MockValidator } from "../../../contracts/mocks/MockValidator.sol"; import { MockMultiModule } from "contracts/mocks/MockMultiModule.sol"; import { MockPaymaster } from "./../../../contracts/mocks/MockPaymaster.sol"; +import { MockTarget } from "../../../contracts/mocks/MockTarget.sol"; import { NexusBootstrap, BootstrapConfig } from "../../../contracts/utils/NexusBootstrap.sol"; import { BiconomyMetaFactory } from "../../../contracts/factory/BiconomyMetaFactory.sol"; import { NexusAccountFactory } from "../../../contracts/factory/NexusAccountFactory.sol"; import { BootstrapLib } from "../../../contracts/lib/BootstrapLib.sol"; -import { MODE_VALIDATION, SUPPORTS_ERC7739_V1 } from "../../../contracts/types/Constants.sol"; import { MockRegistry } from "../../../contracts/mocks/MockRegistry.sol"; -import { HelperConfig } from "../../../scripts/foundry/HelperConfig.s.sol"; +import { EIP712 } from "solady/utils/EIP712.sol"; +import "../../../contracts/types/Constants.sol"; contract TestHelper is CheatCodes, EventsAndErrors { + + address private constant MAINNET_ENTRYPOINT_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + /// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. + bytes32 internal constant _DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + // ----------------------------------------- // State Variables // ----------------------------------------- @@ -59,6 +68,7 @@ contract TestHelper is CheatCodes, EventsAndErrors { MockHandler internal HANDLER_MODULE; MockExecutor internal EXECUTOR_MODULE; MockValidator internal VALIDATOR_MODULE; + MockValidator internal DEFAULT_VALIDATOR_MODULE; MockMultiModule internal MULTI_MODULE; Nexus internal ACCOUNT_IMPLEMENTATION; @@ -104,9 +114,10 @@ contract TestHelper is CheatCodes, EventsAndErrors { } function deployTestContracts() internal { - HelperConfig helperConfig = new HelperConfig(); - ENTRYPOINT = helperConfig.ENTRYPOINT(); - ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT)); + setupEntrypoint(); + DEFAULT_VALIDATOR_MODULE = new MockValidator(); + // This is the implementation of the account => default module initialized with an unusable configuration + ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT), address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xeEeEeEeE))); FACTORY = new NexusAccountFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr)); META_FACTORY = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); vm.prank(FACTORY_OWNER.addr); @@ -116,10 +127,23 @@ contract TestHelper is CheatCodes, EventsAndErrors { EXECUTOR_MODULE = new MockExecutor(); VALIDATOR_MODULE = new MockValidator(); MULTI_MODULE = new MockMultiModule(); - BOOTSTRAPPER = new NexusBootstrap(); + BOOTSTRAPPER = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xa11ce))); REGISTRY = new MockRegistry(); } + function setupEntrypoint() internal { + if (block.chainid == 31337) { + if(address(ENTRYPOINT) != address(0)){ + return; + } + ENTRYPOINT = new EntryPoint(); + vm.etch(address(MAINNET_ENTRYPOINT_ADDRESS), address(ENTRYPOINT).code); + ENTRYPOINT = IEntryPoint(MAINNET_ENTRYPOINT_ADDRESS); + } else { + ENTRYPOINT = IEntryPoint(MAINNET_ENTRYPOINT_ADDRESS); + } + } + // ----------------------------------------- // Account Deployment Functions // ----------------------------------------- @@ -193,8 +217,7 @@ contract TestHelper is CheatCodes, EventsAndErrors { bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); // Prepend the factory address to the encoded function call to form the initCode - initCode = - abi.encodePacked(address(META_FACTORY), abi.encodeWithSelector(META_FACTORY.deployWithFactory.selector, address(FACTORY), factoryData)); + initCode = abi.encodePacked(address(META_FACTORY), abi.encodeWithSelector(META_FACTORY.deployWithFactory.selector, address(FACTORY), factoryData)); } /// @notice Prepares a user operation with init code and call data @@ -309,8 +332,9 @@ contract TestHelper is CheatCodes, EventsAndErrors { /// @param messageHash The hash of the message to sign /// @return signature The packed signature function signMessage(Vm.Wallet memory wallet, bytes32 messageHash) internal pure returns (bytes memory signature) { - bytes32 userOpHash = ECDSA.toEthSignedMessageHash(messageHash); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, userOpHash); + messageHash = ECDSA.toEthSignedMessageHash(messageHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, messageHash); signature = abi.encodePacked(r, s, v); } @@ -318,18 +342,14 @@ contract TestHelper is CheatCodes, EventsAndErrors { /// @param execType The execution type /// @param executions The executions to include /// @return executionCalldata The prepared callData - function prepareERC7579ExecuteCallData( - ExecType execType, - Execution[] memory executions - ) internal virtual view returns (bytes memory executionCalldata) { + function prepareERC7579ExecuteCallData(ExecType execType, Execution[] memory executions) internal view virtual returns (bytes memory executionCalldata) { // Determine mode and calldata based on callType and executions length ExecutionMode mode; uint256 length = executions.length; if (length == 1) { mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleSingle() : ModeLib.encodeTrySingle(); - executionCalldata = - abi.encodeCall(Nexus.execute, (mode, ExecLib.encodeSingle(executions[0].target, executions[0].value, executions[0].callData))); + executionCalldata = abi.encodeCall(Nexus.execute, (mode, ExecLib.encodeSingle(executions[0].target, executions[0].value, executions[0].callData))); } else if (length > 1) { mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleBatch() : ModeLib.encodeTryBatch(); executionCalldata = abi.encodeCall(Nexus.execute, (mode, ExecLib.encodeBatch(executions))); @@ -345,17 +365,19 @@ contract TestHelper is CheatCodes, EventsAndErrors { /// @param data The call data /// @return executionCalldata The prepared callData function prepareERC7579SingleExecuteCallData( - ExecType execType, + ExecType execType, address target, uint256 value, bytes memory data - ) internal virtual view returns (bytes memory executionCalldata) { + ) + internal + view + virtual + returns (bytes memory executionCalldata) + { ExecutionMode mode; mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleSingle() : ModeLib.encodeTrySingle(); - executionCalldata = abi.encodeCall( - Nexus.execute, - (mode, ExecLib.encodeSingle(target, value, data)) - ); + executionCalldata = abi.encodeCall(Nexus.execute, (mode, ExecLib.encodeSingle(target, value, data))); } /// @notice Prepares a packed user operation with specified parameters @@ -370,8 +392,12 @@ contract TestHelper is CheatCodes, EventsAndErrors { ExecType execType, Execution[] memory executions, address validator, - uint256 nonce - ) internal view returns (PackedUserOperation[] memory userOps) { + uint256 nonce + ) + internal + view + returns (PackedUserOperation[] memory userOps) + { // Validate execType require(execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY, "Invalid ExecType"); @@ -379,7 +405,7 @@ contract TestHelper is CheatCodes, EventsAndErrors { userOps = new PackedUserOperation[](1); uint256 nonceToUse; - if(nonce == 0) { + if (nonce == 0) { nonceToUse = getNonce(address(account), MODE_VALIDATION, validator, bytes3(0)); } else { nonceToUse = nonce; @@ -504,16 +530,7 @@ contract TestHelper is CheatCodes, EventsAndErrors { } /// @notice Helper function to execute a single operation. - function executeSingle( - Vm.Wallet memory user, - Nexus userAccount, - address target, - uint256 value, - bytes memory callData, - ExecType execType - ) - internal - { + function executeSingle(Vm.Wallet memory user, Nexus userAccount, address target, uint256 value, bytes memory callData, ExecType execType) internal { Execution[] memory executions = new Execution[](1); executions[0] = Execution({ target: target, value: value, callData: callData }); @@ -646,4 +663,42 @@ contract TestHelper is CheatCodes, EventsAndErrors { return finalPmData; } + + function _hashTypedData(bytes32 structHash, address account) internal view virtual returns (bytes32 digest) { + // We will use `digest` to store the domain separator to save a bit of gas. + digest = _getDomainSeparator(account); + + /// @solidity memory-safe-assembly + assembly { + // Compute the digest. + mstore(0x00, 0x1901000000000000) // Store "\x19\x01". + mstore(0x1a, digest) // Store the domain separator. + mstore(0x3a, structHash) // Store the struct hash. + digest := keccak256(0x18, 0x42) + // Restore the part of the free memory slot that was overwritten. + mstore(0x3a, 0) + } + } + + function _getDomainSeparator(address account) internal view virtual returns (bytes32 separator) { + ( + , + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + , + ) = EIP712(account).eip712Domain(); + separator = keccak256(bytes(name)); + bytes32 versionHash = keccak256(bytes(version)); + assembly { + let m := mload(0x40) // Load the free memory pointer. + mstore(m, _DOMAIN_TYPEHASH) + mstore(add(m, 0x20), separator) // Name hash. + mstore(add(m, 0x40), versionHash) + mstore(add(m, 0x60), chainId) + mstore(add(m, 0x80), verifyingContract) + separator := keccak256(m, 0xa0) + } + } } diff --git a/test/hardhat/smart-account/Nexus.Basics.specs.ts b/test/hardhat/smart-account/Nexus.Basics.specs.ts index 0ac6613c6..371c06dc0 100644 --- a/test/hardhat/smart-account/Nexus.Basics.specs.ts +++ b/test/hardhat/smart-account/Nexus.Basics.specs.ts @@ -63,6 +63,8 @@ describe("Nexus Basic Specs", function () { let bundlerAddress: AddressLike; let counter: Counter; let validatorModule: MockValidator; + let defaultValidator: MockValidator; + let defaultValidatorAddress: AddressLike; let deployer: Signer; let aliceOwner: Signer; let provider: Provider; @@ -76,6 +78,7 @@ describe("Nexus Basic Specs", function () { addresses = setup.addresses; counter = setup.counter; validatorModule = setup.mockValidator; + defaultValidator = setup.defaultValidator; smartAccountOwner = setup.accountOwner; deployer = setup.deployer; aliceOwner = setup.aliceAccountOwner; @@ -88,6 +91,7 @@ describe("Nexus Basic Specs", function () { ownerAddress = await smartAccountOwner.getAddress(); bundler = ethers.Wallet.createRandom(); bundlerAddress = await bundler.getAddress(); + defaultValidatorAddress = await defaultValidator.getAddress(); const accountOwnerAddress = ownerAddress; @@ -133,7 +137,7 @@ describe("Nexus Basic Specs", function () { describe("Smart Account Basics", function () { it("Should correctly return the Nexus's ID", async function () { - expect(await smartAccount.accountId()).to.equal("biconomy.nexus.1.0.0"); + expect(await smartAccount.accountId()).to.equal("biconomy.nexus.1.2.0"); }); it("Should get implementation address of smart account", async () => { @@ -159,11 +163,6 @@ describe("Nexus Basic Specs", function () { expect(entryPointFromContract).to.be.equal(entryPoint); }); - it("Should get domain separator", async () => { - const domainSeparator = await smartAccount.DOMAIN_SEPARATOR(); - expect(domainSeparator).to.not.equal(ZeroAddress); - }); - it("Should verify supported account modes", async function () { expect( await smartAccount.supportsExecutionMode( @@ -317,8 +316,24 @@ describe("Nexus Basic Specs", function () { // Define constants as per the original Solidity function const PARENT_TYPEHASH = "PersonalSign(bytes prefixed)"; - // Calculate the domain separator - const domainSeparator = await smartAccount.DOMAIN_SEPARATOR(); + const _DOMAIN_TYPEHASH = ethers.keccak256( + ethers.toUtf8Bytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + ); + + const [fields, name, version, chainId, verifyingContract, salt, extensions] = await smartAccount.eip712Domain(); + + const nameHash = ethers.keccak256(ethers.toUtf8Bytes(name)); + const versionHash = ethers.keccak256(ethers.toUtf8Bytes(version)); + + // corect this => mimic abi.encode , not encodePacked + + const packedData = ethers.AbiCoder.defaultAbiCoder().encode( + ["bytes32", "bytes32", "bytes32", "uint256", "address"], + [_DOMAIN_TYPEHASH, nameHash, versionHash, chainId, verifyingContract] + ); + + // Compute the Keccak-256 hash of the packed data + const domainSeparator = ethers.keccak256(packedData); // Calculate the parent struct hash const parentStructHash = ethers.keccak256( @@ -512,7 +527,7 @@ describe("Nexus Basic Specs", function () { it("should revert if EntryPoint is zero", async function () { const NexusFactory = await ethers.getContractFactory("Nexus"); await expect( - NexusFactory.deploy(ZeroAddress), + NexusFactory.deploy(ZeroAddress, defaultValidatorAddress, "0x"), ).to.be.revertedWithCustomError(NexusFactory, "EntryPointCanNotBeZero"); }); @@ -569,7 +584,7 @@ describe("Nexus Basic Specs", function () { // Deploy a new Nexus implementation const NewNexusFactory = await ethers.getContractFactory("Nexus"); const deployedNewNexusImplementation = - await NewNexusFactory.deploy(entryPointAddress); + await NewNexusFactory.deploy(entryPointAddress, defaultValidatorAddress, "0x"); await deployedNewNexusImplementation.waitForDeployment(); newImplementation = await deployedNewNexusImplementation.getAddress(); @@ -930,38 +945,4 @@ describe("Nexus Basic Specs", function () { }); }); - describe("Smart Account Typed Data Hashing", function () { - it("Should correctly hash the structured data", async function () { - const structuredDataHash = ethers.keccak256( - ethers.toUtf8Bytes("Structured Data"), - ); - - // Impersonate the smart account - const impersonatedSmartAccount = await impersonateAccount( - smartAccountAddress.toString(), - ); - - // Fetch the domain separator used in the smart contract - const domainSeparator = await smartAccount.DOMAIN_SEPARATOR(); - - // Manually compute the expected hash for comparison - const expectedHash = ethers.keccak256( - ethers.concat([ - "0x1901", // EIP-191 prefix - domainSeparator, - structuredDataHash, - ]), - ); - - // Get the actual result from the smart contract - const result = await smartAccount - .connect(impersonatedSmartAccount) - .hashTypedData(structuredDataHash); - - expect(result).to.equal(expectedHash); - - // Stop impersonating the smart account - await stopImpersonateAccount(smartAccountAddress.toString()); - }); - }); }); diff --git a/test/hardhat/smart-account/Nexus.Factory.specs.ts b/test/hardhat/smart-account/Nexus.Factory.specs.ts index 17eb94403..abf427552 100644 --- a/test/hardhat/smart-account/Nexus.Factory.specs.ts +++ b/test/hardhat/smart-account/Nexus.Factory.specs.ts @@ -165,10 +165,8 @@ describe("Nexus Factory Tests", function () { }); it("Should prevent account reinitialization", async function () { - const response = smartAccount.initializeAccount("0x"); - await expect(response).to.be.revertedWithCustomError( - smartAccount, - "LinkedList_AlreadyInitialized()", + await expect(smartAccount.initializeAccount("0x00000000000000000000000000000000123456784e4e4e4e")).to.be.rejectedWith( + "reverted with an unrecognized custom error (return data: 0xaed59595)", // NotInitializable() ); }); }); @@ -205,12 +203,12 @@ describe("Nexus Factory Tests", function () { const validator = { module: await validatorModule.getAddress(), data: solidityPacked(["address"], [ownerAddress]), - } + }; const hook = { module: await hookModule.getAddress(), data: "0x", - } + }; parsedValidator = { module: validator.module, @@ -286,7 +284,7 @@ describe("Nexus Factory Tests", function () { const salt = keccak256("0x"); const factoryData = factory.interface.encodeFunctionData( "createAccount", - ["0x", salt], + ["0xffffffff", salt], ); await expect( metaFactory.deployWithFactory(await factory.getAddress(), factoryData), @@ -334,16 +332,16 @@ describe("Nexus Factory Tests", function () { const validator = { module: await validatorModule.getAddress(), - data: solidityPacked(["address"], [ownerAddress]), - } + data: solidityPacked(["address"], [ownerAddress]), + }; const executor = { module: await executorModule.getAddress(), data: "0x", - } + }; const hook = { module: await hookModule.getAddress(), data: "0x", - } + }; parsedValidator = { module: validator.module, @@ -441,26 +439,6 @@ describe("Nexus Factory Tests", function () { "NexusInitializationFailed", ); }); - - it("Should revert with NoValidatorInstalled if no validator is installed after initialization", async function () { - // Set up a valid bootstrap address but do not include any validators in the initData - const validBootstrapAddress = await owner.getAddress(); - const bootstrapData = "0x"; // Valid but does not install any validators - - const initData = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "bytes"], - [validBootstrapAddress, bootstrapData], - ); - - const salt = keccak256("0x"); - - await expect( - factory.createAccount(initData, salt), - ).to.be.revertedWithCustomError( - smartAccountImplementation, - "NoValidatorInstalled", - ); - }); }); describe("RegistryFactory", function () { @@ -515,21 +493,21 @@ describe("Nexus Factory Tests", function () { registryFactory = registryFactory.connect(owner); ownerAddress = await owner.getAddress(); - + const validator = { module: await validatorModule.getAddress(), data: solidityPacked(["address"], [ownerAddress]), - } + }; const executor = { module: await executorModule.getAddress(), data: "0x", - } + }; const hook = { module: await hookModule.getAddress(), data: "0x", - } + }; parsedValidator = { module: validator[0], diff --git a/test/hardhat/smart-account/Nexus.ModuleManager.specs.ts b/test/hardhat/smart-account/Nexus.ModuleManager.specs.ts index 7585248b3..230ddde87 100644 --- a/test/hardhat/smart-account/Nexus.ModuleManager.specs.ts +++ b/test/hardhat/smart-account/Nexus.ModuleManager.specs.ts @@ -160,33 +160,6 @@ describe("Nexus Module Management Tests", () => { ).to.be.revertedWithCustomError(deployedNexus, "MismatchModuleTypeId"); }); - it("Should not be able to uninstall last validator module", async () => { - let prevAddress = "0x0000000000000000000000000000000000000001"; - const functionCalldata = deployedNexus.interface.encodeFunctionData( - "uninstallModule", - [ - ModuleType.Validation, - await mockValidator.getAddress(), - encodeData( - ["address", "bytes"], - [prevAddress, ethers.hexlify(ethers.toUtf8Bytes(""))], - ), - ], - ); - - await expect( - mockExecutor.executeViaAccount( - await deployedNexus.getAddress(), - await deployedNexus.getAddress(), - 0n, - functionCalldata, - ), - ).to.be.revertedWithCustomError( - deployedNexus, - "CanNotRemoveLastValidator()", - ); - }); - it("Should revert with AccountAccessUnauthorized", async () => { const installModuleData = deployedNexus.interface.encodeFunctionData( "installModule", diff --git a/test/hardhat/utils/deployment.ts b/test/hardhat/utils/deployment.ts index cbe41592f..a914d4b4f 100644 --- a/test/hardhat/utils/deployment.ts +++ b/test/hardhat/utils/deployment.ts @@ -64,6 +64,22 @@ async function getDeployedEntrypoint() { return Contract.attach(ENTRY_POINT_V7) as EntryPoint; } +export async function getDeployedBootstrap(defaultValidator: string): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const NexusBootstrap = await ethers.getContractFactory("NexusBootstrap"); + const deterministicNexusBootstrap = await deployments.deploy("NexusBootstrap", { + from: addresses[0], + deterministicDeployment: true, + args: [defaultValidator, 0x000000000000000000000000000000000000eEeE], + }); + + return NexusBootstrap.attach(deterministicNexusBootstrap.address) as NexusBootstrap; +} + /** * Deploys the K1ValidatorFactory contract with a deterministic deployment. * @returns A promise that resolves to the deployed EntryPoint contract instance. @@ -268,12 +284,11 @@ export async function getDeployedMetaFactory(): Promise { * Deploys the NexusAccountFactory contract with a deterministic deployment. * @returns A promise that resolves to the deployed NexusAccountFactory contract instance. */ -export async function getDeployedNexusAccountFactory(): Promise { +export async function getDeployedNexusAccountFactory(smartAccountImplementation: string): Promise { const accounts: Signer[] = await ethers.getSigners(); const addresses = await Promise.all( accounts.map((account) => account.getAddress()), ); - const smartAccountImplementation = await getDeployedNexusImplementation(); const NexusAccountFactory = await ethers.getContractFactory( "NexusAccountFactory", ); @@ -282,7 +297,7 @@ export async function getDeployedNexusAccountFactory(): Promise { * Deploys the (Nexus) Smart Account implementation contract with a deterministic deployment. * @returns A promise that resolves to the deployed SA implementation contract instance. */ -export async function getDeployedNexusImplementation(): Promise { +export async function getDeployedNexusImplementation(defaultValidator: string): Promise { const accounts: Signer[] = await ethers.getSigners(); const addresses = await Promise.all( accounts.map((account) => account.getAddress()), @@ -323,7 +338,7 @@ export async function getDeployedNexusImplementation(): Promise { const Nexus = await ethers.getContractFactory("Nexus"); const deterministicNexusImpl = await deployments.deploy("Nexus", { from: addresses[0], - args: [ENTRY_POINT_V7], + args: [ENTRY_POINT_V7, defaultValidator, 0x000000000000000000000000000000000000eEeE], deterministicDeployment: true, }); @@ -367,20 +382,22 @@ export async function deployContractsFixture(): Promise { const entryPoint = await getDeployedEntrypoint(); - const smartAccountImplementation = await getDeployedNexusImplementation(); - - const mockValidator = await deployContract( + const defaultValidator = await deployContract( "MockValidator", deployer, ); - const registry = await getDeployedRegistry(); + const smartAccountImplementation = await getDeployedNexusImplementation(await defaultValidator.getAddress()); + + const bootstrap = await getDeployedBootstrap(await defaultValidator.getAddress()); - const bootstrap = await deployContract( - "NexusBootstrap", + const mockValidator = await deployContract( + "MockValidator", deployer, ); + const registry = await getDeployedRegistry(); + const nexusFactory = await getDeployedAccountK1Factory( await smartAccountImplementation.getAddress(), factoryOwner, @@ -429,8 +446,6 @@ export async function deployContractsAndSAFixture(): Promise( @@ -438,10 +453,15 @@ export async function deployContractsAndSAFixture(): Promise( - "NexusBootstrap", + const defaultValidator = await deployContract( + "MockValidator", deployer, ); + + const smartAccountImplementation = await getDeployedNexusImplementation(await defaultValidator.getAddress()); + + const bootstrap = await getDeployedBootstrap(await defaultValidator.getAddress()); + const BootstrapLib = await deployContract( "BootstrapLib", deployer, @@ -473,7 +493,7 @@ export async function deployContractsAndSAFixture(): Promise