From 27b9c51a3d01f6fa114ebd988f8428f19f4fcf68 Mon Sep 17 00:00:00 2001 From: Gonzalo Othacehe Date: Fri, 17 Jul 2026 11:37:33 -0300 Subject: [PATCH 1/6] gas: GAS-H-01 PaymasterSigner: parse both paymaster gas limits with one calldata load (-712 gas/op, -20.5k deploy) Co-Authored-By: Claude Fable 5 --- .../account/paymaster/extensions/PaymasterSigner.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/account/paymaster/extensions/PaymasterSigner.sol b/contracts/account/paymaster/extensions/PaymasterSigner.sol index 93cd0ff6479..df9bc031d47 100644 --- a/contracts/account/paymaster/extensions/PaymasterSigner.sol +++ b/contracts/account/paymaster/extensions/PaymasterSigner.sol @@ -37,6 +37,11 @@ abstract contract PaymasterSigner is AbstractSigner, EIP712, Paymaster { uint48 validAfter, uint48 validUntil ) internal view virtual returns (bytes32) { + // Both paymaster gas limits share the word at [20:52]: a single load replaces + // the two `ERC4337Utils` accessors, which each length-check and load it. + uint256 paymasterGasLimits = userOp.paymasterAndData.length < 52 + ? 0 + : uint256(bytes32(userOp.paymasterAndData[20:52])); return _hashTypedDataV4( keccak256( @@ -49,8 +54,8 @@ abstract contract PaymasterSigner is AbstractSigner, EIP712, Paymaster { userOp.accountGasLimits, userOp.preVerificationGas, userOp.gasFees, - userOp.paymasterVerificationGasLimit(), - userOp.paymasterPostOpGasLimit(), + paymasterGasLimits >> 128, + uint256(uint128(paymasterGasLimits)), validAfter, validUntil ) From 425b38fc389b6cdc9c33181c4aa377c38cd9d3ff Mon Sep 17 00:00:00 2001 From: Gonzalo Othacehe Date: Fri, 17 Jul 2026 11:39:11 -0300 Subject: [PATCH 2/6] gas: GAS-H-02 PaymasterERC20: skip the mulDiv 512-bit path when the product fits one word (-966 gas/op base, -1,932 guaranteed, +17.1k deploy) Co-Authored-By: Claude Fable 5 --- contracts/account/paymaster/extensions/PaymasterERC20.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/account/paymaster/extensions/PaymasterERC20.sol b/contracts/account/paymaster/extensions/PaymasterERC20.sol index 1e098a6b881..3886a320a31 100644 --- a/contracts/account/paymaster/extensions/PaymasterERC20.sol +++ b/contracts/account/paymaster/extensions/PaymasterERC20.sol @@ -289,9 +289,12 @@ abstract contract PaymasterERC20 is Paymaster { */ function _erc20Cost(uint256 nativeCost, uint256 tokenPerNative) internal view virtual returns (uint256) { uint256 denominator = _tokenPerNativeDenominator(); - (uint256 high, ) = nativeCost.mul512(tokenPerNative); + (uint256 high, uint256 low) = nativeCost.mul512(tokenPerNative); + if (high >= denominator) return type(uint256).max; + // When the product fits in one word, a plain ceiling division avoids repeating + // the 512-bit product inside `mulDiv`. return - high < denominator ? nativeCost.mulDiv(tokenPerNative, denominator, Math.Rounding.Ceil) : type(uint256).max; + high == 0 ? low.ceilDiv(denominator) : nativeCost.mulDiv(tokenPerNative, denominator, Math.Rounding.Ceil); } /// @dev Internal function that allows the withdrawer to extract ERC-20 tokens resulting from gas payments. From 0e75147928857a28b76fb4c268b715d1b29f66c8 Mon Sep 17 00:00:00 2001 From: Gonzalo Othacehe Date: Fri, 17 Jul 2026 13:24:08 -0300 Subject: [PATCH 3/6] gas: GAS-M-01 PaymasterSigner: one-window timestamp decode in _decodePaymasterUserOp (-199 gas/op, +0.9k deploy) Co-Authored-By: Claude Fable 5 --- .../account/paymaster/extensions/PaymasterSigner.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/account/paymaster/extensions/PaymasterSigner.sol b/contracts/account/paymaster/extensions/PaymasterSigner.sol index df9bc031d47..a5c7d539027 100644 --- a/contracts/account/paymaster/extensions/PaymasterSigner.sol +++ b/contracts/account/paymaster/extensions/PaymasterSigner.sol @@ -94,9 +94,11 @@ abstract contract PaymasterSigner is AbstractSigner, EIP712, Paymaster { PackedUserOperation calldata userOp ) internal pure virtual returns (uint48 validAfter, uint48 validUntil, bytes calldata signature) { bytes calldata paymasterData = userOp.paymasterData(); - return - paymasterData.length < 12 - ? (uint48(0), uint48(0), Calldata.emptyBytes()) - : (uint48(bytes6(paymasterData[0:6])), uint48(bytes6(paymasterData[6:12])), paymasterData[12:]); + if (paymasterData.length < 12) return (uint48(0), uint48(0), Calldata.emptyBytes()); + + // Both timestamps share the 12-byte window at [0:12]: a single load replaces + // the two bounds-checked slices. + bytes12 validity = bytes12(paymasterData[:12]); + return (uint48(bytes6(validity)), uint48(bytes6(validity << 48)), paymasterData[12:]); } } From fc1eff94dd26b4bc6f6f862161c96f2d4196ac63 Mon Sep 17 00:00:00 2001 From: Gonzalo Othacehe Date: Fri, 17 Jul 2026 11:40:39 -0300 Subject: [PATCH 4/6] gas: GAS-L-01 PaymasterERC20: unchecked refund subtraction under the documented EntryPoint invariant (-72 gas/postOp) Co-Authored-By: Claude Fable 5 --- contracts/account/paymaster/extensions/PaymasterERC20.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/account/paymaster/extensions/PaymasterERC20.sol b/contracts/account/paymaster/extensions/PaymasterERC20.sol index 3886a320a31..11c8318b374 100644 --- a/contracts/account/paymaster/extensions/PaymasterERC20.sol +++ b/contracts/account/paymaster/extensions/PaymasterERC20.sol @@ -205,8 +205,11 @@ abstract contract PaymasterERC20 is Paymaster { bytes calldata /* prefundContext */ ) internal virtual returns (bool success, uint256 actualAmount) { // Under ERC-4337 EntryPoint, `actualGasCost <= maxCost` and `actualUserOpFeePerGas <= maxFeePerGas`, - // so `actualAmount_ <= prefundAmount` holds. - return (token.trySafeTransfer(prefunder, prefundAmount - actualAmount_), actualAmount_); + // so `actualAmount_ <= prefundAmount` holds and the subtraction cannot underflow. If a caller breaks + // that requirement, the wrapped amount makes `trySafeTransfer` fail and {_postOp} revert. + unchecked { + return (token.trySafeTransfer(prefunder, prefundAmount - actualAmount_), actualAmount_); + } } /** From d7d3530f2cc66e8e15f52139a7680c388c3d9c46 Mon Sep 17 00:00:00 2001 From: Gonzalo Othacehe Date: Fri, 17 Jul 2026 11:41:55 -0300 Subject: [PATCH 5/6] gas: GAS-L-02 PaymasterERC721Owner: branchless validation data via SafeCast.toUint (-12 gas/op, -3.4k deploy) Co-Authored-By: Claude Fable 5 --- .../account/paymaster/extensions/PaymasterERC721Owner.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/account/paymaster/extensions/PaymasterERC721Owner.sol b/contracts/account/paymaster/extensions/PaymasterERC721Owner.sol index 55c009203c6..0e9a102a03f 100644 --- a/contracts/account/paymaster/extensions/PaymasterERC721Owner.sol +++ b/contracts/account/paymaster/extensions/PaymasterERC721Owner.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {IERC721} from "../../../interfaces/IERC721.sol"; import {ERC4337Utils, PackedUserOperation} from "../../utils/ERC4337Utils.sol"; +import {SafeCast} from "../../../utils/math/SafeCast.sol"; import {Paymaster} from "../Paymaster.sol"; /** @@ -44,10 +45,9 @@ abstract contract PaymasterERC721Owner is Paymaster { return ( bytes(""), // balanceOf reverts if the `userOp.sender` is the address(0), so this becomes unreachable with address(0) - // assuming a compliant entrypoint (`_validatePaymasterUserOp` is called after `validateUserOp`), - token().balanceOf(userOp.sender) == 0 - ? ERC4337Utils.SIG_VALIDATION_FAILED - : ERC4337Utils.SIG_VALIDATION_SUCCESS + // assuming a compliant entrypoint (`_validatePaymasterUserOp` is called after `validateUserOp`). + // `SIG_VALIDATION_FAILED == 1` and `SIG_VALIDATION_SUCCESS == 0`, so the cast is branchless. + SafeCast.toUint(token().balanceOf(userOp.sender) == 0) ); } } From f77682c3c638aa6b0cd96307e2fac258e1d70f87 Mon Sep 17 00:00:00 2001 From: Gonzalo Othacehe Date: Fri, 17 Jul 2026 13:34:15 -0300 Subject: [PATCH 6/6] docs: changeset for paymaster gas optimizations Co-Authored-By: Claude Fable 5 --- .changeset/paymaster-gas-council.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/paymaster-gas-council.md diff --git a/.changeset/paymaster-gas-council.md b/.changeset/paymaster-gas-council.md new file mode 100644 index 00000000000..74d48864c8f --- /dev/null +++ b/.changeset/paymaster-gas-council.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`PaymasterSigner`, `PaymasterERC20`, `PaymasterERC721Owner`: Reduce gas usage on the validation and postOp hot paths.