Skip to content

Commit b79f58e

Browse files
ernestognwAmxx
andauthored
Update ERC4337Utils with Entrypoint v09 changes (#6215)
Co-authored-by: Hadrien Croubois <[email protected]>
1 parent fccd38f commit b79f58e

File tree

8 files changed

+527
-68
lines changed

8 files changed

+527
-68
lines changed

.changeset/tame-monkeys-make.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC4337Utils`: Added the `paymasterSignature` function to extract the signature in `paymasterAndData` after Entrypoint v0.9. Similarly, a variant of `paymasterData` that receives a flag to exclude the signature from the returned data.

.changeset/whole-turkeys-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC4337Utils`: Added variants of `packValidationData(address,uint48,uint48)` and `packValidationData(bool,uint48,uint48)` that receive a `ValidationRange` argument, could be timestamp or block number. Similarly, the `parseValidationData` now returns a `ValidationRange` too.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- `ERC1967Proxy` and `TransparentUpgradeableProxy`: Mandate initialization during construction. Deployment now reverts with `ERC1967ProxyUninitialized` if an initialize call is not provided. Developers that rely on the previous behavior and want to disable this check can do so by overriding the internal `_unsafeAllowUninitialized` function to return true. ([#5906](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5906))
77
- `ERC721` and `ERC1155`: Prevent setting an operator for `address(0)`. In the case of `ERC721` this type of operator allowance could lead to obfuscated mint permission. ([#6171](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/6171))
88
- `RLP`: The `encode(bytes32)` function now encodes `bytes32` as a fixed size item and not as a scalar in `encode(uint256)`. Users must replace calls to `encode(bytes32)` with `encode(uint256(bytes32))` to preserve the same behavior. ([#6167](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/6167))
9+
- `ERC4337Utils`: The `parseValidationData` now returns a `ValidationRange` as the last return tuple value indicating whether the `validationData` is compared against a timestamp or block number. Developers must update their code to handle this new return value (e.g. `(aggregator, validAfter, validUntil) -> (aggregator, validAfter, validUntil, range)`).
910

1011
## 5.5.0 (2025-10-31)
1112

contracts/account/utils/draft-ERC4337Utils.sol

Lines changed: 132 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,37 @@ library ERC4337Utils {
3636
/// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) must return this value in case of signature failure, instead of revert.
3737
uint256 internal constant SIG_VALIDATION_FAILED = 1;
3838

39-
/// @dev Parses the validation data into its components. See {packValidationData}.
39+
/// @dev Magic value used in EntryPoint v0.9+ to detect the presence of a paymaster signature in `paymasterAndData`.
40+
bytes8 internal constant PAYMASTER_SIG_MAGIC = 0x22e325a297439656; // keccak256("PaymasterSignature")[:8]
41+
42+
/// @dev Highest bit set to 1 in a 6-bytes field.
43+
uint48 internal constant BLOCK_RANGE_FLAG = 0x800000000000;
44+
45+
/// @dev Mask for the lower 47 bits of a 6-bytes field (equivalent to uint48(~BLOCK_RANGE_FLAG)).
46+
uint48 internal constant BLOCK_RANGE_MASK = 0x7fffffffffff;
47+
48+
/// @dev Validity range of the validation data.
49+
enum ValidationRange {
50+
TIMESTAMP,
51+
BLOCK
52+
}
53+
54+
/**
55+
* @dev Parses the validation data into its components and the validity range. See {packValidationData}.
56+
* Strips away the highest bit flag from the `validAfter` and `validUntil` fields.
57+
*/
4058
function parseValidationData(
4159
uint256 validationData
42-
) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) {
60+
) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil, ValidationRange range) {
4361
validAfter = uint48(bytes32(validationData).extract_32_6(0));
4462
validUntil = uint48(bytes32(validationData).extract_32_6(6));
4563
aggregator = address(bytes32(validationData).extract_32_20(12));
46-
if (validUntil == 0) validUntil = type(uint48).max;
64+
range = ((validAfter & validUntil & BLOCK_RANGE_FLAG) == 0) ? ValidationRange.TIMESTAMP : ValidationRange.BLOCK;
65+
66+
validAfter &= BLOCK_RANGE_MASK;
67+
validUntil &= BLOCK_RANGE_MASK;
68+
69+
if (validUntil == 0) validUntil = BLOCK_RANGE_MASK;
4770
}
4871

4972
/// @dev Packs the validation data into a single uint256. See {parseValidationData}.
@@ -52,10 +75,36 @@ library ERC4337Utils {
5275
uint48 validAfter,
5376
uint48 validUntil
5477
) internal pure returns (uint256) {
78+
return
79+
packValidationData(
80+
aggregator,
81+
validAfter,
82+
validUntil,
83+
(validAfter & validUntil & BLOCK_RANGE_FLAG) == 0 ? ValidationRange.TIMESTAMP : ValidationRange.BLOCK
84+
);
85+
}
86+
87+
/**
88+
* @dev Variant of {packValidationData} that forces which validity range to use. This overwrites the presence of
89+
* flags in `validAfter` and `validUntil`).
90+
*/
91+
function packValidationData(
92+
address aggregator,
93+
uint48 validAfter,
94+
uint48 validUntil,
95+
ValidationRange range
96+
) internal pure returns (uint256) {
97+
if (range == ValidationRange.TIMESTAMP) {
98+
validAfter &= BLOCK_RANGE_MASK;
99+
validUntil &= BLOCK_RANGE_MASK;
100+
} else if (range == ValidationRange.BLOCK) {
101+
validAfter |= BLOCK_RANGE_FLAG;
102+
validUntil |= BLOCK_RANGE_FLAG;
103+
}
55104
return uint256(bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20(bytes20(aggregator)));
56105
}
57106

58-
/// @dev Same as {packValidationData}, but with a boolean signature success flag.
107+
/// @dev Variant of {packValidationData} that uses a boolean success flag instead of an aggregator address.
59108
function packValidationData(bool sigSuccess, uint48 validAfter, uint48 validUntil) internal pure returns (uint256) {
60109
return
61110
packValidationData(
@@ -65,27 +114,59 @@ library ERC4337Utils {
65114
);
66115
}
67116

117+
/**
118+
* @dev Variant of {packValidationData} that uses a boolean success flag instead of an aggregator address and that
119+
* forces which validity range to use. This overwrites the presence of flags in `validAfter` and `validUntil`).
120+
*/
121+
function packValidationData(
122+
bool sigSuccess,
123+
uint48 validAfter,
124+
uint48 validUntil,
125+
ValidationRange range
126+
) internal pure returns (uint256) {
127+
return
128+
packValidationData(
129+
address(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))),
130+
validAfter,
131+
validUntil,
132+
range
133+
);
134+
}
135+
68136
/**
69137
* @dev Combines two validation data into a single one.
70138
*
71139
* The `aggregator` is set to {SIG_VALIDATION_SUCCESS} if both are successful, while
72140
* the `validAfter` is the maximum and the `validUntil` is the minimum of both.
141+
*
142+
* NOTE: Returns `SIG_VALIDATION_FAILED` if the validation ranges differ.
73143
*/
74144
function combineValidationData(uint256 validationData1, uint256 validationData2) internal pure returns (uint256) {
75-
(address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1);
76-
(address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2);
145+
(address aggregator1, uint48 validAfter1, uint48 validUntil1, ValidationRange range1) = parseValidationData(
146+
validationData1
147+
);
148+
(address aggregator2, uint48 validAfter2, uint48 validUntil2, ValidationRange range2) = parseValidationData(
149+
validationData2
150+
);
77151

78-
bool success = aggregator1 == address(uint160(SIG_VALIDATION_SUCCESS)) &&
79-
aggregator2 == address(uint160(SIG_VALIDATION_SUCCESS));
80-
uint48 validAfter = uint48(Math.max(validAfter1, validAfter2));
81-
uint48 validUntil = uint48(Math.min(validUntil1, validUntil2));
82-
return packValidationData(success, validAfter, validUntil);
152+
if (range1 == range2) {
153+
bool success = aggregator1 == address(uint160(SIG_VALIDATION_SUCCESS)) &&
154+
aggregator2 == address(uint160(SIG_VALIDATION_SUCCESS));
155+
uint48 validAfter = uint48(Math.max(validAfter1, validAfter2));
156+
uint48 validUntil = uint48(Math.min(validUntil1, validUntil2));
157+
return packValidationData(success, validAfter, validUntil, range1);
158+
} else {
159+
return SIG_VALIDATION_FAILED;
160+
}
83161
}
84162

85163
/// @dev Returns the aggregator of the `validationData` and whether it is out of time range.
86164
function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) {
87-
(address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData);
88-
return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp);
165+
(address aggregator_, uint48 validAfter, uint48 validUntil, ValidationRange range) = parseValidationData(
166+
validationData
167+
);
168+
uint256 current = Math.ternary(range == ValidationRange.TIMESTAMP, block.timestamp, block.number);
169+
return (aggregator_, current <= validAfter || validUntil < current);
89170
}
90171

91172
/// @dev Get the hash of a user operation for a given entrypoint
@@ -155,8 +236,44 @@ library ERC4337Utils {
155236
return self.paymasterAndData.length < 52 ? 0 : uint128(bytes16(self.paymasterAndData[36:52]));
156237
}
157238

158-
/// @dev Returns the fourth section of `paymasterAndData` from the {PackedUserOperation}.
239+
/**
240+
* @dev Returns the fourth section of `paymasterAndData` from the {PackedUserOperation}.
241+
* If a paymaster signature is present, it is excluded from the returned data.
242+
*/
159243
function paymasterData(PackedUserOperation calldata self) internal pure returns (bytes calldata) {
160-
return self.paymasterAndData.length < 52 ? Calldata.emptyBytes() : self.paymasterAndData[52:];
244+
bool hasSignature = self.paymasterAndData.length > 9 &&
245+
bytes8(self.paymasterAndData[self.paymasterAndData.length - 8:]) == PAYMASTER_SIG_MAGIC;
246+
uint256 suffixLength = hasSignature ? _paymasterSignatureSize(self) + 10 : 0;
247+
return
248+
self.paymasterAndData.length < 52 + suffixLength
249+
? Calldata.emptyBytes()
250+
: self.paymasterAndData[52:self.paymasterAndData.length - suffixLength];
251+
}
252+
253+
/**
254+
* @dev Returns the paymaster signature from `paymasterAndData` (EntryPoint v0.9+).
255+
* Returns empty bytes if no paymaster signature is present.
256+
*/
257+
function paymasterSignature(PackedUserOperation calldata self) internal pure returns (bytes calldata) {
258+
if (
259+
self.paymasterAndData.length < 10 ||
260+
bytes8(self.paymasterAndData[self.paymasterAndData.length - 8:]) != PAYMASTER_SIG_MAGIC
261+
) return Calldata.emptyBytes();
262+
263+
uint256 sigSize = _paymasterSignatureSize(self);
264+
uint256 sigEnd = self.paymasterAndData.length - 10;
265+
return
266+
self.paymasterAndData.length < 62 + sigSize
267+
? Calldata.emptyBytes()
268+
: self.paymasterAndData[sigEnd - sigSize:sigEnd];
269+
}
270+
271+
/**
272+
* @dev Returns the size of the paymaster signature in `paymasterAndData` (EntryPoint v0.9+).
273+
* Does not check minimum length of `paymasterAndData`.
274+
*/
275+
function _paymasterSignatureSize(PackedUserOperation calldata self) private pure returns (uint256) {
276+
return
277+
uint16(bytes2(self.paymasterAndData[self.paymasterAndData.length - 10:self.paymasterAndData.length - 8]));
161278
}
162279
}

contracts/interfaces/draft-IERC4337.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pragma solidity >=0.8.4;
3030
* - `preVerificationGas` (`uint256`)
3131
* - `gasFees` (`bytes32`): concatenation of maxPriorityFeePerGas (16 bytes) and maxFeePerGas (16 bytes)
3232
* - `paymasterAndData` (`bytes`): concatenation of paymaster fields (or empty)
33+
* For EntryPoint v0.9+, may optionally include `paymasterSignature` at the end:
34+
* `paymaster || paymasterVerificationGasLimit || paymasterPostOpGasLimit || paymasterData || paymasterSignature || paymasterSignatureSize || PAYMASTER_SIG_MAGIC`
3335
* - `signature` (`bytes`)
3436
*/
3537
struct PackedUserOperation {
@@ -40,7 +42,7 @@ struct PackedUserOperation {
4042
bytes32 accountGasLimits; // `abi.encodePacked(verificationGasLimit, callGasLimit)` 16 bytes each
4143
uint256 preVerificationGas;
4244
bytes32 gasFees; // `abi.encodePacked(maxPriorityFeePerGas, maxFeePerGas)` 16 bytes each
43-
bytes paymasterAndData; // `abi.encodePacked(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData)` (20 bytes, 16 bytes, 16 bytes, dynamic)
45+
bytes paymasterAndData; // `abi.encodePacked(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData[, paymasterSignature, paymasterSignatureSize, PAYMASTER_SIG_MAGIC])` (20 bytes, 16 bytes, 16 bytes, dynamic[, dynamic, 2 bytes, 8 bytes])
4446
bytes signature;
4547
}
4648

0 commit comments

Comments
 (0)