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. diff --git a/contracts/account/paymaster/extensions/PaymasterERC20.sol b/contracts/account/paymaster/extensions/PaymasterERC20.sol index 1e098a6b881..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_); + } } /** @@ -289,9 +292,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. 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) ); } } diff --git a/contracts/account/paymaster/extensions/PaymasterSigner.sol b/contracts/account/paymaster/extensions/PaymasterSigner.sol index 93cd0ff6479..a5c7d539027 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 ) @@ -89,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:]); } }