Skip to content

Commit

Permalink
Update EIP-4337: Some ERC-4337 changes
Browse files Browse the repository at this point in the history
Merged by EIP-Bot.
  • Loading branch information
forshtat committed Jul 3, 2023
1 parent 1bb34ba commit a36e823
Showing 1 changed file with 101 additions and 15 deletions.
116 changes: 101 additions & 15 deletions EIPS/eip-4337.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
eip: 4337
title: Account Abstraction Using Alt Mempool
description: An account abstraction proposal which completely avoids consensus-layer protocol changes, instead relying on higher-layer infrastructure.
author: Vitalik Buterin (@vbuterin), Yoav Weiss (@yoavw), Kristof Gazso (@kristofgazso), Namra Patel (@namrapatel), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), Tjaden Hess (@tjade273)
author: Vitalik Buterin (@vbuterin), Yoav Weiss (@yoavw), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), Alex Forshtat (@forshtat), Kristof Gazso (@kristofgazso), Tjaden Hess (@tjade273)
discussions-to: https://ethereum-magicians.org/t/erc-4337-account-abstraction-via-entry-point-contract-specification/7160
status: Draft
type: Standards Track
Expand All @@ -12,7 +12,7 @@ created: 2021-09-29

## 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 (either block builders, or users that can send transactions to block builders through a bundle marketplace) 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.
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

Expand All @@ -39,19 +39,18 @@ This proposal takes a different approach, avoiding any adjustments to the consen
* **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 "sender", "to", "calldata", "maxFeePerGas", "maxPriorityFee", "signature", "nonce"
* unlike a transaction, it contains several other fields, described below
* also, the "nonce" and "signature" fields usage is not defined by the protocol, but by each account implementation
* also, the "signature" field usage is not defined by the protocol, but by each account implementation
* **Sender** - the account contract sending a user operation.
* **EntryPoint** - a singleton contract to execute bundles of UserOperations. Bundlers/Clients whitelist the supported entrypoint.
* **Bundler** - a node (block builder) that bundles multiple UserOperations and create an EntryPoint.handleOps() transaction. Note that not all block-builders on the network are required to be bundlers
* **Bundler** - A node (block builder) that can handle UserOperations and create an EntryPoint.handleOps() transaction and add it to the next block. Note that not all block-builders on the network are required to be bundlers.
* **Aggregator** - a helper contract trusted by accounts to validate an aggregated signature. Bundlers/Clients whitelist the supported aggregators.


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 account to take in an ABI-encoded struct called a `UserOperation`:

