OpenZeppelin ERC-4337 Account contract Overview

This article is an overview of the OpenZeppelin ERC-4337 Account contract. The content is mainly based on the OpenZeppelin documentation.

[TOC]

Introduction

This is a simple ERC-4337 account implementation that provides only the minimal logic required to process user operations.

Developers are expected to implement the AbstractSigner._rawSignatureValidation function themselves in order to define how the account validates signatures.

The core account does not include any mechanism for executing arbitrary external calls, even though this is an essential feature for most accounts; instead, developers are free to choose and implement their preferred approach, such as ERC-6900, ERC-7579, or ERC-7821.

Implementing signature validation is a security-critical task, as mistakes could allow attackers to bypass the account’s protections, so developers are encouraged to refer to existing implementations like SignerECDSA, SignerP256, or SignerRSA for guidance. The account is designed to be stateless.

import "@openzeppelin/contracts/account/Account.sol";

For a deep dive into ERC-4437, see my article ERC-4337: Account Abstraction Using Alt Mempool

Code

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.5.0) (account/Account.sol)

pragma solidity ^0.8.20;

import {PackedUserOperation, IAccount, IEntryPoint} from "../interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "./utils/draft-ERC4337Utils.sol";
import {AbstractSigner} from "../utils/cryptography/signers/AbstractSigner.sol";
import {LowLevelCall} from "../utils/LowLevelCall.sol";

/**
 * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process
 * user operations.
 *
 * Developers must implement the {AbstractSigner-_rawSignatureValidation} function to define the account's validation logic.
 *
 * NOTE: This core account doesn't include any mechanism for performing arbitrary external calls. This is an essential
 * feature that all Account should have. We leave it up to the developers to implement the mechanism of their choice.
 * Common choices include ERC-6900, ERC-7579 and ERC-7821 (among others).
 *
 * IMPORTANT: Implementing a mechanism to validate signatures is a security-sensitive operation as it may allow an
 * attacker to bypass the account's security measures. Check out {SignerECDSA}, {SignerP256}, or {SignerRSA} for
 * digital signature validation implementations.
 *
 * @custom:stateless
 */
abstract contract Account is AbstractSigner, IAccount {
    /**
     * @dev Unauthorized call to the account.
     */
    error AccountUnauthorized(address sender);

    /**
     * @dev Revert if the caller is not the entry point or the account itself.
     */
    modifier onlyEntryPointOrSelf() {
        _checkEntryPointOrSelf();
        _;
    }

    /**
     * @dev Revert if the caller is not the entry point.
     */
    modifier onlyEntryPoint() {
        _checkEntryPoint();
        _;
    }

    /**
     * @dev Canonical entry point for the account that forwards and validates user operations.
     */
    function entryPoint() public view virtual returns (IEntryPoint) {
        return ERC4337Utils.ENTRYPOINT_V09;
    }

    /**
     * @dev Return the account nonce for the canonical sequence.
     */
    function getNonce() public view virtual returns (uint256) {
        return getNonce(0);
    }

    /**
     * @dev Return the account nonce for a given sequence (key).
     */
    function getNonce(uint192 key) public view virtual returns (uint256) {
        return entryPoint().getNonce(address(this), key);
    }

    /**
     * @inheritdoc IAccount
     */
    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) public virtual onlyEntryPoint returns (uint256) {
        uint256 validationData = _validateUserOp(userOp, userOpHash, userOp.signature);
        _payPrefund(missingAccountFunds);
        return validationData;
    }

    /**
     * @dev Returns the validationData for a given user operation. By default, this checks the signature of the
     * signable hash (produced by {_signableUserOpHash}) using the abstract signer ({AbstractSigner-_rawSignatureValidation}).
     *
     * The `signature` parameter is taken directly from the user operation's `signature` field.
     * This design enables derived contracts to implement custom signature handling logic,
     * such as embedding additional data within the signature and processing it by overriding this function
     * and optionally invoking `super`.
     *
     * NOTE: The userOpHash is assumed to be correct. Calling this function with a userOpHash that does not match the
     * userOp will result in undefined behavior.
     */
    function _validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        bytes calldata signature
    ) internal virtual returns (uint256) {
        return
            _rawSignatureValidation(_signableUserOpHash(userOp, userOpHash), signature)
                ? ERC4337Utils.SIG_VALIDATION_SUCCESS
                : ERC4337Utils.SIG_VALIDATION_FAILED;
    }

    /**
     * @dev Virtual function that returns the signable hash for a user operations. Since v0.8.0 of the entrypoint,
     * `userOpHash` is an EIP-712 hash that can be signed directly.
     */
    function _signableUserOpHash(
        PackedUserOperation calldata /*userOp*/,
        bytes32 userOpHash
    ) internal view virtual returns (bytes32) {
        return userOpHash;
    }

    /**
     * @dev Sends the missing funds for executing the user operation to the {entrypoint}.
     * The `missingAccountFunds` must be defined by the entrypoint when calling {validateUserOp}.
     */
    function _payPrefund(uint256 missingAccountFunds) internal virtual {
        if (missingAccountFunds > 0) {
            LowLevelCall.callNoReturn(msg.sender, missingAccountFunds, ""); // The entrypoint should validate the result.
        }
    }

    /**
     * @dev Ensures the caller is the {entrypoint}.
     */
    function _checkEntryPoint() internal view virtual {
        address sender = msg.sender;
        if (sender != address(entryPoint())) {
            revert AccountUnauthorized(sender);
        }
    }

    /**
     * @dev Ensures the caller is the {entrypoint} or the account itself.
     */
    function _checkEntryPointOrSelf() internal view virtual {
        address sender = msg.sender;
        if (sender != address(this) && sender != address(entryPoint())) {
            revert AccountUnauthorized(sender);
        }
    }

    /**
     * @dev Receive Ether.
     */
    receive() external payable virtual {}
}

