Verifying UserOperation validity
When a bundler receives a UserOperation, it must first run some basic sanity checks, namely that:
- Either the
senderis an existing contract, or theinitCodeis not empty (but not both) - If
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. - The
verificationGasLimitis sufficiently low (<= MAX_VERIFICATION_GAS) and thepreVerificationGasis sufficiently high (enough to pay for the calldata gas cost of serializing theUserOperationplusPRE_VERIFICATION_OVERHEAD_GAS) - 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 theUserOperation, 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. ThemaxFeePerGasandmaxPriorityFeePerGasare above a configurable minimum value that the bundler is willing to accept. At the minimum, they are sufficiently high to be included with the currentblock.basefee. Thesenderdoesn't have anotherUserOperationalready present in the pool (or it replaces an existing entry with the samesenderandnonce, with a highermaxPriorityFeePerGasand an equally increasedmaxFeePerGas). Only oneUserOperationpersendermay be included in a single batch. Asenderis exempt from this rule and may have multipleUserOperationsin 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.
Simulation
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.
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:
- Slots of contract
Aaddress itself. - Slot
Aon any other address. - Slots of type
keccak256(A || X) + non any other address. (to covermapping(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 formatmapping(address => struct)