Posted in
ERC 4337

Introducing Opensource ERC-4337 Gas Estimation Package

July 1, 2024
value
read time
Introducing Opensource ERC-4337 Gas Estimation Package

Estimating userOp gas limits for Entry Point v0.6 is one of the most complicated parts of Account Abstraction (ERC4337) infra. All AA infra providers have faced issues of inaccurate gas estimations. If gas estimations are not accurate, it does more harm to UX and makes the flow and experience even more complicated.

To tackle these issues faced by the AA builder community, we have come up with a way to estimate each gas limit all wrapped inside an easy-to-use and lightweight SDK that is open source and for the community!

Why accurate gas estimation is important

If gas estimations are not done the right way, the following issues can come up:

For the userOps involving Token Paymasters, the amount of ERC20 tokens to be paid can get pretty off very quickly and may result in the user having the balance but still not being able to pay because the fees shown are higher than what will get consumed

- If the gas limit for the call data execution is low, the execution will fail on the chain without the user coming to know and an unclear message will pop up on every explorer.

- Estimating various validation styles is hard and sometimes inaccurate as while estimating the dummy values will not run all the steps and not capture the actual gas usage meaning one needs to keep adding buffers over the estimated gas values

- The Entry Point’s formula of max gas required gets pretty high in cases of paymasters even though it might not even require such high gas values and funds in the paymaster

- Calculating the roll-up cost and adjusting in preVerficaitonGas so that bundlers don’t lose money and users don’t overpay has been a challenge on various L2s where the math to do it is unclear.

Benefits of using the SDK:

  • Bundlers don’t need to reinvent the wheel for gas estimations. They can focus on other challenging aspects of running a bundler like transaction management, memory management, scalability, or latency
  • Apps don’t need to depend on the eth_estimateUserOperationGas always and in simplified cases can use the package and decrease a lot of latency. This is specifically useful for dapps looking for very very low latency transactions
  • The approach used in the SDK uses what node clients use to tackle the difficulties faced in estimating gas. It is the standard way to handle the edge cases involved in it. These cases cannot be covered by calling the simulation methods on the Entry Point contract.


Gas values involved in a UserOp

First of all, let's look at the various gas values involved in a userOp:

1. callGasLimit: The gas required to execute the call data part of the userOp which is the call from the Entry Point to the Smart Account

2. verificationGasLimit: The gas required to run all validation checks and deploy the wallet if the case be

3. preVerificationGas: This gas value is the only value that is not a limit but a direct number that accounts for all the gas that cannot be measured on the chain. This typically involves the base gas cost and for roll-ups, it has to take in the roll-up fee.

Problem with estimating with the simulateHandleOp method

Entry Point provides a simulateHandleOp method that simulates the validation and call data execution phases.

Issues with callGasLimit:

The problem with using simulateHandleOp to calculate callGasLimit is that on-chain it won’t calculate correctly because the current way is to capture the paid field from simulateHandleOp revert data and divide it by the max fee values to get the gas used in the simulateHandleOp gas metering and then further subtract the preOpGas from it. This should work ideally but is not accurate for two major reasons:

  • This above logic includes postOp gas and there is no way to separate it from the main callData execution gas in EP v6
  • Another is the 63/64 EVM rule. Since EIP-150, the use of the CALL opcode (and all its variants) cannot consume more than 63/64 of the remaining gas. As a transaction’s call stack gets deeper, more gas must be reserved upfront to meet the gas requirements of higher call frames.
  • One needs to send a nonzero maxFeePerGas value to capture the value in paid which forces a smart account to have funds even though a paymaster might be involved in future steps of execution. This gets solved for networks supporting state overrides but remains an issue on networks where eth_call does not support state overrides

Due to the complications of callGasLimit the general approach is to move the callGasLimit calculation outside of the entry point. This can be achieved by using this extra call execution inside simulateHandleOp:

 
if (target != address(0)) {
      (targetSuccess, targetResult) = target.call(targetCallData);
  }

This allows us to call any contract with some data after the validation step is done. This is super helpful as we can now avoid call data being executed in the entry point flow by setting callGasLimit as 0 and forcing the execution to happen in a different logical flow.

Issues with verificationGasLimit:

For verificationGasLimit using simulateHandleOp should ideally work but we have improved it by using on-chain Binary Search. Alchemy introduced a way to calculate callGasLimit with Binary Search on the chain, we have taken inspiration from it and adopted it for the calculation of verificationGasLimit and modified the contract a bit for callGasLimit.

A thing to note is that verificationGasLimit can never be fully captured. It involves calling validation modules that have signature checks. While estimating one sends dummy signatures which will revert in simulations hence the full gas will never be estimated.

How do we do gas estimation?

A lot of our code and inspiration comes from Alchemy’s and Pimlico’s gas estimation style.

We use two special contracts for estimating callGasLimit and verificationGasLimit which run a binary search algorithm to find the optimal gas limit. Alchemy’s Call Gas Estimation Proxy inspires these contracts with modifications to handle edge cases that we observed with some Smart Account implementations.

Both sets of contracts extend the entry point contract and are never deployed but replace the entry point code using state overrides. This ensures that no edge case is broken where a particular entity (SA, paymaster, etc) might enforce that calls should be made from the Entry Point only.

VerificationGasEstimationSimulator:

We call the estimateVerificationGas method which first calls the entry point methods with a gas limit of 30M (max block gas) to check if the user operation is even valid. Once that is successful we start the binary search till the execution runs out of gas and we have a gas value that had the successful binary search which we return as the verificationGasLimit. We also override the callGasLimit to 0 as we don’t need to run call data execution

