diff --git a/src/modules/execution/SchedulingBaseModule.sol b/src/modules/execution/SchedulingBaseModule.sol
new file mode 100644
index 00000000..7838e136
--- /dev/null
+++ b/src/modules/execution/SchedulingBaseModule.sol
@@ -0,0 +1,316 @@
+// This file is part of Modular Account.
+//
+// Copyright 2024 Alchemy Insights, Inc.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General
+// Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
+// option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
+// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with this program. If not, see
+// .
+
+pragma solidity ^0.8.26;
+
+import {ExecutionManifest, ManifestExecutionFunction} from "@erc6900/reference-implementation/interfaces/IExecutionModule.sol";
+import {IExecutionModule} from "@erc6900/reference-implementation/interfaces/IExecutionModule.sol";
+import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
+import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
+
+import {ModuleBase} from "../ModuleBase.sol";
+
+/// @title Scheduling Base Module
+/// @author Rhinestone (adapted for ERC-6900 by @armanmamyan)
+/// @notice Base module that provides scheduling functionality for executing transactions at specific intervals
+/// NOTE:
+/// - This is an abstract contract that provides core scheduling logic
+/// - Concrete implementations should inherit from this and implement specific execution logic
+/// - Each entity ID can have multiple scheduled jobs
+/// - Jobs can be enabled/disabled and have configurable execution intervals
+abstract contract SchedulingBaseModule is IExecutionModule, ModuleBase {
+ /*//////////////////////////////////////////////////////////////////////////
+ CONSTANTS & STORAGE
+ //////////////////////////////////////////////////////////////////////////*/
+
+ struct ExecutionConfig {
+ uint48 executeInterval;
+ uint16 numberOfExecutions;
+ uint16 numberOfExecutionsCompleted;
+ uint48 startDate;
+ bool isEnabled;
+ uint48 lastExecutionTime;
+ bytes executionData;
+ }
+
+ struct ExecutorAccess {
+ uint256 jobId;
+ }
+
+ // entityId => account => jobId => config
+ mapping(uint32 entityId => mapping(address account => mapping(uint256 jobId => ExecutionConfig))) public executionLog;
+
+ // entityId => account => jobCount
+ mapping(uint32 entityId => mapping(address account => uint256 jobCount)) public accountJobCount;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ EVENTS
+ //////////////////////////////////////////////////////////////////////////*/
+
+ event ExecutionAdded(address indexed smartAccount, uint32 indexed entityId, uint256 indexed jobId);
+ event ExecutionTriggered(address indexed smartAccount, uint32 indexed entityId, uint256 indexed jobId);
+ event ExecutionStatusUpdated(address indexed smartAccount, uint32 indexed entityId, uint256 indexed jobId);
+ event ExecutionsCancelled(address indexed smartAccount, uint32 indexed entityId);
+
+ /*//////////////////////////////////////////////////////////////////////////
+ ERRORS
+ //////////////////////////////////////////////////////////////////////////*/
+
+ error InvalidExecution();
+ error NotInitialized();
+ error JobNotFound();
+ error ExecutionNotReady();
+
+ /*//////////////////////////////////////////////////////////////////////////
+ MODULE LIFECYCLE
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IModule
+ /// @notice Initializes the module with initial scheduling data
+ /// @dev data is encoded as: abi.encode(uint32 entityId, bytes orderData)
+ function onInstall(bytes calldata data) external override {
+ (uint32 entityId, bytes memory orderData) = abi.decode(data, (uint32, bytes));
+
+ if (orderData.length > 0) {
+ _createExecution(entityId, orderData);
+ }
+ }
+
+ /// @inheritdoc IModule
+ /// @notice Handles the uninstallation of the module and clears all jobs for the entity
+ /// @dev data is encoded as: abi.encode(uint32 entityId)
+ function onUninstall(bytes calldata data) external override {
+ uint32 entityId = abi.decode(data, (uint32));
+ address account = msg.sender;
+
+ uint256 count = accountJobCount[entityId][account];
+ for (uint256 i = 1; i <= count; i++) {
+ delete executionLog[entityId][account][i];
+ }
+ accountJobCount[entityId][account] = 0;
+
+ emit ExecutionsCancelled(account, entityId);
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ EXECUTION MODULE
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IExecutionModule
+ function executionManifest() external pure override returns (ExecutionManifest memory) {
+ ExecutionManifest memory manifest;
+
+ manifest.executionFunctions = new ManifestExecutionFunction[](4);
+ manifest.executionFunctions[0] = ManifestExecutionFunction({
+ executionSelector: this.addOrder.selector,
+ skipRuntimeValidation: false,
+ allowGlobalValidation: false
+ });
+ manifest.executionFunctions[1] = ManifestExecutionFunction({
+ executionSelector: this.toggleOrder.selector,
+ skipRuntimeValidation: false,
+ allowGlobalValidation: false
+ });
+ manifest.executionFunctions[2] = ManifestExecutionFunction({
+ executionSelector: this.executeOrder.selector,
+ skipRuntimeValidation: true,
+ allowGlobalValidation: true
+ });
+ manifest.executionFunctions[3] = ManifestExecutionFunction({
+ executionSelector: this.getExecutionConfig.selector,
+ skipRuntimeValidation: true,
+ allowGlobalValidation: true
+ });
+
+ // No required interfaces or hooks for basic scheduling
+ return manifest;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ PUBLIC FUNCTIONS
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @notice Checks if the module is initialized for a specific entity ID and account
+ /// @param entityId The entity ID to check
+ /// @param account Address of the account
+ /// @return true if initialized, false otherwise
+ function isInitialized(uint32 entityId, address account) public view returns (bool) {
+ return accountJobCount[entityId][account] != 0;
+ }
+
+ /// @notice Adds a new scheduled order
+ /// @param entityId The entity ID for this scheduling context
+ /// @param orderData Encoded order data specific to the implementation
+ function addOrder(uint32 entityId, bytes calldata orderData) external {
+ address account = msg.sender;
+ if (!isInitialized(entityId, account)) {
+ revert NotInitialized();
+ }
+
+ _createExecution(entityId, orderData);
+ }
+
+ /// @notice Toggles the enabled state of a scheduled order
+ /// @param entityId The entity ID for this scheduling context
+ /// @param jobId The job ID to toggle
+ function toggleOrder(uint32 entityId, uint256 jobId) external {
+ address account = msg.sender;
+
+ ExecutionConfig storage executionConfig = executionLog[entityId][account][jobId];
+
+ if (executionConfig.numberOfExecutions == 0) {
+ revert JobNotFound();
+ }
+
+ executionConfig.isEnabled = !executionConfig.isEnabled;
+
+ emit ExecutionStatusUpdated(account, entityId, jobId);
+ }
+
+ /// @notice Gets the execution configuration for a specific job
+ /// @param entityId The entity ID
+ /// @param account The account address
+ /// @param jobId The job ID
+ /// @return config The execution configuration
+ function getExecutionConfig(uint32 entityId, address account, uint256 jobId)
+ external
+ view
+ returns (ExecutionConfig memory config)
+ {
+ return executionLog[entityId][account][jobId];
+ }
+
+ /// @notice Executes a scheduled order (to be implemented by concrete contracts)
+ /// @param entityId The entity ID for this scheduling context
+ /// @param jobId The job ID to execute
+ function executeOrder(uint32 entityId, uint256 jobId) external virtual;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ INTERNAL FUNCTIONS
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @notice Creates a new execution configuration
+ /// @param entityId The entity ID for this scheduling context
+ /// @param orderData The encoded order data
+ function _createExecution(uint32 entityId, bytes memory orderData) internal {
+ address account = msg.sender;
+
+ uint256 jobId = accountJobCount[entityId][account] + 1;
+ accountJobCount[entityId][account]++;
+
+ // Parse order data: executeInterval (6 bytes) + numberOfExecutions (2 bytes) + startDate (6 bytes) + executionData
+ if (orderData.length < 14) {
+ revert InvalidExecution();
+ }
+
+ uint48 executeInterval;
+ uint16 numberOfExecutions;
+ uint48 startDate;
+ bytes memory executionData;
+
+ assembly {
+ let dataPtr := add(orderData, 0x20)
+ executeInterval := shr(208, mload(dataPtr)) // Extract first 6 bytes (48 bits)
+ numberOfExecutions := shr(240, mload(add(dataPtr, 6))) // Extract next 2 bytes (16 bits)
+ startDate := shr(208, mload(add(dataPtr, 8))) // Extract next 6 bytes (48 bits)
+ }
+
+ // Extract execution data (remaining bytes after first 14)
+ executionData = new bytes(orderData.length - 14);
+ for (uint256 i = 0; i < executionData.length; i++) {
+ executionData[i] = orderData[i + 14];
+ }
+
+ // Prevent user from supplying an invalid number of executions (0)
+ if (numberOfExecutions == 0) {
+ revert InvalidExecution();
+ }
+
+ executionLog[entityId][account][jobId] = ExecutionConfig({
+ numberOfExecutionsCompleted: 0,
+ isEnabled: true,
+ lastExecutionTime: 0,
+ executeInterval: executeInterval,
+ numberOfExecutions: numberOfExecutions,
+ startDate: startDate,
+ executionData: executionData
+ });
+
+ emit ExecutionAdded(account, entityId, jobId);
+ }
+
+ /// @notice Validates if an execution is ready to run
+ /// @param entityId The entity ID
+ /// @param jobId The job ID to validate
+ function _validateExecution(uint32 entityId, uint256 jobId) internal view {
+ ExecutionConfig storage executionConfig = executionLog[entityId][msg.sender][jobId];
+
+ if (!executionConfig.isEnabled) {
+ revert InvalidExecution();
+ }
+
+ if (executionConfig.lastExecutionTime + executionConfig.executeInterval > block.timestamp) {
+ revert ExecutionNotReady();
+ }
+
+ if (executionConfig.numberOfExecutionsCompleted >= executionConfig.numberOfExecutions) {
+ revert InvalidExecution();
+ }
+
+ if (executionConfig.startDate > block.timestamp) {
+ revert ExecutionNotReady();
+ }
+ }
+
+ /// @notice Updates execution state after a successful execution
+ /// @param entityId The entity ID
+ /// @param jobId The job ID that was executed
+ function _updateExecutionState(uint32 entityId, uint256 jobId) internal {
+ ExecutionConfig storage executionConfig = executionLog[entityId][msg.sender][jobId];
+
+ executionConfig.lastExecutionTime = uint48(block.timestamp);
+ executionConfig.numberOfExecutionsCompleted += 1;
+
+ emit ExecutionTriggered(msg.sender, entityId, jobId);
+ }
+
+ /// @notice Modifier to validate execution before allowing it
+ modifier canExecute(uint32 entityId, uint256 jobId) {
+ _validateExecution(entityId, jobId);
+ _;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ MODULE METADATA
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IModule
+ function moduleId() external pure virtual returns (string memory) {
+ return "rhinestone.scheduling-base-module.1.0.0";
+ }
+
+ /// @inheritdoc IERC165
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(ModuleBase, IERC165)
+ returns (bool)
+ {
+ return interfaceId == type(IExecutionModule).interfaceId || super.supportsInterface(interfaceId);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/validation/DeadmanSwitchModule.sol b/src/modules/validation/DeadmanSwitchModule.sol
new file mode 100644
index 00000000..71b58ed0
--- /dev/null
+++ b/src/modules/validation/DeadmanSwitchModule.sol
@@ -0,0 +1,283 @@
+// This file is part of Modular Account.
+//
+// Copyright 2024 Alchemy Insights, Inc.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General
+// Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
+// option) any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
+// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with this program. If not, see
+// .
+
+pragma solidity ^0.8.26;
+
+import {IExecutionHookModule} from "@erc6900/reference-implementation/interfaces/IExecutionHookModule.sol";
+import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
+import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
+import {_packValidationData} from "@eth-infinitism/account-abstraction/core/Helpers.sol";
+import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
+import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
+import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
+import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
+import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
+
+import {ModuleBase} from "../ModuleBase.sol";
+
+/// @title Deadman Switch Module
+/// @author Rhinestone (adapted for ERC-6900 by @armanmamyan)
+/// @notice Module that allows users to set a nominee that can recover their account if they are
+/// inactive for a certain period of time. Combines validation and execution hook functionality.
+/// NOTE:
+/// - This module implements both IValidationModule and IExecutionHookModule
+/// - The execution hook updates the last access time on every transaction
+/// - The validation module allows the nominee to control the account after timeout
+/// - Uninstallation will NOT disable all installed entity IDs of an account. It only uninstalls the
+/// entity ID that is passed in. Account must remove access for each entity ID if want to disable all.
+contract DeadmanSwitchModule is IValidationModule, IExecutionHookModule, ModuleBase {
+ using MessageHashUtils for bytes32;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ CONSTANTS & STORAGE
+ //////////////////////////////////////////////////////////////////////////*/
+
+ struct DeadmanSwitchConfig {
+ uint48 lastAccess;
+ uint48 timeout;
+ address nominee;
+ }
+
+ uint256 internal constant _SIG_VALIDATION_PASSED = 0;
+ uint256 internal constant _SIG_VALIDATION_FAILED = 1;
+
+ // bytes4(keccak256("isValidSignature(bytes32,bytes)"))
+ bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e;
+ bytes4 internal constant _1271_INVALID = 0xffffffff;
+
+ // entityId => account => config
+ mapping(uint32 entityId => mapping(address account => DeadmanSwitchConfig)) public configs;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ EVENTS
+ //////////////////////////////////////////////////////////////////////////*/
+
+ event DeadmanSwitchConfigured(
+ address indexed account,
+ uint32 indexed entityId,
+ address nominee,
+ uint48 timeout
+ );
+ event NomineeUpdated(address indexed account, uint32 indexed entityId, address nominee);
+ event TimeoutUpdated(address indexed account, uint32 indexed entityId, uint48 timeout);
+ event LastAccessUpdated(address indexed account, uint32 indexed entityId, uint48 lastAccess);
+
+ /*//////////////////////////////////////////////////////////////////////////
+ ERRORS
+ //////////////////////////////////////////////////////////////////////////*/
+
+ error UnsupportedOperation();
+ error NotInitialized();
+ error InvalidNominee();
+ error InvalidTimeout();
+ error NotAuthorized();
+
+ /*//////////////////////////////////////////////////////////////////////////
+ MODULE LIFECYCLE
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IModule
+ /// @notice Initializes the module with the nominee and timeout
+ /// @dev data is encoded as: abi.encode(uint32 entityId, address nominee, uint48 timeout)
+ function onInstall(bytes calldata data) external override {
+ (uint32 entityId, address nominee, uint48 timeout) = abi.decode(data, (uint32, address, uint48));
+
+ if (nominee == address(0)) revert InvalidNominee();
+ if (timeout == 0) revert InvalidTimeout();
+
+ configs[entityId][msg.sender] = DeadmanSwitchConfig({
+ lastAccess: uint48(block.timestamp),
+ timeout: timeout,
+ nominee: nominee
+ });
+
+ emit DeadmanSwitchConfigured(msg.sender, entityId, nominee, timeout);
+ }
+
+ /// @inheritdoc IModule
+ /// @notice Handles the uninstallation of the module and clears the config
+ /// @dev data is encoded as: abi.encode(uint32 entityId)
+ function onUninstall(bytes calldata data) external override {
+ uint32 entityId = abi.decode(data, (uint32));
+ delete configs[entityId][msg.sender];
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ CONFIG FUNCTIONS
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @notice Sets the nominee for a specific entity ID
+ /// @param entityId The entity ID to configure
+ /// @param nominee Address of the nominee
+ function setNominee(uint32 entityId, address nominee) external {
+ if (!isInitialized(entityId, msg.sender)) revert NotInitialized();
+ if (nominee == address(0)) revert InvalidNominee();
+
+ configs[entityId][msg.sender].nominee = nominee;
+ emit NomineeUpdated(msg.sender, entityId, nominee);
+ }
+
+ /// @notice Sets the timeout for a specific entity ID
+ /// @param entityId The entity ID to configure
+ /// @param timeout Timeout in seconds
+ function setTimeout(uint32 entityId, uint48 timeout) external {
+ if (!isInitialized(entityId, msg.sender)) revert NotInitialized();
+ if (timeout == 0) revert InvalidTimeout();
+
+ configs[entityId][msg.sender].timeout = timeout;
+ emit TimeoutUpdated(msg.sender, entityId, timeout);
+ }
+
+ /// @notice Checks if the module is initialized for a specific entity ID and account
+ /// @param entityId The entity ID to check
+ /// @param account Address of the account
+ /// @return true if initialized, false otherwise
+ function isInitialized(uint32 entityId, address account) public view returns (bool) {
+ return configs[entityId][account].nominee != address(0);
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ VALIDATION MODULE
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IValidationModule
+ /// @notice Validates a user operation - allows nominee to control account after timeout
+ function validateUserOp(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash)
+ external
+ view
+ override
+ returns (uint256)
+ {
+ DeadmanSwitchConfig memory config = configs[entityId][userOp.sender];
+
+ // If not initialized, validation fails
+ if (config.nominee == address(0)) {
+ return _SIG_VALIDATION_FAILED;
+ }
+
+ // Check if signature is from the nominee
+ bytes32 ethSignedHash = userOpHash.toEthSignedMessageHash();
+ bool sigValid = SignatureChecker.isValidSignatureNow(config.nominee, ethSignedHash, userOp.signature);
+
+ // Calculate when the nominee can act (lastAccess + timeout)
+ uint48 validAfter = config.lastAccess + config.timeout;
+
+ return _packValidationData({
+ sigFailed: !sigValid,
+ validAfter: validAfter,
+ validUntil: type(uint48).max
+ });
+ }
+
+ /// @inheritdoc IValidationModule
+ /// @notice Runtime validation - only allows the account owner during normal operation
+ function validateRuntime(
+ address account,
+ uint32 entityId,
+ address sender,
+ uint256,
+ bytes calldata,
+ bytes calldata
+ ) external view override {
+ DeadmanSwitchConfig memory config = configs[entityId][account];
+
+ // During runtime, only allow if sender is the account itself (self-calls)
+ // or if timeout has passed and sender is the nominee
+ bool timeoutPassed = block.timestamp >= config.lastAccess + config.timeout;
+
+ if (sender != account && !(timeoutPassed && sender == config.nominee)) {
+ revert NotAuthorized();
+ }
+ }
+
+ /// @inheritdoc IValidationModule
+ /// @notice ERC-1271 signature validation
+ /// @dev The signature is valid if signed by the nominee and timeout has passed
+ function validateSignature(
+ address account,
+ uint32 entityId,
+ address,
+ bytes32 digest,
+ bytes calldata signature
+ ) external view override returns (bytes4) {
+ DeadmanSwitchConfig memory config = configs[entityId][account];
+
+ if (config.nominee == address(0)) {
+ return _1271_INVALID;
+ }
+
+ // Only allow ERC-1271 signatures after timeout
+ bool timeoutPassed = block.timestamp >= config.lastAccess + config.timeout;
+ if (!timeoutPassed) {
+ return _1271_INVALID;
+ }
+
+ if (SignatureChecker.isValidSignatureNow(config.nominee, digest, signature)) {
+ return _1271_MAGIC_VALUE;
+ }
+
+ return _1271_INVALID;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ EXECUTION HOOK MODULE
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IExecutionHookModule
+ /// @notice Pre-execution hook that updates the last access time
+ function preExecutionHook(uint32 entityId, address, uint256, bytes calldata)
+ external
+ override
+ returns (bytes memory)
+ {
+ // Update last access time if module is initialized
+ if (isInitialized(entityId, msg.sender)) {
+ configs[entityId][msg.sender].lastAccess = uint48(block.timestamp);
+ emit LastAccessUpdated(msg.sender, entityId, uint48(block.timestamp));
+ }
+
+ return "";
+ }
+
+ /// @inheritdoc IExecutionHookModule
+ /// @notice Post-execution hook (unused)
+ function postExecutionHook(uint32, bytes calldata) external pure override {
+ revert NotImplemented();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ MODULE METADATA
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /// @inheritdoc IModule
+ function moduleId() external pure returns (string memory) {
+ return "rhinestone.deadman-switch-module.1.0.0";
+ }
+
+ /// @inheritdoc IERC165
+ function supportsInterface(bytes4 interfaceId)
+ public
+ view
+ virtual
+ override(ModuleBase, IERC165)
+ returns (bool)
+ {
+ return interfaceId == type(IValidationModule).interfaceId
+ || interfaceId == type(IExecutionHookModule).interfaceId
+ || super.supportsInterface(interfaceId);
+ }
+}
\ No newline at end of file