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.
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:
-
ERC7739Signer: An implementation of the ERC-1271 interface for smart contract signatures. This layer adds a defensive rehashing mechanism that prevents signatures for this account to be replayed in another account controlled by the same signer. See ERC-7739 signatures.
-
ERC721Holder, ERC1155Holder: Allows the account to hold ERC-721 and ERC-1155 tokens.
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. |