Skip to content

PaymasterHub: onboarding postOp lacks postOpReverted fallback #124

@hudsonhrh

Description

@hudsonhrh

Summary

When an onboarding op's first postOp call reverts (e.g., solidarity fund drained by earlier ops in the same bundle), EntryPoint retries with PostOpMode.postOpReverted. The current code calls _updateOnboardingUsage for all modes, which contains if (solidarity.balance < actualGasCost) revert InsufficientFunds() — the exact same revert that caused the first failure. The second postOp also reverts, and the EntryPoint penalizes the paymaster (charges it for gas with no accounting update).

Code Location

src/PaymasterHub.sol, postOp function:

if (isOnboarding) {
    _updateOnboardingUsage(actualGasCost, mode == IPaymaster.PostOpMode.opSucceeded);
}

_updateOnboardingUsage (line 1892):

if (solidarity.balance < actualGasCost) revert InsufficientFunds();
solidarity.balance -= uint128(actualGasCost);

There is no postOpReverted branch for onboarding, unlike org ops which have _postOpFallback.

How This Can Happen

  1. Multiple onboarding ops packed in one bundle (solidarity balance not reserved during validation — see PaymasterHub: solidarity balance not reserved during validation (bundle safety gap) #123)
  2. Each validatePaymasterUserOp passes the solidarity.balance >= maxCost check
  3. PostOps run sequentially, each deducting actualGasCost from solidarity.balance
  4. When solidarity runs out, _updateOnboardingUsage reverts
  5. EntryPoint retries with postOpReverted — same revert
  6. Paymaster is penalized: gas charged with no accounting

Impact

  • Paymaster's EntryPoint deposit is reduced (real cost)
  • Solidarity balance is not decremented (shows phantom surplus)
  • Over time, solidarity.balance diverges from actual available funds
  • Daily creation counter (attemptsToday) is not refunded for the failed op

Practical risk is low because:

  • Onboarding has its own rate limit (dailyCreationLimit)
  • Each op requires a unique initCode (deploying a new Passkey account)
  • maxGasPerCreation bounds per-op cost
  • Block gas limit bounds total ops per bundle
  • Draining solidarity via onboarding requires solidarity.balance / maxGasPerCreation concurrent account deployments

Suggested Fix

Add a postOpReverted branch for onboarding:

if (isOnboarding) {
    if (mode == IPaymaster.PostOpMode.postOpReverted) {
        // Solidarity was insufficient. Paymaster absorbs the loss.
        // Refund daily counter since the op effectively failed.
        OnboardingConfig storage onboarding = _getOnboardingStorage();
        if (onboarding.attemptsToday > 0) onboarding.attemptsToday--;
        return;
    }
    _updateOnboardingUsage(actualGasCost, mode == IPaymaster.PostOpMode.opSucceeded);
}

Tradeoff: This skips the solidarity deduction, creating a discrepancy between solidarity.balance and the paymaster's actual EntryPoint deposit. The paymaster was already charged by EntryPoint, so the funds are gone, but solidarity's accounting doesn't reflect it. This phantom surplus could cause future ops to validate against non-existent funds, cascading the same failure.

Better long-term fix: Add solidarity reservation during validation for onboarding ops (related to #123), preventing the root cause entirely.

Related

Labels

Security, Enhancement, PaymasterHub

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions