An analysis of PreVerificationGas

An initial analysis into a UserOperation's PreVerificationGas value. What is it? Why is it important? And what are some approaches for calculating it?

When it comes to gas in ERC-4337 things can get tricky. Unlike a regular transaction with only one GasLimit value, we now have to deal with three different gas values:

  1. PreVerificationGas
  2. VerificationGasLimit
  3. CallGasLimit

2 and 3 are somewhat intuitive since ERC-4337 is based on the two phases of verification and execution. Just like the GasLimit in a regular transaction, the VerficationGasLimit and CallGasLimit is the amount of gas a UserOperation is willing to spend on verification and execution. These two values are metered, meaning this is the maximum an UserOperation is able to spend. If the actual transaction takes less gas, that’s also okay and the remainder is refunded.

The PreVerificationGas is different and also more complicated to deal with. When a user sends their UserOperation to the bundler, the bundler batches multiple UserOperations together and makes a regular transaction to send them to the EntryPoint contract. The EntryPoint then runs a bunch of processes to co-ordinate the verification and execution of the batch. These processes incur an overhead cost that is “outside” of the two phases. This overhead cost is what we call PreVerificationGas or PVG for short.

What makes the PVG more complicated than the other two gas values is that it isn’t metered. Which means that the value we set here should be as close to the actual PVG since it all gets charged. If it falls below the actual value, then the bundler is at risk of making a loss. On the other hand, if it goes far above the actual value, then the user ends up over paying in gas fees. The ideal scenario is if the PVG value we set is exactly equal to the actual value. In theory, it is possible to calculate what the PVG cost will be since these overhead processes are deterministic.

What are these overhead processes?

Let’s start by looking at the stack trace and gas profile of a SimpleAccount executing an ERC-20 transfer of 0 USDC. We’ll use Tenderly and their gas profiler tool which makes this extremely easy to visualise. You can also see the exact transaction on PolygonScan here.

Gas profile for an ERC-20 transfer of USDC using SimpleAccount.

This stack trace is showing us on a high level what’s happening in the EVM when the bundler makes a transaction to call handleOps on the EntryPoint with a batch size of 1 UserOperation.

Under the handleOps section, we can see two distinct sub-sections starting from _validatePrepayment and _executeUserOp. These are the validation and execution phases at play.

As we scan further down those two sub-sections we notice that both phases also have a significant portion of gas spent on CALL. This is where the EntryPoint calls the actual smart contract account to run its validation and execution specific logic. Everything below this is occurring downstream of the contract account. For instance, in the execution phase we can see the actual gas spent on the ERC-20 transfer as we keep going down below the CALL.

The gas for these two calls is what gets metered by the UserOperation’s VerificationGasLimit and CallGasLimit. However, when we discount for these calls we can also see that there is still a significant amount of gas that needs to be paid for. This is exactly what we mean by the overhead processes that PreVerificationGas covers.

Batch vs per userOp overhead

As we break down the PVG further we need to consider that, at scale, a call to handleOps can contain many userOps. So right away we need to divide the PVG into the batch overhead vs the per userOp overhead.

The implementation of handleOps in EntryPoint v0.6.

If we take a look at the implementation of handleOps we can see two distinct loops. We call these the verification and execution loops. The per userOp overhead is any gas spent in order to run an individual userOp through those two loops. For verification this is the calls to _validatePrePayment and _validateAccountAndPaymasterValidationData. Similarly, for execution its the call to _executeUserOp. The batch overhead is everything outside of these loops such as the initial gas cost made by the bundler to call handleOps and the final compensation.

Impact of userOp size

The next thing to consider is the size of an individual userOp. Complex operations sometimes mean more data is required to be posted on-chain which consequently increases the callData cost. For example, let’s take the same SimpleAccount and now do 30 batched ERC-20 transfers of 0 USDC. You can see the exact transaction on PolygonScan here.

Gas profile for batching 30 ERC-20 transfers of USDC using SimpleAccount.

The immediate thing we notice from this gas profile is 30 equal sections right at the bottom of the execution phase. This is the 30 ERC-20 transfers. Apart from this, lets compare other differences from the first transaction that did only 1 ERC-20 transfer.

The next thing you might notice is that the initial gas is a lot higher (66k vs 27k). A portion of this can be attributed to how we calculate intrinsic gas. Increasing userOp size means more bytes and hence higher callData cost.

