From 3c08abdf6a479a0d42e3ed7b6af7bb794246ca8c Mon Sep 17 00:00:00 2001 From: highskore Date: Tue, 10 Dec 2024 11:34:12 +0100 Subject: [PATCH 01/56] feat: add eip7702 support --- contracts/Nexus.sol | 93 ++++++- contracts/base/ModuleManager.sol | 31 ++- contracts/factory/K1ValidatorFactory.sol | 51 ++-- contracts/factory/NexusAccountFactory.sol | 20 +- contracts/factory/RegistryFactory.sol | 31 +-- contracts/lib/Initializable.sol | 34 +++ contracts/lib/ProxyLib.sol | 48 ++++ contracts/mocks/MockTarget.sol | 11 + contracts/mocks/MockValidator.sol | 14 +- contracts/utils/NexusBootstrap.sol | 36 ++- contracts/utils/NexusProxy.sol | 20 ++ .../unit/concrete/eip7702/TestEIP7702.t.sol | 238 ++++++++++++++++++ .../TestAccountFactory_Deployments.t.sol | 33 ++- .../TestBiconomyMetaFactory_Deployments.t.sol | 2 +- .../TestK1ValidatorFactory_Deployments.t.sol | 47 ++-- test/foundry/utils/TestHelper.t.sol | 47 ++-- 16 files changed, 601 insertions(+), 155 deletions(-) create mode 100644 contracts/lib/Initializable.sol create mode 100644 contracts/lib/ProxyLib.sol create mode 100644 contracts/mocks/MockTarget.sol create mode 100644 contracts/utils/NexusProxy.sol create mode 100644 test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 265dc1de3..9ba419789 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -21,10 +21,31 @@ 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, + 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 { ECDSA } from "solady/utils/ECDSA.sol"; +import { Initializable } from "./lib/Initializable.sol"; /// @title Nexus - Smart Account /// @notice This contract integrates various functionalities to handle modular smart accounts compliant with ERC-7579 and ERC-4337 standards. @@ -39,6 +60,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra using ExecLib for bytes; using NonceLib for uint256; using SentinelListLib for SentinelListLib.SentinelList; + using ECDSA for bytes32; /// @dev The timelock period for emergency hook uninstallation. uint256 internal constant _EMERGENCY_TIMELOCK = 1 days; @@ -78,7 +100,13 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra PackedUserOperation calldata op, bytes32 userOpHash, uint256 missingAccountFunds - ) external virtual payPrefund(missingAccountFunds) onlyEntryPoint returns (uint256 validationData) { + ) + external + virtual + payPrefund(missingAccountFunds) + onlyEntryPoint + returns (uint256 validationData) + { address validator = op.nonce.getValidator(); if (op.nonce.isModuleEnableMode()) { PackedUserOperation memory userOp = op; @@ -86,8 +114,13 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } else { - require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); - validationData = IValidator(validator).validateUserOp(op, userOpHash); + if (!_isValidatorInstalled(validator)) { + // Check the userOp signature if the validator is not installed (used for EIP7702) + validationData = _checkUserOpSignature(op.signature, validator, userOpHash); + } else { + // If the validator is installed, forward the validation task to the validator + validationData = IValidator(validator).validateUserOp(op, userOpHash); + } } } @@ -117,7 +150,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 +180,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. @@ -207,9 +249,15 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra } function initializeAccount(bytes calldata initData) external payable virtual { + // 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(); + } + _initModuleManager(); (address bootstrap, bytes memory bootstrapCall) = abi.decode(initData, (address, bytes)); - (bool success, ) = bootstrap.delegatecall(bootstrapCall); + (bool success,) = bootstrap.delegatecall(bootstrapCall); require(success, NexusInitializationFailed()); require(_hasValidators(), NoValidatorInstalled()); @@ -278,9 +326,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. @@ -329,6 +376,26 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra UUPSUpgradeable.upgradeToAndCall(newImplementation, data); } + /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED + /// @param signature The signature to check. + /// @param validator The address of the validator module. + /// @param userOpHash The hash of the user operation data. + /// @return The validation result. + function _checkUserOpSignature(bytes calldata signature, address validator, bytes32 userOpHash) internal view returns (uint256) { + // If the account is not initialized, check the signature against the account + if (!_isAlreadyInitialized()) { + // Recover the signer from the signature, if it is the account, return success, otherwise revert + address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); + if (signer != address(this)) { + // If the signer is not the account, return validation failure + return VALIDATION_FAILED; + } + return VALIDATION_SUCCESS; + } + // If the validator is not installed, and the account is initialized, revert + revert ValidatorNotInstalled(validator); + } + /// @dev For automatic detection that the smart account supports the ERC7739 workflow /// Iterates over all the validators but only if this is a detection request /// ERC-7739 spec assumes that if the account doesn't support ERC-7739 @@ -340,7 +407,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,7 +425,7 @@ 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 { } /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index a59ca9b38..96d2412b8 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -23,7 +23,15 @@ 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_MULTI, + MODULE_ENABLE_MODE_TYPE_HASH, + ERC1271_MAGICVALUE +} from "../types/Constants.sol"; import { EIP712 } from "solady/utils/EIP712.sol"; import { ExcessivelySafeCall } from "excessively-safe-call/ExcessivelySafeCall.sol"; import { RegistryAdapter } from "./RegistryAdapter.sol"; @@ -61,7 +69,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) { @@ -124,8 +132,9 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError (module, moduleType, moduleInitData, enableModeSignature, userOpSignature) = packedData.parseEnableModeData(); - if (!_checkEnableModeSignature(_getEnableModeDataHash(module, moduleType, userOpHash, moduleInitData), enableModeSignature)) + if (!_checkEnableModeSignature(_getEnableModeDataHash(module, moduleType, userOpHash, moduleInitData), enableModeSignature)) { revert EnableModeSigError(); + } _installModule(moduleType, module, moduleInitData); } @@ -372,6 +381,13 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } + /// @dev Checks if the validator list is already initialized. + function _isAlreadyInitialized() internal view virtual returns (bool) { + // account module storage + AccountStorage storage ams = _getAccountStorage(); + return ams.validators.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. @@ -400,8 +416,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @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); + _getAccountStorage().validators.getNext(address(0x01)) != address(0x01) && _getAccountStorage().validators.getNext(address(0x01)) != address(0x00); } /// @dev Checks if an executor is currently installed. @@ -478,7 +493,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..e6d883e96 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. @@ -55,13 +54,7 @@ contract K1ValidatorFactory is Stakeable { /// @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 +71,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 +98,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/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/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/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..eb8623dda 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -18,11 +18,7 @@ contract MockValidator is ERC7739Validator { 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)); } @@ -63,18 +59,18 @@ 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 + ); } 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"); delete smartAccountOwners[msg.sender]; } diff --git a/contracts/utils/NexusBootstrap.sol b/contracts/utils/NexusBootstrap.sol index 3cebd3b26..65484ebb9 100644 --- a/contracts/utils/NexusBootstrap.sol +++ b/contracts/utils/NexusBootstrap.sol @@ -41,7 +41,10 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external { + ) + external + payable + { _configureRegistry(registry, attesters, threshold); _installValidator(address(validator), data); } @@ -60,7 +63,10 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external { + ) + external + payable + { _configureRegistry(registry, attesters, threshold); // Initialize validators @@ -96,7 +102,10 @@ contract NexusBootstrap is ModuleManager { IERC7484 registry, address[] calldata attesters, uint8 threshold - ) external { + ) + external + payable + { _configureRegistry(registry, attesters, threshold); // Initialize validators @@ -124,7 +133,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 +151,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,10 +167,13 @@ 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)) ); } 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/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol new file mode 100644 index 000000000..07d8a4896 --- /dev/null +++ b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol @@ -0,0 +1,238 @@ +// 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"; + +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, address(ACCOUNT_IMPLEMENTATION).code); + } + + 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_VALIDATION, 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_VALIDATION, 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_VALIDATION, 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_VALIDATION, 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); + } +} diff --git a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol index 6b97f1e40..eaee116f7 100644 --- a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestAccountFactory_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 TestAccountFactory_Deployments /// @notice Tests for deploying accounts using the AccountFactory and various methods. @@ -204,18 +205,14 @@ contract TestAccountFactory_Deployments is NexusTest_Base { 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_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" + 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 { + 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), ""); @@ -225,14 +222,30 @@ contract TestAccountFactory_Deployments is NexusTest_Base { // 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)) + 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 diff --git a/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol index 52dfba2f9..d98d9cbae 100644 --- a/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol @@ -20,7 +20,7 @@ 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(), 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..1c9a48488 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. @@ -21,13 +22,8 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { 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 - ); + 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. @@ -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/utils/TestHelper.t.sol b/test/foundry/utils/TestHelper.t.sol index e6154aa6b..04b8317c8 100644 --- a/test/foundry/utils/TestHelper.t.sol +++ b/test/foundry/utils/TestHelper.t.sol @@ -19,6 +19,7 @@ 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"; @@ -193,8 +194,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 @@ -318,18 +318,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 +341,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 +368,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 +381,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 +506,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 }); From 5689c78d0072c07c97ca370b45a1c6efa9ff4a4f Mon Sep 17 00:00:00 2001 From: highskore Date: Tue, 10 Dec 2024 11:40:15 +0100 Subject: [PATCH 02/56] chore: fix function ordering --- contracts/Nexus.sol | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 9ba419789..e1bab72e6 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -376,26 +376,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra UUPSUpgradeable.upgradeToAndCall(newImplementation, data); } - /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED - /// @param signature The signature to check. - /// @param validator The address of the validator module. - /// @param userOpHash The hash of the user operation data. - /// @return The validation result. - function _checkUserOpSignature(bytes calldata signature, address validator, bytes32 userOpHash) internal view returns (uint256) { - // If the account is not initialized, check the signature against the account - if (!_isAlreadyInitialized()) { - // Recover the signer from the signature, if it is the account, return success, otherwise revert - address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); - if (signer != address(this)) { - // If the signer is not the account, return validation failure - return VALIDATION_FAILED; - } - return VALIDATION_SUCCESS; - } - // If the validator is not installed, and the account is initialized, revert - revert ValidatorNotInstalled(validator); - } - /// @dev For automatic detection that the smart account supports the ERC7739 workflow /// Iterates over all the validators but only if this is a detection request /// ERC-7739 spec assumes that if the account doesn't support ERC-7739 @@ -427,6 +407,26 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @param newImplementation The address of the new implementation to upgrade to. function _authorizeUpgrade(address newImplementation) internal virtual override(UUPSUpgradeable) onlyEntryPointOrSelf { } + /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED + /// @param signature The signature to check. + /// @param validator The address of the validator module. + /// @param userOpHash The hash of the user operation data. + /// @return The validation result. + function _checkUserOpSignature(bytes calldata signature, address validator, bytes32 userOpHash) internal view returns (uint256) { + // If the account is not initialized, check the signature against the account + if (!_isAlreadyInitialized()) { + // Recover the signer from the signature, if it is the account, return success, otherwise revert + address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); + if (signer != address(this)) { + // If the signer is not the account, return validation failure + return VALIDATION_FAILED; + } + return VALIDATION_SUCCESS; + } + // If the validator is not installed, and the account is initialized, revert + revert ValidatorNotInstalled(validator); + } + /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = "Nexus"; From 6a9f7a07554d22535e573e5587f9ebfc319d50de Mon Sep 17 00:00:00 2001 From: highskore Date: Tue, 10 Dec 2024 11:59:22 +0100 Subject: [PATCH 03/56] fix(hardhat): fix hardhat test --- .../smart-account/Nexus.Factory.specs.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/test/hardhat/smart-account/Nexus.Factory.specs.ts b/test/hardhat/smart-account/Nexus.Factory.specs.ts index 17eb94403..ad4646688 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("0x")).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, @@ -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, @@ -515,21 +513,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], From c3b4fc66f687bed63902ba10ca238e4608b879e8 Mon Sep 17 00:00:00 2001 From: highskore Date: Tue, 10 Dec 2024 18:55:11 +0100 Subject: [PATCH 04/56] chore(K1ValidatoFactory): remove unused error --- contracts/factory/K1ValidatorFactory.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/factory/K1ValidatorFactory.sol b/contracts/factory/K1ValidatorFactory.sol index e6d883e96..b9d6713ec 100644 --- a/contracts/factory/K1ValidatorFactory.sol +++ b/contracts/factory/K1ValidatorFactory.sol @@ -46,9 +46,6 @@ 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. From 691cdffb04ed4cc6199f0304998b1087802bc906 Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 13 Dec 2024 13:32:22 +0100 Subject: [PATCH 05/56] chore(MockValidator): revert removing require --- contracts/mocks/MockValidator.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/mocks/MockValidator.sol b/contracts/mocks/MockValidator.sol index eb8623dda..14c5cba96 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -70,6 +70,7 @@ contract MockValidator is ERC7739Validator { } function onUninstall(bytes calldata data) external { + require(!IModuleManager(msg.sender).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(this), ""), "Validator is still installed"); data; delete smartAccountOwners[msg.sender]; } From bb7f70e2c64b7d157d9bc298d66b6424af0f61d5 Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 13 Dec 2024 13:38:40 +0100 Subject: [PATCH 06/56] refactor(validateUserOp): check happy path first --- contracts/Nexus.sol | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index e1bab72e6..ba6e2242e 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -114,12 +114,18 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } else { - if (!_isValidatorInstalled(validator)) { - // Check the userOp signature if the validator is not installed (used for EIP7702) - validationData = _checkUserOpSignature(op.signature, validator, userOpHash); - } else { + if (_isValidatorInstalled(validator)) { // If the validator is installed, forward the validation task to the validator validationData = IValidator(validator).validateUserOp(op, userOpHash); + } else { + // If the account is not initialized, check the signature against the account + if (!_isAlreadyInitialized()) { + // Check the userOp signature if the validator is not installed (used for EIP7702) + validationData = _checkUserOpSignature(op.signature, userOpHash); + } else { + // If the account is initialized, revert as the validator is not installed + revert ValidatorNotInstalled(validator); + } } } } @@ -409,22 +415,16 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED /// @param signature The signature to check. - /// @param validator The address of the validator module. /// @param userOpHash The hash of the user operation data. /// @return The validation result. - function _checkUserOpSignature(bytes calldata signature, address validator, bytes32 userOpHash) internal view returns (uint256) { - // If the account is not initialized, check the signature against the account - if (!_isAlreadyInitialized()) { - // Recover the signer from the signature, if it is the account, return success, otherwise revert - address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); - if (signer != address(this)) { - // If the signer is not the account, return validation failure - return VALIDATION_FAILED; - } - return VALIDATION_SUCCESS; + function _checkUserOpSignature(bytes calldata signature, bytes32 userOpHash) internal view returns (uint256) { + // Recover the signer from the signature, if it is the account, return success, otherwise revert + address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); + if (signer != address(this)) { + // If the signer is not the account, return validation failure + return VALIDATION_FAILED; } - // If the validator is not installed, and the account is initialized, revert - revert ValidatorNotInstalled(validator); + return VALIDATION_SUCCESS; } /// @dev EIP712 domain name and version. From d0cc50e56bd85459f99af60218a1a0e79bdd6f27 Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 13 Dec 2024 13:42:41 +0100 Subject: [PATCH 07/56] refactor(_checkUserOpSignature): check happy path first --- contracts/Nexus.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index ba6e2242e..0533d67a0 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -420,11 +420,10 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra function _checkUserOpSignature(bytes calldata signature, bytes32 userOpHash) internal view returns (uint256) { // Recover the signer from the signature, if it is the account, return success, otherwise revert address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); - if (signer != address(this)) { - // If the signer is not the account, return validation failure - return VALIDATION_FAILED; + if (signer == address(this)) { + return VALIDATION_SUCCESS; } - return VALIDATION_SUCCESS; + return VALIDATION_FAILED; } /// @dev EIP712 domain name and version. From dd96bd76e27c31aa260b5cb98ffc7c67536065c1 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Wed, 18 Dec 2024 16:08:13 +0300 Subject: [PATCH 08/56] fix tests dep in script --- scripts/foundry/HelperConfig.s.sol | 2 +- test/foundry/utils/TestHelper.t.sol | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) 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/utils/TestHelper.t.sol b/test/foundry/utils/TestHelper.t.sol index e6154aa6b..e42f67b76 100644 --- a/test/foundry/utils/TestHelper.t.sol +++ b/test/foundry/utils/TestHelper.t.sol @@ -25,9 +25,11 @@ import { NexusAccountFactory } from "../../../contracts/factory/NexusAccountFact 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"; contract TestHelper is CheatCodes, EventsAndErrors { + + address private constant MAINNET_ENTRYPOINT_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; + // ----------------------------------------- // State Variables // ----------------------------------------- @@ -104,8 +106,7 @@ contract TestHelper is CheatCodes, EventsAndErrors { } function deployTestContracts() internal { - HelperConfig helperConfig = new HelperConfig(); - ENTRYPOINT = helperConfig.ENTRYPOINT(); + setupEntrypoint(); ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT)); FACTORY = new NexusAccountFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr)); META_FACTORY = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); @@ -120,6 +121,19 @@ contract TestHelper is CheatCodes, EventsAndErrors { 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 // ----------------------------------------- From fb07c7e35a7549aec72689d220ef9cb82fc24da9 Mon Sep 17 00:00:00 2001 From: Manank Patni Date: Thu, 19 Dec 2024 00:59:02 +0530 Subject: [PATCH 09/56] Fix test case for delegate call Signed-off-by: Manank Patni --- .../TestAccountExecution_ExecuteFromExecutor.t.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol b/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol index 9bac78537..f9a446cb6 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 From 8cd516919e8170faaeb53b4dd6e01cad1cca315c Mon Sep 17 00:00:00 2001 From: highskore Date: Tue, 7 Jan 2025 20:00:07 +0100 Subject: [PATCH 10/56] feat: add prevalidation hook support --- contracts/Nexus.sol | 42 +- contracts/base/ModuleManager.sol | 138 +++- .../base/IModuleManagerEventsAndErrors.sol | 9 + contracts/interfaces/base/IStorage.sol | 29 +- .../interfaces/modules/IPreValidationHook.sol | 49 ++ contracts/mocks/Mock7739PreValidationHook.sol | 170 +++++ contracts/mocks/MockAccountLocker.sol | 31 + contracts/mocks/MockPreValidationHook.sol | 53 ++ .../MockPreValidationHookMultiplexer.sol | 132 ++++ .../MockResourceLockPreValidationHook.sol | 112 +++ contracts/mocks/MockSimpleValidator.sol | 49 ++ contracts/types/Constants.sol | 9 +- contracts/types/DataTypes.sol | 13 + ...reValidation_Integration_Multiplexer.t.sol | 185 +++++ ...dation_Integration_ResourceLockHooks.t.sol | 208 ++++++ .../TestERC1271Account_MockProtocol.t.sol | 81 ++- .../TestNexus_Hook_Emergency_Uninstall.sol | 654 +++++++++++++++--- test/foundry/utils/EventsAndErrors.sol | 2 +- 18 files changed, 1821 insertions(+), 145 deletions(-) create mode 100644 contracts/interfaces/modules/IPreValidationHook.sol create mode 100644 contracts/mocks/Mock7739PreValidationHook.sol create mode 100644 contracts/mocks/MockAccountLocker.sol create mode 100644 contracts/mocks/MockPreValidationHook.sol create mode 100644 contracts/mocks/MockPreValidationHookMultiplexer.sol create mode 100644 contracts/mocks/MockResourceLockPreValidationHook.sol create mode 100644 contracts/mocks/MockSimpleValidator.sol create mode 100644 test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol create mode 100644 test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 0533d67a0..9bd26e824 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -27,6 +27,8 @@ import { 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 @@ -46,6 +48,7 @@ import { NonceLib } from "./lib/NonceLib.sol"; import { SentinelListLib, SENTINEL, ZERO_ADDRESS } from "sentinellist/SentinelList.sol"; import { ECDSA } from "solady/utils/ECDSA.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. @@ -112,11 +115,14 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra PackedUserOperation memory userOp = op; userOp.signature = _enableMode(userOpHash, op.signature); require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); + (userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, userOp, missingAccountFunds); validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } else { if (_isValidatorInstalled(validator)) { + PackedUserOperation memory userOp; // If the validator is installed, forward the validation task to the validator - validationData = IValidator(validator).validateUserOp(op, userOpHash); + (userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, op, missingAccountFunds); + validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } else { // If the account is not initialized, check the signature against the account if (!_isAlreadyInitialized()) { @@ -197,6 +203,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. @@ -212,6 +220,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 uninstall. /// @param deInitData De-initialization data for the module. /// @dev Ensures that the operation is authorized and valid before proceeding with the uninstallation. @@ -225,13 +235,27 @@ 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); } } - 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]; @@ -246,8 +270,8 @@ 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(); @@ -292,7 +316,9 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra // 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) { + 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); diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 96d2412b8..f08a28df5 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"; @@ -28,13 +29,18 @@ import { 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 } 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"; /// @title Nexus - ModuleManager /// @notice Manages Validator, Executor, Hook, and Fallback modules within the Nexus suite, supporting @@ -161,6 +167,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 { @@ -225,9 +233,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)); } @@ -282,6 +295,93 @@ 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); + } + } + + /// @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(address(this), 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 + view + 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(address(this), userOp, missingAccountFunds, hash); + } + /// @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). @@ -321,6 +421,12 @@ 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]); + } } } @@ -345,6 +451,22 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } + /// @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(msg.sender, hash, signature[20:]) == bytes4(0x1626ba7e)), EmergencyUninstallSigError()); + } + /// @notice Builds the enable mode data hash as per eip712 /// @param module Module being enabled /// @param moduleType Type of the module as per EIP-7579 @@ -355,6 +477,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. @@ -376,6 +508,8 @@ 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; } diff --git a/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol b/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol index 77ddc271f..24ee4fb69 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); 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..c7a17e23f --- /dev/null +++ b/contracts/interfaces/modules/IPreValidationHook.sol @@ -0,0 +1,49 @@ +// 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 account The account calling the hook + /// @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 account, + 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 account The account calling the hook + /// @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( + address account, + PackedUserOperation calldata userOp, + uint256 missingAccountFunds, + bytes32 userOpHash + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature); +} diff --git a/contracts/mocks/Mock7739PreValidationHook.sol b/contracts/mocks/Mock7739PreValidationHook.sol new file mode 100644 index 000000000..3dd00b989 --- /dev/null +++ b/contracts/mocks/Mock7739PreValidationHook.sol @@ -0,0 +1,170 @@ +// 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; + + function preValidationHookERC1271( + address account, + address, + bytes32 hash, + bytes calldata data + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature) + { + // 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..45cf4b0f6 --- /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[account][token]; + } + + function setLockedAmount(address account, address token, uint256 amount) external { + lockedAmount[account][token] = 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/MockPreValidationHook.sol b/contracts/mocks/MockPreValidationHook.sol new file mode 100644 index 000000000..5485a0f5d --- /dev/null +++ b/contracts/mocks/MockPreValidationHook.sol @@ -0,0 +1,53 @@ +// 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, + address, + bytes32 hash, + bytes calldata data + ) + external + pure + returns (bytes32 hookHash, bytes memory hookSignature) + { + return (hash, data); + } + + function preValidationHookERC4337( + address, + 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..e5422e4a8 --- /dev/null +++ b/contracts/mocks/MockPreValidationHookMultiplexer.sol @@ -0,0 +1,132 @@ +// 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(address account => mapping(uint256 hookType => HookConfig)) internal accountConfig; + + error AlreadyInitialized(uint256 hookType); + error NotInitialized(uint256 hookType); + error InvalidHookType(uint256 hookType); + error OnInstallFailed(address hook); + error OnUninstallFailed(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[msg.sender][moduleType].initialized) { + revert AlreadyInitialized(moduleType); + } + + accountConfig[msg.sender][moduleType].hooks = hooks; + accountConfig[msg.sender][moduleType].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[msg.sender][moduleType].hooks; + + delete accountConfig[msg.sender][moduleType]; + + 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( + address account, + PackedUserOperation calldata userOp, + uint256 missingAccountFunds, + bytes32 userOpHash + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature) + { + HookConfig storage config = accountConfig[msg.sender][MODULE_TYPE_PREVALIDATION_HOOK_ERC4337]; + + 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++) { + (hookHash, hookSignature) = IPreValidationHookERC4337(config.hooks[i]).preValidationHookERC4337(account, op, missingAccountFunds, hookHash); + op.signature = hookSignature; + } + + return (hookHash, hookSignature); + } + + function preValidationHookERC1271( + address account, + address sender, + bytes32 hash, + bytes calldata signature + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature) + { + HookConfig storage config = accountConfig[msg.sender][MODULE_TYPE_PREVALIDATION_HOOK_ERC1271]; + + if (!config.initialized) { + revert NotInitialized(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271); + } + + hookHash = hash; + hookSignature = signature; + + for (uint256 i = 0; i < config.hooks.length; i++) { + (hookHash, hookSignature) = IPreValidationHookERC1271(config.hooks[i]).preValidationHookERC1271(account, sender, hookHash, hookSignature); + } + + 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[smartAccount][MODULE_TYPE_PREVALIDATION_HOOK_ERC4337].initialized + || accountConfig[smartAccount][MODULE_TYPE_PREVALIDATION_HOOK_ERC1271].initialized; + } + + function isHookTypeInitialized(address smartAccount, uint256 hookType) external view returns (bool) { + return accountConfig[smartAccount][hookType].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..ac376866a --- /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( + address account, + PackedUserOperation calldata userOp, + uint256 missingAccountFunds, + bytes32 userOpHash + ) + external + view + returns (bytes32 hookHash, bytes memory hookSignature) + { + 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 account, + address sender, + bytes32 hash, + bytes calldata data + ) + external + view + override + returns (bytes32 hookHash, bytes memory hookSignature) + { + 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/types/Constants.sol b/contracts/types/Constants.sol index 22e00ba06..1cf6159ca 100644 --- a/contracts/types/Constants.sol +++ b/contracts/types/Constants.sol @@ -39,12 +39,19 @@ uint256 constant MODULE_TYPE_FALLBACK = 3; // Module type identifier for hooks uint256 constant MODULE_TYPE_HOOK = 4; +// Module type identifiers for pre-validation hooks +uint256 constant MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 = 8; +uint256 constant MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 = 9; + 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)); +string constant EMERGENCY_UNINSTALL_NOTATION = "EmergencyUninstall(address hook,uint256 hookType,bytes deInitData,uint256 nonce)"; +bytes32 constant EMERGENCY_UNINSTALL_TYPE_HASH = keccak256(bytes(EMERGENCY_UNINSTALL_NOTATION)); + // Validation modes bytes1 constant MODE_VALIDATION = 0x00; bytes1 constant MODE_MODULE_ENABLE = 0x01; 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/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol new file mode 100644 index 000000000..91ef6a553 --- /dev/null +++ b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol @@ -0,0 +1,185 @@ +// 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"; + +/// @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; + + 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(); + erc7739Hook = new Mock7739PreValidationHook(); + hookMultiplexer = new MockPreValidationHookMultiplexer(); + resourceLockHook = new MockResourceLockPreValidationHook(address(accountLocker), address(hookMultiplexer)); + // 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); + } + + 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_Fails_WhenResourceLocked() public { + // Install hooks and multiplexer + test_installMultiplePreValidationHooks(); + + // Lock resources + vm.prank(address(accountLocker)); + 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..4320b96db --- /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 + vm.prank(address(accountLocker)); + 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 + vm.prank(address(accountLocker)); + 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 + vm.prank(address(accountLocker)); + 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/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol b/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol index 9d863f1e6..019708364 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))); @@ -72,9 +72,9 @@ contract TestERC1271Account_MockProtocol is NexusTest_Base { 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(); + ( /*t.fields*/ , t.name, t.version, t.chainId, t.verifyingContract, t.salt, /*t.extensions*/ ) = BOB_ACCOUNT.eip712Domain(); bytes32 calculatedDomainSeparator = keccak256( abi.encode( @@ -93,12 +93,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(BOB.privateKey, toERC1271Hash(t.contents, address(ALICE_ACCOUNT))); @@ -117,12 +112,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 +165,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 +181,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 +196,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/hook/TestNexus_Hook_Emergency_Uninstall.sol b/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol index f5853c6ef..cc4cfc159 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,692 @@ 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); + } + + function _hashTypedData(bytes32 structHash, address account) internal view virtual returns (bytes32 digest) { + // Get the domain separator + 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) + } + } + + /// @notice Builds the domain separator for the account. + function buildDomainSeparator(address account) internal view returns (bytes32 separator) { + (, string memory name, string memory version,,,,) = EIP712(account).eip712Domain(); + + bytes32 _DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + // We will use `separator` to store the name hash to save a bit of gas. + bytes32 versionHash; + separator = keccak256(bytes(name)); + 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), separator) // Name hash. + mstore(add(m, 0x40), versionHash) + mstore(add(m, 0x60), chainid()) + mstore(add(m, 0x80), account) + separator := keccak256(m, 0xa0) + } + } } 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 // ========================== From ff8f36f4b6cdd997afeee7ab422dd50d870b5521 Mon Sep 17 00:00:00 2001 From: highskore Date: Tue, 7 Jan 2025 20:03:32 +0100 Subject: [PATCH 11/56] chore: fix linter issues --- .solhint.json | 3 +- contracts/base/ModuleManager.sol | 126 +++++++++++++++---------------- 2 files changed, 65 insertions(+), 64 deletions(-) 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/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index f08a28df5..8da4e963f 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -335,53 +335,6 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } - /// @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(address(this), 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 - view - 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(address(this), userOp, missingAccountFunds, hash); - } - /// @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). @@ -430,6 +383,69 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } + /// @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(msg.sender, hash, signature[20:]) == bytes4(0x1626ba7e)), 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(address(this), 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 + view + 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(address(this), userOp, missingAccountFunds, hash); + } + /// @notice Checks if an enable mode signature is valid. /// @param structHash data hash. /// @param sig Signature. @@ -451,22 +467,6 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } - /// @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(msg.sender, hash, signature[20:]) == bytes4(0x1626ba7e)), EmergencyUninstallSigError()); - } - /// @notice Builds the enable mode data hash as per eip712 /// @param module Module being enabled /// @param moduleType Type of the module as per EIP-7579 From 731daf96c31aeae3fc4e0a2d83dee73a399dd35b Mon Sep 17 00:00:00 2001 From: highskore Date: Wed, 8 Jan 2025 20:49:01 +0100 Subject: [PATCH 12/56] fix: init userOp to op --- contracts/Nexus.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 9bd26e824..951c4976f 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -119,7 +119,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } else { if (_isValidatorInstalled(validator)) { - PackedUserOperation memory userOp; + PackedUserOperation memory userOp = op; // If the validator is installed, forward the validation task to the validator (userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, op, missingAccountFunds); validationData = IValidator(validator).validateUserOp(userOp, userOpHash); From ab75991db2f2884c5a1da986b9afec9d8f5bf09b Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 10 Jan 2025 19:46:10 +0100 Subject: [PATCH 13/56] fix: use 2771 for 4337 prevalidation msg.sender --- contracts/base/ModuleManager.sol | 5 ++-- .../interfaces/modules/IPreValidationHook.sol | 5 +--- contracts/mocks/Mock7739PreValidationHook.sol | 20 +++++++++++++ contracts/mocks/MockPreValidationHook.sol | 1 - .../MockPreValidationHookMultiplexer.sol | 30 ++++++++++--------- .../MockResourceLockPreValidationHook.sol | 2 +- ...reValidation_Integration_Multiplexer.t.sol | 4 +-- ...dation_Integration_ResourceLockHooks.t.sol | 6 ++-- 8 files changed, 45 insertions(+), 28 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 8da4e963f..8d9b270bd 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -396,7 +396,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // Mark nonce as used _getAccountStorage().nonces[data.nonce] = true; // Check if the signature is valid - require((IValidator(validator).isValidSignatureWithSender(msg.sender, hash, signature[20:]) == bytes4(0x1626ba7e)), EmergencyUninstallSigError()); + 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. @@ -434,7 +434,6 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError uint256 missingAccountFunds ) internal - view virtual returns (bytes32 postHash, bytes memory postSig) { @@ -443,7 +442,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // 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(address(this), userOp, missingAccountFunds, hash); + else return IPreValidationHookERC4337(preValidationHook).preValidationHookERC4337(userOp, missingAccountFunds, hash); } /// @notice Checks if an enable mode signature is valid. diff --git a/contracts/interfaces/modules/IPreValidationHook.sol b/contracts/interfaces/modules/IPreValidationHook.sol index c7a17e23f..f014d230f 100644 --- a/contracts/interfaces/modules/IPreValidationHook.sol +++ b/contracts/interfaces/modules/IPreValidationHook.sol @@ -9,7 +9,7 @@ import { IModule } from "./IModule.sol"; 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 account The account calling the hook + /// @param account The account to validate the signature for /// @param sender The original sender of the request /// @param hash The hash of signed data /// @param data The signature data to validate @@ -31,19 +31,16 @@ interface IPreValidationHookERC1271 is IModule { 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 account The account calling the hook /// @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( - address account, PackedUserOperation calldata userOp, uint256 missingAccountFunds, bytes32 userOpHash ) external - view returns (bytes32 hookHash, bytes memory hookSignature); } diff --git a/contracts/mocks/Mock7739PreValidationHook.sol b/contracts/mocks/Mock7739PreValidationHook.sol index 3dd00b989..04477b572 100644 --- a/contracts/mocks/Mock7739PreValidationHook.sol +++ b/contracts/mocks/Mock7739PreValidationHook.sol @@ -10,6 +10,26 @@ 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 account, address, diff --git a/contracts/mocks/MockPreValidationHook.sol b/contracts/mocks/MockPreValidationHook.sol index 5485a0f5d..f1c5d4132 100644 --- a/contracts/mocks/MockPreValidationHook.sol +++ b/contracts/mocks/MockPreValidationHook.sol @@ -39,7 +39,6 @@ contract MockPreValidationHook is IPreValidationHookERC1271, IPreValidationHookE } function preValidationHookERC4337( - address, PackedUserOperation calldata userOp, uint256, bytes32 userOpHash diff --git a/contracts/mocks/MockPreValidationHookMultiplexer.sol b/contracts/mocks/MockPreValidationHookMultiplexer.sol index e5422e4a8..a38ad2de4 100644 --- a/contracts/mocks/MockPreValidationHookMultiplexer.sol +++ b/contracts/mocks/MockPreValidationHookMultiplexer.sol @@ -11,13 +11,14 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali } // Separate configurations for each hook type - mapping(address account => mapping(uint256 hookType => HookConfig)) internal accountConfig; + 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[])); @@ -26,12 +27,12 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali revert InvalidHookType(moduleType); } - if (accountConfig[msg.sender][moduleType].initialized) { + if (accountConfig[moduleType][msg.sender].initialized) { revert AlreadyInitialized(moduleType); } - accountConfig[msg.sender][moduleType].hooks = hooks; - accountConfig[msg.sender][moduleType].initialized = true; + 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]); @@ -47,9 +48,9 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali revert InvalidHookType(moduleType); } - address[] memory hooks = accountConfig[msg.sender][moduleType].hooks; + address[] memory hooks = accountConfig[moduleType][msg.sender].hooks; - delete accountConfig[msg.sender][moduleType]; + delete accountConfig[moduleType][msg.sender]; for (uint256 i = 0; i < hooks.length; i++) { bytes memory subHookOnUninstallCalldata = abi.encodeCall(IModule.onUninstall, hookData[i]); @@ -59,16 +60,14 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali } function preValidationHookERC4337( - address account, PackedUserOperation calldata userOp, uint256 missingAccountFunds, bytes32 userOpHash ) external - view returns (bytes32 hookHash, bytes memory hookSignature) { - HookConfig storage config = accountConfig[msg.sender][MODULE_TYPE_PREVALIDATION_HOOK_ERC4337]; + HookConfig storage config = accountConfig[MODULE_TYPE_PREVALIDATION_HOOK_ERC4337][msg.sender]; if (!config.initialized) { revert NotInitialized(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337); @@ -79,7 +78,10 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali PackedUserOperation memory op = userOp; for (uint256 i = 0; i < config.hooks.length; i++) { - (hookHash, hookSignature) = IPreValidationHookERC4337(config.hooks[i]).preValidationHookERC4337(account, op, missingAccountFunds, hookHash); + bytes memory subHookData = abi.encodeWithSelector(IPreValidationHookERC4337.preValidationHookERC4337.selector, op, missingAccountFunds, hookHash); + (bool success, bytes memory result) = config.hooks[i].call(abi.encodePacked(subHookData, msg.sender)); + require(success, SubHookFailed(config.hooks[i])); + (hookHash, hookSignature) = abi.decode(result, (bytes32, bytes)); op.signature = hookSignature; } @@ -96,7 +98,7 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali view returns (bytes32 hookHash, bytes memory hookSignature) { - HookConfig storage config = accountConfig[msg.sender][MODULE_TYPE_PREVALIDATION_HOOK_ERC1271]; + HookConfig storage config = accountConfig[MODULE_TYPE_PREVALIDATION_HOOK_ERC1271][msg.sender]; if (!config.initialized) { revert NotInitialized(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271); @@ -118,12 +120,12 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali function isInitialized(address smartAccount) external view returns (bool) { // Account is initialized if either hook type is initialized - return accountConfig[smartAccount][MODULE_TYPE_PREVALIDATION_HOOK_ERC4337].initialized - || accountConfig[smartAccount][MODULE_TYPE_PREVALIDATION_HOOK_ERC1271].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[smartAccount][hookType].initialized; + return accountConfig[hookType][smartAccount].initialized; } function isValidModuleType(uint256 moduleTypeId) internal pure returns (bool) { diff --git a/contracts/mocks/MockResourceLockPreValidationHook.sol b/contracts/mocks/MockResourceLockPreValidationHook.sol index ac376866a..38ee67e46 100644 --- a/contracts/mocks/MockResourceLockPreValidationHook.sol +++ b/contracts/mocks/MockResourceLockPreValidationHook.sol @@ -66,7 +66,6 @@ contract MockResourceLockPreValidationHook is IPreValidationHookERC4337, IPreVal } function preValidationHookERC4337( - address account, PackedUserOperation calldata userOp, uint256 missingAccountFunds, bytes32 userOpHash @@ -75,6 +74,7 @@ contract MockResourceLockPreValidationHook is IPreValidationHookERC4337, IPreVal view returns (bytes32 hookHash, bytes memory hookSignature) { + address account = _msgSender(); require(enoughETHAvailable(account, missingAccountFunds), InsufficientUnlockedETH(missingAccountFunds)); return (userOpHash, userOp.signature); } diff --git a/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol index 91ef6a553..3495119ea 100644 --- a/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol +++ b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol @@ -31,8 +31,8 @@ contract TestNexusPreValidation_Integration_HookMultiplexer is TestModuleManagem // Deploy supporting contracts accountLocker = new MockAccountLocker(); - erc7739Hook = new Mock7739PreValidationHook(); hookMultiplexer = new MockPreValidationHookMultiplexer(); + erc7739Hook = new Mock7739PreValidationHook(address(hookMultiplexer)); resourceLockHook = new MockResourceLockPreValidationHook(address(accountLocker), address(hookMultiplexer)); // Deploy the simple validator SIMPLE_VALIDATOR = new MockSimpleValidator(); @@ -132,7 +132,7 @@ contract TestNexusPreValidation_Integration_HookMultiplexer is TestModuleManagem test_installMultiplePreValidationHooks(); // Lock resources - vm.prank(address(accountLocker)); + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), address(this), 1); // Prepare test data diff --git a/test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol b/test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol index 4320b96db..8d1182ec6 100644 --- a/test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol +++ b/test/foundry/integration/TestNexusPreValidation_Integration_ResourceLockHooks.t.sol @@ -58,7 +58,7 @@ contract TestNexusPreValidation_Integration_ResourceLockHooks is TestModuleManag userOps[0] = buildUserOpWithCalldata(BOB, "", address(VALIDATOR_MODULE)); // Set locked amount to block ETH transactions - vm.prank(address(accountLocker)); + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), NATIVE_TOKEN, lockedAmount); // Ensure account has enough total balance @@ -96,7 +96,7 @@ contract TestNexusPreValidation_Integration_ResourceLockHooks is TestModuleManag userOps[0] = buildUserOpWithCalldata(BOB, "", address(VALIDATOR_MODULE)); // Set locked amount - vm.prank(address(accountLocker)); + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), NATIVE_TOKEN, lockedAmount); // Ensure account has enough total balance @@ -153,7 +153,7 @@ contract TestNexusPreValidation_Integration_ResourceLockHooks is TestModuleManag bytes memory validatorSignature = abi.encodePacked(address(VALIDATOR_MODULE), signature); // Set locked amount to block signature validation - vm.prank(address(accountLocker)); + MockAccountLocker(accountLocker).setLockedAmount(address(BOB_ACCOUNT), address(this), 1); // Expect revert due to resource lock From 859bf3b3c53eb4bebdfdfc0b088fba6d597a973b Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 10 Jan 2025 20:10:57 +0100 Subject: [PATCH 14/56] test(pre-validation/multiplexer): add k1 validator tests --- ...reValidation_Integration_Multiplexer.t.sol | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol index 3495119ea..9fb58a62f 100644 --- a/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol +++ b/test/foundry/integration/TestNexusPreValidation_Integration_Multiplexer.t.sol @@ -7,15 +7,18 @@ import { MockResourceLockPreValidationHook } from "../../../contracts/mocks/Mock 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; @@ -34,6 +37,7 @@ contract TestNexusPreValidation_Integration_HookMultiplexer is TestModuleManagem 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 @@ -47,6 +51,11 @@ contract TestNexusPreValidation_Integration_HookMultiplexer is TestModuleManagem 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 { @@ -127,12 +136,60 @@ contract TestNexusPreValidation_Integration_HookMultiplexer is TestModuleManagem 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 From 683ee6eb563b11a30f113532d19e88ea659713b0 Mon Sep 17 00:00:00 2001 From: highskore Date: Fri, 10 Jan 2025 20:16:31 +0100 Subject: [PATCH 15/56] fix: make MockResourceLock 4337 storage compliant --- contracts/mocks/MockAccountLocker.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/mocks/MockAccountLocker.sol b/contracts/mocks/MockAccountLocker.sol index 45cf4b0f6..6c091549f 100644 --- a/contracts/mocks/MockAccountLocker.sol +++ b/contracts/mocks/MockAccountLocker.sol @@ -8,11 +8,11 @@ contract MockAccountLocker is IHook { mapping(address => mapping(address => uint256)) lockedAmount; function getLockedAmount(address account, address token) external view returns (uint256) { - return lockedAmount[account][token]; + return lockedAmount[token][account]; } function setLockedAmount(address account, address token, uint256 amount) external { - lockedAmount[account][token] = amount; + lockedAmount[token][account] = amount; } function onInstall(bytes calldata data) external override { } From 84f9aa08866ca457af6325db8ad1e0e3e667aea4 Mon Sep 17 00:00:00 2001 From: highskore Date: Sat, 11 Jan 2025 02:38:59 +0100 Subject: [PATCH 16/56] fix: remove account from 1271 interface --- contracts/base/ModuleManager.sol | 2 +- .../interfaces/modules/IPreValidationHook.sol | 12 ++---------- contracts/mocks/Mock7739PreValidationHook.sol | 12 ++---------- contracts/mocks/MockPreValidationHook.sol | 11 +---------- .../MockPreValidationHookMultiplexer.sol | 19 +++++++++++++++---- .../MockResourceLockPreValidationHook.sol | 2 +- 6 files changed, 22 insertions(+), 36 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 8d9b270bd..f16a38992 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -419,7 +419,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // 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(address(this), msg.sender, hash, signature); + else return IPreValidationHookERC1271(preValidationHook).preValidationHookERC1271(msg.sender, hash, signature); } /// @dev Calls the pre-validation hook for ERC-4337. diff --git a/contracts/interfaces/modules/IPreValidationHook.sol b/contracts/interfaces/modules/IPreValidationHook.sol index f014d230f..b9b1b6b6d 100644 --- a/contracts/interfaces/modules/IPreValidationHook.sol +++ b/contracts/interfaces/modules/IPreValidationHook.sol @@ -9,21 +9,12 @@ import { IModule } from "./IModule.sol"; 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 account The account to validate the signature for /// @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 account, - address sender, - bytes32 hash, - bytes calldata data - ) - external - view - returns (bytes32 hookHash, bytes memory hookSignature); + function preValidationHookERC1271(address sender, bytes32 hash, bytes calldata data) external view returns (bytes32 hookHash, bytes memory hookSignature); } /// @title Nexus - IPreValidationHookERC4337 Interface @@ -42,5 +33,6 @@ interface IPreValidationHookERC4337 is IModule { bytes32 userOpHash ) external + view returns (bytes32 hookHash, bytes memory hookSignature); } diff --git a/contracts/mocks/Mock7739PreValidationHook.sol b/contracts/mocks/Mock7739PreValidationHook.sol index 04477b572..08a27004f 100644 --- a/contracts/mocks/Mock7739PreValidationHook.sol +++ b/contracts/mocks/Mock7739PreValidationHook.sol @@ -30,16 +30,8 @@ contract Mock7739PreValidationHook is IPreValidationHookERC1271 { return forwarder == prevalidationHookMultiplexer; } - function preValidationHookERC1271( - address account, - address, - bytes32 hash, - bytes calldata data - ) - external - view - returns (bytes32 hookHash, bytes memory hookSignature) - { + 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:])); diff --git a/contracts/mocks/MockPreValidationHook.sol b/contracts/mocks/MockPreValidationHook.sol index f1c5d4132..48f4cab12 100644 --- a/contracts/mocks/MockPreValidationHook.sol +++ b/contracts/mocks/MockPreValidationHook.sol @@ -25,16 +25,7 @@ contract MockPreValidationHook is IPreValidationHookERC1271, IPreValidationHookE return true; } - function preValidationHookERC1271( - address, - address, - bytes32 hash, - bytes calldata data - ) - external - pure - returns (bytes32 hookHash, bytes memory hookSignature) - { + function preValidationHookERC1271(address, bytes32 hash, bytes calldata data) external pure returns (bytes32 hookHash, bytes memory hookSignature) { return (hash, data); } diff --git a/contracts/mocks/MockPreValidationHookMultiplexer.sol b/contracts/mocks/MockPreValidationHookMultiplexer.sol index a38ad2de4..b5d6eff2d 100644 --- a/contracts/mocks/MockPreValidationHookMultiplexer.sol +++ b/contracts/mocks/MockPreValidationHookMultiplexer.sol @@ -65,6 +65,7 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali bytes32 userOpHash ) external + view returns (bytes32 hookHash, bytes memory hookSignature) { HookConfig storage config = accountConfig[MODULE_TYPE_PREVALIDATION_HOOK_ERC4337][msg.sender]; @@ -79,8 +80,12 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali 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].call(abi.encodePacked(subHookData, msg.sender)); - require(success, SubHookFailed(config.hooks[i])); + (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; } @@ -89,7 +94,6 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali } function preValidationHookERC1271( - address account, address sender, bytes32 hash, bytes calldata signature @@ -108,7 +112,14 @@ contract MockPreValidationHookMultiplexer is IPreValidationHookERC1271, IPreVali hookSignature = signature; for (uint256 i = 0; i < config.hooks.length; i++) { - (hookHash, hookSignature) = IPreValidationHookERC1271(config.hooks[i]).preValidationHookERC1271(account, sender, hookHash, hookSignature); + 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); diff --git a/contracts/mocks/MockResourceLockPreValidationHook.sol b/contracts/mocks/MockResourceLockPreValidationHook.sol index 38ee67e46..ebc47c15e 100644 --- a/contracts/mocks/MockResourceLockPreValidationHook.sol +++ b/contracts/mocks/MockResourceLockPreValidationHook.sol @@ -91,7 +91,6 @@ contract MockResourceLockPreValidationHook is IPreValidationHookERC4337, IPreVal } function preValidationHookERC1271( - address account, address sender, bytes32 hash, bytes calldata data @@ -101,6 +100,7 @@ contract MockResourceLockPreValidationHook is IPreValidationHookERC4337, IPreVal override returns (bytes32 hookHash, bytes memory hookSignature) { + address account = _msgSender(); require(notResourceLocked(account, sender), SenderIsResourceLocked()); return (hash, data); } From 451ed01d5f6dd54da4b46b1369276ddc5f8c4cdd Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Feb 2025 16:39:02 +0300 Subject: [PATCH 17/56] secure batch decode + test fix --- contracts/lib/ExecLib.sol | 50 +++++++++++++------ contracts/mocks/MockExecutor.sol | 7 ++- ...AccountExecution_ExecuteFromExecutor.t.sol | 10 +--- 3 files changed, 43 insertions(+), 24 deletions(-) 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/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/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol b/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol index f9a446cb6..c6a0974f4 100644 --- a/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol +++ b/test/foundry/unit/concrete/accountexecution/TestAccountExecution_ExecuteFromExecutor.t.sol @@ -198,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)); @@ -219,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)); From cdd799b744d7e1ab2cd2491d391f90183e5a2795 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Feb 2025 17:13:31 +0300 Subject: [PATCH 18/56] module enable mode for uninitialized 7702 acct --- contracts/Nexus.sol | 18 +----------- contracts/base/ModuleManager.sol | 47 ++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 951c4976f..be9ae939f 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -46,7 +46,6 @@ import { } from "./lib/ModeLib.sol"; import { NonceLib } from "./lib/NonceLib.sol"; import { SentinelListLib, SENTINEL, ZERO_ADDRESS } from "sentinellist/SentinelList.sol"; -import { ECDSA } from "solady/utils/ECDSA.sol"; import { Initializable } from "./lib/Initializable.sol"; import { EmergencyUninstall } from "./types/DataTypes.sol"; @@ -63,7 +62,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra using ExecLib for bytes; using NonceLib for uint256; using SentinelListLib for SentinelListLib.SentinelList; - using ECDSA for bytes32; /// @dev The timelock period for emergency hook uninstallation. uint256 internal constant _EMERGENCY_TIMELOCK = 1 days; @@ -127,7 +125,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra // If the account is not initialized, check the signature against the account if (!_isAlreadyInitialized()) { // Check the userOp signature if the validator is not installed (used for EIP7702) - validationData = _checkUserOpSignature(op.signature, userOpHash); + validationData = _checkSelfSignature(op.signature, userOpHash) ? VALIDATION_SUCCESS : VALIDATION_FAILED; } else { // If the account is initialized, revert as the validator is not installed revert ValidatorNotInstalled(validator); @@ -312,7 +310,6 @@ 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)); @@ -439,19 +436,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @param newImplementation The address of the new implementation to upgrade to. function _authorizeUpgrade(address newImplementation) internal virtual override(UUPSUpgradeable) onlyEntryPointOrSelf { } - /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED - /// @param signature The signature to check. - /// @param userOpHash The hash of the user operation data. - /// @return The validation result. - function _checkUserOpSignature(bytes calldata signature, bytes32 userOpHash) internal view returns (uint256) { - // Recover the signer from the signature, if it is the account, return success, otherwise revert - address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); - if (signer == address(this)) { - return VALIDATION_SUCCESS; - } - return VALIDATION_FAILED; - } - /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = "Nexus"; diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index f16a38992..035b8be8a 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -41,6 +41,7 @@ import { ExcessivelySafeCall } from "excessively-safe-call/ExcessivelySafeCall.s 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 @@ -55,7 +56,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError using LocalCallDataParserLib for bytes; using ExecLib for address; using ExcessivelySafeCall for address; - + using ECDSA for bytes32; /// @notice Ensures the message sender is a registered executor module. modifier onlyExecutorModule() virtual { require(_getAccountStorage().executors.contains(msg.sender), InvalidModule(msg.sender)); @@ -450,20 +451,29 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @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); - } 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; - } catch { - return false; + if (_isValidatorInstalled(enableModeSigValidator)) { + // 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; + } catch { + return false; + } + } else { + // If the account is not initialized, check the signature against the account + if (!_isAlreadyInitialized()) { + // ERC-7739 is not required here as the userOpHash is hashed into the structHash => safe + return _checkSelfSignature(sig[20:], eip712Digest); + } else { + // If the account is initialized, revert as the validator is not installed + revert ValidatorNotInstalled(enableModeSigValidator); + } } + } /// @notice Builds the enable mode data hash as per eip712 @@ -572,6 +582,19 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError hook = address(_getAccountStorage().hook); } + /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED + /// @param signature The signature to check. + /// @param userOpHash The hash of the user operation data. + /// @return The validation result. + function _checkSelfSignature(bytes calldata signature, bytes32 userOpHash) internal view returns (bool) { + // Recover the signer from the signature, if it is the account, return success, otherwise revert + address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); + if (signer == address(this)) { + return true; + } + return false; + } + function _fallback(bytes calldata callData) private returns (bytes memory result) { bool success; FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig]; From 76b5ab4ef28173c76aac8b20660887556c8d8965 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Feb 2025 19:03:56 +0300 Subject: [PATCH 19/56] 7702 enable mode --- contracts/Nexus.sol | 9 ++++ contracts/base/ModuleManager.sol | 31 +++++++---- test/foundry/fork/base/BaseSettings.t.sol | 4 +- .../TestModuleManager_EnableMode.t.sol | 51 +++++++++++++++++++ test/foundry/utils/TestHelper.t.sol | 7 ++- 5 files changed, 88 insertions(+), 14 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index be9ae939f..0b7244762 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -208,6 +208,11 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @dev This function can only be called by the EntryPoint or the account itself for security reasons. /// @dev This function goes through hook checks via withHook modifier through internal function _installModule. function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external payable onlyEntryPointOrSelf { + // protection for EIP7702 accounts which were not initialized + // and try to install a validator or executor during the first userOp not via initializeAccount() + if ((moduleTypeId == MODULE_TYPE_VALIDATOR || moduleTypeId == MODULE_TYPE_EXECUTOR) && !_isAlreadyInitialized()) { + _initModuleManager(); + } _installModule(moduleTypeId, module, initData); emit ModuleInstalled(moduleTypeId, module); } @@ -276,6 +281,10 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra } } + /// @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 { // Protect this function to only be callable when used with the proxy factory or when // account calls itself diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 035b8be8a..f4e72b2a8 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -142,7 +142,9 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError if (!_checkEnableModeSignature(_getEnableModeDataHash(module, moduleType, userOpHash, moduleInitData), enableModeSignature)) { revert EnableModeSigError(); } - + if (!_isAlreadyInitialized()) { + _initModuleManager(); + } _installModule(moduleType, module, moduleInitData); } @@ -182,7 +184,7 @@ 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); - _getAccountStorage().validators.push(validator); + _getAccountStorage().validators.push(validator); IValidator(validator).onInstall(data); } @@ -467,7 +469,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // If the account is not initialized, check the signature against the account if (!_isAlreadyInitialized()) { // ERC-7739 is not required here as the userOpHash is hashed into the structHash => safe - return _checkSelfSignature(sig[20:], eip712Digest); + return _checkSelfSignature(sig, eip712Digest); } else { // If the account is initialized, revert as the validator is not installed revert ValidatorNotInstalled(enableModeSigValidator); @@ -525,10 +527,12 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } /// @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 _isAlreadyInitialized() internal view virtual returns (bool) { // account module storage AccountStorage storage ams = _getAccountStorage(); - return ams.validators.alreadyInitialized(); + return ams.validators.alreadyInitialized() && ams.executors.alreadyInitialized(); } /// @dev Checks if a fallback handler is set for a given selector. @@ -562,6 +566,13 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError _getAccountStorage().validators.getNext(address(0x01)) != address(0x01) && _getAccountStorage().validators.getNext(address(0x01)) != address(0x00); } + /// @dev Checks if there is at least one executor installed. + /// @return True if there is at least one executor, otherwise false. + function _hasExecutors() internal view returns (bool) { + return + _getAccountStorage().executors.getNext(address(0x01)) != address(0x01) && _getAccountStorage().executors.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. @@ -584,14 +595,14 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED /// @param signature The signature to check. - /// @param userOpHash The hash of the user operation data. + /// @param dataHash The hash of the data. /// @return The validation result. - function _checkSelfSignature(bytes calldata signature, bytes32 userOpHash) internal view returns (bool) { + function _checkSelfSignature(bytes calldata signature, bytes32 dataHash) internal view returns (bool) { // Recover the signer from the signature, if it is the account, return success, otherwise revert - address signer = ECDSA.recover(userOpHash.toEthSignedMessageHash(), signature); - if (signer == address(this)) { - return true; - } + address signer = ECDSA.recover(dataHash.toEthSignedMessageHash(), signature); + if (signer == address(this)) return true; + signer = ECDSA.recover(dataHash, signature); + if (signer == address(this)) return true; return false; } 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/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol index c6fbd907d..cc74a6f83 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol @@ -79,6 +79,57 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { ); } + 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 + //skip appending validator address, as it is not installed (emulate uninitialized 7702 account) + + 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); diff --git a/test/foundry/utils/TestHelper.t.sol b/test/foundry/utils/TestHelper.t.sol index 21a734cec..0fa89edf5 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"; @@ -323,8 +325,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); } From 1e4eab1ae09fb6fa4f431c575c60a2b794aa35e5 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Feb 2025 19:17:49 +0300 Subject: [PATCH 20/56] refactor --- contracts/Nexus.sol | 2 +- contracts/base/ModuleManager.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 0b7244762..56877e242 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -123,7 +123,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } else { // If the account is not initialized, check the signature against the account - if (!_isAlreadyInitialized()) { + if (!_hasValidators() && !_hasExecutors()) { // Check the userOp signature if the validator is not installed (used for EIP7702) validationData = _checkSelfSignature(op.signature, userOpHash) ? VALIDATION_SUCCESS : VALIDATION_FAILED; } else { diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index f4e72b2a8..706b8aeeb 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -467,7 +467,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } else { // If the account is not initialized, check the signature against the account - if (!_isAlreadyInitialized()) { + if (!_hasValidators() && !_hasExecutors()) { // ERC-7739 is not required here as the userOpHash is hashed into the structHash => safe return _checkSelfSignature(sig, eip712Digest); } else { From 8c0bbfe7c4f8f26e05c5b57a004a23c25f7d1b03 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 20 Feb 2025 09:57:05 +0700 Subject: [PATCH 21/56] reduce size --- contracts/types/Constants.sol | 9 +++++---- .../modulemanager/TestModuleManager_EnableMode.t.sol | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/types/Constants.sol b/contracts/types/Constants.sol index 1cf6159ca..178fa91a8 100644 --- a/contracts/types/Constants.sol +++ b/contracts/types/Constants.sol @@ -43,11 +43,12 @@ uint256 constant MODULE_TYPE_HOOK = 4; uint256 constant MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 = 8; uint256 constant MODULE_TYPE_PREVALIDATION_HOOK_ERC4337 = 9; -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)); -string constant EMERGENCY_UNINSTALL_NOTATION = "EmergencyUninstall(address hook,uint256 hookType,bytes deInitData,uint256 nonce)"; -bytes32 constant EMERGENCY_UNINSTALL_TYPE_HASH = keccak256(bytes(EMERGENCY_UNINSTALL_NOTATION)); +// 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; diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol index cc74a6f83..72548c2d4 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol @@ -6,7 +6,7 @@ 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 { 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 "solady/utils/EIP712.sol"; contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { @@ -27,6 +27,8 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { bytes32 internal constant _DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + string constant MODULE_ENABLE_MODE_NOTATION = "ModuleEnableMode(address module,uint256 moduleType,bytes32 userOpHash,bytes32 initDataHash)"; + function setUp() public { setUpModuleManagement_Base(); mockMultiModule = new MockMultiModule(); From 87b5df4d7e1eb259901f87c80639a7520bf2f162 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 20 Feb 2025 10:00:14 +0700 Subject: [PATCH 22/56] builds within size --- contracts/Nexus.sol | 2 +- contracts/base/ModuleManager.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 56877e242..0b7244762 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -123,7 +123,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } else { // If the account is not initialized, check the signature against the account - if (!_hasValidators() && !_hasExecutors()) { + if (!_isAlreadyInitialized()) { // Check the userOp signature if the validator is not installed (used for EIP7702) validationData = _checkSelfSignature(op.signature, userOpHash) ? VALIDATION_SUCCESS : VALIDATION_FAILED; } else { diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 706b8aeeb..f4e72b2a8 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -467,7 +467,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } else { // If the account is not initialized, check the signature against the account - if (!_hasValidators() && !_hasExecutors()) { + if (!_isAlreadyInitialized()) { // ERC-7739 is not required here as the userOpHash is hashed into the structHash => safe return _checkSelfSignature(sig, eip712Digest); } else { From 9e7efbc41218c6ee9b69a97919857f084aca0e68 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 20 Feb 2025 10:04:46 +0700 Subject: [PATCH 23/56] fix verision --- contracts/Nexus.sol | 2 +- contracts/base/BaseAccount.sol | 2 +- .../foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol | 2 +- .../concrete/accountconfig/TestAccountConfig_AccountId.t.sol | 2 +- .../unit/concrete/factory/TestAccountFactory_Deployments.t.sol | 2 +- .../concrete/factory/TestNexusAccountFactory_Deployments.t.sol | 2 +- .../foundry/unit/concrete/gas/TestGas_NexusAccountFactory.t.sol | 2 +- test/foundry/unit/concrete/modules/TestK1Validator.t.sol | 2 +- test/hardhat/smart-account/Nexus.Basics.specs.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 0b7244762..d46c6d486 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -448,6 +448,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @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/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol index 06b9584fb..e538088bf 100644 --- a/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol +++ b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol @@ -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/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol index c8e137971..6bf04fe78 100644 --- a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol +++ b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol @@ -19,7 +19,7 @@ contract TestAccountConfig_AccountId is Test { /// @notice Tests if the account ID returns the expected value function test_WhenCheckingTheAccountID() external givenTheAccountConfiguration { - string memory expected = "biconomy.nexus.1.0.0"; + string memory expected = "biconomy.nexus.1.2.0"; assertEq(accountConfig.accountId(), expected, "AccountConfig should return the expected account ID."); } } diff --git a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol index eaee116f7..a0c200394 100644 --- a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol @@ -73,7 +73,7 @@ contract TestAccountFactory_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. diff --git a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol index ce4146d9b..f70907f5d 100644 --- a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol @@ -83,7 +83,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. 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/modules/TestK1Validator.t.sol b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol index 47e5dad62..b028a0537 100644 --- a/test/foundry/unit/concrete/modules/TestK1Validator.t.sol +++ b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol @@ -383,7 +383,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/hardhat/smart-account/Nexus.Basics.specs.ts b/test/hardhat/smart-account/Nexus.Basics.specs.ts index 0ac6613c6..e66cada40 100644 --- a/test/hardhat/smart-account/Nexus.Basics.specs.ts +++ b/test/hardhat/smart-account/Nexus.Basics.specs.ts @@ -133,7 +133,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 () => { From bfb427181ce44e9d65cb0fb6b854bffdc157241c Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 11:33:17 +0300 Subject: [PATCH 24/56] changes to sc re default module --- contracts/Nexus.sol | 108 ++++++++------- contracts/base/ModuleManager.sol | 127 ++++++++++-------- .../base/IModuleManagerEventsAndErrors.sol | 3 + contracts/lib/NonceLib.sol | 10 ++ contracts/modules/validators/K1Validator.sol | 34 +++-- contracts/types/Constants.sol | 1 + contracts/utils/NexusBootstrap.sol | 28 +++- scripts/foundry/Deploy.s.sol | 37 ----- .../TestERC1271Account_MockProtocol.t.sol | 28 ---- test/foundry/utils/TestHelper.t.sol | 43 +++++- 10 files changed, 226 insertions(+), 193 deletions(-) delete mode 100644 scripts/foundry/Deploy.s.sol diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index d46c6d486..8c2c44a12 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -73,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. @@ -108,30 +113,19 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra 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)); - (userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, userOp, missingAccountFunds); - validationData = IValidator(validator).validateUserOp(userOp, userOpHash); + address validator; + if (op.nonce.isDefaultValidatorMode()) { + validator = _DEFAULT_VALIDATOR; } else { - if (_isValidatorInstalled(validator)) { - PackedUserOperation memory userOp = op; - // If the validator is installed, forward the validation task to the validator - (userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, op, missingAccountFunds); - validationData = IValidator(validator).validateUserOp(userOp, userOpHash); - } else { - // If the account is not initialized, check the signature against the account - if (!_isAlreadyInitialized()) { - // Check the userOp signature if the validator is not installed (used for EIP7702) - validationData = _checkSelfSignature(op.signature, userOpHash) ? VALIDATION_SUCCESS : VALIDATION_FAILED; - } else { - // If the account is initialized, revert as the validator is not installed - revert ValidatorNotInstalled(validator); - } - } + validator = op.nonce.getValidator(); + require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); + } + PackedUserOperation memory userOp = op; + if (op.nonce.isModuleEnableMode()) { + userOp.signature = _enableMode(userOpHash, op.signature); } + (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. @@ -208,11 +202,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @dev This function can only be called by the EntryPoint or the account itself for security reasons. /// @dev This function goes through hook checks via withHook modifier through internal function _installModule. function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external payable onlyEntryPointOrSelf { - // protection for EIP7702 accounts which were not initialized - // and try to install a validator or executor during the first userOp not via initializeAccount() - if ((moduleTypeId == MODULE_TYPE_VALIDATOR || moduleTypeId == MODULE_TYPE_EXECUTOR) && !_isAlreadyInitialized()) { - _initModuleManager(); - } _installModule(moduleTypeId, module, initData); emit ModuleInstalled(moduleTypeId, module); } @@ -292,14 +281,29 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra Initializable.requireInitializable(); } - _initModuleManager(); - (address bootstrap, bytes memory bootstrapCall) = abi.decode(initData, (address, bytes)); - (bool success,) = bootstrap.delegatecall(bootstrapCall); + 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); } @@ -320,8 +324,7 @@ 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)); + 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) { @@ -349,12 +352,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. @@ -377,15 +385,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. diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index f4e72b2a8..fa81b15f1 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -57,6 +57,21 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError 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; + /// @notice The flag to indicate the default validator mode for enable mode signature + address internal constant _DEFAULT_VALIDATOR_FLAG = 0x0000000000000000000000000000000000000088; + + /// @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 { require(_getAccountStorage().executors.contains(msg.sender), InvalidModule(msg.sender)); @@ -121,7 +136,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(); @@ -139,12 +154,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(); } - if (!_isAlreadyInitialized()) { - _initModuleManager(); - } _installModule(moduleType, module, moduleInitData); } @@ -161,6 +181,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); @@ -184,7 +207,10 @@ 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); - _getAccountStorage().validators.push(validator); + if (validator == _DEFAULT_VALIDATOR_FLAG) { + revert DefaultValidatorAlreadyInstalled(); + } + _getAccountStorage().validators.push(validator); IValidator(validator).onInstall(data); } @@ -199,9 +225,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)); } @@ -451,31 +474,22 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @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])); + /// @param validator Validator address. + function _checkEnableModeSignature( + bytes32 structHash, + bytes calldata sig, + address validator + ) internal view returns (bool) { bytes32 eip712Digest = _hashTypedData(structHash); - - if (_isValidatorInstalled(enableModeSigValidator)) { - // 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) { + // 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(validator).isValidSignatureWithSender(address(this), eip712Digest, sig) returns (bytes4 res) { return res == ERC1271_MAGICVALUE; - } catch { - return false; - } - } else { - // If the account is not initialized, check the signature against the account - if (!_isAlreadyInitialized()) { - // ERC-7739 is not required here as the userOpHash is hashed into the structHash => safe - return _checkSelfSignature(sig, eip712Digest); - } else { - // If the account is initialized, revert as the validator is not installed - revert ValidatorNotInstalled(enableModeSigValidator); - } + } catch { + return false; } - } /// @notice Builds the enable mode data hash as per eip712 @@ -529,7 +543,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @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 _isAlreadyInitialized() internal view virtual returns (bool) { + function _areSentinelListsInitialized() internal view virtual returns (bool) { // account module storage AccountStorage storage ams = _getAccountStorage(); return ams.validators.alreadyInitialized() && ams.executors.alreadyInitialized(); @@ -559,20 +573,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 there is at least one executor installed. - /// @return True if there is at least one executor, otherwise false. - function _hasExecutors() internal view returns (bool) { - return - _getAccountStorage().executors.getNext(address(0x01)) != address(0x01) && _getAccountStorage().executors.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. @@ -593,17 +593,30 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError hook = address(_getAccountStorage().hook); } - /// @dev Checks if the userOp signer matches address(this), returns VALIDATION_SUCCESS if it does, otherwise VALIDATION_FAILED - /// @param signature The signature to check. - /// @param dataHash The hash of the data. - /// @return The validation result. - function _checkSelfSignature(bytes calldata signature, bytes32 dataHash) internal view returns (bool) { - // Recover the signer from the signature, if it is the account, return success, otherwise revert - address signer = ECDSA.recover(dataHash.toEthSignedMessageHash(), signature); - if (signer == address(this)) return true; - signer = ECDSA.recover(dataHash, signature); - if (signer == address(this)) return true; - return false; + /// @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) + codecopy(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) { diff --git a/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol b/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol index 24ee4fb69..5523f56ff 100644 --- a/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol +++ b/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol @@ -105,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/lib/NonceLib.sol b/contracts/lib/NonceLib.sol index bbff75092..d54118473 100644 --- a/contracts/lib/NonceLib.sol +++ b/contracts/lib/NonceLib.sol @@ -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/modules/validators/K1Validator.sol b/contracts/modules/validators/K1Validator.sol index 110fcfc27..d8bb13607 100644 --- a/contracts/modules/validators/K1Validator.sol +++ b/contracts/modules/validators/K1Validator.sol @@ -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 178fa91a8..2e5cd277e 100644 --- a/contracts/types/Constants.sol +++ b/contracts/types/Constants.sol @@ -53,6 +53,7 @@ bytes32 constant EMERGENCY_UNINSTALL_TYPE_HASH = 0xd3ddfc12654178cc44d4a7b6b969c // Validation modes bytes1 constant MODE_VALIDATION = 0x00; bytes1 constant MODE_MODULE_ENABLE = 0x01; +bytes1 constant MODE_DEFAULT_VALIDATOR = 0x02; bytes4 constant SUPPORTS_ERC7739 = 0x77390000; bytes4 constant SUPPORTS_ERC7739_V1 = 0x77390001; diff --git a/contracts/utils/NexusBootstrap.sol b/contracts/utils/NexusBootstrap.sol index 65484ebb9..3b215375c 100644 --- a/contracts/utils/NexusBootstrap.sol +++ b/contracts/utils/NexusBootstrap.sol @@ -31,6 +31,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. @@ -44,6 +67,7 @@ contract NexusBootstrap is ModuleManager { ) external payable + _withInitSentinelLists { _configureRegistry(registry, attesters, threshold); _installValidator(address(validator), data); @@ -66,6 +90,7 @@ contract NexusBootstrap is ModuleManager { ) external payable + _withInitSentinelLists { _configureRegistry(registry, attesters, threshold); @@ -105,6 +130,7 @@ contract NexusBootstrap is ModuleManager { ) external payable + _withInitSentinelLists { _configureRegistry(registry, attesters, threshold); @@ -180,6 +206,6 @@ contract NexusBootstrap is ModuleManager { /// @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/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/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol b/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol index 019708364..27fa26526 100644 --- a/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol +++ b/test/foundry/unit/concrete/erc1271/TestERC1271Account_MockProtocol.t.sol @@ -60,34 +60,6 @@ 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; diff --git a/test/foundry/utils/TestHelper.t.sol b/test/foundry/utils/TestHelper.t.sol index 0fa89edf5..46e608f8f 100644 --- a/test/foundry/utils/TestHelper.t.sol +++ b/test/foundry/utils/TestHelper.t.sol @@ -110,7 +110,8 @@ contract TestHelper is CheatCodes, EventsAndErrors { function deployTestContracts() internal { setupEntrypoint(); - ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT)); + DEFAULT_VALIDATOR_MODULE = new MockValidator(); + ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT), address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); FACTORY = new NexusAccountFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr)); META_FACTORY = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); vm.prank(FACTORY_OWNER.addr); @@ -120,7 +121,7 @@ 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(0))); REGISTRY = new MockRegistry(); } @@ -656,4 +657,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) + } + } } From c17005500822ac94c1fe13b5a1d3c62fb8a31b43 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 12:28:02 +0300 Subject: [PATCH 25/56] tests --- contracts/base/ModuleManager.sol | 7 + contracts/lib/NonceLib.sol | 2 +- contracts/mocks/ExposedNexus.sol | 18 ++ foundry.toml | 2 +- .../ArbitrumSmartAccountUpgradeTest.t.sol | 2 +- .../integration/UpgradeSmartAccountTest.t.sol | 8 +- .../TestAccountConfig_AccountId.t.sol | 18 +- .../unit/concrete/eip7702/TestEIP7702.t.sol | 17 +- .../TestAccountFactory_Deployments.t.sol | 265 ------------------ .../TestAccountFactory_Deployments.tree | 26 -- .../TestBiconomyMetaFactory_Deployments.t.sol | 8 +- .../TestK1ValidatorFactory_Deployments.t.sol | 6 +- .../TestNexusAccountFactory_Deployments.t.sol | 64 +++-- .../TestNexus_Hook_Emergency_Uninstall.sol | 39 --- .../TestModuleManager_EnableMode.t.sol | 39 +-- .../concrete/modules/TestK1Validator.t.sol | 16 -- test/foundry/utils/Imports.sol | 1 + test/foundry/utils/TestHelper.t.sol | 7 +- 18 files changed, 117 insertions(+), 428 deletions(-) create mode 100644 contracts/mocks/ExposedNexus.sol delete mode 100644 test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol delete mode 100644 test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.tree diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index fa81b15f1..d5c0bd98f 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -43,6 +43,8 @@ import { RegistryAdapter } from "./RegistryAdapter.sol"; import { EmergencyUninstall } from "../types/DataTypes.sol"; import { ECDSA } from "solady/utils/ECDSA.sol"; +import "forge-std/console2.sol"; + /// @title Nexus - ModuleManager /// @notice Manages Validator, Executor, Hook, and Fallback modules within the Nexus suite, supporting /// @dev Implements SentinelList for managing modules via a linked list structure, adhering to ERC-7579. @@ -596,8 +598,10 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @dev Checks if the account is an ERC7702 account function _amIERC7702() internal view returns (bool) { bytes32 c; + uint256 size; assembly { // use extcodesize as the first cheapest check + size := extcodesize(address()) if eq(extcodesize(address()), 23) { // use extcodecopy to copy first 3 bytes of this contract and compare with 0xef0100 let ptr := mload(0x40) @@ -606,6 +610,9 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } // if it is not 23, we do not even check the first 3 bytes } + console2.log("size", size); + console2.logBytes32(c); + return bytes3(c) == bytes3(0xef0100); } diff --git a/contracts/lib/NonceLib.sol b/contracts/lib/NonceLib.sol index d54118473..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 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/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/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol index e538088bf..8830e20db 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(0))); // /!\ The private key is for testing purposes only and should not be used in production. signerPrivateKey = 0x2924d554c046e633f658427df4d0e7726487b1322bd16caaf24a53099f1cda85; signer = vm.createWallet(signerPrivateKey); diff --git a/test/foundry/integration/UpgradeSmartAccountTest.t.sol b/test/foundry/integration/UpgradeSmartAccountTest.t.sol index 09a15c962..06648a055 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(0))); 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(0))); 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(0))); 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(0))); 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 6bf04fe78..a2ac6dfb0 100644 --- a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol +++ b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol @@ -1,25 +1,19 @@ // 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); + deployTestContracts(); } /// @notice Tests if the account ID returns the expected value - function test_WhenCheckingTheAccountID() external givenTheAccountConfiguration { + function test_WhenCheckingTheAccountID() external { string memory expected = "biconomy.nexus.1.2.0"; - assertEq(accountConfig.accountId(), expected, "AccountConfig should return the expected account ID."); + assertEq(ACCOUNT_IMPLEMENTATION.accountId(), expected, "AccountConfig should return the expected account ID."); } } diff --git a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol index 07d8a4896..9810fdb77 100644 --- a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol +++ b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol @@ -5,6 +5,9 @@ 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; @@ -23,7 +26,7 @@ contract TestEIP7702 is NexusTest_Base { } function _doEIP7702(address account) internal { - vm.etch(account, address(ACCOUNT_IMPLEMENTATION).code); + vm.etch(account, abi.encodePacked(bytes3(0xef0100), bytes20(address(ACCOUNT_IMPLEMENTATION)))); } function _getInitData() internal view returns (bytes memory) { @@ -235,4 +238,16 @@ contract TestEIP7702 is NexusTest_Base { // 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(0))); + address eip7702account = address(0x7702acc7702acc7702acc7702acc); + vm.etch(eip7702account, abi.encodePacked(bytes3(0xef0100), bytes20(address(exposedNexus)))); + bytes32 code; + assembly { + extcodecopy(eip7702account, code, 0, 0x20) + } + console2.logBytes32(bytes32(code)); + assertTrue(IExposedNexus(eip7702account).amIERC7702()); + } } 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 a0c200394..000000000 --- a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol +++ /dev/null @@ -1,265 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import "../../../utils/NexusTest_Base.t.sol"; -import { NexusProxy } from "../../../../../contracts/utils/NexusProxy.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.2.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 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"); - } - - /// @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 d98d9cbae..c8c789685 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(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr), 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(0))), + REGISTRY + ) ); } diff --git a/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol index 1c9a48488..61fd5c0c8 100644 --- a/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol @@ -21,7 +21,7 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { user = newWallet("user"); vm.deal(user.addr, 1 ether); initData = abi.encodePacked(user.addr); - bootstrapper = new NexusBootstrap(); + bootstrapper = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); validatorFactory = new K1ValidatorFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr), address(VALIDATOR_MODULE), bootstrapper, REGISTRY); } @@ -30,7 +30,7 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { 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(0))); K1ValidatorFactory factory = new K1ValidatorFactory(implementation, FACTORY_OWNER.addr, k1Validator, bootstrapperInstance, REGISTRY); // Verify the implementation address is set correctly @@ -50,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(0))); K1ValidatorFactory factory = new K1ValidatorFactory(address(ACCOUNT_IMPLEMENTATION), FACTORY_OWNER.addr, k1Validator, bootstrapperInstance, registry); // Verify the registry address 0 diff --git a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol index f70907f5d..9de4683e7 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. @@ -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(0))); } /// @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/hook/TestNexus_Hook_Emergency_Uninstall.sol b/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol index cc4cfc159..8015f77cb 100644 --- a/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol +++ b/test/foundry/unit/concrete/hook/TestNexus_Hook_Emergency_Uninstall.sol @@ -652,43 +652,4 @@ contract TestNexus_Hook_Emergency_Uninstall is TestModuleManagement_Base { (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, hash); return abi.encodePacked(r, s, v); } - - function _hashTypedData(bytes32 structHash, address account) internal view virtual returns (bytes32 digest) { - // Get the domain separator - 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) - } - } - - /// @notice Builds the domain separator for the account. - function buildDomainSeparator(address account) internal view returns (bytes32 separator) { - (, string memory name, string memory version,,,,) = EIP712(account).eip712Domain(); - - bytes32 _DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; - - // We will use `separator` to store the name hash to save a bit of gas. - bytes32 versionHash; - separator = keccak256(bytes(name)); - 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), separator) // Name hash. - mstore(add(m, 0x40), versionHash) - mstore(add(m, 0x60), chainid()) - mstore(add(m, 0x80), account) - separator := keccak256(m, 0xa0) - } - } } diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol index 72548c2d4..884893412 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol @@ -7,7 +7,6 @@ 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 } from "contracts/types/Constants.sol"; -import "solady/utils/EIP712.sol"; contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { @@ -24,8 +23,6 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { MockMultiModule mockMultiModule; Counter public counter; - bytes32 internal constant _DOMAIN_TYPEHASH = - 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; string constant MODULE_ENABLE_MODE_NOTATION = "ModuleEnableMode(address module,uint256 moduleType,bytes32 userOpHash,bytes32 initDataHash)"; @@ -145,7 +142,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); @@ -491,40 +488,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/modules/TestK1Validator.t.sol b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol index b028a0537..1ab78235b 100644 --- a/test/foundry/unit/concrete/modules/TestK1Validator.t.sol +++ b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol @@ -201,22 +201,6 @@ 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 a valid signature with a valid 's' value is accepted function test_ValidateUserOp_ValidSignature() public { bytes32 originalHash = keccak256(abi.encodePacked("valid message")); 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 46e608f8f..13691e806 100644 --- a/test/foundry/utils/TestHelper.t.sol +++ b/test/foundry/utils/TestHelper.t.sol @@ -28,10 +28,14 @@ import { NexusAccountFactory } from "../../../contracts/factory/NexusAccountFact 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 { EIP712 } from "solady/utils/EIP712.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 @@ -64,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; @@ -121,7 +126,7 @@ contract TestHelper is CheatCodes, EventsAndErrors { EXECUTOR_MODULE = new MockExecutor(); VALIDATOR_MODULE = new MockValidator(); MULTI_MODULE = new MockMultiModule(); - BOOTSTRAPPER = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + BOOTSTRAPPER = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0xa11ce))); REGISTRY = new MockRegistry(); } From 3b944d120ec35f88c00a13fd526daa70044b0843 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 12:55:54 +0300 Subject: [PATCH 26/56] default module works --- contracts/Nexus.sol | 6 +++++- contracts/base/ModuleManager.sol | 7 +------ .../interfaces/INexusEventsAndErrors.sol | 3 +++ contracts/mocks/MockValidator.sol | 19 ++++++++++++------- .../ArbitrumSmartAccountUpgradeTest.t.sol | 2 +- .../integration/UpgradeSmartAccountTest.t.sol | 8 ++++---- .../unit/concrete/eip7702/TestEIP7702.t.sol | 15 +++++---------- .../TestBiconomyMetaFactory_Deployments.t.sol | 2 +- .../TestK1ValidatorFactory_Deployments.t.sol | 6 +++--- .../TestNexusAccountFactory_Deployments.t.sol | 2 +- test/foundry/utils/TestHelper.t.sol | 5 +++-- 11 files changed, 39 insertions(+), 36 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 8c2c44a12..777dadf22 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -451,7 +451,11 @@ 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) { diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index d5c0bd98f..474f54262 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -598,21 +598,16 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @dev Checks if the account is an ERC7702 account function _amIERC7702() internal view returns (bool) { bytes32 c; - uint256 size; assembly { // use extcodesize as the first cheapest check - size := extcodesize(address()) if eq(extcodesize(address()), 23) { // use extcodecopy to copy first 3 bytes of this contract and compare with 0xef0100 let ptr := mload(0x40) - codecopy(ptr, 0, 3) + extcodecopy(address(),ptr, 0, 3) c := mload(ptr) } // if it is not 23, we do not even check the first 3 bytes } - console2.log("size", size); - console2.logBytes32(c); - return bytes3(c) == bytes3(0xef0100); } diff --git a/contracts/interfaces/INexusEventsAndErrors.sol b/contracts/interfaces/INexusEventsAndErrors.sol index d44beb968..3ed66b5a8 100644 --- a/contracts/interfaces/INexusEventsAndErrors.sol +++ b/contracts/interfaces/INexusEventsAndErrors.sol @@ -51,4 +51,7 @@ 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(); } diff --git a/contracts/mocks/MockValidator.sol b/contracts/mocks/MockValidator.sol index 14c5cba96..c9718ff51 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -14,7 +14,7 @@ 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; } @@ -44,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 @@ -65,6 +63,16 @@ contract MockValidator is ERC7739Validator { ); } + /** + * 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 { smartAccountOwners[msg.sender] = address(bytes20(data)); } @@ -87,7 +95,4 @@ contract MockValidator is ERC7739Validator { return false; } - function getOwner(address account) external view returns (address) { - return smartAccountOwners[account]; - } } diff --git a/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol index 8830e20db..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, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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); diff --git a/test/foundry/integration/UpgradeSmartAccountTest.t.sol b/test/foundry/integration/UpgradeSmartAccountTest.t.sol index 06648a055..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, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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/eip7702/TestEIP7702.t.sol b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol index 9810fdb77..fbb2f7df0 100644 --- a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol +++ b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol @@ -58,7 +58,7 @@ contract TestEIP7702 is NexusTest_Base { address account = vm.addr(eoaKey); vm.deal(account, 100 ether); - uint256 nonce = getNonce(account, MODE_VALIDATION, address(mockValidator), 0); + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); // Create the userOp and add the data PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); @@ -98,7 +98,7 @@ contract TestEIP7702 is NexusTest_Base { // 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_VALIDATION, address(mockValidator), 0); + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); // Create the userOp and add the data PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); @@ -138,7 +138,7 @@ contract TestEIP7702 is NexusTest_Base { address account = vm.addr(eoaKey); vm.deal(account, 100 ether); - uint256 nonce = getNonce(account, MODE_VALIDATION, address(mockValidator), 0); + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); // Create the userOp and add the data PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); @@ -203,7 +203,7 @@ contract TestEIP7702 is NexusTest_Base { address account = vm.addr(eoaKey); vm.deal(account, 100 ether); - uint256 nonce = getNonce(account, MODE_VALIDATION, address(mockValidator), 0); + uint256 nonce = getNonce(account, MODE_DEFAULT_VALIDATOR, address(mockValidator), 0); // Create the userOp and add the data PackedUserOperation memory userOp = buildPackedUserOp(address(account), nonce); @@ -240,14 +240,9 @@ contract TestEIP7702 is NexusTest_Base { } function test_amIERC7702_success()public { - ExposedNexus exposedNexus = new ExposedNexus(address(ENTRYPOINT), address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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)))); - bytes32 code; - assembly { - extcodecopy(eip7702account, code, 0, 0x20) - } - console2.logBytes32(bytes32(code)); assertTrue(IExposedNexus(eip7702account).amIERC7702()); } } diff --git a/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol index c8c789685..5ba9d5be9 100644 --- a/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestBiconomyMetaFactory_Deployments.t.sol @@ -24,7 +24,7 @@ contract TestBiconomyMetaFactory_Deployments is NexusTest_Base { address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr), address(VALIDATOR_MODULE), - new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))), + 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 61fd5c0c8..9170b4e08 100644 --- a/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestK1ValidatorFactory_Deployments.t.sol @@ -21,7 +21,7 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { user = newWallet("user"); vm.deal(user.addr, 1 ether); initData = abi.encodePacked(user.addr); - bootstrapper = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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); } @@ -30,7 +30,7 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base { function test_ConstructorInitializesFactory() public { address implementation = address(0x123); address k1Validator = address(0x456); - NexusBootstrap bootstrapperInstance = new NexusBootstrap(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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 @@ -50,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(address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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 diff --git a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol index 9de4683e7..1d20b5484 100644 --- a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol @@ -177,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, address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + 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. diff --git a/test/foundry/utils/TestHelper.t.sol b/test/foundry/utils/TestHelper.t.sol index 13691e806..9c250e30d 100644 --- a/test/foundry/utils/TestHelper.t.sol +++ b/test/foundry/utils/TestHelper.t.sol @@ -26,9 +26,9 @@ import { NexusBootstrap, BootstrapConfig } from "../../../contracts/utils/NexusB 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 { EIP712 } from "solady/utils/EIP712.sol"; +import "../../../contracts/types/Constants.sol"; contract TestHelper is CheatCodes, EventsAndErrors { @@ -116,7 +116,8 @@ contract TestHelper is CheatCodes, EventsAndErrors { function deployTestContracts() internal { setupEntrypoint(); DEFAULT_VALIDATOR_MODULE = new MockValidator(); - ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT), address(DEFAULT_VALIDATOR_MODULE), abi.encodePacked(address(0))); + // 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); From f5e0e6ecf7dfc1ed5da6ac39cdccda5519dca75d Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 13:30:32 +0300 Subject: [PATCH 27/56] fix foumdry tests --- contracts/Nexus.sol | 11 ++- contracts/base/ModuleManager.sol | 11 +-- contracts/types/Constants.sol | 3 + .../TestAccountConfig_AccountId.t.sol | 1 + .../TestModuleManager_EnableMode.t.sol | 2 +- .../TestModuleManager_UninstallModule.t.sol | 88 ------------------- .../fuzz/TestFuzz_ExecuteFromExecutor.t.sol | 2 +- 7 files changed, 17 insertions(+), 101 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 777dadf22..0cb536dab 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -114,16 +114,19 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra 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)); } - PackedUserOperation memory userOp = op; - if (op.nonce.isModuleEnableMode()) { - userOp.signature = _enableMode(userOpHash, op.signature); - } + (userOpHash, userOp.signature) = _withPreValidationHook(userOpHash, userOp, missingAccountFunds); validationData = IValidator(validator).validateUserOp(userOp, userOpHash); } diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 474f54262..022058363 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -34,7 +34,8 @@ import { MODULE_TYPE_MULTI, MODULE_ENABLE_MODE_TYPE_HASH, EMERGENCY_UNINSTALL_TYPE_HASH, - ERC1271_MAGICVALUE + ERC1271_MAGICVALUE, + DEFAULT_VALIDATOR_FLAG } from "../types/Constants.sol"; import { EIP712 } from "solady/utils/EIP712.sol"; import { ExcessivelySafeCall } from "excessively-safe-call/ExcessivelySafeCall.sol"; @@ -43,8 +44,6 @@ import { RegistryAdapter } from "./RegistryAdapter.sol"; import { EmergencyUninstall } from "../types/DataTypes.sol"; import { ECDSA } from "solady/utils/ECDSA.sol"; -import "forge-std/console2.sol"; - /// @title Nexus - ModuleManager /// @notice Manages Validator, Executor, Hook, and Fallback modules within the Nexus suite, supporting /// @dev Implements SentinelList for managing modules via a linked list structure, adhering to ERC-7579. @@ -63,8 +62,6 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @dev The default validator address. /// @notice To initialize the default validator, Nexus.execute(_DEFAULT_VALIDATOR.onInstall(...)) should be called. address internal immutable _DEFAULT_VALIDATOR; - /// @notice The flag to indicate the default validator mode for enable mode signature - address internal constant _DEFAULT_VALIDATOR_FLAG = 0x0000000000000000000000000000000000000088; /// @dev initData should block the implementation from being used as a Smart Account constructor(address _defaultValidator, bytes memory _initData) { @@ -209,7 +206,7 @@ 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_FLAG) { + if (validator == _DEFAULT_VALIDATOR) { revert DefaultValidatorAlreadyInstalled(); } _getAccountStorage().validators.push(validator); @@ -613,7 +610,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @dev Returns the validator address to use function _handleSigValidator(address validator) internal view returns (address) { - if (validator == _DEFAULT_VALIDATOR_FLAG) { + if (validator == DEFAULT_VALIDATOR_FLAG) { return _DEFAULT_VALIDATOR; } else { require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); diff --git a/contracts/types/Constants.sol b/contracts/types/Constants.sol index 2e5cd277e..9cbb29195 100644 --- a/contracts/types/Constants.sol +++ b/contracts/types/Constants.sol @@ -55,5 +55,8 @@ 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; diff --git a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol index a2ac6dfb0..1e51d7667 100644 --- a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol +++ b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol @@ -8,6 +8,7 @@ contract TestAccountConfig_AccountId is NexusTest_Base { /// @notice Initialize the testing environment /// @notice Initialize the testing environment function setUp() public { + setupPredefinedWallets(); deployTestContracts(); } diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol index 884893412..3deaaa648 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol @@ -101,7 +101,7 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { (bytes memory multiInstallData, bytes32 hashToSign, ) = makeInstallDataAndHash(BOB_ADDRESS, MODULE_TYPE_MULTI, userOpHash); bytes memory enableModeSig = signMessage(BOB, hashToSign); //should be signed by current owner - //skip appending validator address, as it is not installed (emulate uninitialized 7702 account) + enableModeSig = abi.encodePacked(DEFAULT_VALIDATOR_FLAG, enableModeSig); //append validator address bytes memory enableModeSigPrefix = abi.encodePacked( moduleToEnable, 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/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); From d189e7f1e7bbd55f41b02e4863b7922db319c814 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 14:31:05 +0300 Subject: [PATCH 28/56] fix hh tests --- contracts/Nexus.sol | 2 + .../interfaces/INexusEventsAndErrors.sol | 3 + .../smart-account/Nexus.Basics.specs.ts | 67 +++++++------------ .../smart-account/Nexus.Factory.specs.ts | 24 +------ .../Nexus.ModuleManager.specs.ts | 27 -------- test/hardhat/utils/deployment.ts | 53 ++++++++++----- test/hardhat/utils/types.ts | 1 + 7 files changed, 69 insertions(+), 108 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 0cb536dab..2830ee7fe 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -278,6 +278,8 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @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 { + 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)) { diff --git a/contracts/interfaces/INexusEventsAndErrors.sol b/contracts/interfaces/INexusEventsAndErrors.sol index 3ed66b5a8..23d8657bd 100644 --- a/contracts/interfaces/INexusEventsAndErrors.sol +++ b/contracts/interfaces/INexusEventsAndErrors.sol @@ -54,4 +54,7 @@ interface INexusEventsAndErrors { /// @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/test/hardhat/smart-account/Nexus.Basics.specs.ts b/test/hardhat/smart-account/Nexus.Basics.specs.ts index e66cada40..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; @@ -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 ad4646688..abf427552 100644 --- a/test/hardhat/smart-account/Nexus.Factory.specs.ts +++ b/test/hardhat/smart-account/Nexus.Factory.specs.ts @@ -165,7 +165,7 @@ describe("Nexus Factory Tests", function () { }); it("Should prevent account reinitialization", async function () { - await expect(smartAccount.initializeAccount("0x")).to.be.rejectedWith( + await expect(smartAccount.initializeAccount("0x00000000000000000000000000000000123456784e4e4e4e")).to.be.rejectedWith( "reverted with an unrecognized custom error (return data: 0xaed59595)", // NotInitializable() ); }); @@ -284,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), @@ -439,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 () { 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 Date: Thu, 27 Feb 2025 14:48:53 +0300 Subject: [PATCH 29/56] enable mode + prev hook --- contracts/modules/validators/K1Validator.sol | 2 +- .../TestModuleManager_EnableMode.t.sol | 74 +++++++++++++++++++ .../concrete/modules/TestK1Validator.t.sol | 13 +++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/contracts/modules/validators/K1Validator.sol b/contracts/modules/validators/K1Validator.sol index d8bb13607..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; diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol index 3deaaa648..e0081161f 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol @@ -7,6 +7,8 @@ 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 } 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 { @@ -23,6 +25,8 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { MockMultiModule mockMultiModule; Counter public counter; + MockResourceLockPreValidationHook private resourceLockHook; + MockAccountLocker private accountLocker; string constant MODULE_ENABLE_MODE_NOTATION = "ModuleEnableMode(address module,uint256 moduleType,bytes32 userOpHash,bytes32 initDataHash)"; @@ -30,6 +34,8 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { setUpModuleManagement_Base(); mockMultiModule = new MockMultiModule(); counter = new Counter(); + accountLocker = new MockAccountLocker(); + resourceLockHook = new MockResourceLockPreValidationHook(address(accountLocker), address(0)); } function test_EnableMode_Success_No7739() public { @@ -78,6 +84,74 @@ 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); diff --git a/test/foundry/unit/concrete/modules/TestK1Validator.t.sol b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol index 1ab78235b..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,6 +201,13 @@ contract TestK1Validator is NexusTest_Base { assertFalse(result, "Module type should be invalid"); } + /// @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 function test_ValidateUserOp_ValidSignature() public { bytes32 originalHash = keccak256(abi.encodePacked("valid message")); From 279453789820c6cd97f7e3f080eb43f8de55916f Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 16:19:50 +0300 Subject: [PATCH 30/56] draft handling in validateUserOp --- contracts/Nexus.sol | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 2830ee7fe..223fc1055 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -118,10 +118,23 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra 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()) { + if (op.nonce.isModeValidate()) { + // do nothing special. This is introduced + // to quickly identify the most commonly used + // mode which is validate mode + // and avoid checking two above conditions + } else if (op.nonce.isModuleEnableMode()) {\ + // if it is module enable mode, we need to enable the module first + // and get the cleaned signature userOp.signature = _enableMode(userOpHash, op.signature); + } else if (op.nonce.isPrepMode()) { + // PREP Mode. Authorize prep signature + // and initialize the account + // PREP mode is only used for the uninited PREPs + require(!isInitialized(), AccountAlreadyInitialized()); + bytes calldata initData; + (userOp.signature, initData) = _handlePREP(op.signature); + _initializeAccount(initData); } validator = op.nonce.getValidator(); require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator)); @@ -278,18 +291,21 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @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 { - 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(); } + _initializeAccount(initData); + } + + function _initializeAccount(bytes calldata initData) internal { + equire(initData.length >= 24, InvalidInitData()); address bootstrap; bytes calldata bootstrapCall; assembly { - bootstrap := calldataload(initData.offset) + bootstrap := calldataload(initData.offset)x let s := calldataload(add(initData.offset, 0x20)) let u := add(initData.offset, s) bootstrapCall.offset := add(u, 0x20) From ab3b7363d1c3e7688da86e4d581720530d28be15 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 17:38:21 +0300 Subject: [PATCH 31/56] builds --- contracts/Nexus.sol | 73 ++++++++++++++++++- .../interfaces/INexusEventsAndErrors.sol | 6 ++ contracts/lib/NonceLib.sol | 22 +++++- contracts/types/Constants.sol | 1 + 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 223fc1055..b39b9cdc8 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -48,6 +48,7 @@ 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"; +import { ECDSA } from "solady/utils/ECDSA.sol"; /// @title Nexus - Smart Account /// @notice This contract integrates various functionalities to handle modular smart accounts compliant with ERC-7579 and ERC-4337 standards. @@ -118,12 +119,12 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra if (op.nonce.isDefaultValidatorMode()) { validator = _DEFAULT_VALIDATOR; } else { - if (op.nonce.isModeValidate()) { + if (op.nonce.isValidateMode()) { // do nothing special. This is introduced // to quickly identify the most commonly used // mode which is validate mode // and avoid checking two above conditions - } else if (op.nonce.isModuleEnableMode()) {\ + } else if (op.nonce.isModuleEnableMode()) { // if it is module enable mode, we need to enable the module first // and get the cleaned signature userOp.signature = _enableMode(userOpHash, op.signature); @@ -300,12 +301,12 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra } function _initializeAccount(bytes calldata initData) internal { - equire(initData.length >= 24, InvalidInitData()); + require(initData.length >= 24, InvalidInitData()); address bootstrap; bytes calldata bootstrapCall; assembly { - bootstrap := calldataload(initData.offset)x + bootstrap := calldataload(initData.offset) let s := calldataload(add(initData.offset, 0x20)) let u := add(initData.offset, s) bootstrapCall.offset := add(u, 0x20) @@ -478,6 +479,70 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra } } + /** + * authHash: 32 bytes + * erc7702AuthSignature: 65 bytes + * initData: bytes array + * cleaned 4337 Nexus signature: bytes array + */ + function _handlePREP(bytes calldata data) internal returns (bytes calldata cleanedSignature, bytes calldata initData) { + bytes32 authHash; + bytes calldata erc7702AuthSignature; + + bytes32 r; + bytes32 s; + + assembly { + if lt(data.length, 0x61) { + mstore(0x0, 0xaed59595) // NotInitializable() + revert(0x1c, 0x04) + } + authHash := calldataload(data.offset) + + // erc7702AuthSignature + let p := calldataload(add(data.offset, 0x20)) + let u := add(data.offset, p) + erc7702AuthSignature.offset := add(u, 0x20) + erc7702AuthSignature.length := calldataload(u) + + // initData + p := calldataload(add(data.offset, 0x40)) + u := add(data.offset, p) + initData.offset := add(u, 0x20) + initData.length := calldataload(u) + + // cleanedSignature + p := calldataload(add(data.offset, 0x60)) + u := add(data.offset, p) + cleanedSignature.offset := add(u, 0x20) + cleanedSignature.length := calldataload(u) + + r := calldataload(erc7702AuthSignature.offset) + s := calldataload(add(erc7702AuthSignature.offset, 0x20)) + } + + // check that signature (r value) is based on the hash of the initData provided + bytes32 initDataHash = keccak256(initData); + require(r == initDataHash, InvalidNicksMethodData(authHash, initDataHash, erc7702AuthSignature)); + + // check that signature (s value) matches the expected pattern of having 0s in the 20 leftmost bytes + require(s & 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000 == bytes32(0)); + + // check auth hash signed by address(this) + // we just use authHash provided in the `data` instead of recomputing it + // because it is computationally unlikely to find another hash that + // combined with another `r` (which means another initdata) + // and another `s` that matches the pattern of having 0s in the 20 leftmost bytes + // would result in the same recovered signer (address(this)). + address signer = ECDSA.recoverCalldata(authHash, erc7702AuthSignature); + assembly { + if iszero(eq(signer, address())) { + mstore(0x0, 0xaed59595) // NotInitializable() + revert(0x1c, 0x04) + } + } + } + /// @dev EIP712 domain name and version. function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { name = "Nexus"; diff --git a/contracts/interfaces/INexusEventsAndErrors.sol b/contracts/interfaces/INexusEventsAndErrors.sol index 23d8657bd..2a7ae039c 100644 --- a/contracts/interfaces/INexusEventsAndErrors.sol +++ b/contracts/interfaces/INexusEventsAndErrors.sol @@ -57,4 +57,10 @@ interface INexusEventsAndErrors { /// @notice Error thrown when the provided initData is invalid. error InvalidInitData(); + + /// @notice Error thrown when the provided authHash and erc7702AuthSignature are invalid. + error InvalidNicksMethodData(bytes32 authHash, bytes32 initDataHash, bytes erc7702AuthSignature); + + /// @notice Error thrown when the account is already initialized. + error AccountAlreadyInitialized(); } diff --git a/contracts/lib/NonceLib.sol b/contracts/lib/NonceLib.sol index faf48df6d..59c9813fa 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, MODE_DEFAULT_VALIDATOR } from "../types/Constants.sol"; +import { MODE_MODULE_ENABLE, MODE_DEFAULT_VALIDATOR, MODE_PREP, MODE_VALIDATION } from "../types/Constants.sol"; /** Nonce structure @@ -37,4 +37,24 @@ library NonceLib { res := eq(shl(248, vmode), MODE_DEFAULT_VALIDATOR) } } + + /// @dev Detects if Validaton Mode is Prep Mode + /// @param nonce The nonce + /// @return res boolean result, true if it is the Prep Mode + function isPrepMode(uint256 nonce) internal pure returns (bool res) { + assembly { + let vmode := byte(3, nonce) + res := eq(shl(248, vmode), MODE_PREP) + } + } + + /// @dev Detects if Validaton Mode is Validate Mode + /// @param nonce The nonce + /// @return res boolean result, true if it is the Validate Mode + function isValidateMode(uint256 nonce) internal pure returns (bool res) { + assembly { + let vmode := byte(3, nonce) + res := eq(shl(248, vmode), MODE_VALIDATION) + } + } } diff --git a/contracts/types/Constants.sol b/contracts/types/Constants.sol index 9cbb29195..b2fa58209 100644 --- a/contracts/types/Constants.sol +++ b/contracts/types/Constants.sol @@ -54,6 +54,7 @@ bytes32 constant EMERGENCY_UNINSTALL_TYPE_HASH = 0xd3ddfc12654178cc44d4a7b6b969c bytes1 constant MODE_VALIDATION = 0x00; bytes1 constant MODE_MODULE_ENABLE = 0x01; bytes1 constant MODE_DEFAULT_VALIDATOR = 0x02; +bytes1 constant MODE_PREP = 0x03; // The flag to indicate the default validator mode for enable mode signature address constant DEFAULT_VALIDATOR_FLAG = 0x0000000000000000000000000000000000000088; From ac751b450c6695aa73c2ee4fa8a7f17ca4d5ee3b Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 27 Feb 2025 17:42:23 +0300 Subject: [PATCH 32/56] events in bootstrapper --- contracts/Nexus.sol | 2 +- contracts/utils/NexusBootstrap.sol | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 2830ee7fe..2b32ef3a4 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -222,7 +222,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @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); @@ -235,6 +234,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra ) { _uninstallHook(module, moduleTypeId, deInitData); } + emit ModuleUninstalled(moduleTypeId, module); } function emergencyUninstallHook(EmergencyUninstall calldata data, bytes calldata signature) external payable { diff --git a/contracts/utils/NexusBootstrap.sol b/contracts/utils/NexusBootstrap.sol index 3b215375c..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. @@ -71,6 +77,7 @@ contract NexusBootstrap is ModuleManager { { _configureRegistry(registry, attesters, threshold); _installValidator(address(validator), data); + emit ModuleInstalled(MODULE_TYPE_VALIDATOR, address(validator)); } /// @notice Initializes the Nexus account with multiple modules. @@ -97,23 +104,27 @@ contract NexusBootstrap is ModuleManager { // 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); } } @@ -137,11 +148,13 @@ contract NexusBootstrap is ModuleManager { // 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); } } From 85ff44fa5b9010e430099194946ffb84e560cdb7 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 11:59:38 +0300 Subject: [PATCH 33/56] prep verif --- contracts/Nexus.sol | 50 ++----- .../interfaces/INexusEventsAndErrors.sol | 2 +- package.json | 17 ++- remappings.txt | 1 + .../unit/concrete/eip7702/TestPREP.sol | 137 ++++++++++++++++++ yarn.lock | 66 ++++++++- 6 files changed, 218 insertions(+), 55 deletions(-) create mode 100644 test/foundry/unit/concrete/eip7702/TestPREP.sol diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index b39b9cdc8..bcb92b49e 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -48,7 +48,7 @@ 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"; -import { ECDSA } from "solady/utils/ECDSA.sol"; +import { LibPREP } from "lib-prep/LibPREP.sol"; /// @title Nexus - Smart Account /// @notice This contract integrates various functionalities to handle modular smart accounts compliant with ERC-7579 and ERC-4337 standards. @@ -481,66 +481,36 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /** * authHash: 32 bytes - * erc7702AuthSignature: 65 bytes * initData: bytes array * cleaned 4337 Nexus signature: bytes array */ function _handlePREP(bytes calldata data) internal returns (bytes calldata cleanedSignature, bytes calldata initData) { - bytes32 authHash; - bytes calldata erc7702AuthSignature; - - bytes32 r; - bytes32 s; - + bytes32 saltAndDelegation; assembly { if lt(data.length, 0x61) { mstore(0x0, 0xaed59595) // NotInitializable() revert(0x1c, 0x04) } - authHash := calldataload(data.offset) + saltAndDelegation := calldataload(data.offset) - // erc7702AuthSignature + // initData let p := calldataload(add(data.offset, 0x20)) let u := add(data.offset, p) - erc7702AuthSignature.offset := add(u, 0x20) - erc7702AuthSignature.length := calldataload(u) - - // initData - p := calldataload(add(data.offset, 0x40)) - u := add(data.offset, p) initData.offset := add(u, 0x20) initData.length := calldataload(u) // cleanedSignature - p := calldataload(add(data.offset, 0x60)) + p := calldataload(add(data.offset, 0x40)) u := add(data.offset, p) cleanedSignature.offset := add(u, 0x20) cleanedSignature.length := calldataload(u) - - r := calldataload(erc7702AuthSignature.offset) - s := calldataload(add(erc7702AuthSignature.offset, 0x20)) } - // check that signature (r value) is based on the hash of the initData provided - bytes32 initDataHash = keccak256(initData); - require(r == initDataHash, InvalidNicksMethodData(authHash, initDataHash, erc7702AuthSignature)); - - // check that signature (s value) matches the expected pattern of having 0s in the 20 leftmost bytes - require(s & 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000 == bytes32(0)); - - // check auth hash signed by address(this) - // we just use authHash provided in the `data` instead of recomputing it - // because it is computationally unlikely to find another hash that - // combined with another `r` (which means another initdata) - // and another `s` that matches the pattern of having 0s in the 20 leftmost bytes - // would result in the same recovered signer (address(this)). - address signer = ECDSA.recoverCalldata(authHash, erc7702AuthSignature); - assembly { - if iszero(eq(signer, address())) { - mstore(0x0, 0xaed59595) // NotInitializable() - revert(0x1c, 0x04) - } - } + // check that signature (r value) is based on the hash of the initData provided + bytes32 r = LibPREP.rPREP(address(this), keccak256(initData), saltAndDelegation); + if (r == bytes32(0)) { + revert InvalidNicksMethodData(saltAndDelegation, keccak256(initData)); + } } /// @dev EIP712 domain name and version. diff --git a/contracts/interfaces/INexusEventsAndErrors.sol b/contracts/interfaces/INexusEventsAndErrors.sol index 2a7ae039c..6650ed143 100644 --- a/contracts/interfaces/INexusEventsAndErrors.sol +++ b/contracts/interfaces/INexusEventsAndErrors.sol @@ -59,7 +59,7 @@ interface INexusEventsAndErrors { error InvalidInitData(); /// @notice Error thrown when the provided authHash and erc7702AuthSignature are invalid. - error InvalidNicksMethodData(bytes32 authHash, bytes32 initDataHash, bytes erc7702AuthSignature); + error InvalidNicksMethodData(bytes32 authHash, bytes32 initDataHash); /// @notice Error thrown when the account is already initialized. error AccountAlreadyInitialized(); diff --git a/package.json b/package.json index 1f3b90e2d..3130bd859 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,6 @@ "url": "https://github.com/bcnmy" }, "devDependencies": { - "account-abstraction": "https://github.com/eth-infinitism/account-abstraction#v0.7.0", - "erc7739-validator-base": "https://github.com/erc7579/erc7739Validator#v1.0.0", - "sentinellist": "github:rhinestonewtf/sentinellist#v1.0.0", - "forge-std": "github:foundry-rs/forge-std#v1.8.2", - "solady": "github:vectorized/solady#v0.0.271", - "solarray": "github:sablier-labs/solarray", "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.6", "@nomicfoundation/hardhat-foundry": "^1.1.2", @@ -25,9 +19,13 @@ "@types/chai": "^4.3.16", "@types/mocha": ">=10.0.6", "@types/node": ">=20.12.12", + "account-abstraction": "https://github.com/eth-infinitism/account-abstraction#v0.7.0", "chai": "^4.3.7", "codecov": "^3.8.3", + "dotenv": "^16.4.5", + "erc7739-validator-base": "https://github.com/erc7579/erc7739Validator#v1.0.0", "ethers": "^6.12.1", + "forge-std": "github:foundry-rs/forge-std#v1.9.6", "hardhat": "^2.22.4", "hardhat-deploy": "^0.12.4", "hardhat-deploy-ethers": "^0.4.2", @@ -36,13 +34,16 @@ "husky": "^9.0.11", "prettier": "^3.2.5", "prettier-plugin-solidity": "^1.3.1", + "sentinellist": "github:rhinestonewtf/sentinellist#v1.0.0", + "solady": "github:vectorized/solady#v0.1.9", + "solarray": "github:sablier-labs/solarray", "solhint": "^5.0.1", "solhint-plugin-prettier": "^0.1.0", "solidity-coverage": "^0.8.12", "ts-node": ">=10.9.2", "typechain": "^8.3.2", - "dotenv": "^16.4.5", - "typescript": ">=5.4.5" + "typescript": ">=5.4.5", + "lib-prep": "ithacaxyz/account#vectorized/prep" }, "keywords": [ "nexus", diff --git a/remappings.txt b/remappings.txt index 1acb59a1b..1d5b9b5ff 100644 --- a/remappings.txt +++ b/remappings.txt @@ -6,3 +6,4 @@ excessively-safe-call/=node_modules/excessively-safe-call/src/ sentinellist/=node_modules/sentinellist/src/ solarray/=node_modules/solarray/src/ erc7739Validator/=node_modules/erc7739-validator-base/src/ +lib-prep/=node_modules/lib-prep/src/ \ No newline at end of file diff --git a/test/foundry/unit/concrete/eip7702/TestPREP.sol b/test/foundry/unit/concrete/eip7702/TestPREP.sol new file mode 100644 index 000000000..586dac314 --- /dev/null +++ b/test/foundry/unit/concrete/eip7702/TestPREP.sol @@ -0,0 +1,137 @@ +// 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 { LibRLP } from "solady/utils/LibRLP.sol"; +import { EfficientHashLib } from "solady/utils/EfficientHashLib.sol"; + +contract TestPREP is NexusTest_Base { + + uint8 constant MAGIC = 0x05; + + using ECDSA for bytes32; + using LibRLP for *; + + MockTarget target; + MockValidator public mockValidator; + MockExecutor public mockExecutor; + + function setUp() public { + setupTestEnvironment(); + 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 _getUserOpSignature(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_PREP_Initialization_Success() public { + + /* + struct SignedDelegation { + // The y-parity of the recovered secp256k1 signature (0 or 1). + uint8 v; + // First 32 bytes of the signature. + bytes32 r; + // Second 32 bytes of the signature. + bytes32 s; + // The current nonce of the authority account at signing time. + // Used to ensure signature can't be replayed after account nonce changes. + uint64 nonce; + // Address of the contract implementation that will be delegated to. + // Gets encoded into delegation code: 0xef0100 || implementation. + address implementation; +} +*/ + bytes memory initData = _getInitData(); + bytes32 initDataHash = keccak256(abi.encodePacked(initData)); + + uint256 eoaKey = uint256(1010101010101); + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(ACCOUNT_IMPLEMENTATION), eoaKey); + + // ================================ + + bytes memory rlpAuth = abi.encodePacked(hex"05", LibRLP.p(uint256(0x7a69)).p(signedDelegation.implementation).p(signedDelegation.nonce).encode()); + bytes32 authHash = keccak256(rlpAuth); + + + //signedDelegation.s = bytes32(uint256(0x0000000000000000000000000000000000000000ffffffffffffffffffffffff)) & keccak256(abi.encodePacked(block.timestamp)); + + uint256 i=2**12; + address prep; + console2.log(signedDelegation.v); + + while (prep == address(0)) { + signedDelegation.r = EfficientHashLib.hash(uint256(initDataHash), i) & bytes32(uint256(2 ** 160 - 1)); + //signedDelegation.s = keccak256(abi.encodePacked(initDataHash, i)); + signedDelegation.s = keccak256(abi.encodePacked(signedDelegation.r)); + prep = authHash.tryRecover(signedDelegation.v+27, signedDelegation.r, signedDelegation.s); + i++; + console2.log(i); + console2.log(prep); + } + + console2.log(prep); + + vm.attachDelegation(signedDelegation); + + + bytes32 prepCode; + assembly { + extcodecopy(prep, prepCode, 0, 23) + } + console2.logBytes32(prepCode); + } + + function test_Auth_Hash_generation() public { + uint256 eoaKey = uint256(1010101010101); + + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(ACCOUNT_IMPLEMENTATION), eoaKey); + + bytes memory auth = abi.encodePacked( + MAGIC, + abi.encode( + uint256(0x7a69), + signedDelegation.implementation, + signedDelegation.nonce + ) + ); + + console2.logBytes(auth); + + bytes memory rlpAuth = abi.encodePacked(hex"05", LibRLP.p(uint256(0x7a69)).p(signedDelegation.implementation).p(signedDelegation.nonce).encode()); + + console2.logBytes(rlpAuth); + + bytes32 authHash = keccak256(rlpAuth); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(eoaKey, authHash); + + console2.log("v"); + console2.log(v); + + assertEq(r, signedDelegation.r); + assertEq(s, signedDelegation.s); + + // assertEq(v, signedDelegation.v); + } +} diff --git a/yarn.lock b/yarn.lock index 9047bad38..8703908d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -768,6 +768,27 @@ resolved "https://registry.yarnpkg.com/@prettier/sync/-/sync-0.3.0.tgz#91f2cfc23490a21586d1cf89c6f72157c000ca1e" integrity sha512-3dcmCyAxIcxy036h1I7MQU/uEEBq8oLwf1CE3xeze+MPlgkdlb/+w6rGR/1dhp6Hqi17fRS6nvwnOzkESxEkOw== +"@rhinestone/checknsignatures@github:rhinestonewtf/checknsignatures": + version "0.0.1" + resolved "https://codeload.github.com/rhinestonewtf/checknsignatures/tar.gz/f8389c186fb58480a9a42d598adebaa1a557762d" + dependencies: + forge-std "github:foundry-rs/forge-std" + solady "github:vectorized/solady" + +"@rhinestone/erc4337-validation@0.0.1-alpha.2": + version "0.0.1-alpha.2" + resolved "https://registry.yarnpkg.com/@rhinestone/erc4337-validation/-/erc4337-validation-0.0.1-alpha.2.tgz#9278ca59972e12838e3a25230041cc21d1c8053f" + integrity sha512-sxBSHoR0hV0rN2bv5HfINHR3RyBChfd0OWH0TP8nlA9FolJ1EezLByxcyrvAgi2QLQ2Zf2zVcNky1qYdfF4NjQ== + dependencies: + "@openzeppelin/contracts" "5.0.1" + account-abstraction "github:kopy-kat/account-abstraction#develop" + account-abstraction-v0.6 "github:eth-infinitism/account-abstraction#v0.6.0" + ds-test "github:dapphub/ds-test" + forge-std "github:foundry-rs/forge-std" + prettier "^2.8.8" + solady "github:vectorized/solady" + solhint "^4.1.1" + "@rhinestone/erc4337-validation@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@rhinestone/erc4337-validation/-/erc4337-validation-0.0.4.tgz#85d29a8f624c66ae5cbf9eea86c3bbaccbce649f" @@ -832,6 +853,9 @@ resolved "https://codeload.github.com/rhinestonewtf/safe7579/tar.gz/33f110f08ed5fcab75c29d7cfb93f7f3e4da76a7" dependencies: "@ERC4337/account-abstraction" "github:kopy-kat/account-abstraction#develop" + "@ERC4337/account-abstraction-v0.6" "github:eth-infinitism/account-abstraction#v0.6.0" + "@rhinestone/checknsignatures" "github:rhinestonewtf/checknsignatures" + "@rhinestone/erc4337-validation" "0.0.1-alpha.2" "@rhinestone/module-bases" "github:rhinestonewtf/module-bases" "@rhinestone/sentinellist" "github:rhinestonewtf/sentinellist" "@safe-global/safe-contracts" "^1.4.1" @@ -2510,9 +2534,9 @@ follow-redirects@^1.12.1, follow-redirects@^1.14.0, follow-redirects@^1.15.6: version "1.7.6" resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/ae570fec082bfe1c1f45b0acca4a2b4f84d345ce" -"forge-std@github:foundry-rs/forge-std#v1.8.2": - version "1.8.2" - resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/978ac6fadb62f5f0b723c996f64be52eddba6801" +"forge-std@github:foundry-rs/forge-std#v1.9.6": + version "1.9.6" + resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/3b20d60d14b343ee4f908cb8079495c07f5e8981" form-data-encoder@^2.1.2: version "2.1.4" @@ -3309,6 +3333,10 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lib-prep@ithacaxyz/account#vectorized/prep: + version "0.0.0" + resolved "https://codeload.github.com/ithacaxyz/account/tar.gz/733464120b83c7699ba5a3ccf0b56c9ae6dea001" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -4222,9 +4250,9 @@ slice-ansi@^4.0.0: version "0.0.168" resolved "https://codeload.github.com/vectorized/solady/tar.gz/9deb9ed36a27261a8745db5b7cd7f4cdc3b1cd4e" -"solady@github:vectorized/solady#v0.0.271": - version "0.0.271" - resolved "https://codeload.github.com/vectorized/solady/tar.gz/b38e2c025f8cd1ecbfc1b92678fe05ac950a4bb1" +"solady@github:vectorized/solady#v0.1.9": + version "0.1.9" + resolved "https://codeload.github.com/vectorized/solady/tar.gz/1eb89b9af0a09468c6ccff51ce8ed5eb520b4fb5" "solarray@github:sablier-labs/solarray": version "1.0.0" @@ -4251,6 +4279,32 @@ solhint-plugin-prettier@^0.1.0: "@prettier/sync" "^0.3.0" prettier-linter-helpers "^1.0.0" +solhint@^4.1.1: + version "4.5.4" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-4.5.4.tgz#171cf33f46c36b8499efe60c0e425f6883a54e50" + integrity sha512-Cu1XiJXub2q1eCr9kkJ9VPv1sGcmj3V7Zb76B0CoezDOB9bu3DxKIFFH7ggCl9fWpEPD6xBmRLfZrYijkVmujQ== + dependencies: + "@solidity-parser/parser" "^0.18.0" + ajv "^6.12.6" + antlr4 "^4.13.1-patch-1" + ast-parents "^0.0.1" + chalk "^4.1.2" + commander "^10.0.0" + cosmiconfig "^8.0.0" + fast-diff "^1.2.0" + glob "^8.0.3" + ignore "^5.2.4" + js-yaml "^4.1.0" + latest-version "^7.0.0" + lodash "^4.17.21" + pluralize "^8.0.0" + semver "^7.5.2" + strip-ansi "^6.0.1" + table "^6.8.1" + text-table "^0.2.0" + optionalDependencies: + prettier "^2.8.3" + solhint@^5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/solhint/-/solhint-5.0.3.tgz#b57f6d2534fe09a60f9db1b92e834363edd1cbde" From 50b94c15f873deaa56aa7e42627169bf8a0cf692 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 12:08:25 +0300 Subject: [PATCH 34/56] reduce size --- contracts/Nexus.sol | 9 ++++----- contracts/base/ModuleManager.sol | 12 ++++++------ contracts/interfaces/INexusEventsAndErrors.sol | 9 ++++----- .../base/IModuleManagerEventsAndErrors.sol | 4 ++-- .../TestModuleManager_InstallModule.t.sol | 6 +++--- test/foundry/unit/fuzz/TestFuzz_ModuleManager.t.sol | 4 ++-- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index bcb92b49e..e3ce49ca8 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -199,9 +199,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra function executeUserOp(PackedUserOperation calldata userOp, bytes32) external payable virtual onlyEntryPoint withHook { bytes calldata callData = userOp.callData[4:]; (bool success, bytes memory innerCallRet) = address(this).delegatecall(callData); - if (success) { - emit Executed(userOp, innerCallRet); - } else { + if (!success) { revert ExecutionFailed(); } } @@ -509,8 +507,9 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra // check that signature (r value) is based on the hash of the initData provided bytes32 r = LibPREP.rPREP(address(this), keccak256(initData), saltAndDelegation); if (r == bytes32(0)) { - revert InvalidNicksMethodData(saltAndDelegation, keccak256(initData)); - } + revert InvalidPREP(); + } + emit PREPInitialized(r); } /// @dev EIP712 domain name and version. diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 022058363..9291fbc6e 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -66,7 +66,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @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); + revert MismatchModuleTypeId(); IValidator(_defaultValidator).onInstall(_initData); _DEFAULT_VALIDATOR = _defaultValidator; } @@ -205,7 +205,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @param validator The address of the validator module to be installed. /// @param data Initialization data to configure the validator upon installation. function _installValidator(address validator, bytes calldata data) internal virtual withRegistry(validator, MODULE_TYPE_VALIDATOR) { - if (!IValidator(validator).isModuleType(MODULE_TYPE_VALIDATOR)) revert MismatchModuleTypeId(MODULE_TYPE_VALIDATOR); + if (!IValidator(validator).isModuleType(MODULE_TYPE_VALIDATOR)) revert MismatchModuleTypeId(); if (validator == _DEFAULT_VALIDATOR) { revert DefaultValidatorAlreadyInstalled(); } @@ -231,7 +231,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @param executor The address of the executor module to be installed. /// @param data Initialization data to configure the executor upon installation. function _installExecutor(address executor, bytes calldata data) internal virtual withRegistry(executor, MODULE_TYPE_EXECUTOR) { - if (!IExecutor(executor).isModuleType(MODULE_TYPE_EXECUTOR)) revert MismatchModuleTypeId(MODULE_TYPE_EXECUTOR); + if (!IExecutor(executor).isModuleType(MODULE_TYPE_EXECUTOR)) revert MismatchModuleTypeId(); _getAccountStorage().executors.push(executor); IExecutor(executor).onInstall(data); } @@ -249,7 +249,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @param hook The address of the hook to be installed. /// @param data Initialization data to configure the hook upon installation. function _installHook(address hook, bytes calldata data) internal virtual withRegistry(hook, MODULE_TYPE_HOOK) { - if (!IHook(hook).isModuleType(MODULE_TYPE_HOOK)) revert MismatchModuleTypeId(MODULE_TYPE_HOOK); + if (!IHook(hook).isModuleType(MODULE_TYPE_HOOK)) revert MismatchModuleTypeId(); address currentHook = _getHook(); require(currentHook == address(0), HookAlreadyInstalled(currentHook)); _setHook(hook); @@ -279,7 +279,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError /// @param handler The address of the fallback handler to install. /// @param params The initialization parameters including the selector and call type. function _installFallbackHandler(address handler, bytes calldata params) internal virtual withRegistry(handler, MODULE_TYPE_FALLBACK) { - if (!IFallback(handler).isModuleType(MODULE_TYPE_FALLBACK)) revert MismatchModuleTypeId(MODULE_TYPE_FALLBACK); + if (!IFallback(handler).isModuleType(MODULE_TYPE_FALLBACK)) revert MismatchModuleTypeId(); // Extract the function selector from the provided parameters. bytes4 selector = bytes4(params[0:4]); @@ -333,7 +333,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError virtual withRegistry(preValidationHook, preValidationHookType) { - if (!IModule(preValidationHook).isModuleType(preValidationHookType)) revert MismatchModuleTypeId(MODULE_TYPE_HOOK); + if (!IModule(preValidationHook).isModuleType(preValidationHookType)) revert MismatchModuleTypeId(); address currentPreValidationHook = _getPreValidationHook(preValidationHookType); require(currentPreValidationHook == address(0), PrevalidationHookAlreadyInstalled(currentPreValidationHook)); _setPreValidationHook(preValidationHookType, preValidationHook); diff --git a/contracts/interfaces/INexusEventsAndErrors.sol b/contracts/interfaces/INexusEventsAndErrors.sol index 6650ed143..a88ef5e22 100644 --- a/contracts/interfaces/INexusEventsAndErrors.sol +++ b/contracts/interfaces/INexusEventsAndErrors.sol @@ -22,10 +22,9 @@ import { PackedUserOperation } from "account-abstraction/interfaces/PackedUserOp /// @author @zeroknots | Rhinestone.wtf | zeroknots.eth /// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady interface INexusEventsAndErrors { - /// @notice Emitted when a user operation is executed from `executeUserOp` - /// @param userOp The user operation that was executed. - /// @param innerCallRet The return data from the inner call execution. - event Executed(PackedUserOperation userOp, bytes innerCallRet); + /// @notice Emitted when a PREP is initialized. + /// @param r The r value of the PREP signature. + event PREPInitialized(bytes32 r); /// @notice Error thrown when an unsupported ModuleType is requested. /// @param moduleTypeId The ID of the unsupported module type. @@ -59,7 +58,7 @@ interface INexusEventsAndErrors { error InvalidInitData(); /// @notice Error thrown when the provided authHash and erc7702AuthSignature are invalid. - error InvalidNicksMethodData(bytes32 authHash, bytes32 initDataHash); + error InvalidPREP(); /// @notice Error thrown when the account is already initialized. error AccountAlreadyInitialized(); diff --git a/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol b/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol index 5523f56ff..0a7ab962d 100644 --- a/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol +++ b/contracts/interfaces/base/IModuleManagerEventsAndErrors.sol @@ -93,8 +93,8 @@ interface IModuleManagerEventsAndErrors { /// @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); + /// Error thrown when account installs/uninstalls module with mismatched moduleTypeId + error MismatchModuleTypeId(); /// @dev Thrown when there is an attempt to install a forbidden selector as a fallback handler. error FallbackSelectorForbidden(); diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_InstallModule.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_InstallModule.t.sol index b0b556db0..ca1c6a408 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_InstallModule.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_InstallModule.t.sol @@ -234,7 +234,7 @@ contract TestModuleManager_InstallModule is TestModuleManagement_Base { bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); // Expected revert reason encoded - bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId(uint256)", MODULE_TYPE_EXECUTOR); + bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId()"); // Expect the UserOperationRevertReason event vm.expectEmit(true, true, true, true); @@ -258,7 +258,7 @@ contract TestModuleManager_InstallModule is TestModuleManagement_Base { PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); - bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId(uint256)", MODULE_TYPE_EXECUTOR); + bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId()"); bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); // Expect the UserOperationRevertReason event @@ -288,7 +288,7 @@ contract TestModuleManager_InstallModule is TestModuleManagement_Base { PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); - bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId(uint256)", MODULE_TYPE_VALIDATOR); + bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId()"); bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); // Expect the UserOperationRevertReason event diff --git a/test/foundry/unit/fuzz/TestFuzz_ModuleManager.t.sol b/test/foundry/unit/fuzz/TestFuzz_ModuleManager.t.sol index a2e33b3c5..ea5503ddb 100644 --- a/test/foundry/unit/fuzz/TestFuzz_ModuleManager.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_ModuleManager.t.sol @@ -84,7 +84,7 @@ contract TestFuzz_ModuleManager is TestModuleManagement_Base { // Perform the installation and handle possible mismatches if (!IModule(moduleAddress).isModuleType(moduleTypeId)) { // Expect failure if the module type does not match the expected type ID - bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId(uint256)", moduleTypeId); + bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId()"); bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); vm.expectEmit(true, true, true, true); emit UserOperationRevertReason(userOpHash, address(BOB_ACCOUNT), userOps[0].nonce, expectedRevertReason); @@ -115,7 +115,7 @@ contract TestFuzz_ModuleManager is TestModuleManagement_Base { // First installation should succeed if the module type matches if (!IModule(moduleAddress).isModuleType(moduleTypeId)) { - bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId(uint256)", moduleTypeId); + bytes memory expectedRevertReason = abi.encodeWithSignature("MismatchModuleTypeId()"); bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); vm.expectEmit(true, true, true, true); emit UserOperationRevertReason(userOpHash, address(BOB_ACCOUNT), userOps[0].nonce, expectedRevertReason); From 5b23ccf17d5040e0c53f40b764377959c9083081 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 13:22:15 +0300 Subject: [PATCH 35/56] fix tests --- contracts/base/ModuleManager.sol | 9 +- contracts/mocks/MockValidator.sol | 16 +- .../unit/concrete/eip7702/TestEIP7702.t.sol | 4 - .../unit/concrete/eip7702/TestPREP.sol | 137 ------------------ .../unit/concrete/eip7702/TestPREP.t.sol | 102 +++++++++++++ .../TestModuleManager_EnableMode.t.sol | 2 +- test/foundry/utils/TestHelper.t.sol | 9 ++ 7 files changed, 124 insertions(+), 155 deletions(-) delete mode 100644 test/foundry/unit/concrete/eip7702/TestPREP.sol create mode 100644 test/foundry/unit/concrete/eip7702/TestPREP.t.sol diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 9291fbc6e..30bc63be3 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -593,19 +593,16 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } /// @dev Checks if the account is an ERC7702 account - function _amIERC7702() internal view returns (bool) { - bytes32 c; + function _amIERC7702() internal view returns (bool res) { 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) + extcodecopy(address(), 0, 0, 3) + res := eq(0xef01, shr(240, mload(0x00))) } // 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 diff --git a/contracts/mocks/MockValidator.sol b/contracts/mocks/MockValidator.sol index c9718ff51..5f1fd2973 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -6,11 +6,13 @@ import { IModuleManager } from "../interfaces/base/IModuleManager.sol"; import { VALIDATION_SUCCESS, VALIDATION_FAILED, MODULE_TYPE_VALIDATOR, ERC1271_MAGICVALUE, ERC1271_INVALID } from "../types/Constants.sol"; import { PackedUserOperation } from "account-abstraction/interfaces/PackedUserOperation.sol"; import { ECDSA } from "solady/utils/ECDSA.sol"; -import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol"; import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { ERC7739Validator } from "erc7739Validator/ERC7739Validator.sol"; contract MockValidator is ERC7739Validator { + + using ECDSA for bytes32; + mapping(address => address) public smartAccountOwners; function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external view returns (uint256 validation) { @@ -30,15 +32,15 @@ contract MockValidator is ERC7739Validator { } function _validateSignatureForOwner(address owner, bytes32 hash, bytes calldata signature) internal view returns (bool) { - if (SignatureCheckerLib.isValidSignatureNowCalldata(owner, hash, signature)) { - return true; - } - if (SignatureCheckerLib.isValidSignatureNowCalldata(owner, MessageHashUtils.toEthSignedMessageHash(hash), signature)) { - return true; - } + if (_recoverSigner(hash, signature) == owner) return true; + if (_recoverSigner(hash.toEthSignedMessageHash(), signature) == owner) return true; return false; } + function _recoverSigner(bytes32 hash, bytes calldata signature) internal view returns (address) { + return hash.tryRecoverCalldata(signature); + } + /// @dev Returns whether the `hash` and `signature` are valid. /// Obtains the authorized signer's credentials and calls some /// module's specific internal function to validate the signature diff --git a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol index fbb2f7df0..bb4d9913b 100644 --- a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol +++ b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol @@ -25,10 +25,6 @@ contract TestEIP7702 is NexusTest_Base { 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), ""); diff --git a/test/foundry/unit/concrete/eip7702/TestPREP.sol b/test/foundry/unit/concrete/eip7702/TestPREP.sol deleted file mode 100644 index 586dac314..000000000 --- a/test/foundry/unit/concrete/eip7702/TestPREP.sol +++ /dev/null @@ -1,137 +0,0 @@ -// 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 { LibRLP } from "solady/utils/LibRLP.sol"; -import { EfficientHashLib } from "solady/utils/EfficientHashLib.sol"; - -contract TestPREP is NexusTest_Base { - - uint8 constant MAGIC = 0x05; - - using ECDSA for bytes32; - using LibRLP for *; - - MockTarget target; - MockValidator public mockValidator; - MockExecutor public mockExecutor; - - function setUp() public { - setupTestEnvironment(); - 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 _getUserOpSignature(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_PREP_Initialization_Success() public { - - /* - struct SignedDelegation { - // The y-parity of the recovered secp256k1 signature (0 or 1). - uint8 v; - // First 32 bytes of the signature. - bytes32 r; - // Second 32 bytes of the signature. - bytes32 s; - // The current nonce of the authority account at signing time. - // Used to ensure signature can't be replayed after account nonce changes. - uint64 nonce; - // Address of the contract implementation that will be delegated to. - // Gets encoded into delegation code: 0xef0100 || implementation. - address implementation; -} -*/ - bytes memory initData = _getInitData(); - bytes32 initDataHash = keccak256(abi.encodePacked(initData)); - - uint256 eoaKey = uint256(1010101010101); - Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(ACCOUNT_IMPLEMENTATION), eoaKey); - - // ================================ - - bytes memory rlpAuth = abi.encodePacked(hex"05", LibRLP.p(uint256(0x7a69)).p(signedDelegation.implementation).p(signedDelegation.nonce).encode()); - bytes32 authHash = keccak256(rlpAuth); - - - //signedDelegation.s = bytes32(uint256(0x0000000000000000000000000000000000000000ffffffffffffffffffffffff)) & keccak256(abi.encodePacked(block.timestamp)); - - uint256 i=2**12; - address prep; - console2.log(signedDelegation.v); - - while (prep == address(0)) { - signedDelegation.r = EfficientHashLib.hash(uint256(initDataHash), i) & bytes32(uint256(2 ** 160 - 1)); - //signedDelegation.s = keccak256(abi.encodePacked(initDataHash, i)); - signedDelegation.s = keccak256(abi.encodePacked(signedDelegation.r)); - prep = authHash.tryRecover(signedDelegation.v+27, signedDelegation.r, signedDelegation.s); - i++; - console2.log(i); - console2.log(prep); - } - - console2.log(prep); - - vm.attachDelegation(signedDelegation); - - - bytes32 prepCode; - assembly { - extcodecopy(prep, prepCode, 0, 23) - } - console2.logBytes32(prepCode); - } - - function test_Auth_Hash_generation() public { - uint256 eoaKey = uint256(1010101010101); - - Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(ACCOUNT_IMPLEMENTATION), eoaKey); - - bytes memory auth = abi.encodePacked( - MAGIC, - abi.encode( - uint256(0x7a69), - signedDelegation.implementation, - signedDelegation.nonce - ) - ); - - console2.logBytes(auth); - - bytes memory rlpAuth = abi.encodePacked(hex"05", LibRLP.p(uint256(0x7a69)).p(signedDelegation.implementation).p(signedDelegation.nonce).encode()); - - console2.logBytes(rlpAuth); - - bytes32 authHash = keccak256(rlpAuth); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(eoaKey, authHash); - - console2.log("v"); - console2.log(v); - - assertEq(r, signedDelegation.r); - assertEq(s, signedDelegation.s); - - // assertEq(v, signedDelegation.v); - } -} diff --git a/test/foundry/unit/concrete/eip7702/TestPREP.t.sol b/test/foundry/unit/concrete/eip7702/TestPREP.t.sol new file mode 100644 index 000000000..4b6add401 --- /dev/null +++ b/test/foundry/unit/concrete/eip7702/TestPREP.t.sol @@ -0,0 +1,102 @@ +// 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 { LibRLP } from "solady/utils/LibRLP.sol"; +import { EfficientHashLib } from "solady/utils/EfficientHashLib.sol"; +import { LibPREP } from "lib-prep/LibPREP.sol"; + +contract TestPREP is NexusTest_Base { + + uint8 constant MAGIC = 0x05; + + using ECDSA for bytes32; + using LibRLP for *; + + MockTarget target; + MockValidator public mockValidator; + MockExecutor public mockExecutor; + + function setUp() public { + setupTestEnvironment(); + target = new MockTarget(); + mockValidator = new MockValidator(); + mockExecutor = new MockExecutor(); + } + + 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 _getUserOpSignature(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_PREP_Initialization_Success() public { + + bytes memory initData = _getInitData(); + bytes32 initDataHash = keccak256(abi.encodePacked(initData)); + + (bytes32 saltAndDelegation, address prep) = _mine(initDataHash); + + bytes32 r = LibPREP.rPREP(prep, initDataHash, saltAndDelegation); + + // We can not use vm.attachDelegation by foundry because it + // uses 31337 as chainId in the 7702 auth tuple and it can not be altered. + // as signedDelegation struct doesn't have a chainId field. + // vm.attachDelegation(signedDelegation); + _doEIP7702(prep); + assertEq(LibPREP.isPREP(prep, r), true); + + // Initialize PREP with the first userOp + + + + + } + + function _mine(bytes32 digest) internal returns (bytes32 saltAndDelegation, address prep) { + bytes32 saltRandomnessSeed = bytes32(uint256(0xa11cedecaf)); + + bytes32 h = keccak256(abi.encodePacked(hex"05", LibRLP.p(uint256(0)).p(address(ACCOUNT_IMPLEMENTATION)).p(uint64(0)).encode())); + uint96 salt; + while (true) { + salt = uint96(uint256(saltRandomnessSeed)); + bytes32 r = + EfficientHashLib.hash(uint256(digest), salt) & bytes32(uint256(2 ** 160 - 1)); + bytes32 s = EfficientHashLib.hash(r); + prep = ecrecover(h, 27, r, s); + if (prep != address(0)) break; + saltRandomnessSeed = EfficientHashLib.hash(saltRandomnessSeed); + } + saltAndDelegation = bytes32((uint256(salt) << 160) | uint160(address(ACCOUNT_IMPLEMENTATION))); + } + + function test_Auth_RLP_encoding() public { + uint256 eoaKey = uint256(1010101010101); + + Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(ACCOUNT_IMPLEMENTATION), eoaKey); + + bytes memory rlpAuth = _rlpEncodeAuth(uint256(0x7a69), signedDelegation.implementation, signedDelegation.nonce); + + bytes32 authHash = keccak256(rlpAuth); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(eoaKey, authHash); + + assertEq(r, signedDelegation.r); + assertEq(s, signedDelegation.s); + } + + function _rlpEncodeAuth(uint256 chainId, address implementation, uint64 nonce) internal view returns (bytes memory) { + return abi.encodePacked(hex"05", LibRLP.p(chainId).p(implementation).p(nonce).encode()); + } +} diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol index e0081161f..b12e663a4 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_EnableMode.t.sol @@ -170,7 +170,7 @@ contract TestModuleManager_EnableMode is Test, TestModuleManagement_Base { 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); + _doEIP7702(BOB_ADDRESS); (bytes memory multiInstallData, bytes32 hashToSign, ) = makeInstallDataAndHash(BOB_ADDRESS, MODULE_TYPE_MULTI, userOpHash); diff --git a/test/foundry/utils/TestHelper.t.sol b/test/foundry/utils/TestHelper.t.sol index 9c250e30d..64ac354f3 100644 --- a/test/foundry/utils/TestHelper.t.sol +++ b/test/foundry/utils/TestHelper.t.sol @@ -144,6 +144,15 @@ contract TestHelper is CheatCodes, EventsAndErrors { } } + // etch the 7702 code + function _doEIP7702(address account) internal { + vm.etch(account, abi.encodePacked(hex'ef0100', bytes20(address(ACCOUNT_IMPLEMENTATION)))); + } + + function _doEIP7702_init(address account, address implementation) internal { + vm.etch(account, abi.encodePacked(hex'ef0100', bytes20(implementation))); + } + // ----------------------------------------- // Account Deployment Functions // ----------------------------------------- From 8622e2d7140a7b986cdf257a92dfeef6dc7008fb Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 13:45:27 +0300 Subject: [PATCH 36/56] test PREP --- contracts/Nexus.sol | 1 + .../unit/concrete/eip7702/TestEIP7702.t.sol | 1 - .../unit/concrete/eip7702/TestPREP.t.sol | 40 ++++++++++++++----- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index e3ce49ca8..5c51a72c0 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -489,6 +489,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra mstore(0x0, 0xaed59595) // NotInitializable() revert(0x1c, 0x04) } + saltAndDelegation := calldataload(data.offset) // initData diff --git a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol index bb4d9913b..1ad2a514a 100644 --- a/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol +++ b/test/foundry/unit/concrete/eip7702/TestEIP7702.t.sol @@ -59,7 +59,6 @@ contract TestEIP7702 is NexusTest_Base { // 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); diff --git a/test/foundry/unit/concrete/eip7702/TestPREP.t.sol b/test/foundry/unit/concrete/eip7702/TestPREP.t.sol index 4b6add401..268a12302 100644 --- a/test/foundry/unit/concrete/eip7702/TestPREP.t.sol +++ b/test/foundry/unit/concrete/eip7702/TestPREP.t.sol @@ -7,9 +7,12 @@ import { MockTarget } from "contracts/mocks/MockTarget.sol"; import { LibRLP } from "solady/utils/LibRLP.sol"; import { EfficientHashLib } from "solady/utils/EfficientHashLib.sol"; import { LibPREP } from "lib-prep/LibPREP.sol"; +import { IExecutionHelper } from "contracts/interfaces/base/IExecutionHelper.sol"; contract TestPREP is NexusTest_Base { + event PREPInitialized(bytes32 r); + uint8 constant MAGIC = 0x05; using ECDSA for bytes32; @@ -28,7 +31,7 @@ contract TestPREP is NexusTest_Base { function _getInitData() internal view returns (bytes memory) { // Create config for initial modules - BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(mockValidator), ""); + BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(mockValidator), abi.encodePacked(BOB_ADDRESS)); // set BOB as signer in the validator BootstrapConfig[] memory executors = BootstrapLib.createArrayConfig(address(mockExecutor), ""); BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); BootstrapConfig[] memory fallbacks = BootstrapLib.createArrayConfig(address(0), ""); @@ -36,18 +39,15 @@ contract TestPREP is NexusTest_Base { return BOOTSTRAPPER.getInitNexusCalldata(validators, executors, hook, fallbacks, REGISTRY, ATTESTERS, THRESHOLD); } - function _getUserOpSignature(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_PREP_Initialization_Success() public { + function test_PREP_Initialization_Success(uint256 valueToSet) public { + valueToSet = bound(valueToSet, 0, 77e18); + uint256 valueToSet = 1337; + bytes memory setValueOnTarget = abi.encodeCall(MockTarget.setValue, valueToSet); bytes memory initData = _getInitData(); bytes32 initDataHash = keccak256(abi.encodePacked(initData)); - (bytes32 saltAndDelegation, address prep) = _mine(initDataHash); + (bytes32 saltAndDelegation, address prep) = _mine(initDataHash, valueToSet); bytes32 r = LibPREP.rPREP(prep, initDataHash, saltAndDelegation); @@ -57,16 +57,34 @@ contract TestPREP is NexusTest_Base { // vm.attachDelegation(signedDelegation); _doEIP7702(prep); assertEq(LibPREP.isPREP(prep, r), true); + vm.deal(prep, 100 ether); // Initialize PREP with the first userOp + uint256 nonce = getNonce(prep, MODE_PREP, address(mockValidator), 0); + // Create the userOp and add the data + PackedUserOperation memory userOp = buildPackedUserOp(address(prep), nonce); + userOp.callData = abi.encodeCall(IExecutionHelper.execute, (ModeLib.encodeSimpleSingle(), ExecLib.encodeSingle(address(target), uint256(0), setValueOnTarget))); + userOp.signature = signUserOp(BOB, userOp); + // add prep data to signature + userOp.signature = abi.encode(saltAndDelegation, initData, userOp.signature); + + // Create userOps array + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = userOp; + vm.expectEmit(address(prep)); + emit PREPInitialized(r); + ENTRYPOINT.handleOps(userOps, payable(address(0x69))); + + // Assert that the value was set ie that execution was successful + assertTrue(target.value() == valueToSet); } - function _mine(bytes32 digest) internal returns (bytes32 saltAndDelegation, address prep) { - bytes32 saltRandomnessSeed = bytes32(uint256(0xa11cedecaf)); + function _mine(bytes32 digest, uint256 randomnessSalt) internal returns (bytes32 saltAndDelegation, address prep) { + bytes32 saltRandomnessSeed = EfficientHashLib.hash(uint256(0xa11cedecaf), randomnessSalt); bytes32 h = keccak256(abi.encodePacked(hex"05", LibRLP.p(uint256(0)).p(address(ACCOUNT_IMPLEMENTATION)).p(uint64(0)).encode())); uint96 salt; From 2aa83426c69e06146a73470bac47324ef713f065 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 14:00:00 +0300 Subject: [PATCH 37/56] deps --- package.json | 4 ++-- remappings.txt | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 3130bd859..be1c5d79c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/chai": "^4.3.16", "@types/mocha": ">=10.0.6", "@types/node": ">=20.12.12", + "account": "ithacaxyz/account#vectorized/prep", "account-abstraction": "https://github.com/eth-infinitism/account-abstraction#v0.7.0", "chai": "^4.3.7", "codecov": "^3.8.3", @@ -42,8 +43,7 @@ "solidity-coverage": "^0.8.12", "ts-node": ">=10.9.2", "typechain": "^8.3.2", - "typescript": ">=5.4.5", - "lib-prep": "ithacaxyz/account#vectorized/prep" + "typescript": ">=5.4.5" }, "keywords": [ "nexus", diff --git a/remappings.txt b/remappings.txt index 1d5b9b5ff..6f0df79e2 100644 --- a/remappings.txt +++ b/remappings.txt @@ -6,4 +6,4 @@ excessively-safe-call/=node_modules/excessively-safe-call/src/ sentinellist/=node_modules/sentinellist/src/ solarray/=node_modules/solarray/src/ erc7739Validator/=node_modules/erc7739-validator-base/src/ -lib-prep/=node_modules/lib-prep/src/ \ No newline at end of file +lib-prep/=node_modules/account/src/ \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8703908d8..55c2042d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1307,6 +1307,10 @@ abbrev@1.0.x: table "^6.8.0" typescript "^4.3.5" +account@ithacaxyz/account#vectorized/prep: + version "0.0.0" + resolved "https://codeload.github.com/ithacaxyz/account/tar.gz/733464120b83c7699ba5a3ccf0b56c9ae6dea001" + acorn-walk@^8.1.1: version "8.3.4" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" @@ -3333,10 +3337,6 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lib-prep@ithacaxyz/account#vectorized/prep: - version "0.0.0" - resolved "https://codeload.github.com/ithacaxyz/account/tar.gz/733464120b83c7699ba5a3ccf0b56c9ae6dea001" - lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" From 6cdb51d47ce29a12224dc0da755a8de58182e186 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 14:11:05 +0300 Subject: [PATCH 38/56] clean package --- GAS_OPTIMIZATION.md | 70 --------------------------------------------- GAS_REPORT.md | 59 -------------------------------------- package.json | 18 +++--------- yarn.lock | 2 +- 4 files changed, 5 insertions(+), 144 deletions(-) delete mode 100644 GAS_OPTIMIZATION.md delete mode 100644 GAS_REPORT.md diff --git a/GAS_OPTIMIZATION.md b/GAS_OPTIMIZATION.md deleted file mode 100644 index 7575f163b..000000000 --- a/GAS_OPTIMIZATION.md +++ /dev/null @@ -1,70 +0,0 @@ -# **Biconomy Smart Contract Optimization Bounty Program** - -## **Introduction** - -Biconomy is dedicated to enhancing the efficiency and sustainability of our decentralized ecosystem. Through our **Smart Contract Optimization Bounty Program**, we invite global developers to contribute towards optimizing gas efficiency across our suite of smart contracts. This initiative is key to reducing operational costs, improving scalability, and elevating the user experience within the blockchain ecosystem. - -For details on reporting security vulnerabilities or contributing to our project in other ways, please see our [SECURITY.md](./SECURITY.md) and [CONTRIBUTING.md](./CONTRIBUTING.md) files. We value all contributions that enhance the Biconomy ecosystem, including but not limited to code optimizations and security improvements. - -## **Program Objective** - -Our goal is to maximize gas efficiency in our smart contracts while ensuring the code remains **readable, maintainable, and thoroughly documented**.We value contributions that find the perfect harmony between optimization and clarity, as both are critical for the enduring success of blockchain projects. - -## **Rewards Structure** - -Contributors to the Biconomy Smart Contract Optimization Bounty Program will be rewarded for their efforts with **BICO tokens (ERC20)**, based on the cumulative gas savings achieved across all contracts. Furthermore, in addition to the bounty rewards, successful contributors will gain a spot in our **Contributor Hall of Fame**, where names or GitHub handles are honored within our repository and documentation, showcasing the impactful contributions made to the Biconomy ecosystem. - -### **Reward Tiers** - -- **Tier 1 (up to 10% Cumulative Gas Savings):** Receive $150 in BICO tokens for optimizing up to 10%. -- **Tier 2 (11-25% Cumulative Gas Savings):** Earn $600 in BICO tokens for achieving 11-25% savings. -- **Tier 3 (26-40% Cumulative Gas Savings):** Secure $2,000 in BICO tokens for 26-40% cumulative savings. -- **Tier 4 (41-55% Cumulative Gas Savings):** Gain $4,000 in BICO tokens for optimizing 41-55%. -- **Tier 5 (over 55% Cumulative Gas Savings):** Be rewarded with $7,500 in BICO tokens for surpassing 55% savings. - -## **Submission Guidelines** - -### **Identifying Opportunities** - -- Explore our smart contracts for potential gas optimization areas. - -### **Implementing Changes** - -- **Fork the `develop` branch** of the Biconomy repository. This ensures your updates are built on the latest features and fixes, preventing overlap with existing main branch plans. -- Maintain code clarity and documentation through your optimizations. - -### **Testing and Validation** - -- Validate your optimizations with thorough testing, ensuring both gas efficiency improvement and preserved functionality. -- Attach detailed test results with your submission. - -### **Submitting Your Work** - -- Open a pull request (PR) from your forked **develop branch**, detailing your changes, the rationale behind each optimization, and its gas usage impact. -- Highlight the cumulative gas savings achieved across all functions and contracts. - -## **Evaluation Criteria** - -Submissions will be evaluated on: - -- **Efficiency Improvement:** The cumulative percentage of gas savings. -- **Code Quality:** Enhancements should bolster clarity, maintainability, and documentation. -- **Innovation:** Creative solutions to optimization challenges. -- **Impact:** The significant contribution to the project's efficiency and sustainability. - -## **Eligibility Criteria** - -- Originality is a must, with no infringement on third-party rights. -- Include comprehensive documentation of optimizations and their impacts. -- Demonstrated effectiveness of optimizations with accompanying tests. -- Adherence to smart contract development and security best practices is required. - -## **Terms and Conditions** - -- Rewards, payable in BICO tokens, will be based on their USD value at distribution. -- Biconomy reserves the right to alter the program's terms or discontinue at its discretion. -- Reward tiers will be determined by the Biconomy team based on the evaluation criteria. - -This program is a unique opportunity for developers to showcase their expertise, contribute to a more efficient ecosystem, and earn rewards for their innovative solutions. We eagerly anticipate your contributions and the enhancements your optimizations will bring to our smart contracts. - -**We look forward to your innovative contributions and collectively advancing the efficiency of decentralized applications!** diff --git a/GAS_REPORT.md b/GAS_REPORT.md deleted file mode 100644 index 51b64ec65..000000000 --- a/GAS_REPORT.md +++ /dev/null @@ -1,59 +0,0 @@ -# Gas Report Comparison - -| **Protocol** | **Actions / Function** | **Account Type** | **Is Deployed** | **With Paymaster?** | **Receiver Access** | **Gas Used** | **Gas Difference** | -| :----------: | :------------------------------: | :--------------: | :-------------: | :-----------------: | :-----------------: | :----------: | :----------------: | -| ERC20 | transfer | EOA | False | False | 🧊 ColdAccess | 49833 | 🥵 +459 | -| ERC20 | transfer | EOA | False | False | 🔥 WarmAccess | 25133 | 🥵 +459 | -| ERC20 | transfer | Smart Account | True | False | 🧊 ColdAccess | 98023 | 🥵 +6160 | -| ERC20 | transfer | Smart Account | True | False | 🔥 WarmAccess | 78124 | 🥵 +6161 | -| ERC20 | transfer | Smart Account | False | True | 🧊 ColdAccess | 372899 | 🥵 +12346 | -| ERC20 | transfer | Smart Account | False | True | 🔥 WarmAccess | 353000 | 🥵 +12347 | -| ERC20 | transfer | Smart Account | False | False | 🧊 ColdAccess | 356379 | 🥵 +11060 | -| ERC20 | transfer | Smart Account | False | False | 🔥 WarmAccess | 336480 | 🥵 +11062 | -| ERC20 | transfer | Smart Account | False | False | 🧊 ColdAccess | 404502 | 🥵 +11157 | -| ERC20 | transfer | Smart Account | False | False | 🔥 WarmAccess | 384603 | 🥵 +11158 | -| ERC20 | transfer | Smart Account | True | True | 🧊 ColdAccess | 114176 | 🥵 +7517 | -| ERC20 | transfer | Smart Account | True | True | 🔥 WarmAccess | 94276 | 🥵 +7518 | -| ERC721 | transferFrom | EOA | False | False | 🧊 ColdAccess | 48409 | 🥵 +824 | -| ERC721 | transferFrom | EOA | False | False | 🔥 WarmAccess | 28509 | 🥵 +824 | -| ERC721 | transferFrom | Smart Account | True | False | 🧊 ColdAccess | 101486 | 🥵 +6296 | -| ERC721 | transferFrom | Smart Account | True | False | 🔥 WarmAccess | 81586 | 🥵 +6296 | -| ERC721 | transferFrom | Smart Account | False | True | 🧊 ColdAccess | 371590 | 🥵 +12514 | -| ERC721 | transferFrom | Smart Account | False | True | 🔥 WarmAccess | 351690 | 🥵 +12514 | -| ERC721 | transferFrom | Smart Account | False | False | 🧊 ColdAccess | 355085 | 🥵 +11233 | -| ERC721 | transferFrom | Smart Account | False | False | 🔥 WarmAccess | 335185 | 🥵 +11233 | -| ERC721 | transferFrom | Smart Account | False | False | 🧊 ColdAccess | 403209 | 🥵 +11330 | -| ERC721 | transferFrom | Smart Account | False | False | 🔥 WarmAccess | 383309 | 🥵 +11330 | -| ERC721 | transferFrom | Smart Account | True | True | 🧊 ColdAccess | 117692 | 🥵 +7697 | -| ERC721 | transferFrom | Smart Account | True | True | 🔥 WarmAccess | 97792 | 🥵 +7697 | -| ETH | transfer | EOA | False | False | 🧊 ColdAccess | 53062 | 🥵 +200 | -| ETH | transfer | EOA | False | False | 🔥 WarmAccess | 28062 | 🥵 +200 | -| ETH | call | EOA | False | False | 🧊 ColdAccess | 53129 | 🥵 +203 | -| ETH | call | EOA | False | False | 🔥 WarmAccess | 28129 | 🥵 +203 | -| ETH | send | EOA | False | False | 🧊 ColdAccess | 53129 | 🥵 +203 | -| ETH | send | EOA | False | False | 🔥 WarmAccess | 28129 | 🥵 +203 | -| ETH | transfer | Smart Account | True | False | 🧊 ColdAccess | 105888 | 🥵 +5930 | -| ETH | transfer | Smart Account | True | False | 🔥 WarmAccess | 80888 | 🥵 +5930 | -| ETH | transfer | Smart Account | False | True | 🧊 ColdAccess | 375904 | 🥵 +12102 | -| ETH | transfer | Smart Account | False | True | 🔥 WarmAccess | 350904 | 🥵 +12102 | -| ETH | transfer | Smart Account | False | False | 🧊 ColdAccess | 359455 | 🥵 +10830 | -| ETH | transfer | Smart Account | False | False | 🔥 WarmAccess | 334455 | 🥵 +10830 | -| ETH | transfer | Smart Account | False | False | 🧊 ColdAccess | 407578 | 🥵 +10927 | -| ETH | transfer | Smart Account | False | False | 🔥 WarmAccess | 382578 | 🥵 +10927 | -| ETH | transfer | Smart Account | True | True | 🧊 ColdAccess | 122015 | 🥵 +7316 | -| ETH | transfer | Smart Account | True | True | 🔥 WarmAccess | 97015 | 🥵 +7316 | -| UniswapV2 | swapExactETHForTokens | EOA | False | False | N/A | 148742 | 🥵 +123 | -| UniswapV2 | swapExactETHForTokens | Smart Account | True | False | N/A | 202065 | 🥵 +5508 | -| UniswapV2 | swapExactETHForTokens | Smart Account | False | True | N/A | 472232 | 🥵 +11665 | -| UniswapV2 | swapExactETHForTokens | Smart Account | False | False | N/A | 455654 | 🥵 +10396 | -| UniswapV2 | swapExactETHForTokens | Smart Account | False | False | N/A | 503778 | 🥵 +10493 | -| UniswapV2 | swapExactETHForTokens | Smart Account | True | True | N/A | 218298 | 🥵 +6836 | -| UniswapV2 | swapExactTokensForTokens | EOA | False | False | N/A | 117667 | 🥵 +104 | -| UniswapV2 | swapExactTokensForTokens | Smart Account | True | False | N/A | 170980 | 🥵 +5434 | -| UniswapV2 | swapExactTokensForTokens | Smart Account | False | True | N/A | 441161 | 🥵 +11625 | -| UniswapV2 | swapExactTokensForTokens | Smart Account | False | False | N/A | 424556 | 🥵 +10333 | -| UniswapV2 | approve+swapExactTokensForTokens | Smart Account | True | False | N/A | 202090 | 🥵 +3979 | -| UniswapV2 | approve+swapExactTokensForTokens | Smart Account | False | True | N/A | 472458 | 🥵 +10099 | -| UniswapV2 | approve+swapExactTokensForTokens | Smart Account | False | False | N/A | 455669 | 🥵 +8855 | -| UniswapV2 | approve+swapExactTokensForTokens | Smart Account | False | False | N/A | 503793 | 🥵 +8952 | -| UniswapV2 | swapExactTokensForTokens | Smart Account | True | True | N/A | 187226 | 🥵 +6783 | diff --git a/package.json b/package.json index be1c5d79c..7018d2e78 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "@types/chai": "^4.3.16", "@types/mocha": ">=10.0.6", "@types/node": ">=20.12.12", - "account": "ithacaxyz/account#vectorized/prep", "account-abstraction": "https://github.com/eth-infinitism/account-abstraction#v0.7.0", "chai": "^4.3.7", "codecov": "^3.8.3", @@ -52,20 +51,11 @@ "modular smart account", "smart account", "account abstraction", + "smart contract wallet", "erc4337", - "erc7579", - "erc-7579", - "modular smart account", - "smart account", - "account abstraction", - "erc4337", - "blockchain", - "ethereum", - "forge", - "foundry", - "hardhat", - "smart-contracts", - "solidity" + "eip7702", + "eip-7702", + "PREP" ], "private": true, "scripts": { diff --git a/yarn.lock b/yarn.lock index 55c2042d0..f6d3960a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1307,7 +1307,7 @@ abbrev@1.0.x: table "^6.8.0" typescript "^4.3.5" -account@ithacaxyz/account#vectorized/prep: +"account@github:ithacaxyz/account#vectorized/prep": version "0.0.0" resolved "https://codeload.github.com/ithacaxyz/account/tar.gz/733464120b83c7699ba5a3ccf0b56c9ae6dea001" From bfa0d366a184bf38fc8378055f4d218dd17fe88d Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 14:43:35 +0300 Subject: [PATCH 39/56] package fix --- package.json | 1 + remappings.txt | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7018d2e78..3970e381f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "hardhat-gas-reporter": "^1.0.10", "hardhat-storage-layout": "^0.1.7", "husky": "^9.0.11", + "prep": "github:ithacaxyz/account#vectorized/prep", "prettier": "^3.2.5", "prettier-plugin-solidity": "^1.3.1", "sentinellist": "github:rhinestonewtf/sentinellist#v1.0.0", diff --git a/remappings.txt b/remappings.txt index 6f0df79e2..54dacced8 100644 --- a/remappings.txt +++ b/remappings.txt @@ -6,4 +6,4 @@ excessively-safe-call/=node_modules/excessively-safe-call/src/ sentinellist/=node_modules/sentinellist/src/ solarray/=node_modules/solarray/src/ erc7739Validator/=node_modules/erc7739-validator-base/src/ -lib-prep/=node_modules/account/src/ \ No newline at end of file +lib-prep/=node_modules/prep/src/ \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f6d3960a3..4b08441e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1307,10 +1307,6 @@ abbrev@1.0.x: table "^6.8.0" typescript "^4.3.5" -"account@github:ithacaxyz/account#vectorized/prep": - version "0.0.0" - resolved "https://codeload.github.com/ithacaxyz/account/tar.gz/733464120b83c7699ba5a3ccf0b56c9ae6dea001" - acorn-walk@^8.1.1: version "8.3.4" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" @@ -3827,6 +3823,10 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== +"prep@github:ithacaxyz/account#vectorized/prep": + version "0.0.0" + resolved "https://codeload.github.com/ithacaxyz/account/tar.gz/733464120b83c7699ba5a3ccf0b56c9ae6dea001" + prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" From d9429c1843fd589a77bb940abb51384db5ff088e Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 15:12:37 +0300 Subject: [PATCH 40/56] rfctr --- contracts/Nexus.sol | 12 ++++++------ test/foundry/unit/concrete/eip7702/TestPREP.t.sol | 7 ++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 5c51a72c0..a4aeedfab 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -477,13 +477,13 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra } } - /** - * authHash: 32 bytes - * initData: bytes array - * cleaned 4337 Nexus signature: bytes array - */ + /// @dev Handles the PREP initialization. + /// @param data The packed data to be handled. + /// @return cleanedSignature The cleaned signature for Nexus 4337 (validateUserOp) flow. + /// @return initData The data to initialize the account with. function _handlePREP(bytes calldata data) internal returns (bytes calldata cleanedSignature, bytes calldata initData) { bytes32 saltAndDelegation; + // unpack the data assembly { if lt(data.length, 0x61) { mstore(0x0, 0xaed59595) // NotInitializable() @@ -505,7 +505,7 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra cleanedSignature.length := calldataload(u) } - // check that signature (r value) is based on the hash of the initData provided + // check r is valid bytes32 r = LibPREP.rPREP(address(this), keccak256(initData), saltAndDelegation); if (r == bytes32(0)) { revert InvalidPREP(); diff --git a/test/foundry/unit/concrete/eip7702/TestPREP.t.sol b/test/foundry/unit/concrete/eip7702/TestPREP.t.sol index 268a12302..50f8d1455 100644 --- a/test/foundry/unit/concrete/eip7702/TestPREP.t.sol +++ b/test/foundry/unit/concrete/eip7702/TestPREP.t.sol @@ -12,9 +12,7 @@ import { IExecutionHelper } from "contracts/interfaces/base/IExecutionHelper.sol contract TestPREP is NexusTest_Base { event PREPInitialized(bytes32 r); - - uint8 constant MAGIC = 0x05; - + using ECDSA for bytes32; using LibRLP for *; @@ -85,8 +83,7 @@ contract TestPREP is NexusTest_Base { function _mine(bytes32 digest, uint256 randomnessSalt) internal returns (bytes32 saltAndDelegation, address prep) { bytes32 saltRandomnessSeed = EfficientHashLib.hash(uint256(0xa11cedecaf), randomnessSalt); - - bytes32 h = keccak256(abi.encodePacked(hex"05", LibRLP.p(uint256(0)).p(address(ACCOUNT_IMPLEMENTATION)).p(uint64(0)).encode())); + bytes32 h = EfficientHashLib.hash(_rlpEncodeAuth(uint256(0), address(ACCOUNT_IMPLEMENTATION), 0)); uint96 salt; while (true) { salt = uint96(uint256(saltRandomnessSeed)); From 639bbdfd7ecada36efa91993d06b7404cba42278 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 16:53:52 +0300 Subject: [PATCH 41/56] fix fallback onERC --- contracts/base/ModuleManager.sol | 19 ++++++++++--------- ...exusERC721NFT_Integration_WarmAccess.t.sol | 7 +++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 022058363..5590bc496 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -93,8 +93,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError 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) { - return _fallback(callData); + fallback() external payable withHook { + _fallback(msg.data); } /// @dev Retrieves a paginated list of validator addresses from the linked list. @@ -618,8 +618,9 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } - function _fallback(bytes calldata callData) private returns (bytes memory result) { + function _fallback(bytes calldata callData) private { bool success; + bytes memory result; FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig]; address handler = $fallbackHandler.handler; CallType calltype = $fallbackHandler.calltype; @@ -635,11 +636,13 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } // Use revert message from fallback handler if the call was not successful - if (!success) { - assembly { + assembly { + if iszero(success) { revert(add(result, 0x20), mload(result)) } + return (add(result, 0x20), mload(result)) } + } else { // If there's no handler, the call can be one of onERCXXXReceived() bytes32 s; @@ -651,10 +654,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // 0xbc197c81: `onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)`. if or(eq(s, 0x150b7a02), or(eq(s, 0xf23a6e61), eq(s, 0xbc197c81))) { success := true // it is one of onERCXXXReceived - result := mload(0x40) //result was set to 0x60 as it was empty, so we need to find a new space for it - mstore(result, 0x04) //store length - mstore(add(result, 0x20), shl(224, s)) //store calldata - mstore(0x40, add(result, 0x24)) //allocate memory + mstore(0x20, s) // Store `msg.sig`. + return(0x3c, 0x20) // Return `msg.sig`. } } // if there was no handler and it is not the onERCXXXReceived call, revert diff --git a/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol b/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol index cf828a2d7..c2177aaa1 100644 --- a/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol +++ b/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol @@ -48,8 +48,8 @@ contract TestNexusERC721NFT_Integration_WarmAccess is NexusTest_Base { /// @notice Tests sending ERC721 tokens from an already deployed Nexus smart account with warm access function test_Gas_ERC721NFT_DeployedNexus_Transfer_Warm() public checkERC721NFTBalanceWarm(recipient) { - ERC721NFT.mint(preComputedAddress, tokenId); Nexus deployedNexus = deployNexus(user, 100 ether, address(VALIDATOR_MODULE)); + ERC721NFT.safeMint(preComputedAddress, tokenId); Execution[] memory executions = prepareSingleExecution( address(ERC721NFT), 0, @@ -160,12 +160,11 @@ contract TestNexusERC721NFT_Integration_WarmAccess is NexusTest_Base { checkERC721NFTBalanceWarm(recipient) checkPaymasterBalance(address(paymaster)) { - // Mint the NFT to the precomputed address - ERC721NFT.mint(preComputedAddress, tokenId); - // Deploy the Nexus account Nexus deployedNexus = deployNexus(user, 100 ether, address(VALIDATOR_MODULE)); + // Mint the NFT to the precomputed address + ERC721NFT.safeMint(preComputedAddress, tokenId); // Prepare the execution for ERC721 token transfer Execution[] memory executions = prepareSingleExecution( address(ERC721NFT), From d24a68d8b860b8e410cd6cadcadd350dc3231084 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 17:12:59 +0300 Subject: [PATCH 42/56] proper tests --- contracts/mocks/MockNFT.sol | 13 +++++++ .../TestModuleManager_FallbackHandler.t.sol | 35 +++++++++++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/contracts/mocks/MockNFT.sol b/contracts/mocks/MockNFT.sol index e96a66bc7..ae70ed41c 100644 --- a/contracts/mocks/MockNFT.sol +++ b/contracts/mocks/MockNFT.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; contract MockNFT is ERC721 { constructor(string memory name, string memory symbol) ERC721(name, symbol) {} @@ -18,3 +19,15 @@ contract MockNFT is ERC721 { _safeMint(to, tokenId); } } + +contract MockERC1155 is ERC1155 { + constructor(string memory uri) ERC1155(uri) {} + + function safeMint(address to, uint256 tokenId, uint256 amount) public { + _mint(to, tokenId, amount, ""); + } + + function safeMintBatch(address to, uint256[] memory tokenIds, uint256[] memory amounts) public { + _mintBatch(to, tokenIds, amounts, ""); + } +} diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index f0b52d25f..2c66a8575 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -7,10 +7,16 @@ import "../../../shared/TestModuleManagement_Base.t.sol"; /// @title TestModuleManager_FallbackHandler /// @notice Tests for installing and uninstalling the fallback handler in a smart account. contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { - /// @notice Sets up the base module management environment and installs the fallback handler. + + MockNFT internal erc721; + MockERC1155 internal erc1155; + function setUp() public { init(); + erc721 = new MockNFT("Mock NFT", "MNFT"); + erc1155 = new MockERC1155("Test"); + Execution[] memory execution = new Execution[](2); // Custom data for installing the MockHandler with call type STATIC @@ -290,20 +296,35 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { } function test_onTokenReceived_Success() public { - vm.startPrank(address(ENTRYPOINT)); + + erc721.safeMint(address(BOB_ACCOUNT), 1); + assertEq(erc721.balanceOf(address(BOB_ACCOUNT)), 1); + + erc1155.safeMint(address(BOB_ACCOUNT), 1, 1); + assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 1), 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 2; + tokenIds[1] = 3; + uint256[] memory amounts = new uint256[](2); + amounts[0] = 10; + amounts[1] = 30; + erc1155.safeMintBatch(address(BOB_ACCOUNT), tokenIds, amounts); + + assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 2), 10); + assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 3), 30); + //ERC-721 (bool success, bytes memory data) = address(BOB_ACCOUNT).call{value: 0}(hex'150b7a02'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'150b7a02'))); + assertTrue(keccak256(data) == keccak256((hex'150b7a0200000000000000000000000000000000000000000000000000000000'))); //ERC-1155 (success, data) = address(BOB_ACCOUNT).call{value: 0}(hex'f23a6e61'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'f23a6e61'))); + assertTrue(keccak256(data) == keccak256(bytes(hex'f23a6e6100000000000000000000000000000000000000000000000000000000'))); //ERC-1155 Batch (success, data) = address(BOB_ACCOUNT).call{value: 0}(hex'bc197c81'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'bc197c81'))); - - vm.stopPrank(); + assertTrue(keccak256(data) == keccak256(bytes(hex'bc197c8100000000000000000000000000000000000000000000000000000000'))); } } From 13964fc61fa970ad3d78ca557e132dccefee7243 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 17:45:48 +0300 Subject: [PATCH 43/56] complex return handling by fallback test --- contracts/mocks/MockHandler.sol | 21 +++++++++- .../TestModuleManager_FallbackHandler.t.sol | 39 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/contracts/mocks/MockHandler.sol b/contracts/mocks/MockHandler.sol index dd2ff1467..ccc1d47c5 100644 --- a/contracts/mocks/MockHandler.sol +++ b/contracts/mocks/MockHandler.sol @@ -6,8 +6,8 @@ import { MODULE_TYPE_FALLBACK } from "..//types/Constants.sol"; contract MockHandler is IFallback { uint256 public count; - string public constant NAME = "Default Handler"; - string public constant VERSION = "1.0.0"; + string constant NAME = "Default Handler"; + string constant VERSION = "1.0.0"; event GenericFallbackCalled(address sender, uint256 value, bytes data); // Event for generic fallback event HandlerOnInstallCalled(bytes32 dataFirstWord); @@ -24,6 +24,15 @@ contract MockHandler is IFallback { return this.onGenericFallback.selector; } + function complexReturnData(string memory input, bytes4 selector) external view returns (uint256, bytes memory, address, uint64) { + return ( + uint256(block.timestamp), + abi.encode(input, NAME, VERSION, selector), + address(this), + uint64(block.chainid) + ); + } + function onInstall(bytes calldata data) external override { if (data.length >= 0x20) { emit HandlerOnInstallCalled(bytes32(data[0:32])); @@ -55,4 +64,12 @@ contract MockHandler is IFallback { function getState() external view returns (uint256) { return count; } + + function getName() external pure returns (string memory) { + return NAME; + } + + function getVersion() external pure returns (string memory) { + return VERSION; + } } diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index 2c66a8575..a403d421e 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -296,7 +296,6 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { } function test_onTokenReceived_Success() public { - erc721.safeMint(address(BOB_ACCOUNT), 1); assertEq(erc721.balanceOf(address(BOB_ACCOUNT)), 1); @@ -327,4 +326,42 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { assertTrue(success); assertTrue(keccak256(data) == keccak256(bytes(hex'bc197c8100000000000000000000000000000000000000000000000000000000'))); } + + function test_ComplexReturnData() public { + bytes4 selector = bytes4(keccak256(abi.encodePacked("complexReturnData(string,bytes4)"))); + + bytes memory customData = abi.encode(bytes5(abi.encodePacked(selector, CALLTYPE_SINGLE))); // onInstall selector + bytes memory callData = abi.encodeWithSelector( + IModuleManager.installModule.selector, + MODULE_TYPE_FALLBACK, + address(HANDLER_MODULE), + customData + ); + Execution[] memory execution = new Execution[](1); + execution[0] = Execution(address(BOB_ACCOUNT), 0, callData); + PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); + ENTRYPOINT.handleOps(userOps, payable(address(BOB.addr))); + + // Verify the fallback handler was installed for the given selector + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_FALLBACK, address(HANDLER_MODULE), customData), "Fallback handler not installed"); + + bytes memory testString = "Hello Complex Return Data that is more than 32 bytes"; + bytes4 testSelector = bytes4(abi.encode("foo()")); + + bytes memory data = abi.encodeWithSelector( + MockHandler(address(0)).complexReturnData.selector, + testString, + testSelector + ); + + (bool success, bytes memory result) = address(BOB_ACCOUNT).call{value: 0}(data); + assertTrue(success); + (uint256 timestamp, bytes memory resultData, address addr, uint64 chainId) = abi.decode(result, (uint256, bytes, address, uint64)); + assertEq(timestamp, block.timestamp); + assertEq(addr, address(HANDLER_MODULE)); + assertEq(chainId, block.chainid); + + bytes memory expectedData = abi.encode(testString, HANDLER_MODULE.getName(), HANDLER_MODULE.getVersion(), testSelector); + assertEq(keccak256(resultData), keccak256(expectedData)); + } } From 7df8eaf18c8c804c5073488a96152d73eb543c1e Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 17:53:29 +0300 Subject: [PATCH 44/56] test erc2771 sender handling --- contracts/mocks/MockHandler.sol | 15 +++++++++++++-- .../TestModuleManager_FallbackHandler.t.sol | 6 ++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/contracts/mocks/MockHandler.sol b/contracts/mocks/MockHandler.sol index ccc1d47c5..be209b6d0 100644 --- a/contracts/mocks/MockHandler.sol +++ b/contracts/mocks/MockHandler.sol @@ -24,12 +24,13 @@ contract MockHandler is IFallback { return this.onGenericFallback.selector; } - function complexReturnData(string memory input, bytes4 selector) external view returns (uint256, bytes memory, address, uint64) { + function complexReturnData(string memory input, bytes4 selector) external view returns (uint256, bytes memory, address, uint64, address) { return ( uint256(block.timestamp), abi.encode(input, NAME, VERSION, selector), address(this), - uint64(block.chainid) + uint64(block.chainid), + _msgSender() ); } @@ -72,4 +73,14 @@ contract MockHandler is IFallback { function getVersion() external pure returns (string memory) { return VERSION; } + + function _msgSender() internal pure returns (address sender) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + /* solhint-disable no-inline-assembly */ + /// @solidity memory-safe-assembly + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + /* solhint-enable no-inline-assembly */ + } } diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index a403d421e..745973377 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -354,13 +354,15 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { testSelector ); + address expectedSender = address(0x1234567890123456789012345678901234567890); + vm.prank(expectedSender); (bool success, bytes memory result) = address(BOB_ACCOUNT).call{value: 0}(data); assertTrue(success); - (uint256 timestamp, bytes memory resultData, address addr, uint64 chainId) = abi.decode(result, (uint256, bytes, address, uint64)); + (uint256 timestamp, bytes memory resultData, address addr, uint64 chainId, address sender) = abi.decode(result, (uint256, bytes, address, uint64, address)); assertEq(timestamp, block.timestamp); assertEq(addr, address(HANDLER_MODULE)); assertEq(chainId, block.chainid); - + assertEq(sender, expectedSender); bytes memory expectedData = abi.encode(testString, HANDLER_MODULE.getName(), HANDLER_MODULE.getVersion(), testSelector); assertEq(keccak256(resultData), keccak256(expectedData)); } From 9344ddfdb4d38085eadd9f97acfab7be88963c5a Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 18:28:59 +0300 Subject: [PATCH 45/56] fix fallback --- contracts/base/ModuleManager.sol | 19 ++--- contracts/mocks/MockHandler.sol | 32 +++++++- contracts/mocks/MockNFT.sol | 17 ++++ .../TestModuleManager_FallbackHandler.t.sol | 77 +++++++++++++++++-- 4 files changed, 128 insertions(+), 17 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index a59ca9b38..667a29f4c 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -64,8 +64,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError 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) { - return _fallback(callData); + fallback() external payable withHook { + _fallback(msg.data); } /// @dev Retrieves a paginated list of validator addresses from the linked list. @@ -424,8 +424,9 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError hook = address(_getAccountStorage().hook); } - function _fallback(bytes calldata callData) private returns (bytes memory result) { + function _fallback(bytes calldata callData) private { bool success; + bytes memory result; FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig]; address handler = $fallbackHandler.handler; CallType calltype = $fallbackHandler.calltype; @@ -441,11 +442,13 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } // Use revert message from fallback handler if the call was not successful - if (!success) { - assembly { + assembly { + if iszero(success) { revert(add(result, 0x20), mload(result)) } + return (add(result, 0x20), mload(result)) } + } else { // If there's no handler, the call can be one of onERCXXXReceived() bytes32 s; @@ -457,10 +460,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // 0xbc197c81: `onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)`. if or(eq(s, 0x150b7a02), or(eq(s, 0xf23a6e61), eq(s, 0xbc197c81))) { success := true // it is one of onERCXXXReceived - result := mload(0x40) //result was set to 0x60 as it was empty, so we need to find a new space for it - mstore(result, 0x04) //store length - mstore(add(result, 0x20), shl(224, s)) //store calldata - mstore(0x40, add(result, 0x24)) //allocate memory + mstore(0x20, s) // Store `msg.sig`. + return(0x3c, 0x20) // Return `msg.sig`. } } // if there was no handler and it is not the onERCXXXReceived call, revert diff --git a/contracts/mocks/MockHandler.sol b/contracts/mocks/MockHandler.sol index dd2ff1467..be209b6d0 100644 --- a/contracts/mocks/MockHandler.sol +++ b/contracts/mocks/MockHandler.sol @@ -6,8 +6,8 @@ import { MODULE_TYPE_FALLBACK } from "..//types/Constants.sol"; contract MockHandler is IFallback { uint256 public count; - string public constant NAME = "Default Handler"; - string public constant VERSION = "1.0.0"; + string constant NAME = "Default Handler"; + string constant VERSION = "1.0.0"; event GenericFallbackCalled(address sender, uint256 value, bytes data); // Event for generic fallback event HandlerOnInstallCalled(bytes32 dataFirstWord); @@ -24,6 +24,16 @@ contract MockHandler is IFallback { return this.onGenericFallback.selector; } + function complexReturnData(string memory input, bytes4 selector) external view returns (uint256, bytes memory, address, uint64, address) { + return ( + uint256(block.timestamp), + abi.encode(input, NAME, VERSION, selector), + address(this), + uint64(block.chainid), + _msgSender() + ); + } + function onInstall(bytes calldata data) external override { if (data.length >= 0x20) { emit HandlerOnInstallCalled(bytes32(data[0:32])); @@ -55,4 +65,22 @@ contract MockHandler is IFallback { function getState() external view returns (uint256) { return count; } + + function getName() external pure returns (string memory) { + return NAME; + } + + function getVersion() external pure returns (string memory) { + return VERSION; + } + + function _msgSender() internal pure returns (address sender) { + // The assembly code is more direct than the Solidity version using `abi.decode`. + /* solhint-disable no-inline-assembly */ + /// @solidity memory-safe-assembly + assembly { + sender := shr(96, calldataload(sub(calldatasize(), 20))) + } + /* solhint-enable no-inline-assembly */ + } } diff --git a/contracts/mocks/MockNFT.sol b/contracts/mocks/MockNFT.sol index e96a66bc7..33df10deb 100644 --- a/contracts/mocks/MockNFT.sol +++ b/contracts/mocks/MockNFT.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; contract MockNFT is ERC721 { constructor(string memory name, string memory symbol) ERC721(name, symbol) {} @@ -17,4 +18,20 @@ contract MockNFT is ERC721 { function safeMint(address to, uint256 tokenId) public { _safeMint(to, tokenId); } + + function safeTransfer(address to, uint256 tokenId) public { + _safeTransfer(msg.sender, to, tokenId); + } +} + +contract MockERC1155 is ERC1155 { + constructor(string memory uri) ERC1155(uri) {} + + function safeMint(address to, uint256 tokenId, uint256 amount) public { + _mint(to, tokenId, amount, ""); + } + + function safeMintBatch(address to, uint256[] memory tokenIds, uint256[] memory amounts) public { + _mintBatch(to, tokenIds, amounts, ""); + } } diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index f0b52d25f..4d8988bc0 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -7,10 +7,16 @@ import "../../../shared/TestModuleManagement_Base.t.sol"; /// @title TestModuleManager_FallbackHandler /// @notice Tests for installing and uninstalling the fallback handler in a smart account. contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { - /// @notice Sets up the base module management environment and installs the fallback handler. + + MockNFT internal erc721; + MockERC1155 internal erc1155; + function setUp() public { init(); + erc721 = new MockNFT("Mock NFT", "MNFT"); + erc1155 = new MockERC1155("Test"); + Execution[] memory execution = new Execution[](2); // Custom data for installing the MockHandler with call type STATIC @@ -290,20 +296,79 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { } function test_onTokenReceived_Success() public { - vm.startPrank(address(ENTRYPOINT)); + erc721.safeMint(address(BOB_ACCOUNT), 1); + assertEq(erc721.balanceOf(address(BOB_ACCOUNT)), 1); + + erc721.safeMint(ALICE_ADDRESS, 2); + vm.prank(ALICE_ADDRESS); + erc721.safeTransfer(address(BOB_ACCOUNT), 2); + assertEq(erc721.ownerOf(2), address(BOB_ACCOUNT)); + + erc1155.safeMint(address(BOB_ACCOUNT), 1, 1); + assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 1), 1); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 2; + tokenIds[1] = 3; + uint256[] memory amounts = new uint256[](2); + amounts[0] = 10; + amounts[1] = 30; + erc1155.safeMintBatch(address(BOB_ACCOUNT), tokenIds, amounts); + + assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 2), 10); + assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 3), 30); + //ERC-721 (bool success, bytes memory data) = address(BOB_ACCOUNT).call{value: 0}(hex'150b7a02'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'150b7a02'))); + assertTrue(keccak256(data) == keccak256((hex'150b7a0200000000000000000000000000000000000000000000000000000000'))); //ERC-1155 (success, data) = address(BOB_ACCOUNT).call{value: 0}(hex'f23a6e61'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'f23a6e61'))); + assertTrue(keccak256(data) == keccak256(bytes(hex'f23a6e6100000000000000000000000000000000000000000000000000000000'))); //ERC-1155 Batch (success, data) = address(BOB_ACCOUNT).call{value: 0}(hex'bc197c81'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'bc197c81'))); + assertTrue(keccak256(data) == keccak256(bytes(hex'bc197c8100000000000000000000000000000000000000000000000000000000'))); + } + + function test_ComplexReturnData() public { + bytes4 selector = bytes4(keccak256(abi.encodePacked("complexReturnData(string,bytes4)"))); - vm.stopPrank(); + bytes memory customData = abi.encode(bytes5(abi.encodePacked(selector, CALLTYPE_SINGLE))); // onInstall selector + bytes memory callData = abi.encodeWithSelector( + IModuleManager.installModule.selector, + MODULE_TYPE_FALLBACK, + address(HANDLER_MODULE), + customData + ); + Execution[] memory execution = new Execution[](1); + execution[0] = Execution(address(BOB_ACCOUNT), 0, callData); + PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); + ENTRYPOINT.handleOps(userOps, payable(address(BOB.addr))); + + // Verify the fallback handler was installed for the given selector + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_FALLBACK, address(HANDLER_MODULE), customData), "Fallback handler not installed"); + + bytes memory testString = "Hello Complex Return Data that is more than 32 bytes"; + bytes4 testSelector = bytes4(abi.encode("foo()")); + + bytes memory data = abi.encodeWithSelector( + MockHandler(address(0)).complexReturnData.selector, + testString, + testSelector + ); + + address expectedSender = address(0x1234567890123456789012345678901234567890); + vm.prank(expectedSender); + (bool success, bytes memory result) = address(BOB_ACCOUNT).call{value: 0}(data); + assertTrue(success); + (uint256 timestamp, bytes memory resultData, address addr, uint64 chainId, address sender) = abi.decode(result, (uint256, bytes, address, uint64, address)); + assertEq(timestamp, block.timestamp); + assertEq(addr, address(HANDLER_MODULE)); + assertEq(chainId, block.chainid); + assertEq(sender, expectedSender); + bytes memory expectedData = abi.encode(testString, HANDLER_MODULE.getName(), HANDLER_MODULE.getVersion(), testSelector); + assertEq(keccak256(resultData), keccak256(expectedData)); } } From 07fd84566f572bda8dd460b5cf299a024c0d0be2 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 20:52:05 +0300 Subject: [PATCH 46/56] additional test --- contracts/mocks/MockHandler.sol | 4 ++ .../TestModuleManager_FallbackHandler.t.sol | 46 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/contracts/mocks/MockHandler.sol b/contracts/mocks/MockHandler.sol index be209b6d0..ffa105f04 100644 --- a/contracts/mocks/MockHandler.sol +++ b/contracts/mocks/MockHandler.sol @@ -34,6 +34,10 @@ contract MockHandler is IFallback { ); } + function returnBytes() external view returns (bytes memory) { + return abi.encodePacked(NAME, VERSION); + } + function onInstall(bytes calldata data) external override { if (data.length >= 0x20) { emit HandlerOnInstallCalled(bytes32(data[0:32])); diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index 4d8988bc0..e1953fb16 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -4,6 +4,11 @@ pragma solidity ^0.8.27; import "../../../utils/Imports.sol"; import "../../../shared/TestModuleManagement_Base.t.sol"; +interface IHandler { + function returnBytes() external returns (bytes memory); + function complexReturnData(string memory input, bytes4 selector) external returns (uint256, bytes memory, address, uint64, address); +} + /// @title TestModuleManager_FallbackHandler /// @notice Tests for installing and uninstalling the fallback handler in a smart account. contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { @@ -335,7 +340,7 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { function test_ComplexReturnData() public { bytes4 selector = bytes4(keccak256(abi.encodePacked("complexReturnData(string,bytes4)"))); - bytes memory customData = abi.encode(bytes5(abi.encodePacked(selector, CALLTYPE_SINGLE))); // onInstall selector + bytes memory customData = abi.encode(bytes5(abi.encodePacked(selector, CALLTYPE_SINGLE))); bytes memory callData = abi.encodeWithSelector( IModuleManager.installModule.selector, MODULE_TYPE_FALLBACK, @@ -350,7 +355,7 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { // Verify the fallback handler was installed for the given selector assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_FALLBACK, address(HANDLER_MODULE), customData), "Fallback handler not installed"); - bytes memory testString = "Hello Complex Return Data that is more than 32 bytes"; + string memory testString = "Hello Complex Return Data that is more than 32 bytes"; bytes4 testSelector = bytes4(abi.encode("foo()")); bytes memory data = abi.encodeWithSelector( @@ -370,5 +375,42 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { assertEq(sender, expectedSender); bytes memory expectedData = abi.encode(testString, HANDLER_MODULE.getName(), HANDLER_MODULE.getVersion(), testSelector); assertEq(keccak256(resultData), keccak256(expectedData)); + + vm.prank(expectedSender); + (timestamp, resultData, addr, chainId, sender) = IHandler(address(BOB_ACCOUNT)).complexReturnData(testString, testSelector); + assertEq(timestamp, block.timestamp); + assertEq(addr, address(HANDLER_MODULE)); + assertEq(chainId, block.chainid); + assertEq(sender, expectedSender); + assertEq(keccak256(resultData), keccak256(expectedData)); + } + + function test_ReturnBytes() public { + bytes4 selector = bytes4(keccak256(abi.encodePacked("returnBytes()"))); + bytes memory customData = abi.encode(bytes5(abi.encodePacked(selector, CALLTYPE_SINGLE))); + bytes memory callData = abi.encodeWithSelector( + IModuleManager.installModule.selector, + MODULE_TYPE_FALLBACK, + address(HANDLER_MODULE), + customData + ); + Execution[] memory execution = new Execution[](1); + execution[0] = Execution(address(BOB_ACCOUNT), 0, callData); + PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); + ENTRYPOINT.handleOps(userOps, payable(address(BOB.addr))); + + // Verify the fallback handler was installed for the given selector + assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_FALLBACK, address(HANDLER_MODULE), customData), "Fallback handler not installed"); + + bytes memory data = abi.encodeWithSelector( + MockHandler(address(0)).returnBytes.selector + ); + (bool success, bytes memory result) = address(BOB_ACCOUNT).call{value: 0}(data); + bytes memory expectedResult = abi.encodePacked(HANDLER_MODULE.getName(), HANDLER_MODULE.getVersion()); + assertTrue(success); + assertEq(abi.decode(result, (bytes)), expectedResult); + + bytes memory result2 = IHandler(address(BOB_ACCOUNT)).returnBytes(); + assertEq(result2, expectedResult); } } From e9cbc56dbec6b662248be52ed9e26e881f9a5665 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 22:17:39 +0300 Subject: [PATCH 47/56] Introducing those changes in a separate PR Revert "Merge pull request #247 from bcnmy/fix/onERCxxxReceive-hotfix" This reverts commit 77cd94042ef9fb01854ca4ff27aeee3cfb1d71cf, reversing changes made to bbda8f9a033ab9abc57b8e4c8239f3aba5730891. --- contracts/base/ModuleManager.sol | 19 +++-- contracts/mocks/MockHandler.sol | 32 +-------- contracts/mocks/MockNFT.sol | 13 ---- ...exusERC721NFT_Integration_WarmAccess.t.sol | 7 +- .../TestModuleManager_FallbackHandler.t.sol | 72 ++----------------- 5 files changed, 21 insertions(+), 122 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 5702ba0b7..30bc63be3 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -93,8 +93,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError receive() external payable { } /// @dev Fallback function to manage incoming calls using designated handlers based on the call type. - fallback() external payable withHook { - _fallback(msg.data); + fallback(bytes calldata callData) external payable withHook returns (bytes memory) { + return _fallback(callData); } /// @dev Retrieves a paginated list of validator addresses from the linked list. @@ -615,9 +615,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } } - function _fallback(bytes calldata callData) private { + function _fallback(bytes calldata callData) private returns (bytes memory result) { bool success; - bytes memory result; FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig]; address handler = $fallbackHandler.handler; CallType calltype = $fallbackHandler.calltype; @@ -633,13 +632,11 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } // Use revert message from fallback handler if the call was not successful - assembly { - if iszero(success) { + if (!success) { + assembly { revert(add(result, 0x20), mload(result)) } - return (add(result, 0x20), mload(result)) } - } else { // If there's no handler, the call can be one of onERCXXXReceived() bytes32 s; @@ -651,8 +648,10 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError // 0xbc197c81: `onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)`. if or(eq(s, 0x150b7a02), or(eq(s, 0xf23a6e61), eq(s, 0xbc197c81))) { success := true // it is one of onERCXXXReceived - mstore(0x20, s) // Store `msg.sig`. - return(0x3c, 0x20) // Return `msg.sig`. + result := mload(0x40) //result was set to 0x60 as it was empty, so we need to find a new space for it + mstore(result, 0x04) //store length + mstore(add(result, 0x20), shl(224, s)) //store calldata + mstore(0x40, add(result, 0x24)) //allocate memory } } // if there was no handler and it is not the onERCXXXReceived call, revert diff --git a/contracts/mocks/MockHandler.sol b/contracts/mocks/MockHandler.sol index be209b6d0..dd2ff1467 100644 --- a/contracts/mocks/MockHandler.sol +++ b/contracts/mocks/MockHandler.sol @@ -6,8 +6,8 @@ import { MODULE_TYPE_FALLBACK } from "..//types/Constants.sol"; contract MockHandler is IFallback { uint256 public count; - string constant NAME = "Default Handler"; - string constant VERSION = "1.0.0"; + string public constant NAME = "Default Handler"; + string public constant VERSION = "1.0.0"; event GenericFallbackCalled(address sender, uint256 value, bytes data); // Event for generic fallback event HandlerOnInstallCalled(bytes32 dataFirstWord); @@ -24,16 +24,6 @@ contract MockHandler is IFallback { return this.onGenericFallback.selector; } - function complexReturnData(string memory input, bytes4 selector) external view returns (uint256, bytes memory, address, uint64, address) { - return ( - uint256(block.timestamp), - abi.encode(input, NAME, VERSION, selector), - address(this), - uint64(block.chainid), - _msgSender() - ); - } - function onInstall(bytes calldata data) external override { if (data.length >= 0x20) { emit HandlerOnInstallCalled(bytes32(data[0:32])); @@ -65,22 +55,4 @@ contract MockHandler is IFallback { function getState() external view returns (uint256) { return count; } - - function getName() external pure returns (string memory) { - return NAME; - } - - function getVersion() external pure returns (string memory) { - return VERSION; - } - - function _msgSender() internal pure returns (address sender) { - // The assembly code is more direct than the Solidity version using `abi.decode`. - /* solhint-disable no-inline-assembly */ - /// @solidity memory-safe-assembly - assembly { - sender := shr(96, calldataload(sub(calldatasize(), 20))) - } - /* solhint-enable no-inline-assembly */ - } } diff --git a/contracts/mocks/MockNFT.sol b/contracts/mocks/MockNFT.sol index ae70ed41c..e96a66bc7 100644 --- a/contracts/mocks/MockNFT.sol +++ b/contracts/mocks/MockNFT.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; contract MockNFT is ERC721 { constructor(string memory name, string memory symbol) ERC721(name, symbol) {} @@ -19,15 +18,3 @@ contract MockNFT is ERC721 { _safeMint(to, tokenId); } } - -contract MockERC1155 is ERC1155 { - constructor(string memory uri) ERC1155(uri) {} - - function safeMint(address to, uint256 tokenId, uint256 amount) public { - _mint(to, tokenId, amount, ""); - } - - function safeMintBatch(address to, uint256[] memory tokenIds, uint256[] memory amounts) public { - _mintBatch(to, tokenIds, amounts, ""); - } -} diff --git a/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol b/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol index c2177aaa1..cf828a2d7 100644 --- a/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol +++ b/test/foundry/integration/TestNexusERC721NFT_Integration_WarmAccess.t.sol @@ -48,8 +48,8 @@ contract TestNexusERC721NFT_Integration_WarmAccess is NexusTest_Base { /// @notice Tests sending ERC721 tokens from an already deployed Nexus smart account with warm access function test_Gas_ERC721NFT_DeployedNexus_Transfer_Warm() public checkERC721NFTBalanceWarm(recipient) { + ERC721NFT.mint(preComputedAddress, tokenId); Nexus deployedNexus = deployNexus(user, 100 ether, address(VALIDATOR_MODULE)); - ERC721NFT.safeMint(preComputedAddress, tokenId); Execution[] memory executions = prepareSingleExecution( address(ERC721NFT), 0, @@ -160,11 +160,12 @@ contract TestNexusERC721NFT_Integration_WarmAccess is NexusTest_Base { checkERC721NFTBalanceWarm(recipient) checkPaymasterBalance(address(paymaster)) { + // Mint the NFT to the precomputed address + ERC721NFT.mint(preComputedAddress, tokenId); + // Deploy the Nexus account Nexus deployedNexus = deployNexus(user, 100 ether, address(VALIDATOR_MODULE)); - // Mint the NFT to the precomputed address - ERC721NFT.safeMint(preComputedAddress, tokenId); // Prepare the execution for ERC721 token transfer Execution[] memory executions = prepareSingleExecution( address(ERC721NFT), diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index 745973377..f0b52d25f 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -7,16 +7,10 @@ import "../../../shared/TestModuleManagement_Base.t.sol"; /// @title TestModuleManager_FallbackHandler /// @notice Tests for installing and uninstalling the fallback handler in a smart account. contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { - - MockNFT internal erc721; - MockERC1155 internal erc1155; - + /// @notice Sets up the base module management environment and installs the fallback handler. function setUp() public { init(); - erc721 = new MockNFT("Mock NFT", "MNFT"); - erc1155 = new MockERC1155("Test"); - Execution[] memory execution = new Execution[](2); // Custom data for installing the MockHandler with call type STATIC @@ -296,74 +290,20 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { } function test_onTokenReceived_Success() public { - erc721.safeMint(address(BOB_ACCOUNT), 1); - assertEq(erc721.balanceOf(address(BOB_ACCOUNT)), 1); - - erc1155.safeMint(address(BOB_ACCOUNT), 1, 1); - assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 1), 1); - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 2; - tokenIds[1] = 3; - uint256[] memory amounts = new uint256[](2); - amounts[0] = 10; - amounts[1] = 30; - erc1155.safeMintBatch(address(BOB_ACCOUNT), tokenIds, amounts); - - assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 2), 10); - assertEq(erc1155.balanceOf(address(BOB_ACCOUNT), 3), 30); - + vm.startPrank(address(ENTRYPOINT)); //ERC-721 (bool success, bytes memory data) = address(BOB_ACCOUNT).call{value: 0}(hex'150b7a02'); assertTrue(success); - assertTrue(keccak256(data) == keccak256((hex'150b7a0200000000000000000000000000000000000000000000000000000000'))); + assertTrue(keccak256(data) == keccak256(bytes(hex'150b7a02'))); //ERC-1155 (success, data) = address(BOB_ACCOUNT).call{value: 0}(hex'f23a6e61'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'f23a6e6100000000000000000000000000000000000000000000000000000000'))); + assertTrue(keccak256(data) == keccak256(bytes(hex'f23a6e61'))); //ERC-1155 Batch (success, data) = address(BOB_ACCOUNT).call{value: 0}(hex'bc197c81'); assertTrue(success); - assertTrue(keccak256(data) == keccak256(bytes(hex'bc197c8100000000000000000000000000000000000000000000000000000000'))); - } - - function test_ComplexReturnData() public { - bytes4 selector = bytes4(keccak256(abi.encodePacked("complexReturnData(string,bytes4)"))); - - bytes memory customData = abi.encode(bytes5(abi.encodePacked(selector, CALLTYPE_SINGLE))); // onInstall selector - bytes memory callData = abi.encodeWithSelector( - IModuleManager.installModule.selector, - MODULE_TYPE_FALLBACK, - address(HANDLER_MODULE), - customData - ); - Execution[] memory execution = new Execution[](1); - execution[0] = Execution(address(BOB_ACCOUNT), 0, callData); - PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, execution, address(VALIDATOR_MODULE), 0); - ENTRYPOINT.handleOps(userOps, payable(address(BOB.addr))); + assertTrue(keccak256(data) == keccak256(bytes(hex'bc197c81'))); - // Verify the fallback handler was installed for the given selector - assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_FALLBACK, address(HANDLER_MODULE), customData), "Fallback handler not installed"); - - bytes memory testString = "Hello Complex Return Data that is more than 32 bytes"; - bytes4 testSelector = bytes4(abi.encode("foo()")); - - bytes memory data = abi.encodeWithSelector( - MockHandler(address(0)).complexReturnData.selector, - testString, - testSelector - ); - - address expectedSender = address(0x1234567890123456789012345678901234567890); - vm.prank(expectedSender); - (bool success, bytes memory result) = address(BOB_ACCOUNT).call{value: 0}(data); - assertTrue(success); - (uint256 timestamp, bytes memory resultData, address addr, uint64 chainId, address sender) = abi.decode(result, (uint256, bytes, address, uint64, address)); - assertEq(timestamp, block.timestamp); - assertEq(addr, address(HANDLER_MODULE)); - assertEq(chainId, block.chainid); - assertEq(sender, expectedSender); - bytes memory expectedData = abi.encode(testString, HANDLER_MODULE.getName(), HANDLER_MODULE.getVersion(), testSelector); - assertEq(keccak256(resultData), keccak256(expectedData)); + vm.stopPrank(); } } From 14583d62d70bf8ca4fa2627be451fe9c2c395b77 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 22:50:18 +0300 Subject: [PATCH 48/56] manual hooking for fallback --- contracts/base/ModuleManager.sol | 16 +++++++++++++- .../TestModuleManager_FallbackHandler.t.sol | 22 ++++++------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 667a29f4c..3f18362a7 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -64,7 +64,8 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError receive() external payable {} /// @dev Fallback function to manage incoming calls using designated handlers based on the call type. - fallback() external payable withHook { + /// Hooked manually in the _fallback function + fallback() external payable { _fallback(msg.data); } @@ -425,6 +426,14 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } function _fallback(bytes calldata callData) private { + + // hook manually + address hook = _getHook(); + bytes memory hookData; + if (hook != address(0)) { + hookData = IHook(hook).preCheck(msg.sender, msg.value, msg.data); + } + bool success; bytes memory result; FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig]; @@ -441,6 +450,11 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError revert UnsupportedCallType(calltype); } + // hook post check + if (hook != address(0)) { + IHook(hook).postCheck(hookData); + } + // Use revert message from fallback handler if the call was not successful assembly { if iszero(success) { diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index e1953fb16..3574aa429 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -84,26 +84,14 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { // Prepare UserOperation PackedUserOperation[] memory userOps = buildPackedUserOperation(BOB, BOB_ACCOUNT, EXECTYPE_DEFAULT, executions, address(VALIDATOR_MODULE), 0); - if (!skip) { - // Expect the GenericFallbackCalled event from the MockHandler contract - vm.expectEmit(true, true, false, true, address(HANDLER_MODULE)); - emit GenericFallbackCalled(address(this), 123, "Example data"); - } + // Expect the GenericFallbackCalled event from the MockHandler contract + vm.expectEmit(true, true, false, true, address(HANDLER_MODULE)); + emit GenericFallbackCalled(address(this), 123, "Example data"); // Call handleOps, which should trigger the fallback handler and emit the event ENTRYPOINT.handleOps(userOps, payable(address(BOB.addr))); } - /// @notice Tests that handleOps triggers the generic fallback handler. - function test_HandleOpsTriggersGenericFallback_IsProperlyHooked() public { - vm.expectEmit(address(HOOK_MODULE)); - emit PreCheckCalled(); - vm.expectEmit(address(HOOK_MODULE)); - emit PostCheckCalled(); - // skip fallback emit check as per Matching Sequences section here => https://book.getfoundry.sh/cheatcodes/expect-emit - test_HandleOpsTriggersGenericFallback({skip: true}); - } - /// @notice Tests installing a fallback handler. /// @param selector The function selector for the fallback handler. function test_InstallFallbackHandler(bytes4 selector) internal { @@ -410,6 +398,10 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { assertTrue(success); assertEq(abi.decode(result, (bytes)), expectedResult); + vm.expectEmit(true, true, true, true, address(HOOK_MODULE)); + emit PreCheckCalled(); + vm.expectEmit(true, true, true, true, address(HOOK_MODULE)); + emit PostCheckCalled(); bytes memory result2 = IHandler(address(BOB_ACCOUNT)).returnBytes(); assertEq(result2, expectedResult); } From 32b0976e09ea9c62abc7a275c1bc81c755209891 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 23:18:28 +0300 Subject: [PATCH 49/56] rnm test --- .../modulemanager/TestModuleManager_FallbackHandler.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol index 3574aa429..ed55b3fd4 100644 --- a/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol +++ b/test/foundry/unit/concrete/modulemanager/TestModuleManager_FallbackHandler.t.sol @@ -373,7 +373,7 @@ contract TestModuleManager_FallbackHandler is TestModuleManagement_Base { assertEq(keccak256(resultData), keccak256(expectedData)); } - function test_ReturnBytes() public { + function test_ReturnBytes_and_Hook_fallback() public { bytes4 selector = bytes4(keccak256(abi.encodePacked("returnBytes()"))); bytes memory customData = abi.encode(bytes5(abi.encodePacked(selector, CALLTYPE_SINGLE))); bytes memory callData = abi.encodeWithSelector( From 810107893c9a86a0be83cc3ca0bdb6415abee797 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 3 Mar 2025 23:25:28 +0300 Subject: [PATCH 50/56] no hook onERCxxx --- contracts/base/ModuleManager.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 3f18362a7..04bda373b 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -426,14 +426,6 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } function _fallback(bytes calldata callData) private { - - // hook manually - address hook = _getHook(); - bytes memory hookData; - if (hook != address(0)) { - hookData = IHook(hook).preCheck(msg.sender, msg.value, msg.data); - } - bool success; bytes memory result; FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig]; @@ -441,6 +433,12 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError CallType calltype = $fallbackHandler.calltype; if (handler != address(0)) { + // hook manually + address hook = _getHook(); + bytes memory hookData; + if (hook != address(0)) { + hookData = IHook(hook).preCheck(msg.sender, msg.value, msg.data); + } //if there's a fallback handler, call it if (calltype == CALLTYPE_STATIC) { (success, result) = handler.staticcall(ExecLib.get2771CallData(callData)); @@ -465,6 +463,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError } else { // If there's no handler, the call can be one of onERCXXXReceived() + // No need to hook this as no execution is done here bytes32 s; /// @solidity memory-safe-assembly assembly { From fb7d6fe5442d1868b772aa0778b8f727bc90cd55 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 4 Mar 2025 01:37:54 +0300 Subject: [PATCH 51/56] address comments --- contracts/base/ModuleManager.sol | 38 +++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index 04bda373b..af4e3b4fd 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -453,33 +453,31 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError IHook(hook).postCheck(hookData); } - // Use revert message from fallback handler if the call was not successful assembly { if iszero(success) { + // Use revert message from fallback handler if the call was not successful revert(add(result, 0x20), mload(result)) } - return (add(result, 0x20), mload(result)) + return(add(result, 0x20), mload(result)) } - - } else { - // If there's no handler, the call can be one of onERCXXXReceived() - // No need to hook this as no execution is done here - bytes32 s; - /// @solidity memory-safe-assembly - assembly { - s := shr(224, calldataload(0)) - // 0x150b7a02: `onERC721Received(address,address,uint256,bytes)`. - // 0xf23a6e61: `onERC1155Received(address,address,uint256,uint256,bytes)`. - // 0xbc197c81: `onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)`. - if or(eq(s, 0x150b7a02), or(eq(s, 0xf23a6e61), eq(s, 0xbc197c81))) { - success := true // it is one of onERCXXXReceived - mstore(0x20, s) // Store `msg.sig`. - return(0x3c, 0x20) // Return `msg.sig`. - } + } + + // If there's no handler, the call can be one of onERCXXXReceived() + // No need to hook this as no execution is done here + bytes32 s; + /// @solidity memory-safe-assembly + assembly { + s := shr(224, calldataload(0)) + // 0x150b7a02: `onERC721Received(address,address,uint256,bytes)`. + // 0xf23a6e61: `onERC1155Received(address,address,uint256,uint256,bytes)`. + // 0xbc197c81: `onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)`. + if or(eq(s, 0x150b7a02), or(eq(s, 0xf23a6e61), eq(s, 0xbc197c81))) { + mstore(0x20, s) // Store `msg.sig`. + return(0x3c, 0x20) // Return `msg.sig`. } - // if there was no handler and it is not the onERCXXXReceived call, revert - require(success, MissingFallbackHandler(msg.sig)); } + // if there was no handler and it is not the onERCXXXReceived call, revert + revert MissingFallbackHandler(msg.sig); } /// @dev Helper function to paginate entries in a SentinelList. From 25b37848b7c994164c08ee04892908e9ac8aec9a Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 4 Mar 2025 10:48:26 +0300 Subject: [PATCH 52/56] revert early --- contracts/base/ModuleManager.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/base/ModuleManager.sol b/contracts/base/ModuleManager.sol index af4e3b4fd..919713219 100644 --- a/contracts/base/ModuleManager.sol +++ b/contracts/base/ModuleManager.sol @@ -448,16 +448,20 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError revert UnsupportedCallType(calltype); } + // Use revert message from fallback handler if the call was not successful + assembly { + if iszero(success) { + revert(add(result, 0x20), mload(result)) + } + } + // hook post check if (hook != address(0)) { IHook(hook).postCheck(hookData); } + // return the result assembly { - if iszero(success) { - // Use revert message from fallback handler if the call was not successful - revert(add(result, 0x20), mload(result)) - } return(add(result, 0x20), mload(result)) } } From 495f4011664c10d81d77621a34e6b082fd1d6218 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 4 Mar 2025 11:23:33 +0300 Subject: [PATCH 53/56] remove excessive isContract check in onInstall --- contracts/modules/validators/K1Validator.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/modules/validators/K1Validator.sol b/contracts/modules/validators/K1Validator.sol index 110fcfc27..7968d8ecb 100644 --- a/contracts/modules/validators/K1Validator.sol +++ b/contracts/modules/validators/K1Validator.sol @@ -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:]); From dcaa4ee7a5783d440ecb4f3bffaec46755f22f99 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 4 Mar 2025 14:06:23 +0300 Subject: [PATCH 54/56] fix versions --- contracts/Nexus.sol | 2 +- contracts/base/BaseAccount.sol | 2 +- contracts/modules/validators/K1Validator.sol | 2 +- .../fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol | 2 +- .../concrete/accountconfig/TestAccountConfig_AccountId.t.sol | 2 +- .../concrete/factory/TestAccountFactory_Deployments.t.sol | 2 +- .../factory/TestNexusAccountFactory_Deployments.t.sol | 2 +- .../unit/concrete/gas/TestGas_NexusAccountFactory.t.sol | 2 +- test/foundry/unit/concrete/modules/TestK1Validator.t.sol | 4 ++-- test/hardhat/smart-account/Nexus.Basics.specs.ts | 2 +- test/hardhat/smart-account/Nexus.Module.K1Validator.specs.ts | 4 ++-- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/Nexus.sol b/contracts/Nexus.sol index 265dc1de3..d3917a0bb 100644 --- a/contracts/Nexus.sol +++ b/contracts/Nexus.sol @@ -363,6 +363,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra /// @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.0.2"; } } diff --git a/contracts/base/BaseAccount.sol b/contracts/base/BaseAccount.sol index 1f15e9892..7a2fd52c6 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.0.2"; /// @notice The canonical address for the ERC4337 EntryPoint contract, version 0.7. /// This address is consistent across all supported networks. diff --git a/contracts/modules/validators/K1Validator.sol b/contracts/modules/validators/K1Validator.sol index 7968d8ecb..7a6c887fe 100644 --- a/contracts/modules/validators/K1Validator.sol +++ b/contracts/modules/validators/K1Validator.sol @@ -191,7 +191,7 @@ contract K1Validator is IValidator, ERC7739Validator { /// @notice Returns the version of the module /// @return The version of the module function version() external pure returns (string memory) { - return "1.0.1"; + return "1.0.2"; } /// @notice Checks if the module is of the specified type diff --git a/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol index 06b9584fb..a50dcdbcf 100644 --- a/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol +++ b/test/foundry/fork/arbitrum/ArbitrumSmartAccountUpgradeTest.t.sol @@ -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.0.2"; string memory actualAccountId = IAccountConfig(payable(address(smartAccountV2))).accountId(); assertEq(actualAccountId, expectedAccountId, "Account ID does not match after upgrade."); } diff --git a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol index c8e137971..d2a346d59 100644 --- a/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol +++ b/test/foundry/unit/concrete/accountconfig/TestAccountConfig_AccountId.t.sol @@ -19,7 +19,7 @@ contract TestAccountConfig_AccountId is Test { /// @notice Tests if the account ID returns the expected value function test_WhenCheckingTheAccountID() external givenTheAccountConfiguration { - string memory expected = "biconomy.nexus.1.0.0"; + string memory expected = "biconomy.nexus.1.0.2"; assertEq(accountConfig.accountId(), expected, "AccountConfig should return the expected account ID."); } } diff --git a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol index 6b97f1e40..2845260ee 100644 --- a/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestAccountFactory_Deployments.t.sol @@ -72,7 +72,7 @@ contract TestAccountFactory_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.0.2", "Not deployed properly"); } /// @notice Tests that deploying an account fails if it already exists. diff --git a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol index ce4146d9b..932159b22 100644 --- a/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol +++ b/test/foundry/unit/concrete/factory/TestNexusAccountFactory_Deployments.t.sol @@ -83,7 +83,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.0.2", "Not deployed properly"); } /// @notice Tests that deploying an account fails if it already exists. diff --git a/test/foundry/unit/concrete/gas/TestGas_NexusAccountFactory.t.sol b/test/foundry/unit/concrete/gas/TestGas_NexusAccountFactory.t.sol index 628adbd50..9fcca4a61 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.0.2"; 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/modules/TestK1Validator.t.sol b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol index 47e5dad62..4e5c8d121 100644 --- a/test/foundry/unit/concrete/modules/TestK1Validator.t.sol +++ b/test/foundry/unit/concrete/modules/TestK1Validator.t.sol @@ -187,7 +187,7 @@ contract TestK1Validator is NexusTest_Base { function test_Version() public { string memory contractVersion = validator.version(); - assertEq(contractVersion, "1.0.1", "Contract version should be '1.0.1'"); + assertEq(contractVersion, "1.0.2", "Contract version should be '1.0.2'"); } /// @notice Tests the isModuleType function to return the correct module type @@ -383,7 +383,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.0.2"), block.chainid, address(BOB_ACCOUNT) ) diff --git a/test/hardhat/smart-account/Nexus.Basics.specs.ts b/test/hardhat/smart-account/Nexus.Basics.specs.ts index 0ac6613c6..9dfdc588e 100644 --- a/test/hardhat/smart-account/Nexus.Basics.specs.ts +++ b/test/hardhat/smart-account/Nexus.Basics.specs.ts @@ -133,7 +133,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.0.2"); }); it("Should get implementation address of smart account", async () => { diff --git a/test/hardhat/smart-account/Nexus.Module.K1Validator.specs.ts b/test/hardhat/smart-account/Nexus.Module.K1Validator.specs.ts index 68b4d7fc2..45394706e 100644 --- a/test/hardhat/smart-account/Nexus.Module.K1Validator.specs.ts +++ b/test/hardhat/smart-account/Nexus.Module.K1Validator.specs.ts @@ -61,7 +61,7 @@ describe("K1Validator module tests", () => { }); }); - describe("K1Validtor tests", () => { + describe("K1Validator tests", () => { it("should check if validator is installed", async () => { expect( await deployedNexus.isModuleInstalled( @@ -79,7 +79,7 @@ describe("K1Validator module tests", () => { it("should get module version", async () => { const version = await k1Validator.version(); - expect(version).to.equal("1.0.1"); + expect(version).to.equal("1.0.2"); }); it("should check module type", async () => { From 13c250dd16f599c9b46b427e790dea1b61320170 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 4 Mar 2025 20:54:37 +0300 Subject: [PATCH 55/56] revert isContract check removal --- contracts/modules/validators/K1Validator.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/modules/validators/K1Validator.sol b/contracts/modules/validators/K1Validator.sol index 7a6c887fe..14eaec854 100644 --- a/contracts/modules/validators/K1Validator.sol +++ b/contracts/modules/validators/K1Validator.sol @@ -76,6 +76,7 @@ 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:]); From cc6e73586cc12bc4dad04077359285b38aeb6757 Mon Sep 17 00:00:00 2001 From: highskore Date: Thu, 6 Mar 2025 11:30:30 +0100 Subject: [PATCH 56/56] fix(IPrevalidationHook): make 4337 hook non-view --- contracts/interfaces/modules/IPreValidationHook.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/interfaces/modules/IPreValidationHook.sol b/contracts/interfaces/modules/IPreValidationHook.sol index b9b1b6b6d..70fcda482 100644 --- a/contracts/interfaces/modules/IPreValidationHook.sol +++ b/contracts/interfaces/modules/IPreValidationHook.sol @@ -33,6 +33,5 @@ interface IPreValidationHookERC4337 is IModule { bytes32 userOpHash ) external - view returns (bytes32 hookHash, bytes memory hookSignature); }