CallGasEstimationSimulator:

The algorithm is the same as what happens in estimateVerificationGas.

In this also we override the callGasLimit to 0 to make sure that the call data execution does not run inside the executeUserOp method of simulateHandleOp and is fully executed inside the estimateCallGas method.

preVerificationGas:

preVerificationGas is tricky to calculate but essentially one needs to have a way to calculate how much total cost it would be for the bundler to send a userOp. This should include the unaccounted L2 cost plus the roll-up fee to the L1.

For L2s as we have to calculate the roll-up fee, the exact logic for each is described network side below:

  • We create the handleOpsData that the L2 has to post on L1:



to the endconst handleOpsData = encodeFunctionData({
      abi: ENTRY_POINT_ABI,
      functionName: "handleOps",
      args: [[userOperation], userOperation.sender],
    });

For Optimism:



// We use the Optimism Gas Price to get the L1 Fee 
const l1Fee = (await readContract({
  address: OPTIMISM_L1_GAS_PRICE_ORACLE_ADDRESS,
  abi: OPTIMISM_L1_GAS_PRICE_ORACLE_ABI,
  functionName: "getL1Fee",
  args: [handleOpsData],
}))

// Then we find the L2 view of that L1 fee and add it to the preVerificationGas
const l2MaxFee = userOperation.maxFeePerGas;
const l2PriorityFee =
  baseFeePerGas + userOperation.maxPriorityFeePerGas;
const l2Price = l2MaxFee < l2PriorityFee ? l2MaxFee : l2PriorityFee;
extraPreVerificationGas = l1Fee / l2Price;

For Arbitrum:



// We use Arbitrum's gas price and the oracle's gasEstimateL1
const gasEstimateForL1 = await readContract({
  address: NODE_INTERFACE_ARBITRUM_ADDRESS,
  abi: ARBITRUM_L1_FEE_GAS_PRICE_ORACLE_ABI,
  functionName: "gasEstimateL1Component",
  args: [entryPointAddress, false, handleOpsData],
});

extraPreVerificationGas = gasEstimateForL1[0];

For Scroll:



// We use Scroll's gas price and the oracle's getL1Fee	
const l1Fee = (await readContract({
  address: SCROLL_L1_GAS_PRICE_ORACLE_ADDRESS,
  abi: SCROLL_L1_GAS_PRICE_ORACLE_ABI,
  functionName: "getL1Fee",
  args: [handleOpsData],
}));

// extraPvg = l1Cost / l2Price
const l2MaxFee = userOperation.maxFeePerGas;

extraPreVerificationGas + l1Fee / l2MaxFee;

For Mantle:



const tokenRatio = await readContract({
      address: MANTLE_BVM_GAS_PRICE_ORACLE_ADDRESS,
      abi: MANTLE_BVM_GAS_PRICE_ORACLE_ABI,
      functionName: "tokenRatio",
    });

    const scalar = await readContract({
      address: MANTLE_BVM_GAS_PRICE_ORACLE_ADDRESS,
      abi: MANTLE_BVM_GAS_PRICE_ORACLE_ABI,
      functionName: "scalar",
    });

    const rollupDataGasAndOverhead = await readContract({
      address: MANTLE_BVM_GAS_PRICE_ORACLE_ADDRESS,
      abi: MANTLE_BVM_GAS_PRICE_ORACLE_ABI,
      functionName: "getL1GasUsed",
      args: [toRlp(handleOpsData)],
    });

    const l1GasPrice = await readContract({
      address: MANTLE_BVM_GAS_PRICE_ORACLE_ADDRESS,
      abi: MANTLE_BVM_GAS_PRICE_ORACLE_ABI,
      functionName: "l1BaseFee",
    });

    const l1RollupFee =
      (rollupDataGasAndOverhead * l1GasPrice * tokenRatio * scalar) /
        MANTLE_L1_ROLL_UP_FEE_DIVISION_FACTOR;
    const l2MaxFee = BigInt(userOperation.maxFeePerGas);
    
    extraPreVerificationGas + l1Fee / l2MaxFee;

---------------------------------

This piece is authored by Yash Chaudhary. Follow him on twitter.

Subscribe to the Biconomy Academy

Building a decentralised ecosystem is a grind. That’s why education is a core part of our ethos. Benefit from our research and accelerate your time to market.

You're in! Thank you for subscribing to Biconomy.
Oops! Something went wrong while submitting the form.
By subscribing you agree to with our Privacy Policy
Copied link

Heading

This is some text inside of a div block.
value
read time

What’s a Rich Text element?

What’s a Rich Text element?

The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.

The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.

Static and dynamic content editing

Static and dynamic content editing

A rich text element can be used with static or dynamic content. For static content, just drop it into any page and begin editing. For dynamic content, add a rich text field to any collection and then connect a rich text element to that field in the settings panel. Voila!

A rich text element can be used with static or dynamic content. For static content, just drop it into any page and begin editing. For dynamic content, add a rich text field to any collection and then connect a rich text element to that field in the settings panel. Voila!

How to customize formatting for each rich text

How to customize formatting for each rich text

Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.

Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.
Subscribe to the Biconomy Academy

Building a decentralised ecosystem is a grind. That’s why education is a core part of our ethos. Benefit from our research and accelerate your time to market.

You're in! Thank you for subscribing to Biconomy.
Oops! Something went wrong while submitting the form.
By subscribing you agree to with our Privacy Policy
Read next
Copied link