Skip to content

Commit 1fd395e

Browse files
ernestognwAmxxarr00
authored
Add ERC4337 Accounts docs (#40)
Co-authored-by: Hadrien Croubois <[email protected]> Co-authored-by: Arr00 <[email protected]>
1 parent 97e8c42 commit 1fd395e

File tree

8 files changed

+275
-32
lines changed

8 files changed

+275
-32
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// contracts/MyAccount.sol
2+
// SPDX-License-Identifier: MIT
3+
4+
pragma solidity ^0.8.20;
5+
6+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7+
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
8+
import {Account} from "../../../account/Account.sol"; // or AccountCore
9+
10+
contract MyAccount is Account, Initializable {
11+
/**
12+
* NOTE: EIP-712 domain is set at construction because each account clone
13+
* will recalculate its domain separator based on their own address.
14+
*/
15+
constructor() EIP712("MyAccount", "1") {}
16+
17+
/// @dev Signature validation logic.
18+
function _rawSignatureValidation(
19+
bytes32 hash,
20+
bytes calldata signature
21+
) internal view virtual override returns (bool) {
22+
// Custom validation logic
23+
}
24+
25+
function initializeSigner() public initializer {
26+
// Most accounts will require some form of signer initialization logic
27+
}
28+
}

contracts/mocks/docs/account/MyAccountCustom.sol

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// contracts/MyAccount.sol
2+
// SPDX-License-Identifier: MIT
3+
4+
pragma solidity ^0.8.20;
5+
6+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7+
import {Account} from "../../../account/Account.sol";
8+
import {SignerECDSA} from "../../../utils/cryptography/SignerECDSA.sol";
9+
10+
contract MyAccountECDSA is Account, SignerECDSA {
11+
constructor() EIP712("MyAccountECDSA", "1") {}
12+
13+
function initializeSigner(address signerAddr) public virtual {
14+
// Will revert if the signer is already initialized
15+
_initializeSigner(signerAddr);
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// contracts/MyAccount.sol
2+
// SPDX-License-Identifier: MIT
3+
4+
pragma solidity ^0.8.20;
5+
6+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7+
import {Account} from "../../../account/Account.sol";
8+
import {SignerP256} from "../../../utils/cryptography/SignerP256.sol";
9+
10+
contract MyAccountP256 is Account, SignerP256 {
11+
constructor() EIP712("MyAccountP256", "1") {}
12+
13+
function initializeSigner(bytes32 qx, bytes32 qy) public virtual {
14+
// Will revert if the signer is already initialized
15+
_initializeSigner(qx, qy);
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// contracts/MyAccount.sol
2+
// SPDX-License-Identifier: MIT
3+
4+
pragma solidity ^0.8.20;
5+
6+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7+
import {Account} from "../../../account/Account.sol";
8+
import {SignerRSA} from "../../../utils/cryptography/SignerRSA.sol";
9+
10+
contract MyAccountRSA is Account, SignerRSA {
11+
constructor() EIP712("MyAccountRSA", "1") {}
12+
13+
function initializeSigner(bytes memory e, bytes memory n) public virtual {
14+
// Will revert if the signer is already initialized
15+
_initializeSigner(e, n);
16+
}
17+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// contracts/MyFactoryAccount.sol
2+
// SPDX-License-Identifier: MIT
3+
4+
pragma solidity ^0.8.20;
5+
6+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
7+
import {MyAccountECDSA} from "./MyAccountECDSA.sol";
8+
9+
/**
10+
* @dev A factory contract to create ECDSA accounts on demand.
11+
*/
12+
contract MyFactoryAccount {
13+
using Clones for address;
14+
15+
address private immutable _impl = address(new MyAccountECDSA());
16+
17+
/// @dev Predict the address of the account
18+
function predictAddress(bytes32 salt) public view returns (address) {
19+
return _impl.predictDeterministicAddress(salt, address(this));
20+
}
21+
22+
/// @dev Create clone accounts on demand
23+
function cloneAndInitialize(bytes32 salt, address signer) public returns (address) {
24+
return _cloneAndInitialize(salt, signer);
25+
}
26+
27+
/// @dev Create clone accounts on demand and return the address. Uses `signer` to initialize the clone.
28+
function _cloneAndInitialize(bytes32 salt, address signer) internal returns (address) {
29+
// Scope salt to the signer to avoid front-running the salt with a different signer
30+
bytes32 _signerSalt = keccak256(abi.encodePacked(salt, signer));
31+
32+
address predicted = predictAddress(_signerSalt);
33+
if (predicted.code.length == 0) {
34+
_impl.cloneDeterministic(_signerSalt);
35+
MyAccountECDSA(payable(predicted)).initializeSigner(signer);
36+
}
37+
return predicted;
38+
}
39+
}

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
* xref:index.adoc[Overview]
2+
* xref:account-abstraction.adoc[Account Abstraction]
23
* xref:utilities.adoc[Utilities]
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
= Account Abstraction
2+
3+
Unlike Externally Owned Accounts (EOAs), smart contracts may contain arbitrary verification logic based on authentication mechanisms different to Ethereum's native xref:api:utils.adoc#ECDSA[ECDSA] and have execution advantages such as batching or gas sponsorship. To leverage these properties of smart contracts, the community has widely adopted https://eips.ethereum.org/EIPS/eip-4337[ERC-4337], a standard to process user operations through an alternative mempool.
4+
5+
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.
6+
7+
These capabilities can be supercharged with a modularity approach following standards such as https://eips.ethereum.org/EIPS/eip-7579[ERC-7579] or https://eips.ethereum.org/EIPS/eip-6909[ERC-6909].
8+
9+
== Smart Accounts
10+
11+
OpenZeppelin provides an abstract xref:api:account.adoc#AccountCore[`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.
12+
13+
User operations are validated using an xref:api:utils.adoc#AbstractSigner[`AbstractSigner`], which requires to implement the internal xref:api:utils.adoc#AbstractSigner-_rawSignatureValidation[`_rawSignatureValidation`] function. This is the lowest-level signature validation layer and is used to wrap other validation methods like the Account's xref:api:account.adoc#AccountCore-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`validateUserOp`].
14+
15+
A more opinionated version is the xref:api:account.adoc#Account[`Account`] contract, which also inherits from:
16+
17+
* xref:api:utils.adoc#ERC7739Signer[ERC7739Signer]: An implementation of the https://eips.ethereum.org/EIPS/eip-1271[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 xref:account-abstraction.adoc#erc7739_signatures[ERC-7739 signatures].
18+
* https://docs.openzeppelin.com/contracts/api/token/erc721#AccountERC7821[AccountERC7821]: An extension that provides the minimal logic batch multiple calls in a single execution. Useful to execute multiple operations within a single user operation.
19+
* https://docs.openzeppelin.com/contracts/api/token/erc721#ERC721Holder[ERC721Holder], https://docs.openzeppelin.com/contracts/api/token/erc1155#ERC1155Holder[ERC1155Holder]: Allows the account to hold https://eips.ethereum.org/EIPS/eip-721[ERC-721] and https://eips.ethereum.org/EIPS/eip-1155[ERC-1155] tokens.
20+
21+
[source,solidity]
22+
----
23+
include::api:example$account/MyAccount.sol[]
24+
----
25+
26+
=== Setting up an account
27+
28+
To setup an account, you can either bring your own validation logic and start with xref:api:account.adoc#Account[`Account`] or xref:api:account.adoc#AccountCore[`AccountCore`], or import any of the predefined signers that can be used to control an account.
29+
30+
=== Selecting a signer
31+
32+
The library includes specializations of the `AbstractSigner` contract that use custom digital signature verification algorithms. These are xref:api:utils.adoc#SignerECDSA[`SignerECDSA`], xref:api:utils.adoc#SignerP256[`SignerP256`] and xref:api:utils.adoc#SignerRSA[`SignerRSA`].
33+
34+
Since smart accounts are deployed by a factory, the best practice is to create https://docs.openzeppelin.com/contracts/5.x/api/proxy#minimal_clones[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.
35+
36+
WARNING: Leaving an account uninitialized may leave it unusable since no public key was associated with it.
37+
38+
[source,solidity]
39+
----
40+
include::api:example$account/MyAccountECDSA.sol[]
41+
----
42+
43+
NOTE: xref:api:account.adoc#Account[`Account`] initializes xref:api:utils.adoc#EIP712[`EIP712`] to generate a domain separator that prevents replayability in other accounts controlled by the same key. See xref:account-abstraction.adoc#erc7739_signatures[ERC-7739 signatures]
44+
45+
Along with the regular EOA signature verification, the library also provides the xref:api:utils.adoc#SignerP256[`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).
46+
47+
[source,solidity]
48+
----
49+
include::api:example$account/MyAccountP256.sol[]
50+
----
51+
52+
Similarly, some government and corporate public key infrastructures use RSA for signature verification. For those cases, the xref:api:account.adoc#AccountRSA[`AccountRSA`] may be a good fit.
53+
54+
[source,solidity]
55+
----
56+
include::api:example$account/MyAccountRSA.sol[]
57+
----
58+
59+
== Account Factory
60+
61+
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.
62+
63+
For this purpose, developers can create an account factory using the https://docs.openzeppelin.com/contracts/5.x/api/proxy#Clones[Clones library from OpenZeppelin Contracts]. It exposes methods to calculate the address of an account before deployment.
64+
65+
[source,solidity]
66+
----
67+
include::api:example$account/MyFactoryAccount.sol[]
68+
----
69+
70+
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.
71+
72+
== ERC-4337 Overview
73+
74+
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:
75+
76+
=== UserOperation
77+
78+
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.
79+
80+
```solidity
81+
struct PackedUserOperation {
82+
address sender;
83+
uint256 nonce;
84+
bytes initCode; // concatenation of factory address and factoryData (or empty)
85+
bytes callData;
86+
bytes32 accountGasLimits; // concatenation of verificationGas (16 bytes) and callGas (16 bytes)
87+
uint256 preVerificationGas;
88+
bytes32 gasFees; // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes)
89+
bytes paymasterAndData; // concatenation of paymaster fields (or empty)
90+
bytes signature;
91+
}
92+
```
93+
94+
=== Entrypoint
95+
96+
Each `UserOperation` is executed through a contract known as the https://etherscan.io/address/0x0000000071727de22e5e9d8baf0edac6f37da032#code[`EntryPoint`]. This contract is a singleton deployed across multiple networks at the same address although other custom implementations may be used.
97+
98+
The Entrypoint contracts is considered a trusted entity by the account.
99+
100+
=== Bundlers
101+
102+
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.
103+
104+
During the process, the bundler pays for the gas of executing the transaction and gets refunded during the execution phase of the Entrypoint contract.
105+
106+
=== Account Contract
107+
108+
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.
109+
110+
```solidity
111+
interface IAccount {
112+
function validateUserOp(PackedUserOperation calldata, bytes32, uint256) external returns (uint256 validationData);
113+
}
114+
```
115+
116+
Similarly, an Account should have a way to execute these operations by either handling arbitrary calldata on its `fallback` or implementing the `IAccountExecute` interface:
117+
118+
```solidity
119+
interface IAccountExecute {
120+
function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external;
121+
}
122+
```
123+
124+
NOTE: The `IAccountExecute` interface is optional. Developers might want to use xref:api:account.adoc#AccountERC7821[`AccountERC7821`] for a minimal batched execution interface or rely on ERC-7579, ERC-6909 or any other execution logic.
125+
126+
To build your own account, see xref:account-abstraction.adoc#smart_accounts[Smart Accounts].
127+
128+
=== Factory Contract
129+
130+
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.
131+
132+
To build your own factory, see xref:account-abstraction.adoc#account_factory[Account Factory]
133+
134+
=== Paymaster Contract
135+
136+
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.
137+
138+
== Further notes
139+
140+
=== ERC-7739 Signatures
141+
142+
A common security practice to prevent user operation https://mirror.xyz/curiousapple.eth/pFqAdW2LiJ-6S4sg_u1z08k4vK6BCJ33LcyXpnNb8yU[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.
143+
144+
The problem with this approach is that the user might be prompted by the wallet provider to sign an https://x.com/howydev/status/1780353754333634738[obfuscated message], which is a phishing vector that may lead to a user losing its assets.
145+
146+
To prevent this, developers may use xref:api:account#ERC7739Signer[`ERC7739Signer`], a utility that implements xref:api:interfaces#IERC1271[`IERC1271`] for smart contract signatures with a defensive rehashing mechanism based on a https://github.com/frangio/eip712-wrapper-for-eip1271[nested EIP-712 approach] to wrap the signature request in a context where there's clearer information for the end user.
147+
148+
=== ERC-7562 Validation Rules
149+
150+
To process a bundle of `UserOperations`, bundlers call xref:api:account.adoc#AccountCore-validateUserOp-struct-PackedUserOperation-bytes32-uint256-[`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, https://eips.ethereum.org/EIPS/eip-7562[ERC-7562] proposes a set of limitations to EVM code so that bundlers (or node operators) are protected from unexpected state changes.
151+
152+
These rules outline the requirements for operations to be processed by the canonical mempool.
153+
154+
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)
155+
156+
TIP: 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.

0 commit comments

Comments
 (0)