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

Add Default Validator and clean 7702 init flows (AA-521) #243

Draft
wants to merge 20 commits into
base: release/eip-7702-sepolia-pre-release
Choose a base branch
from
Draft
177 changes: 102 additions & 75 deletions contracts/Nexus.sol

Large diffs are not rendered by default.

140 changes: 58 additions & 82 deletions contracts/base/ModuleManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ pragma solidity ^0.8.27;
// Learn more at https://biconomy.io. To report security issues, please contact us at: [email protected]

import { SentinelListLib, SENTINEL } from "sentinellist/SentinelList.sol";
import { NexusSentinelListLib } from "../lib/NexusSentinelList.sol";
import { Storage } from "./Storage.sol";
import { IHook } from "../interfaces/modules/IHook.sol";
import { IModule } from "../interfaces/modules/IModule.sol";
Expand Down Expand Up @@ -55,11 +54,25 @@ import { ECDSA } from "solady/utils/ECDSA.sol";
/// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady
abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndErrors, RegistryAdapter, ERC7779Adaptor {
using SentinelListLib for SentinelListLib.SentinelList;
using NexusSentinelListLib for NexusSentinelListLib.SentinelList;
using LocalCallDataParserLib for bytes;
using ExecLib for address;
using ExcessivelySafeCall for address;
using ECDSA for bytes32;

/// @dev The default validator address.
/// @notice To initialize the default validator, Nexus.execute(_DEFAULT_VALIDATOR.onInstall(...)) should be called.
address internal immutable _DEFAULT_VALIDATOR;
/// @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));
Expand Down Expand Up @@ -145,7 +158,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();
Expand All @@ -167,11 +180,19 @@ 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();
if (!_areSentinelListsInitialized()) {
_initSentinelLists();
}
_installModule(moduleType, module, moduleInitData);
}
Expand All @@ -189,6 +210,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);
Expand All @@ -212,6 +236,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) revert DefaultValidatorCanNotBeInstalled();
_getAccountStorage().validators.push(validator);
IValidator(validator).onInstall(data);
}
Expand All @@ -227,10 +252,9 @@ 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));
// no check for removing the last validator, as with 7702 and DEFAULT_VALIDATOR,
// it doesn't necessarily mean the account is unusable
}

/// @dev Uninstalls all validators and emits an event if any validator fails to uninstall.
Expand Down Expand Up @@ -268,7 +292,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError

/// @dev Uninstalls all executors and emits an event if any executor fails to uninstall.
function _tryUninstallExecutors() internal {
NexusSentinelListLib.SentinelList storage executors = _getAccountStorage().executors;
SentinelListLib.SentinelList storage executors = _getAccountStorage().executors;
address executor = executors.getNext(SENTINEL);
while (executor != SENTINEL) {
try IExecutor(executor).onUninstall("") {} catch (bytes memory reason) {
Expand Down Expand Up @@ -532,25 +556,20 @@ 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]));
function _checkEnableModeSignature(
bytes32 structHash,
bytes calldata sig,
address validator
) internal view returns (bool) {
bytes32 eip712Digest = _hashTypedData(structHash);

// TODO: HERE AND IN THE SIMILAR LOGIC (validate userOp, 7821 execution guard) => instead of checking isInitialized to understand if it is 7702,
// just check sig length. if it is 65 only, then validator is not attached, then it can be 7702 if the sig is valid

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 {
return _eip7702SignatureValidation(eip712Digest, sig, enableModeSigValidator);
} catch {
return false;
}
}

Expand Down Expand Up @@ -605,7 +624,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();
Expand Down Expand Up @@ -635,20 +654,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.
Expand All @@ -669,48 +674,31 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
hook = address(_getAccountStorage().hook);
}

function _eip7702SignatureValidation(bytes32 dataHash, bytes calldata signature, address validator) internal view returns (bool) {
if (!_isAlreadyInitialized()) {
// Check the userOp signature if the validator is not installed (used for EIP7702)
return _checkSelfSignature(signature, dataHash);
} else {
// If the account is initialized, revert as the validator is not installed
revert ValidatorNotInstalled(validator);
}
}

/// @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
// toEthSignedMessageHash() is now considered fallback as userOpHash is eip-712 since ep v0.8
address signer = ECDSA.recover(dataHash, signature);
if (signer == address(this)) return true;
signer = ECDSA.recover(dataHash.toEthSignedMessageHash(), signature);
if (signer == address(this)) return true;
return false;
}

/// @dev Checks if the account is an ERC7702 account
/// by checking the codehash of the account
/// and comparing it to the known ERC7702 codehash
function _amIERC7702() internal view returns (bool) {
bytes32 c;
assembly {
// use extcodesize as the first cheapest check
if eq(extcodesize(address()), 23) {
// use extcodecopy to copy first 3 bytes of this contract and compare with 0xef0100
let ptr := mload(0x40)
extcodecopy(address(), ptr, 0, 3)
codecopy(ptr, 0, 3)
c := mload(ptr)
}
// if it is not 23, we do not even check the code
// if it is not 23, we do not even check the first 3 bytes
}
return bytes3(c) == bytes3(0xef0100);
}

function _handleSigValidator(address validator) internal view returns (address) {
if (validator == _DEFAULT_VALIDATOR_FLAG) {
return _DEFAULT_VALIDATOR;
} else {
require(_isValidatorInstalled(validator), ValidatorNotInstalled(validator));
return validator;
}
}

function _fallback(bytes calldata callData) private returns (bytes memory result) {
bool success;
FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig];
Expand Down Expand Up @@ -773,16 +761,4 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
{
(array, nextCursor) = list.getEntriesPaginated(cursor, size);
}

function _paginate(
NexusSentinelListLib.SentinelList storage list,
address cursor,
uint256 size
)
private
view
returns (address[] memory array, address nextCursor)
{
(array, nextCursor) = list.getEntriesPaginated(cursor, size);
}
}
3 changes: 3 additions & 0 deletions contracts/interfaces/INexusEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ interface INexusEventsAndErrors {
/// @param innerCallRet The return data from the inner call execution.
event Executed(PackedUserOperation userOp, bytes innerCallRet);

/// @notice Emitted when a keyless Nexus is initialized.
event KeylessNexusInitialized(address indexed keylessNexus);

/// @notice Error thrown when an unsupported ModuleType is requested.
/// @param moduleTypeId The ID of the unsupported module type.
error UnsupportedModuleType(uint256 moduleTypeId);
Expand Down
3 changes: 3 additions & 0 deletions contracts/interfaces/base/IModuleManagerEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,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);

/// @dev Thrown when there is an attempt to install the default validator as a regular validator.
error DefaultValidatorCanNotBeInstalled();
}
3 changes: 1 addition & 2 deletions contracts/interfaces/base/IStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ pragma solidity ^0.8.27;
// Learn more at https://biconomy.io. To report security issues, please contact us at: [email protected]

import { SentinelListLib } from "sentinellist/SentinelList.sol";
import { NexusSentinelListLib } from "../../lib/NexusSentinelList.sol";
import { IPreValidationHookERC1271, IPreValidationHookERC4337 } from "../modules/IPreValidationHook.sol";
import { IHook } from "../modules/IHook.sol";
import { CallType } from "../../lib/ModeLib.sol";
Expand All @@ -35,7 +34,7 @@ interface IStorage {
///< List of validators, initialized upon contract deployment.
SentinelListLib.SentinelList validators;
///< List of executors, similarly initialized.
NexusSentinelListLib.SentinelList executors;
SentinelListLib.SentinelList executors;
///< Mapping of selectors to their respective fallback handlers.
mapping(bytes4 => FallbackHandler) fallbacks;
///< Current hook module associated with this account.
Expand Down
Loading
Loading