Browse Source

Add Account framework (#5657)

Ernesto García 4 months ago
parent
commit
83d2a247be
38 changed files with 3082 additions and 42 deletions
  1. 5 0
      .changeset/clean-ways-push.md
  2. 5 0
      .changeset/funny-years-yawn.md
  3. 5 0
      .changeset/lazy-poets-cheer.md
  4. 5 0
      .changeset/rotten-apes-lie.md
  5. 5 0
      .changeset/strong-points-change.md
  6. 5 0
      .changeset/tame-bears-mix.md
  7. 144 0
      contracts/account/Account.sol
  8. 20 2
      contracts/account/README.adoc
  9. 404 0
      contracts/account/extensions/AccountERC7579.sol
  10. 106 0
      contracts/account/extensions/AccountERC7579Hooked.sol
  11. 69 0
      contracts/account/extensions/ERC7821.sol
  12. 43 0
      contracts/interfaces/IERC7821.sol
  13. 5 0
      contracts/mocks/CallReceiverMock.sol
  14. 138 0
      contracts/mocks/account/AccountMock.sol
  15. 115 0
      contracts/mocks/account/modules/ERC7579Mock.sol
  16. 13 13
      contracts/mocks/utils/cryptography/ERC7739Mock.sol
  17. 11 1
      contracts/utils/README.adoc
  18. 51 0
      contracts/utils/cryptography/SignerECDSA.sol
  19. 24 0
      contracts/utils/cryptography/SignerERC7702.sol
  20. 59 0
      contracts/utils/cryptography/SignerP256.sol
  21. 60 0
      contracts/utils/cryptography/SignerRSA.sol
  22. 30 0
      scripts/upgradeable/upgradeable.patch
  23. 144 0
      test/account/Account.behavior.js
  24. 48 0
      test/account/Account.test.js
  25. 52 0
      test/account/AccountECDSA.test.js
  26. 97 0
      test/account/AccountERC7702.t.sol
  27. 52 0
      test/account/AccountERC7702.test.js
  28. 58 0
      test/account/AccountP256.test.js
  29. 58 0
      test/account/AccountRSA.test.js
  30. 99 0
      test/account/examples/AccountERC7702WithModulesMock.test.js
  31. 563 0
      test/account/extensions/AccountERC7579.behavior.js
  32. 60 0
      test/account/extensions/AccountERC7579.test.js
  33. 60 0
      test/account/extensions/AccountERC7579Hooked.test.js
  34. 145 0
      test/account/extensions/ERC7821.behavior.js
  35. 27 25
      test/helpers/eip712-types.js
  36. 125 1
      test/helpers/erc4337.js
  37. 147 0
      test/helpers/signers.js
  38. 25 0
      test/utils/cryptography/ERC7739.test.js

+ 5 - 0
.changeset/clean-ways-push.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`AccountERC7579`: Extension of `Account` that implements support for ERC-7579 modules of type executor, validator, and fallback handler.

+ 5 - 0
.changeset/funny-years-yawn.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Account`: Added a simple ERC-4337 account implementation with minimal logic to process user operations.

+ 5 - 0
.changeset/lazy-poets-cheer.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`SignerERC7702`: Implementation of `AbstractSigner` for Externally Owned Accounts (EOAs). Useful with ERC-7702.

+ 5 - 0
.changeset/rotten-apes-lie.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`IERC7821`, `ERC7821`: Interface and logic for minimal batch execution. No support for additional `opData` is included.

+ 5 - 0
.changeset/strong-points-change.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`AccountERC7579Hooked`: Extension of `AccountERC7579` that implements support for ERC-7579 hook modules.

+ 5 - 0
.changeset/tame-bears-mix.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`AbstractSigner`, `SignerECDSA`, `SignerP256`, and `SignerRSA`: Add an abstract contract and various implementations for contracts that deal with signature verification.

+ 144 - 0
contracts/account/Account.sol

@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {PackedUserOperation, IAccount, IEntryPoint} from "../interfaces/draft-IERC4337.sol";
+import {ERC4337Utils} from "./utils/draft-ERC4337Utils.sol";
+import {AbstractSigner} from "../utils/cryptography/AbstractSigner.sol";
+
+/**
+ * @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process
+ * user operations.
+ *
+ * Developers must implement the {AbstractSigner-_rawSignatureValidation} function to define the account's validation logic.
+ *
+ * NOTE: This core account doesn't include any mechanism for performing arbitrary external calls. This is an essential
+ * feature that all Account should have. We leave it up to the developers to implement the mechanism of their choice.
+ * Common choices include ERC-6900, ERC-7579 and ERC-7821 (among others).
+ *
+ * IMPORTANT: Implementing a mechanism to validate signatures is a security-sensitive operation as it may allow an
+ * attacker to bypass the account's security measures. Check out {SignerECDSA}, {SignerP256}, or {SignerRSA} for
+ * digital signature validation implementations.
+ *
+ * @custom:stateless
+ */
+abstract contract Account is AbstractSigner, IAccount {
+    /**
+     * @dev Unauthorized call to the account.
+     */
+    error AccountUnauthorized(address sender);
+
+    /**
+     * @dev Revert if the caller is not the entry point or the account itself.
+     */
+    modifier onlyEntryPointOrSelf() {
+        _checkEntryPointOrSelf();
+        _;
+    }
+
+    /**
+     * @dev Revert if the caller is not the entry point.
+     */
+    modifier onlyEntryPoint() {
+        _checkEntryPoint();
+        _;
+    }
+
+    /**
+     * @dev Canonical entry point for the account that forwards and validates user operations.
+     */
+    function entryPoint() public view virtual returns (IEntryPoint) {
+        return ERC4337Utils.ENTRYPOINT_V08;
+    }
+
+    /**
+     * @dev Return the account nonce for the canonical sequence.
+     */
+    function getNonce() public view virtual returns (uint256) {
+        return getNonce(0);
+    }
+
+    /**
+     * @dev Return the account nonce for a given sequence (key).
+     */
+    function getNonce(uint192 key) public view virtual returns (uint256) {
+        return entryPoint().getNonce(address(this), key);
+    }
+
+    /**
+     * @inheritdoc IAccount
+     */
+    function validateUserOp(
+        PackedUserOperation calldata userOp,
+        bytes32 userOpHash,
+        uint256 missingAccountFunds
+    ) public virtual onlyEntryPoint returns (uint256) {
+        uint256 validationData = _validateUserOp(userOp, userOpHash);
+        _payPrefund(missingAccountFunds);
+        return validationData;
+    }
+
+    /**
+     * @dev Returns the validationData for a given user operation. By default, this checks the signature of the
+     * signable hash (produced by {_signableUserOpHash}) using the abstract signer ({AbstractSigner-_rawSignatureValidation}).
+     *
+     * NOTE: The userOpHash is assumed to be correct. Calling this function with a userOpHash that does not match the
+     * userOp will result in undefined behavior.
+     */
+    function _validateUserOp(
+        PackedUserOperation calldata userOp,
+        bytes32 userOpHash
+    ) internal virtual returns (uint256) {
+        return
+            _rawSignatureValidation(_signableUserOpHash(userOp, userOpHash), userOp.signature)
+                ? ERC4337Utils.SIG_VALIDATION_SUCCESS
+                : ERC4337Utils.SIG_VALIDATION_FAILED;
+    }
+
+    /**
+     * @dev Virtual function that returns the signable hash for a user operations. Since v0.8.0 of the entrypoint,
+     * `userOpHash` is an EIP-712 hash that can be signed directly.
+     */
+    function _signableUserOpHash(
+        PackedUserOperation calldata /*userOp*/,
+        bytes32 userOpHash
+    ) internal view virtual returns (bytes32) {
+        return userOpHash;
+    }
+
+    /**
+     * @dev Sends the missing funds for executing the user operation to the {entrypoint}.
+     * The `missingAccountFunds` must be defined by the entrypoint when calling {validateUserOp}.
+     */
+    function _payPrefund(uint256 missingAccountFunds) internal virtual {
+        if (missingAccountFunds > 0) {
+            (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
+            success; // Silence warning. The entrypoint should validate the result.
+        }
+    }
+
+    /**
+     * @dev Ensures the caller is the {entrypoint}.
+     */
+    function _checkEntryPoint() internal view virtual {
+        address sender = msg.sender;
+        if (sender != address(entryPoint())) {
+            revert AccountUnauthorized(sender);
+        }
+    }
+
+    /**
+     * @dev Ensures the caller is the {entrypoint} or the account itself.
+     */
+    function _checkEntryPointOrSelf() internal view virtual {
+        address sender = msg.sender;
+        if (sender != address(this) && sender != address(entryPoint())) {
+            revert AccountUnauthorized(sender);
+        }
+    }
+
+    /**
+     * @dev Receive Ether.
+     */
+    receive() external payable virtual {}
+}

+ 20 - 2
contracts/account/README.adoc

@@ -1,9 +1,27 @@
 = Account
 = Account
-
 [.readme-notice]
 [.readme-notice]
 NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account
 NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account
 
 
-This directory includes contracts to build accounts for ERC-4337.
+This directory includes contracts to build accounts for ERC-4337. These include:
+
+ * {Account}: An ERC-4337 smart account implementation that includes the core logic to process user operations.
+ * {AccountERC7579}: An extension of `Account` that implements support for ERC-7579 modules.
+ * {AccountERC7579Hooked}: An extension of `AccountERC7579` with support for a single hook module (type 4).
+ * {ERC7821}: Minimal batch executor implementation contracts. Useful to enable easy batch execution for smart contracts.
+ * {ERC4337Utils}: Utility functions for working with ERC-4337 user operations.
+ * {ERC7579Utils}: Utility functions for working with ERC-7579 modules and account modularity.
+
+== Core
+
+{{Account}}
+
+== Extensions
+
+{{AccountERC7579}}
+
+{{AccountERC7579Hooked}}
+
+{{ERC7821}}
 
 
 == Utilities
 == Utilities
 
 

+ 404 - 0
contracts/account/extensions/AccountERC7579.sol

@@ -0,0 +1,404 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.27;
+
+import {PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
+import {IERC1271} from "../../interfaces/IERC1271.sol";
+import {IERC7579Module, IERC7579Validator, IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig, MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_FALLBACK} from "../../interfaces/draft-IERC7579.sol";
+import {ERC7579Utils, Mode, CallType, ExecType} from "../../account/utils/draft-ERC7579Utils.sol";
+import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";
+import {Bytes} from "../../utils/Bytes.sol";
+import {Packing} from "../../utils/Packing.sol";
+import {Address} from "../../utils/Address.sol";
+import {Calldata} from "../../utils/Calldata.sol";
+import {Account} from "../Account.sol";
+
+/**
+ * @dev Extension of {Account} that implements support for ERC-7579 modules.
+ *
+ * To comply with the ERC-1271 support requirement, this contract defers signature validation to
+ * installed validator modules by calling {IERC7579Validator-isValidSignatureWithSender}.
+ *
+ * This contract does not implement validation logic for user operations since this functionality
+ * is often delegated to self-contained validation modules. Developers must install a validator module
+ * upon initialization (or any other mechanism to enable execution from the account):
+ *
+ * ```solidity
+ * contract MyAccountERC7579 is AccountERC7579, Initializable {
+ *   function initializeAccount(address validator, bytes calldata validatorData) public initializer {
+ *     _installModule(MODULE_TYPE_VALIDATOR, validator, validatorData);
+ *   }
+ * }
+ * ```
+ *
+ * [NOTE]
+ * ====
+ * * Hook support is not included. See {AccountERC7579Hooked} for a version that hooks to execution.
+ * * Validator selection, when verifying either ERC-1271 signature or ERC-4337 UserOperation is implemented in
+ *   internal virtual functions {_extractUserOpValidator} and {_extractSignatureValidator}. Both are implemented
+ *   following common practices. However, this part is not standardized in ERC-7579 (or in any follow-up ERC). Some
+ *   accounts may want to override these internal functions.
+ * * When combined with {ERC7739}, resolution ordering of {isValidSignature} may have an impact ({ERC7739} does not
+ *   call super). Manual resolution might be necessary.
+ * * Static calls (using callType `0xfe`) are currently NOT supported.
+ * ====
+ *
+ * WARNING: Removing all validator modules will render the account inoperable, as no user operations can be validated thereafter.
+ */
+abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig {
+    using Bytes for *;
+    using ERC7579Utils for *;
+    using EnumerableSet for *;
+    using Packing for bytes32;
+
+    EnumerableSet.AddressSet private _validators;
+    EnumerableSet.AddressSet private _executors;
+    mapping(bytes4 selector => address) private _fallbacks;
+
+    /// @dev The account's {fallback} was called with a selector that doesn't have an installed handler.
+    error ERC7579MissingFallbackHandler(bytes4 selector);
+
+    /// @dev Modifier that checks if the caller is an installed module of the given type.
+    modifier onlyModule(uint256 moduleTypeId, bytes calldata additionalContext) {
+        _checkModule(moduleTypeId, msg.sender, additionalContext);
+        _;
+    }
+
+    /// @dev See {_fallback}.
+    fallback(bytes calldata) external payable virtual returns (bytes memory) {
+        return _fallback();
+    }
+
+    /// @inheritdoc IERC7579AccountConfig
+    function accountId() public view virtual returns (string memory) {
+        // vendorname.accountname.semver
+        return "@openzeppelin/community-contracts.AccountERC7579.v0.0.0";
+    }
+
+    /**
+     * @inheritdoc IERC7579AccountConfig
+     *
+     * @dev Supported call types:
+     * * Single (`0x00`): A single transaction execution.
+     * * Batch (`0x01`): A batch of transactions execution.
+     * * Delegate (`0xff`): A delegate call execution.
+     *
+     * Supported exec types:
+     * * Default (`0x00`): Default execution type (revert on failure).
+     * * Try (`0x01`): Try execution type (emits ERC7579TryExecuteFail on failure).
+     */
+    function supportsExecutionMode(bytes32 encodedMode) public view virtual returns (bool) {
+        (CallType callType, ExecType execType, , ) = Mode.wrap(encodedMode).decodeMode();
+        return
+            (callType == ERC7579Utils.CALLTYPE_SINGLE ||
+                callType == ERC7579Utils.CALLTYPE_BATCH ||
+                callType == ERC7579Utils.CALLTYPE_DELEGATECALL) &&
+            (execType == ERC7579Utils.EXECTYPE_DEFAULT || execType == ERC7579Utils.EXECTYPE_TRY);
+    }
+
+    /**
+     * @inheritdoc IERC7579AccountConfig
+     *
+     * @dev Supported module types:
+     *
+     * * Validator: A module used during the validation phase to determine if a transaction is valid and
+     * should be executed on the account.
+     * * Executor: A module that can execute transactions on behalf of the smart account via a callback.
+     * * Fallback Handler: A module that can extend the fallback functionality of a smart account.
+     */
+    function supportsModule(uint256 moduleTypeId) public view virtual returns (bool) {
+        return
+            moduleTypeId == MODULE_TYPE_VALIDATOR ||
+            moduleTypeId == MODULE_TYPE_EXECUTOR ||
+            moduleTypeId == MODULE_TYPE_FALLBACK;
+    }
+
+    /// @inheritdoc IERC7579ModuleConfig
+    function installModule(
+        uint256 moduleTypeId,
+        address module,
+        bytes calldata initData
+    ) public virtual onlyEntryPointOrSelf {
+        _installModule(moduleTypeId, module, initData);
+    }
+
+    /// @inheritdoc IERC7579ModuleConfig
+    function uninstallModule(
+        uint256 moduleTypeId,
+        address module,
+        bytes calldata deInitData
+    ) public virtual onlyEntryPointOrSelf {
+        _uninstallModule(moduleTypeId, module, deInitData);
+    }
+
+    /// @inheritdoc IERC7579ModuleConfig
+    function isModuleInstalled(
+        uint256 moduleTypeId,
+        address module,
+        bytes calldata additionalContext
+    ) public view virtual returns (bool) {
+        if (moduleTypeId == MODULE_TYPE_VALIDATOR) return _validators.contains(module);
+        if (moduleTypeId == MODULE_TYPE_EXECUTOR) return _executors.contains(module);
+        if (moduleTypeId == MODULE_TYPE_FALLBACK) return _fallbacks[bytes4(additionalContext[0:4])] == module;
+        return false;
+    }
+
+    /// @inheritdoc IERC7579Execution
+    function execute(bytes32 mode, bytes calldata executionCalldata) public payable virtual onlyEntryPointOrSelf {
+        _execute(Mode.wrap(mode), executionCalldata);
+    }
+
+    /// @inheritdoc IERC7579Execution
+    function executeFromExecutor(
+        bytes32 mode,
+        bytes calldata executionCalldata
+    )
+        public
+        payable
+        virtual
+        onlyModule(MODULE_TYPE_EXECUTOR, Calldata.emptyBytes())
+        returns (bytes[] memory returnData)
+    {
+        return _execute(Mode.wrap(mode), executionCalldata);
+    }
+
+    /**
+     * @dev Implement ERC-1271 through IERC7579Validator modules. If module based validation fails, fallback to
+     * "native" validation by the abstract signer.
+     *
+     * NOTE: when combined with {ERC7739}, resolution ordering may have an impact ({ERC7739} does not call super).
+     * Manual resolution might be necessary.
+     */
+    function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
+        // check signature length is enough for extraction
+        if (signature.length >= 20) {
+            (address module, bytes calldata innerSignature) = _extractSignatureValidator(signature);
+            // if module is not installed, skip
+            if (isModuleInstalled(MODULE_TYPE_VALIDATOR, module, Calldata.emptyBytes())) {
+                // try validation, skip any revert
+                try IERC7579Validator(module).isValidSignatureWithSender(msg.sender, hash, innerSignature) returns (
+                    bytes4 magic
+                ) {
+                    return magic;
+                } catch {}
+            }
+        }
+        return bytes4(0xffffffff);
+    }
+
+    /**
+     * @dev Validates a user operation with {_signableUserOpHash} and returns the validation data
+     * if the module specified by the first 20 bytes of the nonce key is installed. Falls back to
+     * {Account-_validateUserOp} otherwise.
+     *
+     * See {_extractUserOpValidator} for the module extraction logic.
+     */
+    function _validateUserOp(
+        PackedUserOperation calldata userOp,
+        bytes32 userOpHash
+    ) internal virtual override returns (uint256) {
+        address module = _extractUserOpValidator(userOp);
+        return
+            isModuleInstalled(MODULE_TYPE_VALIDATOR, module, Calldata.emptyBytes())
+                ? IERC7579Validator(module).validateUserOp(userOp, _signableUserOpHash(userOp, userOpHash))
+                : super._validateUserOp(userOp, userOpHash);
+    }
+
+    /**
+     * @dev ERC-7579 execution logic. See {supportsExecutionMode} for supported modes.
+     *
+     * Reverts if the call type is not supported.
+     */
+    function _execute(
+        Mode mode,
+        bytes calldata executionCalldata
+    ) internal virtual returns (bytes[] memory returnData) {
+        (CallType callType, ExecType execType, , ) = mode.decodeMode();
+        if (callType == ERC7579Utils.CALLTYPE_SINGLE) return executionCalldata.execSingle(execType);
+        if (callType == ERC7579Utils.CALLTYPE_BATCH) return executionCalldata.execBatch(execType);
+        if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) return executionCalldata.execDelegateCall(execType);
+        revert ERC7579Utils.ERC7579UnsupportedCallType(callType);
+    }
+
+    /**
+     * @dev Installs a module of the given type with the given initialization data.
+     *
+     * For the fallback module type, the `initData` is expected to be the (packed) concatenation of a 4-byte
+     * selector and the rest of the data to be sent to the handler when calling {IERC7579Module-onInstall}.
+     *
+     * Requirements:
+     *
+     * * Module type must be supported. See {supportsModule}. Reverts with {ERC7579UnsupportedModuleType}.
+     * * Module must be of the given type. Reverts with {ERC7579MismatchedModuleTypeId}.
+     * * Module must not be already installed. Reverts with {ERC7579AlreadyInstalledModule}.
+     *
+     * Emits a {ModuleInstalled} event.
+     */
+    function _installModule(uint256 moduleTypeId, address module, bytes memory initData) internal virtual {
+        require(supportsModule(moduleTypeId), ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId));
+        require(
+            IERC7579Module(module).isModuleType(moduleTypeId),
+            ERC7579Utils.ERC7579MismatchedModuleTypeId(moduleTypeId, module)
+        );
+
+        if (moduleTypeId == MODULE_TYPE_VALIDATOR) {
+            require(_validators.add(module), ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module));
+        } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) {
+            require(_executors.add(module), ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module));
+        } else if (moduleTypeId == MODULE_TYPE_FALLBACK) {
+            bytes4 selector;
+            (selector, initData) = _decodeFallbackData(initData);
+            require(
+                _fallbacks[selector] == address(0),
+                ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module)
+            );
+            _fallbacks[selector] = module;
+        }
+
+        IERC7579Module(module).onInstall(initData);
+        emit ModuleInstalled(moduleTypeId, module);
+    }
+
+    /**
+     * @dev Uninstalls a module of the given type with the given de-initialization data.
+     *
+     * For the fallback module type, the `deInitData` is expected to be the (packed) concatenation of a 4-byte
+     * selector and the rest of the data to be sent to the handler when calling {IERC7579Module-onUninstall}.
+     *
+     * Requirements:
+     *
+     * * Module must be already installed. Reverts with {ERC7579UninstalledModule} otherwise.
+     */
+    function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual {
+        require(supportsModule(moduleTypeId), ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId));
+
+        if (moduleTypeId == MODULE_TYPE_VALIDATOR) {
+            require(_validators.remove(module), ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
+        } else if (moduleTypeId == MODULE_TYPE_EXECUTOR) {
+            require(_executors.remove(module), ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
+        } else if (moduleTypeId == MODULE_TYPE_FALLBACK) {
+            bytes4 selector;
+            (selector, deInitData) = _decodeFallbackData(deInitData);
+            require(
+                _fallbackHandler(selector) == module && module != address(0),
+                ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module)
+            );
+            delete _fallbacks[selector];
+        }
+
+        IERC7579Module(module).onUninstall(deInitData);
+        emit ModuleUninstalled(moduleTypeId, module);
+    }
+
+    /**
+     * @dev Fallback function that delegates the call to the installed handler for the given selector.
+     *
+     * Reverts with {ERC7579MissingFallbackHandler} if the handler is not installed.
+     *
+     * Calls the handler with the original `msg.sender` appended at the end of the calldata following
+     * the ERC-2771 format.
+     */
+    function _fallback() internal virtual returns (bytes memory) {
+        address handler = _fallbackHandler(msg.sig);
+        require(handler != address(0), ERC7579MissingFallbackHandler(msg.sig));
+
+        // From https://eips.ethereum.org/EIPS/eip-7579#fallback[ERC-7579 specifications]:
+        // - MUST utilize ERC-2771 to add the original msg.sender to the calldata sent to the fallback handler
+        // - MUST use call to invoke the fallback handler
+        (bool success, bytes memory returndata) = handler.call{value: msg.value}(
+            abi.encodePacked(msg.data, msg.sender)
+        );
+
+        if (success) return returndata;
+
+        assembly ("memory-safe") {
+            revert(add(returndata, 0x20), mload(returndata))
+        }
+    }
+
+    /// @dev Returns the fallback handler for the given selector. Returns `address(0)` if not installed.
+    function _fallbackHandler(bytes4 selector) internal view virtual returns (address) {
+        return _fallbacks[selector];
+    }
+
+    /// @dev Checks if the module is installed. Reverts if the module is not installed.
+    function _checkModule(
+        uint256 moduleTypeId,
+        address module,
+        bytes calldata additionalContext
+    ) internal view virtual {
+        require(
+            isModuleInstalled(moduleTypeId, module, additionalContext),
+            ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module)
+        );
+    }
+
+    /**
+     * @dev Extracts the nonce validator from the user operation.
+     *
+     * To construct a nonce key, set nonce as follows:
+     *
+     * ```
+     * <module address (20 bytes)> | <key (4 bytes)> | <nonce (8 bytes)>
+     * ```
+     * NOTE: The default behavior of this function replicates the behavior of
+     * https://github.com/rhinestonewtf/safe7579/blob/bb29e8b1a66658790c4169e72608e27d220f79be/src/Safe7579.sol#L266[Safe adapter],
+     * https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L227[Etherspot's Prime Account], and
+     * https://github.com/erc7579/erc7579-implementation/blob/16138d1afd4e9711f6c1425133538837bd7787b5/src/MSAAdvanced.sol#L247[ERC7579 reference implementation].
+     *
+     * This is not standardized in ERC-7579 (or in any follow-up ERC). Some accounts may want to override these internal functions.
+     *
+     * For example, https://github.com/bcnmy/nexus/blob/54f4e19baaff96081a8843672977caf712ef19f4/contracts/lib/NonceLib.sol#L17[Biconomy's Nexus]
+     * uses a similar yet incompatible approach (the validator address is also part of the nonce, but not at the same location)
+     */
+    function _extractUserOpValidator(PackedUserOperation calldata userOp) internal pure virtual returns (address) {
+        return address(bytes32(userOp.nonce).extract_32_20(0));
+    }
+
+    /**
+     * @dev Extracts the signature validator from the signature.
+     *
+     * To construct a signature, set the first 20 bytes as the module address and the remaining bytes as the
+     * signature data:
+     *
+     * ```
+     * <module address (20 bytes)> | <signature data>
+     * ```
+     *
+     * NOTE: The default behavior of this function replicates the behavior of
+     * https://github.com/rhinestonewtf/safe7579/blob/bb29e8b1a66658790c4169e72608e27d220f79be/src/Safe7579.sol#L350[Safe adapter],
+     * https://github.com/bcnmy/nexus/blob/54f4e19baaff96081a8843672977caf712ef19f4/contracts/Nexus.sol#L239[Biconomy's Nexus],
+     * https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L252[Etherspot's Prime Account], and
+     * https://github.com/erc7579/erc7579-implementation/blob/16138d1afd4e9711f6c1425133538837bd7787b5/src/MSAAdvanced.sol#L296[ERC7579 reference implementation].
+     *
+     * This is not standardized in ERC-7579 (or in any follow-up ERC). Some accounts may want to override these internal functions.
+     */
+    function _extractSignatureValidator(
+        bytes calldata signature
+    ) internal pure virtual returns (address module, bytes calldata innerSignature) {
+        return (address(bytes20(signature[0:20])), signature[20:]);
+    }
+
+    /**
+     * @dev Extract the function selector from initData/deInitData for MODULE_TYPE_FALLBACK
+     *
+     * NOTE: If we had calldata here, we could use calldata slice which are cheaper to manipulate and don't require
+     * actual copy. However, this would require `_installModule` to get a calldata bytes object instead of a memory
+     * bytes object. This would prevent calling `_installModule` from a contract constructor and would force the use
+     * of external initializers. That may change in the future, as most accounts will probably be deployed as
+     * clones/proxy/ERC-7702 delegates and therefore rely on initializers anyway.
+     */
+    function _decodeFallbackData(
+        bytes memory data
+    ) internal pure virtual returns (bytes4 selector, bytes memory remaining) {
+        return (bytes4(data), data.slice(4));
+    }
+
+    /// @dev By default, only use the modules for validation of userOp and signature. Disable raw signatures.
+    function _rawSignatureValidation(
+        bytes32 /*hash*/,
+        bytes calldata /*signature*/
+    ) internal view virtual override returns (bool) {
+        return false;
+    }
+}

+ 106 - 0
contracts/account/extensions/AccountERC7579Hooked.sol

@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.27;
+
+import {IERC7579Hook, MODULE_TYPE_HOOK} from "../../interfaces/draft-IERC7579.sol";
+import {ERC7579Utils, Mode} from "../../account/utils/draft-ERC7579Utils.sol";
+import {AccountERC7579} from "./AccountERC7579.sol";
+
+/**
+ * @dev Extension of {AccountERC7579} with support for a single hook module (type 4).
+ *
+ * If installed, this extension will call the hook module's {IERC7579Hook-preCheck} before executing any operation
+ * with {_execute} (including {execute} and {executeFromExecutor} by default) and {IERC7579Hook-postCheck} thereafter.
+ *
+ * NOTE: Hook modules break the check-effect-interaction pattern. In particular, the {IERC7579Hook-preCheck} hook can
+ * lead to potentially dangerous reentrancy. Using the `withHook()` modifier is safe if no effect is performed
+ * before the preHook or after the postHook. That is the case on all functions here, but it may not be the case if
+ * functions that have this modifier are overridden. Developers should be extremely careful when implementing hook
+ * modules or further overriding functions that involve hooks.
+ */
+abstract contract AccountERC7579Hooked is AccountERC7579 {
+    address private _hook;
+
+    /// @dev A hook module is already present. This contract only supports one hook module.
+    error ERC7579HookModuleAlreadyPresent(address hook);
+
+    /**
+     * @dev Calls {IERC7579Hook-preCheck} before executing the modified function and {IERC7579Hook-postCheck}
+     * thereafter.
+     */
+    modifier withHook() {
+        address hook_ = hook();
+        bytes memory hookData;
+
+        // slither-disable-next-line reentrancy-no-eth
+        if (hook_ != address(0)) hookData = IERC7579Hook(hook_).preCheck(msg.sender, msg.value, msg.data);
+        _;
+        if (hook_ != address(0)) IERC7579Hook(hook_).postCheck(hookData);
+    }
+
+    /// @inheritdoc AccountERC7579
+    function accountId() public view virtual override returns (string memory) {
+        // vendorname.accountname.semver
+        return "@openzeppelin/community-contracts.AccountERC7579Hooked.v0.0.0";
+    }
+
+    /// @dev Returns the hook module address if installed, or `address(0)` otherwise.
+    function hook() public view virtual returns (address) {
+        return _hook;
+    }
+
+    /// @dev Supports hook modules. See {AccountERC7579-supportsModule}
+    function supportsModule(uint256 moduleTypeId) public view virtual override returns (bool) {
+        return moduleTypeId == MODULE_TYPE_HOOK || super.supportsModule(moduleTypeId);
+    }
+
+    /// @inheritdoc AccountERC7579
+    function isModuleInstalled(
+        uint256 moduleTypeId,
+        address module,
+        bytes calldata data
+    ) public view virtual override returns (bool) {
+        return
+            (moduleTypeId == MODULE_TYPE_HOOK && module == hook()) ||
+            super.isModuleInstalled(moduleTypeId, module, data);
+    }
+
+    /// @dev Installs a module with support for hook modules. See {AccountERC7579-_installModule}
+    function _installModule(
+        uint256 moduleTypeId,
+        address module,
+        bytes memory initData
+    ) internal virtual override withHook {
+        if (moduleTypeId == MODULE_TYPE_HOOK) {
+            require(_hook == address(0), ERC7579HookModuleAlreadyPresent(_hook));
+            _hook = module;
+        }
+        super._installModule(moduleTypeId, module, initData);
+    }
+
+    /// @dev Uninstalls a module with support for hook modules. See {AccountERC7579-_uninstallModule}
+    function _uninstallModule(
+        uint256 moduleTypeId,
+        address module,
+        bytes memory deInitData
+    ) internal virtual override withHook {
+        if (moduleTypeId == MODULE_TYPE_HOOK) {
+            require(_hook == module, ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
+            _hook = address(0);
+        }
+        super._uninstallModule(moduleTypeId, module, deInitData);
+    }
+
+    /// @dev Hooked version of {AccountERC7579-_execute}.
+    function _execute(
+        Mode mode,
+        bytes calldata executionCalldata
+    ) internal virtual override withHook returns (bytes[] memory) {
+        return super._execute(mode, executionCalldata);
+    }
+
+    /// @dev Hooked version of {AccountERC7579-_fallback}.
+    function _fallback() internal virtual override withHook returns (bytes memory) {
+        return super._fallback();
+    }
+}

+ 69 - 0
contracts/account/extensions/ERC7821.sol

@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {ERC7579Utils, Mode, CallType, ExecType, ModeSelector} from "../utils/draft-ERC7579Utils.sol";
+import {IERC7821} from "../../interfaces/IERC7821.sol";
+import {Account} from "../Account.sol";
+
+/**
+ * @dev Minimal batch executor following ERC-7821.
+ *
+ * Only supports supports single batch mode (`0x01000000000000000000`). Does not support optional "opData".
+ *
+ * @custom:stateless
+ */
+abstract contract ERC7821 is IERC7821 {
+    using ERC7579Utils for *;
+
+    error UnsupportedExecutionMode();
+
+    /**
+     * @dev Executes the calls in `executionData` with no optional `opData` support.
+     *
+     * NOTE: Access to this function is controlled by {_erc7821AuthorizedExecutor}. Changing access permissions, for
+     * example to approve calls by the ERC-4337 entrypoint, should be implemented by overriding it.
+     *
+     * Reverts and bubbles up error if any call fails.
+     */
+    function execute(bytes32 mode, bytes calldata executionData) public payable virtual {
+        if (!_erc7821AuthorizedExecutor(msg.sender, mode, executionData))
+            revert Account.AccountUnauthorized(msg.sender);
+        if (!supportsExecutionMode(mode)) revert UnsupportedExecutionMode();
+        executionData.execBatch(ERC7579Utils.EXECTYPE_DEFAULT);
+    }
+
+    /// @inheritdoc IERC7821
+    function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) {
+        (CallType callType, ExecType execType, ModeSelector modeSelector, ) = Mode.wrap(mode).decodeMode();
+        return
+            callType == ERC7579Utils.CALLTYPE_BATCH &&
+            execType == ERC7579Utils.EXECTYPE_DEFAULT &&
+            modeSelector == ModeSelector.wrap(0x00000000);
+    }
+
+    /**
+     * @dev Access control mechanism for the {execute} function.
+     * By default, only the contract itself is allowed to execute.
+     *
+     * Override this function to implement custom access control, for example to allow the
+     * ERC-4337 entrypoint to execute.
+     *
+     * ```solidity
+     * function _erc7821AuthorizedExecutor(
+     *   address caller,
+     *   bytes32 mode,
+     *   bytes calldata executionData
+     * ) internal view virtual override returns (bool) {
+     *   return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
+     * }
+     * ```
+     */
+    function _erc7821AuthorizedExecutor(
+        address caller,
+        bytes32 /* mode */,
+        bytes calldata /* executionData */
+    ) internal view virtual returns (bool) {
+        return caller == address(this);
+    }
+}

+ 43 - 0
contracts/interfaces/IERC7821.sol

@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+/**
+ * @dev Interface for minimal batch executor.
+ */
+interface IERC7821 {
+    /**
+     * @dev Executes the calls in `executionData`.
+     * Reverts and bubbles up error if any call fails.
+     *
+     * `executionData` encoding:
+     * - If `opData` is empty, `executionData` is simply `abi.encode(calls)`.
+     * - Else, `executionData` is `abi.encode(calls, opData)`.
+     *   See: https://eips.ethereum.org/EIPS/eip-7579
+     *
+     * Supported modes:
+     * - `bytes32(0x01000000000000000000...)`: does not support optional `opData`.
+     * - `bytes32(0x01000000000078210001...)`: supports optional `opData`.
+     *
+     * Authorization checks:
+     * - If `opData` is empty, the implementation SHOULD require that
+     *   `msg.sender == address(this)`.
+     * - If `opData` is not empty, the implementation SHOULD use the signature
+     *   encoded in `opData` to determine if the caller can perform the execution.
+     *
+     * `opData` may be used to store additional data for authentication,
+     * paymaster data, gas limits, etc.
+     *
+     * For calldata compression efficiency, if a Call.to is `address(0)`,
+     * it will be replaced with `address(this)`.
+     */
+    function execute(bytes32 mode, bytes calldata executionData) external payable;
+
+    /**
+     * @dev This function is provided for frontends to detect support.
+     * Only returns true for:
+     * - `bytes32(0x01000000000000000000...)`: does not support optional `opData`.
+     * - `bytes32(0x01000000000078210001...)`: supports optional `opData`.
+     */
+    function supportsExecutionMode(bytes32 mode) external view returns (bool);
+}

+ 5 - 0
contracts/mocks/CallReceiverMock.sol

@@ -5,6 +5,7 @@ pragma solidity ^0.8.20;
 contract CallReceiverMock {
 contract CallReceiverMock {
     event MockFunctionCalled();
     event MockFunctionCalled();
     event MockFunctionCalledWithArgs(uint256 a, uint256 b);
     event MockFunctionCalledWithArgs(uint256 a, uint256 b);
+    event MockFunctionCalledExtra(address caller, uint256 value);
 
 
     uint256[] private _array;
     uint256[] private _array;
 
 
@@ -58,6 +59,10 @@ contract CallReceiverMock {
         }
         }
         return "0x1234";
         return "0x1234";
     }
     }
+
+    function mockFunctionExtra() public payable {
+        emit MockFunctionCalledExtra(msg.sender, msg.value);
+    }
 }
 }
 
 
 contract CallReceiverMockTrustingForwarder is CallReceiverMock {
 contract CallReceiverMockTrustingForwarder is CallReceiverMock {

+ 138 - 0
contracts/mocks/account/AccountMock.sol

@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.27;
+
+import {Account} from "../../account/Account.sol";
+import {AccountERC7579} from "../../account/extensions/AccountERC7579.sol";
+import {AccountERC7579Hooked} from "../../account/extensions/AccountERC7579Hooked.sol";
+import {ERC721Holder} from "../../token/ERC721/utils/ERC721Holder.sol";
+import {ERC1155Holder} from "../../token/ERC1155/utils/ERC1155Holder.sol";
+import {ERC4337Utils} from "../../account/utils/draft-ERC4337Utils.sol";
+import {ERC7739} from "../../utils/cryptography/ERC7739.sol";
+import {ERC7821} from "../../account/extensions/ERC7821.sol";
+import {MODULE_TYPE_VALIDATOR} from "../../interfaces/draft-IERC7579.sol";
+import {PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
+import {AbstractSigner} from "../../utils/cryptography/AbstractSigner.sol";
+import {SignerECDSA} from "../../utils/cryptography/SignerECDSA.sol";
+import {SignerP256} from "../../utils/cryptography/SignerP256.sol";
+import {SignerRSA} from "../../utils/cryptography/SignerRSA.sol";
+import {SignerERC7702} from "../../utils/cryptography/SignerERC7702.sol";
+
+abstract contract AccountMock is Account, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
+    /// Validates a user operation with a boolean signature.
+    function _rawSignatureValidation(bytes32 hash, bytes calldata signature) internal pure override returns (bool) {
+        return signature.length >= 32 && bytes32(signature) == hash;
+    }
+
+    /// @inheritdoc ERC7821
+    function _erc7821AuthorizedExecutor(
+        address caller,
+        bytes32 mode,
+        bytes calldata executionData
+    ) internal view virtual override returns (bool) {
+        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
+    }
+}
+
+abstract contract AccountECDSAMock is Account, SignerECDSA, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
+    constructor(address signerAddr) {
+        _setSigner(signerAddr);
+    }
+
+    /// @inheritdoc ERC7821
+    function _erc7821AuthorizedExecutor(
+        address caller,
+        bytes32 mode,
+        bytes calldata executionData
+    ) internal view virtual override returns (bool) {
+        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
+    }
+}
+
+abstract contract AccountP256Mock is Account, SignerP256, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
+    constructor(bytes32 qx, bytes32 qy) {
+        _setSigner(qx, qy);
+    }
+
+    /// @inheritdoc ERC7821
+    function _erc7821AuthorizedExecutor(
+        address caller,
+        bytes32 mode,
+        bytes calldata executionData
+    ) internal view virtual override returns (bool) {
+        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
+    }
+}
+
+abstract contract AccountRSAMock is Account, SignerRSA, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
+    constructor(bytes memory e, bytes memory n) {
+        _setSigner(e, n);
+    }
+
+    /// @inheritdoc ERC7821
+    function _erc7821AuthorizedExecutor(
+        address caller,
+        bytes32 mode,
+        bytes calldata executionData
+    ) internal view virtual override returns (bool) {
+        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
+    }
+}
+
+abstract contract AccountERC7702Mock is Account, SignerERC7702, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
+    /// @inheritdoc ERC7821
+    function _erc7821AuthorizedExecutor(
+        address caller,
+        bytes32 mode,
+        bytes calldata executionData
+    ) internal view virtual override returns (bool) {
+        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
+    }
+}
+
+abstract contract AccountERC7702WithModulesMock is
+    Account,
+    AccountERC7579,
+    SignerERC7702,
+    ERC7739,
+    ERC721Holder,
+    ERC1155Holder
+{
+    function _validateUserOp(
+        PackedUserOperation calldata userOp,
+        bytes32 userOpHash
+    ) internal virtual override(Account, AccountERC7579) returns (uint256) {
+        return super._validateUserOp(userOp, userOpHash);
+    }
+
+    /// @dev Resolve implementation of ERC-1271 by both ERC7739 and AccountERC7579 to support both schemes.
+    function isValidSignature(
+        bytes32 hash,
+        bytes calldata signature
+    ) public view virtual override(ERC7739, AccountERC7579) returns (bytes4) {
+        // ERC-7739 can return the fn selector (success), 0xffffffff (invalid) or 0x77390001 (detection).
+        // If the return is 0xffffffff, we fallback to validation using ERC-7579 modules.
+        bytes4 erc7739magic = ERC7739.isValidSignature(hash, signature);
+        return erc7739magic == bytes4(0xffffffff) ? AccountERC7579.isValidSignature(hash, signature) : erc7739magic;
+    }
+
+    /// @dev Enable signature using the ERC-7702 signer.
+    function _rawSignatureValidation(
+        bytes32 hash,
+        bytes calldata signature
+    ) internal view virtual override(AbstractSigner, AccountERC7579, SignerERC7702) returns (bool) {
+        return SignerERC7702._rawSignatureValidation(hash, signature);
+    }
+}
+
+abstract contract AccountERC7579Mock is AccountERC7579 {
+    constructor(address validator, bytes memory initData) {
+        _installModule(MODULE_TYPE_VALIDATOR, validator, initData);
+    }
+}
+
+abstract contract AccountERC7579HookedMock is AccountERC7579Hooked {
+    constructor(address validator, bytes memory initData) {
+        _installModule(MODULE_TYPE_VALIDATOR, validator, initData);
+    }
+}

+ 115 - 0
contracts/mocks/account/modules/ERC7579Mock.sol

@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {MODULE_TYPE_HOOK, MODULE_TYPE_FALLBACK, MODULE_TYPE_VALIDATOR, IERC7579Hook, IERC7579Module, IERC7579Validator} from "../../../interfaces/draft-IERC7579.sol";
+import {SignatureChecker} from "../../../utils/cryptography/SignatureChecker.sol";
+import {PackedUserOperation} from "../../../interfaces/draft-IERC4337.sol";
+import {IERC1271} from "../../../interfaces/IERC1271.sol";
+import {ERC4337Utils} from "../../../account/utils/draft-ERC4337Utils.sol";
+
+abstract contract ERC7579ModuleMock is IERC7579Module {
+    uint256 private _moduleTypeId;
+
+    event ModuleInstalledReceived(address account, bytes data);
+    event ModuleUninstalledReceived(address account, bytes data);
+
+    constructor(uint256 moduleTypeId) {
+        _moduleTypeId = moduleTypeId;
+    }
+
+    function onInstall(bytes calldata data) public virtual {
+        emit ModuleInstalledReceived(msg.sender, data);
+    }
+
+    function onUninstall(bytes calldata data) public virtual {
+        emit ModuleUninstalledReceived(msg.sender, data);
+    }
+
+    function isModuleType(uint256 moduleTypeId) external view returns (bool) {
+        return moduleTypeId == _moduleTypeId;
+    }
+}
+
+abstract contract ERC7579HookMock is ERC7579ModuleMock(MODULE_TYPE_HOOK), IERC7579Hook {
+    event PreCheck(address sender, uint256 value, bytes data);
+    event PostCheck(bytes hookData);
+
+    function preCheck(
+        address msgSender,
+        uint256 value,
+        bytes calldata msgData
+    ) external returns (bytes memory hookData) {
+        emit PreCheck(msgSender, value, msgData);
+        return msgData;
+    }
+
+    function postCheck(bytes calldata hookData) external {
+        emit PostCheck(hookData);
+    }
+}
+
+abstract contract ERC7579FallbackHandlerMock is ERC7579ModuleMock(MODULE_TYPE_FALLBACK) {
+    event ERC7579FallbackHandlerMockCalled(address account, address sender, uint256 value, bytes data);
+
+    error ERC7579FallbackHandlerMockRevert();
+
+    function _msgAccount() internal view returns (address) {
+        return msg.sender;
+    }
+
+    function _msgSender() internal pure returns (address) {
+        return address(bytes20(msg.data[msg.data.length - 20:]));
+    }
+
+    function _msgData() internal pure returns (bytes calldata) {
+        return msg.data[:msg.data.length - 20];
+    }
+
+    function callPayable() public payable {
+        emit ERC7579FallbackHandlerMockCalled(_msgAccount(), _msgSender(), msg.value, _msgData());
+    }
+
+    function callView() public view returns (address, address) {
+        return (_msgAccount(), _msgSender());
+    }
+
+    function callRevert() public pure {
+        revert ERC7579FallbackHandlerMockRevert();
+    }
+}
+
+abstract contract ERC7579ValidatorMock is ERC7579ModuleMock(MODULE_TYPE_VALIDATOR), IERC7579Validator {
+    mapping(address sender => address signer) private _associatedSigners;
+
+    function onInstall(bytes calldata data) public virtual override(IERC7579Module, ERC7579ModuleMock) {
+        _associatedSigners[msg.sender] = address(bytes20(data[0:20]));
+        super.onInstall(data);
+    }
+
+    function onUninstall(bytes calldata data) public virtual override(IERC7579Module, ERC7579ModuleMock) {
+        delete _associatedSigners[msg.sender];
+        super.onUninstall(data);
+    }
+
+    function validateUserOp(
+        PackedUserOperation calldata userOp,
+        bytes32 userOpHash
+    ) public view virtual returns (uint256) {
+        return
+            SignatureChecker.isValidSignatureNow(_associatedSigners[msg.sender], userOpHash, userOp.signature)
+                ? ERC4337Utils.SIG_VALIDATION_SUCCESS
+                : ERC4337Utils.SIG_VALIDATION_FAILED;
+    }
+
+    function isValidSignatureWithSender(
+        address /*sender*/,
+        bytes32 hash,
+        bytes calldata signature
+    ) public view virtual returns (bytes4) {
+        return
+            SignatureChecker.isValidSignatureNow(_associatedSigners[msg.sender], hash, signature)
+                ? IERC1271.isValidSignature.selector
+                : bytes4(0xffffffff);
+    }
+}

+ 13 - 13
contracts/mocks/utils/cryptography/ERC7739Mock.sol

@@ -5,24 +5,24 @@ pragma solidity ^0.8.20;
 import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
 import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
 import {EIP712} from "../../../utils/cryptography/EIP712.sol";
 import {EIP712} from "../../../utils/cryptography/EIP712.sol";
 import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
 import {ERC7739} from "../../../utils/cryptography/ERC7739.sol";
-import {AbstractSigner} from "../../../utils/cryptography/AbstractSigner.sol";
-
-contract ERC7739ECDSAMock is AbstractSigner, ERC7739 {
-    address private _signer;
+import {SignerECDSA} from "../../../utils/cryptography/SignerECDSA.sol";
+import {SignerP256} from "../../../utils/cryptography/SignerP256.sol";
+import {SignerRSA} from "../../../utils/cryptography/SignerRSA.sol";
 
 
+contract ERC7739ECDSAMock is ERC7739, SignerECDSA {
     constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
     constructor(address signerAddr) EIP712("ERC7739ECDSA", "1") {
-        _signer = signerAddr;
+        _setSigner(signerAddr);
     }
     }
+}
 
 
-    function signer() public view virtual returns (address) {
-        return _signer;
+contract ERC7739P256Mock is ERC7739, SignerP256 {
+    constructor(bytes32 qx, bytes32 qy) EIP712("ERC7739P256", "1") {
+        _setSigner(qx, qy);
     }
     }
+}
 
 
-    function _rawSignatureValidation(
-        bytes32 hash,
-        bytes calldata signature
-    ) internal view virtual override returns (bool) {
-        (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
-        return signer() == recovered && err == ECDSA.RecoverError.NoError;
+contract ERC7739RSAMock is ERC7739, SignerRSA {
+    constructor(bytes memory e, bytes memory n) EIP712("ERC7739RSA", "1") {
+        _setSigner(e, n);
     }
     }
 }
 }

+ 11 - 1
contracts/utils/README.adoc

@@ -46,10 +46,12 @@ Miscellaneous contracts and libraries containing utility functions you can use t
  * {Comparators}: A library that contains comparator functions to use with the {Heap} library.
  * {Comparators}: A library that contains comparator functions to use with the {Heap} library.
  * {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
  * {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
  * {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality.
  * {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality.
+ * {Time}: A library that provides helpers for manipulating time-related objects, including a `Delay` type.
  * {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
  * {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
  * {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
  * {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
  * {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
  * {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
- * {Time}: A library that provides helpers for manipulating time-related objects, including a `Delay` type.
+ * {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
+ * {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
  
  
 [NOTE]
 [NOTE]
 ====
 ====
@@ -90,6 +92,14 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable
 
 
 {{AbstractSigner}}
 {{AbstractSigner}}
 
 
+{{SignerECDSA}}
+
+{{SignerP256}}
+
+{{SignerERC7702}}
+
+{{SignerRSA}}
+
 == Security
 == Security
 
 
 {{ReentrancyGuard}}
 {{ReentrancyGuard}}

+ 51 - 0
contracts/utils/cryptography/SignerECDSA.sol

@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {ECDSA} from "../cryptography/ECDSA.sol";
+import {AbstractSigner} from "./AbstractSigner.sol";
+
+/**
+ * @dev Implementation of {AbstractSigner} using xref:api:utils#ECDSA[ECDSA] signatures.
+ *
+ * For {Account} usage, a {_setSigner} function is provided to set the {signer} address.
+ * Doing so is easier for a factory, who is likely to use initializable clones of this contract.
+ *
+ * Example of usage:
+ *
+ * ```solidity
+ * contract MyAccountECDSA is Account, SignerECDSA, Initializable {
+ *     function initialize(address signerAddr) public initializer {
+ *       _setSigner(signerAddr);
+ *     }
+ * }
+ * ```
+ *
+ * IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
+ * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
+ */
+abstract contract SignerECDSA is AbstractSigner {
+    address private _signer;
+
+    /**
+     * @dev Sets the signer with the address of the native signer. This function should be called during construction
+     * or through an initializer.
+     */
+    function _setSigner(address signerAddr) internal {
+        _signer = signerAddr;
+    }
+
+    /// @dev Return the signer's address.
+    function signer() public view virtual returns (address) {
+        return _signer;
+    }
+
+    /// @inheritdoc AbstractSigner
+    function _rawSignatureValidation(
+        bytes32 hash,
+        bytes calldata signature
+    ) internal view virtual override returns (bool) {
+        (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
+        return signer() == recovered && err == ECDSA.RecoverError.NoError;
+    }
+}

+ 24 - 0
contracts/utils/cryptography/SignerERC7702.sol

@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {ECDSA} from "./ECDSA.sol";
+import {AbstractSigner} from "./AbstractSigner.sol";
+
+/**
+ * @dev Implementation of {AbstractSigner} for implementation for an EOA. Useful for ERC-7702 accounts.
+ *
+ * @custom:stateless
+ */
+abstract contract SignerERC7702 is AbstractSigner {
+    /**
+     * @dev Validates the signature using the EOA's address (i.e. `address(this)`).
+     */
+    function _rawSignatureValidation(
+        bytes32 hash,
+        bytes calldata signature
+    ) internal view virtual override returns (bool) {
+        (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
+        return address(this) == recovered && err == ECDSA.RecoverError.NoError;
+    }
+}

+ 59 - 0
contracts/utils/cryptography/SignerP256.sol

@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {P256} from "./P256.sol";
+import {AbstractSigner} from "./AbstractSigner.sol";
+
+/**
+ * @dev Implementation of {AbstractSigner} using xref:api:utils#P256[P256] signatures.
+ *
+ * For {Account} usage, a {_setSigner} function is provided to set the {signer} public key.
+ * Doing so is easier for a factory, who is likely to use initializable clones of this contract.
+ *
+ * Example of usage:
+ *
+ * ```solidity
+ * contract MyAccountP256 is Account, SignerP256, Initializable {
+ *     function initialize(bytes32 qx, bytes32 qy) public initializer {
+ *       _setSigner(qx, qy);
+ *     }
+ * }
+ * ```
+ *
+ * IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
+ * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
+ */
+abstract contract SignerP256 is AbstractSigner {
+    bytes32 private _qx;
+    bytes32 private _qy;
+
+    error SignerP256InvalidPublicKey(bytes32 qx, bytes32 qy);
+
+    /**
+     * @dev Sets the signer with a P256 public key. This function should be called during construction
+     * or through an initializer.
+     */
+    function _setSigner(bytes32 qx, bytes32 qy) internal {
+        if (!P256.isValidPublicKey(qx, qy)) revert SignerP256InvalidPublicKey(qx, qy);
+        _qx = qx;
+        _qy = qy;
+    }
+
+    /// @dev Return the signer's P256 public key.
+    function signer() public view virtual returns (bytes32 qx, bytes32 qy) {
+        return (_qx, _qy);
+    }
+
+    /// @inheritdoc AbstractSigner
+    function _rawSignatureValidation(
+        bytes32 hash,
+        bytes calldata signature
+    ) internal view virtual override returns (bool) {
+        if (signature.length < 0x40) return false;
+        bytes32 r = bytes32(signature[0x00:0x20]);
+        bytes32 s = bytes32(signature[0x20:0x40]);
+        (bytes32 qx, bytes32 qy) = signer();
+        return P256.verify(hash, r, s, qx, qy);
+    }
+}

+ 60 - 0
contracts/utils/cryptography/SignerRSA.sol

@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {RSA} from "./RSA.sol";
+import {AbstractSigner} from "./AbstractSigner.sol";
+
+/**
+ * @dev Implementation of {AbstractSigner} using xref:api:utils#RSA[RSA] signatures.
+ *
+ * For {Account} usage, a {_setSigner} function is provided to set the {signer} public key.
+ * Doing so is easier for a factory, who is likely to use initializable clones of this contract.
+ *
+ * Example of usage:
+ *
+ * ```solidity
+ * contract MyAccountRSA is Account, SignerRSA, Initializable {
+ *     function initialize(bytes memory e, bytes memory n) public initializer {
+ *       _setSigner(e, n);
+ *     }
+ * }
+ * ```
+ *
+ * IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
+ * or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
+ */
+abstract contract SignerRSA is AbstractSigner {
+    bytes private _e;
+    bytes private _n;
+
+    /**
+     * @dev Sets the signer with a RSA public key. This function should be called during construction
+     * or through an initializer.
+     */
+    function _setSigner(bytes memory e, bytes memory n) internal {
+        _e = e;
+        _n = n;
+    }
+
+    /// @dev Return the signer's RSA public key.
+    function signer() public view virtual returns (bytes memory e, bytes memory n) {
+        return (_e, _n);
+    }
+
+    /**
+     * @dev See {AbstractSigner-_rawSignatureValidation}. Verifies a PKCSv1.5 signature by calling
+     * xref:api:utils.adoc#RSA-pkcs1Sha256-bytes-bytes-bytes-bytes-[RSA.pkcs1Sha256].
+     *
+     * IMPORTANT: Following the RSASSA-PKCS1-V1_5-VERIFY procedure outlined in RFC8017 (section 8.2.2), the
+     * provided `hash` is used as the `M` (message) and rehashed using SHA256 according to EMSA-PKCS1-v1_5
+     * encoding as per section 9.2 (step 1) of the RFC.
+     */
+    function _rawSignatureValidation(
+        bytes32 hash,
+        bytes calldata signature
+    ) internal view virtual override returns (bool) {
+        (bytes memory e, bytes memory n) = signer();
+        return RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n);
+    }
+}

+ 30 - 0
scripts/upgradeable/upgradeable.patch

@@ -334,6 +334,36 @@ index 304d1386a..a1cd63bee 100644
 -@openzeppelin/contracts/=contracts/
 -@openzeppelin/contracts/=contracts/
 +@openzeppelin/contracts-upgradeable/=contracts/
 +@openzeppelin/contracts-upgradeable/=contracts/
 +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
 +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
+diff --git a/test/account/AccountERC7702.test.js b/test/account/AccountERC7702.test.js
+index d08a52209..7a44bccfe 100644
+--- a/test/account/AccountERC7702.test.js
++++ b/test/account/AccountERC7702.test.js
+@@ -26,8 +26,8 @@ async function fixture() {
+ 
+   // domain cannot be fetched using getDomain(mock) before the mock is deployed
+   const domain = {
+-    name: 'AccountERC7702Mock',
+-    version: '1',
++    name: '', // Not initialized in the context of signer
++    version: '', // Not initialized in the context of signer
+     chainId: entrypointDomain.chainId,
+     verifyingContract: mock.address,
+   };
+diff --git a/test/account/examples/AccountERC7702WithModulesMock.test.js b/test/account/examples/AccountERC7702WithModulesMock.test.js
+index 9ee5f9177..f6106bcc7 100644
+--- a/test/account/examples/AccountERC7702WithModulesMock.test.js
++++ b/test/account/examples/AccountERC7702WithModulesMock.test.js
+@@ -36,8 +36,8 @@ async function fixture() {
+ 
+   // domain cannot be fetched using getDomain(mock) before the mock is deployed
+   const domain = {
+-    name: 'AccountERC7702WithModulesMock',
+-    version: '1',
++    name: '', // Not initialized in the context of signer
++    version: '', // Not initialized in the context of signer
+     chainId: entrypointDomain.chainId,
+     verifyingContract: mock.address,
+   };
 diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js
 diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js
 index 2b6e7fa97..268e0d29d 100644
 index 2b6e7fa97..268e0d29d 100644
 --- a/test/utils/cryptography/EIP712.test.js
 --- a/test/utils/cryptography/EIP712.test.js

+ 144 - 0
test/account/Account.behavior.js

@@ -0,0 +1,144 @@
+const { ethers, entrypoint } = require('hardhat');
+const { expect } = require('chai');
+const { impersonate } = require('../helpers/account');
+const { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE } = require('../helpers/erc4337');
+const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
+
+function shouldBehaveLikeAccountCore() {
+  describe('entryPoint', function () {
+    it('should return the canonical entrypoint', async function () {
+      await this.mock.deploy();
+      await expect(this.mock.entryPoint()).to.eventually.equal(entrypoint.v08);
+    });
+  });
+
+  describe('validateUserOp', function () {
+    beforeEach(async function () {
+      await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
+      await this.mock.deploy();
+      this.userOp ??= {};
+    });
+
+    it('should revert if the caller is not the canonical entrypoint', async function () {
+      // empty operation (does nothing)
+      const operation = await this.mock.createUserOp(this.userOp).then(op => this.signUserOp(op));
+
+      await expect(this.mock.connect(this.other).validateUserOp(operation.packed, operation.hash(), 0))
+        .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
+        .withArgs(this.other);
+    });
+
+    describe('when the caller is the canonical entrypoint', function () {
+      beforeEach(async function () {
+        this.mockFromEntrypoint = this.mock.connect(await impersonate(entrypoint.v08.target));
+      });
+
+      it('should return SIG_VALIDATION_SUCCESS if the signature is valid', async function () {
+        // empty operation (does nothing)
+        const operation = await this.mock.createUserOp(this.userOp).then(op => this.signUserOp(op));
+
+        expect(await this.mockFromEntrypoint.validateUserOp.staticCall(operation.packed, operation.hash(), 0)).to.eq(
+          SIG_VALIDATION_SUCCESS,
+        );
+      });
+
+      it('should return SIG_VALIDATION_FAILURE if the signature is invalid', async function () {
+        // empty operation (does nothing)
+        const operation = await this.mock.createUserOp(this.userOp);
+        operation.signature = (await this.invalidSig?.()) ?? '0x00';
+
+        expect(await this.mockFromEntrypoint.validateUserOp.staticCall(operation.packed, operation.hash(), 0)).to.eq(
+          SIG_VALIDATION_FAILURE,
+        );
+      });
+
+      it('should pay missing account funds for execution', async function () {
+        // empty operation (does nothing)
+        const operation = await this.mock.createUserOp(this.userOp).then(op => this.signUserOp(op));
+        const value = 42n;
+
+        await expect(
+          this.mockFromEntrypoint.validateUserOp(operation.packed, operation.hash(), value),
+        ).to.changeEtherBalances([this.mock, entrypoint.v08], [-value, value]);
+      });
+    });
+  });
+
+  describe('fallback', function () {
+    it('should receive ether', async function () {
+      await this.mock.deploy();
+      const value = 42n;
+
+      await expect(this.other.sendTransaction({ to: this.mock, value })).to.changeEtherBalances(
+        [this.other, this.mock],
+        [-value, value],
+      );
+    });
+  });
+}
+
+function shouldBehaveLikeAccountHolder() {
+  describe('onReceived', function () {
+    beforeEach(async function () {
+      await this.mock.deploy();
+    });
+
+    shouldSupportInterfaces(['ERC1155Receiver']);
+
+    describe('onERC1155Received', function () {
+      const ids = [1n, 2n, 3n];
+      const values = [1000n, 2000n, 3000n];
+      const data = '0x12345678';
+
+      beforeEach(async function () {
+        this.token = await ethers.deployContract('$ERC1155', ['https://somedomain.com/{id}.json']);
+        await this.token.$_mintBatch(this.other, ids, values, '0x');
+      });
+
+      it('receives ERC1155 tokens from a single ID', async function () {
+        await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, ids[0], values[0], data);
+
+        await expect(
+          this.token.balanceOfBatch(
+            ids.map(() => this.mock),
+            ids,
+          ),
+        ).to.eventually.deep.equal(values.map((v, i) => (i == 0 ? v : 0n)));
+      });
+
+      it('receives ERC1155 tokens from a multiple IDs', async function () {
+        await expect(
+          this.token.balanceOfBatch(
+            ids.map(() => this.mock),
+            ids,
+          ),
+        ).to.eventually.deep.equal(ids.map(() => 0n));
+
+        await this.token.connect(this.other).safeBatchTransferFrom(this.other, this.mock, ids, values, data);
+        await expect(
+          this.token.balanceOfBatch(
+            ids.map(() => this.mock),
+            ids,
+          ),
+        ).to.eventually.deep.equal(values);
+      });
+    });
+
+    describe('onERC721Received', function () {
+      const tokenId = 1n;
+
+      beforeEach(async function () {
+        this.token = await ethers.deployContract('$ERC721', ['Some NFT', 'SNFT']);
+        await this.token.$_mint(this.other, tokenId);
+      });
+
+      it('receives an ERC721 token', async function () {
+        await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId);
+
+        await expect(this.token.ownerOf(tokenId)).to.eventually.equal(this.mock);
+      });
+    });
+  });
+}
+
+module.exports = { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder };

+ 48 - 0
test/account/Account.test.js

@@ -0,0 +1,48 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../helpers/eip712');
+const { ERC4337Helper } = require('../helpers/erc4337');
+const { PackedUserOperation } = require('../helpers/eip712-types');
+const { NonNativeSigner } = require('../helpers/signers');
+
+const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
+const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
+const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
+
+async function fixture() {
+  // EOAs and environment
+  const [beneficiary, other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+
+  // ERC-4337 signer
+  const signer = new NonNativeSigner({ sign: hash => ({ serialized: hash }) });
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountMock', ['Account', '1']);
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  // domain cannot be fetched using getDomain(mock) before the mock is deployed
+  const domain = { name: 'Account', version: '1', chainId: entrypointDomain.chainId, verifyingContract: mock.address };
+
+  const signUserOp = async userOp =>
+    signer
+      .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
+      .then(signature => Object.assign(userOp, { signature }));
+
+  return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
+}
+
+describe('Account', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  shouldBehaveLikeAccountCore();
+  shouldBehaveLikeAccountHolder();
+  shouldBehaveLikeERC1271({ erc7739: true });
+  shouldBehaveLikeERC7821();
+});

+ 52 - 0
test/account/AccountECDSA.test.js

@@ -0,0 +1,52 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../helpers/eip712');
+const { ERC4337Helper } = require('../helpers/erc4337');
+const { PackedUserOperation } = require('../helpers/eip712-types');
+
+const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
+const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
+const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
+
+async function fixture() {
+  // EOAs and environment
+  const [beneficiary, other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+
+  // ERC-4337 signer
+  const signer = ethers.Wallet.createRandom();
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountECDSAMock', ['AccountECDSA', '1', signer]);
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  // domain cannot be fetched using getDomain(mock) before the mock is deployed
+  const domain = {
+    name: 'AccountECDSA',
+    version: '1',
+    chainId: entrypointDomain.chainId,
+    verifyingContract: mock.address,
+  };
+
+  const signUserOp = userOp =>
+    signer
+      .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
+      .then(signature => Object.assign(userOp, { signature }));
+
+  return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
+}
+
+describe('AccountECDSA', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  shouldBehaveLikeAccountCore();
+  shouldBehaveLikeAccountHolder();
+  shouldBehaveLikeERC1271({ erc7739: true });
+  shouldBehaveLikeERC7821();
+});

+ 97 - 0
test/account/AccountERC7702.t.sol

@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {AccountERC7702Mock} from "@openzeppelin/contracts/mocks/account/AccountMock.sol";
+import {CallReceiverMock} from "@openzeppelin/contracts/mocks/CallReceiverMock.sol";
+import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
+import {ERC7579Utils, Execution, Mode, ModeSelector, ModePayload} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
+import {ERC4337Utils, IEntryPointExtra} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
+import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
+import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
+import {ERC7821} from "@openzeppelin/contracts/account/extensions/ERC7821.sol";
+
+contract AccountERC7702MockConstructor is AccountERC7702Mock {
+    constructor() EIP712("MyAccount", "1") {}
+}
+
+contract AccountERC7702Test is Test {
+    using ERC7579Utils for *;
+    using ERC4337Utils for PackedUserOperation;
+    using Strings for *;
+
+    uint256 private constant MAX_ETH = type(uint128).max;
+
+    // Test accounts
+    CallReceiverMock private _target;
+
+    // ERC-4337 signer
+    uint256 private _signerPrivateKey;
+    AccountERC7702MockConstructor private _signer;
+
+    function setUp() public {
+        // Deploy target contract
+        _target = new CallReceiverMock();
+
+        // Setup signer
+        _signerPrivateKey = 0x1234;
+        _signer = AccountERC7702MockConstructor(payable(vm.addr(_signerPrivateKey)));
+        vm.deal(address(_signer), MAX_ETH);
+
+        // Sign and attach delegation
+        vm.signAndAttachDelegation(address(new AccountERC7702MockConstructor()), _signerPrivateKey);
+
+        // Setup entrypoint
+        vm.deal(address(ERC4337Utils.ENTRYPOINT_V08), MAX_ETH);
+        vm.etch(address(ERC4337Utils.ENTRYPOINT_V08), vm.readFileBinary("test/bin/EntryPoint070.bytecode"));
+    }
+
+    function testExecuteBatch(uint256 argA, uint256 argB) public {
+        // Create the mode for batch execution
+        Mode mode = ERC7579Utils.CALLTYPE_BATCH.encodeMode(
+            ERC7579Utils.EXECTYPE_DEFAULT,
+            ModeSelector.wrap(0x00000000),
+            ModePayload.wrap(0x00000000)
+        );
+
+        Execution[] memory execution = new Execution[](2);
+        execution[0] = Execution({
+            target: address(_target),
+            value: 1 ether,
+            callData: abi.encodeCall(CallReceiverMock.mockFunctionExtra, ())
+        });
+        execution[1] = Execution({
+            target: address(_target),
+            value: 0,
+            callData: abi.encodeCall(CallReceiverMock.mockFunctionWithArgs, (argA, argB))
+        });
+
+        // Pack the batch within a PackedUserOperation
+        PackedUserOperation[] memory ops = new PackedUserOperation[](1);
+        ops[0] = PackedUserOperation({
+            sender: address(_signer),
+            nonce: 0,
+            initCode: bytes(""),
+            callData: abi.encodeCall(ERC7821.execute, (Mode.unwrap(mode), execution.encodeBatch())),
+            preVerificationGas: 100000,
+            accountGasLimits: bytes32(abi.encodePacked(uint128(100000), uint128(100000))),
+            gasFees: bytes32(abi.encodePacked(uint128(1000000), uint128(1000000))),
+            paymasterAndData: bytes(""),
+            signature: bytes("")
+        });
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            _signerPrivateKey,
+            IEntryPointExtra(address(ERC4337Utils.ENTRYPOINT_V08)).getUserOpHash(ops[0])
+        );
+        ops[0].signature = abi.encodePacked(r, s, v);
+
+        // Expect the events to be emitted
+        vm.expectEmit(true, true, true, true);
+        emit CallReceiverMock.MockFunctionCalledExtra(address(_signer), 1 ether);
+        vm.expectEmit(true, true, true, true);
+        emit CallReceiverMock.MockFunctionCalledWithArgs(argA, argB);
+
+        // Execute the batch
+        _signer.entryPoint().handleOps(ops, payable(makeAddr("beneficiary")));
+    }
+}

+ 52 - 0
test/account/AccountERC7702.test.js

@@ -0,0 +1,52 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../helpers/eip712');
+const { ERC4337Helper } = require('../helpers/erc4337');
+const { PackedUserOperation } = require('../helpers/eip712-types');
+
+const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
+const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
+const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
+
+async function fixture() {
+  // EOAs and environment
+  const [beneficiary, other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+
+  // ERC-4337 signer
+  const signer = ethers.Wallet.createRandom(ethers.provider);
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountERC7702Mock', ['AccountERC7702Mock', '1'], { erc7702signer: signer });
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  // domain cannot be fetched using getDomain(mock) before the mock is deployed
+  const domain = {
+    name: 'AccountERC7702Mock',
+    version: '1',
+    chainId: entrypointDomain.chainId,
+    verifyingContract: mock.address,
+  };
+
+  const signUserOp = userOp =>
+    signer
+      .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
+      .then(signature => Object.assign(userOp, { signature }));
+
+  return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
+}
+
+describe('AccountERC7702', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  shouldBehaveLikeAccountCore();
+  shouldBehaveLikeAccountHolder();
+  shouldBehaveLikeERC1271({ erc7739: true });
+  shouldBehaveLikeERC7821({ deployable: false });
+});

+ 58 - 0
test/account/AccountP256.test.js

@@ -0,0 +1,58 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../helpers/eip712');
+const { ERC4337Helper } = require('../helpers/erc4337');
+const { NonNativeSigner, P256SigningKey } = require('../helpers/signers');
+const { PackedUserOperation } = require('../helpers/eip712-types');
+
+const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
+const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
+const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
+
+async function fixture() {
+  // EOAs and environment
+  const [beneficiary, other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+
+  // ERC-4337 signer
+  const signer = new NonNativeSigner(P256SigningKey.random());
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountP256Mock', [
+    'AccountP256',
+    '1',
+    signer.signingKey.publicKey.qx,
+    signer.signingKey.publicKey.qy,
+  ]);
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  // domain cannot be fetched using getDomain(mock) before the mock is deployed
+  const domain = {
+    name: 'AccountP256',
+    version: '1',
+    chainId: entrypointDomain.chainId,
+    verifyingContract: mock.address,
+  };
+
+  const signUserOp = userOp =>
+    signer
+      .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
+      .then(signature => Object.assign(userOp, { signature }));
+
+  return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
+}
+
+describe('AccountP256', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  shouldBehaveLikeAccountCore();
+  shouldBehaveLikeAccountHolder();
+  shouldBehaveLikeERC1271({ erc7739: true });
+  shouldBehaveLikeERC7821();
+});

+ 58 - 0
test/account/AccountRSA.test.js

@@ -0,0 +1,58 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../helpers/eip712');
+const { ERC4337Helper } = require('../helpers/erc4337');
+const { NonNativeSigner, RSASHA256SigningKey } = require('../helpers/signers');
+const { PackedUserOperation } = require('../helpers/eip712-types');
+
+const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
+const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
+const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
+
+async function fixture() {
+  // EOAs and environment
+  const [beneficiary, other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+
+  // ERC-4337 signer
+  const signer = new NonNativeSigner(RSASHA256SigningKey.random());
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountRSAMock', [
+    'AccountRSA',
+    '1',
+    signer.signingKey.publicKey.e,
+    signer.signingKey.publicKey.n,
+  ]);
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  // domain cannot be fetched using getDomain(mock) before the mock is deployed
+  const domain = {
+    name: 'AccountRSA',
+    version: '1',
+    chainId: entrypointDomain.chainId,
+    verifyingContract: mock.address,
+  };
+
+  const signUserOp = userOp =>
+    signer
+      .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
+      .then(signature => Object.assign(userOp, { signature }));
+
+  return { helper, mock, domain, signer, target, beneficiary, other, signUserOp };
+}
+
+describe('AccountRSA', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  shouldBehaveLikeAccountCore();
+  shouldBehaveLikeAccountHolder();
+  shouldBehaveLikeERC1271({ erc7739: true });
+  shouldBehaveLikeERC7821();
+});

+ 99 - 0
test/account/examples/AccountERC7702WithModulesMock.test.js

@@ -0,0 +1,99 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../../helpers/eip712');
+const { ERC4337Helper } = require('../../helpers/erc4337');
+const { PackedUserOperation } = require('../../helpers/eip712-types');
+
+const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('../Account.behavior');
+const { shouldBehaveLikeAccountERC7579 } = require('../extensions/AccountERC7579.behavior');
+const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
+const { shouldBehaveLikeERC7821 } = require('../extensions/ERC7821.behavior');
+
+const { MODULE_TYPE_VALIDATOR } = require('../../helpers/erc7579');
+
+async function fixture() {
+  // EOAs and environment
+  const [beneficiary, other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+  const anotherTarget = await ethers.deployContract('CallReceiverMock');
+
+  // Signer with EIP-7702 support + funding
+  const eoa = ethers.Wallet.createRandom(ethers.provider);
+  await setBalance(eoa.address, ethers.WeiPerEther);
+
+  // ERC-7579 validator module
+  const validator = await ethers.deployContract('$ERC7579ValidatorMock');
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountERC7702WithModulesMock', ['AccountERC7702WithModulesMock', '1'], {
+    erc7702signer: eoa,
+  });
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  // domain cannot be fetched using getDomain(mock) before the mock is deployed
+  const domain = {
+    name: 'AccountERC7702WithModulesMock',
+    version: '1',
+    chainId: entrypointDomain.chainId,
+    verifyingContract: mock.address,
+  };
+
+  return { helper, validator, mock, domain, entrypointDomain, eoa, target, anotherTarget, beneficiary, other };
+}
+
+describe('AccountERC7702WithModules: ERC-7702 account with ERC-7579 modules supports', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  describe('using ERC-7702 signer', function () {
+    beforeEach(async function () {
+      this.signer = this.eoa;
+      this.signUserOp = userOp =>
+        this.signer
+          .signTypedData(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
+          .then(signature => Object.assign(userOp, { signature }));
+    });
+
+    shouldBehaveLikeAccountCore();
+    shouldBehaveLikeAccountHolder();
+    shouldBehaveLikeERC7821({ deployable: false });
+    shouldBehaveLikeERC1271({ erc7739: true });
+  });
+
+  describe('using ERC-7579 validator', function () {
+    beforeEach(async function () {
+      // signer that adds a prefix to all signatures (except the userOp ones)
+      this.signer = ethers.Wallet.createRandom();
+      this.signer.signMessage = message =>
+        ethers.Wallet.prototype.signMessage
+          .bind(this.signer)(message)
+          .then(sign => ethers.concat([this.validator.target, sign]));
+      this.signer.signTypedData = (domain, types, values) =>
+        ethers.Wallet.prototype.signTypedData
+          .bind(this.signer)(domain, types, values)
+          .then(sign => ethers.concat([this.validator.target, sign]));
+
+      this.signUserOp = userOp =>
+        ethers.Wallet.prototype.signTypedData
+          .bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
+          .then(signature => Object.assign(userOp, { signature }));
+
+      // Use the first 20 bytes from the nonce key (24 bytes) to identify the validator module
+      this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
+
+      // Deploy (using ERC-7702) and add the validator module using EOA
+      await this.mock.deploy();
+      await this.mock.connect(this.eoa).installModule(MODULE_TYPE_VALIDATOR, this.validator, this.signer.address);
+    });
+
+    shouldBehaveLikeAccountCore();
+    shouldBehaveLikeAccountHolder();
+    shouldBehaveLikeAccountERC7579();
+    shouldBehaveLikeERC1271({ erc7739: false });
+  });
+});

+ 563 - 0
test/account/extensions/AccountERC7579.behavior.js

@@ -0,0 +1,563 @@
+const { ethers, entrypoint } = require('hardhat');
+const { expect } = require('chai');
+const { impersonate } = require('../../helpers/account');
+const { selector } = require('../../helpers/methods');
+const { zip } = require('../../helpers/iterate');
+const {
+  encodeMode,
+  encodeBatch,
+  encodeSingle,
+  encodeDelegate,
+  MODULE_TYPE_VALIDATOR,
+  MODULE_TYPE_EXECUTOR,
+  MODULE_TYPE_FALLBACK,
+  MODULE_TYPE_HOOK,
+  CALL_TYPE_CALL,
+  CALL_TYPE_BATCH,
+  CALL_TYPE_DELEGATE,
+  EXEC_TYPE_DEFAULT,
+  EXEC_TYPE_TRY,
+} = require('../../helpers/erc7579');
+
+const CALL_TYPE_INVALID = '0x42';
+const EXEC_TYPE_INVALID = '0x17';
+const MODULE_TYPE_INVALID = 999n;
+
+const coder = ethers.AbiCoder.defaultAbiCoder();
+
+function shouldBehaveLikeAccountERC7579({ withHooks = false } = {}) {
+  describe('AccountERC7579', function () {
+    beforeEach(async function () {
+      await this.mock.deploy();
+      await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
+
+      this.modules = {};
+      this.modules[MODULE_TYPE_VALIDATOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_VALIDATOR]);
+      this.modules[MODULE_TYPE_EXECUTOR] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_EXECUTOR]);
+      this.modules[MODULE_TYPE_FALLBACK] = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
+      this.modules[MODULE_TYPE_HOOK] = await ethers.deployContract('$ERC7579HookMock');
+
+      this.mockFromEntrypoint = this.mock.connect(await impersonate(entrypoint.v08.target));
+      this.mockFromExecutor = this.mock.connect(await impersonate(this.modules[MODULE_TYPE_EXECUTOR].target));
+    });
+
+    describe('accountId', function () {
+      it('should return the account ID', async function () {
+        await expect(this.mock.accountId()).to.eventually.equal(
+          withHooks
+            ? '@openzeppelin/community-contracts.AccountERC7579Hooked.v0.0.0'
+            : '@openzeppelin/community-contracts.AccountERC7579.v0.0.0',
+        );
+      });
+    });
+
+    describe('supportsExecutionMode', function () {
+      for (const [callType, execType] of zip(
+        [CALL_TYPE_CALL, CALL_TYPE_BATCH, CALL_TYPE_DELEGATE, CALL_TYPE_INVALID],
+        [EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY, EXEC_TYPE_INVALID],
+      )) {
+        const result = callType != CALL_TYPE_INVALID && execType != EXEC_TYPE_INVALID;
+
+        it(`${
+          result ? 'does not support' : 'supports'
+        } CALL_TYPE=${callType} and EXEC_TYPE=${execType} execution mode`, async function () {
+          await expect(this.mock.supportsExecutionMode(encodeMode({ callType, execType }))).to.eventually.equal(result);
+        });
+      }
+    });
+
+    describe('supportsModule', function () {
+      it('supports MODULE_TYPE_VALIDATOR module type', async function () {
+        await expect(this.mock.supportsModule(MODULE_TYPE_VALIDATOR)).to.eventually.equal(true);
+      });
+
+      it('supports MODULE_TYPE_EXECUTOR module type', async function () {
+        await expect(this.mock.supportsModule(MODULE_TYPE_EXECUTOR)).to.eventually.equal(true);
+      });
+
+      it('supports MODULE_TYPE_FALLBACK module type', async function () {
+        await expect(this.mock.supportsModule(MODULE_TYPE_FALLBACK)).to.eventually.equal(true);
+      });
+
+      it(
+        withHooks ? 'supports MODULE_TYPE_HOOK module type' : 'does not support MODULE_TYPE_HOOK module type',
+        async function () {
+          await expect(this.mock.supportsModule(MODULE_TYPE_HOOK)).to.eventually.equal(withHooks);
+        },
+      );
+
+      it('does not support invalid module type', async function () {
+        await expect(this.mock.supportsModule(MODULE_TYPE_INVALID)).to.eventually.equal(false);
+      });
+    });
+
+    describe('module installation', function () {
+      it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
+        await expect(this.mock.connect(this.other).installModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
+          .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
+          .withArgs(this.other);
+      });
+
+      it('should revert if the module type is not supported', async function () {
+        await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_INVALID, this.mock, '0x'))
+          .to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
+          .withArgs(MODULE_TYPE_INVALID);
+      });
+
+      it('should revert if the module is not the provided type', async function () {
+        const instance = this.modules[MODULE_TYPE_EXECUTOR];
+        await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_VALIDATOR, instance, '0x'))
+          .to.be.revertedWithCustomError(this.mock, 'ERC7579MismatchedModuleTypeId')
+          .withArgs(MODULE_TYPE_VALIDATOR, instance);
+      });
+
+      for (const moduleTypeId of [
+        MODULE_TYPE_VALIDATOR,
+        MODULE_TYPE_EXECUTOR,
+        MODULE_TYPE_FALLBACK,
+        withHooks && MODULE_TYPE_HOOK,
+      ].filter(Boolean)) {
+        const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
+        const initData = ethers.hexlify(ethers.randomBytes(256));
+        const fullData = ethers.concat([prefix, initData]);
+
+        it(`should install a module of type ${moduleTypeId}`, async function () {
+          const instance = this.modules[moduleTypeId];
+
+          await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
+
+          await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
+            .to.emit(this.mock, 'ModuleInstalled')
+            .withArgs(moduleTypeId, instance)
+            .to.emit(instance, 'ModuleInstalledReceived')
+            .withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
+
+          await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
+        });
+
+        it(`does not allow to install a module of ${moduleTypeId} id twice`, async function () {
+          const instance = this.modules[moduleTypeId];
+
+          await this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData);
+
+          await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
+
+          await expect(this.mockFromEntrypoint.installModule(moduleTypeId, instance, fullData))
+            .to.be.revertedWithCustomError(
+              this.mock,
+              moduleTypeId == MODULE_TYPE_HOOK ? 'ERC7579HookModuleAlreadyPresent' : 'ERC7579AlreadyInstalledModule',
+            )
+            .withArgs(...[moduleTypeId != MODULE_TYPE_HOOK && moduleTypeId, instance].filter(Boolean));
+        });
+      }
+
+      withHooks &&
+        describe('with hook', function () {
+          beforeEach(async function () {
+            await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
+          });
+
+          it('should call the hook of the installed module when performing an module install', async function () {
+            const instance = this.modules[MODULE_TYPE_EXECUTOR];
+            const initData = ethers.hexlify(ethers.randomBytes(256));
+
+            const precheckData = this.mock.interface.encodeFunctionData('installModule', [
+              MODULE_TYPE_EXECUTOR,
+              instance.target,
+              initData,
+            ]);
+
+            await expect(this.mockFromEntrypoint.installModule(MODULE_TYPE_EXECUTOR, instance, initData))
+              .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
+              .withArgs(entrypoint.v08, 0n, precheckData)
+              .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
+              .withArgs(precheckData);
+          });
+        });
+    });
+
+    describe('module uninstallation', function () {
+      it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
+        await expect(this.mock.connect(this.other).uninstallModule(MODULE_TYPE_VALIDATOR, this.mock, '0x'))
+          .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
+          .withArgs(this.other);
+      });
+
+      it('should revert if the module type is not supported', async function () {
+        await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_INVALID, this.mock, '0x'))
+          .to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedModuleType')
+          .withArgs(MODULE_TYPE_INVALID);
+      });
+
+      for (const moduleTypeId of [
+        MODULE_TYPE_VALIDATOR,
+        MODULE_TYPE_EXECUTOR,
+        MODULE_TYPE_FALLBACK,
+        withHooks && MODULE_TYPE_HOOK,
+      ].filter(Boolean)) {
+        const prefix = moduleTypeId == MODULE_TYPE_FALLBACK ? '0x12345678' : '0x';
+        const initData = ethers.hexlify(ethers.randomBytes(256));
+        const fullData = ethers.concat([prefix, initData]);
+
+        it(`should uninstall a module of type ${moduleTypeId}`, async function () {
+          const instance = this.modules[moduleTypeId];
+
+          await this.mock.$_installModule(moduleTypeId, instance, fullData);
+
+          await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(true);
+
+          await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
+            .to.emit(this.mock, 'ModuleUninstalled')
+            .withArgs(moduleTypeId, instance)
+            .to.emit(instance, 'ModuleUninstalledReceived')
+            .withArgs(this.mock, initData); // After decoding MODULE_TYPE_FALLBACK, it should remove the fnSig
+
+          await expect(this.mock.isModuleInstalled(moduleTypeId, instance, fullData)).to.eventually.equal(false);
+        });
+
+        it(`should revert uninstalling a module of type ${moduleTypeId} if it was not installed`, async function () {
+          const instance = this.modules[moduleTypeId];
+
+          await expect(this.mockFromEntrypoint.uninstallModule(moduleTypeId, instance, fullData))
+            .to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
+            .withArgs(moduleTypeId, instance);
+        });
+      }
+
+      it('should revert uninstalling a module of type MODULE_TYPE_FALLBACK if a different module was installed for the provided selector', async function () {
+        const instance = this.modules[MODULE_TYPE_FALLBACK];
+        const anotherInstance = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
+        const initData = '0x12345678abcdef';
+
+        await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_FALLBACK, instance, initData);
+        await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_FALLBACK, anotherInstance, initData))
+          .to.be.revertedWithCustomError(this.mock, 'ERC7579UninstalledModule')
+          .withArgs(MODULE_TYPE_FALLBACK, anotherInstance);
+      });
+
+      withHooks &&
+        describe('with hook', function () {
+          beforeEach(async function () {
+            await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
+          });
+
+          it('should call the hook of the installed module when performing a module uninstall', async function () {
+            const instance = this.modules[MODULE_TYPE_EXECUTOR];
+            const initData = ethers.hexlify(ethers.randomBytes(256));
+
+            const precheckData = this.mock.interface.encodeFunctionData('uninstallModule', [
+              MODULE_TYPE_EXECUTOR,
+              instance.target,
+              initData,
+            ]);
+
+            await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, instance, initData);
+            await expect(this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_EXECUTOR, instance, initData))
+              .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
+              .withArgs(entrypoint.v08, 0n, precheckData)
+              .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
+              .withArgs(precheckData);
+          });
+        });
+    });
+
+    describe('execution', function () {
+      beforeEach(async function () {
+        await this.mock.$_installModule(MODULE_TYPE_EXECUTOR, this.modules[MODULE_TYPE_EXECUTOR], '0x');
+      });
+
+      for (const [execFn, mock] of [
+        ['execute', 'mockFromEntrypoint'],
+        ['executeFromExecutor', 'mockFromExecutor'],
+      ]) {
+        describe(`executing with ${execFn}`, function () {
+          it('should revert if the call type is not supported', async function () {
+            await expect(
+              this[mock][execFn](encodeMode({ callType: CALL_TYPE_INVALID }), encodeSingle(this.other, 0, '0x')),
+            )
+              .to.be.revertedWithCustomError(this.mock, 'ERC7579UnsupportedCallType')
+              .withArgs(ethers.solidityPacked(['bytes1'], [CALL_TYPE_INVALID]));
+          });
+
+          it('should revert if the caller is not authorized / installed', async function () {
+            const error = execFn == 'execute' ? 'AccountUnauthorized' : 'ERC7579UninstalledModule';
+            const args = execFn == 'execute' ? [this.other] : [MODULE_TYPE_EXECUTOR, this.other];
+
+            await expect(
+              this[mock]
+                .connect(this.other)
+                [execFn](encodeMode({ callType: CALL_TYPE_CALL }), encodeSingle(this.other, 0, '0x')),
+            )
+              .to.be.revertedWithCustomError(this.mock, error)
+              .withArgs(...args);
+          });
+
+          describe('single execution', function () {
+            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']),
+              );
+
+              const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), data);
+
+              await expect(tx).to.emit(this.target, 'MockFunctionCalledWithArgs').withArgs(42, '0x1234');
+              await expect(tx).to.changeEtherBalances([this.mock, this.target], [-value, 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[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL }), 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[mock][execFn](encodeMode({ callType: CALL_TYPE_CALL, execType: EXEC_TYPE_TRY }), data))
+                .to.emit(this.mock, 'ERC7579TryExecuteFail')
+                .withArgs(
+                  CALL_TYPE_CALL,
+                  ethers.solidityPacked(
+                    ['bytes4', 'bytes'],
+                    [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
+                  ),
+                );
+            });
+          });
+
+          describe('batch execution', function () {
+            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']),
+                ],
+              );
+
+              const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), data);
+              await expect(tx)
+                .to.emit(this.target, 'MockFunctionCalledWithArgs')
+                .to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
+              await expect(tx).to.changeEtherBalances(
+                [this.mock, this.target, this.anotherTarget],
+                [-value1 - value2, value1, 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[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH }), 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'),
+                ],
+              );
+
+              const tx = this[mock][execFn](encodeMode({ callType: CALL_TYPE_BATCH, execType: EXEC_TYPE_TRY }), data);
+
+              await expect(tx)
+                .to.emit(this.mock, 'ERC7579TryExecuteFail')
+                .withArgs(
+                  CALL_TYPE_BATCH,
+                  ethers.solidityPacked(
+                    ['bytes4', 'bytes'],
+                    [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
+                  ),
+                );
+
+              await expect(tx).to.changeEtherBalances(
+                [this.mock, this.target, this.anotherTarget],
+                [-value1, value1, 0],
+              );
+            });
+          });
+
+          describe('delegate call execution', 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]),
+              );
+
+              await expect(ethers.provider.getStorage(this.mock.target, slot)).to.eventually.equal(ethers.ZeroHash);
+              await this[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), data);
+              await expect(ethers.provider.getStorage(this.mock.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[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE }), 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[mock][execFn](encodeMode({ callType: CALL_TYPE_DELEGATE, execType: EXEC_TYPE_TRY }), data),
+              )
+                .to.emit(this.mock, 'ERC7579TryExecuteFail')
+                .withArgs(
+                  CALL_TYPE_CALL,
+                  ethers.solidityPacked(
+                    ['bytes4', 'bytes'],
+                    [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
+                  ),
+                );
+            });
+          });
+
+          withHooks &&
+            describe('with hook', function () {
+              beforeEach(async function () {
+                await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
+              });
+
+              it(`should call the hook of the installed module when executing ${execFn}`, async function () {
+                const caller = execFn === 'execute' ? entrypoint.v08 : this.modules[MODULE_TYPE_EXECUTOR];
+                const value = 17;
+                const data = this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']);
+
+                const mode = encodeMode({ callType: CALL_TYPE_CALL });
+                const call = encodeSingle(this.target, value, data);
+                const precheckData = this[mock].interface.encodeFunctionData(execFn, [mode, call]);
+
+                const tx = this[mock][execFn](mode, call, { value });
+
+                await expect(tx)
+                  .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
+                  .withArgs(caller, value, precheckData)
+                  .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
+                  .withArgs(precheckData);
+                await expect(tx).to.changeEtherBalances([caller, this.mock, this.target], [-value, 0n, value]);
+              });
+            });
+        });
+      }
+    });
+
+    describe('fallback', function () {
+      beforeEach(async function () {
+        this.fallbackHandler = await ethers.deployContract('$ERC7579FallbackHandlerMock');
+      });
+
+      it('reverts if there is no fallback module installed', async function () {
+        const { selector } = this.fallbackHandler.callPayable.getFragment();
+
+        await expect(this.fallbackHandler.attach(this.mock).callPayable())
+          .to.be.revertedWithCustomError(this.mock, 'ERC7579MissingFallbackHandler')
+          .withArgs(selector);
+      });
+
+      describe('with a fallback module installed', function () {
+        beforeEach(async function () {
+          await Promise.all(
+            [
+              this.fallbackHandler.callPayable.getFragment().selector,
+              this.fallbackHandler.callView.getFragment().selector,
+              this.fallbackHandler.callRevert.getFragment().selector,
+            ].map(selector =>
+              this.mock.$_installModule(
+                MODULE_TYPE_FALLBACK,
+                this.fallbackHandler,
+                coder.encode(['bytes4', 'bytes'], [selector, '0x']),
+              ),
+            ),
+          );
+        });
+
+        it('forwards the call to the fallback handler', async function () {
+          const calldata = this.fallbackHandler.interface.encodeFunctionData('callPayable');
+          const value = 17n;
+
+          await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
+            .to.emit(this.fallbackHandler, 'ERC7579FallbackHandlerMockCalled')
+            .withArgs(this.mock, this.other, value, calldata);
+        });
+
+        it('returns answer from the fallback handler', async function () {
+          await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callView()).to.eventually.deep.equal([
+            this.mock.target,
+            this.other.address,
+          ]);
+        });
+
+        it('bubble up reverts from the fallback handler', async function () {
+          await expect(
+            this.fallbackHandler.attach(this.mock).connect(this.other).callRevert(),
+          ).to.be.revertedWithCustomError(this.fallbackHandler, 'ERC7579FallbackHandlerMockRevert');
+        });
+
+        withHooks &&
+          describe('with hook', function () {
+            beforeEach(async function () {
+              await this.mockFromEntrypoint.$_installModule(MODULE_TYPE_HOOK, this.modules[MODULE_TYPE_HOOK], '0x');
+            });
+
+            it('should call the hook of the installed module when performing a callback', async function () {
+              const precheckData = this.fallbackHandler.interface.encodeFunctionData('callPayable');
+              const value = 17n;
+
+              // call with interface: decode returned data
+              await expect(this.fallbackHandler.attach(this.mock).connect(this.other).callPayable({ value }))
+                .to.emit(this.modules[MODULE_TYPE_HOOK], 'PreCheck')
+                .withArgs(this.other, value, precheckData)
+                .to.emit(this.modules[MODULE_TYPE_HOOK], 'PostCheck')
+                .withArgs(precheckData);
+            });
+          });
+      });
+    });
+  });
+}
+
+module.exports = {
+  shouldBehaveLikeAccountERC7579,
+};

+ 60 - 0
test/account/extensions/AccountERC7579.test.js

@@ -0,0 +1,60 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../../helpers/eip712');
+const { ERC4337Helper } = require('../../helpers/erc4337');
+const { PackedUserOperation } = require('../../helpers/eip712-types');
+
+const { shouldBehaveLikeAccountCore } = require('../Account.behavior');
+const { shouldBehaveLikeAccountERC7579 } = require('./AccountERC7579.behavior');
+const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
+
+async function fixture() {
+  // EOAs and environment
+  const [other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+  const anotherTarget = await ethers.deployContract('CallReceiverMock');
+
+  // ERC-7579 validator
+  const validator = await ethers.deployContract('$ERC7579ValidatorMock');
+
+  // ERC-4337 signer
+  const signer = ethers.Wallet.createRandom();
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountERC7579Mock', [
+    validator,
+    ethers.solidityPacked(['address'], [signer.address]),
+  ]);
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  return { helper, validator, mock, entrypointDomain, signer, target, anotherTarget, other };
+}
+
+describe('AccountERC7579', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+
+    this.signer.signMessage = message =>
+      ethers.Wallet.prototype.signMessage
+        .bind(this.signer)(message)
+        .then(sign => ethers.concat([this.validator.target, sign]));
+    this.signer.signTypedData = (domain, types, values) =>
+      ethers.Wallet.prototype.signTypedData
+        .bind(this.signer)(domain, types, values)
+        .then(sign => ethers.concat([this.validator.target, sign]));
+    this.signUserOp = userOp =>
+      ethers.Wallet.prototype.signTypedData
+        .bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
+        .then(signature => Object.assign(userOp, { signature }));
+
+    this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
+  });
+
+  shouldBehaveLikeAccountCore();
+  shouldBehaveLikeAccountERC7579();
+  shouldBehaveLikeERC1271();
+});

+ 60 - 0
test/account/extensions/AccountERC7579Hooked.test.js

@@ -0,0 +1,60 @@
+const { ethers, entrypoint } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const { getDomain } = require('../../helpers/eip712');
+const { ERC4337Helper } = require('../../helpers/erc4337');
+const { PackedUserOperation } = require('../../helpers/eip712-types');
+
+const { shouldBehaveLikeAccountCore } = require('../Account.behavior');
+const { shouldBehaveLikeAccountERC7579 } = require('./AccountERC7579.behavior');
+const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');
+
+async function fixture() {
+  // EOAs and environment
+  const [other] = await ethers.getSigners();
+  const target = await ethers.deployContract('CallReceiverMock');
+  const anotherTarget = await ethers.deployContract('CallReceiverMock');
+
+  // ERC-7579 validator
+  const validator = await ethers.deployContract('$ERC7579ValidatorMock');
+
+  // ERC-4337 signer
+  const signer = ethers.Wallet.createRandom();
+
+  // ERC-4337 account
+  const helper = new ERC4337Helper();
+  const mock = await helper.newAccount('$AccountERC7579HookedMock', [
+    validator,
+    ethers.solidityPacked(['address'], [signer.address]),
+  ]);
+
+  // ERC-4337 Entrypoint domain
+  const entrypointDomain = await getDomain(entrypoint.v08);
+
+  return { helper, validator, mock, entrypointDomain, signer, target, anotherTarget, other };
+}
+
+describe('AccountERC7579Hooked', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+
+    this.signer.signMessage = message =>
+      ethers.Wallet.prototype.signMessage
+        .bind(this.signer)(message)
+        .then(sign => ethers.concat([this.validator.target, sign]));
+    this.signer.signTypedData = (domain, types, values) =>
+      ethers.Wallet.prototype.signTypedData
+        .bind(this.signer)(domain, types, values)
+        .then(sign => ethers.concat([this.validator.target, sign]));
+    this.signUserOp = userOp =>
+      ethers.Wallet.prototype.signTypedData
+        .bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
+        .then(signature => Object.assign(userOp, { signature }));
+
+    this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
+  });
+
+  shouldBehaveLikeAccountCore();
+  shouldBehaveLikeAccountERC7579({ withHooks: true });
+  shouldBehaveLikeERC1271();
+});

+ 145 - 0
test/account/extensions/ERC7821.behavior.js

@@ -0,0 +1,145 @@
+const { ethers, entrypoint } = require('hardhat');
+const { expect } = require('chai');
+
+const { CALL_TYPE_BATCH, encodeMode, encodeBatch } = require('../../helpers/erc7579');
+
+function shouldBehaveLikeERC7821({ deployable = true } = {}) {
+  describe('supports ERC-7821', function () {
+    beforeEach(async function () {
+      // give eth to the account (before deployment)
+      await this.other.sendTransaction({ to: this.mock.target, value: ethers.parseEther('1') });
+
+      // account is not initially deployed
+      await expect(ethers.provider.getCode(this.mock)).to.eventually.equal('0x');
+
+      this.encodeUserOpCalldata = (...calls) =>
+        this.mock.interface.encodeFunctionData('execute', [
+          encodeMode({ callType: CALL_TYPE_BATCH }),
+          encodeBatch(...calls),
+        ]);
+    });
+
+    it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
+      await this.mock.deploy();
+
+      await expect(
+        this.mock.connect(this.other).execute(
+          encodeMode({ callType: CALL_TYPE_BATCH }),
+          encodeBatch({
+            target: this.target,
+            data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
+          }),
+        ),
+      )
+        .to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
+        .withArgs(this.other);
+    });
+
+    if (deployable) {
+      describe('when not deployed', function () {
+        it('should be created with handleOps and increase nonce', async function () {
+          const operation = await this.mock
+            .createUserOp({
+              callData: this.encodeUserOpCalldata({
+                target: this.target,
+                value: 17,
+                data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
+              }),
+            })
+            .then(op => op.addInitCode())
+            .then(op => this.signUserOp(op));
+
+          // Can't call the account to get its nonce before it's deployed
+          await expect(entrypoint.v08.getNonce(this.mock.target, 0)).to.eventually.equal(0);
+          await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary))
+            .to.emit(entrypoint.v08, 'AccountDeployed')
+            .withArgs(operation.hash(), this.mock, this.helper.factory, ethers.ZeroAddress)
+            .to.emit(this.target, 'MockFunctionCalledExtra')
+            .withArgs(this.mock, 17);
+          await expect(this.mock.getNonce()).to.eventually.equal(1);
+        });
+
+        it('should revert if the signature is invalid', async function () {
+          const operation = await this.mock
+            .createUserOp({
+              callData: this.encodeUserOpCalldata({
+                target: this.target,
+                value: 17,
+                data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
+              }),
+            })
+            .then(op => op.addInitCode());
+
+          operation.signature = '0x00';
+
+          await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary)).to.be.reverted;
+        });
+      });
+    }
+
+    describe('when deployed', function () {
+      beforeEach(async function () {
+        await this.mock.deploy();
+      });
+
+      it('should increase nonce and call target', async function () {
+        const operation = await this.mock
+          .createUserOp({
+            callData: this.encodeUserOpCalldata({
+              target: this.target,
+              value: 42,
+              data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
+            }),
+          })
+          .then(op => this.signUserOp(op));
+
+        await expect(this.mock.getNonce()).to.eventually.equal(0);
+        await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary))
+          .to.emit(this.target, 'MockFunctionCalledExtra')
+          .withArgs(this.mock, 42);
+        await expect(this.mock.getNonce()).to.eventually.equal(1);
+      });
+
+      it('should support sending eth to an EOA', async function () {
+        const operation = await this.mock
+          .createUserOp({ callData: this.encodeUserOpCalldata({ target: this.other, value: 42 }) })
+          .then(op => this.signUserOp(op));
+
+        await expect(this.mock.getNonce()).to.eventually.equal(0);
+        await expect(entrypoint.v08.handleOps([operation.packed], this.beneficiary)).to.changeEtherBalance(
+          this.other,
+          42,
+        );
+        await expect(this.mock.getNonce()).to.eventually.equal(1);
+      });
+
+      it('should support batch execution', async function () {
+        const value1 = 43374337n;
+        const value2 = 69420n;
+
+        const operation = await this.mock
+          .createUserOp({
+            callData: this.encodeUserOpCalldata(
+              { target: this.other, value: value1 },
+              {
+                target: this.target,
+                value: value2,
+                data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
+              },
+            ),
+          })
+          .then(op => this.signUserOp(op));
+
+        await expect(this.mock.getNonce()).to.eventually.equal(0);
+        const tx = entrypoint.v08.handleOps([operation.packed], this.beneficiary);
+        await expect(tx).to.changeEtherBalances([this.other, this.target], [value1, value2]);
+        await expect(tx).to.emit(this.target, 'MockFunctionCalledExtra').withArgs(this.mock, value2);
+        await expect(this.mock.getNonce()).to.eventually.equal(1);
+      });
+    });
+  });
+}
+
+module.exports = {
+  shouldBehaveLikeERC7821,
+};

+ 27 - 25
test/helpers/eip712-types.js

@@ -11,19 +11,8 @@ module.exports = mapValues(
       verifyingContract: 'address',
       verifyingContract: 'address',
       salt: 'bytes32',
       salt: 'bytes32',
     },
     },
-    Permit: {
-      owner: 'address',
-      spender: 'address',
-      value: 'uint256',
-      nonce: 'uint256',
-      deadline: 'uint256',
-    },
-    Ballot: {
-      proposalId: 'uint256',
-      support: 'uint8',
-      voter: 'address',
-      nonce: 'uint256',
-    },
+    Permit: { owner: 'address', spender: 'address', value: 'uint256', nonce: 'uint256', deadline: 'uint256' },
+    Ballot: { proposalId: 'uint256', support: 'uint8', voter: 'address', nonce: 'uint256' },
     ExtendedBallot: {
     ExtendedBallot: {
       proposalId: 'uint256',
       proposalId: 'uint256',
       support: 'uint8',
       support: 'uint8',
@@ -32,18 +21,8 @@ module.exports = mapValues(
       reason: 'string',
       reason: 'string',
       params: 'bytes',
       params: 'bytes',
     },
     },
-    OverrideBallot: {
-      proposalId: 'uint256',
-      support: 'uint8',
-      voter: 'address',
-      nonce: 'uint256',
-      reason: 'string',
-    },
-    Delegation: {
-      delegatee: 'address',
-      nonce: 'uint256',
-      expiry: 'uint256',
-    },
+    OverrideBallot: { proposalId: 'uint256', support: 'uint8', voter: 'address', nonce: 'uint256', reason: 'string' },
+    Delegation: { delegatee: 'address', nonce: 'uint256', expiry: 'uint256' },
     ForwardRequest: {
     ForwardRequest: {
       from: 'address',
       from: 'address',
       to: 'address',
       to: 'address',
@@ -53,6 +32,29 @@ module.exports = mapValues(
       deadline: 'uint48',
       deadline: 'uint48',
       data: 'bytes',
       data: 'bytes',
     },
     },
+    PackedUserOperation: {
+      sender: 'address',
+      nonce: 'uint256',
+      initCode: 'bytes',
+      callData: 'bytes',
+      accountGasLimits: 'bytes32',
+      preVerificationGas: 'uint256',
+      gasFees: 'bytes32',
+      paymasterAndData: 'bytes',
+    },
+    UserOperationRequest: {
+      sender: 'address',
+      nonce: 'uint256',
+      initCode: 'bytes',
+      callData: 'bytes',
+      accountGasLimits: 'bytes32',
+      preVerificationGas: 'uint256',
+      gasFees: 'bytes32',
+      paymasterVerificationGasLimit: 'uint256',
+      paymasterPostOpGasLimit: 'uint256',
+      validAfter: 'uint48',
+      validUntil: 'uint48',
+    },
   },
   },
   formatType,
   formatType,
 );
 );

+ 125 - 1
test/helpers/erc4337.js

@@ -1,4 +1,4 @@
-const { ethers } = require('hardhat');
+const { ethers, config, entrypoint, senderCreator } = require('hardhat');
 
 
 const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
 const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
 const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
 const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
@@ -83,6 +83,129 @@ class UserOperation {
   }
   }
 }
 }
 
 
+const parseInitCode = initCode => ({
+  factory: '0x' + initCode.replace(/0x/, '').slice(0, 40),
+  factoryData: '0x' + initCode.replace(/0x/, '').slice(40),
+});
+
+/// Global ERC-4337 environment helper.
+class ERC4337Helper {
+  constructor() {
+    this.factoryAsPromise = ethers.deployContract('$Create2');
+  }
+
+  async wait() {
+    this.factory = await this.factoryAsPromise;
+    return this;
+  }
+
+  async newAccount(name, extraArgs = [], params = {}) {
+    const env = {
+      entrypoint: params.entrypoint ?? entrypoint.v08,
+      senderCreator: params.senderCreator ?? senderCreator.v08,
+    };
+
+    const { factory } = await this.wait();
+
+    const accountFactory = await ethers.getContractFactory(name);
+
+    if (params.erc7702signer) {
+      const delegate = await accountFactory.deploy(...extraArgs);
+      const instance = await params.erc7702signer.getAddress().then(address => accountFactory.attach(address));
+      const authorization = await params.erc7702signer.authorize({ address: delegate.target });
+      return new ERC7702SmartAccount(instance, authorization, env);
+    } else {
+      const initCode = await accountFactory
+        .getDeployTransaction(...extraArgs)
+        .then(tx =>
+          factory.interface.encodeFunctionData('$deploy', [0, params.salt ?? ethers.randomBytes(32), tx.data]),
+        )
+        .then(deployCode => ethers.concat([factory.target, deployCode]));
+
+      const instance = await ethers.provider
+        .call({
+          from: env.entrypoint,
+          to: env.senderCreator,
+          data: env.senderCreator.interface.encodeFunctionData('createSender', [initCode]),
+        })
+        .then(result => ethers.getAddress(ethers.hexlify(ethers.getBytes(result).slice(-20))))
+        .then(address => accountFactory.attach(address));
+
+      return new SmartAccount(instance, initCode, env);
+    }
+  }
+}
+
+/// Represent one ERC-4337 account contract.
+class SmartAccount extends ethers.BaseContract {
+  constructor(instance, initCode, env) {
+    super(instance.target, instance.interface, instance.runner, instance.deployTx);
+    this.address = instance.target;
+    this.initCode = initCode;
+    this._env = env;
+  }
+
+  async deploy(account = this.runner) {
+    const { factory: to, factoryData: data } = parseInitCode(this.initCode);
+    this.deployTx = await account.sendTransaction({ to, data });
+    return this;
+  }
+
+  async createUserOp(userOp = {}) {
+    userOp.sender ??= this;
+    userOp.nonce ??= await this._env.entrypoint.getNonce(userOp.sender, 0);
+    if (ethers.isAddressable(userOp.paymaster)) {
+      userOp.paymaster = await ethers.resolveAddress(userOp.paymaster);
+      userOp.paymasterVerificationGasLimit ??= 100_000n;
+      userOp.paymasterPostOpGasLimit ??= 100_000n;
+    }
+    return new UserOperationWithContext(userOp, this._env);
+  }
+}
+
+class ERC7702SmartAccount extends SmartAccount {
+  constructor(instance, authorization, env) {
+    super(instance, undefined, env);
+    this.authorization = authorization;
+  }
+
+  async deploy() {
+    // hardhat signers from @nomicfoundation/hardhat-ethers do not support type 4 txs.
+    // so we rebuild it using "native" ethers
+    await ethers.Wallet.fromPhrase(config.networks.hardhat.accounts.mnemonic, ethers.provider).sendTransaction({
+      to: ethers.ZeroAddress,
+      authorizationList: [this.authorization],
+      gasLimit: 46_000n, // 21,000 base + PER_EMPTY_ACCOUNT_COST
+    });
+
+    return this;
+  }
+}
+
+class UserOperationWithContext extends UserOperation {
+  constructor(userOp, env) {
+    super(userOp);
+    this._sender = userOp.sender;
+    this._env = env;
+  }
+
+  addInitCode() {
+    if (this._sender?.initCode) {
+      return Object.assign(this, parseInitCode(this._sender.initCode));
+    } else throw new Error('No init code available for the sender of this user operation');
+  }
+
+  getAuthorization() {
+    if (this._sender?.authorization) {
+      return this._sender.authorization;
+    } else throw new Error('No EIP-7702 authorization available for the sender of this user operation');
+  }
+
+  hash() {
+    return super.hash(this._env.entrypoint);
+  }
+}
+
 module.exports = {
 module.exports = {
   SIG_VALIDATION_SUCCESS,
   SIG_VALIDATION_SUCCESS,
   SIG_VALIDATION_FAILURE,
   SIG_VALIDATION_FAILURE,
@@ -90,4 +213,5 @@ module.exports = {
   packInitCode,
   packInitCode,
   packPaymasterAndData,
   packPaymasterAndData,
   UserOperation,
   UserOperation,
+  ERC4337Helper,
 };
 };

+ 147 - 0
test/helpers/signers.js

@@ -0,0 +1,147 @@
+const {
+  AbstractSigner,
+  Signature,
+  TypedDataEncoder,
+  assert,
+  assertArgument,
+  concat,
+  dataLength,
+  decodeBase64,
+  getBytes,
+  getBytesCopy,
+  hashMessage,
+  hexlify,
+  sha256,
+  toBeHex,
+} = require('ethers');
+const { secp256r1 } = require('@noble/curves/p256');
+const { generateKeyPairSync, privateEncrypt } = require('crypto');
+
+// Lightweight version of BaseWallet
+class NonNativeSigner extends AbstractSigner {
+  #signingKey;
+
+  constructor(privateKey, provider) {
+    super(provider);
+    assertArgument(
+      privateKey && typeof privateKey.sign === 'function',
+      'invalid private key',
+      'privateKey',
+      '[ REDACTED ]',
+    );
+    this.#signingKey = privateKey;
+  }
+
+  get signingKey() {
+    return this.#signingKey;
+  }
+  get privateKey() {
+    return this.signingKey.privateKey;
+  }
+
+  async getAddress() {
+    throw new Error("NonNativeSigner doesn't have an address");
+  }
+
+  connect(provider) {
+    return new NonNativeSigner(this.#signingKey, provider);
+  }
+
+  async signTransaction(/*tx: TransactionRequest*/) {
+    throw new Error('NonNativeSigner cannot send transactions');
+  }
+
+  async signMessage(message /*: string | Uint8Array*/) /*: Promise<string>*/ {
+    return this.signingKey.sign(hashMessage(message)).serialized;
+  }
+
+  async signTypedData(
+    domain /*: TypedDataDomain*/,
+    types /*: Record<string, Array<TypedDataField>>*/,
+    value /*: Record<string, any>*/,
+  ) /*: Promise<string>*/ {
+    // Populate any ENS names
+    const populated = await TypedDataEncoder.resolveNames(domain, types, value, async name => {
+      assert(this.provider != null, 'cannot resolve ENS names without a provider', 'UNSUPPORTED_OPERATION', {
+        operation: 'resolveName',
+        info: { name },
+      });
+      const address = await this.provider.resolveName(name);
+      assert(address != null, 'unconfigured ENS name', 'UNCONFIGURED_NAME', { value: name });
+      return address;
+    });
+
+    return this.signingKey.sign(TypedDataEncoder.hash(populated.domain, types, populated.value)).serialized;
+  }
+}
+
+class P256SigningKey {
+  #privateKey;
+
+  constructor(privateKey) {
+    this.#privateKey = getBytes(privateKey);
+  }
+
+  static random() {
+    return new this(secp256r1.utils.randomPrivateKey());
+  }
+
+  get privateKey() {
+    return hexlify(this.#privateKey);
+  }
+
+  get publicKey() {
+    const publicKeyBytes = secp256r1.getPublicKey(this.#privateKey, false);
+    return { qx: hexlify(publicKeyBytes.slice(0x01, 0x21)), qy: hexlify(publicKeyBytes.slice(0x21, 0x41)) };
+  }
+
+  sign(digest /*: BytesLike*/) /*: Signature*/ {
+    assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest);
+
+    const sig = secp256r1.sign(getBytesCopy(digest), getBytesCopy(this.#privateKey), { lowS: true });
+
+    return Signature.from({ r: toBeHex(sig.r, 32), s: toBeHex(sig.s, 32), v: sig.recovery ? 0x1c : 0x1b });
+  }
+}
+
+class RSASigningKey {
+  #privateKey;
+  #publicKey;
+
+  constructor(keyPair) {
+    const jwk = keyPair.publicKey.export({ format: 'jwk' });
+    this.#privateKey = keyPair.privateKey;
+    this.#publicKey = { e: decodeBase64(jwk.e), n: decodeBase64(jwk.n) };
+  }
+
+  static random(modulusLength = 2048) {
+    return new this(generateKeyPairSync('rsa', { modulusLength }));
+  }
+
+  get privateKey() {
+    return hexlify(this.#privateKey);
+  }
+
+  get publicKey() {
+    return { e: hexlify(this.#publicKey.e), n: hexlify(this.#publicKey.n) };
+  }
+
+  sign(digest /*: BytesLike*/) /*: Signature*/ {
+    assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest);
+    // SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes)
+    return {
+      serialized: hexlify(
+        privateEncrypt(this.#privateKey, getBytes(concat(['0x3031300d060960864801650304020105000420', digest]))),
+      ),
+    };
+  }
+}
+
+class RSASHA256SigningKey extends RSASigningKey {
+  sign(digest /*: BytesLike*/) /*: Signature*/ {
+    assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest);
+    return super.sign(sha256(getBytes(digest)));
+  }
+}
+
+module.exports = { NonNativeSigner, P256SigningKey, RSASigningKey, RSASHA256SigningKey };

+ 25 - 0
test/utils/cryptography/ERC7739.test.js

@@ -1,5 +1,6 @@
 const { ethers } = require('hardhat');
 const { ethers } = require('hardhat');
 const { shouldBehaveLikeERC1271 } = require('./ERC1271.behavior');
 const { shouldBehaveLikeERC1271 } = require('./ERC1271.behavior');
+const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../../helpers/signers');
 
 
 describe('ERC7739', function () {
 describe('ERC7739', function () {
   describe('for an ECDSA signer', function () {
   describe('for an ECDSA signer', function () {
@@ -10,4 +11,28 @@ describe('ERC7739', function () {
 
 
     shouldBehaveLikeERC1271({ erc7739: true });
     shouldBehaveLikeERC1271({ erc7739: true });
   });
   });
+
+  describe('for a P256 signer', function () {
+    before(async function () {
+      this.signer = new NonNativeSigner(P256SigningKey.random());
+      this.mock = await ethers.deployContract('ERC7739P256Mock', [
+        this.signer.signingKey.publicKey.qx,
+        this.signer.signingKey.publicKey.qy,
+      ]);
+    });
+
+    shouldBehaveLikeERC1271({ erc7739: true });
+  });
+
+  describe('for an RSA signer', function () {
+    before(async function () {
+      this.signer = new NonNativeSigner(RSASHA256SigningKey.random());
+      this.mock = await ethers.deployContract('ERC7739RSAMock', [
+        this.signer.signingKey.publicKey.e,
+        this.signer.signingKey.publicKey.n,
+      ]);
+    });
+
+    shouldBehaveLikeERC1271({ erc7739: true });
+  });
 });
 });