Browse Source

Merge account abstraction work into master (#5274)

Co-authored-by: Ernesto García <ernestognw@gmail.com>
Co-authored-by: Elias Rad <146735585+nnsW3@users.noreply.github.com>
Co-authored-by: cairo <cairoeth@protonmail.com>
Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
Hadrien Croubois 11 months ago
parent
commit
28aed34dc5

+ 5 - 0
.changeset/hot-shrimps-wait.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Packing`: Add variants for packing `bytes10` and `bytes22`

+ 5 - 0
.changeset/small-seahorses-bathe.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`ERC7579Utils`: Add a reusable library to interact with ERC-7579 modular accounts

+ 5 - 0
.changeset/weak-roses-bathe.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`ERC4337Utils`: Add a reusable library to manipulate user operations and interact with ERC-4337 contracts

+ 1 - 0
.codecov.yml

@@ -13,3 +13,4 @@ coverage:
 ignore:
   - "test"
   - "contracts/mocks"
+  - "contracts/vendor"

+ 1 - 1
.github/workflows/checks.yml

@@ -133,4 +133,4 @@ jobs:
         with:
           check_hidden: true
           check_filenames: true
-          skip: package-lock.json,*.pdf
+          skip: package-lock.json,*.pdf,vendor

+ 12 - 0
contracts/account/README.adoc

@@ -0,0 +1,12 @@
+= Account
+
+[.readme-notice]
+NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account
+
+This directory includes contracts to build accounts for ERC-4337.
+
+== Utilities
+
+{{ERC4337Utils}}
+
+{{ERC7579Utils}}

+ 150 - 0
contracts/account/utils/draft-ERC4337Utils.sol

@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {IEntryPoint, PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
+import {Math} from "../../utils/math/Math.sol";
+import {Packing} from "../../utils/Packing.sol";
+
+/**
+ * @dev Library with common ERC-4337 utility functions.
+ *
+ * See https://eips.ethereum.org/EIPS/eip-4337[ERC-4337].
+ */
+library ERC4337Utils {
+    using Packing for *;
+
+    /// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success.
+    uint256 internal constant SIG_VALIDATION_SUCCESS = 0;
+
+    /// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) must return this value in case of signature failure, instead of revert.
+    uint256 internal constant SIG_VALIDATION_FAILED = 1;
+
+    /// @dev Parses the validation data into its components. See {packValidationData}.
+    function parseValidationData(
+        uint256 validationData
+    ) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) {
+        validAfter = uint48(bytes32(validationData).extract_32_6(0x00));
+        validUntil = uint48(bytes32(validationData).extract_32_6(0x06));
+        aggregator = address(bytes32(validationData).extract_32_20(0x0c));
+        if (validUntil == 0) validUntil = type(uint48).max;
+    }
+
+    /// @dev Packs the validation data into a single uint256. See {parseValidationData}.
+    function packValidationData(
+        address aggregator,
+        uint48 validAfter,
+        uint48 validUntil
+    ) internal pure returns (uint256) {
+        return uint256(bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20(bytes20(aggregator)));
+    }
+
+    /// @dev Same as {packValidationData}, but with a boolean signature success flag.
+    function packValidationData(bool sigSuccess, uint48 validAfter, uint48 validUntil) internal pure returns (uint256) {
+        return
+            packValidationData(
+                address(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))),
+                validAfter,
+                validUntil
+            );
+    }
+
+    /**
+     * @dev Combines two validation data into a single one.
+     *
+     * The `aggregator` is set to {SIG_VALIDATION_SUCCESS} if both are successful, while
+     * the `validAfter` is the maximum and the `validUntil` is the minimum of both.
+     */
+    function combineValidationData(uint256 validationData1, uint256 validationData2) internal pure returns (uint256) {
+        (address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1);
+        (address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2);
+
+        bool success = aggregator1 == address(0) && aggregator2 == address(0);
+        uint48 validAfter = uint48(Math.max(validAfter1, validAfter2));
+        uint48 validUntil = uint48(Math.min(validUntil1, validUntil2));
+        return packValidationData(success, validAfter, validUntil);
+    }
+
+    /// @dev Returns the aggregator of the `validationData` and whether it is out of time range.
+    function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) {
+        (address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData);
+        return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp);
+    }
+
+    /// @dev Computes the hash of a user operation with the current entrypoint and chainid.
+    function hash(PackedUserOperation calldata self) internal view returns (bytes32) {
+        return hash(self, address(this), block.chainid);
+    }
+
+    /// @dev Sames as {hash}, but with a custom entrypoint and chainid.
+    function hash(
+        PackedUserOperation calldata self,
+        address entrypoint,
+        uint256 chainid
+    ) internal pure returns (bytes32) {
+        bytes32 result = keccak256(
+            abi.encode(
+                keccak256(
+                    abi.encode(
+                        self.sender,
+                        self.nonce,
+                        keccak256(self.initCode),
+                        keccak256(self.callData),
+                        self.accountGasLimits,
+                        self.preVerificationGas,
+                        self.gasFees,
+                        keccak256(self.paymasterAndData)
+                    )
+                ),
+                entrypoint,
+                chainid
+            )
+        );
+        return result;
+    }
+
+    /// @dev Returns `verificationGasLimit` from the {PackedUserOperation}.
+    function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
+        return uint128(self.accountGasLimits.extract_32_16(0x00));
+    }
+
+    /// @dev Returns `accountGasLimits` from the {PackedUserOperation}.
+    function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
+        return uint128(self.accountGasLimits.extract_32_16(0x10));
+    }
+
+    /// @dev Returns the first section of `gasFees` from the {PackedUserOperation}.
+    function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
+        return uint128(self.gasFees.extract_32_16(0x00));
+    }
+
+    /// @dev Returns the second section of `gasFees` from the {PackedUserOperation}.
+    function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
+        return uint128(self.gasFees.extract_32_16(0x10));
+    }
+
+    /// @dev Returns the total gas price for the {PackedUserOperation} (ie. `maxFeePerGas` or `maxPriorityFeePerGas + basefee`).
+    function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) {
+        unchecked {
+            // Following values are "per gas"
+            uint256 maxPriorityFee = maxPriorityFeePerGas(self);
+            uint256 maxFee = maxFeePerGas(self);
+            return Math.ternary(maxFee == maxPriorityFee, maxFee, Math.min(maxFee, maxPriorityFee + block.basefee));
+        }
+    }
+
+    /// @dev Returns the first section of `paymasterAndData` from the {PackedUserOperation}.
+    function paymaster(PackedUserOperation calldata self) internal pure returns (address) {
+        return address(bytes20(self.paymasterAndData[0:20]));
+    }
+
+    /// @dev Returns the second section of `paymasterAndData` from the {PackedUserOperation}.
+    function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
+        return uint128(bytes16(self.paymasterAndData[20:36]));
+    }
+
+    /// @dev Returns the third section of `paymasterAndData` from the {PackedUserOperation}.
+    function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
+        return uint128(bytes16(self.paymasterAndData[36:52]));
+    }
+}

+ 242 - 0
contracts/account/utils/draft-ERC7579Utils.sol

@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {Execution} from "../../interfaces/draft-IERC7579.sol";
+import {Packing} from "../../utils/Packing.sol";
+import {Address} from "../../utils/Address.sol";
+
+type Mode is bytes32;
+type CallType is bytes1;
+type ExecType is bytes1;
+type ModeSelector is bytes4;
+type ModePayload is bytes22;
+
+/**
+ * @dev Library with common ERC-7579 utility functions.
+ *
+ * See https://eips.ethereum.org/EIPS/eip-7579[ERC-7579].
+ */
+// slither-disable-next-line unused-state
+library ERC7579Utils {
+    using Packing for *;
+
+    /// @dev A single `call` execution.
+    CallType constant CALLTYPE_SINGLE = CallType.wrap(0x00);
+
+    /// @dev A batch of `call` executions.
+    CallType constant CALLTYPE_BATCH = CallType.wrap(0x01);
+
+    /// @dev A `delegatecall` execution.
+    CallType constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF);
+
+    /// @dev Default execution type that reverts on failure.
+    ExecType constant EXECTYPE_DEFAULT = ExecType.wrap(0x00);
+
+    /// @dev Execution type that does not revert on failure.
+    ExecType constant EXECTYPE_TRY = ExecType.wrap(0x01);
+
+    /// @dev Emits when an {EXECTYPE_TRY} execution fails.
+    event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes result);
+
+    /// @dev The provided {CallType} is not supported.
+    error ERC7579UnsupportedCallType(CallType callType);
+
+    /// @dev The provided {ExecType} is not supported.
+    error ERC7579UnsupportedExecType(ExecType execType);
+
+    /// @dev The provided module doesn't match the provided module type.
+    error ERC7579MismatchedModuleTypeId(uint256 moduleTypeId, address module);
+
+    /// @dev The module is not installed.
+    error ERC7579UninstalledModule(uint256 moduleTypeId, address module);
+
+    /// @dev The module is already installed.
+    error ERC7579AlreadyInstalledModule(uint256 moduleTypeId, address module);
+
+    /// @dev The module type is not supported.
+    error ERC7579UnsupportedModuleType(uint256 moduleTypeId);
+
+    /// @dev Executes a single call.
+    function execSingle(
+        ExecType execType,
+        bytes calldata executionCalldata
+    ) internal returns (bytes[] memory returnData) {
+        (address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata);
+        returnData = new bytes[](1);
+        returnData[0] = _call(0, execType, target, value, callData);
+    }
+
+    /// @dev Executes a batch of calls.
+    function execBatch(
+        ExecType execType,
+        bytes calldata executionCalldata
+    ) internal returns (bytes[] memory returnData) {
+        Execution[] calldata executionBatch = decodeBatch(executionCalldata);
+        returnData = new bytes[](executionBatch.length);
+        for (uint256 i = 0; i < executionBatch.length; ++i) {
+            returnData[i] = _call(
+                i,
+                execType,
+                executionBatch[i].target,
+                executionBatch[i].value,
+                executionBatch[i].callData
+            );
+        }
+    }
+
+    /// @dev Executes a delegate call.
+    function execDelegateCall(
+        ExecType execType,
+        bytes calldata executionCalldata
+    ) internal returns (bytes[] memory returnData) {
+        (address target, bytes calldata callData) = decodeDelegate(executionCalldata);
+        returnData = new bytes[](1);
+        returnData[0] = _delegatecall(0, execType, target, callData);
+    }
+
+    /// @dev Encodes the mode with the provided parameters. See {decodeMode}.
+    function encodeMode(
+        CallType callType,
+        ExecType execType,
+        ModeSelector selector,
+        ModePayload payload
+    ) internal pure returns (Mode mode) {
+        return
+            Mode.wrap(
+                CallType
+                    .unwrap(callType)
+                    .pack_1_1(ExecType.unwrap(execType))
+                    .pack_2_4(bytes4(0))
+                    .pack_6_4(ModeSelector.unwrap(selector))
+                    .pack_10_22(ModePayload.unwrap(payload))
+            );
+    }
+
+    /// @dev Decodes the mode into its parameters. See {encodeMode}.
+    function decodeMode(
+        Mode mode
+    ) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) {
+        return (
+            CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)),
+            ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)),
+            ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)),
+            ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10))
+        );
+    }
+
+    /// @dev Encodes a single call execution. See {decodeSingle}.
+    function encodeSingle(
+        address target,
+        uint256 value,
+        bytes calldata callData
+    ) internal pure returns (bytes memory executionCalldata) {
+        return abi.encodePacked(target, value, callData);
+    }
+
+    /// @dev Decodes a single call execution. See {encodeSingle}.
+    function decodeSingle(
+        bytes calldata executionCalldata
+    ) internal pure returns (address target, uint256 value, bytes calldata callData) {
+        target = address(bytes20(executionCalldata[0:20]));
+        value = uint256(bytes32(executionCalldata[20:52]));
+        callData = executionCalldata[52:];
+    }
+
+    /// @dev Encodes a delegate call execution. See {decodeDelegate}.
+    function encodeDelegate(
+        address target,
+        bytes calldata callData
+    ) internal pure returns (bytes memory executionCalldata) {
+        return abi.encodePacked(target, callData);
+    }
+
+    /// @dev Decodes a delegate call execution. See {encodeDelegate}.
+    function decodeDelegate(
+        bytes calldata executionCalldata
+    ) internal pure returns (address target, bytes calldata callData) {
+        target = address(bytes20(executionCalldata[0:20]));
+        callData = executionCalldata[20:];
+    }
+
+    /// @dev Encodes a batch of executions. See {decodeBatch}.
+    function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) {
+        return abi.encode(executionBatch);
+    }
+
+    /// @dev Decodes a batch of executions. See {encodeBatch}.
+    function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) {
+        assembly ("memory-safe") {
+            let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset))
+            // Extract the ERC7579 Executions
+            executionBatch.offset := add(ptr, 32)
+            executionBatch.length := calldataload(ptr)
+        }
+    }
+
+    /// @dev Executes a `call` to the target with the provided {ExecType}.
+    function _call(
+        uint256 index,
+        ExecType execType,
+        address target,
+        uint256 value,
+        bytes calldata data
+    ) private returns (bytes memory) {
+        (bool success, bytes memory returndata) = target.call{value: value}(data);
+        return _validateExecutionMode(index, execType, success, returndata);
+    }
+
+    /// @dev Executes a `delegatecall` to the target with the provided {ExecType}.
+    function _delegatecall(
+        uint256 index,
+        ExecType execType,
+        address target,
+        bytes calldata data
+    ) private returns (bytes memory) {
+        (bool success, bytes memory returndata) = target.delegatecall(data);
+        return _validateExecutionMode(index, execType, success, returndata);
+    }
+
+    /// @dev Validates the execution mode and returns the returndata.
+    function _validateExecutionMode(
+        uint256 index,
+        ExecType execType,
+        bool success,
+        bytes memory returndata
+    ) private returns (bytes memory) {
+        if (execType == ERC7579Utils.EXECTYPE_DEFAULT) {
+            Address.verifyCallResult(success, returndata);
+        } else if (execType == ERC7579Utils.EXECTYPE_TRY) {
+            if (!success) emit ERC7579TryExecuteFail(index, returndata);
+        } else {
+            revert ERC7579UnsupportedExecType(execType);
+        }
+        return returndata;
+    }
+}
+
+// Operators
+using {eqCallType as ==} for CallType global;
+using {eqExecType as ==} for ExecType global;
+using {eqModeSelector as ==} for ModeSelector global;
+using {eqModePayload as ==} for ModePayload global;
+
+/// @dev Compares two `CallType` values for equality.
+function eqCallType(CallType a, CallType b) pure returns (bool) {
+    return CallType.unwrap(a) == CallType.unwrap(b);
+}
+
+/// @dev Compares two `ExecType` values for equality.
+function eqExecType(ExecType a, ExecType b) pure returns (bool) {
+    return ExecType.unwrap(a) == ExecType.unwrap(b);
+}
+
+/// @dev Compares two `ModeSelector` values for equality.
+function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) {
+    return ModeSelector.unwrap(a) == ModeSelector.unwrap(b);
+}
+
+/// @dev Compares two `ModePayload` values for equality.
+function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) {
+    return ModePayload.unwrap(a) == ModePayload.unwrap(b);
+}

+ 215 - 0
contracts/interfaces/draft-IERC4337.sol

@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+/**
+ * @dev A https://github.com/ethereum/ercs/blob/master/ERCS/erc-4337.md#useroperation[user operation] is composed of the following elements:
+ * - `sender` (`address`): The account making the operation
+ * - `nonce` (`uint256`): Anti-replay parameter (see “Semi-abstracted Nonce Support” )
+ * - `factory` (`address`): account factory, only for new accounts
+ * - `factoryData` (`bytes`): data for account factory (only if account factory exists)
+ * - `callData` (`bytes`): The data to pass to the sender during the main execution call
+ * - `callGasLimit` (`uint256`): The amount of gas to allocate the main execution call
+ * - `verificationGasLimit` (`uint256`): The amount of gas to allocate for the verification step
+ * - `preVerificationGas` (`uint256`): Extra gas to pay the bunder
+ * - `maxFeePerGas` (`uint256`): Maximum fee per gas (similar to EIP-1559 max_fee_per_gas)
+ * - `maxPriorityFeePerGas` (`uint256`): Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas)
+ * - `paymaster` (`address`): Address of paymaster contract, (or empty, if account pays for itself)
+ * - `paymasterVerificationGasLimit` (`uint256`): The amount of gas to allocate for the paymaster validation code
+ * - `paymasterPostOpGasLimit` (`uint256`): The amount of gas to allocate for the paymaster post-operation code
+ * - `paymasterData` (`bytes`): Data for paymaster (only if paymaster exists)
+ * - `signature` (`bytes`): Data passed into the account to verify authorization
+ *
+ * When passed to on-chain contacts, the following packed version is used.
+ * - `sender` (`address`)
+ * - `nonce` (`uint256`)
+ * - `initCode` (`bytes`): concatenation of factory address and factoryData (or empty)
+ * - `callData` (`bytes`)
+ * - `accountGasLimits` (`bytes32`): concatenation of verificationGas (16 bytes) and callGas (16 bytes)
+ * - `preVerificationGas` (`uint256`)
+ * - `gasFees` (`bytes32`): concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes)
+ * - `paymasterAndData` (`bytes`): concatenation of paymaster fields (or empty)
+ * - `signature` (`bytes`)
+ */
+struct PackedUserOperation {
+    address sender;
+    uint256 nonce;
+    bytes initCode; // `abi.encodePacked(factory, factoryData)`
+    bytes callData;
+    bytes32 accountGasLimits; // `abi.encodePacked(verificationGasLimit, callGasLimit)` 16 bytes each
+    uint256 preVerificationGas;
+    bytes32 gasFees; // `abi.encodePacked(maxPriorityFee, maxFeePerGas)` 16 bytes each
+    bytes paymasterAndData; // `abi.encodePacked(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData)`
+    bytes signature;
+}
+
+/**
+ * @dev Aggregates and validates multiple signatures for a batch of user operations.
+ */
+interface IAggregator {
+    /**
+     * @dev Validates the signature for a user operation.
+     */
+    function validateUserOpSignature(
+        PackedUserOperation calldata userOp
+    ) external view returns (bytes memory sigForUserOp);
+
+    /**
+     * @dev Returns an aggregated signature for a batch of user operation's signatures.
+     */
+    function aggregateSignatures(
+        PackedUserOperation[] calldata userOps
+    ) external view returns (bytes memory aggregatesSignature);
+
+    /**
+     * @dev Validates that the aggregated signature is valid for the user operations.
+     *
+     * Requirements:
+     *
+     * - The aggregated signature MUST match the given list of operations.
+     */
+    function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view;
+}
+
+/**
+ * @dev Handle nonce management for accounts.
+ */
+interface IEntryPointNonces {
+    /**
+     * @dev Returns the nonce for a `sender` account and a `key`.
+     *
+     * Nonces for a certain `key` are always increasing.
+     */
+    function getNonce(address sender, uint192 key) external view returns (uint256 nonce);
+}
+
+/**
+ * @dev Handle stake management for accounts.
+ */
+interface IEntryPointStake {
+    /**
+     * @dev Returns the balance of the account.
+     */
+    function balanceOf(address account) external view returns (uint256);
+
+    /**
+     * @dev Deposits `msg.value` to the account.
+     */
+    function depositTo(address account) external payable;
+
+    /**
+     * @dev Withdraws `withdrawAmount` from the account to `withdrawAddress`.
+     */
+    function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external;
+
+    /**
+     * @dev Adds stake to the account with an unstake delay of `unstakeDelaySec`.
+     */
+    function addStake(uint32 unstakeDelaySec) external payable;
+
+    /**
+     * @dev Unlocks the stake of the account.
+     */
+    function unlockStake() external;
+
+    /**
+     * @dev Withdraws the stake of the account to `withdrawAddress`.
+     */
+    function withdrawStake(address payable withdrawAddress) external;
+}
+
+/**
+ * @dev Entry point for user operations.
+ */
+interface IEntryPoint is IEntryPointNonces, IEntryPointStake {
+    /**
+     * @dev A user operation at `opIndex` failed with `reason`.
+     */
+    error FailedOp(uint256 opIndex, string reason);
+
+    /**
+     * @dev A user operation at `opIndex` failed with `reason` and `inner` returned data.
+     */
+    error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner);
+
+    /**
+     * @dev Batch of aggregated user operations per aggregator.
+     */
+    struct UserOpsPerAggregator {
+        PackedUserOperation[] userOps;
+        IAggregator aggregator;
+        bytes signature;
+    }
+
+    /**
+     * @dev Executes a batch of user operations.
+     */
+    function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external;
+
+    /**
+     * @dev Executes a batch of aggregated user operations per aggregator.
+     */
+    function handleAggregatedOps(
+        UserOpsPerAggregator[] calldata opsPerAggregator,
+        address payable beneficiary
+    ) external;
+}
+
+/**
+ * @dev Base interface for an account.
+ */
+interface IAccount {
+    /**
+     * @dev Validates a user operation.
+     */
+    function validateUserOp(
+        PackedUserOperation calldata userOp,
+        bytes32 userOpHash,
+        uint256 missingAccountFunds
+    ) external returns (uint256 validationData);
+}
+
+/**
+ * @dev Support for executing user operations by prepending the {executeUserOp} function selector
+ * to the UserOperation's `callData`.
+ */
+interface IAccountExecute {
+    /**
+     * @dev Executes a user operation.
+     */
+    function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external;
+}
+
+/**
+ * @dev Interface for a paymaster contract that agrees to pay for the gas costs of a user operation.
+ *
+ * NOTE: A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction.
+ */
+interface IPaymaster {
+    enum PostOpMode {
+        opSucceeded,
+        opReverted,
+        postOpReverted
+    }
+
+    /**
+     * @dev Validates whether the paymaster is willing to pay for the user operation.
+     *
+     * NOTE: Bundlers will reject this method if it modifies the state, unless it's whitelisted.
+     */
+    function validatePaymasterUserOp(
+        PackedUserOperation calldata userOp,
+        bytes32 userOpHash,
+        uint256 maxCost
+    ) external returns (bytes memory context, uint256 validationData);
+
+    /**
+     * @dev Verifies the sender is the entrypoint.
+     */
+    function postOp(
+        PostOpMode mode,
+        bytes calldata context,
+        uint256 actualGasCost,
+        uint256 actualUserOpFeePerGas
+    ) external;
+}

+ 196 - 0
contracts/interfaces/draft-IERC7579.sol

@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {PackedUserOperation} from "./draft-IERC4337.sol";
+
+uint256 constant VALIDATION_SUCCESS = 0;
+uint256 constant VALIDATION_FAILED = 1;
+uint256 constant MODULE_TYPE_VALIDATOR = 1;
+uint256 constant MODULE_TYPE_EXECUTOR = 2;
+uint256 constant MODULE_TYPE_FALLBACK = 3;
+uint256 constant MODULE_TYPE_HOOK = 4;
+
+interface IERC7579Module {
+    /**
+     * @dev This function is called by the smart account during installation of the module
+     * @param data arbitrary data that may be required on the module during `onInstall` initialization
+     *
+     * MUST revert on error (e.g. if module is already enabled)
+     */
+    function onInstall(bytes calldata data) external;
+
+    /**
+     * @dev This function is called by the smart account during uninstallation of the module
+     * @param data arbitrary data that may be required on the module during `onUninstall` de-initialization
+     *
+     * MUST revert on error
+     */
+    function onUninstall(bytes calldata data) external;
+
+    /**
+     * @dev Returns boolean value if module is a certain type
+     * @param moduleTypeId the module type ID according the ERC-7579 spec
+     *
+     * MUST return true if the module is of the given type and false otherwise
+     */
+    function isModuleType(uint256 moduleTypeId) external view returns (bool);
+}
+
+interface IERC7579Validator is IERC7579Module {
+    /**
+     * @dev Validates a UserOperation
+     * @param userOp the ERC-4337 PackedUserOperation
+     * @param userOpHash the hash of the ERC-4337 PackedUserOperation
+     *
+     * MUST validate that the signature is a valid signature of the userOpHash
+     * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch
+     */
+    function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256);
+
+    /**
+     * @dev Validates a signature using ERC-1271
+     * @param sender the address that sent the ERC-1271 request to the smart account
+     * @param hash the hash of the ERC-1271 request
+     * @param signature the signature of the ERC-1271 request
+     *
+     * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid
+     * MUST NOT modify state
+     */
+    function isValidSignatureWithSender(
+        address sender,
+        bytes32 hash,
+        bytes calldata signature
+    ) external view returns (bytes4);
+}
+
+interface IERC7579Hook is IERC7579Module {
+    /**
+     * @dev Called by the smart account before execution
+     * @param msgSender the address that called the smart account
+     * @param value the value that was sent to the smart account
+     * @param msgData the data that was sent to the smart account
+     *
+     * MAY return arbitrary data in the `hookData` return value
+     */
+    function preCheck(
+        address msgSender,
+        uint256 value,
+        bytes calldata msgData
+    ) external returns (bytes memory hookData);
+
+    /**
+     * @dev Called by the smart account after execution
+     * @param hookData the data that was returned by the `preCheck` function
+     *
+     * MAY validate the `hookData` to validate transaction context of the `preCheck` function
+     */
+    function postCheck(bytes calldata hookData) external;
+}
+
+struct Execution {
+    address target;
+    uint256 value;
+    bytes callData;
+}
+
+interface IERC7579Execution {
+    /**
+     * @dev Executes a transaction on behalf of the account.
+     * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details
+     * @param executionCalldata The encoded execution call data
+     *
+     * MUST ensure adequate authorization control: e.g. onlyEntryPointOrSelf if used with ERC-4337
+     * If a mode is requested that is not supported by the Account, it MUST revert
+     */
+    function execute(bytes32 mode, bytes calldata executionCalldata) external;
+
+    /**
+     * @dev Executes a transaction on behalf of the account.
+     *         This function is intended to be called by Executor Modules
+     * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details
+     * @param executionCalldata The encoded execution call data
+     *
+     * MUST ensure adequate authorization control: i.e. onlyExecutorModule
+     * If a mode is requested that is not supported by the Account, it MUST revert
+     */
+    function executeFromExecutor(
+        bytes32 mode,
+        bytes calldata executionCalldata
+    ) external returns (bytes[] memory returnData);
+}
+
+interface IERC7579AccountConfig {
+    /**
+     * @dev Returns the account id of the smart account
+     * @return accountImplementationId the account id of the smart account
+     *
+     * MUST return a non-empty string
+     * The accountId SHOULD be structured like so:
+     *        "vendorname.accountname.semver"
+     * The id SHOULD be unique across all smart accounts
+     */
+    function accountId() external view returns (string memory accountImplementationId);
+
+    /**
+     * @dev Function to check if the account supports a certain execution mode (see above)
+     * @param encodedMode the encoded mode
+     *
+     * MUST return true if the account supports the mode and false otherwise
+     */
+    function supportsExecutionMode(bytes32 encodedMode) external view returns (bool);
+
+    /**
+     * @dev Function to check if the account supports a certain module typeId
+     * @param moduleTypeId the module type ID according to the ERC-7579 spec
+     *
+     * MUST return true if the account supports the module type and false otherwise
+     */
+    function supportsModule(uint256 moduleTypeId) external view returns (bool);
+}
+
+interface IERC7579ModuleConfig {
+    event ModuleInstalled(uint256 moduleTypeId, address module);
+    event ModuleUninstalled(uint256 moduleTypeId, address module);
+
+    /**
+     * @dev Installs a Module of a certain type on the smart account
+     * @param moduleTypeId the module type ID according to the ERC-7579 spec
+     * @param module the module address
+     * @param initData arbitrary data that may be required on the module during `onInstall`
+     * initialization.
+     *
+     * MUST implement authorization control
+     * MUST call `onInstall` on the module with the `initData` parameter if provided
+     * MUST emit ModuleInstalled event
+     * MUST revert if the module is already installed or the initialization on the module failed
+     */
+    function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external;
+
+    /**
+     * @dev Uninstalls a Module of a certain type on the smart account
+     * @param moduleTypeId the module type ID according the ERC-7579 spec
+     * @param module the module address
+     * @param deInitData arbitrary data that may be required on the module during `onInstall`
+     * initialization.
+     *
+     * MUST implement authorization control
+     * MUST call `onUninstall` on the module with the `deInitData` parameter if provided
+     * MUST emit ModuleUninstalled event
+     * MUST revert if the module is not installed or the deInitialization on the module failed
+     */
+    function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external;
+
+    /**
+     * @dev Returns whether a module is installed on the smart account
+     * @param moduleTypeId the module type ID according the ERC-7579 spec
+     * @param module the module address
+     * @param additionalContext arbitrary data that may be required to determine if the module is installed
+     *
+     * MUST return true if the module is installed and false otherwise
+     */
+    function isModuleInstalled(
+        uint256 moduleTypeId,
+        address module,
+        bytes calldata additionalContext
+    ) external view returns (bool);
+}

+ 2 - 0
contracts/mocks/Stateless.sol

@@ -25,6 +25,8 @@ import {ERC165} from "../utils/introspection/ERC165.sol";
 import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol";
 import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol";
 import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol";
+import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol";
+import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol";
 import {Heap} from "../utils/structs/Heap.sol";
 import {Math} from "../utils/math/Math.sol";
 import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";

+ 23 - 0
contracts/mocks/account/utils/ERC7579UtilsMock.sol

@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {CallType, ExecType, ModeSelector, ModePayload} from "../../../account/utils/draft-ERC7579Utils.sol";
+
+contract ERC7579UtilsGlobalMock {
+    function eqCallTypeGlobal(CallType callType1, CallType callType2) internal pure returns (bool) {
+        return callType1 == callType2;
+    }
+
+    function eqExecTypeGlobal(ExecType execType1, ExecType execType2) internal pure returns (bool) {
+        return execType1 == execType2;
+    }
+
+    function eqModeSelectorGlobal(ModeSelector modeSelector1, ModeSelector modeSelector2) internal pure returns (bool) {
+        return modeSelector1 == modeSelector2;
+    }
+
+    function eqModePayloadGlobal(ModePayload modePayload1, ModePayload modePayload2) internal pure returns (bool) {
+        return modePayload1 == modePayload2;
+    }
+}

+ 513 - 0
contracts/utils/Packing.sol

@@ -68,6 +68,38 @@ library Packing {
         }
     }
 
+    function pack_2_8(bytes2 left, bytes8 right) internal pure returns (bytes10 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(240, not(0)))
+            right := and(right, shl(192, not(0)))
+            result := or(left, shr(16, right))
+        }
+    }
+
+    function pack_2_10(bytes2 left, bytes10 right) internal pure returns (bytes12 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(240, not(0)))
+            right := and(right, shl(176, not(0)))
+            result := or(left, shr(16, right))
+        }
+    }
+
+    function pack_2_20(bytes2 left, bytes20 right) internal pure returns (bytes22 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(240, not(0)))
+            right := and(right, shl(96, not(0)))
+            result := or(left, shr(16, right))
+        }
+    }
+
+    function pack_2_22(bytes2 left, bytes22 right) internal pure returns (bytes24 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(240, not(0)))
+            right := and(right, shl(80, not(0)))
+            result := or(left, shr(16, right))
+        }
+    }
+
     function pack_4_2(bytes4 left, bytes2 right) internal pure returns (bytes6 result) {
         assembly ("memory-safe") {
             left := and(left, shl(224, not(0)))
@@ -84,6 +116,14 @@ library Packing {
         }
     }
 
+    function pack_4_6(bytes4 left, bytes6 right) internal pure returns (bytes10 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(208, not(0)))
+            result := or(left, shr(32, right))
+        }
+    }
+
     function pack_4_8(bytes4 left, bytes8 right) internal pure returns (bytes12 result) {
         assembly ("memory-safe") {
             left := and(left, shl(224, not(0)))
@@ -140,6 +180,14 @@ library Packing {
         }
     }
 
+    function pack_6_4(bytes6 left, bytes4 right) internal pure returns (bytes10 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(208, not(0)))
+            right := and(right, shl(224, not(0)))
+            result := or(left, shr(48, right))
+        }
+    }
+
     function pack_6_6(bytes6 left, bytes6 right) internal pure returns (bytes12 result) {
         assembly ("memory-safe") {
             left := and(left, shl(208, not(0)))
@@ -148,6 +196,38 @@ library Packing {
         }
     }
 
+    function pack_6_10(bytes6 left, bytes10 right) internal pure returns (bytes16 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(208, not(0)))
+            right := and(right, shl(176, not(0)))
+            result := or(left, shr(48, right))
+        }
+    }
+
+    function pack_6_16(bytes6 left, bytes16 right) internal pure returns (bytes22 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(208, not(0)))
+            right := and(right, shl(128, not(0)))
+            result := or(left, shr(48, right))
+        }
+    }
+
+    function pack_6_22(bytes6 left, bytes22 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(208, not(0)))
+            right := and(right, shl(80, not(0)))
+            result := or(left, shr(48, right))
+        }
+    }
+
+    function pack_8_2(bytes8 left, bytes2 right) internal pure returns (bytes10 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(192, not(0)))
+            right := and(right, shl(240, not(0)))
+            result := or(left, shr(64, right))
+        }
+    }
+
     function pack_8_4(bytes8 left, bytes4 right) internal pure returns (bytes12 result) {
         assembly ("memory-safe") {
             left := and(left, shl(192, not(0)))
@@ -196,6 +276,46 @@ library Packing {
         }
     }
 
+    function pack_10_2(bytes10 left, bytes2 right) internal pure returns (bytes12 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(176, not(0)))
+            right := and(right, shl(240, not(0)))
+            result := or(left, shr(80, right))
+        }
+    }
+
+    function pack_10_6(bytes10 left, bytes6 right) internal pure returns (bytes16 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(176, not(0)))
+            right := and(right, shl(208, not(0)))
+            result := or(left, shr(80, right))
+        }
+    }
+
+    function pack_10_10(bytes10 left, bytes10 right) internal pure returns (bytes20 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(176, not(0)))
+            right := and(right, shl(176, not(0)))
+            result := or(left, shr(80, right))
+        }
+    }
+
+    function pack_10_12(bytes10 left, bytes12 right) internal pure returns (bytes22 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(176, not(0)))
+            right := and(right, shl(160, not(0)))
+            result := or(left, shr(80, right))
+        }
+    }
+
+    function pack_10_22(bytes10 left, bytes22 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(176, not(0)))
+            right := and(right, shl(80, not(0)))
+            result := or(left, shr(80, right))
+        }
+    }
+
     function pack_12_4(bytes12 left, bytes4 right) internal pure returns (bytes16 result) {
         assembly ("memory-safe") {
             left := and(left, shl(160, not(0)))
@@ -212,6 +332,14 @@ library Packing {
         }
     }
 
+    function pack_12_10(bytes12 left, bytes10 right) internal pure returns (bytes22 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(160, not(0)))
+            right := and(right, shl(176, not(0)))
+            result := or(left, shr(96, right))
+        }
+    }
+
     function pack_12_12(bytes12 left, bytes12 right) internal pure returns (bytes24 result) {
         assembly ("memory-safe") {
             left := and(left, shl(160, not(0)))
@@ -244,6 +372,14 @@ library Packing {
         }
     }
 
+    function pack_16_6(bytes16 left, bytes6 right) internal pure returns (bytes22 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(128, not(0)))
+            right := and(right, shl(208, not(0)))
+            result := or(left, shr(128, right))
+        }
+    }
+
     function pack_16_8(bytes16 left, bytes8 right) internal pure returns (bytes24 result) {
         assembly ("memory-safe") {
             left := and(left, shl(128, not(0)))
@@ -268,6 +404,14 @@ library Packing {
         }
     }
 
+    function pack_20_2(bytes20 left, bytes2 right) internal pure returns (bytes22 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(96, not(0)))
+            right := and(right, shl(240, not(0)))
+            result := or(left, shr(160, right))
+        }
+    }
+
     function pack_20_4(bytes20 left, bytes4 right) internal pure returns (bytes24 result) {
         assembly ("memory-safe") {
             left := and(left, shl(96, not(0)))
@@ -292,6 +436,30 @@ library Packing {
         }
     }
 
+    function pack_22_2(bytes22 left, bytes2 right) internal pure returns (bytes24 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(80, not(0)))
+            right := and(right, shl(240, not(0)))
+            result := or(left, shr(176, right))
+        }
+    }
+
+    function pack_22_6(bytes22 left, bytes6 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(80, not(0)))
+            right := and(right, shl(208, not(0)))
+            result := or(left, shr(176, right))
+        }
+    }
+
+    function pack_22_10(bytes22 left, bytes10 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(80, not(0)))
+            right := and(right, shl(176, not(0)))
+            result := or(left, shr(176, right))
+        }
+    }
+
     function pack_24_4(bytes24 left, bytes4 right) internal pure returns (bytes28 result) {
         assembly ("memory-safe") {
             left := and(left, shl(64, not(0)))
@@ -466,6 +634,81 @@ library Packing {
         }
     }
 
+    function extract_10_1(bytes10 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 9) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_10_1(bytes10 self, bytes1 value, uint8 offset) internal pure returns (bytes10 result) {
+        bytes1 oldValue = extract_10_1(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(248, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_10_2(bytes10 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 8) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_10_2(bytes10 self, bytes2 value, uint8 offset) internal pure returns (bytes10 result) {
+        bytes2 oldValue = extract_10_2(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(240, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_10_4(bytes10 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 6) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_10_4(bytes10 self, bytes4 value, uint8 offset) internal pure returns (bytes10 result) {
+        bytes4 oldValue = extract_10_4(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(224, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_10_6(bytes10 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_10_6(bytes10 self, bytes6 value, uint8 offset) internal pure returns (bytes10 result) {
+        bytes6 oldValue = extract_10_6(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(208, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_10_8(bytes10 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 2) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_10_8(bytes10 self, bytes8 value, uint8 offset) internal pure returns (bytes10 result) {
+        bytes8 oldValue = extract_10_8(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(192, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_12_1(bytes12 self, uint8 offset) internal pure returns (bytes1 result) {
         if (offset > 11) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -541,6 +784,21 @@ library Packing {
         }
     }
 
+    function extract_12_10(bytes12 self, uint8 offset) internal pure returns (bytes10 result) {
+        if (offset > 2) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(176, not(0)))
+        }
+    }
+
+    function replace_12_10(bytes12 self, bytes10 value, uint8 offset) internal pure returns (bytes12 result) {
+        bytes10 oldValue = extract_12_10(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(176, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_16_1(bytes16 self, uint8 offset) internal pure returns (bytes1 result) {
         if (offset > 15) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -616,6 +874,21 @@ library Packing {
         }
     }
 
+    function extract_16_10(bytes16 self, uint8 offset) internal pure returns (bytes10 result) {
+        if (offset > 6) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(176, not(0)))
+        }
+    }
+
+    function replace_16_10(bytes16 self, bytes10 value, uint8 offset) internal pure returns (bytes16 result) {
+        bytes10 oldValue = extract_16_10(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(176, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_16_12(bytes16 self, uint8 offset) internal pure returns (bytes12 result) {
         if (offset > 4) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -706,6 +979,21 @@ library Packing {
         }
     }
 
+    function extract_20_10(bytes20 self, uint8 offset) internal pure returns (bytes10 result) {
+        if (offset > 10) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(176, not(0)))
+        }
+    }
+
+    function replace_20_10(bytes20 self, bytes10 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes10 oldValue = extract_20_10(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(176, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_20_12(bytes20 self, uint8 offset) internal pure returns (bytes12 result) {
         if (offset > 8) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -736,6 +1024,141 @@ library Packing {
         }
     }
 
+    function extract_22_1(bytes22 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 21) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_22_1(bytes22 self, bytes1 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes1 oldValue = extract_22_1(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(248, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_2(bytes22 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 20) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_22_2(bytes22 self, bytes2 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes2 oldValue = extract_22_2(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(240, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_4(bytes22 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 18) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_22_4(bytes22 self, bytes4 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes4 oldValue = extract_22_4(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(224, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_6(bytes22 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 16) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_22_6(bytes22 self, bytes6 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes6 oldValue = extract_22_6(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(208, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_8(bytes22 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 14) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_22_8(bytes22 self, bytes8 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes8 oldValue = extract_22_8(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(192, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_10(bytes22 self, uint8 offset) internal pure returns (bytes10 result) {
+        if (offset > 12) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(176, not(0)))
+        }
+    }
+
+    function replace_22_10(bytes22 self, bytes10 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes10 oldValue = extract_22_10(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(176, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_12(bytes22 self, uint8 offset) internal pure returns (bytes12 result) {
+        if (offset > 10) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(160, not(0)))
+        }
+    }
+
+    function replace_22_12(bytes22 self, bytes12 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes12 oldValue = extract_22_12(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(160, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_16(bytes22 self, uint8 offset) internal pure returns (bytes16 result) {
+        if (offset > 6) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(128, not(0)))
+        }
+    }
+
+    function replace_22_16(bytes22 self, bytes16 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes16 oldValue = extract_22_16(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(128, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_22_20(bytes22 self, uint8 offset) internal pure returns (bytes20 result) {
+        if (offset > 2) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(96, not(0)))
+        }
+    }
+
+    function replace_22_20(bytes22 self, bytes20 value, uint8 offset) internal pure returns (bytes22 result) {
+        bytes20 oldValue = extract_22_20(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(96, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_24_1(bytes24 self, uint8 offset) internal pure returns (bytes1 result) {
         if (offset > 23) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -811,6 +1234,21 @@ library Packing {
         }
     }
 
+    function extract_24_10(bytes24 self, uint8 offset) internal pure returns (bytes10 result) {
+        if (offset > 14) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(176, not(0)))
+        }
+    }
+
+    function replace_24_10(bytes24 self, bytes10 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes10 oldValue = extract_24_10(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(176, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_24_12(bytes24 self, uint8 offset) internal pure returns (bytes12 result) {
         if (offset > 12) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -856,6 +1294,21 @@ library Packing {
         }
     }
 
+    function extract_24_22(bytes24 self, uint8 offset) internal pure returns (bytes22 result) {
+        if (offset > 2) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(80, not(0)))
+        }
+    }
+
+    function replace_24_22(bytes24 self, bytes22 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes22 oldValue = extract_24_22(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(80, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_28_1(bytes28 self, uint8 offset) internal pure returns (bytes1 result) {
         if (offset > 27) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -931,6 +1384,21 @@ library Packing {
         }
     }
 
+    function extract_28_10(bytes28 self, uint8 offset) internal pure returns (bytes10 result) {
+        if (offset > 18) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(176, not(0)))
+        }
+    }
+
+    function replace_28_10(bytes28 self, bytes10 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes10 oldValue = extract_28_10(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(176, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_28_12(bytes28 self, uint8 offset) internal pure returns (bytes12 result) {
         if (offset > 16) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -976,6 +1444,21 @@ library Packing {
         }
     }
 
+    function extract_28_22(bytes28 self, uint8 offset) internal pure returns (bytes22 result) {
+        if (offset > 6) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(80, not(0)))
+        }
+    }
+
+    function replace_28_22(bytes28 self, bytes22 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes22 oldValue = extract_28_22(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(80, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_28_24(bytes28 self, uint8 offset) internal pure returns (bytes24 result) {
         if (offset > 4) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -1066,6 +1549,21 @@ library Packing {
         }
     }
 
+    function extract_32_10(bytes32 self, uint8 offset) internal pure returns (bytes10 result) {
+        if (offset > 22) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(176, not(0)))
+        }
+    }
+
+    function replace_32_10(bytes32 self, bytes10 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes10 oldValue = extract_32_10(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(176, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_32_12(bytes32 self, uint8 offset) internal pure returns (bytes12 result) {
         if (offset > 20) revert OutOfRangeAccess();
         assembly ("memory-safe") {
@@ -1111,6 +1609,21 @@ library Packing {
         }
     }
 
+    function extract_32_22(bytes32 self, uint8 offset) internal pure returns (bytes22 result) {
+        if (offset > 10) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(80, not(0)))
+        }
+    }
+
+    function replace_32_22(bytes32 self, bytes22 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes22 oldValue = extract_32_22(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(80, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
     function extract_32_24(bytes32 self, uint8 offset) internal pure returns (bytes24 result) {
         if (offset > 8) revert OutOfRangeAccess();
         assembly ("memory-safe") {

+ 1 - 1
scripts/generate/templates/Packing.opts.js

@@ -1,3 +1,3 @@
 module.exports = {
-  SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32],
+  SIZES: [1, 2, 4, 6, 8, 10, 12, 16, 20, 22, 24, 28, 32],
 };

+ 2 - 2
scripts/generate/templates/Packing.t.js

@@ -11,14 +11,14 @@ import {Packing} from "@openzeppelin/contracts/utils/Packing.sol";
 `;
 
 const testPack = (left, right) => `\
-function testPack(bytes${left} left, bytes${right} right) external {
+function testPack(bytes${left} left, bytes${right} right) external pure {
     assertEq(left, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${left}(0));
     assertEq(right, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${right}(${left}));
 }
 `;
 
 const testReplace = (outer, inner) => `\
-function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external {
+function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external pure {
     offset = uint8(bound(offset, 0, ${outer - inner}));
 
     bytes${inner} oldValue = container.extract_${outer}_${inner}(offset);

+ 1 - 1
slither.config.json

@@ -1,5 +1,5 @@
 {
     "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",
-    "filter_paths": "contracts/mocks,contracts-exposed",
+    "filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed",
     "compile_force_framework": "hardhat"
 }

+ 211 - 0
test/account/utils/draft-ERC4337Utils.test.js

@@ -0,0 +1,211 @@
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { packValidationData, packPaymasterData, UserOperation } = require('../../helpers/erc4337');
+const { MAX_UINT48 } = require('../../helpers/constants');
+
+const fixture = async () => {
+  const [authorizer, sender, entrypoint, paymaster] = await ethers.getSigners();
+  const utils = await ethers.deployContract('$ERC4337Utils');
+  return { utils, authorizer, sender, entrypoint, paymaster };
+};
+
+describe('ERC4337Utils', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  describe('parseValidationData', function () {
+    it('parses the validation data', async function () {
+      const authorizer = this.authorizer;
+      const validUntil = 0x12345678n;
+      const validAfter = 0x9abcdef0n;
+      const validationData = packValidationData(validAfter, validUntil, authorizer);
+
+      expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([
+        authorizer.address,
+        validAfter,
+        validUntil,
+      ]);
+    });
+
+    it('returns an type(uint48).max if until is 0', async function () {
+      const authorizer = this.authorizer;
+      const validAfter = 0x12345678n;
+      const validationData = packValidationData(validAfter, 0, authorizer);
+
+      expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([
+        authorizer.address,
+        validAfter,
+        MAX_UINT48,
+      ]);
+    });
+  });
+
+  describe('packValidationData', function () {
+    it('packs the validation data', async function () {
+      const authorizer = this.authorizer;
+      const validUntil = 0x12345678n;
+      const validAfter = 0x9abcdef0n;
+      const validationData = packValidationData(validAfter, validUntil, authorizer);
+
+      expect(
+        this.utils.$packValidationData(ethers.Typed.address(authorizer), validAfter, validUntil),
+      ).to.eventually.equal(validationData);
+    });
+
+    it('packs the validation data (bool)', async function () {
+      const success = false;
+      const validUntil = 0x12345678n;
+      const validAfter = 0x9abcdef0n;
+      const validationData = packValidationData(validAfter, validUntil, false);
+
+      expect(this.utils.$packValidationData(ethers.Typed.bool(success), validAfter, validUntil)).to.eventually.equal(
+        validationData,
+      );
+    });
+  });
+
+  describe('combineValidationData', function () {
+    const validUntil1 = 0x12345678n;
+    const validAfter1 = 0x9abcdef0n;
+    const validUntil2 = 0x87654321n;
+    const validAfter2 = 0xabcdef90n;
+
+    it('combines the validation data', async function () {
+      const validationData1 = packValidationData(validAfter1, validUntil1, ethers.ZeroAddress);
+      const validationData2 = packValidationData(validAfter2, validUntil2, ethers.ZeroAddress);
+      const expected = packValidationData(validAfter2, validUntil1, true);
+
+      // check symmetry
+      expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected);
+      expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected);
+    });
+
+    for (const [authorizer1, authorizer2] of [
+      [ethers.ZeroAddress, '0xbf023313b891fd6000544b79e353323aa94a4f29'],
+      ['0xbf023313b891fd6000544b79e353323aa94a4f29', ethers.ZeroAddress],
+    ]) {
+      it('returns SIG_VALIDATION_FAILURE if one of the authorizers is not address(0)', async function () {
+        const validationData1 = packValidationData(validAfter1, validUntil1, authorizer1);
+        const validationData2 = packValidationData(validAfter2, validUntil2, authorizer2);
+        const expected = packValidationData(validAfter2, validUntil1, false);
+
+        // check symmetry
+        expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected);
+        expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected);
+      });
+    }
+  });
+
+  describe('getValidationData', function () {
+    it('returns the validation data with valid validity range', async function () {
+      const aggregator = this.authorizer;
+      const validAfter = 0;
+      const validUntil = MAX_UINT48;
+      const validationData = packValidationData(validAfter, validUntil, aggregator);
+
+      expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, false]);
+    });
+
+    it('returns the validation data with invalid validity range (expired)', async function () {
+      const aggregator = this.authorizer;
+      const validAfter = 0;
+      const validUntil = 1;
+      const validationData = packValidationData(validAfter, validUntil, aggregator);
+
+      expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]);
+    });
+
+    it('returns the validation data with invalid validity range (not yet valid)', async function () {
+      const aggregator = this.authorizer;
+      const validAfter = MAX_UINT48;
+      const validUntil = MAX_UINT48;
+      const validationData = packValidationData(validAfter, validUntil, aggregator);
+
+      expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]);
+    });
+
+    it('returns address(0) and false for validationData = 0', function () {
+      expect(this.utils.$getValidationData(0n)).to.eventually.deep.equal([ethers.ZeroAddress, false]);
+    });
+  });
+
+  describe('hash', function () {
+    it('returns the user operation hash', async function () {
+      const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
+      const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId);
+
+      expect(this.utils.$hash(userOp.packed)).to.eventually.equal(userOp.hash(this.utils.target, chainId));
+    });
+
+    it('returns the operation hash with specified entrypoint and chainId', async function () {
+      const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
+      const chainId = 0xdeadbeef;
+
+      expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal(
+        userOp.hash(this.entrypoint, chainId),
+      );
+    });
+  });
+
+  describe('userOp values', function () {
+    it('returns verificationGasLimit', async function () {
+      const userOp = new UserOperation({ sender: this.sender, nonce: 1, verificationGas: 0x12345678n });
+      expect(this.utils.$verificationGasLimit(userOp.packed)).to.eventually.equal(userOp.verificationGas);
+    });
+
+    it('returns callGasLimit', async function () {
+      const userOp = new UserOperation({ sender: this.sender, nonce: 1, callGas: 0x12345678n });
+      expect(this.utils.$callGasLimit(userOp.packed)).to.eventually.equal(userOp.callGas);
+    });
+
+    it('returns maxPriorityFeePerGas', async function () {
+      const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxPriorityFee: 0x12345678n });
+      expect(this.utils.$maxPriorityFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee);
+    });
+
+    it('returns maxFeePerGas', async function () {
+      const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxFeePerGas: 0x12345678n });
+      expect(this.utils.$maxFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxFeePerGas);
+    });
+
+    it('returns gasPrice', async function () {
+      const userOp = new UserOperation({
+        sender: this.sender,
+        nonce: 1,
+        maxPriorityFee: 0x12345678n,
+        maxFeePerGas: 0x87654321n,
+      });
+      expect(this.utils.$gasPrice(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee);
+    });
+
+    describe('paymasterAndData', function () {
+      beforeEach(async function () {
+        this.verificationGasLimit = 0x12345678n;
+        this.postOpGasLimit = 0x87654321n;
+        this.paymasterAndData = packPaymasterData(this.paymaster, this.verificationGasLimit, this.postOpGasLimit);
+        this.userOp = new UserOperation({
+          sender: this.sender,
+          nonce: 1,
+          paymasterAndData: this.paymasterAndData,
+        });
+      });
+
+      it('returns paymaster', async function () {
+        expect(this.utils.$paymaster(this.userOp.packed)).to.eventually.equal(this.paymaster);
+      });
+
+      it('returns verificationGasLimit', async function () {
+        expect(this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.eventually.equal(
+          this.verificationGasLimit,
+        );
+      });
+
+      it('returns postOpGasLimit', async function () {
+        expect(this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.eventually.equal(this.postOpGasLimit);
+      });
+    });
+  });
+});

+ 354 - 0
test/account/utils/draft-ERC7579Utils.test.js

@@ -0,0 +1,354 @@
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers');
+const {
+  EXEC_TYPE_DEFAULT,
+  EXEC_TYPE_TRY,
+  encodeSingle,
+  encodeBatch,
+  encodeDelegate,
+  CALL_TYPE_CALL,
+  CALL_TYPE_BATCH,
+  encodeMode,
+} = require('../../helpers/erc7579');
+const { selector } = require('../../helpers/methods');
+
+const coder = ethers.AbiCoder.defaultAbiCoder();
+
+const fixture = async () => {
+  const [sender] = await ethers.getSigners();
+  const utils = await ethers.deployContract('$ERC7579Utils');
+  const utilsGlobal = await ethers.deployContract('$ERC7579UtilsGlobalMock');
+  const target = await ethers.deployContract('CallReceiverMock');
+  const anotherTarget = await ethers.deployContract('CallReceiverMock');
+  await setBalance(utils.target, ethers.parseEther('1'));
+  return { utils, utilsGlobal, target, anotherTarget, sender };
+};
+
+describe('ERC7579Utils', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  describe('execSingle', function () {
+    it('calls the target with value', async function () {
+      const value = 0x012;
+      const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
+
+      await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.emit(this.target, 'MockFunctionCalled');
+
+      expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
+    });
+
+    it('calls the target with value and args', async function () {
+      const value = 0x432;
+      const data = encodeSingle(
+        this.target,
+        value,
+        this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
+      );
+
+      await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data))
+        .to.emit(this.target, 'MockFunctionCalledWithArgs')
+        .withArgs(42, '0x1234');
+
+      expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
+    });
+
+    it('reverts when target reverts in default ExecType', async function () {
+      const value = 0x012;
+      const data = encodeSingle(
+        this.target,
+        value,
+        this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
+      );
+
+      await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting');
+    });
+
+    it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
+      const value = 0x012;
+      const data = encodeSingle(
+        this.target,
+        value,
+        this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
+      );
+
+      await expect(this.utils.$execSingle(EXEC_TYPE_TRY, data))
+        .to.emit(this.utils, 'ERC7579TryExecuteFail')
+        .withArgs(
+          CALL_TYPE_CALL,
+          ethers.solidityPacked(
+            ['bytes4', 'bytes'],
+            [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
+          ),
+        );
+    });
+
+    it('reverts with an invalid exec type', async function () {
+      const value = 0x012;
+      const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
+
+      await expect(this.utils.$execSingle('0x03', data))
+        .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
+        .withArgs('0x03');
+    });
+  });
+
+  describe('execBatch', function () {
+    it('calls the targets with value', async function () {
+      const value1 = 0x012;
+      const value2 = 0x234;
+      const data = encodeBatch(
+        [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
+        [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
+      );
+
+      await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data))
+        .to.emit(this.target, 'MockFunctionCalled')
+        .to.emit(this.anotherTarget, 'MockFunctionCalled');
+
+      expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
+      expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2);
+    });
+
+    it('calls the targets with value and args', async function () {
+      const value1 = 0x012;
+      const value2 = 0x234;
+      const data = encodeBatch(
+        [this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])],
+        [
+          this.anotherTarget,
+          value2,
+          this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
+        ],
+      );
+
+      await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data))
+        .to.emit(this.target, 'MockFunctionCalledWithArgs')
+        .to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
+
+      expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
+      expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2);
+    });
+
+    it('reverts when any target reverts in default ExecType', async function () {
+      const value1 = 0x012;
+      const value2 = 0x234;
+      const data = encodeBatch(
+        [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
+        [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
+      );
+
+      await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting');
+    });
+
+    it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () {
+      const value1 = 0x012;
+      const value2 = 0x234;
+      const data = encodeBatch(
+        [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
+        [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
+      );
+
+      await expect(this.utils.$execBatch(EXEC_TYPE_TRY, data))
+        .to.emit(this.utils, 'ERC7579TryExecuteFail')
+        .withArgs(
+          CALL_TYPE_BATCH,
+          ethers.solidityPacked(
+            ['bytes4', 'bytes'],
+            [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
+          ),
+        );
+
+      // Check balances
+      expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1);
+      expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(0);
+    });
+
+    it('reverts with an invalid exec type', async function () {
+      const value1 = 0x012;
+      const value2 = 0x234;
+      const data = encodeBatch(
+        [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')],
+        [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
+      );
+
+      await expect(this.utils.$execBatch('0x03', data))
+        .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
+        .withArgs('0x03');
+    });
+  });
+
+  describe('execDelegateCall', function () {
+    it('delegate calls the target', async function () {
+      const slot = ethers.hexlify(ethers.randomBytes(32));
+      const value = ethers.hexlify(ethers.randomBytes(32));
+      const data = encodeDelegate(
+        this.target,
+        this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]),
+      );
+
+      expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(ethers.ZeroHash);
+      await this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data);
+      expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(value);
+    });
+
+    it('reverts when target reverts in default ExecType', async function () {
+      const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
+      await expect(this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith(
+        'CallReceiverMock: reverting',
+      );
+    });
+
+    it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
+      const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
+      await expect(this.utils.$execDelegateCall(EXEC_TYPE_TRY, data))
+        .to.emit(this.utils, 'ERC7579TryExecuteFail')
+        .withArgs(
+          CALL_TYPE_CALL,
+          ethers.solidityPacked(
+            ['bytes4', 'bytes'],
+            [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
+          ),
+        );
+    });
+
+    it('reverts with an invalid exec type', async function () {
+      const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunction'));
+      await expect(this.utils.$execDelegateCall('0x03', data))
+        .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
+        .withArgs('0x03');
+    });
+  });
+
+  it('encodes Mode', async function () {
+    const callType = CALL_TYPE_BATCH;
+    const execType = EXEC_TYPE_TRY;
+    const selector = '0x12345678';
+    const payload = ethers.toBeHex(0, 22);
+
+    expect(this.utils.$encodeMode(callType, execType, selector, payload)).to.eventually.equal(
+      encodeMode({
+        callType,
+        execType,
+        selector,
+        payload,
+      }),
+    );
+  });
+
+  it('decodes Mode', async function () {
+    const callType = CALL_TYPE_BATCH;
+    const execType = EXEC_TYPE_TRY;
+    const selector = '0x12345678';
+    const payload = ethers.toBeHex(0, 22);
+
+    expect(
+      this.utils.$decodeMode(
+        encodeMode({
+          callType,
+          execType,
+          selector,
+          payload,
+        }),
+      ),
+    ).to.eventually.deep.equal([callType, execType, selector, payload]);
+  });
+
+  it('encodes single', async function () {
+    const target = this.target;
+    const value = 0x123;
+    const data = '0x12345678';
+
+    expect(this.utils.$encodeSingle(target, value, data)).to.eventually.equal(encodeSingle(target, value, data));
+  });
+
+  it('decodes single', async function () {
+    const target = this.target;
+    const value = 0x123;
+    const data = '0x12345678';
+
+    expect(this.utils.$decodeSingle(encodeSingle(target, value, data))).to.eventually.deep.equal([
+      target.target,
+      value,
+      data,
+    ]);
+  });
+
+  it('encodes batch', async function () {
+    const entries = [
+      [this.target, 0x123, '0x12345678'],
+      [this.anotherTarget, 0x456, '0x12345678'],
+    ];
+
+    expect(this.utils.$encodeBatch(entries)).to.eventually.equal(encodeBatch(...entries));
+  });
+
+  it('decodes batch', async function () {
+    const entries = [
+      [this.target.target, 0x123, '0x12345678'],
+      [this.anotherTarget.target, 0x456, '0x12345678'],
+    ];
+
+    expect(this.utils.$decodeBatch(encodeBatch(...entries))).to.eventually.deep.equal(entries);
+  });
+
+  it('encodes delegate', async function () {
+    const target = this.target;
+    const data = '0x12345678';
+
+    expect(this.utils.$encodeDelegate(target, data)).to.eventually.equal(encodeDelegate(target, data));
+  });
+
+  it('decodes delegate', async function () {
+    const target = this.target;
+    const data = '0x12345678';
+
+    expect(this.utils.$decodeDelegate(encodeDelegate(target, data))).to.eventually.deep.equal([target.target, data]);
+  });
+
+  describe('global', function () {
+    describe('eqCallTypeGlobal', function () {
+      it('returns true if both call types are equal', async function () {
+        expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_BATCH, CALL_TYPE_BATCH)).to.eventually.be.true;
+      });
+
+      it('returns false if both call types are different', async function () {
+        expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.eventually.be.false;
+      });
+    });
+
+    describe('eqExecTypeGlobal', function () {
+      it('returns true if both exec types are equal', async function () {
+        expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_TRY, EXEC_TYPE_TRY)).to.eventually.be.true;
+      });
+
+      it('returns false if both exec types are different', async function () {
+        expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.eventually.be.false;
+      });
+    });
+
+    describe('eqModeSelectorGlobal', function () {
+      it('returns true if both selectors are equal', async function () {
+        expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x12345678')).to.eventually.be.true;
+      });
+
+      it('returns false if both selectors are different', async function () {
+        expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.eventually.be.false;
+      });
+    });
+
+    describe('eqModePayloadGlobal', function () {
+      it('returns true if both payloads are equal', async function () {
+        expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(0, 22))).to.eventually.be
+          .true;
+      });
+
+      it('returns false if both payloads are different', async function () {
+        expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.eventually.be
+          .false;
+      });
+    });
+  });
+});

+ 95 - 0
test/helpers/erc4337.js

@@ -0,0 +1,95 @@
+const { ethers } = require('hardhat');
+
+const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
+const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
+
+function getAddress(account) {
+  return account.target ?? account.address ?? account;
+}
+
+function pack(left, right) {
+  return ethers.solidityPacked(['uint128', 'uint128'], [left, right]);
+}
+
+function packValidationData(validAfter, validUntil, authorizer) {
+  return ethers.solidityPacked(
+    ['uint48', 'uint48', 'address'],
+    [
+      validAfter,
+      validUntil,
+      typeof authorizer == 'boolean'
+        ? authorizer
+          ? SIG_VALIDATION_SUCCESS
+          : SIG_VALIDATION_FAILURE
+        : getAddress(authorizer),
+    ],
+  );
+}
+
+function packPaymasterData(paymaster, verificationGasLimit, postOpGasLimit) {
+  return ethers.solidityPacked(
+    ['address', 'uint128', 'uint128'],
+    [getAddress(paymaster), verificationGasLimit, postOpGasLimit],
+  );
+}
+
+/// Represent one user operation
+class UserOperation {
+  constructor(params) {
+    this.sender = getAddress(params.sender);
+    this.nonce = params.nonce;
+    this.initCode = params.initCode ?? '0x';
+    this.callData = params.callData ?? '0x';
+    this.verificationGas = params.verificationGas ?? 10_000_000n;
+    this.callGas = params.callGas ?? 100_000n;
+    this.preVerificationGas = params.preVerificationGas ?? 100_000n;
+    this.maxPriorityFee = params.maxPriorityFee ?? 100_000n;
+    this.maxFeePerGas = params.maxFeePerGas ?? 100_000n;
+    this.paymasterAndData = params.paymasterAndData ?? '0x';
+    this.signature = params.signature ?? '0x';
+  }
+
+  get packed() {
+    return {
+      sender: this.sender,
+      nonce: this.nonce,
+      initCode: this.initCode,
+      callData: this.callData,
+      accountGasLimits: pack(this.verificationGas, this.callGas),
+      preVerificationGas: this.preVerificationGas,
+      gasFees: pack(this.maxPriorityFee, this.maxFeePerGas),
+      paymasterAndData: this.paymasterAndData,
+      signature: this.signature,
+    };
+  }
+
+  hash(entrypoint, chainId) {
+    const p = this.packed;
+    const h = ethers.keccak256(
+      ethers.AbiCoder.defaultAbiCoder().encode(
+        ['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'],
+        [
+          p.sender,
+          p.nonce,
+          ethers.keccak256(p.initCode),
+          ethers.keccak256(p.callData),
+          p.accountGasLimits,
+          p.preVerificationGas,
+          p.gasFees,
+          ethers.keccak256(p.paymasterAndData),
+        ],
+      ),
+    );
+    return ethers.keccak256(
+      ethers.AbiCoder.defaultAbiCoder().encode(['bytes32', 'address', 'uint256'], [h, getAddress(entrypoint), chainId]),
+    );
+  }
+}
+
+module.exports = {
+  SIG_VALIDATION_SUCCESS,
+  SIG_VALIDATION_FAILURE,
+  packValidationData,
+  packPaymasterData,
+  UserOperation,
+};

+ 58 - 0
test/helpers/erc7579.js

@@ -0,0 +1,58 @@
+const { ethers } = require('hardhat');
+
+const MODULE_TYPE_VALIDATOR = 1;
+const MODULE_TYPE_EXECUTOR = 2;
+const MODULE_TYPE_FALLBACK = 3;
+const MODULE_TYPE_HOOK = 4;
+
+const EXEC_TYPE_DEFAULT = '0x00';
+const EXEC_TYPE_TRY = '0x01';
+
+const CALL_TYPE_CALL = '0x00';
+const CALL_TYPE_BATCH = '0x01';
+const CALL_TYPE_DELEGATE = '0xff';
+
+const encodeMode = ({
+  callType = '0x00',
+  execType = '0x00',
+  selector = '0x00000000',
+  payload = '0x00000000000000000000000000000000000000000000',
+} = {}) =>
+  ethers.solidityPacked(
+    ['bytes1', 'bytes1', 'bytes4', 'bytes4', 'bytes22'],
+    [callType, execType, '0x00000000', selector, payload],
+  );
+
+const encodeSingle = (target, value = 0n, data = '0x') =>
+  ethers.solidityPacked(['address', 'uint256', 'bytes'], [target.target ?? target.address ?? target, value, data]);
+
+const encodeBatch = (...entries) =>
+  ethers.AbiCoder.defaultAbiCoder().encode(
+    ['(address,uint256,bytes)[]'],
+    [
+      entries.map(entry =>
+        Array.isArray(entry)
+          ? [entry[0].target ?? entry[0].address ?? entry[0], entry[1] ?? 0n, entry[2] ?? '0x']
+          : [entry.target.target ?? entry.target.address ?? entry.target, entry.value ?? 0n, entry.data ?? '0x'],
+      ),
+    ],
+  );
+
+const encodeDelegate = (target, data = '0x') =>
+  ethers.solidityPacked(['address', 'bytes'], [target.target ?? target.address ?? target, data]);
+
+module.exports = {
+  MODULE_TYPE_VALIDATOR,
+  MODULE_TYPE_EXECUTOR,
+  MODULE_TYPE_FALLBACK,
+  MODULE_TYPE_HOOK,
+  EXEC_TYPE_DEFAULT,
+  EXEC_TYPE_TRY,
+  CALL_TYPE_CALL,
+  CALL_TYPE_BATCH,
+  CALL_TYPE_DELEGATE,
+  encodeMode,
+  encodeSingle,
+  encodeBatch,
+  encodeDelegate,
+};

File diff suppressed because it is too large
+ 401 - 89
test/utils/Packing.t.sol


Some files were not shown because too many files changed in this diff