Decoding EntryPoint and UserOperations with ERC-4337 Part 2
This is Part-2 of our series on decoding EntryPoint and UserOps with ERC-4337. For introduction & basic definitions, please check out part-1.
This part will dive deeper into the line by line code and transaction flow.
Decoding EntryPoint code line by line
💡While going through this explanation i recommend you open the EntryPoint code in another tab/screen to compare the actual code to this explanation. Here is the code Github link.
Before proceeding lets make a mental image of all the methods are going to be called and their call hierarchy.
The first point of interaction with EntryPoint is handleOps method. This method is called by Bundlers to execute a bundle of UserOperations.
hanldeOps method
We are here
You’ll find more “We are here” section, that will help you visualise where in the call hierarchy you are right now in EntryPoint contract. We are starting with handleOps method which is the entry point method, so we see only this method in the image. In next sections you’ll see more method calls branching out of handleOps method.
Function declaration
This is a public function with nonReentrant modifier(to prevent reentrancy attacks) that accepts two parameters:
UserOperation[] calldata ops ⇒ Array of UserOperation objects with calldata storage.
address payable beneficiary ⇒ Beneficiary address where the gas is refunded after execution. This can be any address where bundler wants to receive the refund.
💡Note that this handleOps is called by the bundler account and if bundler send this transaction in public mempool on blockchain validator node, anyone can frontrun this transaction and change the beneficiary address so front runner can get the refund of this transaction. So it’s important for bundler to send these transaction via private RPC to node providers or Block Builders so it doesn’t end up in public mempool.
Function definition
We are inside => handleOps
Above lines are self explanatory. We define an array of UserOpInfo type of same length as of UserOperation array. So we can see that for each UserOperation there is a UserOpInfo object that we can access.
Proceeding to next lines of code,
Here an unchecked block is started (it’s closed at the end of the method) and we start a for loop that will iterate over each UserOp object. In each iteration,
- We get the UserOpInfo object at same index position as current UserOp object in their corresponding array. At this point this object is completely empty.
- We call an internal method _validatePrepayment, that takes the for loop index, UserOp object and UserOpInfo object as input parameter and returns two validation data fields. First one corresponds to validation data for SCW and other one for Paymaster.
- Now we call another internal method _validateAccountAndPaymasterValidationData whose purpose is to validate the validation data we got in step 2 above.
Next,
We are inside => handleOps
A BeforeExecution() event is emitted to mark the flow where any event emitted before this event, is part of the validation.
For each UserOp, an internal function _executeUserOp is called which internally calls the SCW and do the actual execution (calling a dapp smart contract or transferring funds to other address etc) and also calls postOp method on Paymaster if paymaster info is available in UserOp.
It returns the total gasFee for this UserOp that needs to be refunded to the beneficiary.
This happen for each UserOperation, and the fee refund for each UserOp is accumulated in collected variable, which will contain the total gas fee refund to be given to beneficiary for all the UserOp.
Next,
We are inside => handleOps
At last, _compensate method is called. It’s a very simple function which just transfer the collected amount of native currency to the beneficiary address. Nothing else happens in this method.
Complete Code Reference
View on Github
Pseudo Code
Here is a simple pseudo code for you to understand what exactly is happening in handleOps method.
Pseudo code for handleOps function:
1. Take the length of userOps as n.
2. Create a UserOpInfo array of length n.
3. For each user operation (Verification Loop):
a. Call _validatePrepayment(index, userOp, userOpInfo), which returns validationData and paymasterValidationData.
b. Call validateAccountAndpaymasterValidationData(index, validationData, pmValidationData, address(0)).
4. For each user operation (Execution Loop):
a. Call _executeUserOp(index, userOp, userOpInfo), which returns the gas fee to be refunded to beneficiary.
b. Sum all the fee refunds for each user operation.
5. Compensate the beneficiary address with all the collected gas fee.
Summary
To make it easier to understand, there are four main method calls that occur in handleOps:
- validatePrepayment
- validateAccountAndPaymasterValidationData
- executeUserOp
- compensate
We are here
Now that we have simple idea of 4 methods that are being called from handleOps method, let’s see what’s happening inside each method
validatePrepayment method
We are here
Function Declaration
We are inside => handleOps > validatePrepayment
Remember this function is being called from the Verification Loop in hanldeOps method, so
- First parameter is the index of the for loop happening in handleOps
- Second parameter is the UserOp object itself
- Third parameter is UserOpInfo object. This is an empty object. It’ll be initialised in this method.
Returns,
validationData ⇒ validation data returned by Smart Contract Wallet validateUserOp method
paymasterValidationData ⇒ validation data returned by Paymaster validatePaymasterUserOp method
Function Definition
This function has below responsibilities,
- Create and Initialise MemoryUserOp type object
- Deploy new SCW if needed
- Validate account and paymaster data (if defined)
- Perform some gas fields validation logic
- Initialise UserOpInfo type object outOpInfo
Let’s start with the code
We are inside => handleOps > validatePrepayment
In first line the on chain gas tracking is started. We get the amount of gas left at the start of the method.
Next, we are taking the mUserOp field from the userOpInfo object.
Then we initialise the mUserOp object using values from userOp. Check here to see how it is done visually.
Next we calculate userOpHash field.
UserOpHash calculation
We are here
In actual code there are internal method calls that happens to get userOpHash but here we’ll just combine all method calls and present a simple code to see how userOpHash is calculated.
We are inside => handleOps > validatePrepayment > getUserOpHash
The pack function is an internal function that takes a UserOperation object and returns a bytes array. It does this by copying the UserOperation object from calldata up to, but not including, the signature field. It then returns this data as a bytes array.
The purpose of this function is to create a lighter representation of the UserOperation object that can be more efficiently passed around in memory.
Next,
The bitwise OR operation (|) combines some userOp values by setting each bit in the result to 1 if the corresponding bit is set in any of the input values. This means that the resulting maxGasValues variable will contain a value that is a combination of all of the specified gas limits and fees.
Here, the code checks that all numeric values in the userOp object are below 128 bits, so that they can be safely added and multiplied without causing an overflow error.
Next,
we are inside => handleOps > validatePrepayment
Here we defined a variable gasUsedByValidateAccountPrepayment that we’ll initialise in next lines, and we calculate requiredPreFund by calling _getRequiredPrefund(mUserOp) method.
Before we understand what _requiredPreFund is we need to understand that one of the responsibilities of EntryPoint is to ensure that bundler is paid back the gas fee used to execute UserOperations. Now the question is who pays this gas to bundler. This is either paid by Paymaster or Smart Contract Wallet itself.
In order to do that, Paymaster or SCW are expected to do a deposit of ether (or native currency of the blockchain) on EP contract and then EP uses this deposit to pay back the bundler.
Now that we understand this, let’s come back to _requiredPreFund calculation. It is the max amount of gas fee that is pre deducted from Paymaster/SCW deposit on EP to ensure that EP has enough deposit to pay back the bundler. Later if the actual gas cost comes out to be less than the requiredPreFund, excess amount is given back to the EP deposit.
We are inside => handle Ops > validatePrepayment > _getRequiredPrefund
We are here
We all know how to calculate gas fee using the formula GasFee = GasPrice * GasUsed
Here the gasPrice part is calculated from mUserOp.maxFeePerGas because we are trying to calculate the maximum fee that can be deducted from EP deposit.
And the gasUsed part is calculate using the formula
callGasLimit + verificationGasLimit * (3 in case of Paymaster or 1 in absense of Paymaster) + preVerificationGas
Important Fact
💡These Gas Limits are all coming from UserOperation object passed from outside. And now you should know that why we can’t just pass a very high number in these fields to avoid OOG(Out Of Gas) errors. Because higher values would mean high gas fee pre deducted from the deposit on EP, and even if the deposit would be enough to cover the actual gas fee, your transaction would revert here.So this is actually a challenge for clients when generating these gasLimit values for UserOperation to be just enough to avoid out of gas errors and not put too high values.
Next,
We are inside => handleOps > validatePrepayment
We are here
This step is the validation step done on Smart Contract Wallet, summary of this method is mentioned below.
- First the smart contract wallet is created using userOp.initCode if the wallet is not deployed already.
- If SCW pays for gas, it checks if wallet deposit on EP is enough to pay for the gas (requiredPrefund calculated above), if not it calculate missingAccountFunds.
- Call validateUserOp on SCW, it will return a validationData field.
- Again check if wallet deposit on EP is enough to pay for the gas (coz in validateUserOp call in step 3, wallet might have deposited missing funds)
- If funds are still not enough, it reverts.
- Else it decrement account deposit and calculate total gas used by this method.
- Returns total gas used by this method and validationData returned in step 3.
Code Snippet
We are inside => handleOps > validatePrepayment >_validateAccountPrepayment
Explanation of above code snippet:
1. First the on chain gas tracking is started.
2. We get mUserOp and sender from opInfo object. opInfo is the same UserOpInfo object whose type is defined here.
3. Next we call _createSenderIfNeeded() method to call Factory contract to deploy new SCW if it’s not already deployed.
4. Call numberMarker() method. This function is used as a checkpoint in the code. It adds a specific opcode in the flow, so that during off chain simulation we can get this while tracing the simulation call in bundlers.
5. Check if paymaster address is zero, this means that SCW is supposed to pay for the current UserOperation execution.
a. Calls balanceOf() method defined in the EP to get the deposit balance of sender (SCW)
b. Check if current deposit is enough to cover the max gas fee for this UserOperation.
6. Calls validateUserOp method on SCW. It uses mUserOp.verificationGasLimit as gas limit in this call. It pass (userOp, userOpHash, missingAccountFunds) as parameter to validateUserOp method.
7. It returns a validationData which is stored in the return variable validationData
8. If validateUserOp call fails, the whole operation reverts.
9. If SCW is paying for the gas fee, again the deposit is checked to see if it’s enough to cover the max gas fee for this operation. See the explanation above to see why this is done again.
10. Finish the gas tracking and assigns the total gas used in this method in return variable gasUsedByValidateAccountPrepayment
Next,
We are inside => handleOps > validatePrepayment
We are here
First, _validateAndUpdateNonce is called which takes mUserOp.sender and mUserOp.nonce as argument. It validate the nonce field and increment the value against sender address.
Note: Smart Contract Wallet is not supposed to handle the nonce field in validateUserOp method anymore as this is done in EntryPoint now.
Code Snippet
We are inside => handleOps > validatePrepayment >_validateAndUpdateNonce
Explanation of above code snippet:
- The nonce in EntryPoint is supposed to be a 2D nonce where both key and sequence number is represented by single uint256 value.
- Here first 192 bits represents the key part and rest 64 bits represents sequence value.
- For normal operations, key value will be 0 and sequence value is incremented sequentially.
- That’s why here first key part is extracted by right shifting nonce value by 64 and casting value to uint192 and then sequence number is extracted by casting nonce value to uint64.
- Then the stored sequence number is compared with the sequence number from input. If both matches, the stored value is incremented using post ++ operator.
Then the numberMarker() is used again. (Explained here)
And if paymaster is present in UserOperation, an internal method _validatePaymasterPrepayment is called. Summary of this method is mentioned below.
- Check if gas used by account validation is less than userOp.verificationGasLimit.
- Check if paymaster deposit on EP is enough to cover the max gas fee for this UserOperation.
- If not, whole operation reverts.
- Else deduct the max gas fee from the paymaster deposit on EP.
- Calls validatePaymasterUserOp on Paymaster. It will return a context object and validationData.
- Here, context object is totally upto Paymaster to define. EP just pass this context to Paymaster later while calling postOp method.
- If call in step 5 fails, whole operation reverts.
Code Snippet
We are inside => handleOps > validatePrepayment >_validatePaymasterPrepayment
Explanation of above code snippet:
- Get mUserOp object from opInfo. opInfo is the same UserOpInfo object whose type is defined here.
- Check if gas used by account validation is less than userOp.verificationGasLimit.
- By now this is understood that, userOp.verificationGasLimit value should cover at least the gas for account validation (including account deployment) and paymaster validation.
- Get the paymaster deposit from the deposits mapping in EP.
- If paymaster deposit is not enough to cover the max gas fee, EP reverts.
- Else deduct the max gas fee (requiredPrefund) from paymaster deposit in EP.
- Call Paymaster validatePaymasterUserOp method. Pass userOp.verificationGasLimit as gas limit.
- Pass (userOp, userOpHash, requiredPrefund) as arguments.
- If Paymaster validation call fails, whole operation reverts.
- Else return the context object and validation data returned by as returned by validatePaymasterUserOp call.
Quick Recap
Ok take a deep breath, we have done most of the verification part till now. So far as part of UserOperation verification,
- We called SCW.validateUserOp() which internally deploys the wallet if required.
- We called called Paymaster.validatePaymasterUserOp()
Looks simple, right?
There are many small checks related to gas and deposits inside these methods. So the code might look long, but conceptually its just getting the verification done by SCW and Paymaster. Remember devil is in the details.
Just to remind you again the handleOps flow looks like this
- validatePrepayment ← We are here
- validateAccountAndPaymasterValidationData
- executeUserOp
- compensate
We are still at the first step validatePrepayment. Now let’s finish the rest of the part of this method.
Next,
We are inside => handleOps > validatePrepayment
We are here
Explanation of above code snippet:
- We stop the on chain gas tracking.
- Now we check if userOp.verificationGasLimit is able to cover the gas used so far.
- If not, the whole operation reverts.
- Else we proceed and fill out rest of the fields on UserOpInfo object.
- outOpInfo.prefund is assigned the requiredPreFund value which is max gas fee deducted from the deposit on EP.
- outOpInfo.contextOffset is assigned the offset of context object in memory. Remember context is the object returned by Paymaster.validatePaymasterUserOp call.So instead of assigning the whole context object, we just save its memory offset. So we don’t have to pass around this heavy context object while calling internal methods.
- outOpInfo.preOpGas is assigned (Total gas used so far + userOp.preVerificationGas)
Important Fact
💡As explained in the beginning, UserOpInfo.preOpGas will contain total gas used so far, it includes
1. The actual logic written in EntryPoint contract
2. Other gas units, which is
a. Base gas fee(21000)
b. Gas spent in calling the handleOps and it’s parameters
c. Gas that will be used in later part of EntryPoint which can’t be tracked using gasleft() opcode. Will come back to this point again later in this article.
The second point can’t be tracked on chain, so we rely on userOp.preVerificationGas field and assume its value has covered these gas units.
OK, the prePayment part is done, let’s move to the next method that is called from handleOps method.
Just to remind you again the handleOps flow looks like this
- validatePrepayment ← This is Done
- validateAccountAndPaymasterValidationData ← We’ll begin with this method now
- executeUserOp
- compensate
validateAccountAndPaymasterValidationData method
Function Declaration
We are inside => handleOps > validateAccountAndPaymasterValidationData
Remember this function is being called from the Verification Loop in hanldeOps method, so
- First parameter is the index of the for loop happening in handleOps
- Second parameter is the validation data returned by validateUserOp method on SCW
- Third parameter is the validation data returned by validatePaymasterUserOp method on Paymaster
- Fourth parameter is the expected aggregator address (This is out of scope for this article, and in our flow address(0) is passed from handleOps
This function doesn’t return anything, it just revert if any of the validation data is not valid.
Function Declaration
This is a very small function that just do some validations on the validationData passed to it. Now to understand it better lets go into this validationData we’ve been talking about. How does it look like.
You must have observed that the type of validationData is uint256 which is returned by both SCW and Paymaster.
As per this ERC, the uint256 validationData value returned by SCW or Paymaster, MUST be a packed value consisting of authorizer, validUntil and validAfter timestamps.
Above all three values are packed in a single uint256 value.
authorizer ⇒ 0 for valid signature, 1 to mark signature failure. Otherwise, an address of an authorizer contract. This ERC defines “signature aggregator” as authorizer.
validUntil ⇒ 6-byte timestamp value, or zero for “infinite”. The UserOp is valid only up to this time.
validAfter ⇒ 6-byte timestamp. The UserOp is valid only after this time.
It’s upto the SCW or Paymaster to defined these values depending on their own use case. If you are not using BLS wallet or signature aggregator contract, then you can just return 0 value. EP will handle 0 value as positive validation.
Let’s start with code
We are inside => handleOps > validateAccountAndPaymasterValidationData
We are here
The code is self explanatory,
- First the validationData is decoded into aggregator address and a boolean value which tells if validUntil and validAfter part of validationData is valid as per the block.timestamp or not.
- If aggregator returned by SCW, doesn’t match with expectedAggregator, EP reverts.
- Remember expectedAggregator is passed as address(0) from handleOps. So in this flow SCW just need to return 0 value.
- If validUntil and validAfter are out of time range as per block.timestamp, EP reverts.
- Step 1-4 is repeated for paymasterValidationData as well.
- Only with one exception that if aggregator address is non-zero, EP reverts. So paymaster must return 0 value for this in validatePaymasterUserOp method.
💡Note that we are not doing any gas tracking in this function. So whatever gas is used here, needs to be accounted in userOp.preVerificationGas field. This value comes from outside and client calculates this value while building the UserOperation.
OK, our second method in handleOps is also done. Let’s see where we are now.
handleOps flow looks like this
- validatePrepayment ← This is Done
- validateAccountAndPaymasterValidationData ← This is Done
- executeUserOp ← We’ll begin with this method now
- compensate
executeUserOp method
Function Declaration
We are insldue => handleOps > executeUserOp
Remember this function is being called from the Execution Loop in hanldeOps method. So
- First parameter is the index of the for loop happening in handleOps
- Second parameter is UserOperation to be executed.
- Third parameter is object. In earlier part of code, this object has been initialised using values from userOp.
Returns,
collected ⇒ the amount of gas fee that needs to be compensated to the beneficiary address passed in handleOps method. It corresponds to total gas fee used in verification and execution and any extra execution of given UserOperation.
💡This amount should be ≥ the gas fee paid by the bundler transaction to call handleOps() otherwise there’s no incentive for bundler to execute this UserOperation.
We are here
Function Definition
Main purpose of this function is to execute the UserOperation, as validation has been done already and take any action that needs to be done post execution. Let’s start with the code
We are inside => handleOps > executeUserOp
- First, the on chain gas tracking again starts.
- We fetch the context object from the memory using opInfo.contextOffset. This context object was returned by Paymaster when we called validatePaymasterUserOp.Now we need to pass this object back to Paymaster after userOp execution, when we’ll call postOp method of Paymaster.
Next
We are inside => handleOps > executeUserOp
Explanation of above code snippet:
1. It starts with a try-catch block where this calls an internal method innerHandleOp that does the actual execution of userOp.
2. innerHandleOp returns the actualGasCost for given userOp.
3. It’s interesting to see all the code in catch block. Let’s see what it is.
4. In case call to innerHandleOp reverts, code execution will come to this catch block. Here we need to know the failure reason in catch block.
5. The flow will come to catch block because of following reasons
a. If OOG error comes during execution
b. If paymaster postOp method reverts which called from innerHandleOp method.
6. Now in catch block, first the revertCode is extracted using assembly code.
a. returndatacopy(0, 0, 32) copies 32 bytes of data from EP contract's memory starting at position 0 (which is where the returned data from the inner function call will be stored) to memory position 0
b. innerRevertCode := mload(0) loads 32 bytes of data from the memory position 0 and stores it in the innerRevertCode variable.
7. If the revertCode matches with INNER_OUT_OF_GAS, EP reverts the operation.
a. This INNER_OUT_OF_GAS can be emitted by OOG checks in case bundler didn’t pass enough gas limit while calling handleOps.
b. But even a malicious paymaster can revert with the same error code and cause a bundler to think it is "internal" error. That’s why in this case bundler should report the paymaster if this happens.
8. If the revert reason is something else, then EP calls _handlePostOp() method with IPaymaster.PostOpMode.postOpReverted mode. We’ll come back to this method later.
Now let’s go inside innerHandleOp method and see what’s happening.
We are here
We are inside => handleOps > executeUserOp > innerHandleOp
1. First thing to notice here is that it is declared as external method to open a call context, but it can only be called by handleOps method.
2. It starts on chain gas tracking using gasleft() opcode.
3. Check if msg.sender is address of EntryPoint only, else reverts.
4. Then in unchecked block, it checks if gas left so far is less than callGasLimit + mUserOp.verificationGasLimit + 5000
a. Here callGasLimit is the userOp.callGasLimit ⇒ gas limit used while calling SCW method to execute userOp.callData
b. We add userOp.verificationGasLimit coz we are going to make a call to Paymaster postOp method where this value is used as gas limit.
c. This 5000 value is there to protect against an edge-case where bundler crafted gas-limit can cause inner call (SCW call) to revert and still pay.
5. If step 4 is true, it revert with INNER_OUT_OF_GAS as revert reason.
6. Else we proceed to call SCW using userOp.callData. Here a library Exec is used to make this call. This is regular assembly code to call a destination with callData.
7. You can check Exec library code here.
8. If SCW call revert, the EP does’t revert but it just emits an event and initialise mode variable with value IPaymaster.PostOpMode.opReverted
9. This mode variable is passed as argument to _handlePostOp method (called at the end of this method), for it to know from where it is being called.
10. Remember _handlePostOp() is also called from _executeUserOp method in the catch block.
11. The last unchecked block, first calculate total gas used so far till this point.
12. It is calculated as total gas used in this method + all earlier gas used starting from handleOps call which is already captured in opInfo.preOpGas
13. At last it calls _handlePostOp.
Now let’s go inside _handlePostOp method and see what’s happening. This is the final internal method call in Execution Flow.
💡Till here, SCW execution is completed and now we just need to make last call to Paymaster post0p method
We are here
This is a bit long function, so lets break down this in Function Declaration and Function Definition
Function Declaration
We are inside => handleOps > executeUserOp > innerHandleOp > _handlePostOp
opIndex ⇒ index in the user operation batch
mode ⇒ whether is called from innerHandleOp, or outside (postOpReverted)
opInfo ⇒ userOp fields and info collected during validation
context ⇒ the context returned in validatePaymasterUserOp
actualGas ⇒ the gas used so far by this user operation
Function Definition
This method can be a bit hard to understand so let’s start to understand the code in chunks.
We are inside => handleOps > executeUserOP > innerHandleOp > _handlePostOp
This is the part where we calculate the userOp gas price.
- It calculate the gasPrice to be used to calculate the final gas cost of this userOp.
- maxFeePerGas and maxPriorityFeePerGas param are taken from userOp for this.
- gasPrice ← min(maxFeePerGas, maxPriorityFeePerGas + block.basefee) Calculation is same as done in EIP-1559 for transactions.
Next,
We are inside => handleOps > executeUserOP > innerHandleOp > _handlePostOp
1. Then we check who is paying for the gas for this userOp, SCW or Paymaster. Then we assign a refundAddress accordingly.
2. If context object is not empty, we proceed to call postOp method on Paymaster.
3. If this call is coming from innerHandleOp method, it calls postOp method. Here we don’t care if postOp reverts or not. EP will still pay the bundler the gas fee for this userOp.
4. If this call is coming from _executeUserOp catch block, it calls postOp method.
a. If postOp reverts this time, the whole operation reverts with proper revert reason (if available).
b. If flow is coming here, that means postOp is already called once, and this is second time the postOp is called.
Next,
We are inside => handleOps > executeUserOP > innerHandleOp > _handlePostOp
- At the end, it calculate the final gas cost of user operation.
- It checks if the actual gas cost used is less than the requiredPrefund calculated very early in handleOps flow.
- If yes, then it refund the excess gas cost in the EP deposit corresponding to refundAddress calculated earlier.
- It emits UserOperationEvent event with all relevant information. The success field indicated if call to SCW was succeeded or not.
OK we are almost done with the whole handleOps flow. Lets see where we are now
handleOps flow looks like this:
- validatePrepayment ← This is Done
- validateAccountAndPaymasterValidationData ← This is Done
- executeUserOp ← This is Done
- compensate ← We’ll begin with this method now
Compensate Method
We are here
Function Declaration
We are inside => handleOps >_compensate
This method is not called from any for loop in handleOps so there’s no index parameter.
- First parameter is the beneficiary address where the EP should transfer gas fee from its deposit.
- Second parameter is the amount of gas fee to be transferred to beneficiary.
Function Definition
This is very simple and small method that just transfers the gas fee from EP deposits to beneficiary address.
Let’s check the code
This code is pretty self explanatory,
- It checks if beneficiary is not a zero address.
- Then it transfers the gas fee in native currency of blockchain to the beneficiary.
- It checks if the transfer was successful or not. If not, it reverts.
And we are done!
Huh, let’s take a break. Grab a cup of coffee or whatever calm your mind.
This was a pretty long piece of code to understand. And it might take a lot of reading again to understand it end to end.
So I’d recommend you go through the EntryPoint contract code yourself now and try to make some other developer in your team understand this code.
Quick Recap
If you have made it this far, it calls for a recap of some important concept again.
1. EntryPoint (EP) contract is the core contract in ERC-4337 which orchestrate the whole transaction flow of UserOperation by interacting with SCW Factory contract, SCW contract and Paymaster contract.
2. EP always have deposit in native token on the blockchain. If someone wants to pay gas in ERC20 tokens for UserOp then it needs to be handled in Paymaster or SCW. For example, in case SCW want’s to pay for the gas in ERC20 token, it needs to first convert those ERC20 to native token (by interacting with some DEX) and then deposit it on EP contract.
3. EP has handleOps method which is called by the bundler. Bundler can pass multiple UserOperation to this method and a beneficiary address where the gas refund goes from EP.
4. EP stores the gas deposited by either Paymaster or SCW. Based on the UserOperation fields it decides from which deposit it will refund the bundler(beneficiary) address.
5. EP does its best to calculate the actual gas used during the execution. But unfortunately it can’t track all of the gas used, so it relies on UserOp.preVerificationGas field to cover the untracked gas during the execution.
a. So higher value of preVerificationGas means more profit for the Bundler.
6. UserOp.maxFeePerGas and UserOp.maxPriorityFeePerGas decides the gasPrice to be used by EP for calculate the bundler refund.
a. So Bundler should try to send handleOps transaction with lower gas fee for actual handleOps transaction to make some profit.