Skip to content

Commit

Permalink
Merge pull request #236 from bcnmy/feat/7702-compatible-moduleEnableMode
Browse files Browse the repository at this point in the history
Make Module Enable Mode compatible with 7702
  • Loading branch information
filmakarov authored Feb 3, 2025
2 parents a078cd7 + 1e4eab1 commit f611a15
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 37 deletions.
29 changes: 11 additions & 18 deletions contracts/Nexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand Down Expand Up @@ -125,9 +123,9 @@ 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 = _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);
Expand Down Expand Up @@ -210,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);
}
Expand Down Expand Up @@ -278,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
Expand Down Expand Up @@ -312,7 +319,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));
Expand Down Expand Up @@ -439,19 +445,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";
Expand Down
64 changes: 49 additions & 15 deletions contracts/base/ModuleManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
Expand Down Expand Up @@ -141,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);
}

Expand Down Expand Up @@ -181,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);
}

Expand Down Expand Up @@ -450,20 +453,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 (!_hasValidators() && !_hasExecutors()) {
// 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);
}
}

}

/// @notice Builds the enable mode data hash as per eip712
Expand Down Expand Up @@ -515,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.
Expand Down Expand Up @@ -552,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.
Expand All @@ -572,6 +593,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 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;
}

function _fallback(bytes calldata callData) private returns (bytes memory result) {
bool success;
FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig];
Expand Down
4 changes: 2 additions & 2 deletions test/foundry/fork/base/BaseSettings.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions test/foundry/utils/TestHelper.t.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}

Expand Down

0 comments on commit f611a15

Please sign in to comment.