diff --git a/ERCS/erc-4337.md b/ERCS/erc-4337.md index 72c0c4ddf2..884df9057e 100644 --- a/ERCS/erc-4337.md +++ b/ERCS/erc-4337.md @@ -2,13 +2,13 @@ eip: 4337 title: Account Abstraction Using Alt Mempool description: An account abstraction proposal which completely avoids consensus-layer protocol changes, instead relying on higher-layer infrastructure. -author: Vitalik Buterin (@vbuterin), Yoav Weiss (@yoavw), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), Alex Forshtat (@forshtat), Kristof Gazso (@kristofgazso), Tjaden Hess (@tjade273) +author: Vitalik Buterin (@vbuterin), Yoav Weiss (@yoavw), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), Alex Forshtat (@forshtat), Kristof Gazso (@kristofgazso), Tjaden Hess (@tjade273), Tim Pechersky (@peersky) discussions-to: https://ethereum-magicians.org/t/erc-4337-account-abstraction-via-entry-point-contract-specification/7160 status: Draft type: Standards Track category: ERC created: 2021-09-29 -requires: 7562 +requires: 7562, 7746 --- ## Abstract @@ -117,23 +117,13 @@ struct UserOpsPerAggregator { ### Account Contract Interface -The core interface required for an account to have is: - -```solidity -interface IAccount { - function validateUserOp - (PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external returns (uint256 validationData); -} -``` - -The `userOpHash` is a hash over the userOp (except signature), entryPoint and chainId. +The core interface required for an account is defined in [ERC-7746](./eip-7746). The account: * MUST validate the caller is a trusted EntryPoint -* If the account does not support signature aggregation, it MUST validate that the signature is a valid signature of the `userOpHash`, and - SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error MUST revert. +* If the account does not support signature aggregation, it MUST validate that the signature is a valid signature of the `beforeCall`, and + SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. **(ToDo: [ERC-7746](./eip-7746.md) defines that validating contracts MUST revert if validation fails, if catch clause used in 4337 implementation it can preserve current functional requirements)**. Any other error MUST revert. * MUST pay the entryPoint (caller) at least the "missingAccountFunds" (which might be zero, in case the current account's deposit is high enough) * The account MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it) * The return value MUST be packed of `authorizer`, `validUntil` and `validAfter` timestamps. @@ -141,7 +131,7 @@ The account: * `validUntil` is 6-byte timestamp value, or zero for "infinite". The UserOp is valid only up to this time. * `validAfter` is 6-byte timestamp. The UserOp is valid only after this time. -An account that works with aggregated signature, should return its signature aggregator address in the "sigAuthorizer" return value of validateUserOp. +An account that works with aggregated signature, should return its signature aggregator address "sigAuthorizer" return value of beforeCall. It MAY ignore the signature field. The account MAY implement the interface `IAccountExecute` @@ -232,7 +222,7 @@ The entry point's `handleOps` function must perform the following steps (we firs * **Create the account if it does not yet exist**, using the initcode provided in the `UserOperation`. If the account does not exist, _and_ the initcode is empty, or does not deploy a contract at the "sender" address, the call must fail. * calculate the maximum possible fee the account needs to pay (based on validation and call gas limits, and current gas values) * calculate the fee the account must add to its "deposit" in the EntryPoint -* **Call `validateUserOp` on the account**, passing in the `UserOperation`, its hash and the required fee. The account should verify the operation's signature, and pay the fee if the account considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely. +* **Perform [ERC-7746](./eip-7746.md) validation on the account**, passing in the `UserOperation`, its hash and the required fee. The account should verify the operation's signature, and pay the fee if the account considers the operation valid. If `ERC-7746` call reverts, `handleOps` must skip execution of at least that operation, and may revert entirely. * Validate the account's deposit in the entryPoint is high enough to cover the max possible cost (cover the already-done verification and max execution gas) In the execution loop, the `handleOps` call must perform the following steps for each `UserOperation`: @@ -255,28 +245,15 @@ We extend the entry point logic to support **paymasters** that can sponsor trans ![](../assets/eip-4337/bundle-seq-pm.svg) -During the verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster has enough ETH deposited with the entry point to pay for the operation, and then call `validatePaymasterUserOp` on the paymaster to verify that the paymaster is willing to pay for the operation. Note that in this case, the `validateUserOp` is called with a `missingAccountFunds` of 0 to reflect that the account's deposit is not used for payment for this userOp. +During the verification loop, the `handleOps` execution must check that the paymaster has enough ETH deposited with the entry point to pay for the operation, and then call [ERC-7746](./eip-7746.md) validation methods on the paymaster: `beforeCall` with `data` parameter equal to the `UserOperation`, to verify that the paymaster is willing to pay for the operation. Note that in this case, the account `beforeCall` is called with `missingAccountFunds` of 0 to reflect that the account's deposit is not used for payment for this userOp. -If the paymaster's validatePaymasterUserOp returns a "context", then `handleOps` must call `postOp` on the paymaster after making the main execution call. +is called with a `missingAccountFunds` of 0 to reflect that the account's deposit is not used for payment for this userOp. -Maliciously crafted paymasters _can_ DoS the system. To prevent this, we use a reputation system. paymaster must either limit its storage usage, or have a stake. see the [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. +Regardless if the paymaster's beforeCall returns a "context", a `handleOps` must call `afterCall` on the paymaster after making the main execution call with supplying "context" in accordance with[ERC-7746](./eip-7746.md). -The paymaster interface is as follows: +Maliciously crafted paymasters _can_ DoS the system. To prevent this, we use a reputation system. paymaster must either limit its storage usage, or have a stake. see the [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. -```solidity -function validatePaymasterUserOp - (PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) - external returns (bytes memory context, uint256 validationData); - -function postOp - (PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) - external; - -enum PostOpMode { - opSucceeded, // user op succeeded - opReverted, // user op reverted. still has to pay for gas. - postOpReverted // Regardless of the UserOp call status, the postOp reverted, and caused both executions to revert. -} +The paymaster interface MUST implement[ERC-7746](./eip-7746.md) ``` The EntryPoint must implement the following API to let entities like paymasters have a stake, and thus have more flexibility in their storage access (see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details.) @@ -338,12 +315,12 @@ interface IAggregator { } ``` -* An account signifies it uses signature aggregation returning its address from `validateUserOp`. +* An account signifies it uses signature aggregation returning its address from `beforeCall`. * During `simulateValidation`, this aggregator is returned to the bundler as part of the `aggregatorInfo` struct. * The bundler should first accept the aggregator (aggregators must be staked. bundler should verify it is not throttled/banned) * To accept the UserOp, the bundler must call **validateUserOpSignature()** to validate the userOp's signature. This method returned an alternate signature (usually empty) that should be used during bundling. -* The bundler MUST call `validateUserOp` a second time on the account with the UserOperation using that returned signature, and make sure it returns the same value. +* The bundler MUST call [ERC-7746](./eip-7746.md) `beforeCall` a second time on the account with the UserOperation using that returned signature, and make sure it returns the same value. * **aggregateSignatures()** must aggregate all UserOp signatures into a single value. * Note that the above methods are helper methods for the bundler. The bundler MAY use a native library to perform the same validation and aggregation logic. * **validateSignatures()** MUST validate the aggregated signature matches for all UserOperations in the array, and revert otherwise. @@ -409,10 +386,10 @@ The node should drop the UserOperation if the simulation fails (either by revert The simulated call performs the full validation, by calling: 1. If `initCode` is present, create the account. -2. `account.validateUserOp`. -3. if specified a paymaster: `paymaster.validatePaymasterUserOp`. +2. `account.beforeCall`. +3. if specified a paymaster: `paymaster.beforeCall`. -The simulateValidation should validate the return value (validationData) returned by the account's `validateUserOp` and paymaster's `validatePaymasterUserOp`. +The `simulateValidation` function should validate the `validationData` returned by the `beforeCall` of the account and paymaster. This is done by sending the `validationData` back to the respective validating contracts after execution, calling `afterCall` on the account and paymaster with the return value provided in `beforeCallResult`. The account MAY return an aggregator. See [Using Signature Aggregator](#using-signature-aggregator) The paymaster MUST return either "0" (success) or SIG_VALIDATION_FAILED for aggregator, and not an address. Either return value may contain a "validAfter" and "validUntil" timestamps, which is the time-range that this UserOperation is valid on-chain. @@ -479,8 +456,8 @@ The attribution of a revert to an entity is done using call-tracing: the last en * For diagnostic purposes, the EntryPoint must only revert with explicit FailedOp() or FailedOpWithRevert() errors. * The message of the error starts with event code, AA## * Event code starting with "AA1" signifies an error during account creation -* Event code starting with "AA2" signifies an error during account validation (validateUserOp) -* Event code starting with "AA3" signifies an error during paymaster validation (validatePaymasterUserOp) +* Event code starting with "AA2" signifies an error during account validation ([ERC-7746](./eip-7746.md) validation workflow) +* Event code starting with "AA3" signifies an error during paymaster validation ([ERC-7746](./eip-7746.md) validation workflow) ## Rationale @@ -489,12 +466,16 @@ The main challenge with a purely smart contract wallet-based account abstraction Requiring the block builder to execute the entire operation opens a DoS attack vector, as an attacker could easily send many operations that pretend to pay a fee but then revert at the last moment after a long execution. Similarly, to prevent attackers from cheaply clogging the mempool, nodes in the P2P network need to check if an operation will pay a fee before they are willing to forward it. + The first step is a clean separation between validation (acceptance of UserOperation, and acceptance to pay) and execution. -In this proposal, we expect accounts to have a `validateUserOp` method that takes as input a `UserOperation`, verifies the signature and pays the fee. +In this proposal, we expect accounts to have a `beforeCall` and `afterCall` methods according to [ERC-7746](./eip-7746.md) that takes as input a `UserOperation`, verifies the signature and pays the fee. Only if this method returns successfully, the execution will happen. The entry point-based approach allows for a clean separation between verification and execution, and keeps accounts' logic simple. It enforces the simple rule that only after validation is successful (and the UserOp can pay), the execution is done, and also guarantees the fee payment. +### Use of [ERC-7746](./eip-7746.md) as validation mechanism +The [ERC-7746](./eip-7746.md) is a generic way to implement a security middleware in smart contracts. It allows to encapsulate the validation interfaces in a separate standard, and thus making this ERC smaller and better focused. It also allows for a more generic approach that can be used in other contexts. + ### Validation Rules Rationale The next step is protecting the bundlers from denial-of-service attacks by a mass number of UserOperations that appear to be valid (and pay) but that eventually revert, and thus block the bundler from processing valid UserOperations. @@ -542,7 +523,7 @@ If the factory does use CREATE2 or some other deterministic method to create the When `initCode` is specified, if either the `sender` address points to an existing contract, or (after calling the initCode) the `sender` address still does not exist, then the operation is aborted. The `initCode` MUST NOT be called directly from the entryPoint, but from another address. -The contract created by this factory method should accept a call to `validateUserOp` to validate the UserOp's signature. +The contract created by this factory method should implement [ERC-7746](./eip-7746.md) to validate the UserOp's signature. For security reasons, it is important that the generated contract address will depend on the initial signature. This way, even if someone can create a wallet at that address, he can't set different credentials to control it. The factory has to be staked if it accesses global storage - see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. @@ -1021,7 +1002,7 @@ Assume the given UserOperations all pass validation (without actually validating ## Backwards Compatibility -This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. Unfortunately it is not easily compatible with pre-[ERC-4337](./eip-4337.md) accounts, because those accounts do not have a `validateUserOp` function. If the account has a function for authorizing a trusted op submitter, then this could be fixed by creating an [ERC-4337](./eip-4337.md) compatible account that re-implements the verification logic as a wrapper and setting it to be the original account's trusted op submitter. +This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. Unfortunately it is not easily compatible with contracts not supporting [ERC-7746](./eip-7746.md) accounts, because those accounts do not have a call validation functions. If the account has a function for authorizing a trusted op submitter, then this could be fixed by creating an [ERC-4337](./eip-4337.md) compatible account that re-implements the verification logic as a wrapper and setting it to be the original account's trusted op submitter. ## Reference Implementation @@ -1029,12 +1010,12 @@ See `https://github.com/eth-infinitism/account-abstraction/tree/main/contracts` ## Security Considerations -The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-4337]. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust. +The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-4337]. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ have to do becomes much smaller (they need only verify the [ERC-7746](./eip-7746.md) implementation and its "check signature and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust. Verification would need to cover two primary claims (not including claims needed to protect paymasters, and claims needed to establish p2p-level DoS resistance): -* **Safety against arbitrary hijacking**: The entry point only calls an account generically if `validateUserOp` to that specific account has passed (and with `op.calldata` equal to the generic call's calldata) -* **Safety against fee draining**: If the entry point calls `validateUserOp` and passes, it also must make the generic call with calldata equal to `op.calldata` +* **Safety against arbitrary hijacking**: The entry point only calls an account generically if `account.beforeCall` to that specific account has passed (and with `op.calldata` equal to the generic call's calldata) AND if after the call `account.afterCall` does not revert. +* **Safety against fee draining**: If the entry point calls `beforeCall` and passes, it also must make the generic call with calldata equal to `op.calldata` ## Copyright