Skip to content

Commit

Permalink
sync EIP with public repo. (#320)
Browse files Browse the repository at this point in the history
Co-authored-by: Dror Tirosh <[email protected]>
  • Loading branch information
drortirosh and Dror Tirosh authored Jul 25, 2023
1 parent 0a9d55c commit 87bcb3e
Showing 1 changed file with 111 additions and 30 deletions.
141 changes: 111 additions & 30 deletions eip/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,25 @@ 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,
create a valid an EntryPoint.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 PBS (proposer-builder separation)
* The `bundler` can also rely on an experimental `eth_sendRawTransactionConditional` RPC API if it is available.
* **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 |
| `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 @@ -128,7 +134,6 @@ The account:
* MUST validate the caller is a trusted EntryPoint
* If the account does not support signature aggregation, it MUST validate the signature is a valid signature of the `userOpHash`, and
SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error should revert.
* The MAY check the nonce field, but should not implement the replay protection mechanism: the EntryPoint maintains uniqueness of nonces per user account.
* MUST pay the entryPoint (caller) at least the "missingAccountFunds" (which might be zero, in case current account's deposit is high enough)
* The account MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it)
* The return value MUST be packed of `authorizer`, `validUntil` and `validAfter` timestamps.
Expand Down Expand Up @@ -160,6 +165,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 All @@ -183,7 +254,6 @@ The entry point's `handleOps` function must perform the following steps (we firs
* **Create the account if it does not yet exist**, using the initcode provided in the `UserOperation`. If the account does not exist, _and_ the initcode is empty, or does not deploy a contract at the "sender" address, the call must fail.
* **Call `validateUserOp` on the account**, passing in the `UserOperation`, the required fee and aggregator (if there is one). The account should verify the operation's signature, and pay the fee if the account considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, 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)
* Validate the nonce uniqueness. see [Keep Nonce Uniqueness](#keep-nonce-uniqueness) below

In the execution loop, the `handleOps` call must perform the following steps for each `UserOperation`:

Expand All @@ -194,17 +264,6 @@ In the execution loop, the `handleOps` call must perform the following steps for
Before accepting a `UserOperation`, bundlers should use an RPC method to locally call the `simulateValidation` function of the entry point, to verify that the signature is correct and the operation actually pays fees; see the [Simulation section below](#simulation) for details.
A node/bundler SHOULD drop (not add to the mempool) a `UserOperation` that fails the validation

### Keep Nonce Uniqueness

The EntryPoint maintains nonce uniqueness for each submitted UserOperation using the following algorithm:
* The nonce is treated as 2 separate fields:
* 64-bit "sequence"
* 192-bit "key"
* Within each "key", the "sequence" value must have consecutive values, starting with zero.
* That is, a nonce with a new "key" value is allowed, as long as the "sequence" part is zero. The next nonce for that key must be "1", and so on.
* The EntryPoint exports a method `getNonce(address sender, uint192 key)` to return the next valid nonce for this key.
* The behaviour of a "classic" sequential nonce can be achieved by validating that the "key" part is always zero.

### Extension: paymasters

We extend the entry point 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](./eip-20.md) tokens and many other use cases. When the paymaster is not equal to the zero address, the entry point implements a different flow:
Expand Down Expand Up @@ -319,12 +378,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 @@ -337,7 +399,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 @@ -362,12 +423,31 @@ 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.
* Remove the offending UserOperation from the current bundle and from mempool.
* 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 @@ -408,7 +488,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 All @@ -421,7 +502,7 @@ Clients maintain two mappings with a value for staked entities:
If an entity doesn't use storage at all, or only reference storage associated with the "sender" (see [Storage associated with an address](#storage-associated-with-an-address)), then it is considered "OK", without using the rules below.
When the client learns of a new staked entity, it sets `opsSeen[paymaster] = 0` and `opsIncluded[paymaster] = 0` .
When the client learns of a new staked entity, it sets `opsSeen[entity] = 0` and `opsIncluded[entity] = 0` .
The client sets `opsSeen[entity] +=1` each time it adds an op with that `entity` to the `UserOperationPool`, and the client sets `opsIncluded[entity] += 1` each time an op that was in the `UserOperationPool` is included on-chain.
Expand Down Expand Up @@ -920,7 +1001,7 @@ See `https://github.com/eth-infinitism/account-abstraction/tree/main/contracts`

## Security Considerations

The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [EIP-4337](./eip-4337.md). 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 entry point contract that needs to be verified to be very robust.
The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-4337](./eip-4337.md). 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, increment nonce 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 entry point 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):

Expand Down

0 comments on commit 87bcb3e

Please sign in to comment.