Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/paymaster-gas-council.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---

`PaymasterSigner`, `PaymasterERC20`, `PaymasterERC721Owner`: Reduce gas usage on the validation and postOp hot paths.
14 changes: 10 additions & 4 deletions contracts/account/paymaster/extensions/PaymasterERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
}
}

/**
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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)
);
}
}
19 changes: 13 additions & 6 deletions contracts/account/paymaster/extensions/PaymasterSigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
)
Expand Down Expand Up @@ -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:]);
}
}
Loading