Breaking down the paymaster
In this page, we will deep-dive the paymaster by code block-to-block and then pieces-by-pieces. If you already know the basic operations of the paymaster, skip to the several types of the paymaster.
The interfaces described by ERC-4337
In ERC-4337, there's an interface that defines the interaction functions and an enum used by the paymaster when working with the EntryPoint contract. These components are essential for the paymaster's proper operation. The paymaster interacts with these interfaces in two key steps: the validation step and the execution step, represented by the validatePaymasterUserOp() and postOp() functions, respectively.
Let’s dive into how these functions and the enum PostOpMode interact with the EntryPoint and the PackedUserOperation struct, which is passed as calldata from EntryPoint.
Well...it sounds pretty complicated, but don't worry. we will explore it step-by-step with the diagram and the descriptions!
Before we start: intertaction with the EntryPoint
Before we deep-dive how the function actually works, we will see the top-level of how these functions interact with the Entrypoint contract. Let's see how it works with the flow graph below.

In the flow graph, you can see how the paymaster interacts with EntryPoint through the two functions we previously discussed. EntryPoint processes the userOp in two main phases: the validation phase and the execution phase.
During the validation phase, EntryPoint ensures that all involved entities—such as the paymaster, SCW, SCW factory (which can deploy the wallet contract), and the aggregator—are valid and meet the required conditions. In the execution phase, EntryPoint allows the paymaster to check whether the user has sufficient gas, if necessary. This is the key feature that allows users to perform transactions without holding ETH in their wallets.
For example, if a user uses paymaster that converts their tokens into ETH, they can cover the gas fees using tokens like ERC-20 Token, which are repaid to the bundler. Additionally, if the paymaster is running a promotion, the user may benefit from reduced gas fees, as the paymaster can cover a portion of the total gas costs. (For more details, check out the Several types of the paymaster section.) The flexibility of the paymaster interface also opens up many possibilities for customizing operations further.
Paymaster-validation phase
Now that we’ve covered the flow and use of the paymaster, let's dive into the actual parameters, return values, and examples of how the paymaster operates in practice.
Try-Catch mechanism of EntryPoint in validation phase
try
IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}(
op,
opInfo.userOpHash,
requiredPreFund
)
returns (bytes memory _context, uint256 _validationData) {
context = _context;
validationData = _validationData;
} catch {
revert FailedOpWithRevert(opIndex, "AA33 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN));
}validatePaymasterUserOp() function is called by EntryPoint in validation phase. All external contract calls are handled within try-catch blocks to manage potential revert messages issued by the EntryPoint. These revert messages follow a specific format (e.g. AA33) and are used by the bundler to impose penalties within its system when a malicious operation triggers a revert.
validatePaymasterUserOp: params
[gas: pmVerificationGasLimit] This parameter originates from the
userOpstruct. Its value is taken frommUserOp, which is used by theEntrypointto duplicate theuserOpcalldata into memory, helping to save gas fees. Typically, this value is packed within thepaymasterAndDatafield in theuserOpcalldata. However, if the user isn't utilizing a paymaster, the value inmUserOpwill be set to zero. The value is then unpacked and stored inmUserOp.paymasterVerificationGasLimitbefore being passed topmVerificationGasLimit. This process ensures that the value resides in the stack area sincemUserOpis located in memory, andpmVerificationGasLimit(uint256type) will be pushed onto the stack.[param0: op -> userOp] The
PackedUserOperationstruct that is parsed from the bundler's calldata, which should be verified in the paymaster's context. Since there is no standardized verification method for this struct, the paymaster has the freedom to validate it however it sees fit. For example, the paymaster might perform signature validation to confirm permission or simply check whether the user has the ability to cover the payment.[param1: opInfo.userOpHash -> userOpHash] The hash of the
userOpallows the paymaster to verify whether theuserOpis valid. For instance, the paymaster can use this hash to check the signature and confirm if thepaymasterAndDatafield in the struct is accurate.[param2: requiredPreFund -> maxCost] This parameter represents the maximum predicted cost of the transaction, estimated by the bundler (or the user). The paymaster uses this value to determine the compensation amount it may need to collect from the user, if necessary. Additionally, during the validation phase, the
EntryPointtemporarily deducts this amount in ether from the paymaster's deposited balance.
validatePaymasterUserOp: returns
[returns0: bytes memory context] The value to send to the postOp. If the paymaster needs to cache something after this validation and toss it to the postOp, it can be passed by this bytes. This value is sent to
postOpfunction. If the paymaster needs to temporarily store data after validation and pass it topostOpfunctionon, it can do so using thisbytesfield. Q: Why don't we just cache it to the storage slot in the paymaster? A: Due to gas costs. Using theSSTOREopcode (writing to storage) is significantly more expensive than theMSTOREopcode (storing in memory). Thus, returning the value directly through memory is much cheaper.[returns1: uint256 validationData]
This field consists of:
sigAuthorizer (20 bytes)
validUntil (6 bytes)
validAfter (6 bytes)
The
validatePaymasterUserOpfunction doesn't revert if the signature validation fails. Instead, it returns a flag value of1, encoded within the 20-bytesigAuthorizersection. This design avoids immediate reversion since theEntryPointhas its own reversion format, and it allows the bundler to handle potential penalties. If the signature validation succeeds, the function returns0. ThevalidUntilandvalidAfterfields represent timestamps. The operation will only be executed if the current timestamp is greater thanvalidAfterand less thanvalidUntil. This check is performed immediately after receiving the return value in the_validateAccountAndPaymasterValidationDatafunction.
Paymaster-execution phase
The execution phase is significantly more complex than the validation phase. This is due to the need to handle the operation's result and the potential inability of the user to pay the required amount after the execution of the userOp. Multiple calls to paymaster contract may occur during this phase.
To provide a clearer overview, we organized the process in a time table and present it as a flow graph, accompanied by additional explanations.
Before we start: PostOpMode
The enum introduced in the paymaster interface is crucial for the operation state when calling the paymaster's postOp function. There are three values in the enum.
[value0: opSucceeded ] [value1: opReverted ] [value2: postOpReverted]
opSucceeded is the state when the userOperation is executed by the SCW successfully. If it dosen't succeed, the state would be opReverted. postOpReverted is the state when the postOp has been reverted for unexpected operation. In this case, the postOp is not called again.

Let's begin with the innerHandleOp() function. This function is invoked by _executeUserOp(), which is responsible for handling all operations during the execution phase. Within this process, innerHandleOp() is called to execute the actual operation and to interact with the paymaster through the postExecution() function. When calling postExecution(), innerHandleOp() passes the following parameters:
_postExecution: params
[param0: IPaymaster.PostOpMode mode] The value which represents whether the user's TX succeed or failed.
[param1: UserOpInfo memory opInfo]
struct UserOpInfo { MemoryUserOp mUserOp; bytes32 userOpHash; uint256 prefund; uint256 contextOffset; uint256 preOpGas; }- mUserOp: A copy of the original transaction’s calldata sent by the bundler. - userOpHash: The hash of the
userOp, used for verifying its correctness through ECDSA. However, it is not utilized during the validation phase. - prefund: The estimated total gas required for executing theuserOp. TheinnerHandleOp()function uses this value when calculating the expected gas cost to reimburse the bundler. - contextOffset: The memory offset used to load the operation’s context. Since the context is cached in theEntryPointcontract’s memory, it needs to be retrieved before passing it topostOpfunction in the external paymaster contract. - preOpGas: The amount of gas consumed before executing theuserOpcall to the SCW. This value must be passed toinnerHandleOp()function to correctly calculate the total gas usage for the operation.[param2: bytes memory context] This field holds the cached values from the validation phase. As discussed, the paymaster passes this data to the
EntryPointthrough thevalidatePaymasterUserOp()function. The contents of thecontextare determined by the paymaster, based on its internal logic and requirements.[param3: uint256 actualGas] The exact total gas used right before
postOpfunction. It is passed topostOpfunction to verify whether the user or paymaster can cover the gas costs. This ensures that the transaction can be settled without issues.
In the postexecution() function, there will be a call to the paymaster with several parameters.
postOp: params
[param0: PostOpMode mode] This value indicates whether the user’s TX succeeded or failed. The paymaster can apply different logic based on the mode. For instance, if a user’s TX reverts frequently, the paymaster might decide to block the user from accessing its services.
[param1: bytes calldata context] This parameter holds the data cached by the paymaster during the validation phase.
[param2: uint256 actualGasCost] The exact total amount of gas used right before the
postOpfunction. It is provided topostOpfunction to ensure that the gas cost can be covered, either by the user or the paymaster.[param3: uint256 actualUserOpFeePerGas] This parameter represents the gas price used for the TX. In the context of SCWs, the gas fee must be calculated based on the initial price provided by the bundler to ensure consistency between off-chain and on-chain calculations. Since gas prices can fluctuate due to network volatility, mismatches between the predicted gas fee (off-chain) and the actual gas fee (on-chain) could lead to unexpected issues like denial of service (DoS). To mitigate this, all operations must use a consistent gas price to ensure stability and avoid such risks in account abstraction.
What operation would be in the postOp?
Typically, operation in postOp function is used to calculate the gas consumed and claim it from another entity. However, this is just an example—the specific gas claiming logic varies depending on the paymaster. Some paymasters cover all the gas fees and claim them off-chain, while others claim the gas fees during the transaction itself. If you're reviewing a specific paymaster, refer to this documentation. For examples of how different paymasters operate, check the postOp section in the documentation for Several types of the paymaster.
Caution: What if the initial postOp reverts?
If the call to the postOp function fails, the innerHandleOp() function will also revert, triggering the catch block.
} else {
emit PostOpRevertReason(
opInfo.userOpHash,
opInfo.mUserOp.sender,
opInfo.mUserOp.nonce,
Exec.getReturnData(REVERT_REASON_MAX_LEN)
);
uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
collected = _postExecution(
IPaymaster.PostOpMode.postOpReverted,
opInfo,
context,
actualGas
);
}There are several reversion catch codes, but the one we need to focus on is the else statement. In this scenario, _executeUserOp() emits the PostOpRevertReason and then calls another function, _postExecution(), with the mode set to postOpReverted. However, it does not invoke postOp again because the state changes made by SCW are lost due to the revert. As a result, the responsibility for handling the revert in postOp now falls on the paymaster.
Last updated