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
userOp
struct. Its value is taken frommUserOp
, which is used by theEntrypoint
to duplicate theuserOp
calldata into memory, helping to save gas fees. Typically, this value is packed within thepaymasterAndData
field in theuserOp
calldata. However, if the user isn't utilizing a paymaster, the value inmUserOp
will be set to zero. The value is then unpacked and stored inmUserOp.paymasterVerificationGasLimit
before being passed topmVerificationGasLimit
. This process ensures that the value resides in the stack area sincemUserOp
is located in memory, andpmVerificationGasLimit
(uint256
type) will be pushed onto the stack.[param0: op -> userOp] The
PackedUserOperation
struct 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
userOp
allows the paymaster to verify whether theuserOp
is valid. For instance, the paymaster can use this hash to check the signature and confirm if thepaymasterAndData
field 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
EntryPoint
temporarily 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
postOp
function. If the paymaster needs to temporarily store data after validation and pass it topostOp
functionon, it can do so using thisbytes
field. Q: Why don't we just cache it to the storage slot in the paymaster? A: Due to gas costs. Using theSSTORE
opcode (writing to storage) is significantly more expensive than theMSTORE
opcode (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
validatePaymasterUserOp
function doesn't revert if the signature validation fails. Instead, it returns a flag value of1
, encoded within the 20-bytesigAuthorizer
section. This design avoids immediate reversion since theEntryPoint
has its own reversion format, and it allows the bundler to handle potential penalties. If the signature validation succeeds, the function returns0
. ThevalidUntil
andvalidAfter
fields represent timestamps. The operation will only be executed if the current timestamp is greater thanvalidAfter
and less thanvalidUntil
. This check is performed immediately after receiving the return value in the_validateAccountAndPaymasterValidationData
function.
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 theEntryPoint
contract’s memory, it needs to be retrieved before passing it topostOp
function in the external paymaster contract. - preOpGas: The amount of gas consumed before executing theuserOp
call 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
EntryPoint
through thevalidatePaymasterUserOp()
function. The contents of thecontext
are determined by the paymaster, based on its internal logic and requirements.[param3: uint256 actualGas] The exact total gas used right before
postOp
function. It is passed topostOp
function 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
postOp
function. It is provided topostOp
function 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