| Field | Type | Description
| - | - | - |
| `sender` | `address` | The account making the operation |
| `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time account creation |
| `nonce` | `uint256` | Anti-replay parameter (see "Semi-abstracted Nonce Support" ) |
| `initCode` | `bytes` | The initCode of the account (needed if and only if the account is not yet on-chain and needs to be created) |
| `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 |
Expand Down Expand Up @@ -159,6 +158,72 @@ interface IAggregator {
* **validateSignatures()** MUST validate the aggregated signature matches for all UserOperations in the array, and revert otherwise.
This method is called on-chain by `handleOps()`

#### 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 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:

```solidity
function getNonce(address sender, uint192 key) external view returns (uint256 nonce);
```

For each `key` the `sequence` is validated and incremented sequentially and monotonically by the `EntryPoint` for
each UserOperation, however a new key can be introduced with an arbitrary value at any point.

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 UserOp clients may make a view call to this method to determine a valid value for the `nonce` field.

Bundler's validation of a UserOp 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

1. Classic sequential nonce.

In order to require the wallet to have classic, sequential nonce, the validation function should perform:

```solidity
require(userOp.nonce<type(uint64).max)
```

2. 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 specific `key` when calling methods on the account itself:

```solidity
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");
}
```

#### Using signature aggregators

An account signifies it uses signature aggregation returning its address from `validateUserOp`.
Expand Down Expand Up @@ -306,12 +371,15 @@ While simulating `userOp` validation, the client should make sure that:
4. Limitation on "CALL" opcodes (`CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL`):
1. must not use value (except from account to the entrypoint)
2. must not revert with out-of-gas
3. destination address must have code (EXTCODESIZE>0)
3. destination address must have code (EXTCODESIZE>0) or be a standard Ethereum precompile defined at addresses from `0x01` to `0x09`
4. cannot call EntryPoint's methods, except `depositFor` (to avoid recursion)
5. `EXTCODEHASH` of every address accessed (by any opcode) does not change between first and second simulations of the op.
6. `EXTCODEHASH`, `EXTCODELENGTH`, `EXTCODECOPY` may not access address with no code.
7. If `op.initcode.length != 0` , allow only one `CREATE2` opcode call (in the first (deployment) block), otherwise forbid `CREATE2`.
Transient Storage slots defined in [EIP-1153](./eip-1153.md) and accessed using `TLOAD` (`0x5c`) and `TSTORE` (`0x5d`) opcodes
must follow the exact same validation rules as persistent storage if Transient Storage is enabled.
#### Storage associated with an address
We define storage slots as "associated with an address" as all the slots that uniquely related on this address, and cannot be related with any other address.
Expand All @@ -324,7 +392,6 @@ An address `A` is associated with:
3. Slots of type `keccak256(A || X) + n` on any other address. (to cover `mapping(address => value)`, which is usually used for balance in ERC-20 tokens).
`n` is an offset value up to 128, to allow accessing fields in the format `mapping(address => struct)`
#### Alternative Mempools
The simulation rules above are strict and prevent the ability of paymasters and signature aggregators to grief the system.
Expand All @@ -349,12 +416,30 @@ During bundling, the client should:
After creating the batch, before including the transaction in a block, the client should:
* Run `eth_estimateGas` with maximum possible gas, to verify the entire `handleOps` batch transaction, and use the estimated gas for the actual transaction execution.
* If the call reverted, check the `FailedOp` event. A `FailedOp` during `handleOps` simulation is an unexpected event since it was supposed to be caught by the single-UserOperation simulation. Remove the failed op that caused the revert from the batch and drop from the mempool.
If the error is caused by a factory (error code is "AA1.") or paymaster (error code is "AA3."), then also drop from mempool all other UserOps of this entity.
Repeat until `eth_estimateGas` succeeds.
In practice, restrictions (2) and (3) basically mean that the only external accesses that the account and the paymaster can make are reading code of other contracts if their code is guaranteed to be immutable (eg. this is useful for calling or delegatecalling to libraries).
* Run `debug_traceCall` with maximum possible gas, to enforce the validation opcode and precompile banning and storage access rules,
as well as to verify the entire `handleOps` batch transaction,
and use the consumed gas for the actual transaction execution.
* If the call reverted, check the `FailedOp` event.
A `FailedOp` during `handleOps` simulation is an unexpected event since it was supposed to be caught
by the single-UserOperation simulation.
* If any verification context rule was violated the bundlers should treat it the same as
if this UserOperation reverted with a `FailedOp` event.
* If the error is caused by a `factory` (error code is `AA1x`) or a paymaster (error code is `AA3x`), and the `sender`
of the UserOp **is not** a staked entity, then issue a "ban" (see ["Reputation, throttling and banning"](#reputation-scoring-and-throttlingbanning-for-global-entities))
for the guilty factory or paymaster.
* If the error is caused by a `factory` (error code is `AA1x`) or a paymaster (error code is `AA3x`), and the `sender`
of the UserOp **is** a staked entity, do not ban the `factory` / `paymaster` from the mempool.
Instead, issue a "ban" for the staked sender 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.
Banning an offending entity for a given bundler is achieved by increasing its `opsSeen` value by `1000000`
and removing all UserOperations for this entity already present in the mempool.
This change will allow the negative reputation value to deteriorate over time consistent with other banning reasons.
If any of the three conditions is violated, the client should reject the `op`. If both calls succeed (or, if `op.paymaster == ZERO_ADDRESS` and the first call succeeds)without violating the three conditions, the client should accept the op. On a bundler node, the storage keys accessed by both calls must be saved as the `accessList` of the `UserOperation`
Expand Down Expand Up @@ -395,7 +480,8 @@ The value of MIN_STAKE_VALUE is determined per chain, and specified in the "bund
Under the following special conditions, unstaked entities still can be used:
* An entity that doesn't use any storage at all, or only the senders's storage (not the entity's storage - that does require a stake)
* If the UserOp doesn't create a new account (that is initCode is empty), then the entity may also use [storage associated with the sender](#storage-associated-with-an-address))
* If the UserOp doesn't create a new account (that is initCode is empty), or the UserOp creates a new account using a
staked `factory` contract, then the entity may also use [storage associated with the sender](#storage-associated-with-an-address))
* A paymaster that has a “postOp()” method (that is, validatePaymasterUserOp returns “context”) must be staked
#### Specification.
Expand Down

0 comments on commit a36e823

Please sign in to comment.