Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add prevalidation hook support #232

Merged
merged 7 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
42 changes: 34 additions & 8 deletions contracts/Nexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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 = op;
// 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()) {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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];

Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
137 changes: 135 additions & 2 deletions contracts/base/ModuleManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -282,6 +295,46 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
fallbackHandler.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, data[4:]));
}

/// @dev Installs a pre-validation hook module, ensuring no other pre-validation hooks are installed before proceeding.
/// @param preValidationHookType The type of the pre-validation hook.
/// @param preValidationHook The address of the pre-validation hook to be installed.
/// @param data Initialization data to configure the hook upon installation.
function _installPreValidationHook(
uint256 preValidationHookType,
address preValidationHook,
bytes calldata data
)
internal
virtual
withRegistry(preValidationHook, preValidationHookType)
{
if (!IModule(preValidationHook).isModuleType(preValidationHookType)) revert MismatchModuleTypeId(MODULE_TYPE_HOOK);
address currentPreValidationHook = _getPreValidationHook(preValidationHookType);
require(currentPreValidationHook == address(0), PrevalidationHookAlreadyInstalled(currentPreValidationHook));
_setPreValidationHook(preValidationHookType, preValidationHook);
IModule(preValidationHook).onInstall(data);
}

/// @dev Uninstalls a pre-validation hook module
/// @param preValidationHook The address of the pre-validation hook to be uninstalled.
/// @param hookType The type of the pre-validation hook.
/// @param data De-initialization data to configure the hook upon uninstallation.
function _uninstallPreValidationHook(address preValidationHook, uint256 hookType, bytes calldata data) internal virtual {
_setPreValidationHook(hookType, address(0));
preValidationHook.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, data));
}

/// @dev Sets the current pre-validation hook in the storage to the specified address, based on the hook type.
/// @param hookType The type of the pre-validation hook.
/// @param hook The new hook address.
function _setPreValidationHook(uint256 hookType, address hook) internal virtual {
if (hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271) {
_getAccountStorage().preValidationHookERC1271 = IPreValidationHookERC1271(hook);
} else if (hookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337) {
_getAccountStorage().preValidationHookERC4337 = IPreValidationHookERC4337(hook);
}
}

/// @notice Installs a module with multiple types in a single operation.
/// @dev This function handles installing a multi-type module by iterating through each type and initializing it.
/// The initData should include an ABI-encoded tuple of (uint[] types, bytes[] initDatas).
Expand Down Expand Up @@ -321,9 +374,77 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
else if (theType == MODULE_TYPE_HOOK) {
_installHook(module, initDatas[i]);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INSTALL PRE-VALIDATION HOOK */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
else if (theType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271 || theType == MODULE_TYPE_PREVALIDATION_HOOK_ERC4337) {
_installPreValidationHook(theType, module, initDatas[i]);
}
}
}

/// @notice Checks if an emergency uninstall signature is valid.
/// @param data The emergency uninstall data.
/// @param signature The signature to validate.
function _checkEmergencyUninstallSignature(EmergencyUninstall calldata data, bytes calldata signature) internal {
address validator = address(bytes20(signature[0:20]));
require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator));
// Hash the data
bytes32 hash = _getEmergencyUninstallDataHash(data.hook, data.hookType, data.deInitData, data.nonce);
// Check if nonce is valid
require(!_getAccountStorage().nonces[data.nonce], InvalidNonce());
// Mark nonce as used
_getAccountStorage().nonces[data.nonce] = true;
// Check if the signature is valid
require((IValidator(validator).isValidSignatureWithSender(address(this), hash, signature[20:]) == ERC1271_MAGICVALUE), EmergencyUninstallSigError());
}

/// @dev Retrieves the pre-validation hook from the storage based on the hook type.
/// @param preValidationHookType The type of the pre-validation hook.
/// @return preValidationHook The address of the pre-validation hook.
function _getPreValidationHook(uint256 preValidationHookType) internal view returns (address preValidationHook) {
preValidationHook = preValidationHookType == MODULE_TYPE_PREVALIDATION_HOOK_ERC1271
? address(_getAccountStorage().preValidationHookERC1271)
: address(_getAccountStorage().preValidationHookERC4337);
}

/// @dev Calls the pre-validation hook for ERC-1271.
/// @param hash The hash of the user operation.
/// @param signature The signature to validate.
/// @return postHash The updated hash after the pre-validation hook.
/// @return postSig The updated signature after the pre-validation hook.
function _withPreValidationHook(bytes32 hash, bytes calldata signature) internal view virtual returns (bytes32 postHash, bytes memory postSig) {
// Get the pre-validation hook for ERC-1271
address preValidationHook = _getPreValidationHook(MODULE_TYPE_PREVALIDATION_HOOK_ERC1271);
// If no pre-validation hook is installed, return the original hash and signature
if (preValidationHook == address(0)) return (hash, signature);
// Otherwise, call the pre-validation hook and return the updated hash and signature
else return IPreValidationHookERC1271(preValidationHook).preValidationHookERC1271(msg.sender, hash, signature);
}

/// @dev Calls the pre-validation hook for ERC-4337.
/// @param hash The hash of the user operation.
/// @param userOp The user operation data.
/// @param missingAccountFunds The amount of missing account funds.
/// @return postHash The updated hash after the pre-validation hook.
/// @return postSig The updated signature after the pre-validation hook.
function _withPreValidationHook(
bytes32 hash,
PackedUserOperation memory userOp,
uint256 missingAccountFunds
)
internal
virtual
returns (bytes32 postHash, bytes memory postSig)
{
// Get the pre-validation hook for ERC-4337
address preValidationHook = _getPreValidationHook(MODULE_TYPE_PREVALIDATION_HOOK_ERC4337);
// If no pre-validation hook is installed, return the original hash and signature
if (preValidationHook == address(0)) return (hash, userOp.signature);
// Otherwise, call the pre-validation hook and return the updated hash and signature
else return IPreValidationHookERC4337(preValidationHook).preValidationHookERC4337(userOp, missingAccountFunds, hash);
}

/// @notice Checks if an enable mode signature is valid.
/// @param structHash data hash.
/// @param sig Signature.
Expand Down Expand Up @@ -355,6 +476,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.
Expand All @@ -376,6 +507,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;
}
Expand Down
9 changes: 9 additions & 0 deletions contracts/interfaces/base/IModuleManagerEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down
Loading
Loading