EIP-4337: Account Abstraction using alt mempool
An account abstraction proposal which completely avoids consensus-layer protocol changes, instead relying on higher-layer infrastructure.
作者 | Vitalik Buterin, Yoav Weiss, Kristof Gazso, Namra Patel, Dror Tirosh, Shahaf Nacson, Tjaden Hess |
---|---|
讨论-To | https://ethereum-magicians.org/t/erc-4337-account-abstraction-via-entry-point-contract-specification/7160 |
状态 | Draft |
类型 | Standards Track |
分类 | ERC |
创建日期 | 2021-09-29 |
英文版 | https://eips.ethereum.org/EIPS/eip-4337 |
摘要
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.
动机
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 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
规范
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, 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.
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 (needed if and only 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 |
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 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 |
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.
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:
function handleOps(UserOperation[] calldata ops, address payable beneficiary);
function handleAggregatedOps(
UserOpsPerAggregator[] calldata opsPerAggregator,
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;
}
The core interface required for a wallet to have is:
interface IWallet {
function validateUserOp
(UserOperation calldata userOp, bytes32 requestId, address aggregator, uint256 missingWalletFunds)
external;
}
The wallet
- 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:
interface IAggregatedWallet is IWallet {
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 asgetAggregator
- 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) - validateUserOp MAY ignore the signature field (the signature might contain data extracted by the
aggregator.validateUserOpSignature
, and used byaggregator.validateSignatures
)
The core interface required by an aggregator is:
interface IAggregator {
function validateUserOpSignature(UserOperation calldata userOp, bool offChainSigCheck) external view returns (bytes memory sigForUserOp, bytes memory sigForAggregation, bytes memory offChainSigInfo);
function aggregateSignatures(bytes[] calldata sigsForAggregation) 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
- 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
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 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.
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.
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.handleAggregatedOps
can handle a batch that contains userOps of multiple aggregators (and also requests without any aggregator)-
handleAggregatedOps
performs the same logic below ashandleOps
, but it must transfer the correct aggregator to each userOp, and also must callvalidateSignatures
on each aggregator after doing all the per-wallet validation. The entry point’shandleOps
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, thehandleOps
call must perform the following steps for eachUserOperation
: - 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 theUserOperation
, 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 anyvalidateUserOp
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)
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 anexecute
function that parses the remaining calldata as a series of one or more calls that the wallet should make.
Before accepting a UserOperation
, bundlers should use an RPC method to locally call the simulateValidation
function of the entry point, to verify that the signature is correct and the operation actually pays fees; see the Simulation section below for details. A node/bundler SHOULD drop (and not add to the mempool) UserOperation
that fails the validation
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:
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 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 for details.
The paymaster interface is as follows:
function validatePaymasterUserOp
(UserOperation calldata userOp, bytes32 requestId, uint256 maxCost)
external returns (bytes memory context);
function postOp
(PostOpMode mode, bytes calldata context, uint256 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:
// add a paymaster stake (must be called by the paymaster)
function addStake(uint32 _unstakeDelaySec) external payable
// unlock the stake (must wait unstakeDelay before can withdraw)
function unlockStake() external
// withdraw the unlocked stake
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:
// return the deposit of an account
function balanceOf(address account) public view returns (uint256)
// add to the deposit of the given account
function depositTo(address account) public payable
// withdraw from the deposit
function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) 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 theinitCode
is not empty (but not both) - The
verificationGasLimit
is sufficiently low (<= MAX_VERIFICATION_GAS
) and thepreVerificationGas
is sufficiently high (enough to pay for the calldata gas cost of serializing theUserOperation
plusPRE_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 callgas is at least the cost of a
CALL
with non-zero value. - The
maxFeePerGas
andmaxPriorityFeePerGas
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 currentblock.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 highermaxPriorityFeePerGas
and an equally increasedmaxFeePerGas
). Only oneUserOperation
per sender may be included in a single batch.
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.
Simulation
To simulate a UserOperation
validation, the client makes a view call to simulateValidation(userop)
, with a “from” address set to all-zeros
If the call returns an error, the client rejects this userOp
.
The simulated call performs the full validation, by calling:
wallet.validateUserOp
.- if specified a paymaster:
paymaster.validatePaymasterUserOp
. - if using an aggregator:
aggregator.validateUserOpSignature
.
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. While simulating userOp
validation, the client should make sure that:
- Neither call’s execution trace invokes any forbidden opcodes
- 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.
- 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
- The third (validateUserOpSignature) view-call doesn’t access mutable state of any contract except the aggregator AND wallet itself.
- 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.
- Any
CALL
orCALLCODE
during validation hasvalue=0
, except for the transfer from the wallet to the entry point. - No
CALL
,DELEGATECALL
,CALLCODE
,STATICCALL
results in an out-of-gas revert. - No
CALL
,DELEGATECALL
,CALLCODE
,STATICCALL
to addresses withEXTCODESIZE=0
. - Any
GAS
opcode is followed immediately by one of {CALL
,DELEGATECALL
,CALLCODE
,STATICCALL
}. EXTCODEHASH
of every address accessed (by any opcode) does not change between first and second simulations of the op.- If
op.initcode.length != 0
, allow only oneCREATE2
opcode call (in the validateUserOp block), otherwise forbidCREATE2
.
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:
function getSenderStorage(address sender) external view returns (uint256[] memory senderStorageCells)
Bundling
During bundling, the client should:
- Exclude UserOps that access any sender address created by another UserOp on the same batch (via CREATE2 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
After creating the batch, before including the transaction in a block, the client should:
- Run
eth_estimateGas
with maximum possible gas, to verify the entirehandleOps
batch transaction, and use the estimated gas for the actual transaction execution. - If the call reverted, check the
FailedOp
event. AFailedOp
duringhandleOps
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 untileth_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).
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
When a bundler includes a bundle in a block it must ensure that earlier transactions in the block don’t make any UserOperation fail. It should either use access lists to prevent conflicts, or place the bundle as the first transaction in the block.
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.
Exceptions to the forbidden opcodes:
- A single
CREATE2
is allowed ifop.initcode.length != 0
and must result in the deployment of a previously-undeployedUserOperation.sender
. GAS
is allowed if followed immediately by one of {CALL
,DELEGATECALL
,CALLCODE
,STATICCALL
}. (that is, making calls is allowed, usinggasleft()
orgas
opcode directly is forbidden)
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:
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 <= opsIncluded[paymaster] + THROTTLING_SLACK:
return OK
elif min_expected_included <= opsIncluded[paymaster] + BAN_SLACK:
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
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.
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.
# Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_sendUserOperation",
"params": [
{
sender, // address
nonce, // uint256
initCode, // bytes
callData, // bytes
callGasLimit, // uint256
verificationGasLimit, // uint256
preVerificationGas, // uint256
maxFeePerGas, // uint256
maxPriorityFeePerGas, // uint256
paymasterAndData, // bytes
signature // bytes
},
entryPoint // address
]
}
# Response
{
"jsonrpc": "2.0",
"id": 1,
"result": true
}
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.
# Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_supportedEntryPoints",
"params": []
}
# Response
{
"jsonrpc": "2.0",
"id": 1,
"result": [
"0xcd01C8aa8995A59eB7B2627E69b40e0524B5ecf8",
"0x7A0A0d159218E6a2f407B99173A2b12A6DDfC2a6"
]
}
基本原理
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.
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.
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 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
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).
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.
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.
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()
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.
向后兼容性
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.
Reference Implementation
See https://github.com/eth-infinitism/account-abstraction/tree/main/contracts
Security Considerations
The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for all EIP-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 withop.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 toop.calldata
版权声明
Copyright and related rights waived via CC0.
参考文献
Please cite this document as:
Vitalik Buterin, Yoav Weiss, Kristof Gazso, Namra Patel, Dror Tirosh, Shahaf Nacson, Tjaden Hess, "EIP-4337: Account Abstraction using alt mempool [DRAFT]," Ethereum Improvement Proposals, no. 4337, September 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4337.