Account Abstraction Using Alt Mempool
相关视频
正文
Abstract
An account abstraction proposal which completely avoids the need for consensus-layer protocol changes. Instead of adding new protocol features and changing the bottom-layer transaction type, this proposal instead introduces a higher-layer pseudo-transaction object called a UserOperation
. Users send UserOperation
objects into a separate mempool. A special class of actor called bundlers package up a set of these objects into a transaction making a handleOps
call to a special contract, and that transaction then gets included in a block.
Motivation
See also https://ethereum-magicians.org/t/implementing-account-abstraction-as-part-of-eth1-x/4020
and the links therein for historical work and motivation, and EIP-2938 for a consensus layer proposal for implementing the same goal.
This proposal takes a different approach, avoiding any adjustments to the consensus layer. It seeks to achieve the following goals:
- Achieve the key goal of Account Abstraction: allow users to use Smart Contract Accounts containing arbitrary verification logic instead of EOAs as their primary account. Completely remove any need at all for users to also have EOAs, as required by both status quo Smart Contract Accounts and EIP-7702.
- Decentralization
- Allow any bundler (think: block builder) to participate in the process of including account-abstracted
UserOperations
- Work with all activity happening over a public mempool; users do not need to know the direct communication addresses (eg. IP, onion) of any specific actors
- Avoid trust assumptions on bundlers
- Allow any bundler (think: block builder) to participate in the process of including account-abstracted
- Do not require any Ethereum consensus changes: Ethereum consensus layer development is focusing on scalability-oriented features, and there may not be any opportunity for further protocol changes for a long time. Hence, to increase the chance of faster adoption, this proposal avoids Ethereum consensus changes.
- Support other use cases
- Privacy-preserving applications
- Atomic multi-operations (similar goal to EIP-7702)
- Pay tx fees with ERC-20 tokens, allow developers to pay fees for their users, and EIP-7702-like sponsored transaction use cases more generally
- abstracting the validation allows the contract to use different signature schemes, multisig configuration, custom recovery, and more.
- abstracting gas payments allows easy onboarding by 3rd party payments, paying with tokens, cross-chain gas payments
- abstracting execution allows batch transactions
Specification
Definitions
- UserOperation - a structure that describes a transaction to be sent on behalf of a user. To avoid confusion, it is not named "transaction".
- Like a transaction, it contains
to
,calldata
,maxFeePerGas
,maxPriorityFeePerGas
,nonce
,signature
. - Unlike a transaction, it contains several other fields, described below.
- Notably, the
signature
field usage is not defined by the protocol, but by the Smart Contract Account implementation.
- Like a transaction, it contains
- Sender - the Smart Contract Account sending a
UserOperation
. - EntryPoint - a singleton contract to execute bundles of
UserOperations
. Bundlers MUST whitelist the supportedEntryPoint
. - Bundler - a node (block builder) that can handle
UserOperations
, create a validentryPoint.handleOps()
transaction, and add it to the block while it is still valid. This can be achieved by a number of ways:- Bundler can act as a block builder itself.
- If the bundler is not a block builder, it MUST work with the block building infrastructure such as
mev-boost
or other kind of proposer-builder separation, such as EIP-7732. - The
bundler
can also rely on an experimentaleth_sendRawTransactionConditional
RPC API defined in ERC-7796 if it is available.
- Paymaster - a helper contract that agrees to pay for the transaction, instead of the sender itself.
- Factory - a helper contract that performs a deployment for a new
sender
contract if necessary. - Aggregator - also known as "authorizer contract" - a contract that enables multiple
UserOperations
to share a single validation, fully defined in ERC-7766. - Canonical UserOperation mempool - a decentralized permissionless P2P network where bundlers exchange
UserOperations
that are valid and conform with ERC-7562. - Alternative UserOperation mempool - any other P2P mempool where the validity of
UserOperations
is determined by rules that are different from ERC-7562 in any way. - Deposit - an amount of Ether (or any L2 native currency) that a
Sender
orPaymaster
contract has transferred to theEntryPoint
contract intended to pay gas costs of the futureUserOperations
.
UserOperation
To avoid Ethereum consensus changes, we do not attempt to create new transaction types for account-abstracted transactions. Instead, users package up the action they want their Smart Contract Account to take in a struct named UserOperation
:
Field | Type | Description |
---|---|---|
sender | address | The Account making the UserOperation |
nonce | uint256 | Anti-replay parameter (see "Semi-abstracted Nonce Support" ) |
factory | address | Account Factory for new Accounts OR 0x7702 flag for EIP-7702 Accounts, otherwise address(0) |
factoryData | bytes | data for the Account Factory if factory is provided OR EIP-7702 initialization data, or empty array |
callData | bytes | The data to pass to the sender during the main execution call |
callGasLimit | uint256 | The amount of gas to allocate the main execution call |
verificationGasLimit | uint256 | The amount of gas to allocate for the verification step |
preVerificationGas | uint256 | Extra gas to pay the bundler |
maxFeePerGas | uint256 | Maximum fee per gas (similar to EIP-1559 max_fee_per_gas ) |
maxPriorityFeePerGas | uint256 | Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas ) |
paymaster | address | Address of paymaster contract, (or empty, if the sender pays for gas by itself) |
paymasterVerificationGasLimit | uint256 | The amount of gas to allocate for the paymaster validation code (only if paymaster exists) |
paymasterPostOpGasLimit | uint256 | The amount of gas to allocate for the paymaster post-operation code (only if paymaster exists) |
paymasterData | bytes | Data for paymaster (only if paymaster exists) |
signature | bytes | Data passed into the sender to verify authorization |
Users send UserOperation
objects to a dedicated UserOperation
mempool.
To prevent replay attacks, either cross-chain or with multiple EntryPoint
contract versions,
the signature
MUST depend on chainid
and the EntryPoint
address.
Note that one EIP-7702 "authorization tuple" value can be provided alongside the UserOperation
struct,
but "authorization tuples" are not included in the UserOperation
itself.
EntryPoint
interface
When passed on-chain, to the EntryPoint
contract, the Account
and the Paymaster
, a "packed" version of the above structure called PackedUserOperation
is used:
Field | Type | Description |
---|---|---|
sender | address | |
nonce | uint256 | |
initCode | bytes | concatenation of factory address and factoryData (or empty) |
callData | bytes | |
accountGasLimits | bytes32 | concatenation of verificationGasLimit (16 bytes) and callGas (16 bytes) |
preVerificationGas | uint256 | |
gasFees | bytes32 | concatenation of maxPriorityFeePerGas (16 bytes) and maxFeePerGas (16 bytes) |
paymasterAndData | bytes | concatenation of paymaster fields (or empty) |
signature | bytes |
The core interface of the EntryPoint
contract is as follows:
function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary);
The beneficiary
is the address that will be paid with all the gas fees collected during the execution of the bundle.
Smart Contract Account Interface
The core interface required for the Smart Contract Account to have is:
interface IAccount { function validateUserOp (PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 validationData); }
The userOpHash
is a hash over the userOp
(except signature
), entryPoint
and chainId
.
The Smart Contract Account:
- MUST validate the caller is a trusted
EntryPoint
- MUST validate that the signature is a valid signature of the
userOpHash
, and SHOULD returnSIG_VALIDATION_FAILED
(1
) without reverting on signature mismatch. Any other error MUST revert. - SHOULD not return early when returning
SIG_VALIDATION_FAILED
(1
). Instead, it SHOULD complete the normal flow to enable performing a gas estimation for the validation function. - MUST pay the
EntryPoint
(caller) at least themissingAccountFunds
(which might be zero, in case the currentsender
's deposit is sufficient) - The
sender
MAY pay more than this minimum to cover future transactions. It can also callwithdrawTo
to retrieve it later at any time. - The return value MUST be packed of
aggregator
/authorizer
,validUntil
andvalidAfter
timestamps.aggregator
/authorizer
- 0 for valid signature, 1 to mark signature failure. Otherwise, an address of anaggregator
/authorizer
contract, as defined in ERC-7766.validUntil
is 6-byte timestamp value, or zero for "infinite". TheUserOperation
is valid only up to this time.validAfter
is 6-byte timestamp. TheUserOperation
is valid only after this time.
The Smart Contract Account MAY implement the interface IAccountExecute
interface IAccountExecute { function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; }
This method will be called by the EntryPoint
with the current UserOperation, instead of executing the callData
itself directly on the sender
.
Semi-abstracted Nonce Support
In Ethereum protocol, the sequential transaction nonce
value is used as a replay protection method as well as to
determine the valid order of transaction being included in blocks.
It also contributes to the transaction hash uniqueness, as a transaction by the same sender with the same nonce may not be included in the chain twice.
However, requiring a single sequential nonce
value is limiting to the senders' ability to define their custom logic
with regard to transaction ordering and replay protection.
Instead of sequential nonce
we implement a nonce mechanism that uses a single uint256
nonce value in the UserOperation
,
but treats it as two values:
- 192-bit "key"
- 64-bit "sequence"
These values are represented on-chain in the EntryPoint
contract.
We define the following method in the EntryPoint
interface to expose these values:
function getNonce(address sender, uint192 key) external view returns (uint256 nonce);
For each key
the sequence
is validated by the EntryPoint
for each UserOperation.
If the nonce validation fails the UserOperation
is considered invalid and the bundle is reverted.
The sequence
value is incremented sequentially and monotonically for the sender
for each UserOperation.
A new key
can be introduced with an arbitrary value at any point, with its sequence
starting at 0
.
This approach maintains the guarantee of UserOperation
hash uniqueness on-chain on the protocol level while allowing
wallets to implement any custom logic they may need operating on a 192-bit "key" field, while fitting the 32 byte word.
Reading and validating the nonce
When preparing the UserOperation
bundlers may make a view call to this method to determine a valid value for the nonce
field.
Bundler's validation of a UserOperation
SHOULD start with getNonce
to ensure the transaction has a valid nonce
field.
If the bundler is willing to accept multiple UserOperations
by the same sender into their mempool,
this bundler is supposed to track the key
and sequence
pair of the UserOperations
already added in the mempool.
Usage examples
-
Classic sequential nonce.
In order to require the wallet to have classic, sequential nonce, the validation function MUST perform:
require(userOp.nonce<type(uint64).max)
-
Ordered administrative events
In some cases, an account may need to have an "administrative" channel of operations running in parallel to normal operations.
In this case, the account may use a specific
key
when calling methods on the account itself:bytes4 sig = bytes4(userOp.callData[0 : 4]); uint key = userOp.nonce >> 64; if (sig == ADMIN_METHODSIG) { require(key == ADMIN_KEY, "wrong nonce-key for admin operation"); } else { require(key == 0, "wrong nonce-key for normal operation"); }
Required EntryPoint
contract functionality
The EntryPoint
method is handleOps
, which handles an array of UserOperations
The EntryPoint
's handleOps
function must perform the following steps (we first describe the simpler non-paymaster case). It must make two loops, the verification loop and the execution loop.
In the verification loop, the handleOps
call must perform the following steps for each UserOperation
:
- Create the
sender
Smart Contract Account if it does not yet exist, using theinitcode
provided in theUserOperation
.- If the
sender
is an EOA with an EIP-7702 authorization designation, theEntryPoint
validates the authorized address matches the one specified in theUserOperation
signature (see Support for [EIP-7702] authorizations). - If the
sender
does not exist, and theinitcode
is empty, or does not deploy a contract at the "sender" address, the call must fail.
- If the
- calculate the maximum possible fee the
sender
needs to pay based on validation and call gas limits, and current gas values. - calculate the fee the
sender
must add to its "deposit" in theEntryPoint
- Call
validateUserOp
on thesender
contract, passing in theUserOperation
, its hash and the required fee. The Smart Contract Account SHOULD verify theUserOperation
's signature, and pay the fee if thesender
considers theUserOperation
valid. If anyvalidateUserOp
call fails,handleOps
must skip execution of at least thatUserOperation
, and may revert entirely. - Validate the account's deposit in the
EntryPoint
is high enough to cover the max possible cost (cover the already-done verification and max execution gas)
In the execution loop, the handleOps
call must perform the following steps for each UserOperation
:
- Call the account with the
UserOperation
's calldata. It's up to the account to choose how to parse the calldata; an expected workflow is for the account to have anexecute
function that parses the remaining calldata as a series of one or more calls that the account should make. - If the calldata starts with the methodsig
IAccountExecute.executeUserOp
, then theEntryPoint
must build a calldata by encodingexecuteUserOp(userOp,userOpHash)
and call the account using that calldata. - After the call, refund the account's deposit with the excess gas cost that was pre-charged.
A penalty of10%
(UNUSED_GAS_PENALTY_PERCENT
) is applied on the amounts ofcallGasLimit
andpaymasterPostOpGasLimit
gas that remains unused.
This penalty is only applied if the amount of the remaining unused gas is greater than or equal40000
(PENALTY_GAS_THRESHOLD
).
This penalty is necessary to prevent theUserOperations
from reserving large parts of the gas space in the bundle but leaving it unused and preventing the bundler from including otherUserOperations
. - After the execution of all calls, pay the collected fees from all
UserOperations
to thebeneficiary
address provided by the bundler.
Before accepting a UserOperation
, bundlers SHOULD use an RPC method to locally call the handleOps
function on the EntryPoint
,
to verify that the signature is correct and the UserOperation
actually pays fees; see the Simulation section below for details.
A node/bundler MUST reject a UserOperation
that fails the validation, meaning not adding it to the local mempool
and not propagating it to other peers.
Support for EIP-712 signatures
The userOpHash
is calculated as an [EIP-712] typed message hash with the following parameters:
bytes32 constant TYPE_HASH = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ); bytes32 constant PACKED_USEROP_TYPEHASH = keccak256( "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)" );
Support for EIP-7702 authorizations
On networks with EIP-7702 enabled, the eth_sendUserOperation
method accepts an extra eip7702Auth
parameter.
If this parameter is set, it MUST be a valid EIP-7702 authorization tuple, and signed by the sender
address.
The bundler MUST add all required eip7702Auth
of all UserOperations
in a bundle to the authorizationList
and execute
the bundle using a transaction type SET_CODE_TX_TYPE
.
Additionally, the UserOperation
hash calculation is updated to include the desired EIP-7702 delegation address.
If the initCode
field starts with 0x7702
padded with zeros, and this account was deployed using an EIP-7702 transaction, then the hash is calculated as follows:
- For the purpose of hash calculation, the first 20 bytes of the
initCode
field of theUserOperation
are set to account's EIP-7702 delegate address (fetched with EXTCODECOPY) - The
initCode
is not used to call a factory contract. - If the
initCode
is longer than 20 bytes, then the rest of the initCode is used to call an initialization function in the account itself.
Note that a UserOperation
may still be executed without such initCode
.
In this case the EntryPoint
doesn't hash the current EIP-7702 delegate, and can be potentially executed against a modified account.
Additionally, EIP-7702 defines the gas cost of executing an authorization equal to PER_EMPTY_ACCOUNT_COST = 25000
.
This gas consumption is not observable on-chain by the EntryPoint
contract and MUST be included in the preVerificationGas
value.
Extension: paymasters
We extend the EntryPoint
logic to support paymasters that can sponsor transactions for other users. This feature can be used to allow application developers to subsidize fees for their users, allow users to pay fees with [ERC-20] tokens and many other use cases. When the paymasterAndData
field in the UserOperation
is not empty, the EntryPoint
implements a different flow for that UserOperation:
During the verification loop, in addition to calling validateUserOp
, the handleOps
execution also must check that the paymaster has enough ETH deposited with the EntryPoint
to pay for the UserOperation
, and then call validatePaymasterUserOp
on the paymaster to verify that the paymaster is willing to pay for the UserOperation
. Note that in this case, the validateUserOp
is called with a missingAccountFunds
of 0 to reflect that the account's deposit is not used for payment for this UserOperation
.
If the paymaster's validatePaymasterUserOp
returns a non-empty context
byte array, then handleOps
must call postOp
on the paymaster after making the main execution call.
Otherwise, no call is done to the postOp
function.
Maliciously crafted paymasters can DoS the system. To prevent this, we use a reputation system. paymaster must either limit its storage usage, or have a stake. see the reputation, throttling and banning section for details.
The paymaster interface is as follows:
function validatePaymasterUserOp (PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) external returns (bytes memory context, uint256 validationData); function postOp (PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) external; enum PostOpMode { opSucceeded, // UserOperation succeeded opReverted // UserOperation reverted. paymaster still has to pay for gas. }
The EntryPoint
must implement the following API to let entities like paymasters have a stake, and thus have more flexibility in their storage access (see reputation, throttling and banning section for details.)
// add a stake to the calling entity function addStake(uint32 _unstakeDelaySec) external payable; // unlock the stake (must wait unstakeDelay before can withdraw) function unlockStake() external; // withdraw the unlocked stake function withdrawStake(address payable withdrawAddress) external;
The paymaster must also have a deposit, which the EntryPoint
will charge UserOperation
costs from.
The deposit (for paying gas fees) is separate from the stake (which is locked).
The EntryPoint
must implement the following interface to allow Paymasters (and optionally Accounts) to manage their deposit:
// return the deposit of an account function balanceOf(address account) public view returns (uint256); // add to the deposit of the given account function depositTo(address account) public payable; // add to the deposit of the calling account receive() external payable; // withdraw from the deposit of the current account function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external;
Bundler behavior upon receiving a UserOperation
Similar to an Ethereum transaction, the offchain flow of a UserOperation
can be described as follows:
- Client sends a
UserOperation
to the bundler through an RPC calleth_sendUserOperation
. - Before including the
UserOperation
in the mempool, the bundler runs the first validation of the newly received UserOperation. If theUserOperation
fails validation, the bundler drops it and returns an error in response toeth_sendUserOperation
. - Later, once building a bundle, the bundler takes
UserOperations
from the mempool and runs the second validation of a singleUserOperation
on each of them. If it succeeds, it is scheduled for inclusion in the next bundle, and dropped otherwise. - Before submitting the new bundle onchain, the bundler performs the third validation of the entire
UserOperations
bundle. If any of theUserOperations
fail validation, the bundler drops them, and updates their reputation, as described in ERC-7562 in detail.
When a bundler receives a UserOperation
, it must first run some basic sanity checks, namely that:
- Either the
sender
is an existing contract, or theinitCode
is not empty (but not both) - If
initCode
is not empty, parse its first 20 bytes as a factory address or an EIP-7702 flag.
Record whether the factory is staked, in case the later simulation indicates that it needs to be. If the factory accesses the global state, it must be staked - see reputation, throttling and banning section for details. - The
verificationGasLimit
andpaymasterVerificationGasLimits
are lower thanMAX_VERIFICATION_GAS
(500000
) and thepreVerificationGas
is high enough to pay for the calldata gas cost of serializing theUserOperation
plusPRE_VERIFICATION_OVERHEAD_GAS
(50000
). - The
paymasterAndData
is either empty, or starts with the paymaster address, which is a contract that (i) currently has nonempty code on chain, (ii) has a sufficient deposit to pay for the UserOperation, and (iii) is not currently banned. During simulation, the paymaster's stake is also checked, depending on its storage usage - see reputation, throttling and banning section for details. - The
callGasLimit
is at least the cost of aCALL
with non-zero value. - The
maxFeePerGas
andmaxPriorityFeePerGas
are above a configurable minimum value that the bundler is willing to accept. At the minimum, they are sufficiently high to be included with the currentblock.basefee
. - The
sender
doesn't have anotherUserOperation
already present in the mempool (or it replaces an existing entry with the same sender and nonce, with a highermaxPriorityFeePerGas
and an equally increasedmaxFeePerGas
). Only oneUserOperation
per sender may be included in a single bundle. A sender is exempt from this rule and may have multipleUserOperations
in the mempool and in a bundle if it is staked (see reputation, throttling and banning section below).
UserOperation Simulation
We define UserOperation
simulation, as the offchain view call (or trace call) to the EntryPoint
contract with the UserOperation
, and the enforcement of ERC-7562 rules, as part of the UserOperation
validation.
Simulation Rationale
To validate a normal Ethereum transaction tx
, the bundler performs static checks, like:
ecrecover(tx.v, tx.r, tx.s)
has to return a valid EOAtx.nonce
has to be the current nonce of the recovered EOAbalance
of the recovered EOA has to be sufficient to pay for the transactiontx.gasLimit
has to be sufficient to cover the intrinsic gas cost of a transactionchainId
has to match the current chain
All of these checks do not rely on EVM state, and cannot be affected by other Accounts' transactions.
In contrast, UserOperation
validation rely on EVM state (calls to validateUserOp
, validatePaymasterUserOp
), can be changed by other UserOperations
(or normal Ethereum transactions). Therefore, we introduce simulation as a new mechanism to check its validity.
Intuitively, the aim of the simulation is to ensure the onchain validation code of a UserOperation
is sandboxed, isolated from other UserOperations
in the same bundle.
Simulation Specification:
To simulate a UserOperation
validation, the bundler makes a view call to the handleOps()
method with the UserOperation
to check.
Simulation should run only on the validation section of the sender
and paymaster
, and is not required for the UserOperation
's execution.
A bundler MAY add second "always failed" UserOperation
to the bundle, so that the simulation will
end as soon as the first UserOperation's validation complete.
The bundler MUST drop the UserOperation
if the simulation reverts
The simulated call performs the full validation, by calling:
- If
initCode
is present, create thesender
Account. account.validateUserOp
.- if specified a paymaster:
paymaster.validatePaymasterUserOp
.
Either sender
or paymaster
may return a time-range (validAfter
/validUntil
).
The UserOperation
MUST be valid at the current time to be considered valid, defined as validAfter<=block.timestamp
.
A bundler MAY drop a UserOperation
if it expires too soon and is likely to become invalid before the next block.
To decode the returned time-ranges, the bundler MUST run the validation using tracing, to decode the return value from the validateUserOp
and validatePaymasterUserOp
methods.
To prevent DoS attacks on bundlers, they must make sure the validation methods above pass the validation rules, which constrain their usage of opcodes and storage. For the complete procedure see ERC-7562
Alternative Mempools
The simulation rules above are strict and prevent the ability of paymasters to grief the system. However, there might be use cases where specific paymasters can be validated (through manual auditing) and verified that they cannot cause any problem, while still require relaxing of the opcode rules. A bundler cannot simply "whitelist" a request from a specific paymaster: if that paymaster is not accepted by all bundlers, then its support will be sporadic at best. Instead, we introduce the term "alternate mempool": a modified validation rules, and procedure of propagating them to other bundlers.
The procedure of using alternate mempools is defined in ERC-7562
Bundling
Bundling is the process where a node/bundler collects multiple UserOperations
and creates a single transaction to submit on-chain.
During bundling, the bundler MUST:
- Exclude
UserOperations
that access any sender address of anotherUserOperation
in the same batch. - Exclude
UserOperations
that access any address created by anotherUserOperation
validation in the same batch (via a factory). - For each paymaster used in the batch, keep track of the balance while adding
UserOperations
. Ensure that it has sufficient deposit to pay for all theUserOperations
that use it.
After creating the batch, before including the transaction in a block, the bundler SHOULD:
- Run
debug_traceCall
with maximum possible gas, to enforce the validation rules on opcode and storage access, as well as to verify the entirehandleOps
batch transaction, and use the consumed gas for the actual transaction execution. - If the call reverted, the bundler MUST use the trace result to find the entity that reverted the call.
This is the last entity that is CALL'ed by theEntryPoint
prior to the revert.
(the bundler cannot assume the revert isFailedOp
) - If any verification context rule was violated the bundlers MUST treat it the same as
if this
UserOperation
reverted. - Remove the offending
UserOperation
from the current bundle and from mempool. - If the error is caused by a
factory
or apaymaster
, and thesender
of theUserOperation
is not a staked entity, then issue a "ban" (see "Reputation, throttling and banning") for the guilty factory or paymaster. - If the error is caused by a
factory
or apaymaster
, and thesender
of theUserOperation
is a staked entity, do not ban thefactory
/paymaster
from the mempool. Instead, issue a "ban" for the stakedsender
entity. - Repeat until
debug_traceCall
succeeds.
As staked entries may use some kind of transient storage to communicate data between UserOperations
in the same bundle,
it is critical that the exact same opcode and precompile banning rules as well as storage access rules are enforced
for the handleOps
validation in its entirety as for individual UserOperations
.
Otherwise, attackers may be able to use the banned opcodes to detect running on-chain and trigger a FailedOp
revert.
When a bundler includes a bundle in a block it must ensure that earlier transactions in the block don't make any UserOperation
fail.
It SHOULD either use an "access lists" parameter as defined in EIP-2930 to prevent conflicts,
or place the bundle as the first transaction in the block.
Error codes.
While performing validation, the EntryPoint
must revert on failures. During simulation, the calling bundler MUST be able to determine which entity (sender
,factory
or paymaster
) caused the failure.
The attribution of a revert to an entity is done using call-tracing: the last entity called by the EntryPoint
prior to the revert is the entity that caused the revert.
- For diagnostic purposes, the
EntryPoint
must only revert with explicitSignatureValidationFailed()
,FailedOp()
orFailedOpWithRevert()
errors. - The message of the error starts with event code, AA##
- Event code starting with "AA1" signifies an error during
sender
creation - Event code starting with "AA2" signifies an error during
sender
validation (validateUserOp
) - Event code starting with "AA3" signifies an error during
paymaster
validation (validatePaymasterUserOp
)
Rationale
The main challenge with a purely "Smart Contract Accounts" based Account Abstraction system is DoS safety: how can a block builder including an operation make sure that it will actually pay fees, without having to first execute the entire operation? Requiring the block builder to execute the entire operation opens a DoS attack vector, as an attacker could easily send many operations that pretend to pay a fee but then revert at the last moment after a long execution. Similarly, to prevent attackers from cheaply clogging the mempool, nodes in the P2P network need to check if an operation will pay a fee before they are willing to forward it.
The first step is a clean separation between validation (acceptance of UserOperation, and acceptance to pay) and execution.
In this proposal, we expect Accounts to have a validateUserOp
method that takes as input a UserOperation
, verifies the signature and pays the fee.
Only if this method returns successfully, the execution will happen.
The EntryPoint
-based approach allows for a clean separation between verification and execution, and keeps Smart Contract Accounts' logic simple. It enforces the simple rule that only after validation is successful and the UserOperation
can pay, the execution is done and only done once, and also guarantees the fee payment.
Validation Rules Rationale
The next step is protecting the bundlers from denial-of-service attacks by a mass number of UserOperations
that appear to be valid (and pay) but that eventually revert, and thus block the bundler from processing valid UserOperations
.
There are two types of UserOperations
that can fail validation:
UserOperations
that succeed in initial validation (and accepted into the mempool), but rely on the environment state to fail later when attempting to include them in a block.UserOperations
that are valid when checked independently but fail when bundled together to be put on-chain. To prevent such rogueUserOperations
, the bundler is required to follow a set of restrictions on the validation function, to prevent such denial-of-service attacks.
Reputation Rationale
UserOperation's storage access rules prevent them from interfering with each other.
But "global" entities - paymasters and factories are accessed by multiple UserOperations
, and thus might invalidate multiple previously valid UserOperations
.
To prevent abuse, we throttle down (or completely ban for a period of time) an entity that causes invalidation of a large number of UserOperations
in the mempool.
To prevent such entities from "Sybil-attack", we require them to stake with the system, and thus make such DoS attack very expensive.
Note that this stake is never slashed. There is no slashing mechanism involved and the only use for the stake in sybil attack prevention.
The stake can be withdrawn at any time after the specified unstake delay.
Unstaked entities are allowed, under the rules below.
When staked, an entity is less restricted in its use of contract storage.
The stake value is not enforced on-chain, but specifically by each bundler while simulating a transaction.
Reputation scoring and throttling/banning for global entities
[ERC-7562] defines a set of rules a bundler must follow when accepting UserOperations
into the mempool.
It also describes the "reputation"
Paymasters
Paymaster contracts allow the abstraction of gas: having a contract, that is not the sender of the transaction, to pay for the transaction fees.
Paymaster architecture allows them to follow the model of "pre-charge, and later refund". E.g. a token-paymaster may pre-charge the user with the max possible price of the transaction, and refund the user with the excess afterwards.
First-time Smart Contract Account creation
NOTE: for contracts using EIP-7702 this flow is described in Support for [EIP-7702] authorizations.
It is an important design goal of this proposal to replicate the key property of EOAs that users do not need to perform some custom action or rely on an existing user to create their Smart Contract Account; they can simply generate an address locally and immediately start accepting funds.
The Smart Contract Account creation itself is done by a "factory" contract, with some Account-specific data.
The Factory is expected to use CREATE2 0xF5
(not CREATE 0xF0
) to create the Account, so that the order of creation of the Accounts doesn't interfere with the generated addresses.
The initCode
field (if non-zero length) is parsed as a 20-byte factory
address, followed by calldata
to pass to this address.
This method call is expected to create the Account and return its address.
If the factory does use CREATE2 0xF5
or some other deterministic method to create the Account, it's expected to return the Account address even if it had already been created.
This comes to make it easier for bundlers to query the address without knowing if the Account has already been deployed, by simulating a call to entryPoint.getSenderAddress()
, which calls the factory
under the hood.
When initCode
is specified, if either the sender
address points to an existing contract or the sender
address still does not exist after calling the initCode
,
then the operation is aborted.
The initCode
MUST NOT be called directly from the EntryPoint
, but from another address.
The contract created by this factory method MUST accept a call to validateUserOp
to validate the UserOperation
's signature.
For security reasons, it is important that the generated contract address will depend on the initial signature.
This way, even if someone can deploy an Account at that address, he can't set different credentials to control it.
The Factory has to be staked if it accesses global storage - see reputation, throttling and banning section for details.
NOTE: In order for the Wallet Application to determine the "counterfactual" address of the Account prior to its creation,
it SHOULD make a static call to the entryPoint.getSenderAddress()
Backwards Compatibility
This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. Unfortunately it is not easily compatible with pre-ERC-4337 Smart Contract Accounts, because those Accounts do not have a validateUserOp
function. If the Smart Contract Account has a function for authorizing a trusted UserOperation
submitter, then this could be fixed by creating an ERC-4337 compatible Account that re-implements the verification logic as a wrapper and setting it to be the original Account's trusted UserOperation
submitter.
Reference Implementation
See https://github.com/eth-infinitism/account-abstraction/tree/main/contracts
Security Considerations
The EntryPoint
contract will need to be audited and formally verified, because it will serve as a central trust point for all [ERC-4337]. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual accounts have to do becomes much smaller (they need only verify the validateUserOp
function and its "check signature and pay fees" logic) and check that other functions are msg.sender == ENTRY_POINT
gated (perhaps also allowing msg.sender == self
), but it is nevertheless the case that this is done precisely by concentrating security risk in the EntryPoint
contract that needs to be verified to be very robust.
Verification would need to cover two primary claims (not including claims needed to protect paymasters, and claims needed to establish p2p-level DoS resistance):
- Safety against arbitrary hijacking: The
EntryPoint
only calls to thesender
withuserOp.calldata
and only ifvalidateUserOp
to that specificsender
has passed. - Safety against fee draining: If the
EntryPoint
callsvalidateUserOp
and passes, it also must make the generic call with calldata equal touserOp.calldata
Factory contracts
All factory
contracts MUST check that all calls to the createAccount()
function originate from the entryPoint.senderCreator()
address.
Paymasters contracts
All paymaster
contracts MUST check that all calls to the validatePaymasterUserOp()
and postOp()
functions originate from the EntryPoint
.
Aggregator contracts
All paymaster
contracts MUST check that all calls to the validateSignatures()
function originates from the EntryPoint
.
EIP-7702 delegated Smart Contract Accounts
All EIP-7702 delegated Smart Contract Account implementations MUST check that all calls to the initialization function originate from the entryPoint.senderCreator()
address.
Smart Contract Accounts
Storage layout collisions
It is expected that most of ERC-4337 Smart Contract Account will be upgradeable, either via on-chain delegate proxy contracts or via EIP-7702.
When changing the underlying implementation, all Accounts MUST ensure that there are no conflicts in the storage layout of the two contracts.
One common approach to this problem is often referred to as "diamond storage" and is fully described in ERC-7201.
Transient Storage
Contracts using the EIP-1153 transient storage MUST take into account that ERC-4337 allows multiple
UserOperations
from different unrelated sender
addresses to be included in the same underlying transaction.
The transient storage MUST be cleaned up manually if contains any sensitive information or is used for access control.
All account contracts
Storage layout collisions
It is expected that most of ERC-4337 smart contract accounts will be upgradeable, either via on-chain delegate proxy contracts or via EIP-7702.
When changing the underlying implementation, Wallet Applications MUST ensure that there are no conflicts in the storage layout of the two contracts.
One common approach to this problem is often referred to as "diamond storage" and is fully described in ERC-7201.
Transient Storage
Contracts using the EIP-1153 transient storage MUST take into account that ERC-4337 allows multiple
UserOperations
from different unrelated sender
addresses to be included in the same underlying transaction.
The transient storage MUST be cleaned up manually if contains any sensitive information or is used for access control.
Copyright
Copyright and related rights waived via CC0.