Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ERC-4337: Use ERC-7746 for userOp and PaymasterOp validation #631

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 29 additions & 48 deletions ERCS/erc-4337.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
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), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), Alex Forshtat (@forshtat), Kristof Gazso (@kristofgazso), 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), Tim Pechersky (@peersky)
discussions-to: https://ethereum-magicians.org/t/erc-4337-account-abstraction-via-entry-point-contract-specification/7160
status: Draft
type: Standards Track
category: ERC
created: 2021-09-29
requires: 7562
requires: 7562, 7746
---

## Abstract
Expand Down Expand Up @@ -117,31 +117,21 @@ struct UserOpsPerAggregator {

### Account Contract Interface

The core interface required for an account to have is:

```solidity
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 core interface required for an account is defined in [ERC-7746](./eip-7746).

The account:

* MUST validate the caller is a trusted EntryPoint
* If the account does not support signature aggregation, it MUST validate that the signature is a valid signature of the `userOpHash`, and
SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error MUST revert.
* If the account does not support signature aggregation, it MUST validate that the signature is a valid signature of the `beforeCall`, and
SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. **(ToDo: [ERC-7746](./eip-7746.md) defines that validating contracts MUST revert if validation fails, if catch clause used in 4337 implementation it can preserve current functional requirements)**. Any other error MUST revert.
* MUST pay the entryPoint (caller) at least the "missingAccountFunds" (which might be zero, in case the 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.
* authorizer - 0 for valid signature, 1 to mark signature failure. Otherwise, an address of an authorizer contract. This ERC defines a "signature aggregator" as an authorizer.
* `validUntil` is 6-byte timestamp value, or zero for "infinite". The UserOp is valid only up to this time.
* `validAfter` is 6-byte timestamp. The UserOp is valid only after this time.

An account that works with aggregated signature, should return its signature aggregator address in the "sigAuthorizer" return value of validateUserOp.
An account that works with aggregated signature, should return its signature aggregator address "sigAuthorizer" return value of beforeCall.
It MAY ignore the signature field.

The account MAY implement the interface `IAccountExecute`
Expand Down Expand Up @@ -232,7 +222,7 @@ 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.
* calculate the maximum possible fee the account needs to pay (based on validation and call gas limits, and current gas values)
* calculate the fee the account must add to its "deposit" in the EntryPoint
* **Call `validateUserOp` on the account**, passing in the `UserOperation`, its hash and the required fee. 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.
* **Perform [ERC-7746](./eip-7746.md) validation on the account**, passing in the `UserOperation`, its hash and the required fee. The account should verify the operation's signature, and pay the fee if the account considers the operation valid. If `ERC-7746` call reverts, `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)

In the execution loop, the `handleOps` call must perform the following steps for each `UserOperation`:
Expand All @@ -255,28 +245,15 @@ We extend the entry point logic to support **paymasters** that can sponsor trans

![](../assets/eip-4337/bundle-seq-pm.svg)

During the verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster has enough ETH deposited with the entry point to pay for the operation, and then call `validatePaymasterUserOp` on the paymaster to verify that the paymaster is willing to pay for the operation. 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 userOp.
During the verification loop, the `handleOps` execution must check that the paymaster has enough ETH deposited with the entry point to pay for the operation, and then call [ERC-7746](./eip-7746.md) validation methods on the paymaster: `beforeCall` with `data` parameter equal to the `UserOperation`, to verify that the paymaster is willing to pay for the operation. Note that in this case, the account `beforeCall` is called with `missingAccountFunds` of 0 to reflect that the account's deposit is not used for payment for this userOp.

