Account Abstraction

Unlike Externally Owned Accounts (EOAs), smart contracts may contain arbitrary verification logic based on authentication mechanisms different to Ethereum’s native ECDSA and have execution advantages such as batching or gas sponsorship. To leverage these properties of smart contracts, the community has widely adopted ERC-4337, a standard to process user operations through an alternative mempool.

The library provides multiple contracts for Account Abstraction following this standard as it enables more flexible and user-friendly interactions with applications. Account Abstraction use cases include wallets in novel contexts (e.g. embedded wallets), more granular configuration of accounts, and recovery mechanisms.

These capabilities can be supercharged with a modularity approach following standards such as ERC-7579 or ERC-6909.

Smart Accounts

OpenZeppelin provides an abstract AccountCore contract that implements the basic logic to handle user operations in compliance with ERC-4337. Developers who want to build their own account can use this to bootstrap.

User operations are validated using an AbstractSigner, which requires to implement the internal _rawSignatureValidation function. This is the lowest-level signature validation layer and is used to wrap other validation methods like the Account’s validateUserOp.

A more opinionated version is the Account contract, which also inherits from:

The Account doesn’t include an execution mechanism. Using ERC7821 is a recommended solution with the minimal logic to batch multiple calls in a single execution. This is useful to execute multiple calls within a single user operation (e.g. approve and transfer).
// contracts/MyAccount.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {Account} from "@openzeppelin/community-contracts/account/Account.sol"; // or AccountCore
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";

contract MyAccount is Account, ERC7821, Initializable {
    /**
     * NOTE: EIP-712 domain is set at construction because each account clone
     * will recalculate its domain separator based on their own address.
     */
    constructor() EIP712("MyAccount", "1") {}

    /// @dev Signature validation logic.
    function _rawSignatureValidation(
        bytes32 hash,
        bytes calldata signature
    ) internal view virtual override returns (bool) {
        // Custom validation logic
    }

    function initializeSigner() public initializer {
        // Most accounts will require some form of signer initialization logic
    }

    /// @dev Allows the entry point as an authorized executor.
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}

Setting up an account

To setup an account, you can either bring your own validation logic and start with Account or AccountCore, or import any of the predefined signers that can be used to control an account.

Selecting a signer

The library includes specializations of the AbstractSigner contract that use custom digital signature verification algorithms. These are SignerECDSA, SignerP256 and SignerRSA.

Since smart accounts are deployed by a factory, the best practice is to create minimal clones of initializable contracts. These signer implementations provide an initializable design by default so that the factory can interact with the account to set it up after deployment in a single transaction.

Leaving an account uninitialized may leave it unusable since no public key was associated with it.
// contracts/MyAccount.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
import {SignerECDSA} from "@openzeppelin/community-contracts/utils/cryptography/SignerECDSA.sol";

contract MyAccountECDSA is Account, SignerECDSA, ERC7821 {
    constructor() EIP712("MyAccountECDSA", "1") {}

    function initializeSigner(address signerAddr) public virtual {
        // Will revert if the signer is already initialized
        _initializeSigner(signerAddr);
    }

    /// @dev Allows the entry point as an authorized executor.
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}
Account initializes EIP712 to generate a domain separator that prevents replayability in other accounts controlled by the same key. See ERC-7739 signatures

Along with the regular EOA signature verification, the library also provides the SignerP256 for P256 signatures, a widely used elliptic curve verification algorithm that’s present in mobile device security enclaves, FIDO keys, and corporate environments (i.e. public key infrastructures).

// contracts/MyAccount.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
import {SignerP256} from "@openzeppelin/community-contracts/utils/cryptography/SignerP256.sol";

contract MyAccountP256 is Account, SignerP256, ERC7821 {
    constructor() EIP712("MyAccountP256", "1") {}

    function initializeSigner(bytes32 qx, bytes32 qy) public virtual {
        // Will revert if the signer is already initialized
        _initializeSigner(qx, qy);
    }

    /// @dev Allows the entry point as an authorized executor.
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}

Similarly, some government and corporate public key infrastructures use RSA for signature verification. For those cases, the AccountRSA may be a good fit.

// contracts/MyAccount.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
import {SignerRSA} from "@openzeppelin/community-contracts/utils/cryptography/SignerRSA.sol";

contract MyAccountRSA is Account, SignerRSA, ERC7821 {
    constructor() EIP712("MyAccountRSA", "1") {}

    function initializeSigner(bytes memory e, bytes memory n) public virtual {
        // Will revert if the signer is already initialized
        _initializeSigner(e, n);
    }

    /// @dev Allows the entry point as an authorized executor.
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}

Account Factory

The first time a user sends an user operation, the account will be created deterministically (i.e. its code and address can be predicted) using the the initCode field in the UserOperation. This field contains both the address of a smart contract (the factory) and the data required to call it and deploy the smart account.

For this purpose, developers can create an account factory using the Clones library from OpenZeppelin Contracts. It exposes methods to calculate the address of an account before deployment.

// contracts/MyFactoryAccount.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {MyAccountECDSA} from "./MyAccountECDSA.sol";

/**
 * @dev A factory contract to create ECDSA accounts on demand.
 */
