When a bundler receives a
UserOperation, it must first run some basic sanity checks, namely that:
- Either the
senderis an existing contract, or the
initCodeis not empty (but not both)
initCodeis 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 for details.
verificationGasLimitis sufficiently low (
<= MAX_VERIFICATION_GAS) and the
preVerificationGasis sufficiently high (enough to pay for the calldata gas cost of serializing the
paymasterAndDatais 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 for details.
- The callgas is at least the cost of a
CALLwith non-zero value. The
maxPriorityFeePerGasare above a configurable minimum value that the bundler is willing to accept. At the minimum, they are sufficiently high to be included with the current
senderdoesn't have another
UserOperationalready present in the pool (or it replaces an existing entry with the same
nonce, with a higher
maxPriorityFeePerGasand an equally increased
maxFeePerGas). Only one
sendermay be included in a single batch. A
senderis exempt from this rule and may have multiple
UserOperationsin the pool and in a batch if it is staked (see reputation, throttling and banning section), but this exception is of limited use to normal accounts.
If the UserOperation object passes these sanity checks, the bundler must next run the first op simulation, and if the simulation succeeds, the bundler must add the op to the pool. A second simulation must also happen during bundling to make sure the
UserOperation is still valid.
In order to add a
UserOperation into the UserOp mempool (and later to add it into a bundle) we need to "simulate" its validation 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. Each of these contracts is also restricted in its storage access, to make sure
UserOperation validations are isolated.
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.
A is associated with:
- Slots of contract
Aon any other address.
- Slots of type
keccak256(A || X) + non any other address. (to cover
mapping(address => value), which is usually used for balance in ERC-20 tokens).
nis an offset value up to 128, to allow accessing fields in the format
mapping(address => struct)