Learn
Versions
Four separate singletons with separate deposits, nonces and event streams. Which one emitted the event decides everything else.
Four EntryPoint singletons are deployed on C-Chain (chain 43114). They are separate contracts with separate deposits, nonces and event streams. An account or paymaster built for one version does not automatically work with another. When you read a transaction, the first question is always: which EntryPoint emitted the UserOperationEvent? Everything else (struct layout, hash formula, paymaster encoding) follows from that.
Sources used on this page:
- Release notes: v0.7.0, v0.8.0, v0.9.0.
- Source at release tags: v0.6.0 UserOperation.sol, v0.6.0 EntryPoint.sol, v0.7.0 PackedUserOperation.sol, v0.8.0 EntryPoint.sol, v0.8.0 Eip7702Support.sol, v0.9.0 UserOperationLib.sol.
- The standard: ERC-4337 (current text, which already describes the v0.9 features).
- Local:
epi/constants.py, the earlier Oct 18 reports, and the parquet archive.
All four pages were fetched on 2026-10-19 and the statements below were checked against them.
What the archive shows, per version
| Version | First UserOp | Last UserOp | Outer txs | UserOps | Op success | Paymaster-paid | Receipt fees (AVAX) | Distinct accounts | Busiest day |
|---|---|---|---|---|---|---|---|---|---|
| v0.6 | 2023-04-17 | 2026-10-25 | 1,632,676 | 1,653,222 | 99.44% | 92.3% | 7,478.95 | 162,409 | 2026-04-24 (16,693 ops) |
| v0.7 | 2024-04-20 | 2026-10-25 | 4,758,342 | 4,867,819 | 99.98% | 99.1% | 1,506.25 | 785,319 | 2026-05-03 (73,641 ops) |
| v0.8 | 2025-04-15 | 2026-10-22 | 273 | 927 | 99.24% | 100.0% | 7.75 | 79 | 2026-08-30 (440 ops) |
| v0.9 | 2026-04-28 | 2026-10-25 | 9,336 | 9,336 | 100.00% | 45.8% | 1.63 | 74 | 2026-09-10 (80 ops) |
Each small chart has its own y-scale: v0.7 is roughly a thousand times larger than v0.9. The numbers page has the combined chart and data tables.
EntryPoint v0.6: UserOperations per month
EntryPoint v0.7: UserOperations per month
EntryPoint v0.8: UserOperations per month
EntryPoint v0.9: UserOperations per month
1. Addresses and selectors
| Version | EntryPoint address (from epi/constants.py) |
Released (tag date) | Struct on chain | handleOps selector |
|---|---|---|---|---|
| v0.6 | 0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789 evidence level: primary source |
24 Apr 2023 | UserOperation, 11 fields |
0x1fad948c |
| v0.7 | 0x0000000071727de22e5e9d8baf0edac6f37da032 evidence level: primary source |
22 Feb 2024 | PackedUserOperation, 9 fields |
0x765e827f |
| v0.8 | 0x4337084d9e255ff0702461cf8895ce9e3b5ff108 evidence level: primary source |
26 Mar 2025 | PackedUserOperation, 9 fields |
0x765e827f |
| v0.9 | 0x433709009b8330fda32311df1c2afa402ed8d009 evidence level: primary source |
16 Nov 2025 | PackedUserOperation, 9 fields |
0x765e827f |
The v0.7, v0.8 and v0.9 addresses match the addresses printed in their release notes. The v0.6 address matches the earlier identity record.
Other selectors (computed in epi/constants.py SEL):
| Function | v0.6 | v0.7 / v0.8 / v0.9 |
|---|---|---|
handleOps(ops[], beneficiary) |
0x1fad948c |
0x765e827f |
handleAggregatedOps(...) |
0x4b1d7cf5 |
0xdbed18e0 |
depositTo(address) |
0xb760faf9 |
0xb760faf9 |
v0.7, v0.8 and v0.9 share a handleOps selector because the struct is the same. The selector alone cannot tell you the version; the to address (or, for wrapped calls, the address that emits UserOperationEvent) can.
Event topics are the same across all four versions (epi/constants.py TOPIC). For example, UserOperationEvent is 0x49628fd1…419f everywhere. Again, the emitter address tells you the version.
2. The struct: 11 fields (v0.6) vs 9 packed fields (v0.7+)
v0.6 UserOperation
address sender
uint256 nonce
bytes initCode
bytes callData
uint256 callGasLimit
uint256 verificationGasLimit
uint256 preVerificationGas
uint256 maxFeePerGas
uint256 maxPriorityFeePerGas
bytes paymasterAndData // paymaster address (20 bytes) + paymaster-specific data
bytes signature
In v0.6 one verificationGasLimit covers both account validation and paymaster validation. The paymaster's postOp also runs under that limit. Because postOp could be called twice in v0.6, the required prefund with a paymaster is (callGasLimit + 3 × verificationGasLimit + preVerificationGas) × maxFeePerGas (v0.6 _getRequiredPrefund).
v0.7+ PackedUserOperation
address sender
uint256 nonce
bytes initCode
bytes callData
bytes32 accountGasLimits // verificationGasLimit (high 16 bytes) | callGasLimit (low 16 bytes)
uint256 preVerificationGas
bytes32 gasFees // maxPriorityFeePerGas (high 16 bytes) | maxFeePerGas (low 16 bytes)
bytes paymasterAndData
bytes signature
The v0.7 release notes explain the change: the off-chain UserOperation (what wallets send to bundlers) and the on-chain struct became separate. Four gas numbers were packed into two bytes32 words to save calldata. The paymaster got its own two gas limits, stored inside paymasterAndData.
paymasterAndData layout
| Version | Layout |
|---|---|
| v0.6 | [0:20] paymaster address, [20:] paymaster-specific data |
| v0.7, v0.8 | [0:20] paymaster, [20:36] paymasterVerificationGasLimit (uint128), [36:52] paymasterPostOpGasLimit (uint128), [52:] paymasterData |
| v0.9 | same as v0.7, plus an optional suffix: paymasterSignature ‖ uint16(len) ‖ 0x22e325a297439656 |
The offsets 20/36/52 are the constants PAYMASTER_VALIDATION_GAS_OFFSET, PAYMASTER_POSTOP_GAS_OFFSET and PAYMASTER_DATA_OFFSET in UserOperationLib.sol. The v0.9 magic is PAYMASTER_SIG_MAGIC = 0x22e325a297439656, the first 8 bytes of keccak("PaymasterSignature"). (I recomputed this locally and it matches.)
What sits after the paymaster address or the gas limits is defined by each paymaster, not by the EntryPoint. Pimlico's singleton paymaster is an example. Its source is kept as a snapshot. Its data starts with one byte, mode << 1 | allowAllBundlers, where mode 0 is verifying and mode 1 is ERC-20. In verifying mode, that byte is followed by validUntil (6 bytes), validAfter (6 bytes) and a signature. So a Pimlico v0.6 verifying-mode paymasterAndData is 20 + 1 + 12 + 65 = 98 bytes, and the v0.7 one is 52 + 1 + 12 + 65 = 130 bytes. Both lengths appear in the Oct 18 data.
3. The userOpHash formula
The userOpHash is the operation's identity. It is topic1 of UserOperationEvent and AccountDeployed, and it is usually what the account signs. Use the wrong formula and nothing matches.
v0.6 (EntryPoint.getUserOpHash, UserOperationLib.pack):
inner = keccak(abi.encode(sender, nonce, keccak(initCode), keccak(callData),
callGasLimit, verificationGasLimit, preVerificationGas,
maxFeePerGas, maxPriorityFeePerGas, keccak(paymasterAndData)))
userOpHash = keccak(abi.encode(inner, entryPoint, chainId))
v0.7: same shape, with the packed fields:
inner = keccak(abi.encode(sender, nonce, keccak(initCode), keccak(callData),
accountGasLimits, preVerificationGas, gasFees, keccak(paymasterAndData)))
userOpHash = keccak(abi.encode(inner, entryPoint, chainId))
v0.8 and v0.9: EIP-712 typed data with domain name="ERC4337", version="1", chainId, verifyingContract=EntryPoint:
PACKED_USEROP_TYPEHASH = keccak("PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)")
structHash = keccak(abi.encode(TYPEHASH, sender, nonce, keccak(initCode*), keccak(callData),
accountGasLimits, preVerificationGas, gasFees, keccak(paymasterAndData**)))
userOpHash = keccak(0x1901 ‖ domainSeparator ‖ structHash)
*If initCode starts with the EIP-7702 marker, the initCode hash is replaced bykeccak(delegate ‖ initCode[20:]), wheredelegateis read from the sender's code (v0.8Eip7702Support._getEip7702InitCodeHashOverride).**In v0.9 only, ifpaymasterAndDataends with the magic suffix, the paymaster signature and its 10-byte suffix are excluded from the hash. The 8-byte magic itself is kept. So the user's signature does not cover the paymaster's signature, and the two can sign in parallel (v0.9 release note §2;getPaymasterSignatureLength/paymasterDataKeccak).
The v0.8 domain constants DOMAIN_NAME = "ERC4337" and DOMAIN_VERSION = "1" are in v0.8.0 EntryPoint.sol. The earlier decoder implements all three formulas. All 1,397 Oct 18 UserOperations re-hash to their event topic1 under their own version's formula. Using the v0.7 formula on v0.9 operations fails even though the bytes have the same shape (all-versions report).
Walked example: Reading a transaction recomputes a v0.7 hash byte by byte.
4. Behavior differences that matter when reading data
| Topic | v0.6 | v0.7 | v0.8 | v0.9 |
|---|---|---|---|---|
| Paymaster gas | shares verificationGasLimit; postOp may run twice |
own paymasterVerificationGasLimit + paymasterPostOpGasLimit; postOp runs once |
same as v0.7 | same as v0.7 |
| Unused-gas penalty | none | 10% of unused callGasLimit/postOp gas |
10%, but only if unused ≥ 40,000 gas | same as v0.8 |
| Hash | abi-encode + EP + chainId | abi-encode + EP + chainId | EIP-712 "ERC4337" v1 | EIP-712, paymaster-signature suffix excluded |
| EIP-7702 | no | no | yes: initCode marker 0x7702, delegate in hash |
yes, plus EIP7702AccountInitialized event |
executeUserOp account hook |
no | yes (callData starting with executeUserOp selector 0x8dd7712f) |
yes | yes |
| initCode when account already exists | revert AA10 |
revert AA10 |
revert AA10 |
ignored, emits IgnoredInitCode |
| Validity range | timestamps | timestamps | timestamps | timestamps or block numbers (high bit set) |
| Zero-address paymaster with data | tolerated | tolerated | rejected | rejected |
| Simulation functions on chain | yes (simulateValidation, always reverts) |
removed | removed | removed |
Sources: v0.7, v0.8 and v0.9 release notes. The 40,000 threshold and the 10% figure are also in v0.8 EntryPoint.sol (PENALTY_GAS_THRESHOLD, UNUSED_GAS_PENALTY_PERCENT). The v0.6 postOp/prefund rule comes from v0.6 EntryPoint.sol.
Two consequences for analysis:
- v0.9 initCode no longer means "first operation". A nonzero initCode on an already-deployed v0.9 account is silently ignored. Count deployments from
AccountDeployedevents, never from "initCode was non-empty". (That is good practice in every version.) - The user's gas price is
min(maxFeePerGas, maxPriorityFeePerGas + basefee). It is computed from the UserOperation's own fields, not from the bundler's transaction gas price. This is whyactualGasCostdiffers from the outer receipt fee (see the primer's money section).
5. EIP-7702 support: capability vs observed use
From v0.8, an EOA that has delegated its code via an EIP-7702 (type-4) transaction can act as a UserOperation sender. Such an operation marks its initCode with 0x7702 padded to 20 bytes. The EntryPoint then puts the delegate address into the hash instead of calling a factory.
What was observed on C-Chain:
- Oct 18 (earlier all-versions report): no EIP-7702 initCode marker in any v0.9 operation, and no type-4 outer transaction. The v0.9 sender is an ordinary Safe proxy.
- v0.8 history (a block-archive query, 2025-01-01 → 2026-10-18): all 258 transactions sent directly to v0.8 are type 2. None is type 4. I did not check v0.8 initCode bytes for the
0x7702marker. Parquet has no calldata, so that check needs full calldata from the transaction database.
Capability is not usage. "Supports 7702" describes the contract version, not what the operations did.
6. What was actually observed on C-Chain
Oct 18, 2026 (earlier, verified population)
| Version | Outer txs / UserOps | Receipt fees (AVAX) | Accounts | Notes |
|---|---|---|---|---|
| v0.6 | 732 / 733 | 1.532096008949977859 | 364 | collateral withdrawals, small app calls, scheduled probe, Safe |
| v0.7 | 600 / 600 | 0.842293586804707706 | 141 | ERC1155 transfers, signed daily results, probe, Safe, routes |
| v0.8 | 0 / 0 | 0 | 0 | no UserOperations on Oct 18 |
| v0.9 | 64 / 64 | 0.055555465949760675 | 1 | one Safe doing self-calls |
Whole-archive check of v0.8 (correction to an earlier note)
epi/constants.py says v0.8 "only sees deposits". That is wrong over the archive's history. It holds only for the days around Oct 18. I scanned the parquet archive from 2025-01-01 (start of the local archive; v0.8 was not released until 26 Mar 2025) through 2026-10-18:
Measure (v0.8 0x4337084d…f108 evidence level: primary source) |
Value |
|---|---|
Transactions sent to it, handleOps 0x765e827f |
254 (251 succeeded, 3 reverted) |
Transactions sent to it, depositTo 0xb760faf9 |
4 (6.65 AVAX total) |
| UserOperationEvent logs it emitted | 921 |
| …of which in transactions not sent directly to it (wrappers) | 16 transactions |
| Distinct senders (UserOperationEvent topic2) | 79 |
| Distinct paymasters (topic3, including zero) | 6 |
| AccountDeployed events | 13 |
| First Deposited event | block 59,382,539 (2025-03-28) |
| First handleOps | block 60,327,056 (2025-04-15), tx 0x78287250fe1acd17cb32fb9346fe232b8f32fb4add14d675d3618de435ff4812 |
| Last handleOps | block 96,876,734 (2026-10-06), tx 0x9e9f348b617410956dfa19c5dfa04ffed56298744a4b9398e44f02d183f90cba explained → |
| Busiest months by UserOperations | 2026-08 (440 ops in 16 txs), 2025-10 (238 ops in 74 txs) |
| Latest txs before Oct 18 | two depositTo on 2026-10-17 (blocks 97,825,378 and 97,825,384), e.g. 0x8043fbc26c66e397b58835eeecef5c5e786ece6530c638caf5d0063f02924288 |
So v0.8 is real but small: about 921 operations over 18 months, against millions on v0.7. On Oct 18 it had no operations. The deposits that prompted the "deposit-only" note are two 3-AVAX depositTo calls on Oct 17. A deposit funds a sender's or paymaster's balance inside the EntryPoint. It is not a user action. I verified the three tx hashes in the table above by indexed transaction-database hash lookup: each exists, has the expected to, selector and status, and the handleOps ones contain a v0.8 UserOperationEvent.
Update through 2026-10-25 (from epi history; the table above is the hand-verified snapshot to Oct 18): v0.8 reached 273 successful bundles and 927 UserOperations, the last on 2026-10-22. On that day one bundler, 0x4337000c…8680 (also v0.8's busiest bundler), sent 183 handleOps to v0.8 that reverted within about 30 minutes, alongside 67 more to v0.6 and v0.7. That brings v0.8's reverted bundles from 3 to 187.
For scale, in the archive window to Oct 18: v0.7 emitted 4,842,555 UserOperationEvents, v0.6 946,645, v0.9 8,927 (first at block 84,089,763) and v0.8 921. This is a count of events emitted, not a count of distinct users.
v0.9-specific features actually used on Oct 18 (earlier, verified)
- All 64 hashes use the EIP-712 formula.
- 19 operations carry the paymaster-signature suffix. The other sponsored operations do not.
- The account is Safe
0xa1fdd9daed9004f685b6900d5463f72d13dfcccaevidence level: cluster with module0x22939e839e3c0f479b713eaf95e0df128554aeadevidence level: primary source. That module address matches Candide's published multichain-signature module. All 64 signatures use Merkle depth zero (single-chain branch). - No EIP-7702 marker, no type-4 transaction.
7. Quick version checklist for a decoder
- Find the UserOperationEvent emitter. That is the version. Don't infer the version from the selector.
- Decode
handleOpswith the matching struct: 11 fields for v0.6, 9 packed fields otherwise. Iftois not an EntryPoint, the call is wrapped. Find the embeddedhandleOpsbytes and say explicitly that the wrapper's own behavior is unknown. - Recompute the userOpHash with the version's formula. Only then join ops to events by topic1, not by position.
- Split
paymasterAndDataat 20 bytes (v0.6) or 20/36/52 bytes (v0.7+). In v0.9, check for the magic suffix. - Treat 7702, block-number validity and "ignored initCode" as v0.8/v0.9 capabilities to test for, not to assume.