If the paymaster's validatePaymasterUserOp returns a "context", then `handleOps` must call `postOp` on the paymaster after making the main execution call.
is called with a `missingAccountFunds` of 0 to reflect that the account's deposit is not used for payment for this userOp.

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](#reputation-scoring-and-throttlingbanning-for-global-entities) for details.
Regardless if the paymaster's beforeCall returns a "context", a `handleOps` must call `afterCall` on the paymaster after making the main execution call with supplying "context" in accordance with[ERC-7746](./eip-7746.md).

The paymaster interface is as follows:
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](#reputation-scoring-and-throttlingbanning-for-global-entities) for details.

```solidity
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, // user op succeeded
opReverted, // user op reverted. still has to pay for gas.
postOpReverted // Regardless of the UserOp call status, the postOp reverted, and caused both executions to revert.
}
The paymaster interface MUST implement[ERC-7746](./eip-7746.md)
```

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](#reputation-scoring-and-throttlingbanning-for-global-entities) for details.)
Expand Down Expand Up @@ -338,12 +315,12 @@ interface IAggregator {
}
```

* An account signifies it uses signature aggregation returning its address from `validateUserOp`.
* An account signifies it uses signature aggregation returning its address from `beforeCall`.
* During `simulateValidation`, this aggregator is returned to the bundler as part of the `aggregatorInfo` struct.
* The bundler should first accept the aggregator (aggregators must be staked. bundler should verify it is not throttled/banned)
* To accept the UserOp, the bundler must call **validateUserOpSignature()** to validate the userOp's signature.
This method returned an alternate signature (usually empty) that should be used during bundling.
* The bundler MUST call `validateUserOp` a second time on the account with the UserOperation using that returned signature, and make sure it returns the same value.
* The bundler MUST call [ERC-7746](./eip-7746.md) `beforeCall` a second time on the account with the UserOperation using that returned signature, and make sure it returns the same value.
* **aggregateSignatures()** must aggregate all UserOp signatures into a single value.
* Note that the above methods are helper methods for the bundler. The bundler MAY use a native library to perform the same validation and aggregation logic.
* **validateSignatures()** MUST validate the aggregated signature matches for all UserOperations in the array, and revert otherwise.
Expand Down Expand Up @@ -409,10 +386,10 @@ The node should drop the UserOperation if the simulation fails (either by revert
The simulated call performs the full validation, by calling:

1. If `initCode` is present, create the account.
2. `account.validateUserOp`.
3. if specified a paymaster: `paymaster.validatePaymasterUserOp`.
2. `account.beforeCall`.
3. if specified a paymaster: `paymaster.beforeCall`.

The simulateValidation should validate the return value (validationData) returned by the account's `validateUserOp` and paymaster's `validatePaymasterUserOp`.
The `simulateValidation` function should validate the `validationData` returned by the `beforeCall` of the account and paymaster. This is done by sending the `validationData` back to the respective validating contracts after execution, calling `afterCall` on the account and paymaster with the return value provided in `beforeCallResult`.
The account MAY return an aggregator. See [Using Signature Aggregator](#using-signature-aggregator)
The paymaster MUST return either "0" (success) or SIG_VALIDATION_FAILED for aggregator, and not an address.
Either return value may contain a "validAfter" and "validUntil" timestamps, which is the time-range that this UserOperation is valid on-chain.
Expand Down Expand Up @@ -479,8 +456,8 @@ The attribution of a revert to an entity is done using call-tracing: the last en
* For diagnostic purposes, the EntryPoint must only revert with explicit FailedOp() or FailedOpWithRevert() errors.
* The message of the error starts with event code, AA##
* Event code starting with "AA1" signifies an error during account creation
* Event code starting with "AA2" signifies an error during account validation (validateUserOp)
* Event code starting with "AA3" signifies an error during paymaster validation (validatePaymasterUserOp)
* Event code starting with "AA2" signifies an error during account validation ([ERC-7746](./eip-7746.md) validation workflow)
* Event code starting with "AA3" signifies an error during paymaster validation ([ERC-7746](./eip-7746.md) validation workflow)


## Rationale
Expand All @@ -489,12 +466,16 @@ The main challenge with a purely smart contract wallet-based account abstraction
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.
In this proposal, we expect accounts to have a `beforeCall` and `afterCall` methods according to [ERC-7746](./eip-7746.md) that takes as input a `UserOperation`, verifies the signature and pays the fee.
Only if this method returns successfully, the execution will happen.

The entry point-based approach allows for a clean separation between verification and execution, and keeps accounts' logic simple. It enforces the simple rule that only after validation is successful (and the UserOp can pay), the execution is done, and also guarantees the fee payment.

### Use of [ERC-7746](./eip-7746.md) as validation mechanism
The [ERC-7746](./eip-7746.md) is a generic way to implement a security middleware in smart contracts. It allows to encapsulate the validation interfaces in a separate standard, and thus making this ERC smaller and better focused. It also allows for a more generic approach that can be used in other contexts.

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

Expand Down Expand Up @@ -542,7 +523,7 @@ If the factory does use CREATE2 or some other deterministic method to create the
When `initCode` is specified, if either the `sender` address points to an existing contract, or (after calling the initCode) the `sender` address still does not exist,
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 should accept a call to `validateUserOp` to validate the UserOp's signature.
The contract created by this factory method should implement [ERC-7746](./eip-7746.md) to validate the UserOp'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 create a wallet 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](#reputation-scoring-and-throttlingbanning-for-global-entities) for details.
Expand Down Expand Up @@ -1021,20 +1002,20 @@ Assume the given UserOperations all pass validation (without actually validating

## 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](./eip-4337.md) accounts, because those accounts do not have a `validateUserOp` function. If the account has a function for authorizing a trusted op submitter, then this could be fixed by creating an [ERC-4337](./eip-4337.md) compatible account that re-implements the verification logic as a wrapper and setting it to be the original account's trusted op submitter.
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 contracts not supporting [ERC-7746](./eip-7746.md) accounts, because those accounts do not have a call validation functions. If the account has a function for authorizing a trusted op submitter, then this could be fixed by creating an [ERC-4337](./eip-4337.md) compatible account that re-implements the verification logic as a wrapper and setting it to be the original account's trusted op submitter.

## Reference Implementation

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_ [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 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]. 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 [ERC-7746](./eip-7746.md) implementation 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.

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 entry point only calls an account generically if `validateUserOp` to that specific account has passed (and with `op.calldata` equal to the generic call's calldata)
* **Safety against fee draining**: If the entry point calls `validateUserOp` and passes, it also must make the generic call with calldata equal to `op.calldata`
* **Safety against arbitrary hijacking**: The entry point only calls an account generically if `account.beforeCall` to that specific account has passed (and with `op.calldata` equal to the generic call's calldata) AND if after the call `account.afterCall` does not revert.
* **Safety against fee draining**: If the entry point calls `beforeCall` and passes, it also must make the generic call with calldata equal to `op.calldata`

## Copyright

Expand Down
Loading