From 9be7526883f4fabaa44f4b56db4278d71778dcca Mon Sep 17 00:00:00 2001 From: vbuterin Date: Fri, 8 Oct 2021 06:28:21 +0300 Subject: [PATCH] ERC: Account Abstraction via Entry Point Contract specification (#4337) * Account Abstraction via Entry Point Contract specification An account abstraction proposal which completely avoids the need for consensus-layer protocol changes, instead relying on a separate mempool of `UserOperation` objects and miners either running custom code or connecting to a bundle marketplace. * Rename file to match ERC number * Update eip-4337.md * Apply suggestions from code review Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> * Replaced simple summary with abstract * Apply suggestions from code review * Changing to relative links for images * Update EIPS/eip-4337.md * Added mandatory sections * reference EIP-2470 (create2 deployer) (#4344) * Made EIP links relative * Update EIPS/eip-4337.md Co-authored-by: Micah Zoltu * Update EIPS/eip-4337.md Co-authored-by: Micah Zoltu * Update EIPS/eip-4337.md * Update EIPS/eip-4337.md * Update EIPS/eip-4337.md * add images for eip-4337 Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> Co-authored-by: Dror Tirosh Co-authored-by: Micah Zoltu Co-authored-by: lightclient@protonmail.com --- EIPS/eip-4337.md | 331 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 EIPS/eip-4337.md diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md new file mode 100644 index 00000000000000..8adc0ff6b7ee6b --- /dev/null +++ b/EIPS/eip-4337.md @@ -0,0 +1,331 @@ +--- +eip: 4337 +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) +discussions-to: https://ethereum-magicians.org/t/erc-4337-account-abstraction-via-entry-point-contract-specification/7160 +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. + +## Motivation + +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 both require) +* **Decentralization** + * Allow any bundler (think: miner) 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 ERC-20 tokens, allow developers to pay fees for their users, and [EIP-3074](./eip-3074.md)-like **sponsored transaction** use cases more generally + +## 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 wallet 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 (only needed if the wallet is not yet on-chain and needs to be created) | +| `callData` | `bytes` | The data to pass to the `sender` during the main execution call | +| `callGas` | `uint256` | The amount of gas to allocate the main execution call | +| `verificationGas` | `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 for pre-verification execution and calldata | +| `maxFeePerGas` | `uint256` | Maximum fee per gas (similar to EIP 1559 `max_fee_per_gas`) | +| `maxPriorityFeePerGas` | `uint256` | Maximum priority fee per gas (similar to EIP 1559 `max_priority_fee_per_gas`) | +| `paymaster` | `address` | Address sponsoring the transaction (or zero for regular self-sponsored transactions) | +| `paymasterData` | `bytes` | Extra data to send to the paymaster | +| `signature` | `bytes` | Data passed into the wallet 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**. + +The core interface of the entry point contract is as follows: + +```c++ +function handleOps + (UserOperation[] calldata ops, address payable redeemer) + external; + +function simulateWalletValidation + (UserOperation calldata userOp) + external returns (uint gasUsedByPayForSelfOp); + +function simulatePaymasterValidation + (UserOperation calldata userOp, uint gasUsedByPayForSelfOp) + external view returns (bytes memory context, uint gasUsedByPayForOp); +``` + +The core interface required for a wallet to have is: + +```c++ +function validateUserOp + (UserOperation calldata userOp, uint requiredPrefund) + external; +``` + +### Required entry point contract functionality + +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, the call must fail) +* **Call `validateUserOp` on the wallet**, passing in the `UserOperation` and the required fee. 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. + +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 worlflow 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. +* **Refund unused gas fees** to the wallet + +![](../assets/eip-4337/image1.png) + +Before accepting a `UserOperation`, bundlers must use an RPC method to locally simulate calling the `simulateWalletValidation` function of the entry point, to verify that the signature is correct and the operation actually pays fees; see the [Simulation section below](#simulation) for details. + +### Extension: paymasters + +We extend the entry point logic to support **paymasters** that can sponsor transactions for other users. This feature can be used to allow application developers to subsidize fees for their users, allow users to pay fees with ERC-20 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 has enough ETH staked 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. Additionally, the `validateUserOp` must be called with a `requiredPrefund` of 0 to reflect that it's the paymaster, and not the wallet, that's paying the fees. + +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. + +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. + +The paymaster interface is as follows: + +```c++ +function validatePaymasterUserOp + (UserOperation calldata userOp, uint maxcost) + external view returns (bytes memory context); + +function postOp + (PostOpMode mode, bytes calldata context, uint actualGasCost) + external; + +enum PostOpMode { + opSucceeded, // user op succeeded + opReverted, // user op reverted. still has to pay for gas. + postOpReverted // user op succeeded, but caused postOp to revert +} +``` + +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++ +function addStake() external payable +function lockStake() external +function unlockStake(address paymaster) external +function withdrawStake(address payable withdrawAddress) external +``` + +### Client behavior upon receiving a UserOperation + +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) +- The `verificationGas` 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 has sufficient stake, and (iii) has not been banned +- 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 +- The `UserOperation` object is not already present in the pool (or it replaces an existing entry with the same sender, nonce with a higher priority) + +If the `UserOperation` object passes these sanity checks, the client must next simulate the op, and if the simulation succeeds, the client must add the op to the pool. 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. + +### Simulation + +To simulate a `UserOperation` `op`, the client makes an `eth_call` with the following params: + +```python +{ + "from": 0x0000000000000000000000000000000000000000, + "to": [entry point address], + "input": [simulateWalletValidation header] + serialize(op), + "gas": op.verificationGas, +} +``` + +If the call returns an error, the client rejects the `op`. If it succeeds, the output is interpreted as an integer `gasUsedByPayForSelfOp`. If `op.paymaster != ZERO_ADDRESS`, the client must then make an `eth_call` with the following params: + +```python +{ + "from": 0x0000000000000000000000000000000000000000, + "to": [entry point address], + "input": [simulatePaymasterValidation header] + + abi_encode(serialize(op), gasUsedByPayForSelfOp), + "gas": op.verificationGas - gasUsedByPayForSelfOp, +} +``` + +While simulating, the client should make sure that: + +1. Neither call's execution trace invokes any **forbidden opcodes** +2. The first call does not access _mutable state_ of any contract except the wallet itself +3. The second call does not access _mutable state_ of any contract except the paymaster itself + +If a contract (i) does not contain the `SELFDESTRUCT` or `DELEGATECALL` opcode [except perhaps inside pushdata] and (ii) has nonempty code, _mutable state_ of that contract is defined as storage slots (accessed with SLOAD or SSTORE). If a contract does not satisfy either of the above conditions, mutable state of that contract includes code and storage. Note that balance can not be read in any case because of the forbidden opcode restriction. _Writing_ balance (via value-bearing calls) to any address is not restricted. + +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). + +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` + +#### 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`. 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. + +### Reputation scoring and throttling/banning for paymasters + +Clients maintain two mappings with a value for each paymaster: + +* `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` . + +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. + +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). + +We define the **status** of a paymaster as follows: + +```python +OK, THROTTLED, BANNED = 0, 1, 2 + +def status(paymaster: Address, + opsSeen: Map[Address, int], + opsIncluded: Map[Address, int]): + if paymaster not in opsSeen: + return OK + min_expected_included = opsSeen[paymaster] // MIN_INCLUSION_RATE_DENOMINATOR + if min_expected_included + THROTTLING_SLACK >= opsIncluded[paymaster]: + return OK + elif min_expected_included + BAN_SLACK >= opsIncluded[paymaster]: + return THROTTLED + else: + 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. + +**Non-bundling clients and bundlers should use different settings for the above params**: + +| Param | Client setting | Bundler setting | +| - | - | - | +| `MIN_INCLUSION_RATE_DENOMINATOR` | 100 | 10 | +| `THROTTLING_SLACK` | 10 | 10 | +| `BAN_SLACK` | 50 | 50 | + +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 + +`eth_sendUserOperation` + +```json= +{ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_sendUserOperation", + "params": [ + { + sender, // Address + nonce, // uint256 + initCode, // bytes + callData, //bytes + callGas, // uint256 + verificationGas, // uint256 + maxFeePerGas, // uint256 + maxPriorityFeePerGas, // uint256 + paymaster, // address + paymasterData, // bytes + signature // bytes + } + ] +} +``` + +`eth_callUserOperation` + +```json= +{ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_callUserOperation", + "params": [ + { + sender, // Address + nonce, // uint256 + initCode, // bytes + callData, //bytes + callGas, // uint256 + verificationGas, // uint256 + maxFeePerGas, // uint256 + maxPriorityFeePerGas, // uint256 + paymaster, // address + paymasterData, // bytes + signature, // bytes + blockNumber, // hex-encoded uint256 + } + ] +} +``` + +## Rationale + +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 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 a operation will pay a fee before they are willing to forward it. + +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 `verificationGas` of the `UserOperation`; nodes can choose to reject operations whose `verificationGas` 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. + +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. + +### 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 wallet 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. + +This is accomplished by having the entry point itself create wallets using CREATE2. The `UserOperation` struct has an `initCode` field; this field would be empty for all operations by a given wallet after the first, but the first operation would fill in the `initCode`. The entry point uses [EIP-2470](./eip-2470.md) deployer contract to create the wallet, and then performs the operation. The user can compute the address of their wallet by locally running the [EIP 1014](./eip-1014.md) CREATE2 address formula. The salt used is the `nonce` of the `UserOperation`. +(The entry point contract has a utility method `getSenderAddress()` for that purpose) + +### 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. + +## 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 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 ERC-4337-compatible wallet that re-implements the verification logic as a wrapper and setting it to be the original wallet's trusted op submitter. + +## Reference Implementation + +See https://github.com/opengsn/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 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. + +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 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](https://creativecommons.org/publicdomain/zero/1.0/).