Account Modules

Smart accounts built with ERC-7579 provide a standardized way to extend account functionality through modules (i.e. smart contract instances). This architecture allows accounts to support various features that are compatible with a wide variety of account implementations. See compatible modules.

ERC-7579

ERC-7579 defines a standardized interface for modular smart accounts. This standard enables accounts to install, uninstall, and interact with modules that extend their capabilities in a composable manner with different account implementations.

Accounts

OpenZeppelin offers an implementation of an AccountERC7579 contract that allows installing modules compliant with this standard. There’s also an AccountERC7579Hooked variant that supports installation of hooks. Like most accounts, an instance should define an initializer function where the first module that controls the account will be set:

// contracts/MyAccountERC7579.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
import {AccountERC7579} from "@openzeppelin/community-contracts/account/extensions/AccountERC7579.sol";

contract MyAccountERC7579 is Initializable, AccountERC7579 {
    function initializeAccount(address validator, bytes calldata validatorData) public initializer {
        // Install a validator module to handle signature verification
        _installModule(MODULE_TYPE_VALIDATOR, validator, validatorData);
    }
}
For simplicity, the AccountERC7579Hooked only supports a single hook. A common workaround is to install a single hook with a multiplexer pattern to extend the functionality to multiple hooks.

Modules

Functionality is added to accounts through encapsulated functionality deployed as smart contracts called modules. The standard defines four primary module types:

  • Validator modules (type 1): Handle signature verification and user operation validation

  • Executor modules (type 2): Execute operations on behalf of the account

  • Fallback modules (type 3): Handle fallback calls for specific function selectors

  • Hook modules (type 4): Execute logic before and after operations

Modules can implement multiple types simultaneously, which means you could combine an executor module with hooks to enforce behaviors on an account, such as maintaining ERC-20 approvals or preventing the removal of certain permissions.

Building Custom Modules

The library provides standard composable modules as building blocks with an internal API for developers. By combining these components, you can create a rich set of variants without including unnecessary features.

A good starting point is the ERC7579Executor or ERC7579Validator, which include an opinionated base layer easily combined with other abstract modules. Hooks and fallback handlers are more straightforward to implement directly from interfaces:

// contracts/MyERC7579Modules.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {IERC7579Module, IERC7579Hook} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
import {ERC7579Executor} from "@openzeppelin/community-contracts/account/modules/ERC7579Executor.sol";
import {ERC7579Validator} from "@openzeppelin/community-contracts/account/modules/ERC7579Validator.sol";

// Basic validator module
abstract contract MyERC7579RecoveryValidator is ERC7579Validator {}

// Basic executor module
abstract contract MyERC7579RecoveryExecutor is ERC7579Executor {}

// Basic fallback handler
abstract contract MyERC7579RecoveryFallback is IERC7579Module {}

// Basic hook
abstract contract MyERC7579RecoveryHook is IERC7579Hook {}
Explore these abstract ERC-7579 modules in the API Reference.

Execution Modes

ERC-7579 supports various execution modes, which are encoded as a bytes32 value. The ERC7579Utils library provides utility functions to work with these modes:

// Parts of an execution mode
type Mode is bytes32;
type CallType is bytes1;
type ExecType is bytes1;
type ModeSelector is bytes4;
type ModePayload is bytes22;
Call Types

Call types determine the kind of execution:

Type Value Description

CALLTYPE_SINGLE

0x00

A single call execution

CALLTYPE_BATCH

0x01

A batch of call executions

CALLTYPE_DELEGATECALL

0xFF

A delegatecall execution

Execution Types

Execution types determine how failures are handled:

Type Value Description

EXECTYPE_DEFAULT

0x00

Reverts on failure

EXECTYPE_TRY

0x01

Does not revert on failure, emits an event instead

Execution Data Format

The execution data format varies depending on the call type:

  • For single calls: abi.encodePacked(target, value, callData)

  • For batched calls: abi.encode(Execution[]) where Execution is a struct containing target, value, and callData

  • For delegate calls: abi.encodePacked(target, callData)

Examples

Social Recovery

Social recovery allows an account to be recovered when access is lost by relying on trusted parties ("guardians") who verify the user’s identity and help restore access.

Social recovery is not a single solution but a design space with multiple configuration options:

  • Delay configuration

  • Expiration settings

  • Different guardian types

  • Cancellation windows

  • Confirmation requirements

To support different guardian types, we can leverage ERC-7913 as discussed in the multisig section. For ERC-7579 modules, this is implemented through the ERC7579Multisig validator.

Combined with an ERC7579Executor, it provides a basic foundation that can be extended with more sophisticated features:

// contracts/MyERC7579SocialRecovery.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC7579Executor} from "@openzeppelin/community-contracts/account/modules/ERC7579Executor.sol";
import {ERC7579Validator} from "@openzeppelin/community-contracts/account/modules/ERC7579Validator.sol";
import {ERC7579Multisig} from "@openzeppelin/community-contracts/account/modules/ERC7579Multisig.sol";

abstract contract MyERC7579SocialRecovery is EIP712, ERC7579Executor, ERC7579Multisig, Nonces {
    bytes32 private constant RECOVER_TYPEHASH =
        keccak256("Recover(address account,bytes32 salt,uint256 nonce,bytes32 mode,bytes executionCalldata)");

    function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
        return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
    }

    // Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
    function _validateExecution(
        address account,
        bytes32 salt,
        bytes32 mode,
        bytes calldata data
    ) internal override returns (bytes calldata) {
        uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
        bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
        bytes calldata signature = data[2 + executionCalldataLength:]; // Remaining bytes are the signature
        require(_rawERC7579Validation(account, _getExecuteTypeHash(account, salt, mode, executionCalldata), signature));
        return executionCalldata;
    }

    function _getExecuteTypeHash(
        address account,
        bytes32 salt,
        bytes32 mode,
        bytes calldata executionCalldata
    ) internal returns (bytes32) {
        return
            _hashTypedDataV4(
                keccak256(abi.encode(RECOVER_TYPEHASH, account, salt, _useNonce(account), mode, executionCalldata))
            );
    }
}

For enhanced security, you can extend this foundation with scheduling, delays, and cancellations using ERC7579DelayedExecutor. This allows guardians to schedule recovery operations with a time delay, providing a security window to detect and cancel suspicious recovery attempts before they execute:

// contracts/MyERC7579DelayedSocialRecovery.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC7579Executor} from "@openzeppelin/community-contracts/account/modules/ERC7579Executor.sol";
import {ERC7579Validator} from "@openzeppelin/community-contracts/account/modules/ERC7579Validator.sol";
import {Calldata} from "@openzeppelin/contracts/utils/Calldata.sol";
import {ERC7579DelayedExecutor} from "@openzeppelin/community-contracts/account/modules/ERC7579DelayedExecutor.sol";
import {ERC7579Multisig} from "@openzeppelin/community-contracts/account/modules/ERC7579Multisig.sol";

abstract contract MyERC7579DelayedSocialRecovery is EIP712, ERC7579DelayedExecutor, ERC7579Multisig {
    bytes32 private constant RECOVER_TYPEHASH =
        keccak256("Recover(address account,bytes32 salt,bytes32 mode,bytes executionCalldata)");

    function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
        return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
    }

    // Data encoding: [uint16(executorArgsLength), executorArgs, uint16(multisigArgsLength), multisigArgs]
    function onInstall(bytes calldata data) public override(ERC7579DelayedExecutor, ERC7579Multisig) {
        uint16 executorArgsLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
        bytes calldata executorArgs = data[2:2 + executorArgsLength]; // Next bytes are the args
        uint16 multisigArgsLength = uint16(bytes2(data[2 + executorArgsLength:4 + executorArgsLength])); // Next 2 bytes are the length
        bytes calldata multisigArgs = data[4 + executorArgsLength:4 + executorArgsLength + multisigArgsLength]; // Next bytes are the args

        ERC7579DelayedExecutor.onInstall(executorArgs);
        ERC7579Multisig.onInstall(multisigArgs);
    }

    function onUninstall(bytes calldata) public override(ERC7579DelayedExecutor, ERC7579Multisig) {
        ERC7579DelayedExecutor.onUninstall(Calldata.emptyBytes());
        ERC7579Multisig.onUninstall(Calldata.emptyBytes());
    }

    // Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
    function _validateSchedule(
        address account,
        bytes32 salt,
        bytes32 mode,
        bytes calldata data
    ) internal view override {
        uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
        bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
        bytes calldata signature = data[2 + executionCalldataLength:]; // Remaining bytes are the signature
        require(_rawERC7579Validation(account, _getExecuteTypeHash(account, salt, mode, executionCalldata), signature));
    }

    function _getExecuteTypeHash(
        address account,
        bytes32 salt,
        bytes32 mode,
        bytes calldata executionCalldata
    ) internal view returns (bytes32) {
        return _hashTypedDataV4(keccak256(abi.encode(RECOVER_TYPEHASH, account, salt, mode, executionCalldata)));
    }
}
The delayed executor’s signature validation doesn’t require a nonce since operations are uniquely identified by their operation id and cannot be scheduled twice.

These implementations demonstrate how to build progressively more secure social recovery mechanisms, from basic multi-signature recovery to time-delayed recovery with cancellation capabilities.

For additional functionality, developers can use: