diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index 474d9fae853805..193efb4e03dcf8 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -1,72 +1,73 @@ --- eip: 4337 -title: Account Abstraction using alt mempool +title: Account Abstraction via Entry Point Contract specification 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) discussions-to: https://ethereum-magicians.org/t/erc-4337-account-abstraction-via-entry-point-contract-specification/7160 -status: Draft type: Standards Track category: ERC +status: Draft 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 miners, or users that can send transactions to miners 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 (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. ## 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](./eip-2938.md) for a consensus layer proposal for implementing the same goal. +See also ["Implementing Account Abstraction as Part of Eth 1.x"](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](./eip-2938.md) 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 wallets containing arbitrary verification logic instead of EOAs as their primary account. Completely remove any need at all for users to also have EOAs (as status quo SC wallets and [EIP-3074](./eip-3074.md) both require) +* **Achieve the key goal of account abstraction**: allow users to use smart contract wallets containing arbitrary verification logic instead of EOAs as their primary account. Completely remove any need at all for users to also have EOAs (as status quo SC wallets and EIP-3074 both require) * **Decentralization** - * Allow any bundler (think: miner) to participate in the process of including account-abstracted user operations + * Allow any bundler (think: block builder) to participate in the process of including account-abstracted user operations * 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 * **Do not require any Ethereum consensus changes**: Ethereum consensus layer development is focusing on the merge and later 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. * **Try to support other use cases** * Privacy-preserving applications * Atomic multi-operations (similar goal to EIP-3074) - * Pay tx fees with [EIP-20](./eip-20.md) tokens, allow developers to pay fees for their users, and [EIP-3074](./eip-3074.md)-like **sponsored transaction** use cases more generally + * Pay tx fees with ERC-20 tokens, allow developers to pay fees for their users, and [EIP-3074](./eip-3074.md)-like **sponsored transaction** use cases more generally * Support aggregated signature (e.g. BLS) -## Specification - -### Definitions +## Definitions * **UserOperation** - a structure that describes a transaction to be sent on behalf of a user. To avoid confusion, it is named "transaction". * Like a transaction, it contains "sender", "to", "calldata", "maxFeePerGas", "maxPriorityFee", "signature", "nonce" - * unlike transaction, it contains several other fields, described below + * unlike transaction, it contains several other fields, desribed below * also, the "nonce" and "signature" fields 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. -* **Aggregator** - a helper contract trusted by wallets to validate an aggregated signature. Bundlers/Clients whitelist the supported aggregators. +* **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 +* **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 wallet to take in an ABI-encoded struct called a `UserOperation`: +## Specification + +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 wallet making the operation | -| `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time wallet creation | -| `initCode` | `bytes` | The initCode of the wallet (needed if and only if the wallet is not yet on-chain and needs to be created) | +| `sender` | `address` | The account making the operation | +| `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time account creation | +| `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 | | `verificationGasLimit` | `uint256` | The amount of gas to allocate for the verification step | | `preVerificationGas` | `uint256` | The amount of gas to pay for to compensate the bundler for pre-verification execution and calldata | -| `maxFeePerGas` | `uint256` | Maximum fee per gas (similar to [EIP-1559](./eip-1559.md) `max_fee_per_gas`) | -| `maxPriorityFeePerGas` | `uint256` | Maximum priority fee per gas (similar to EIP-1559 `max_priority_fee_per_gas`) | +| `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`) | | `paymasterAndData` | `bytes` | Address of paymaster sponsoring the transaction, followed by extra data to send to the paymaster (empty for self-sponsored transaction) | -| `signature` | `bytes` | Data passed into the wallet along with the nonce during the verification step | +| `signature` | `bytes` | Data passed into the account along with the nonce during the verification step | -Users send `UserOperation` objects to a dedicated user operation mempool. A specialized class of actors called **bundlers** (either miners running special-purpose code, or users that can relay transactions to miners eg. through a bundle marketplace such as Flashbots that can guarantee next-block-or-never inclusion) listen in on the user operation mempool, and create **bundle transactions**. A bundle transaction packages up multiple `UserOperation` objects into a single `handleOps` call to a pre-published global **entry point contract**. +Users send `UserOperation` objects to a dedicated user operation mempool. A specialized class of actors called **bundlers** (either block builders running special-purpose code, or users that can relay transactions to block builders eg. through a bundle marketplace such as Flashbots that can guarantee next-block-or-never inclusion) listen in on the user operation mempool, and create **bundle transactions**. A bundle transaction packages up multiple `UserOperation` objects into a single `handleOps` call to a pre-published global **entry point contract**. To prevent replay attacks (both cross-chain and multiple `EntryPoint` implementations), the `signature` should depend on `chainid` and the `EntryPoint` address. The core interface of the entry point contract is as follows: -```c++ +```solidity function handleOps(UserOperation[] calldata ops, address payable beneficiary); function handleAggregatedOps( @@ -74,104 +75,111 @@ function handleAggregatedOps( address payable beneficiary ); -function simulateValidation - (UserOperation calldata userOp, bool offChainSigCheck) - external returns (uint256 preOpGas, uint256 prefund, address actualAggregator, bytes memory sigForUserOp, bytes memory sigForAggregation, bytes memory offChainSigInfo) { - struct UserOpsPerAggregator { UserOperation[] userOps; IAggregator aggregator; bytes signature; } + +function simulateValidation(UserOperation calldata userOp); + +error ValidationResult(uint256 preOpGas, uint256 prefund, uint256 deadline, + StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo); + +error ValidationResultWithAggregation(uint256 preOpGas, uint256 prefund, uint256 deadline, + StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo, AggregatorStakeInfo aggregatorInfo); + +struct StakeInfo { + uint256 stake; + uint256 unstakeDelaySec; +} + +struct AggregatorStakeInfo { + address actualAggregator; + StakeInfo stakeInfo; +} ``` -The core interface required for a wallet to have is: +The core interface required for an account to have is: ```solidity -interface IWallet { +interface IAccount { function validateUserOp - (UserOperation calldata userOp, bytes32 requestId, address aggregator, uint256 missingWalletFunds) - external; + (UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds) + external returns (uint256 deadline); } ``` -The wallet +The account * MUST validate the caller is a trusted EntryPoint -* The requestId is a hash over the userOp (except signature), entryPoint and chainId -* If the wallet does not support signature aggregation, it MUST validate the signature is a valid signature of the `requestId` -* MUST pay the entryPoint (caller) at least the "missingWalletFunds" (which might be zero, in case current wallet's deposit is high enough) -* The wallet MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it) -* The `aggregator` SHOULD be ignored for wallets that don't use an aggregator - -A Wallet that works with aggregated signature should have the interface: +* The userOpHash is a hash over the userOp (except signature), entryPoint and chainId +* 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. +* 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 `aggregator` SHOULD be ignored for accounts that don't use an aggregator +* The return value `deadline` is either zero (meaning "indefinitely"), or the last timestamp this request is deemed valid. + (or SIG_VALIDATION_FAILED on signature mismatch) + +An account that works with aggregated signature should have the interface: ```solidity -interface IAggregatedWallet is IWallet { +interface IAggregatedAccount is IAccount { function getAggregator() view returns (address); } ``` -* **getAggregator()** returns the aggregator this wallet supports. -* **validateUserOp()** (inherited from IWallet interface) MUST verify the `aggregator` parameter is valid and the same as `getAggregator` -* The wallet should also support aggregator-specific getter (e.g. `getAggregationInfo()`). - This method should export the wallet's public-key to the aggregator, and possibly more info - (note that it is not called directly by the entryPoint) +* **getAggregator()** returns the aggregator this account supports. +* **validateUserOp()** (inherited from IAccount interface) MUST verify the `aggregator` parameter is valid and the same as `getAggregator` +* The account should also support aggregator-specific getter (e.g. `getAggregationInfo()`). + This method should export the account's public-key to the aggregator, and possibly more info + (note that it is not called directly by the entryPoint) * validateUserOp MAY ignore the signature field - (the signature might contain data extracted by the `aggregator.validateUserOpSignature`, and used by `aggregator.validateSignatures`) The core interface required by an aggregator is: ```solidity interface IAggregator { - function validateUserOpSignature(UserOperation calldata userOp, bool offChainSigCheck) external view returns (bytes memory sigForUserOp, bytes memory sigForAggregation, bytes memory offChainSigInfo); + function validateUserOpSignature(UserOperation calldata userOp) + external view returns (bytes memory sigForUserOp); - function aggregateSignatures(bytes[] calldata sigsForAggregation) external view returns (bytes memory aggregatesSignature); + function aggregateSignatures(UserOperation[] calldata userOps) external view returns (bytes memory aggregatesSignature); function validateSignatures(UserOperation[] calldata userOps, bytes calldata signature) view external; } ``` -* **validateUserOpSignature()** must validate the userOp's signature against the UserOp's hash (the same hash MUST be used for - **validateUserOpSignature** and later for **validateSignatures**) -* it is called (as an off-chain view call) from `simulateValidation()`. -* The method should return a replacement value for the UserOp.signature, and a value used for aggregation - (the trivial "split" is return "" for the sigForUserOp, and the signature itself for sigForAggregation) -* if the **offChainSigCheck** param is true, it should not validate the signature, and instead return **offChainSigInfo**, - which is used by a companion off-chain library code to validate the signature -* **aggregateSignatures()** must aggregate all "sigForAggregation" into a single value. - This method is a helper method for the bundler. The bundler MAY use a native library to perform the signature aggregation +* If an account uses an aggregator (returns it with getAggregator()), then its address is returned by `simulateValidation()` reverting with `ValidationResultWithAggregator` instead of `ValidationResult` +* To accept the UserOp, the bundler must call **validateUserOpSignature()** to validate the userOp's signature. +* **aggregateSignatures()** must aggregate all UserOp signature into a single value. +* Note that the above methods are helper method 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. This method is called on-chain by `handleOps()` -#### Trusting aggregators and off-chain optimization +#### Using signature aggregators + +An account signify it uses signature aggregation by exposing the aggregator's address in the `getAggregator()` method. +During `simulateValidation`, this aggregator is returned (in the `ValidationResultWithAggregator`) -The aggregators SHOULD stake just like a paymaster. Bundlers MAY throttle down and ban aggregators in case they take too much -resources (or revert) when the above methods are called in view mode. -Alternately, bundlers, MAY "whitelist" specific implementations of aggregators. -Blocking userOp with a banned aggregator is done during [Simulation](#simulation) -The aggregator of a given wallet is exposed by the `getAggregator()` method. -In case the UserOp creates a new wallet, the aggregator is returned by the `simulateValidation()` call. +The bundler should first accept the aggregator (validate its stake info and that it is not throttled/banned) +Then it MUST verify the userOp using `aggregator.validateUserOpSignature()` -To use an off-chain optimized implementation, the bundler MAY: -* call `simulateValidation()` with **offChainSigCheck=true** -* validate the response to have the correct aggregator. -* call off-chain library to validate the signature -* In case the aggregator is trusted, but doesn't support off-chain optimization, the bundler MAY - repeat the simulateValidation, or just call the aggregator.validateUserOpSignature (as a view call) to complete the validation. +Signature aggregator SHOULD stake just like a paymaster, unless it is exempt due to not accessing global storage - see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. Bundlers MAY throttle down and ban aggregators in case they take too much +resources (or revert) when the above methods are called in view mode, or if the signature aggregation fails. ### Required entry point contract functionality There are 2 separate entry point methods: `handleOps` and `handleAggregatedOps` -* `handleOps` handle userOps of wallets that don't require any signature aggregator. +* `handleOps` handle userOps of accounts that don't require any signature aggregator. * `handleAggregatedOps` can handle a batch that contains userOps of multiple aggregators (and also requests without any aggregator) -* `handleAggregatedOps` performs the same logic below as `handleOps`, but it must transfer the correct aggregator to each userOp, and also must call `validateSignatures` on each aggregator after doing all the per-wallet validation. +* `handleAggregatedOps` performs the same logic below as `handleOps`, but it must transfer the correct aggregator to each userOp, and also must call `validateSignatures` on each aggregator after doing all the per-account validation. The entry point'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 wallet if it does not yet exist**, using the initcode provided in the `UserOperation`. If the wallet 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 wallet**, passing in the `UserOperation`, the required fee and aggregator (if there is one). The wallet should verify the operation's signature, and pay the fee if the wallet considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely. -* Validate the wallet's deposit in the entryPoint is high enough to cover the max possible cost (cover the already-done verification and max execution gas) +* **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) In the execution loop, the `handleOps` call must perform the following steps for each `UserOperation`: -* **Call the wallet with the `UserOperation`'s calldata**. It's up to the wallet to choose how to parse the calldata; an expected workflow is for the wallet to have an `execute` function that parses the remaining calldata as a series of one or more calls that the wallet should make. +* **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 an `execute` function that parses the remaining calldata as a series of one or more calls that the account should make. ![](../assets/eip-4337/image1.png) @@ -180,22 +188,22 @@ A node/bundler SHOULD drop (and not add to the mempool) `UserOperation` that fai ### 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 EIP-20 tokens and many other use cases. When the paymaster is not equal to the zero address, the entry point implements a different flow: +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 tokens and many other use cases. When the paymaster is not equal to the zero address, the entry point implements a different flow: ![](../assets/eip-4337/image2.png) -During the verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster is staked, and also 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 `missingWalletFunds` of 0 to reflect that the wallet's deposit is not used for payment for this userOp. +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 execution loop, the `handleOps` execution must call `postOp` on the paymaster after making the main execution call. It must guarantee the execution of `postOp`, by making the main execution inside an inner call context, and if the inner call context reverts attempting to call `postOp` again in an outer call context. +If the paymaster's validatePaymasterUserOp returns a "context", then `handleOps` must call `postOp` on the paymaster after making the main execution call. It must guarantee the execution of `postOp`, by making the main execution inside an inner call context, and if the inner call context reverts attempting to call `postOp` again in an outer call context. -Maliciously crafted paymasters _can_ DoS the system. To prevent this, we use a paymaster reputation system; see the [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-paymasters) for details. +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. The paymaster interface is as follows: ```c++ function validatePaymasterUserOp - (UserOperation calldata userOp, bytes32 requestId, uint256 maxCost) - external returns (bytes memory context); + (UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) + external returns (bytes memory context, uint256 deadline); function postOp (PostOpMode mode, bytes calldata context, uint256 actualGasCost) @@ -208,7 +216,6 @@ enum PostOpMode { } ``` -To prevent attacks involving malicious `UserOperation` objects listing other users' wallets as their paymasters, the entry point contract must require a paymaster to call the entry point to lock their stake and thereby consent to being a paymaster. Unlocking stake must have a delay. The extended interface for the entry point, adding functions for paymasters to add and withdraw stake, is: ```c++ // add a paymaster stake (must be called by the paymaster) @@ -221,7 +228,10 @@ function unlockStake() external function withdrawStake(address payable withdrawAddress) external ``` -The paymaster must also have a deposit, which the entry point will charge UserOperation costs from. The entry point must implement the following interface to allow paymasters (and optionally wallets) manage their deposit: +The paymaster must also have a deposit, which the entry point will charge UserOperation costs from. +The deposit (for paying gas fees) is separate from the stake (which is locked). + +The entry point must implement the following interface to allow paymasters (and optionally accounts) manage their deposit: ```c++ // return the deposit of an account @@ -240,53 +250,89 @@ function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) ext When a client receives a `UserOperation`, it must first run some basic sanity checks, namely that: - Either the `sender` is an existing contract, or the `initCode` is not empty (but not both) +- If `initCode` is not empty, parse its first 20 bytes as a factory address. Record whether the factory is staked, in case the later simulation indicates that it needs to be. If the factory accesses global state, it must be staked - see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. - The `verificationGasLimit` is sufficiently low (`<= MAX_VERIFICATION_GAS`) and the `preVerificationGas` is sufficiently high (enough to pay for the calldata gas cost of serializing the `UserOperation` plus `PRE_VERIFICATION_OVERHEAD_GAS`) -- The paymaster is either the zero address or is a contract which (i) currently has nonempty code on chain, (ii) has registered and staked, (iii) has a sufficient deposit to pay for the UserOperation, and (iv) is not currently banned. +- The `paymasterAndData` is either empty, or start 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](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. - The callgas is at least the cost of a `CALL` with non-zero value. - The `maxFeePerGas` and `maxPriorityFeePerGas` are above a configurable minimum value that the client is willing to accept. At the minimum, they are sufficiently high to be included with the current `block.basefee`. -- The sender doesn't have another `UserOperation` already present in the pool (or it replaces an existing entry with the same sender and nonce, with a higher `maxPriorityFeePerGas` and an equally increased `maxFeePerGas`). Only one `UserOperation` per sender may be included in a single batch. +- The sender doesn't have another `UserOperation` already present in the pool (or it replaces an existing entry with the same sender and nonce, with a higher `maxPriorityFeePerGas` and an equally increased `maxFeePerGas`). Only one `UserOperation` per sender may be included in a single batch. A sender is exempt from this rule and may have multiple `UserOperations` in the pool and in a batch if it is staked (see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) below), but this exception is of limited use to normal accounts. -If the `UserOperation` object passes these sanity checks, the client must next run the first op simulation, and if the simulation succeeds, the client must add the op to the pool. A second simulation must also happen during bundling to make sure that the storage accessed is the same as the `accessList` that was saved during the initial simulation. +If the `UserOperation` object passes these sanity checks, the client must next run the first op simulation, and if the simulation succeeds, the client must add the op to the pool. A second simulation must also happen during bundling to make sure the UserOperation is still valid. ### Simulation -To simulate a `UserOperation` validation, the client makes a view call to `simulateValidation(userop)`, with a "from" address set to all-zeros +#### Rationale + +In order to add a UserOperation into the mempool (and later to add it into a bundle) we need to "simulate" it to make sure it is valid, and that it is capable of paying for its own execution. +In addition, we need to verify that the same will hold true when executed on-chain. +For this purpose, a UserOperation is not allowed to access any information that might change between simulation and execution, such as current block time, number, hash etc. +In addition, a UserOperation is only allowed to access data related to this sender address: Multiple UserOperations should not access the same storage, so that it is impossible to invalidate a large number of UserOperations with a single state change. +There are 3 special contracts that interact with the account: the factory (initCode) that deploys the contract, the paymaster that can pay for the gas, and signature aggregator (described later) +Each of these contracts is also restricted in its storage access, to make sure UserOperation validations are isolated. -If the call returns an error, the client rejects this `userOp`. +#### Specification +To simulate a `UserOperation` validation, the client makes a view call to `simulateValidation(userop)` + +This method always revert with `ValidationResult` as successful response. +If the call reverts with other error, the client rejects this `userOp`. The simulated call performs the full validation, by calling: -1. `wallet.validateUserOp`. -2. if specified a paymaster: `paymaster.validatePaymasterUserOp`. -3. if using an aggregator: `aggregator.validateUserOpSignature`. +1. If `initCode` is present, create the account. +2. `account.validateUserOp`. +3. if specified a paymaster: `paymaster.validatePaymasterUserOp`. + +Either `validateUserOp` or `validatePaymasterUserOp` may return a "deadline", which is the latest timestamp that this UserOperation is valid on-chain. +the simulateValidation call returns the minimum of those deadlines. +A node MAY drop a UserOperation if the deadline is too soon (e.g. wouldn't make it to the next block) The operations differ in their opcode banning policy. -In order to distinguish between them, there is a call to the NUMBER opcode (`block.number`), used as a delimiter between the validation functions. +In order to distinguish between them, there is a call to the NUMBER opcode (`block.number`), used as a delimiter between the 3 functions. While simulating `userOp` validation, the client should make sure that: -1. Neither call's execution trace invokes any **forbidden opcodes** -2. The first (validateUserOp) call does not access _mutable state_ of any contract except the wallet itself and its deposit in the entry point contract. _Mutable state_ definition includes both storage and balance. -3. The second (validatePaymasterUserOp) call does not access _mutable state_ of any contract except the paymaster itself. The paymaster is also not allowed to make any state-change calls (SSTORE) - A bundler MAY whitelist specific paymaster addresses, and not enforce the above storage limitations -4. The third (validateUserOpSignature) view-call doesn't access _mutable state_ of any contract except the aggregator AND wallet itself. -5. The third (validateUserOpSignature) is the first view call after the "NUMBER" marker. If that aggregator is banned, then this staticcall MUST revert immediately, as if it was called with zero gas. -6. Any `CALL` or `CALLCODE` during validation has `value=0`, except for the transfer from the wallet to the entry point. -7. No `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` results in an out-of-gas revert. -8. No `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` to addresses with `EXTCODESIZE=0`. -9. Any `GAS` opcode is followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }. -10. `EXTCODEHASH` of every address accessed (by any opcode) does not change between first and second simulations of the op. -11. If `op.initcode.length != 0` , allow only one `CREATE2` opcode call (in the validateUserOp block), otherwise forbid `CREATE2`. - -Since the wallet is allowed to access its own entry point deposit in order to top it up when needed, the client must know the storage slot in order to whitelist it. The entry point therefore implements the following view function: - -```c++ -function getSenderStorage(address sender) external view returns (uint256[] memory senderStorageCells) -``` +1. May not invokes any **forbidden opcodes** +2. Must not use GAS opcode (unless followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }.) +3. Storage access is limited as follows: + 1. self storage (of factory/paymaster, respectively) is allowed, but only if self entity is staked + 2. account storage access is allowed (see Storage access by Slots, below), + 3. in any case, may not use storage used by another UserOp `sender` in the same bundle (that is, paymaster and factory are not allowed as senders) +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) + 4. cannot call EntryPoint's `handleOps` method (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`. + +#### 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. +In solidity, this includes all storage of the contract itself, and any storage of other contracts that use this contract address as a mapping key. + +An address `A` is associated with: +1. Slots of contract `A` address itself. +2. Slot `A` on any other address. +3. Slots of type `keccak256(A || X) + n` on any other address. (to cover `mapping(address => value)`, which is usually used for balance in ERC20 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. +However, there might be use-cases where specific paymasters (and signature aggregators) 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" 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". +UserOperations that use whitelisted paymasters (or signature aggregators) are put into a separate mempool. +Only bundlers that support this whitelist will use UserOperations from this mempool. +These UserOperations can be bundled together with UserOperations from the main mempool ### Bundling During bundling, the client should: -- Exclude UserOps that access any sender address created by another UserOp on the same batch (via CREATE2 factory). +- Exclude UserOps that access any sender address created by another UserOp on the same batch (via a factory). - For each paymaster used in the batch, keep track of the balance while adding UserOps. Ensure that it has sufficient deposit to pay for all the UserOps that use it. - Sort UserOps by aggregator, to create the lists of UserOps-per-aggregator. - For each aggregator, run the aggregator-specific code to create aggregated signature, and update the UserOps @@ -295,7 +341,7 @@ After creating the batch, before including the transaction in a block, the clien - 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. Other ops from the same paymaster should be removed from the current batch, but kept in the mempool. Repeat until `eth_estimateGas` succeeds. -In practice, restrictions (2) and (3) basically mean that the only external accesses that the wallet 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). +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). 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` @@ -303,27 +349,48 @@ When a bundler includes a bundle in a block it must ensure that earlier transact #### Forbidden opcodes -The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the wallet, paymaster, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain. +The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the factory, account, paymaster, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`, `SELFDESTRUCT`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain. Exceptions to the forbidden opcodes: 1. A single `CREATE2` is allowed if `op.initcode.length != 0` and must result in the deployment of a previously-undeployed `UserOperation.sender`. 2. `GAS` is allowed if followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }. (that is, making calls is allowed, using `gasleft()` or `gas` opcode directly is forbidden) -### Reputation scoring and throttling/banning for paymasters +### Reputation scoring and throttling/banning for global entities +#### Rationale + +UserOperation's storage access rules prevent them from interfere with each other. +But "global" entities - paymasters, factories and aggregators 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 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, and can be withdrawn any time (after unstake delay) +The only exemption from staking is if the entity doesn't use storage during validation. +(unstaked entity may use storage [associated with the sender](#storage-associated-with-an-address)) +When staked, an entity is also allowed to use its own associated storage, in addition to sender's associated storage. + +The stake value is not enforced on-chain, but specifically by each node while simulating a transaction. +The stake is expected to be above MIN_STAKE_VALUE, and unstake delay above MIN_UNSTAKE_DELAY +The value of MIN_UNSTAKE_DELAY is 84600 (one day) +The value of MIN_STAKE_VALUE is determined per chain, and specified in the "bundler specification test suite" -Clients maintain two mappings with a value for each paymaster: +#### Specification + +In the following specification, "entity" is either address that is explicitly referenced by the UserOperation: sender, factory, paymaster and aggregator. +Clients maintain two mappings with a value for staked entities: * `opsSeen: Map[Address, int]` * `opsIncluded: Map[Address, int]` -When the client learns of a new `paymaster`, it sets `opsSeen[paymaster] = 0` and `opsIncluded[paymaster] = 0` . +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` . -The client sets `opsSeen[paymaster] +=1` each time it adds an op with that `paymaster` to the `UserOperationPool`, and the client sets `opsIncluded[paymaster] += 1` each time an op that was in the `UserOperationPool` is included on-chain. +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. -Every hour, the client sets `opsSeen[paymaster] -= opsSeen[paymaster] // 24` and `opsIncluded[paymaster] -= opsIncluded[paymaster] // 24` for all paymasters (so both values are 24-hour exponential moving averages). +Every hour, the client sets `opsSeen[entity] -= opsSeen[entity] // 24` and `opsIncluded[entity] -= opsIncluded[entity] // 24` for all entities (so both values are 24-hour exponential moving averages). -We define the **status** of a paymaster as follows: +We define the **status** of an entity as follows: ```python OK, THROTTLED, BANNED = 0, 1, 2 @@ -342,7 +409,7 @@ def status(paymaster: Address, return BANNED ``` -Stated in simpler terms, we expect at least `1 / MIN_INCLUSION_RATE_DENOMINATOR` of all ops seen on the network to get included. If a paymaster falls too far behind this minimum, the paymaster gets **throttled** (meaning, the client does not accept ops from that paymaster if there is already an op from that paymaster, and an op only stays in the pool for 10 blocks), If the paymaster falls even further behind, it gets **banned**. Throttling and banning naturally reverse over time because of the exponential-moving-average rule. +Stated in simpler terms, we expect at least `1 / MIN_INCLUSION_RATE_DENOMINATOR` of all ops seen on the network to get included. If an entity falls too far behind this minimum, it gets **throttled** (meaning, the client does not accept ops from that paymaster if there is already an op with that entity, and an op only stays in the pool for 10 blocks), If the entity falls even further behind, it gets **banned**. Throttling and banning naturally decay over time because of the exponential-moving-average rule. **Non-bundling clients and bundlers should use different settings for the above params**: @@ -354,16 +421,84 @@ Stated in simpler terms, we expect at least `1 / MIN_INCLUSION_RATE_DENOMINATOR` To help make sense of these params, note that a malicious paymaster can at most cause the network (only the p2p network, not the blockchain) to process `BAN_SLACK * MIN_INCLUSION_RATE_DENOMINATOR / 24` non-paying ops per hour. -### RPC methods +## Rationale -`eth_sendUserOperation` +The main challenge with a purely smart contract wallet 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. -eth_sendUserOperation submits a User Operation object to the User Operation pool of the client. An entryPoint address `MUST` be specified, and the client `MUST` only simulate and submit the User Operation through the specified entryPoint. +In this proposal, we expect accounts to have a `validateUserOp` method that takes as input a `UserOperation`, and verify the signature and pay the fee. This method is required to be almost-pure: it is only allowed to access the storage of the account itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the account, and can also send out ETH (needed to pay the entry point). The method is gas-limited by the `verificationGasLimit` of the `UserOperation`; nodes can choose to reject operations whose `verificationGasLimit` is too high. These restrictions allow block builders and network nodes to simulate the verification step locally, and be confident that the result will match the result when the operation actually gets included into a block. -The result `SHOULD` be set to true if and only if the request passed simulation and was accepted in the client's User Operation pool. If the validation, simulation, or User Operation pool inclusion fails, `result` `SHOULD NOT` be returned. Rather, the client `SHOULD` return the failure reason. +The entry point-based approach allows for a clean separation between verification and execution, and keeps accounts' logic simple. The alternative would be to require accounts to follow a template where they first self-call to verify and then self-call to execute (so that the execution is sandboxed and cannot cause the fee payment to revert); template-based approaches were rejected due to being harder to implement, as existing code compilation and verification tooling is not designed around template verification. +### Paymasters + +Paymasters facilitate transaction sponsorship, allowing third-party-designed mechanisms to pay for transactions. Many of these mechanisms _could_ be done by having the paymaster wrap a `UserOperation` with their own, but there are some important fundamental limitations to that approach: + +* No possibility for "passive" paymasters (eg. that accept fees in some ERC-20 token at an exchange rate pulled from an on-chain DEX) +* Paymasters run the risk of getting griefed, as users could send ops that appear to pay the paymaster but then change their behavior after a block + +The paymaster scheme allows a contract to passively pay on users' behalf under arbitrary conditions. It even allows ERC-20 token paymasters to secure a guarantee that they would only need to pay if the user pays them: the paymaster contract can check that there is sufficient approved ERC-20 balance in the `validatePaymasterUserOp` method, and then extract it with `transferFrom` in the `postOp` call; if the op itself transfers out or de-approves too much of the ERC-20s, the inner `postOp` will fail and revert the execution and the outer `postOp` can extract payment (note that because of storage access restrictions the ERC-20 would need to be a wrapper defined within the paymaster itself). + +### First-time account creation + +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 wallet; they can simply generate an address locally and immediately start accepting funds. + +The wallet creation itself is done by a "factory" contract, with wallet-specific data. +The factory is expected to use CREATE2 (not CREATE) to create the wallet, so that the order of creation of wallets doesn't interfere with the generated addresses. +The `initCode` field (if non-zero length) is parsed as a 20-byte address, followed by "calldata" to pass to this address. +This method call is expected to create a wallet and return its address. +If the factory does use CREATE2 or some other deterministic method to create the wallet, it's expected to return the wallet address even if the wallet has already been created. This is to make it easier for clients to query the address without knowing if the wallet 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 (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. +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. + +NOTE: In order for the wallet to determine the "counterfactual" address of the wallet (prior its creation), +it should make a static call to the `entryPoint.getSenderAddress()` + +### Entry point upgrading + +Accounts are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow account upgradability. The account code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their account's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that two mempools will run in parallel. + +## RPC methods + +### eth Namespace +#### * eth_sendUserOperation + +eth_sendUserOperation submits a User Operation object to the User Operation pool of the client. The client MUST validate the UserOperation, and return a result accordingly. + +The result `SHOULD` be set to the **userOpHash** if and only if the request passed simulation and was accepted in the client's User Operation pool. If the validation, simulation, or User Operation pool inclusion fails, `result` `SHOULD NOT` be returned. Rather, the client `SHOULD` return the failure reason. + +##### Parameters: +1. **UserOperation** a full user-operation struct. All fields MUST be set as hex values. empty `bytes` block (e.g. empty `initCode`) MUST be set to `"0x"` +2. **EntryPoint** the entrypoint address the request should be sent through. this MUST be one of the entry points returned by the `supportedEntryPoints` rpc call. + +##### Return value: +* If the UserOperation is valid, the client MUST return the calculated **userOpHash** for it +* in case of failure, MUST return an `error` result object, with `code` and `message`. The error code and message SHOULD be set as follows: + * **code: -32602** - invalid UserOperation struct/fields + * **code: -32500** - transaction rejected by entryPoint's simulateValidation, during wallet creation or validation + * The `message` field MUST be set to the FailedOp's "`AAxx`" error message from the EntryPoint + * **code: -32501** - transaction rejected by paymaster's validatePaymasterUserOp + * The `message` field SHOULD be set to the revert message from the paymaster + * The `data` field MUST contain a `paymaster` value + * **code: -32502** - transaction rejected because of opcode validation + * **code: -32503** - UserOperation expires shortly: either wallet or paymaster returned an `deadline` that will expire soon + * The `data` field SHOULD contain a `deadline` value + * The `data` field SHOULD contain a `paymaster` value, if this error was triggered by the paymaster + * **code: -32504** - transaction rejected because paymaster (or signature aggregator) is throttled/banned + * The `data` field SHOULD contain a `paymaster` or `aggregator` value, depending on the failed entity + * **code: -32505** - transaction rejected because paymaster (or signature aggregator) stake or unstake-delay is too low + * The `data` field SHOULD contain a `paymaster` or `aggregator` value, depending on the failed entity + * The `data` field SHOULD contain a `minimumStake` and `minimumUnstakeDelay` + * **code: -32506** - transaction rejected because wallet specified unsupported signature aggregator + * The `data` field SHOULD contain an `aggregator` value + +##### Example: +Request: ```json= -# Request { "jsonrpc": "2.0", "id": 1, @@ -386,17 +521,103 @@ The result `SHOULD` be set to true if and only if the request passed simulation ] } -# Response +``` +Response: +``` { "jsonrpc": "2.0", "id": 1, - "result": true + "result": "0x1234...5678" } ``` -`eth_supportedEntryPoints` +##### Example failure responses: +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "message": "AA21 didn't pay prefund", + "code": -32500 + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "message": "paymaster stake too low", + "data": { + "paymaster": "0x123456789012345678901234567890123456790", + "minimumStake": "0xde0b6b3a7640000", + "minimumUnstakeDelay": "0x15180" + }, + "code": -32504 + } +} +``` + + +#### * eth_estimateUserOperationGas + +Estimate the gas values for a UserOperation. +Given UserOperation optionally without gas limits and gas prices, return the needed gas limits. +The signature field is ignored by the wallet, so that the operation will not require user's approval. +Still, it might require putting a "semi-valid" signature (e.g. a signature in the right length) + +**Parameters**: same as `eth_sendUserOperation` + gas limits (and prices) parameters are optional, but are used if specified. + `maxFeePerGas` and `maxPriorityFeePerGas` default to zero, so no payment is required by neither account nor paymaster. + +##### Return Values: +* **preVerificationGas** gas overhead of this UserOperation +* **verificationGasLimit** actual gas used by the validation of this UserOperation +* **callGasLimit** value used by inner account execution + +##### Error Codes: +Same as `eth_sendUserOperation` +This operation may also return an error if the inner call to the account contract reverts. + +#### * eth_getUserOperationByHash + +Return a UserOperation based on a hash (userOpHash) returned by `eth_sendUserOperation` + +**Parameters** +* **hash** a userOpHash value returned by `eth_sendUserOperation` + +**Return value**: + +`null` in case the UserOperation is not yet included in a block, or a full UserOperation, with the addition of `entryPoint`, `blockNumber`, `blockHash` and `transactionHash` + +#### * eth_getUserOperationReceipt + +Return a UserOperation receipt based on a hash (userOpHash) returned by `eth_sendUserOperation` + +**Parameters** +* **hash** a userOpHash value returned by `eth_sendUserOperation` + +**Return value**: + +`null` in case the UserOperation is not yet included in a block, or: + +* **userOpHash** the request hash +* **entryPoint** +* **sender** +* **nonce** +* **paymaster** the paymaster used for this userOp (or empty) +* **actualGasCost** - actual amount paid (by account or paymaster) for this UserOperation +* **actualGasUsed** - total gas used by this UserOperation (including preVerification, creation, validation and execution) +* **success** boolean - did this execution completed without revert +* **reason** in case of revert, this is the revert reason +* **logs** the logs generated by this UserOperation (not including logs of other UserOperations in the same bundle) +* **receipt** the TransactionReceipt object. + Note that the returned TransactionReceipt is for the entire bundle, not only for this UserOperation. + +#### * eth_supportedEntryPoints -eth_supportedEntryPoints returns an array of the entryPoint addresses supported by the client. The first element of the array `SHOULD` be the entryPoint addressed preferred by the client. +Returns an array of the entryPoint addresses supported by the client. The first element of the array `SHOULD` be the entryPoint addressed preferred by the client. ```json= # Request @@ -417,63 +638,245 @@ eth_supportedEntryPoints returns an array of the entryPoint addresses supported ] } ``` +#### * eth_chainId -## Rationale +Returns EIP155 Chain ID. -The main challenge with a purely smart contract wallet based account abstraction system is DoS safety: how can a miner including an operation make sure that it will actually pay fees, without having to first execute the entire operation? Requiring the miner 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. +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_chainId", + "params": [] +} -In this proposal, we expect wallets to have a `validateUserOp` method that takes as input a `UserOperation`, and verify the signature and pay the fee. This method is required to be almost-pure: it is only allowed to access the storage of the wallet itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the wallet, and can also send out ETH (needed to pay the entry point). The method is gas-limited by the `verificationGasLimit` of the `UserOperation`; nodes can choose to reject operations whose `verificationGasLimit` is too high. These restrictions allow miners and network nodes to simulate the verification step locally, and be confident that the result will match the result when the operation actually gets included into a block. +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" +} +``` -The entry point-based approach allows for a clean separation between verification and execution, and keeps wallets' logic simple. The alternative would be to require wallets to follow a template where they first self-call to verify and then self-call to execute (so that the execution is sandboxed and cannot cause the fee payment to revert); template-based approaches were rejected due to being harder to implement, as existing code compilation and verification tooling is not designed around template verification. +### debug Namespace -### Paymasters +This api must only be available on testing mode and is required by the compatibility test suite. In production, any `debug_*` rpc calls should be blocked. -Paymasters facilitate transaction sponsorship, allowing third-party-designed mechanisms to pay for transactions. Many of these mechanisms _could_ be done by having the paymaster wrap a `UserOperation` with their own, but there are some important fundamental limitations to that approach: +#### * debug_bundler_clearState -* No possibility for "passive" paymasters (eg. that accept fees in some EIP-20 token at an exchange rate pulled from an on-chain DEX) -* Paymasters run the risk of getting griefed, as users could send ops that appear to pay the paymaster but then change their behavior after a block +Clears the bundler mempool and reputation data of paymasters/accounts/factories/aggregators. -The paymaster scheme allows a contract to passively pay on users' behalf under arbitrary conditions. It even allows EIP-20 token paymasters to secure a guarantee that they would only need to pay if the user pays them: the paymaster contract can check that there is sufficient approved EIP-20 balance in the `validatePaymasterUserOp` method, and then extract it with `transferFrom` in the `postOp` call; if the op itself transfers out or de-approves too much of the EIP-20s, the inner `postOp` will fail and revert the execution and the outer `postOp` can extract payment (note that because of storage access restrictions the EIP-20 would need to be a wrapper defined within the paymaster itself). +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_clearState", + "params": [] +} -### First-time wallet creation +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "ok" +} +``` -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 wallet; they can simply generate an address locally and immediately start accepting funds. +#### * debug_bundler_dumpMempool -The wallet creation itself is done by a "factory" contract, with wallet-specific data. -The factory is expected to use CREATE2 (not CREATE) to create the wallet, so that the order of creation of wallets doesn't interfere with the generated addresses. -The `initCode` field (if non-zero length) is parsed as a 20-byte address, followed by "calldata" to pass to this address. -This method call is expected to create a wallet and return its address. -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. -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. +Dumps the current UserOperations mempool -NOTE: In order for the wallet to determine the "counterfactual" address of the wallet (prior its creation), -it should make a static call to the `entryPoint.createSender()` +##### Parameters: + +- **EntryPoint** the entrypoint used by eth_sendUserOperation + +##### Returns: + +`array` - Array of UserOperations currently in the mempool. + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_dumpMempool", + "params": ["0x1306b01bC3e4AD202612D3843387e94737673F53"] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + sender, // address + nonce, // uint256 + initCode, // bytes + callData, // bytes + callGasLimit, // uint256 + verificationGasLimit, // uint256 + preVerificationGas, // uint256 + maxFeePerGas, // uint256 + maxPriorityFeePerGas, // uint256 + paymasterAndData, // bytes + signature // bytes + } + ] +} +``` + +#### * debug_bundler_sendBundleNow + +Forces the bundler to build and execute a bundle from the mempool as `handleOps()` transaction. + +Returns: `transactionHash` + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_sendBundleNow", + "params": [] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0xdead9e43632ac70c46b4003434058b18db0ad809617bd29f3448d46ca9085576" +} +``` + +#### * debug_bundler_setBundlingMode + +Sets bundling mode. + +After setting mode to "manual", an explicit call to debug_bundler_sendBundleNow is required to send a bundle. + +##### parameters: + +`mode` - 'manual' | 'auto' + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_setBundlingMode", + "params": ["manual"] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "ok" +} +``` + +#### * debug_bundler_setReputation + +Sets reputation of given addresses. parameters: + +##### Parameters: + +- An array of reputation entries to add/replace, with the fields: + + - `address` - The address to set the reputation for. + - `opsSeen` - number of times a user operations with that entity was seen and added to the mempool + - `opsIncluded` - number of times a user operations that uses this entity was included on-chain + - `status` - (string) The status of the address in the bundler 'ok' | 'throttled' | 'banned'. + +- **EntryPoint** the entrypoint used by eth_sendUserOperation + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_setReputation", + "params": [ + [ + { + "address": "0x7A0A0d159218E6a2f407B99173A2b12A6DDfC2a6", + "opsSeen": 20, + "opsIncluded": 13 + } + ], + "0x1306b01bC3e4AD202612D3843387e94737673F53" + ] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "ok" +} +``` + + +#### * debug_bundler_dumpReputation + +Returns the reputation data of all observed addresses. +Returns an array of reputation objects, each with the fields described above in `debug_bundler_setReputation` with the -### Entry point upgrading -Wallets are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow wallet upgradability. The wallet code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their wallet's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that two mempools will run in parallel. +##### Parameters: + +- **EntryPoint** the entrypoint used by eth_sendUserOperation + +##### Return value: + +An array of reputation entries with the fields: + +- `address` - The address to set the reputation for. +- `opsSeen` - number of times a user operations with that entity was seen and added to the mempool +- `opsIncluded` - number of times a user operations that uses this entity was included on-chain +- `status` - (string) The status of the address in the bundler 'ok' | 'throttled' | 'banned'. + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_dumpReputation", + "params": ["0x1306b01bC3e4AD202612D3843387e94737673F53"] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": [ + { "address": "0x7A0A0d159218E6a2f407B99173A2b12A6DDfC2a6", + "opsSeen": 20, + "opsIncluded": 19, + "status": "ok" + } + ] +} +``` ## Backwards Compatibility -This EIP 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-4337 wallets, because those wallets do not have a `validateUserOp` function. If the wallet has a function for authorizing a trusted op submitter, then this could be fixed by creating an 4337-compatible wallet that re-implements the verification logic as a wrapper and setting it to be the original wallet'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 pre-ERC-4337 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-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` +See [https://github.com/eth-infinitism/account-abstraction/tree/main/contracts](https://github.com/eth-infinitism/account-abstraction/tree/main/contracts) -## Security Considerations +## 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) wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ 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. +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 accounts. 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): -* **Safety against arbitrary hijacking**: The entry point only calls a wallet generically if `validateUserOp` to that specific wallet has passed (and with `op.calldata` equal to the generic call's calldata) +* **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` ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). - +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).