Skip to content

Commit 28aed34

Browse files
AmxxernestognwnnsW3cairoetharr00
authoredOct 23, 2024··
Merge account abstraction work into master (#5274)
Co-authored-by: Ernesto García <[email protected]> Co-authored-by: Elias Rad <[email protected]> Co-authored-by: cairo <[email protected]> Co-authored-by: Arr00 <[email protected]>
1 parent 2fa4d10 commit 28aed34

21 files changed

+2494
-95
lines changed
 

‎.changeset/hot-shrimps-wait.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Packing`: Add variants for packing `bytes10` and `bytes22`

‎.changeset/small-seahorses-bathe.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC7579Utils`: Add a reusable library to interact with ERC-7579 modular accounts

‎.changeset/weak-roses-bathe.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC4337Utils`: Add a reusable library to manipulate user operations and interact with ERC-4337 contracts

‎.codecov.yml

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ coverage:
1313
ignore:
1414
- "test"
1515
- "contracts/mocks"
16+
- "contracts/vendor"

‎.github/workflows/checks.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,4 @@ jobs:
133133
with:
134134
check_hidden: true
135135
check_filenames: true
136-
skip: package-lock.json,*.pdf
136+
skip: package-lock.json,*.pdf,vendor

‎contracts/account/README.adoc

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
= Account
2+
3+
[.readme-notice]
4+
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account
5+
6+
This directory includes contracts to build accounts for ERC-4337.
7+
8+
== Utilities
9+
10+
{{ERC4337Utils}}
11+
12+
{{ERC7579Utils}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {IEntryPoint, PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
6+
import {Math} from "../../utils/math/Math.sol";
7+
import {Packing} from "../../utils/Packing.sol";
8+
9+
/**
10+
* @dev Library with common ERC-4337 utility functions.
11+
*
12+
* See https://eips.ethereum.org/EIPS/eip-4337[ERC-4337].
13+
*/
14+
library ERC4337Utils {
15+
using Packing for *;
16+
17+
/// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success.
18+
uint256 internal constant SIG_VALIDATION_SUCCESS = 0;
19+
20+
/// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) must return this value in case of signature failure, instead of revert.
21+
uint256 internal constant SIG_VALIDATION_FAILED = 1;
22+
23+
/// @dev Parses the validation data into its components. See {packValidationData}.
24+
function parseValidationData(
25+
uint256 validationData
26+
) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) {
27+
validAfter = uint48(bytes32(validationData).extract_32_6(0x00));
28+
validUntil = uint48(bytes32(validationData).extract_32_6(0x06));
29+
aggregator = address(bytes32(validationData).extract_32_20(0x0c));
30+
if (validUntil == 0) validUntil = type(uint48).max;
31+
}
32+
33+
/// @dev Packs the validation data into a single uint256. See {parseValidationData}.
34+
function packValidationData(
35+
address aggregator,
36+
uint48 validAfter,
37+
uint48 validUntil
38+
) internal pure returns (uint256) {
39+
return uint256(bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20(bytes20(aggregator)));
40+
}
41+
42+
/// @dev Same as {packValidationData}, but with a boolean signature success flag.
43+
function packValidationData(bool sigSuccess, uint48 validAfter, uint48 validUntil) internal pure returns (uint256) {
44+
return
45+
packValidationData(
46+
address(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))),
47+
validAfter,
48+
validUntil
49+
);
50+
}
51+
52+
/**
53+
* @dev Combines two validation data into a single one.
54+
*
55+
* The `aggregator` is set to {SIG_VALIDATION_SUCCESS} if both are successful, while
56+
* the `validAfter` is the maximum and the `validUntil` is the minimum of both.
57+
*/
58+
function combineValidationData(uint256 validationData1, uint256 validationData2) internal pure returns (uint256) {
59+
(address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1);
60+
(address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2);
61+
62+
bool success = aggregator1 == address(0) && aggregator2 == address(0);
63+
uint48 validAfter = uint48(Math.max(validAfter1, validAfter2));
64+
uint48 validUntil = uint48(Math.min(validUntil1, validUntil2));
65+
return packValidationData(success, validAfter, validUntil);
66+
}
67+
68+
/// @dev Returns the aggregator of the `validationData` and whether it is out of time range.
69+
function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) {
70+
(address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData);
71+
return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp);
72+
}
73+
74+
/// @dev Computes the hash of a user operation with the current entrypoint and chainid.
75+
function hash(PackedUserOperation calldata self) internal view returns (bytes32) {
76+
return hash(self, address(this), block.chainid);
77+
}
78+
79+
/// @dev Sames as {hash}, but with a custom entrypoint and chainid.
80+
function hash(
81+
PackedUserOperation calldata self,
82+
address entrypoint,
83+
uint256 chainid
84+
) internal pure returns (bytes32) {
85+
bytes32 result = keccak256(
86+
abi.encode(
87+
keccak256(
88+
abi.encode(
89+
self.sender,
90+
self.nonce,
91+
keccak256(self.initCode),
92+
keccak256(self.callData),
93+
self.accountGasLimits,
94+
self.preVerificationGas,
95+
self.gasFees,
96+
keccak256(self.paymasterAndData)
97+
)
98+
),
99+
entrypoint,
100+
chainid
101+
)
102+
);
103+
return result;
104+
}
105+
106+
/// @dev Returns `verificationGasLimit` from the {PackedUserOperation}.
107+
function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
108+
return uint128(self.accountGasLimits.extract_32_16(0x00));
109+
}
110+
111+
/// @dev Returns `accountGasLimits` from the {PackedUserOperation}.
112+
function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
113+
return uint128(self.accountGasLimits.extract_32_16(0x10));
114+
}
115+
116+
/// @dev Returns the first section of `gasFees` from the {PackedUserOperation}.
117+
function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
118+
return uint128(self.gasFees.extract_32_16(0x00));
119+
}
120+
121+
/// @dev Returns the second section of `gasFees` from the {PackedUserOperation}.
122+
function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
123+
return uint128(self.gasFees.extract_32_16(0x10));
124+
}
125+
126+
/// @dev Returns the total gas price for the {PackedUserOperation} (ie. `maxFeePerGas` or `maxPriorityFeePerGas + basefee`).
127+
function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) {
128+
unchecked {
129+
// Following values are "per gas"
130+
uint256 maxPriorityFee = maxPriorityFeePerGas(self);
131+
uint256 maxFee = maxFeePerGas(self);
132+
return Math.ternary(maxFee == maxPriorityFee, maxFee, Math.min(maxFee, maxPriorityFee + block.basefee));
133+
}
134+
}
135+
136+
/// @dev Returns the first section of `paymasterAndData` from the {PackedUserOperation}.
137+
function paymaster(PackedUserOperation calldata self) internal pure returns (address) {
138+
return address(bytes20(self.paymasterAndData[0:20]));
139+
}
140+
141+
/// @dev Returns the second section of `paymasterAndData` from the {PackedUserOperation}.
142+
function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
143+
return uint128(bytes16(self.paymasterAndData[20:36]));
144+
}
145+
146+
/// @dev Returns the third section of `paymasterAndData` from the {PackedUserOperation}.
147+
function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
148+
return uint128(bytes16(self.paymasterAndData[36:52]));
149+
}
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Execution} from "../../interfaces/draft-IERC7579.sol";
6+
import {Packing} from "../../utils/Packing.sol";
7+
import {Address} from "../../utils/Address.sol";
8+
9+
type Mode is bytes32;
10+
type CallType is bytes1;
11+
type ExecType is bytes1;
12+
type ModeSelector is bytes4;
13+
type ModePayload is bytes22;
14+
15+
/**
16+
* @dev Library with common ERC-7579 utility functions.
17+
*
18+
* See https://eips.ethereum.org/EIPS/eip-7579[ERC-7579].
19+
*/
20+
// slither-disable-next-line unused-state
21+
library ERC7579Utils {
22+
using Packing for *;
23+
24+
/// @dev A single `call` execution.
25+
CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00);
26+
27+
/// @dev A batch of `call` executions.
28+
CallType constant CALLTYPE_BATCH = CallType.wrap(0x01);
29+
30+
/// @dev A `delegatecall` execution.
31+
CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF);
32+
33+
/// @dev Default execution type that reverts on failure.
34+
ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00);
35+
36+
/// @dev Execution type that does not revert on failure.
37+
ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01);
38+
39+
/// @dev Emits when an {EXECTYPE_TRY} execution fails.
40+
event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes result);
41+
42+
/// @dev The provided {CallType} is not supported.
43+
error ERC7579UnsupportedCallType(CallType callType);
44+
45+
/// @dev The provided {ExecType} is not supported.
46+
error ERC7579UnsupportedExecType(ExecType execType);
47+
48+
/// @dev The provided module doesn't match the provided module type.
49+
error ERC7579MismatchedModuleTypeId(uint256 moduleTypeId, address module);
50+
51+
/// @dev The module is not installed.
52+
error ERC7579UninstalledModule(uint256 moduleTypeId, address module);
53+
54+
/// @dev The module is already installed.
55+
error ERC7579AlreadyInstalledModule(uint256 moduleTypeId, address module);
56+
57+
/// @dev The module type is not supported.
58+
error ERC7579UnsupportedModuleType(uint256 moduleTypeId);
59+
60+
/// @dev Executes a single call.
61+
function execSingle(
62+
ExecType execType,
63+
bytes calldata executionCalldata
64+
) internal returns (bytes[] memory returnData) {
65+
(address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata);
66+
returnData = new bytes[](1);
67+
returnData[0] = _call(0, execType, target, value, callData);
68+
}
69+
70+
/// @dev Executes a batch of calls.
71+
function execBatch(
72+
ExecType execType,
73+
bytes calldata executionCalldata
74+
) internal returns (bytes[] memory returnData) {
75+
Execution[] calldata executionBatch = decodeBatch(executionCalldata);
76+
returnData = new bytes[](executionBatch.length);
77+
for (uint256 i = 0; i < executionBatch.length; ++i) {
78+
returnData[i] = _call(
79+
i,
80+
execType,
81+
executionBatch[i].target,
82+
executionBatch[i].value,
83+
executionBatch[i].callData
84+
);
85+
}
86+
}
87+
88+
/// @dev Executes a delegate call.
89+
function execDelegateCall(
90+
ExecType execType,
91+
bytes calldata executionCalldata
92+
) internal returns (bytes[] memory returnData) {
93+
(address target, bytes calldata callData) = decodeDelegate(executionCalldata);
94+
returnData = new bytes[](1);
95+
returnData[0] = _delegatecall(0, execType, target, callData);
96+
}
97+
98+
/// @dev Encodes the mode with the provided parameters. See {decodeMode}.
99+
function encodeMode(
100+
CallType callType,
101+
ExecType execType,
102+
ModeSelector selector,
103+
ModePayload payload
104+
) internal pure returns (Mode mode) {
105+
return
106+
Mode.wrap(
107+
CallType
108+
.unwrap(callType)
109+
.pack_1_1(ExecType.unwrap(execType))
110+
.pack_2_4(bytes4(0))
111+
.pack_6_4(ModeSelector.unwrap(selector))
112+
.pack_10_22(ModePayload.unwrap(payload))
113+
);
114+
}
115+
116+
/// @dev Decodes the mode into its parameters. See {encodeMode}.
117+
function decodeMode(
118+
Mode mode
119+
) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) {
120+
return (
121+
CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)),
122+
ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)),
123+
ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)),
124+
ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10))
125+
);
126+
}
127+
128+
/// @dev Encodes a single call execution. See {decodeSingle}.
129+
function encodeSingle(
130+
address target,
131+
uint256 value,
132+
bytes calldata callData
133+
) internal pure returns (bytes memory executionCalldata) {
134+
return abi.encodePacked(target, value, callData);
135+
}
136+
137+
/// @dev Decodes a single call execution. See {encodeSingle}.
138+
function decodeSingle(
139+
bytes calldata executionCalldata
140+
) internal pure returns (address target, uint256 value, bytes calldata callData) {
141+
target = address(bytes20(executionCalldata[0:20]));
142+
value = uint256(bytes32(executionCalldata[20:52]));
143+
callData = executionCalldata[52:];
144+
}
145+
146+
/// @dev Encodes a delegate call execution. See {decodeDelegate}.
147+
function encodeDelegate(
148+
address target,
149+
bytes calldata callData
150+
) internal pure returns (bytes memory executionCalldata) {
151+
return abi.encodePacked(target, callData);
152+
}
153+
154+
/// @dev Decodes a delegate call execution. See {encodeDelegate}.
155+
function decodeDelegate(
156+
bytes calldata executionCalldata
157+
) internal pure returns (address target, bytes calldata callData) {
158+
target = address(bytes20(executionCalldata[0:20]));
159+
callData = executionCalldata[20:];
160+
}
161+
162+
/// @dev Encodes a batch of executions. See {decodeBatch}.
163+
function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) {
164+
return abi.encode(executionBatch);
165+
}
166+
167+
/// @dev Decodes a batch of executions. See {encodeBatch}.
168+
function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) {
169+
assembly ("memory-safe") {
170+
let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset))
171+
// Extract the ERC7579 Executions
172+
executionBatch.offset := add(ptr, 32)
173+
executionBatch.length := calldataload(ptr)
174+
}
175+
}
176+
177+
/// @dev Executes a `call` to the target with the provided {ExecType}.
178+
function _call(
179+
uint256 index,
180+
ExecType execType,
181+
address target,
182+
uint256 value,
183+
bytes calldata data
184+
) private returns (bytes memory) {
185+
(bool success, bytes memory returndata) = target.call{value: value}(data);
186+
return _validateExecutionMode(index, execType, success, returndata);
187+
}
188+
189+
/// @dev Executes a `delegatecall` to the target with the provided {ExecType}.
190+
function _delegatecall(
191+
uint256 index,
192+
ExecType execType,
193+
address target,
194+
bytes calldata data
195+
) private returns (bytes memory) {
196+
(bool success, bytes memory returndata) = target.delegatecall(data);
197+
return _validateExecutionMode(index, execType, success, returndata);
198+
}
199+
200+
/// @dev Validates the execution mode and returns the returndata.
201+
function _validateExecutionMode(
202+
uint256 index,
203+
ExecType execType,
204+
bool success,
205+
bytes memory returndata
206+
) private returns (bytes memory) {
207+
if (execType == ERC7579Utils.EXECTYPE_DEFAULT) {
208+
Address.verifyCallResult(success, returndata);
209+
} else if (execType == ERC7579Utils.EXECTYPE_TRY) {
210+
if (!success) emit ERC7579TryExecuteFail(index, returndata);
211+
} else {
212+
revert ERC7579UnsupportedExecType(execType);
213+
}
214+
return returndata;
215+
}
216+
}
217+
218+
// Operators
219+
using {eqCallType as ==} for CallType global;
220+
using {eqExecType as ==} for ExecType global;
221+
using {eqModeSelector as ==} for ModeSelector global;
222+
using {eqModePayload as ==} for ModePayload global;
223+
224+
/// @dev Compares two `CallType` values for equality.
225+
function eqCallType(CallType a, CallType b) pure returns (bool) {
226+
return CallType.unwrap(a) == CallType.unwrap(b);
227+
}
228+
229+
/// @dev Compares two `ExecType` values for equality.
230+
function eqExecType(ExecType a, ExecType b) pure returns (bool) {
231+
return ExecType.unwrap(a) == ExecType.unwrap(b);
232+
}
233+
234+
/// @dev Compares two `ModeSelector` values for equality.
235+
function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) {
236+
return ModeSelector.unwrap(a) == ModeSelector.unwrap(b);
237+
}
238+
239+
/// @dev Compares two `ModePayload` values for equality.
240+
function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) {
241+
return ModePayload.unwrap(a) == ModePayload.unwrap(b);
242+
}
+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
/**
6+
* @dev A https://github.com/ethereum/ercs/blob/master/ERCS/erc-4337.md#useroperation[user operation] is composed of the following elements:
7+
* - `sender` (`address`): The account making the operation
8+
* - `nonce` (`uint256`): Anti-replay parameter (see “Semi-abstracted Nonce Support” )
9+
* - `factory` (`address`): account factory, only for new accounts
10+
* - `factoryData` (`bytes`): data for account factory (only if account factory exists)
11+
* - `callData` (`bytes`): The data to pass to the sender during the main execution call
12+
* - `callGasLimit` (`uint256`): The amount of gas to allocate the main execution call
13+
* - `verificationGasLimit` (`uint256`): The amount of gas to allocate for the verification step
14+
* - `preVerificationGas` (`uint256`): Extra gas to pay the bunder
15+
* - `maxFeePerGas` (`uint256`): Maximum fee per gas (similar to EIP-1559 max_fee_per_gas)
16+
* - `maxPriorityFeePerGas` (`uint256`): Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas)
17+
* - `paymaster` (`address`): Address of paymaster contract, (or empty, if account pays for itself)
18+
* - `paymasterVerificationGasLimit` (`uint256`): The amount of gas to allocate for the paymaster validation code
19+
* - `paymasterPostOpGasLimit` (`uint256`): The amount of gas to allocate for the paymaster post-operation code
20+
* - `paymasterData` (`bytes`): Data for paymaster (only if paymaster exists)
21+
* - `signature` (`bytes`): Data passed into the account to verify authorization
22+
*
23+
* When passed to on-chain contacts, the following packed version is used.
24+
* - `sender` (`address`)
25+
* - `nonce` (`uint256`)
26+
* - `initCode` (`bytes`): concatenation of factory address and factoryData (or empty)
27+
* - `callData` (`bytes`)
28+
* - `accountGasLimits` (`bytes32`): concatenation of verificationGas (16 bytes) and callGas (16 bytes)
29+
* - `preVerificationGas` (`uint256`)
30+
* - `gasFees` (`bytes32`): concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes)
31+
* - `paymasterAndData` (`bytes`): concatenation of paymaster fields (or empty)
32+
* - `signature` (`bytes`)
33+
*/
34+
struct PackedUserOperation {
35+
address sender;
36+
uint256 nonce;
37+
bytes initCode; // `abi.encodePacked(factory, factoryData)`
38+
bytes callData;
39+
bytes32 accountGasLimits; // `abi.encodePacked(verificationGasLimit, callGasLimit)` 16 bytes each
40+
uint256 preVerificationGas;
41+
bytes32 gasFees; // `abi.encodePacked(maxPriorityFee, maxFeePerGas)` 16 bytes each
42+
bytes paymasterAndData; // `abi.encodePacked(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData)`
43+
bytes signature;
44+
}
45+
46+
/**
47+
* @dev Aggregates and validates multiple signatures for a batch of user operations.
48+
*/
49+
interface IAggregator {
50+
/**
51+
* @dev Validates the signature for a user operation.
52+
*/
53+
function validateUserOpSignature(
54+
PackedUserOperation calldata userOp
55+
) external view returns (bytes memory sigForUserOp);
56+
57+
/**
58+
* @dev Returns an aggregated signature for a batch of user operation's signatures.
59+
*/
60+
function aggregateSignatures(
61+
PackedUserOperation[] calldata userOps
62+
) external view returns (bytes memory aggregatesSignature);
63+
64+
/**
65+
* @dev Validates that the aggregated signature is valid for the user operations.
66+
*
67+
* Requirements:
68+
*
69+
* - The aggregated signature MUST match the given list of operations.
70+
*/
71+
function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view;
72+
}
73+
74+
/**
75+
* @dev Handle nonce management for accounts.
76+
*/
77+
interface IEntryPointNonces {
78+
/**
79+
* @dev Returns the nonce for a `sender` account and a `key`.
80+
*
81+
* Nonces for a certain `key` are always increasing.
82+
*/
83+
function getNonce(address sender, uint192 key) external view returns (uint256 nonce);
84+
}
85+
86+
/**
87+
* @dev Handle stake management for accounts.
88+
*/
89+
interface IEntryPointStake {
90+
/**
91+
* @dev Returns the balance of the account.
92+
*/
93+
function balanceOf(address account) external view returns (uint256);
94+
95+
/**
96+
* @dev Deposits `msg.value` to the account.
97+
*/
98+
function depositTo(address account) external payable;
99+
100+
/**
101+
* @dev Withdraws `withdrawAmount` from the account to `withdrawAddress`.
102+
*/
103+
function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external;
104+
105+
/**
106+
* @dev Adds stake to the account with an unstake delay of `unstakeDelaySec`.
107+
*/
108+
function addStake(uint32 unstakeDelaySec) external payable;
109+
110+
/**
111+
* @dev Unlocks the stake of the account.
112+
*/
113+
function unlockStake() external;
114+
115+
/**
116+
* @dev Withdraws the stake of the account to `withdrawAddress`.
117+
*/
118+
function withdrawStake(address payable withdrawAddress) external;
119+
}
120+
121+
/**
122+
* @dev Entry point for user operations.
123+
*/
124+
interface IEntryPoint is IEntryPointNonces, IEntryPointStake {
125+
/**
126+
* @dev A user operation at `opIndex` failed with `reason`.
127+
*/
128+
error FailedOp(uint256 opIndex, string reason);
129+
130+
/**
131+
* @dev A user operation at `opIndex` failed with `reason` and `inner` returned data.
132+
*/
133+
error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner);
134+
135+
/**
136+
* @dev Batch of aggregated user operations per aggregator.
137+
*/
138+
struct UserOpsPerAggregator {
139+
PackedUserOperation[] userOps;
140+
IAggregator aggregator;
141+
bytes signature;
142+
}
143+
144+
/**
145+
* @dev Executes a batch of user operations.
146+
*/
147+
function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external;
148+
149+
/**
150+
* @dev Executes a batch of aggregated user operations per aggregator.
151+
*/
152+
function handleAggregatedOps(
153+
UserOpsPerAggregator[] calldata opsPerAggregator,
154+
address payable beneficiary
155+
) external;
156+
}
157+
158+
/**
159+
* @dev Base interface for an account.
160+
*/
161+
interface IAccount {
162+
/**
163+
* @dev Validates a user operation.
164+
*/
165+
function validateUserOp(
166+
PackedUserOperation calldata userOp,
167+
bytes32 userOpHash,
168+
uint256 missingAccountFunds
169+
) external returns (uint256 validationData);
170+
}
171+
172+
/**
173+
* @dev Support for executing user operations by prepending the {executeUserOp} function selector
174+
* to the UserOperation's `callData`.
175+
*/
176+
interface IAccountExecute {
177+
/**
178+
* @dev Executes a user operation.
179+
*/
180+
function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external;
181+
}
182+
183+
/**
184+
* @dev Interface for a paymaster contract that agrees to pay for the gas costs of a user operation.
185+
*
186+
* NOTE: A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction.
187+
*/
188+
interface IPaymaster {
189+
enum PostOpMode {
190+
opSucceeded,
191+
opReverted,
192+
postOpReverted
193+
}
194+
195+
/**
196+
* @dev Validates whether the paymaster is willing to pay for the user operation.
197+
*
198+
* NOTE: Bundlers will reject this method if it modifies the state, unless it's whitelisted.
199+
*/
200+
function validatePaymasterUserOp(
201+
PackedUserOperation calldata userOp,
202+
bytes32 userOpHash,
203+
uint256 maxCost
204+
) external returns (bytes memory context, uint256 validationData);
205+
206+
/**
207+
* @dev Verifies the sender is the entrypoint.
208+
*/
209+
function postOp(
210+
PostOpMode mode,
211+
bytes calldata context,
212+
uint256 actualGasCost,
213+
uint256 actualUserOpFeePerGas
214+
) external;
215+
}
+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {PackedUserOperation} from "./draft-IERC4337.sol";
5+
6+
uint256 constant VALIDATION_SUCCESS = 0;
7+
uint256 constant VALIDATION_FAILED = 1;
8+
uint256 constant MODULE_TYPE_VALIDATOR = 1;
9+
uint256 constant MODULE_TYPE_EXECUTOR = 2;
10+
uint256 constant MODULE_TYPE_FALLBACK = 3;
11+
uint256 constant MODULE_TYPE_HOOK = 4;
12+
13+
interface IERC7579Module {
14+
/**
15+
* @dev This function is called by the smart account during installation of the module
16+
* @param data arbitrary data that may be required on the module during `onInstall` initialization
17+
*
18+
* MUST revert on error (e.g. if module is already enabled)
19+
*/
20+
function onInstall(bytes calldata data) external;
21+
22+
/**
23+
* @dev This function is called by the smart account during uninstallation of the module
24+
* @param data arbitrary data that may be required on the module during `onUninstall` de-initialization
25+
*
26+
* MUST revert on error
27+
*/
28+
function onUninstall(bytes calldata data) external;
29+
30+
/**
31+
* @dev Returns boolean value if module is a certain type
32+
* @param moduleTypeId the module type ID according the ERC-7579 spec
33+
*
34+
* MUST return true if the module is of the given type and false otherwise
35+
*/
36+
function isModuleType(uint256 moduleTypeId) external view returns (bool);
37+
}
38+
39+
interface IERC7579Validator is IERC7579Module {
40+
/**
41+
* @dev Validates a UserOperation
42+
* @param userOp the ERC-4337 PackedUserOperation
43+
* @param userOpHash the hash of the ERC-4337 PackedUserOperation
44+
*
45+
* MUST validate that the signature is a valid signature of the userOpHash
46+
* SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch
47+
*/
48+
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256);
49+
50+
/**
51+
* @dev Validates a signature using ERC-1271
52+
* @param sender the address that sent the ERC-1271 request to the smart account
53+
* @param hash the hash of the ERC-1271 request
54+
* @param signature the signature of the ERC-1271 request
55+
*
56+
* MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid
57+
* MUST NOT modify state
58+
*/
59+
function isValidSignatureWithSender(
60+
address sender,
61+
bytes32 hash,
62+
bytes calldata signature
63+
) external view returns (bytes4);
64+
}
65+
66+
interface IERC7579Hook is IERC7579Module {
67+
/**
68+
* @dev Called by the smart account before execution
69+
* @param msgSender the address that called the smart account
70+
* @param value the value that was sent to the smart account
71+
* @param msgData the data that was sent to the smart account
72+
*
73+
* MAY return arbitrary data in the `hookData` return value
74+
*/
75+
function preCheck(
76+
address msgSender,
77+
uint256 value,
78+
bytes calldata msgData
79+
) external returns (bytes memory hookData);
80+
81+
/**
82+
* @dev Called by the smart account after execution
83+
* @param hookData the data that was returned by the `preCheck` function
84+
*
85+
* MAY validate the `hookData` to validate transaction context of the `preCheck` function
86+
*/
87+
function postCheck(bytes calldata hookData) external;
88+
}
89+
90+
struct Execution {
91+
address target;
92+
uint256 value;
93+
bytes callData;
94+
}
95+
96+
interface IERC7579Execution {
97+
/**
98+
* @dev Executes a transaction on behalf of the account.
99+
* @param mode The encoded execution mode of the transaction. See ModeLib.sol for details
100+
* @param executionCalldata The encoded execution call data
101+
*
102+
* MUST ensure adequate authorization control: e.g. onlyEntryPointOrSelf if used with ERC-4337
103+
* If a mode is requested that is not supported by the Account, it MUST revert
104+
*/
105+
function execute(bytes32 mode, bytes calldata executionCalldata) external;
106+
107+
/**
108+
* @dev Executes a transaction on behalf of the account.
109+
* This function is intended to be called by Executor Modules
110+
* @param mode The encoded execution mode of the transaction. See ModeLib.sol for details
111+
* @param executionCalldata The encoded execution call data
112+
*
113+
* MUST ensure adequate authorization control: i.e. onlyExecutorModule
114+
* If a mode is requested that is not supported by the Account, it MUST revert
115+
*/
116+
function executeFromExecutor(
117+
bytes32 mode,
118+
bytes calldata executionCalldata
119+
) external returns (bytes[] memory returnData);
120+
}
121+
122+
interface IERC7579AccountConfig {
123+
/**
124+
* @dev Returns the account id of the smart account
125+
* @return accountImplementationId the account id of the smart account
126+
*
127+
* MUST return a non-empty string
128+
* The accountId SHOULD be structured like so:
129+
* "vendorname.accountname.semver"
130+
* The id SHOULD be unique across all smart accounts
131+
*/
132+
function accountId() external view returns (string memory accountImplementationId);
133+
134+
/**
135+
* @dev Function to check if the account supports a certain execution mode (see above)
136+
* @param encodedMode the encoded mode
137+
*
138+
* MUST return true if the account supports the mode and false otherwise
139+
*/
140+
function supportsExecutionMode(bytes32 encodedMode) external view returns (bool);
141+
142+
/**
143+
* @dev Function to check if the account supports a certain module typeId
144+
* @param moduleTypeId the module type ID according to the ERC-7579 spec
145+
*
146+
* MUST return true if the account supports the module type and false otherwise
147+
*/
148+
function supportsModule(uint256 moduleTypeId) external view returns (bool);
149+
}
150+
151+
interface IERC7579ModuleConfig {
152+
event ModuleInstalled(uint256 moduleTypeId, address module);
153+
event ModuleUninstalled(uint256 moduleTypeId, address module);
154+
155+
/**
156+
* @dev Installs a Module of a certain type on the smart account
157+
* @param moduleTypeId the module type ID according to the ERC-7579 spec
158+
* @param module the module address
159+
* @param initData arbitrary data that may be required on the module during `onInstall`
160+
* initialization.
161+
*
162+
* MUST implement authorization control
163+
* MUST call `onInstall` on the module with the `initData` parameter if provided
164+
* MUST emit ModuleInstalled event
165+
* MUST revert if the module is already installed or the initialization on the module failed
166+
*/
167+
function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external;
168+
169+
/**
170+
* @dev Uninstalls a Module of a certain type on the smart account
171+
* @param moduleTypeId the module type ID according the ERC-7579 spec
172+
* @param module the module address
173+
* @param deInitData arbitrary data that may be required on the module during `onInstall`
174+
* initialization.
175+
*
176+
* MUST implement authorization control
177+
* MUST call `onUninstall` on the module with the `deInitData` parameter if provided
178+
* MUST emit ModuleUninstalled event
179+
* MUST revert if the module is not installed or the deInitialization on the module failed
180+
*/
181+
function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external;
182+
183+
/**
184+
* @dev Returns whether a module is installed on the smart account
185+
* @param moduleTypeId the module type ID according the ERC-7579 spec
186+
* @param module the module address
187+
* @param additionalContext arbitrary data that may be required to determine if the module is installed
188+
*
189+
* MUST return true if the module is installed and false otherwise
190+
*/
191+
function isModuleInstalled(
192+
uint256 moduleTypeId,
193+
address module,
194+
bytes calldata additionalContext
195+
) external view returns (bool);
196+
}

‎contracts/mocks/Stateless.sol

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {ERC165} from "../utils/introspection/ERC165.sol";
2525
import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol";
2626
import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol";
2727
import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol";
28+
import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol";
29+
import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol";
2830
import {Heap} from "../utils/structs/Heap.sol";
2931
import {Math} from "../utils/math/Math.sol";
3032
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {CallType, ExecType, ModeSelector, ModePayload} from "../../../account/utils/draft-ERC7579Utils.sol";
6+
7+
contract ERC7579UtilsGlobalMock {
8+
function eqCallTypeGlobal(CallType callType1, CallType callType2) internal pure returns (bool) {
9+
return callType1 == callType2;
10+
}
11+
12+
function eqExecTypeGlobal(ExecType execType1, ExecType execType2) internal pure returns (bool) {
13+
return execType1 == execType2;
14+
}
15+
16+
function eqModeSelectorGlobal(ModeSelector modeSelector1, ModeSelector modeSelector2) internal pure returns (bool) {
17+
return modeSelector1 == modeSelector2;
18+
}
19+
20+
function eqModePayloadGlobal(ModePayload modePayload1, ModePayload modePayload2) internal pure returns (bool) {
21+
return modePayload1 == modePayload2;
22+
}
23+
}

‎contracts/utils/Packing.sol

+513
Large diffs are not rendered by default.
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module.exports = {
2-
SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32],
2+
SIZES: [1, 2, 4, 6, 8, 10, 12, 16, 20, 22, 24, 28, 32],
33
};

‎scripts/generate/templates/Packing.t.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {Packing} from "@openzeppelin/contracts/utils/Packing.sol";
1111
`;
1212

1313
const testPack = (left, right) => `\
14-
function testPack(bytes${left} left, bytes${right} right) external {
14+
function testPack(bytes${left} left, bytes${right} right) external pure {
1515
assertEq(left, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${left}(0));
1616
assertEq(right, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${right}(${left}));
1717
}
1818
`;
1919

2020
const testReplace = (outer, inner) => `\
21-
function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external {
21+
function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external pure {
2222
offset = uint8(bound(offset, 0, ${outer - inner}));
2323
2424
bytes${inner} oldValue = container.extract_${outer}_${inner}(offset);

‎slither.config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-state,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,pragma,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this",
3-
"filter_paths": "contracts/mocks,contracts-exposed",
3+
"filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed",
44
"compile_force_framework": "hardhat"
55
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
const { packValidationData, packPaymasterData, UserOperation } = require('../../helpers/erc4337');
6+
const { MAX_UINT48 } = require('../../helpers/constants');
7+
8+
const fixture = async () => {
9+
const [authorizer, sender, entrypoint, paymaster] = await ethers.getSigners();
10+
const utils = await ethers.deployContract('$ERC4337Utils');
11+
return { utils, authorizer, sender, entrypoint, paymaster };
12+
};
13+
14+
describe('ERC4337Utils', function () {
15+
beforeEach(async function () {
16+
Object.assign(this, await loadFixture(fixture));
17+
});
18+
19+
describe('parseValidationData', function () {
20+
it('parses the validation data', async function () {
21+
const authorizer = this.authorizer;
22+
const validUntil = 0x12345678n;
23+
const validAfter = 0x9abcdef0n;
24+
const validationData = packValidationData(validAfter, validUntil, authorizer);
25+
26+
expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([
27+
authorizer.address,
28+
validAfter,
29+
validUntil,
30+
]);
31+
});
32+
33+
it('returns an type(uint48).max if until is 0', async function () {
34+
const authorizer = this.authorizer;
35+
const validAfter = 0x12345678n;
36+
const validationData = packValidationData(validAfter, 0, authorizer);
37+
38+
expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([
39+
authorizer.address,
40+
validAfter,
41+
MAX_UINT48,
42+
]);
43+
});
44+
});
45+
46+
describe('packValidationData', function () {
47+
it('packs the validation data', async function () {
48+
const authorizer = this.authorizer;
49+
const validUntil = 0x12345678n;
50+
const validAfter = 0x9abcdef0n;
51+
const validationData = packValidationData(validAfter, validUntil, authorizer);
52+
53+
expect(
54+
this.utils.$packValidationData(ethers.Typed.address(authorizer), validAfter, validUntil),
55+
).to.eventually.equal(validationData);
56+
});
57+
58+
it('packs the validation data (bool)', async function () {
59+
const success = false;
60+
const validUntil = 0x12345678n;
61+
const validAfter = 0x9abcdef0n;
62+
const validationData = packValidationData(validAfter, validUntil, false);
63+
64+
expect(this.utils.$packValidationData(ethers.Typed.bool(success), validAfter, validUntil)).to.eventually.equal(
65+
validationData,
66+
);
67+
});
68+
});
69+
70+
describe('combineValidationData', function () {
71+
const validUntil1 = 0x12345678n;
72+
const validAfter1 = 0x9abcdef0n;
73+
const validUntil2 = 0x87654321n;
74+
const validAfter2 = 0xabcdef90n;
75+
76+
it('combines the validation data', async function () {
77+
const validationData1 = packValidationData(validAfter1, validUntil1, ethers.ZeroAddress);
78+
const validationData2 = packValidationData(validAfter2, validUntil2, ethers.ZeroAddress);
79+
const expected = packValidationData(validAfter2, validUntil1, true);
80+
81+
// check symmetry
82+
expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected);
83+
expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected);
84+
});
85+
86+
for (const [authorizer1, authorizer2] of [
87+
[ethers.ZeroAddress, '0xbf023313b891fd6000544b79e353323aa94a4f29'],
88+
['0xbf023313b891fd6000544b79e353323aa94a4f29', ethers.ZeroAddress],
89+
]) {
90+
it('returns SIG_VALIDATION_FAILURE if one of the authorizers is not address(0)', async function () {
91+
const validationData1 = packValidationData(validAfter1, validUntil1, authorizer1);
92+
const validationData2 = packValidationData(validAfter2, validUntil2, authorizer2);
93+
const expected = packValidationData(validAfter2, validUntil1, false);
94+
95+
// check symmetry
96+
expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected);
97+
expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected);
98+
});
99+
}
100+
});
101+
102+
describe('getValidationData', function () {
103+
it('returns the validation data with valid validity range', async function () {
104+
const aggregator = this.authorizer;
105+
const validAfter = 0;
106+
const validUntil = MAX_UINT48;
107+
const validationData = packValidationData(validAfter, validUntil, aggregator);
108+
109+
expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, false]);
110+
});
111+
112+
it('returns the validation data with invalid validity range (expired)', async function () {
113+
const aggregator = this.authorizer;
114+
const validAfter = 0;
115+
const validUntil = 1;
116+
const validationData = packValidationData(validAfter, validUntil, aggregator);
117+
118+
expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]);
119+
});
120+
121+
it('returns the validation data with invalid validity range (not yet valid)', async function () {
122+
const aggregator = this.authorizer;
123+
const validAfter = MAX_UINT48;
124+
const validUntil = MAX_UINT48;
125+
const validationData = packValidationData(validAfter, validUntil, aggregator);
126+
127+
expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]);
128+
});
129+
130+
it('returns address(0) and false for validationData = 0', function () {
131+
expect(this.utils.$getValidationData(0n)).to.eventually.deep.equal([ethers.ZeroAddress, false]);
132+
});
133+
});
134+
135+
describe('hash', function () {
136+
it('returns the user operation hash', async function () {
137+
const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
138+
const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId);
139+
140+
expect(this.utils.$hash(userOp.packed)).to.eventually.equal(userOp.hash(this.utils.target, chainId));
141+
});
142+
143+
it('returns the operation hash with specified entrypoint and chainId', async function () {
144+
const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
145+
const chainId = 0xdeadbeef;
146+
147+
expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal(
148+
userOp.hash(this.entrypoint, chainId),
149+
);
150+
});
151+
});
152+
153+
describe('userOp values', function () {
154+
it('returns verificationGasLimit', async function () {
155+
const userOp = new UserOperation({ sender: this.sender, nonce: 1, verificationGas: 0x12345678n });
156+
expect(this.utils.$verificationGasLimit(userOp.packed)).to.eventually.equal(userOp.verificationGas);
157+
});
158+
159+
it('returns callGasLimit', async function () {
160+
const userOp = new UserOperation({ sender: this.sender, nonce: 1, callGas: 0x12345678n });
161+
expect(this.utils.$callGasLimit(userOp.packed)).to.eventually.equal(userOp.callGas);
162+
});
163+
164+
it('returns maxPriorityFeePerGas', async function () {
165+
const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxPriorityFee: 0x12345678n });
166+
expect(this.utils.$maxPriorityFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee);
167+
});
168+
169+
it('returns maxFeePerGas', async function () {
170+
const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxFeePerGas: 0x12345678n });
171+
expect(this.utils.$maxFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxFeePerGas);
172+
});
173+
174+
it('returns gasPrice', async function () {
175+
const userOp = new UserOperation({
176+
sender: this.sender,
177+
nonce: 1,
178+
maxPriorityFee: 0x12345678n,
179+
maxFeePerGas: 0x87654321n,
180+
});
181+
expect(this.utils.$gasPrice(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee);
182+
});
183+
184+
describe('paymasterAndData', function () {
185+
beforeEach(async function () {
186+
this.verificationGasLimit = 0x12345678n;
187+
this.postOpGasLimit = 0x87654321n;
188+
this.paymasterAndData = packPaymasterData(this.paymaster, this.verificationGasLimit, this.postOpGasLimit);
189+
this.userOp = new UserOperation({
190+
sender: this.sender,
191+
nonce: 1,
192+
paymasterAndData: this.paymasterAndData,
193+
});
194+
});
195+
196+
it('returns paymaster', async function () {
197+
expect(this.utils.$paymaster(this.userOp.packed)).to.eventually.equal(this.paymaster);
198+
});
199+
200+
it('returns verificationGasLimit', async function () {
201+
expect(this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.eventually.equal(
202+
this.verificationGasLimit,
203+
);
204+
});
205+
206+
it('returns postOpGasLimit', async function () {
207+
expect(this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.eventually.equal(this.postOpGasLimit);
208+
});
209+
});
210+
});
211+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers');
4+
const {
5+
EXEC_TYPE_DEFAULT,
6+
EXEC_TYPE_TRY,
7+
encodeSingle,
8+
encodeBatch,
9+
encodeDelegate,
10+
CALL_TYPE_CALL,
11+
CALL_TYPE_BATCH,
12+
encodeMode,
13+
} = require('../../helpers/erc7579');
14+
const { selector } = require('../../helpers/methods');
15+
16+
const coder = ethers.AbiCoder.defaultAbiCoder();
17+
18+
const fixture = async () => {
19+
const [sender] = await ethers.getSigners();
20+
const utils = await ethers.deployContract('$ERC7579Utils');
21+
const utilsGlobal = await ethers.deployContract('$ERC7579UtilsGlobalMock');
22+
const target = await ethers.deployContract('CallReceiverMock');
23+
const anotherTarget = await ethers.deployContract('CallReceiverMock');
24+
await setBalance(utils.target, ethers.parseEther('1'));
25+
return { utils, utilsGlobal, target, anotherTarget, sender };
26+
};
27+
28+
describe('ERC7579Utils', function () {
29+
beforeEach(async function () {
30+
Object.assign(this, await loadFixture(fixture));
31+
});
32+
33+
describe('execSingle', function () {
34+
it('calls the target with value', async function () {
35+
const value = 0x012;
36+
const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
37+
38+
await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.emit(this.target, 'MockFunctionCalled');
39+
40+
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
41+
});
42+
43+
it('calls the target with value and args', async function () {
44+
const value = 0x432;
45+
const data = encodeSingle(
46+
this.target,
47+
value,
48+
this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
49+
);
50+
51+
await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data))
52+
.to.emit(this.target, 'MockFunctionCalledWithArgs')
53+
.withArgs(42, '0x1234');
54+
55+
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
56+
});
57+
58+
it('reverts when target reverts in default ExecType', async function () {
59+
const value = 0x012;
60+
const data = encodeSingle(
61+
this.target,
62+
value,
63+
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
64+
);
65+
66+
await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting');
67+
});
68+
69+
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
70+
const value = 0x012;
71+
const data = encodeSingle(
72+
this.target,
73+
value,
74+
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
75+
);
76+
77+
await expect(this.utils.$execSingle(EXEC_TYPE_TRY, data))
78+
.to.emit(this.utils, 'ERC7579TryExecuteFail')
79+
.withArgs(
80+
CALL_TYPE_CALL,
81+
ethers.solidityPacked(
82+
['bytes4', 'bytes'],
83+
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
84+
),
85+
);
86+
});
87+
88+
it('reverts with an invalid exec type', async function () {
89+
const value = 0x012;
90+
const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
91+
92+
await expect(this.utils.$execSingle('0x03', data))
93+
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
94+
.withArgs('0x03');
95+
});
96+
});
97+
98+
describe('execBatch', function () {
99+
it('calls the targets with value', async function () {
100+
const value1 = 0x012;
101+
const value2 = 0x234;
102+
const data = encodeBatch(
103+
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
104+
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
105+
);
106+
107+
await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data))
108+
.to.emit(this.target, 'MockFunctionCalled')
109+
.to.emit(this.anotherTarget, 'MockFunctionCalled');
110+
111+
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
112+
expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2);
113+
});
114+
115+
it('calls the targets with value and args', async function () {
116+
const value1 = 0x012;
117+
const value2 = 0x234;
118+
const data = encodeBatch(
119+
[this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])],
120+
[
121+
this.anotherTarget,
122+
value2,
123+
this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
124+
],
125+
);
126+
127+
await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data))
128+
.to.emit(this.target, 'MockFunctionCalledWithArgs')
129+
.to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
130+
131+
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
132+
expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2);
133+
});
134+
135+
it('reverts when any target reverts in default ExecType', async function () {
136+
const value1 = 0x012;
137+
const value2 = 0x234;
138+
const data = encodeBatch(
139+
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
140+
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
141+
);
142+
143+
await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting');
144+
});
145+
146+
it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () {
147+
const value1 = 0x012;
148+
const value2 = 0x234;
149+
const data = encodeBatch(
150+
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
151+
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
152+
);
153+
154+
await expect(this.utils.$execBatch(EXEC_TYPE_TRY, data))
155+
.to.emit(this.utils, 'ERC7579TryExecuteFail')
156+
.withArgs(
157+
CALL_TYPE_BATCH,
158+
ethers.solidityPacked(
159+
['bytes4', 'bytes'],
160+
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
161+
),
162+
);
163+
164+
// Check balances
165+
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
166+
expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(0);
167+
});
168+
169+
it('reverts with an invalid exec type', async function () {
170+
const value1 = 0x012;
171+
const value2 = 0x234;
172+
const data = encodeBatch(
173+
[this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
174+
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
175+
);
176+
177+
await expect(this.utils.$execBatch('0x03', data))
178+
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
179+
.withArgs('0x03');
180+
});
181+
});
182+
183+
describe('execDelegateCall', function () {
184+
it('delegate calls the target', async function () {
185+
const slot = ethers.hexlify(ethers.randomBytes(32));
186+
const value = ethers.hexlify(ethers.randomBytes(32));
187+
const data = encodeDelegate(
188+
this.target,
189+
this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]),
190+
);
191+
192+
expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(ethers.ZeroHash);
193+
await this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data);
194+
expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(value);
195+
});
196+
197+
it('reverts when target reverts in default ExecType', async function () {
198+
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
199+
await expect(this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith(
200+
'CallReceiverMock: reverting',
201+
);
202+
});
203+
204+
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
205+
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
206+
await expect(this.utils.$execDelegateCall(EXEC_TYPE_TRY, data))
207+
.to.emit(this.utils, 'ERC7579TryExecuteFail')
208+
.withArgs(
209+
CALL_TYPE_CALL,
210+
ethers.solidityPacked(
211+
['bytes4', 'bytes'],
212+
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
213+
),
214+
);
215+
});
216+
217+
it('reverts with an invalid exec type', async function () {
218+
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunction'));
219+
await expect(this.utils.$execDelegateCall('0x03', data))
220+
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
221+
.withArgs('0x03');
222+
});
223+
});
224+
225+
it('encodes Mode', async function () {
226+
const callType = CALL_TYPE_BATCH;
227+
const execType = EXEC_TYPE_TRY;
228+
const selector = '0x12345678';
229+
const payload = ethers.toBeHex(0, 22);
230+
231+
expect(this.utils.$encodeMode(callType, execType, selector, payload)).to.eventually.equal(
232+
encodeMode({
233+
callType,
234+
execType,
235+
selector,
236+
payload,
237+
}),
238+
);
239+
});
240+
241+
it('decodes Mode', async function () {
242+
const callType = CALL_TYPE_BATCH;
243+
const execType = EXEC_TYPE_TRY;
244+
const selector = '0x12345678';
245+
const payload = ethers.toBeHex(0, 22);
246+
247+
expect(
248+
this.utils.$decodeMode(
249+
encodeMode({
250+
callType,
251+
execType,
252+
selector,
253+
payload,
254+
}),
255+
),
256+
).to.eventually.deep.equal([callType, execType, selector, payload]);
257+
});
258+
259+
it('encodes single', async function () {
260+
const target = this.target;
261+
const value = 0x123;
262+
const data = '0x12345678';
263+
264+
expect(this.utils.$encodeSingle(target, value, data)).to.eventually.equal(encodeSingle(target, value, data));
265+
});
266+
267+
it('decodes single', async function () {
268+
const target = this.target;
269+
const value = 0x123;
270+
const data = '0x12345678';
271+
272+
expect(this.utils.$decodeSingle(encodeSingle(target, value, data))).to.eventually.deep.equal([
273+
target.target,
274+
value,
275+
data,
276+
]);
277+
});
278+
279+
it('encodes batch', async function () {
280+
const entries = [
281+
[this.target, 0x123, '0x12345678'],
282+
[this.anotherTarget, 0x456, '0x12345678'],
283+
];
284+
285+
expect(this.utils.$encodeBatch(entries)).to.eventually.equal(encodeBatch(...entries));
286+
});
287+
288+
it('decodes batch', async function () {
289+
const entries = [
290+
[this.target.target, 0x123, '0x12345678'],
291+
[this.anotherTarget.target, 0x456, '0x12345678'],
292+
];
293+
294+
expect(this.utils.$decodeBatch(encodeBatch(...entries))).to.eventually.deep.equal(entries);
295+
});
296+
297+
it('encodes delegate', async function () {
298+
const target = this.target;
299+
const data = '0x12345678';
300+
301+
expect(this.utils.$encodeDelegate(target, data)).to.eventually.equal(encodeDelegate(target, data));
302+
});
303+
304+
it('decodes delegate', async function () {
305+
const target = this.target;
306+
const data = '0x12345678';
307+
308+
expect(this.utils.$decodeDelegate(encodeDelegate(target, data))).to.eventually.deep.equal([target.target, data]);
309+
});
310+
311+
describe('global', function () {
312+
describe('eqCallTypeGlobal', function () {
313+
it('returns true if both call types are equal', async function () {
314+
expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_BATCH, CALL_TYPE_BATCH)).to.eventually.be.true;
315+
});
316+
317+
it('returns false if both call types are different', async function () {
318+
expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.eventually.be.false;
319+
});
320+
});
321+
322+
describe('eqExecTypeGlobal', function () {
323+
it('returns true if both exec types are equal', async function () {
324+
expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_TRY, EXEC_TYPE_TRY)).to.eventually.be.true;
325+
});
326+
327+
it('returns false if both exec types are different', async function () {
328+
expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.eventually.be.false;
329+
});
330+
});
331+
332+
describe('eqModeSelectorGlobal', function () {
333+
it('returns true if both selectors are equal', async function () {
334+
expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x12345678')).to.eventually.be.true;
335+
});
336+
337+
it('returns false if both selectors are different', async function () {
338+
expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.eventually.be.false;
339+
});
340+
});
341+
342+
describe('eqModePayloadGlobal', function () {
343+
it('returns true if both payloads are equal', async function () {
344+
expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(0, 22))).to.eventually.be
345+
.true;
346+
});
347+
348+
it('returns false if both payloads are different', async function () {
349+
expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.eventually.be
350+
.false;
351+
});
352+
});
353+
});
354+
});

‎test/helpers/erc4337.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const { ethers } = require('hardhat');
2+
3+
const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
4+
const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
5+
6+
function getAddress(account) {
7+
return account.target ?? account.address ?? account;
8+
}
9+
10+
function pack(left, right) {
11+
return ethers.solidityPacked(['uint128', 'uint128'], [left, right]);
12+
}
13+
14+
function packValidationData(validAfter, validUntil, authorizer) {
15+
return ethers.solidityPacked(
16+
['uint48', 'uint48', 'address'],
17+
[
18+
validAfter,
19+
validUntil,
20+
typeof authorizer == 'boolean'
21+
? authorizer
22+
? SIG_VALIDATION_SUCCESS
23+
: SIG_VALIDATION_FAILURE
24+
: getAddress(authorizer),
25+
],
26+
);
27+
}
28+
29+
function packPaymasterData(paymaster, verificationGasLimit, postOpGasLimit) {
30+
return ethers.solidityPacked(
31+
['address', 'uint128', 'uint128'],
32+
[getAddress(paymaster), verificationGasLimit, postOpGasLimit],
33+
);
34+
}
35+
36+
/// Represent one user operation
37+
class UserOperation {
38+
constructor(params) {
39+
this.sender = getAddress(params.sender);
40+
this.nonce = params.nonce;
41+
this.initCode = params.initCode ?? '0x';
42+
this.callData = params.callData ?? '0x';
43+
this.verificationGas = params.verificationGas ?? 10_000_000n;
44+
this.callGas = params.callGas ?? 100_000n;
45+
this.preVerificationGas = params.preVerificationGas ?? 100_000n;
46+
this.maxPriorityFee = params.maxPriorityFee ?? 100_000n;
47+
this.maxFeePerGas = params.maxFeePerGas ?? 100_000n;
48+
this.paymasterAndData = params.paymasterAndData ?? '0x';
49+
this.signature = params.signature ?? '0x';
50+
}
51+
52+
get packed() {
53+
return {
54+
sender: this.sender,
55+
nonce: this.nonce,
56+
initCode: this.initCode,
57+
callData: this.callData,
58+
accountGasLimits: pack(this.verificationGas, this.callGas),
59+
preVerificationGas: this.preVerificationGas,
60+
gasFees: pack(this.maxPriorityFee, this.maxFeePerGas),
61+
paymasterAndData: this.paymasterAndData,
62+
signature: this.signature,
63+
};
64+
}
65+
66+
hash(entrypoint, chainId) {
67+
const p = this.packed;
68+
const h = ethers.keccak256(
69+
ethers.AbiCoder.defaultAbiCoder().encode(
70+
['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'],
71+
[
72+
p.sender,
73+
p.nonce,
74+
ethers.keccak256(p.initCode),
75+
ethers.keccak256(p.callData),
76+
p.accountGasLimits,
77+
p.preVerificationGas,
78+
p.gasFees,
79+
ethers.keccak256(p.paymasterAndData),
80+
],
81+
),
82+
);
83+
return ethers.keccak256(
84+
ethers.AbiCoder.defaultAbiCoder().encode(['bytes32', 'address', 'uint256'], [h, getAddress(entrypoint), chainId]),
85+
);
86+
}
87+
}
88+
89+
module.exports = {
90+
SIG_VALIDATION_SUCCESS,
91+
SIG_VALIDATION_FAILURE,
92+
packValidationData,
93+
packPaymasterData,
94+
UserOperation,
95+
};

‎test/helpers/erc7579.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const { ethers } = require('hardhat');
2+
3+
const MODULE_TYPE_VALIDATOR = 1;
4+
const MODULE_TYPE_EXECUTOR = 2;
5+
const MODULE_TYPE_FALLBACK = 3;
6+
const MODULE_TYPE_HOOK = 4;
7+
8+
const EXEC_TYPE_DEFAULT = '0x00';
9+
const EXEC_TYPE_TRY = '0x01';
10+
11+
const CALL_TYPE_CALL = '0x00';
12+
const CALL_TYPE_BATCH = '0x01';
13+
const CALL_TYPE_DELEGATE = '0xff';
14+
15+
const encodeMode = ({
16+
callType = '0x00',
17+
execType = '0x00',
18+
selector = '0x00000000',
19+
payload = '0x00000000000000000000000000000000000000000000',
20+
} = {}) =>
21+
ethers.solidityPacked(
22+
['bytes1', 'bytes1', 'bytes4', 'bytes4', 'bytes22'],
23+
[callType, execType, '0x00000000', selector, payload],
24+
);
25+
26+
const encodeSingle = (target, value = 0n, data = '0x') =>
27+
ethers.solidityPacked(['address', 'uint256', 'bytes'], [target.target ?? target.address ?? target, value, data]);
28+
29+
const encodeBatch = (...entries) =>
30+
ethers.AbiCoder.defaultAbiCoder().encode(
31+
['(address,uint256,bytes)[]'],
32+
[
33+
entries.map(entry =>
34+
Array.isArray(entry)
35+
? [entry[0].target ?? entry[0].address ?? entry[0], entry[1] ?? 0n, entry[2] ?? '0x']
36+
: [entry.target.target ?? entry.target.address ?? entry.target, entry.value ?? 0n, entry.data ?? '0x'],
37+
),
38+
],
39+
);
40+
41+
const encodeDelegate = (target, data = '0x') =>
42+
ethers.solidityPacked(['address', 'bytes'], [target.target ?? target.address ?? target, data]);
43+
44+
module.exports = {
45+
MODULE_TYPE_VALIDATOR,
46+
MODULE_TYPE_EXECUTOR,
47+
MODULE_TYPE_FALLBACK,
48+
MODULE_TYPE_HOOK,
49+
EXEC_TYPE_DEFAULT,
50+
EXEC_TYPE_TRY,
51+
CALL_TYPE_CALL,
52+
CALL_TYPE_BATCH,
53+
CALL_TYPE_DELEGATE,
54+
encodeMode,
55+
encodeSingle,
56+
encodeBatch,
57+
encodeDelegate,
58+
};

‎test/utils/Packing.t.sol

+402-90
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.