Symptom
A USDC-funded Paymaster contract that uses the nonReentrant modifier on validatePaymasterUserOp causes all UserOperations to be silently rejected by Pimlico (and other ERC-7562-compliant bundlers) during simulation. The error message is generic:
AA33 reverted (or) validatePaymasterUserOp simulation failed
No on-chain revert occurs — the rejection happens at the bundler's simulation step, before the UserOp is ever broadcast.
Root cause
ERC-7562 restricts what storage an unstaked paymaster may write during validation:
An unstaked paymaster may only write to storage slots associated with the userOp.sender (sender-keyed storage).
A standard nonReentrant modifier writes to a global boolean (_locked), which is NOT keyed on userOp.sender. This write is detected during simulation and the UserOp is rejected.
Affected code pattern
// ❌ WRONG — violates ERC-7562 for unstaked paymasters
function validatePaymasterUserOp(...) external nonReentrant onlyEntryPoint ... {
// _locked (global bool) write is forbidden during validation
}
Fix
Remove nonReentrant from validatePaymasterUserOp. The onlyEntryPoint modifier already provides sufficient protection — the EntryPoint never re-enters its own validation phase:
// ✅ CORRECT
function validatePaymasterUserOp(...) external onlyEntryPoint whenNotPaused ... {
// Only writes locked[user] — sender-keyed, allowed by ERC-7562
locked[user] += maxUsdcCost;
}
nonReentrant is safe (and good practice) on user-callable functions like deposit(), withdraw(), deductGas(), and postOp() — just not on validatePaymasterUserOp.
Staking as alternative
Staking the paymaster in the EntryPoint via addStake() allows global storage writes. However, staking requires a dedicated function in the contract and a separate ETH deposit. Removing nonReentrant from validatePaymasterUserOp is simpler and fully correct.
Related: deductGas must check unlocked balance
If your paymaster supports both a legacy relayer path (deductGas) and the ERC-4337 path, make sure deductGas checks the available (unlocked) balance, not the total balance:
// ❌ WRONG — can double-charge if a UserOp is pending
require(balances[user] >= usdcCost, "underfunded");
// ✅ CORRECT — respects in-flight reservations
uint256 available = balances[user] > locked[user] ? balances[user] - locked[user] : 0;
require(available >= usdcCost, "underfunded");
Without this check, a concurrent pending UserOp and a relayer deduction can violate the locked ≤ balance invariant, leading to double-charging.
References
Symptom
A USDC-funded Paymaster contract that uses the
nonReentrantmodifier onvalidatePaymasterUserOpcauses all UserOperations to be silently rejected by Pimlico (and other ERC-7562-compliant bundlers) during simulation. The error message is generic:No on-chain revert occurs — the rejection happens at the bundler's simulation step, before the UserOp is ever broadcast.
Root cause
ERC-7562 restricts what storage an unstaked paymaster may write during validation:
A standard
nonReentrantmodifier writes to a global boolean (_locked), which is NOT keyed onuserOp.sender. This write is detected during simulation and the UserOp is rejected.Affected code pattern
Fix
Remove
nonReentrantfromvalidatePaymasterUserOp. TheonlyEntryPointmodifier already provides sufficient protection — the EntryPoint never re-enters its own validation phase:nonReentrantis safe (and good practice) on user-callable functions likedeposit(),withdraw(),deductGas(), andpostOp()— just not onvalidatePaymasterUserOp.Staking as alternative
Staking the paymaster in the EntryPoint via
addStake()allows global storage writes. However, staking requires a dedicated function in the contract and a separate ETH deposit. RemovingnonReentrantfromvalidatePaymasterUserOpis simpler and fully correct.Related: deductGas must check unlocked balance
If your paymaster supports both a legacy relayer path (
deductGas) and the ERC-4337 path, make suredeductGaschecks the available (unlocked) balance, not the total balance:Without this check, a concurrent pending UserOp and a relayer deduction can violate the
locked ≤ balanceinvariant, leading to double-charging.References