contract MyFactoryAccount {
    using Clones for address;

    address private immutable _impl = address(new MyAccountECDSA());

    /// @dev Predict the address of the account
    function predictAddress(bytes32 salt) public view returns (address) {
        return _impl.predictDeterministicAddress(salt, address(this));
    }

    /// @dev Create clone accounts on demand
    function cloneAndInitialize(bytes32 salt, address signer) public returns (address) {
        return _cloneAndInitialize(salt, signer);
    }

    /// @dev Create clone accounts on demand and return the address. Uses `signer` to initialize the clone.
    function _cloneAndInitialize(bytes32 salt, address signer) internal returns (address) {
        // Scope salt to the signer to avoid front-running the salt with a different signer
        bytes32 _signerSalt = keccak256(abi.encodePacked(salt, signer));

        address predicted = predictAddress(_signerSalt);
        if (predicted.code.length == 0) {
            _impl.cloneDeterministic(_signerSalt);
            MyAccountECDSA(payable(predicted)).initializeSigner(signer);
        }
        return predicted;
    }
}

You’ve setup your own account and its corresponding factory. Both are ready to be used with ERC-4337 infrastructure. Customizing the factory to other validation mechanisms must be straightforward.

ERC-4337 Overview

The ERC-4337 is a detailed specification of how to implement the necessary logic to handle operations without making changes to the protocol level (i.e. the rules of the blockchain itself). This specification defines the following components:

UserOperation

A UserOperation is a higher-layer pseudo-transaction object that represents the intent of the account. This shares some similarities with regular EVM transactions like the concept of gasFees or callData but includes fields that enable new capabilities.

struct PackedUserOperation {
    address sender;
    uint256 nonce;
    bytes initCode; // concatenation of factory address and factoryData (or empty)
    bytes callData;
    bytes32 accountGasLimits; // concatenation of verificationGas (16 bytes) and callGas (16 bytes)
    uint256 preVerificationGas;
    bytes32 gasFees; // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes)
    bytes paymasterAndData; // concatenation of paymaster fields (or empty)
    bytes signature;
}

Entrypoint

Each UserOperation is executed through a contract known as the EntryPoint. This contract is a singleton deployed across multiple networks at the same address although other custom implementations may be used.

The Entrypoint contracts is considered a trusted entity by the account.

Bundlers

The bundler is a piece of offchain infrastructure that is in charge of processing an alternative mempool of user operations. Bundlers themselves call the Entrypoint contract’s handleOps function with an array of UserOperations that are executed and included in a block.

During the process, the bundler pays for the gas of executing the transaction and gets refunded during the execution phase of the Entrypoint contract.

Account Contract

The Account Contract is a smart contract that implements the logic required to validate a UserOperation in the context of ERC-4337. Any smart contract account should conform with the IAccount interface to validate operations.

interface IAccount {
    function validateUserOp(PackedUserOperation calldata, bytes32, uint256) external returns (uint256 validationData);
}

Similarly, an Account should have a way to execute these operations by either handling arbitrary calldata on its fallback or implementing the IAccountExecute interface:

interface IAccountExecute {
    function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external;
}
The IAccountExecute interface is optional. Developers might want to use AccountERC7821 for a minimal batched execution interface or rely on ERC-7579, ERC-6909 or any other execution logic.

To build your own account, see Smart Accounts.

Factory Contract

The smart contract accounts are created by a Factory contract defined by the Account developer. This factory receives arbitrary bytes as initData and returns an address where the logic of the account is deployed.

To build your own factory, see Account Factory

Paymaster Contract

A Paymaster is an optional entity that can sponsor gas fees for Accounts, or allow them to pay for those fees in ERC-20 instead of native currency. This abstracts gas away of the user experience in the same way that computational costs of cloud servers are abstracted away from end-users.

Further notes

ERC-7739 Signatures

A common security practice to prevent user operation replayability across smart contract accounts controlled by the same private key (i.e. multiple accounts for the same signer) is to link the signature to the address and chainId of the account. This can be done by asking the user to sign a hash that includes these values.

The problem with this approach is that the user might be prompted by the wallet provider to sign an obfuscated message, which is a phishing vector that may lead to a user losing its assets.

To prevent this, developers may use ERC7739Signer, a utility that implements IERC1271 for smart contract signatures with a defensive rehashing mechanism based on a nested EIP-712 approach to wrap the signature request in a context where there’s clearer information for the end user.

ERC-7562 Validation Rules

To process a bundle of UserOperations, bundlers call validateUserOp on each operation sender to check whether the operation can be executed. However, the bundler has no guarantee that the state of the blockchain will remain the same after the validation phase. To overcome this problem, ERC-7562 proposes a set of limitations to EVM code so that bundlers (or node operators) are protected from unexpected state changes.

These rules outline the requirements for operations to be processed by the canonical mempool.

Accounts can access its own storage during the validation phase, they might easily violate ERC-7562 storage access rules in undirect ways. For example, most accounts access their public keys from storage when validating a signature, limiting the ability of having accounts that validate operations for other accounts (e.g. via ERC-1271)

Although any Account that breaks such rules may still be processed by a private bundler, developers should keep in mind the centralization tradeoffs of relying on private infrastructure instead of permissionless execution.