OpenZeppelin ERC-4337 Account contract Overview
- Introduction
- Sūrya’s Description Report
- Modifiers
- Functions
- Errors
- Related Interfaces
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

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→ success1→ 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 signatureis taken directly fromuserOp.signature- Can be overridden to support custom signature formats
⚠️
userOpHashis 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.
Related Interfaces
IAccountAbstractSigner_rawSignatureValidation(bytes32 hash, bytes signature)