Skip to content

Commit

Permalink
Merge pull request #232 from highskore/feat/pre-validation-hooks
Browse files Browse the repository at this point in the history
feat: add prevalidation hook support
  • Loading branch information
filmakarov authored Jan 14, 2025
2 parents be790e8 + 84f9aa0 commit a078cd7
Show file tree
Hide file tree
Showing 19 changed files with 1,883 additions and 146 deletions.
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

0 comments on commit a078cd7

Please sign in to comment.