Something else which might not be so obvious is that if you discount for the CALL segment in both phases you’ll also notice that the per userOp overhead increases too.

A comparison of per userOp overhead gas for two different transactions.

This shows that the overhead is not completely static. There is a portion of the PVG that increases based on userOp size.

Putting it all together

Using the above analysis we can derive a value for PVG as the sum of the batch and per userOp overhead. We also know that there is a positive correlation between userOp size and total overhead.

If we put it all together we can calculate PVG as:

PVG = 
	(batch_fixed / batch_size) +
	batch_variable +
	per_userop_overhead

In simple english, the PreVerificationGas paid by each userOp is equal to the sum of:

  1. (batch_fixed / batch_size): The share of the fixed portion of the batch overhead.
  2. batch_variable: The delta in the batch overhead required to add a userOp of its size to the batch.
  3. per_userop_overhead: The per userOp overhead of running a userOp of its size through the verification and execution loops.

Calculating the PVG in practice

For now we’ll use the intrinsic gas cost calculation to determine the batch overhead portion of the PVG.

PVG = 
	(21000 / batch_size) + 
	[(4*number_zero_bytes) + (16*number_non_zero_bytes)] +
	per_userop_overhead

In the rest of this analysis we’ll figure out how to determine a reasonable value for per_userop_overhead.

Approach 1: Add a large number and call it a day

This might work for VerificationGasLimit and CallGasLimit but remember that PVG is not metered. So if we add a really large number here, the user will pay for all of it. Alternatively, if you add a low number, the bundler will be at a loss and will likely reject the op from entering the mempool.

Even if the PVG was metered, this would still not be a great solution as an account requires enough native tokens to pay up to the max limit at the given gas price. Which means the higher you set a limit the more ETH, MATIC, or native gas tokens you need.

Approach 2: Account for every opcode

The other side of the spectrum would be to tally up the gas cost of each opcode used during the overhead processes. Since the EntryPoint is a permission-less contract that cannot be changed, all it’s code paths are deterministic. Which means this is possible to do.

In practice, the effort to do this is non-trivial. Navigating the dynamic gas costs of the EVM can get messy. On top of that, we’d also have to account for the optimisations made by the solidity compiler too. If done right, this could likely result in a super precise PVG value, however we'll defer this deeper analysis for another day.

Approach 3: Use a trend line

One thing we know is that the per userOp overhead grows based on the size of the userOp. Short of actually analysing each opcode, we could instead create a trend line based on a graph of userOp words vs per userOp overhead. The userOp words refer to the number of 32-byte words in a UserOperation.

We can use the same SimpleAccount to do a batch ERC-20 transfer starting from 1 and increasing it up to a max of 30 (giving us a sample size of 30). This was done on Polygon Mumbai using a test ERC-20 token which yielded the following results.

A chart showing the relationship between userOp size and per userOp overhead.

We can then use this trend line as the last component of our PVG formula.

PVG = 
	(21000 / batch_size) + 
	[(4*number_zero_bytes) + (16*number_non_zero_bytes)] +
	(25*number_userop_words + 22874)

On a side note, it would be worth doing the same analysis but with a more random mix of historical UserOperations to check for other factors besides userOp size that might make an impact.

The results

A good result means that the compensation by the EntryPoint is equal to or greater than the total transaction fee paid for by the bundler (i.e. a profit or break even). A bad result would mean that the compensation is less than the total transaction fee (i.e. a loss). A good solution should also hold as the size of a userOp increases.

A view of the bundler compensation vs transaction fee on etherscan.

In the image, the green line shows the compensation from the EntryPoint and the red line is the transaction fee paid by the bundler.

A comparison of the difference between compensation and transaction fee.

Using the trend line we can see that in both cases the the bundler is still profitable. However we are likely still over-shooting the actual value based on the diff of 4-9 GWEI.

What’s next?

The bundler is a complex piece of infrastructure that plays an essential role in ERC-4337 and account abstraction. Everything from DoS protection to P2P networking and gas calculations need to be properly implemented and accounted for. This analysis begins to deep dive into one aspect of gas calculations known as the PreVerificationGas. Although it's a good start, this analysis is not yet complete and further optimisation can still be made to reduce the diff between compensation and transaction fee.

In the meantime, this solution provides a viable approach for bundlers to operate without incurring a loss while attempting to minimise overcharge to users.