Sūrya’s Description Report

Files Description Table

File Name SHA-1 Hash
./account/Account.sol cf8870c32bb09e5d4bbc1be5d73b5768a3fa8380

Contracts Description Table

Contract Type Bases    
Function Name Visibility Mutability Modifiers
         
Account Implementation AbstractSigner, IAccount    
entryPoint Public ❗️   NO❗️
getNonce Public ❗️   NO❗️
getNonce Public ❗️   NO❗️
validateUserOp Public ❗️ 🛑 onlyEntryPoint
_validateUserOp Internal 🔒 🛑  
_signableUserOpHash Internal 🔒    
_payPrefund Internal 🔒 🛑  
_checkEntryPoint Internal 🔒    
_checkEntryPointOrSelf Internal 🔒    
External ❗️ 💵 NO❗️

Legend

Symbol Meaning
🛑 Function can modify state
💵 Function is payable

Mindmap

erc4337-openzeppelin-account-mindmap

Openzeppelin documentation

From the API

Custom annotation: @custom:stateless

Modifiers

onlyEntryPointOrSelf()

Visibility: internal

Reverts if the caller is not the EntryPoint or the account itself.


onlyEntryPoint()

Visibility: internal

Reverts if the caller is not the EntryPoint.


Functions

Public Functions

entryPoint() → IEntryPoint

Returns the canonical EntryPoint for the account.


getNonce() → uint256

Returns the account nonce for the canonical sequence.


getNonce(uint192 key) → uint256

Returns the account nonce for a given sequence key.


`validateUserOp(PackedUserOperation userOp,

bytes32 userOpHash, uint256 missingAccountFunds) → uint256`

Validates a user operation.

Requirements

  • MUST validate the caller is a trusted EntryPoint
  • MUST validate the signature against userOpHash
  • SHOULD return SIG_VALIDATION_FAILED (not revert) on signature mismatch
  • MUST revert on any other error
  • MUST pay the EntryPoint at least missingAccountFunds

Return value

Encoded validationData containing:

  • authorizer (address)
    • 0 → success
    • 1 → failure
    • otherwise → authorizer contract
  • validUntil (uint48) — expiration time (0 = infinite)
  • validAfter (uint48) — earliest valid time

Internal Functions

_validateUserOp(PackedUserOperation userOp,bytes32 userOpHash,bytes signature) → uint256

Returns validation data for a user operation.

  • Verifies the signature using AbstractSigner._rawSignatureValidation
  • Uses the signable hash from _signableUserOpHash
  • signature is taken directly from userOp.signature
  • Can be overridden to support custom signature formats

⚠️ userOpHash is assumed to be correct. Passing a mismatched hash results in undefined behavior.


_signableUserOpHash(PackedUserOperation userOp,bytes32 userOpHash) → bytes32

Mutability: virtual

Returns the hash that should be signed for a user operation.

Since EntryPoint v0.8.0, userOpHash is already an EIP-712 hash and can be signed directly.


_payPrefund(uint256 missingAccountFunds)

Transfers missing funds to the EntryPoint to cover execution costs.


_checkEntryPoint()

Ensures the caller is the EntryPoint.


_checkEntryPointOrSelf()

Ensures the caller is the EntryPoint or the account itself.


External Functions

receive()

Allows the contract to receive Ether.


Errors

AccountUnauthorized(address sender)

Thrown when an unauthorized caller attempts to access restricted account functionality.


  • IAccount
  • AbstractSigner
    • _rawSignatureValidation(bytes32 hash, bytes signature)

You might also enjoy