Skip to content

Paymaster gas optimization#6613

Closed
gonzaotc wants to merge 6 commits into
masterfrom
gas/paymaster-council
Closed

Paymaster gas optimization#6613
gonzaotc wants to merge 6 commits into
masterfrom
gas/paymaster-council

Conversation

@gonzaotc

@gonzaotc gonzaotc commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Note

AI-generated with the Solidity Gas Optimizer skill in council mode: 4 models (Composer, Fable 5, GPT-5.6 Sol, Grok 4.5) each ran the full audit independently, the runs were merged and adjudicated, and every finding below was re-measured on this branch. All tests pass, but it's encouraged to review before merging to confirm behavior is unaffected.

Run

  • Measurement: Hardhat, solc 0.8.35, runs 200, via-IR false; runtime deltas from deterministic whole-transaction handleOps probes, since the gas reporter cannot attribute calls made through the EntryPoint predeploy
  • Tests: Full project suite: 8,007 passing and 1 pending (pre-existing); paymaster suite 90 passing
  • Source runs: 4 independent audits at db06a6ef / d017d3e5; the scope tree is byte-identical across both commits
  • Scope:
    contracts/account/paymaster/
      Paymaster.sol
      extensions/
        PaymasterERC20.sol
        PaymasterERC20Guarantor.sol
        PaymasterERC721Owner.sol
        PaymasterSigner.sol
    

Results

Five independent gas optimization candidates survived adjudication, one commit each and keyed by the IDs below. It's recommended to review them one by one, via the commits view.

ID Change Function Δ runtime /call Δ deploy gas Found by
GAS-H-01 parse both paymaster gas limits with one calldata load PaymasterSigner._signableUserOpHash −712 −20,547 / −20,558 Fable 5, GPT-5.6 Sol
GAS-H-02 skip the mulDiv 512-bit path when the product fits one word PaymasterERC20._erc20Cost −966 base, −1,932 guaranteed +17,102 Fable 5, GPT-5.6 Sol
GAS-M-01 one-window timestamp decode PaymasterSigner._decodePaymasterUserOp −199 +876 / +864 Grok 4.5, Composer, GPT-5.6 Sol
GAS-L-01 unchecked refund subtraction under the EntryPoint invariant PaymasterERC20._refund −72 per postOp within noise Fable 5
GAS-L-02 branchless validation data via SafeCast.toUint PaymasterERC721Owner._validatePaymasterUserOp −12 −3,458 / −3,465 Fable 5

GAS-H-01 · parse both paymaster gas limits with one calldata load (PaymasterSigner._signableUserOpHash)

  • What it does: replaces the two ERC4337Utils accessor calls with one length < 52 check and one 32-byte load of paymasterAndData[20:52], split on the stack into the verification and postOp gas limits; the encoded hash is byte-identical, including the zero fallback for short data.
  • Why it's cheaper: each accessor re-derived the calldata slice, re-checked the length, and loaded overlapping bytes of the same word; one load removes the duplicated bounds checks and slice construction, and dropping the accessor code removes about 20.5k of deployment bytecode.
  • Tradeoff: the [20:52] offsets and the 128-bit split are spelled out locally in EIP-712 hashing code, duplicating layout knowledge the ERC4337Utils accessors centralize. Cherry-picks independently.
  • Found by: Fable 5 and GPT-5.6 Sol, independently at the same base commit; the deploy delta was reproduced by both runs and again on this branch.

GAS-H-02 · skip the mulDiv 512-bit path when the product fits one word (PaymasterERC20._erc20Cost)

  • What it does: branches on high == 0 from the already-computed mul512 and finishes the common case with Math.ceilDiv, keeping mulDiv for the genuine multi-word case and preserving the type(uint256).max saturation sentinel, including the denominator == 0 edge.
  • Why it's cheaper: the overflow guard already computes the full 512-bit product and Math.mulDiv recomputes it internally; the one-word case needs only a ceiling division. The function runs twice per sponsored operation and four times per guaranteed one.
  • Tradeoff: a second ceiling-division path in money math that must stay congruent with mulDiv (the equivalence at the high == 0 boundary and both sentinel edges was independently verified in review), plus +17.1k deployment gas per concrete paymaster. Cherry-picks independently.
  • Found by: Fable 5 (measured); GPT-5.6 Sol raised the same waste as an advisory proposing a shared single-pass mulDiv primitive in Math, which remains the API-level alternative.

GAS-M-01 · one-window timestamp decode (PaymasterSigner._decodePaymasterUserOp)

  • What it does: reads the 12-byte validity window once as bytes12 and splits it with casts (uint48(bytes6(validity)), uint48(bytes6(validity << 48))) instead of two adjacent bounds-checked 6-byte slices; the short-data path is unchanged.
  • Why it's cheaper: one calldata slice and one bounds check replace two of each, on the decode executed by every signed user operation.
  • Tradeoff: roughly 870 gas more at deployment and one shift-based byte offset in place of two explicit slice ranges, an idiom this file already uses for the gas-limits split. Cherry-picks independently.
  • Found by: Grok 4.5, Composer, and GPT-5.6 Sol (whose isolated-function probe measured it flat; the whole-transaction re-measurement on this branch shows −199 per signed operation).

GAS-L-01 · unchecked refund subtraction under the EntryPoint invariant (PaymasterERC20._refund)

  • What it does: wraps prefundAmount - actualAmount_ in unchecked.
  • Why it's cheaper: the EntryPoint guarantees actualGasCost <= maxCost, so actualAmount_ <= prefundAmount holds and the compiler's underflow check is unreachable; removing it saves 72 gas on every postOp refund.
  • Tradeoff: the no-underflow guarantee moves from local code to the EntryPoint invariant; a non-compliant caller that breaks it now produces a wrapped amount that fails inside trySafeTransfer (reverting with PaymasterERC20FailedRefund) instead of a Panic. Cherry-picks independently.
  • Found by: Fable 5.

GAS-L-02 · branchless validation data via SafeCast.toUint (PaymasterERC721Owner._validatePaymasterUserOp)

  • What it does: returns SafeCast.toUint(token().balanceOf(userOp.sender) == 0) instead of a ternary selecting between SIG_VALIDATION_FAILED and SIG_VALIDATION_SUCCESS.
  • Why it's cheaper: the two constants are exactly 1 and 0, so the conditional jump becomes a bool-to-uint cast, saving 12 gas per operation and 3.4k deployment gas.
  • Tradeoff: the named constants leave the return expression; the failure-to-1 mapping is documented by an inline comment and relies on the constants' numeric values. Cherry-picks independently.
  • Found by: Fable 5.

🤖 Generated with Claude Code

gonzaotc and others added 6 commits July 17, 2026 13:33
…ne calldata load (-712 gas/op, -20.5k deploy)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…roduct fits one word (-966 gas/op base, -1,932 guaranteed, +17.1k deploy)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…PaymasterUserOp (-199 gas/op, +0.9k deploy)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…documented EntryPoint invariant (-72 gas/postOp)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…feCast.toUint (-12 gas/op, -3.4k deploy)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f77682c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gonzaotc

Copy link
Copy Markdown
Contributor Author

Superseded by #6614, same content from a fork branch; this one was opened from a repo branch by mistake.

@gonzaotc gonzaotc closed this Jul 17, 2026
@gonzaotc
gonzaotc deleted the gas/paymaster-council branch July 17, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant