+
+```solidity
+import "@openzeppelin-accounts/contracts/extensions/AccountERC8043Upgradeable.sol";
+```
+
+Extension of `AccountERC7579Upgradeable` that implements atomic module installation and usage
+through _init mode_ following [ERC-8043](https://eip.tools/eip/8043).
+
+This extension allows installing and using one or more modules within the same user operation.
+Init mode is triggered when `userOp.initCode` starts with the 4-byte magic value `0x80438043`.
+The remaining bytes of `initCode` carry the ABI-encoded module installation payload and an
+EIP-712 installation signature from the account owner.
+
+Because the `ERC-4337` EntryPoint v0.9 silently ignores `initCode` for already-deployed accounts,
+and because `initCode` is included in the `userOpHash`, the module installation parameters are
+both inert at the EntryPoint level and cryptographically committed to by the user operation signature.
+
+This pattern is particularly useful for session keys and other modules that need to be
+installed and immediately used, reducing user operation count from N+1 to 1.
+
+
+Requires EntryPoint v0.9 or later.
+
+
+
+
+Validates a user operation, checking for the init mode magic value in `initCode`.
+
+If `userOp.initCode` starts with `INIT_MODE_MAGIC_VALUE`, enters init mode:
+
+1. Decodes module installation data from `userOp.initCode[4:]`
+2. Validates the EIP-712 `installationSignature` over the module installation parameters
+3. Installs the modules if validation succeeds
+4. Continues with normal user operation validation using the forwarded `signature`
+Otherwise, falls back to `super._validateUserOp`.
+
+
+
+
diff --git a/content/accounts/api/index.mdx b/content/accounts/api/index.mdx
new file mode 100644
index 00000000..49ddeec4
--- /dev/null
+++ b/content/accounts/api/index.mdx
@@ -0,0 +1,589 @@
+---
+title: "API Reference"
+description: "Smart contract API reference"
+---
+
+This package provides a production-ready ERC-4337 and ERC-7579 smart account and a set of ERC-7579 modules that can be installed on any ERC-7579 compliant account.
+
+- [`OpenAccount`](#OpenAccount): A modular smart account with ERC-7579 module support, ERC-1271 signature validation and ERC-721/ERC-1155 holder capabilities.
+- [`OpenAccountImplementation`](#OpenAccountImplementation): Upgradeable implementation of [`OpenAccount`](#OpenAccount) with ERC-8043 init mode, used by [`OpenAccountFactory`](#OpenAccountFactory).
+- [`OpenAccountFactory`](#OpenAccountFactory): A factory that deploys [`OpenAccountImplementation`](#OpenAccountImplementation) instances as ERC-1967 proxy clones at deterministic addresses.
+
+Modules, extensions and utilities are documented in their own pages:
+
+- [Modules](/accounts/api/modules): Validators, executors and the signer, multisig and session key primitives they are built from.
+- [Extensions](/accounts/api/extensions): Account extensions such as ERC-8043 init mode.
+- [Utilities](/accounts/api/utils): Libraries and helpers for multisignatures, crosschain signatures and FHE coordination.
+
+## Account
+
+[`OpenAccount`](#OpenAccount)
+
+[`OpenAccountImplementation`](#OpenAccountImplementation)
+
+## Factory
+
+[`OpenAccountFactory`](#OpenAccountFactory)
+
+
+
+
+
+Initializes the account with a validator or executor module.
+
+It receives an array of `moduleTypeIds`, `modules`, and `initDatas` that are used
+to install modules during initialization. It also receives a `mode` and `executionCalldata`
+that are used to execute the account, these can be used to configure the account.
+
+Set the `configCalldata` to `bytes("")` to skip the configuration call.
+
+
+
+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 that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
+`upgradeToAndCall`.
+
+Normally, this function will use an [access control](/contracts/5.x/api/access) modifier such as `Ownable-onlyOwner`.
+
+```solidity
+function _authorizeUpgrade(address) internal onlyOwner {}
+```
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/OpenAccountFactory.sol";
+```
+
+A factory contract to create OpenAccount instances on demand.
+
+Each account is created as a minimal ERC-1967 proxy clone that delegates to the implementation contract.
+The proxy follows the ERC-1967 standard for proxy storage slots, storing the implementation address at
+`bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)`.
+
+See https://eips.ethereum.org/EIPS/eip-1967 for the full specification.
+
+
+
+Predicts the address of an OpenAccount before it is created.
+
+The address is deterministically computed based on the initialization parameters and salt.
+Each account is deployed as an ERC-1967 proxy clone.
+
+
+
+Creates and initializes a new OpenAccount instance.
+
+Deploys a minimal ERC-1967 proxy clone using CREATE2 for deterministic addresses.
+The proxy delegates all calls to the implementation contract and maintains its own storage.
+If an account at the predicted address already exists, returns that address without redeploying.
+
+
+
+
diff --git a/content/accounts/api/modules.mdx b/content/accounts/api/modules.mdx
new file mode 100644
index 00000000..886de0ab
--- /dev/null
+++ b/content/accounts/api/modules.mdx
@@ -0,0 +1,9208 @@
+---
+title: "Modules"
+description: "Smart contract modules utilities and implementations"
+---
+
+ERC-7579 modules that can be installed on any compliant account. Validators authorize user operations and ERC-1271 signatures, while executors trigger actions through the account's `executeFromExecutor` function. Both are built on top of abstract signer modules that store per-account signer configuration.
+
+- [`ERC7579ECDSA`](#ERC7579ECDSA), [`ERC7579P256`](#ERC7579P256), [`ERC7579WebAuthn`](#ERC7579WebAuthn): Single-signer modules for ECDSA, P-256 and WebAuthn keys.
+- [`ERC7579Multisig`](#ERC7579Multisig), [`ERC7579MultisigWeighted`](#ERC7579MultisigWeighted): Threshold and weighted multisignature modules using ERC-7913 signers.
+- [`ERC7579MultisigConfidential`](#ERC7579MultisigConfidential), [`ERC7579MultisigWeightedConfidential`](#ERC7579MultisigWeightedConfidential): Multisignature modules that keep signers and thresholds encrypted using FHE.
+- [`ERC7579SessionKeys`](#ERC7579SessionKeys): Scoped, time-limited signing authority with per-session policies.
+- [`ERC7579Validator`](#ERC7579Validator): Abstract validator module, with implementations for each signer module.
+- [`ERC7579Executor`](#ERC7579Executor): Abstract executor module, extended by [`ERC7579DelayedExecutor`](#ERC7579DelayedExecutor) for time-delayed execution.
+- [`ERC7579SocialRecoveryExecutor`](#ERC7579SocialRecoveryExecutor): Time-delayed account recovery authorized by a weighted set of guardians.
+- [`ERC7579NativeSpender`](#ERC7579NativeSpender), [`ERC7579ERC20Spender`](#ERC7579ERC20Spender): Session-key-driven spending limits for native and ERC-20 tokens.
+
+## Signers
+
+[`ERC7579ECDSA`](#ERC7579ECDSA)
+
+[`ERC7579P256`](#ERC7579P256)
+
+[`ERC7579WebAuthn`](#ERC7579WebAuthn)
+
+[`ERC7579SessionKeys`](#ERC7579SessionKeys)
+
+### Multisig
+
+[`ERC7579Multisig`](#ERC7579Multisig)
+
+[`ERC7579MultisigWeighted`](#ERC7579MultisigWeighted)
+
+[`ERC7579MultisigConfidential`](#ERC7579MultisigConfidential)
+
+[`ERC7579MultisigWeightedConfidential`](#ERC7579MultisigWeightedConfidential)
+
+## Validators
+
+[`ERC7579Validator`](#ERC7579Validator)
+
+[`ERC7579ECDSAValidator`](#ERC7579ECDSAValidator)
+
+[`ERC7579P256Validator`](#ERC7579P256Validator)
+
+[`ERC7579WebAuthnValidator`](#ERC7579WebAuthnValidator)
+
+[`ERC7579MultisigValidator`](#ERC7579MultisigValidator)
+
+[`ERC7579MultisigWeightedValidator`](#ERC7579MultisigWeightedValidator)
+
+[`ERC7579MultisigConfidentialValidator`](#ERC7579MultisigConfidentialValidator)
+
+[`ERC7579MultisigWeightedConfidentialValidator`](#ERC7579MultisigWeightedConfidentialValidator)
+
+[`ERC7579SessionKeysValidator`](#ERC7579SessionKeysValidator)
+
+## Executors
+
+[`ERC7579Executor`](#ERC7579Executor)
+
+[`ERC7579DelayedExecutor`](#ERC7579DelayedExecutor)
+
+[`ERC7579SocialRecoveryExecutor`](#ERC7579SocialRecoveryExecutor)
+
+[`ERC7579NativeSpender`](#ERC7579NativeSpender)
+
+[`ERC7579ERC20Spender`](#ERC7579ERC20Spender)
+
+
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579ECDSA.sol";
+```
+
+Implementation of an `IERC7579Module` that uses an ECDSA signer for signature
+validation.
+
+This module provides a base implementation for ECDSA signer validation that can be
+attached to any function through the [`ERC7579ECDSA._validateECDSA`](#ERC7579ECDSA-_validateECDSA-address-bytes32-bytes-) internal function.
+
+
+
+Sets up the module's initial configuration when installed by an account.
+The `data` parameter should contain the signer address (20 bytes minimum).
+
+If the account doesn't already have a signer configured, the first 20 bytes
+of `data` will be used as the authorized signer address.
+See [`ERC7579ECDSA._extractSigner`](#ERC7579ECDSA-_extractSigner-bytes-) for more details about the encoding.
+
+Requirements:
+
+* The `data` must be at least 20 bytes. Reverts with [`ERC7579ECDSA.ERC7579ECDSAInvalidSignerLength`](#ERC7579ECDSA-ERC7579ECDSAInvalidSignerLength--) if not.
+* The `data` must contain a valid signer address. Reverts with [`ERC7579ECDSA.ERC7579ECDSAInvalidSigner`](#ERC7579ECDSA-ERC7579ECDSAInvalidSigner-address-) if not.
+* The `account` must have no signer installed. Reverts with [`ERC7579ECDSA.ERC7579ECDSAAlreadyInstalled`](#ERC7579ECDSA-ERC7579ECDSAAlreadyInstalled--) if not.
+
+Emits a [`ERC7579ECDSA.ERC7579ECDSASignerSet`](#ERC7579ECDSA-ERC7579ECDSASignerSet-address-address-) event if the signer is set.
+
+
+
+Cleans up the module's configuration when uninstalled from an account.
+Removes the authorized signer for the calling account.
+
+If the account has a configured signer, it will be cleared. If no signer
+is configured, this function will behave as a no-op.
+
+Emits a [`ERC7579ECDSA.ERC7579ECDSASignerSet`](#ERC7579ECDSA-ERC7579ECDSASignerSet-address-address-) event if the signer is cleared.
+
+
+
+Sets the signer for the specified account.
+
+Requirements:
+
+* The `newSigner` must not be the zero address.
+* The caller must be the specified account.
+
+Emits a [`ERC7579ECDSA.ERC7579ECDSASignerSet`](#ERC7579ECDSA-ERC7579ECDSASignerSet-address-address-) event.
+
+
+
+Internal version of [`ERC7579ECDSA.setSigner`](#ERC7579ECDSA-setSigner-address-address-) without access control.
+
+Requirements:
+
+* The `newSigner` must not be the zero address.
+
+Emits a [`ERC7579ECDSA.ERC7579ECDSASignerSet`](#ERC7579ECDSA-ERC7579ECDSASignerSet-address-address-) event.
+
+
+
+Validates an ECDSA signature against the configured signer for the account.
+
+This function recovers the signer address from the provided signature and hash,
+then compares it against the authorized signer for the given account.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579Multisig.sol";
+```
+
+Implementation of an `IERC7579Module` that uses ERC-7913 signers for multisignature
+validation.
+
+This module provides a base implementation for multisignature validation that can be
+attached to any function through the [`ERC7579Multisig._validateMultisig`](#ERC7579Multisig-_validateMultisig-address-bytes32-bytes---bytes---) internal function. The signers
+are represented using the ERC-7913 format, which concatenates a verifier address and
+a key: `verifier || key`.
+
+A smart account with this module installed can require multiple signers to approve
+operations before they are executed, such as requiring 3-of-5 guardians to approve
+a social recovery operation.
+
+
+
+Sets up the module's initial configuration when installed by an account.
+See `IERC7579Module-onInstall`. The `initData` encodes `signers` and `threshold`.
+
+See [`ERC7579Multisig._decodeMultisigInitData`](#ERC7579Multisig-_decodeMultisigInitData-bytes-) for the encoding details.
+
+
+An account can only call onInstall once. If called directly by the account,
+the signers will be set to the provided data. Future installations will revert with
+[`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--).
+
+
+Requirements:
+
+* The `account` must have no signers installed. Reverts with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--) if not.
+* The `signers` array must be non-empty. Reverts with [`ERC7579Multisig.ERC7579MultisigInvalidInitData`](#ERC7579Multisig-ERC7579MultisigInvalidInitData--) if not.
+* Those of [`ERC7579Multisig._decodeMultisigInitData`](#ERC7579Multisig-_decodeMultisigInitData-bytes-) for the decoding requirements.
+* Those of [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---) and [`ERC7579Multisig._setThreshold`](#ERC7579Multisig-_setThreshold-address-uint64-) for signer configuration requirements.
+
+
+
+Cleans up module's configuration when uninstalled from an account.
+Clears all signers and resets the threshold.
+
+See `IERC7579Module-onUninstall`.
+
+
+This function has unbounded gas costs and may become uncallable if the set grows too large.
+See `EnumerableSet-clear`.
+
+
+
+
+Returns a slice of the set of authorized signers for the specified account.
+
+Using `start = 0` and `end = type(uint64).max` will return the entire set of signers.
+
+
+Depending on the `start` and `end`, this operation can copy a large amount of data to memory, which
+can be expensive. This is designed for view accessors queried without gas fees. Using it in state-changing
+functions may become uncallable if the slice grows too large.
+
+
+
+
+Adds new signers to the authorized set for the calling account.
+Can only be called by the account itself.
+
+Requirements:
+
+* Each of `newSigners` must be at least 20 bytes long.
+* Each of `newSigners` must not be already authorized.
+
+
+
+Removes signers from the authorized set for the calling account.
+Can only be called by the account itself.
+
+Requirements:
+
+* Each of `oldSigners` must be authorized.
+* After removal, the threshold must still be reachable.
+
+
+
+Sets the threshold for the calling account.
+Can only be called by the account itself.
+
+Requirements:
+
+* The threshold must be reachable with the current number of signers.
+
+
+
+Adds the `newSigners` to those allowed to sign on behalf of the account.
+
+Requirements:
+
+* Each of `newSigners` must be at least 20 bytes long. Reverts with [`ERC7579Multisig.ERC7579MultisigInvalidSigner`](#ERC7579Multisig-ERC7579MultisigInvalidSigner-bytes-) if not.
+* Each of `newSigners` must not be authorized. Reverts with [`ERC7579Multisig.ERC7579MultisigAlreadyExists`](#ERC7579Multisig-ERC7579MultisigAlreadyExists-bytes-) if it already exists.
+
+
+This function does not validate that signers are controlled or represent appropriate entities. Integrators
+must ensure signers are properly validated before adding them. Problematic signers can compromise
+the multisig's security or functionality. Examples include uncontrolled addresses (e.g., `address(0)`),
+the account's own address (which may cause recursive validation loops), or contracts that may unintentionally
+allow arbitrary validation (e.g. using the identity precompile at `address(0x04)`, which would return the
+ERC-1271 magic value for any `isValidSignature` call).
+
+
+
+
+Removes the `oldSigners` from the authorized signers for the account.
+
+Requirements:
+
+* Each of `oldSigners` must be authorized. Reverts with [`ERC7579Multisig.ERC7579MultisigNonexistentSigner`](#ERC7579Multisig-ERC7579MultisigNonexistentSigner-bytes-) if not.
+* The threshold must remain reachable after removal. See [`ERC7579Multisig._validateReachableThreshold`](#ERC7579Multisig-_validateReachableThreshold-address-) for details.
+
+
+
+Sets the signatures `threshold` required to approve a multisignature operation.
+
+Requirements:
+
+* The threshold must be greater than 0. Reverts with [`ERC7579Multisig.ERC7579MultisigZeroThreshold`](#ERC7579Multisig-ERC7579MultisigZeroThreshold--) if not.
+* The threshold must be reachable with the current number of signers. See [`ERC7579Multisig._validateReachableThreshold`](#ERC7579Multisig-_validateReachableThreshold-address-) for details.
+
+
+
+Validates the current threshold is reachable with the number of `signers`.
+
+Requirements:
+
+* The number of signers must be >= the threshold. Reverts with [`ERC7579Multisig.ERC7579MultisigUnreachableThreshold`](#ERC7579Multisig-ERC7579MultisigUnreachableThreshold-uint64-uint64-) if not.
+
+
+
+Validates the multisignature operation by checking the threshold and the signatures.
+
+Where `signingSigners` are the authorized signers and `signatures` are their corresponding
+signatures of the operation `hash`.
+
+
+
+Validates the signatures using the signers and their corresponding signatures.
+Returns whether the signers are authorized, are not duplicated and the signatures
+are valid for the given hash.
+
+
+Sorting the signers by their `keccak256` hash will improve the gas
+efficiency of this function.
+
+
+
+
+Validates that the number of signers meets the [`ERC7579Multisig.threshold`](#ERC7579Multisig-threshold-address-) requirement.
+Assumes the signers were already validated. See [`ERC7579Multisig._validateSignatures`](#ERC7579Multisig-_validateSignatures-address-bytes32-bytes---bytes---) for more details.
+
+
+
+Decodes multisig initialization data from calldata without memory allocation.
+
+The initData should be encoded as:
+`abi.encode(bytes[] signers, uint64 threshold)`
+
+Requirements:
+
+* The `initData` must be at least 96 bytes long to contain the minimum ABI-encoded structure.
+* The signers array offset must be properly aligned and within bounds.
+* The array length field must be accessible within the provided data.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579MultisigConfidential.sol";
+```
+
+Implementation of an `IERC7579Module` that provides confidential multisignature
+validation.
+
+This module provides a base implementation for multisignature validation that can be
+attached to any function through the [`ERC7579Multisig._validateMultisig`](#ERC7579Multisig-_validateMultisig-address-bytes32-bytes---bytes---) internal function. Unlike
+the standard [`ERC7579Multisig`](#ERC7579Multisig) module, this confidential version uses Fully Homomorphic
+Encryption (FHE) to keep signer identities and the threshold value encrypted on-chain.
+This provides privacy benefits by hiding the identity of authorized signers and the
+number of signatures required to approve operations.
+
+A smart account with this module installed can require multiple signers to approve
+operations, such as requiring 3-of-5 guardians to approve a social recovery operation,
+while keeping both the signer identities and threshold confidential.
+
+Operations are identified by their hash, which represents arbitrary data for the application
+to validate. Therefore, replay protection and other considersations about the operation must be
+handled by the application.
+
+To request disclosure of the multisignature validation result for a given operation, the
+application must call `requestDiscloseOperation` to make the result publicly decryptable,
+then call `discloseOperation` to verify the decryption proof and get the result.
+
+
+Due to the non-deterministic nature of FHE encryption, encrypting the same plaintext
+uint256 value multiple times produces different ciphertexts. This means the same underlying signer
+can be added multiple times with different encrypted handles, and duplicate detection by
+comparing encrypted handles is not possible. Consider duplicate signers may cause the
+threshold to become unreachable since [`ERC7579Multisig._validateSignatures`](#ERC7579Multisig-_validateSignatures-address-bytes32-bytes---bytes---) checks for duplicates.
+
+
+
+This module supports both address-based signers (EOAs and ERC-1271 contracts) and
+ERC-7913 signers (arbitrary keys with verifiers). Addresses are cast to uint256 and
+encrypted, while ERC-7913 keys are hashed with `keccak256(verifier || key)` before
+encryption. This hybrid approach allows addresses to be recognizable after decryption,
+while ERC-7913 keys require an off-chain mapping from hash to preimage.
+
+
+
+The threshold can be set to 0 given it can't be validated in runtime. The account
+operators must rely on external validation to ensure the threshold is not 0, which allows
+arbitrary execution.
+
+
+
+A per-account configuration nonce (from `Nonces`) is incremented on every configuration
+change ([`ERC7579Multisig.addSigners`](#ERC7579Multisig-addSigners-bytes---), [`ERC7579Multisig.removeSigners`](#ERC7579Multisig-removeSigners-bytes---), [`ERC7579Multisig.setThreshold`](#ERC7579Multisig-setThreshold-uint64-), [`ERC7579ECDSA.onInstall`](#ERC7579ECDSA-onInstall-bytes-)). Disclosure state is scoped
+to the nonce active at the time of [`ERC7579MultisigConfidential.requestDiscloseOperation`](#ERC7579MultisigConfidential-requestDiscloseOperation-address-bytes32-bytes---euint256---bytes---), so any configuration change
+automatically invalidates outstanding disclosure entries and closes the stale-approval window.
+
+
+
+When using ERC-1271 contract signers, signature validity can change over time (e.g. the
+signer contract updates its owner). Because [`ERC7579MultisigConfidential.requestDiscloseOperation`](#ERC7579MultisigConfidential-requestDiscloseOperation-address-bytes32-bytes---euint256---bytes---) snapshots the validation
+result, a later call to [`ERC7579ECDSAValidator._rawERC7579Validation`](#ERC7579ECDSAValidator-_rawERC7579Validation-address-bytes32-bytes-) may accept a stale authorization even if the
+ERC-1271 signer would now reject the signature. Prefer signers with non-revocable signatures
+(e.g. EOAs), or call [`ERC7579MultisigConfidential.dismissDisclosure`](#ERC7579MultisigConfidential-dismissDisclosure--) to invalidate outstanding disclosures when an
+ERC-1271 signer's validity changes.
+
+
+
+This module provides confidentiality of signer identities and threshold values, but
+does not provide confidentiality of threshold relationships among participants. Once a valid
+disclosure is made via [`ERC7579MultisigConfidential.requestDiscloseOperation`](#ERC7579MultisigConfidential-requestDiscloseOperation-address-bytes32-bytes---euint256---bytes---), any party that has observed valid signatures
+for the operation hash (e.g. from mempool or calldata) can probe arbitrary signer subsets to
+infer the threshold or weight distribution. To limit the exposure window, accounts can perform
+a signer reset after sensitive operations: re-encrypting the signer set with fresh FHE handles
+(and optionally updating the threshold) invalidates any oracle knowledge accumulated over the
+previous handle set, since FHE encryption is non-deterministic and old handles are unlinkable
+to new ones.
+
+
+
+
+Sets up the module's initial configuration when installed by an account.
+See `IERC7579Module-onInstall`. The `initData` encodes externally encrypted
+`signerIds` and `threshold`. See [`ERC7579Multisig._decodeMultisigInitData`](#ERC7579Multisig-_decodeMultisigInitData-bytes-) for the encoding details.
+
+
+An account can only call `onInstall` once. If called directly by the account,
+the signerIds will be set to the provided data. Future installations will revert with
+[`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--).
+
+
+Requirements:
+
+* The `account` must have no signers installed. Reverts with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--) if not.
+* The `signerIds` array must be non-empty. Reverts with [`ERC7579Multisig.ERC7579MultisigEmptySigners`](#ERC7579Multisig-ERC7579MultisigEmptySigners--) if not.
+* Those of [`ERC7579Multisig._decodeMultisigInitData`](#ERC7579Multisig-_decodeMultisigInitData-bytes-) for the decoding requirements.
+* Those of [`ERC7579Multisig.addSigners`](#ERC7579Multisig-addSigners-bytes---) for encrypted signers configuration requirements.
+
+
+
+Cleans up module's configuration when uninstalled from an account.
+Clears all signerIds and resets the threshold.
+
+See `IERC7579Module-onUninstall`.
+
+
+This function has unbounded gas costs and may become uncallable if the set grows too large.
+See `EnumerableSet-clear`.
+
+
+
+
+Returns a slice of the set of authorized encrypted signers for the specified account.
+
+The returned signers are encrypted uint256 values (euint256). Using `start = 0` and
+`end = type(uint32).max` will return the entire set of signerIds.
+
+
+Depending on the `start` and `end`, this operation can copy a large amount of data to memory, which
+can be expensive. This is designed for view accessors queried without gas fees. Using it in state-changing
+functions may become uncallable if the slice grows too large.
+
+
+
+
+Returns whether the multisignature validation result for an operation (hash) is requested for
+disclosure under the account's current configuration nonce.
+
+
+Returns an uninitialized handle if the configuration has changed since the disclosure was requested.
+
+
+
+
+Returns whether the multisignature validation result for an operation (hash) is disclosed
+under the account's current configuration nonce.
+
+
+Returns false if the configuration has changed since the operation was disclosed.
+
+
+
+
+Adds new encrypted signerIds to the authorized set for the calling account.
+Can only be called by the account itself. See [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---).
+
+Increments the account's configuration nonce, invalidating any outstanding disclosure entries
+made under the previous nonce.
+
+The signerIds are provided as externally encrypted uint256 values. Each signerId represents
+either an address (cast to uint256) or an ERC-7913 key (hashed with `keccak256(verifier || key)`
+and cast to uint256). The caller must encrypt the signerId off-chain before calling this function.
+
+Requirements:
+
+* Those of [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---) for additional requirements.
+
+
+
+Removes encrypted signerIds from the authorized set for the calling account.
+Can only be called by the account itself. See [`ERC7579Multisig._removeSigners`](#ERC7579Multisig-_removeSigners-address-bytes---).
+
+Increments the account's configuration nonce, invalidating any outstanding disclosure entries
+made under the previous nonce.
+
+
+
+Sets the encrypted threshold for the calling account.
+Can only be called by the account itself. See [`ERC7579Multisig._setThreshold`](#ERC7579Multisig-_setThreshold-address-uint64-).
+
+Increments the account's configuration nonce, invalidating any outstanding disclosure entries
+made under the previous nonce.
+
+
+
+Requests disclosure of the multisignature validation result for an operation (hash).
+
+This function validates the multisignature using encrypted values and makes the
+result publicly decryptable. Applications that require using the validation result
+must call this function off-chain first to enable the decryption proof to be included
+in a subsequent call to `discloseOperation` to verify the proof.
+
+
+This function does not perform any access control. Anyone can request disclosure
+of the multisignature validation result for any operation.
+
+
+
+Each call produces a secret-dependent encrypted boolean that is made publicly
+decryptable. A caller who possesses valid signatures for the operation `hash` can
+probe arbitrary signer subsets across multiple calls to infer the threshold or weight
+distribution, since the decrypted result reveals whether the chosen subset meets the
+threshold.
+
+
+Emits a [`ERC7579MultisigConfidential.ERC7579MultisigOperationDiscloseRequested`](#ERC7579MultisigConfidential-ERC7579MultisigOperationDiscloseRequested-address-address-bytes32-ebool-) event.
+
+
+
+Verifies the decryption proof for a disclosed operation (hash).
+
+Requirements:
+
+* The decryption proof must be cryptographically valid. Reverts if invalid.
+
+Emits a [`ERC7579MultisigConfidential.ERC7579MultisigOperationDisclosed`](#ERC7579MultisigConfidential-ERC7579MultisigOperationDisclosed-address-bytes32-ebool-) event.
+
+
+
+Same as [`ERC7579MultisigConfidential.discloseOperation`](#ERC7579MultisigConfidential-discloseOperation-address-bytes32-bytes-) but with the relayer set to `msg.sender`.
+
+
+
+Consumes the caller's configuration nonce, invalidating all outstanding disclosure
+entries. Useful when an ERC-1271 signer's validity changes and the account needs to
+revoke previously snapshotted authorization results.
+
+
+
+Adds the encrypted `newSignerIds` to those allowed to sign on behalf of the `account`.
+
+Requirements:
+
+* The number of signers must not exceed `type(uint32).max`. Reverts with [`ERC7579MultisigConfidential.ERC7579MultisigTooManySigners`](#ERC7579MultisigConfidential-ERC7579MultisigTooManySigners--) if exceeded.
+
+
+This function does not increment the account's configuration nonce.
+Callers that change the configuration must call `Nonces-_useNonce` to invalidate
+outstanding disclosure entries.
+
+
+
+FHE encryption is non-deterministic. Encrypting the same plaintext uint256 value multiple times
+produces different ciphertexts and thus different handles. This allows the same underlying
+signer to be added multiple times, so duplicate detection by comparing encrypted handles
+is not possible.
+
+
+Emits [`ERC7579MultisigConfidential.ERC7579MultisigSignerAdded`](#ERC7579MultisigConfidential-ERC7579MultisigSignerAdded-address-euint256-) for each new signer.
+
+
+
+Removes the encrypted `oldSignerIds` from the authorized signerIds for the account.
+
+Requirements:
+
+* Each of `oldSignerIds` must be authorized. Reverts with [`ERC7579Multisig.ERC7579MultisigNonexistentSigner`](#ERC7579Multisig-ERC7579MultisigNonexistentSigner-bytes-) if not.
+* After removal, at least one signer must remain. Reverts with [`ERC7579Multisig.ERC7579MultisigEmptySigners`](#ERC7579Multisig-ERC7579MultisigEmptySigners--) if not.
+
+
+This function does not increment the account's configuration nonce.
+Callers that change the configuration must call `Nonces-_useNonce` to invalidate
+outstanding disclosure entries.
+
+
+
+The threshold will be automatically capped at the number of remaining signers to ensure it's
+always reachable.
+
+
+Emits [`ERC7579MultisigConfidential.ERC7579MultisigSignerRemoved`](#ERC7579MultisigConfidential-ERC7579MultisigSignerRemoved-address-euint256-) for each removed signer.
+
+
+
+Sets the encrypted signatures `threshold` required to approve a multisignature operation.
+
+
+This function does not increment the account's configuration nonce.
+Callers that change the configuration must call `Nonces-_useNonce` to invalidate
+outstanding disclosure entries.
+
+
+
+The threshold is automatically capped at the current number of signers to ensure it's
+always reachable.
+
+
+Emits [`ERC7579MultisigConfidential.ERC7579MultisigThresholdSet`](#ERC7579MultisigConfidential-ERC7579MultisigThresholdSet-address-euint32-) with the final threshold value.
+
+
+
+Validates the multisignature operation by checking the threshold and the signatures.
+
+The validation is performed using encrypted values to maintain confidentiality. The function
+verifies that the number of valid signatures meets or exceeds the encrypted threshold.
+
+Where `signingSigners` are the plaintext signers (addresses or ERC-7913 keys as bytes),
+`signerIdHandles` are their corresponding encrypted id handles, and `signatures` are their
+signatures of the operation `hash`.
+
+
+Given that both the threshold and signers are encrypted, it's possible to pass-in
+signatures that don't count towards the threshold.
+
+
+
+
+Validates the signatures using the signers and their corresponding signatures.
+Returns the number of valid signatures as an encrypted value.
+
+To count towards the threshold, for each signer:
+
+1. The handle must exist in the authorized set
+2. The handle must match the corresponding plaintext signer
+ (address cast to uint256 or keccak256 hash for ERC-7913 keys)
+3. The signature must be valid for the given hash
+4. The signer must not be a duplicate of a previous signer in the array
+
+The signers should be ordered by their uint256 representation to ensure efficient duplication check.
+Unordered signers are supported, but the uniqueness check will be more expensive. See [`ERC7579MultisigConfidential._toId`](#ERC7579MultisigConfidential-_toId-bytes-).
+
+Requirements:
+
+* The `signingSigners`, `signerIdHandles`, and `signatures` arrays must have the same length.
+ Reverts with [`ERC7579MultisigConfidential.ERC7579MultisigMismatchedValidationLength`](#ERC7579MultisigConfidential-ERC7579MultisigMismatchedValidationLength-uint256-uint256-uint256-) if not.
+
+
+
+Decodes multisig initialization data from calldata without memory allocation.
+
+The initData should be encoded as:
+`abi.encode(bytes inputProof, externalEuint256[] signerIds, externalEuint32 threshold)`
+
+Requirements:
+
+* The `initData` must be at least 160 bytes long to contain the minimum structure.
+* All offsets must be properly aligned and within bounds.
+* All array length fields must be accessible within the provided data.
+* The proof data must be fully contained within the provided data.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579MultisigWeighted.sol";
+```
+
+Extension of [`ERC7579Multisig`](#ERC7579Multisig) that supports weighted signatures.
+
+This module extends the multisignature module to allow assigning different weights
+to each signer, enabling more flexible governance schemes. For example, some guardians
+could have higher weight than others, allowing for weighted voting or prioritized authorization.
+
+Example use case:
+
+A smart account with this module installed can schedule social recovery operations
+after obtaining approval from guardians with sufficient total weight (e.g., requiring
+a total weight of 10, with 3 guardians weighted as 5, 3, and 2), and then execute them
+after the time delay has passed.
+
+
+When setting a threshold value, ensure it matches the scale used for signer weights.
+For example, if signers have weights like 1, 2, or 3, then a threshold of 4 would require
+signatures with a total weight of at least 4 (e.g., one with weight 1 and one with weight 3).
+
+
+
+
+Sets up the module's initial configuration when installed by an account.
+Besides the standard delay and signer configuration, this can also include
+signer weights.
+
+The initData should be encoded as:
+`abi.encode(bytes[] signers, uint64 threshold, uint64[] weights)`
+
+
+An account can only call onInstall once. If called directly by the account,
+the signer will be set to the provided data. Future installations will revert with
+[`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--).
+
+
+
+This function skips `super.onInstall` to avoid setting the threshold during installation
+since it can be greater than the total amount of signers. This function adds the signers, sets the weights,
+and sets the threshold.
+
+
+Requirements:
+
+* The `account` must have no signers installed. Reverts with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--) if not.
+* Those of [`ERC7579MultisigWeighted._decodeMultisigWeightedInitData`](#ERC7579MultisigWeighted-_decodeMultisigWeightedInitData-bytes-) for the decoding requirements.
+
+
+
+Cleans up module's configuration when uninstalled from an account.
+Clears all signers, weights, and total weights.
+
+See [`ERC7579Multisig.onUninstall`](#ERC7579Multisig-onUninstall-bytes-).
+
+
+
+Decodes weighted multisig initialization data from calldata without memory allocation.
+
+The initData should be encoded as:
+`abi.encode(bytes[] signers, uint64 threshold, uint64[] weights)`
+
+Requirements:
+
+* The `initData` must be at least 160 bytes long to contain the minimum ABI-encoded structure.
+* The signers array offset must be properly aligned and within bounds.
+* The weights array offset must be properly aligned and within bounds.
+* The array length fields must be accessible within the provided data.
+
+
+
+Sets weights for multiple signers at once. Internal version without access control.
+
+Requirements:
+
+* `signers` and `weights` arrays must have the same length. Reverts with [`ERC7579MultisigWeighted.ERC7579MultisigMismatchedLength`](#ERC7579MultisigWeighted-ERC7579MultisigMismatchedLength--) on mismatch.
+* Each signer must exist in the set of authorized signers. Reverts with [`ERC7579Multisig.ERC7579MultisigNonexistentSigner`](#ERC7579Multisig-ERC7579MultisigNonexistentSigner-bytes-) if not.
+* Each weight must be greater than 0. Reverts with [`ERC7579MultisigWeighted.ERC7579MultisigInvalidWeight`](#ERC7579MultisigWeighted-ERC7579MultisigInvalidWeight-bytes-uint64-) if not.
+* See [`ERC7579Multisig._validateReachableThreshold`](#ERC7579Multisig-_validateReachableThreshold-address-) for the threshold validation.
+
+Emits [`ERC7579MultisigWeighted.ERC7579MultisigWeightChanged`](#ERC7579MultisigWeighted-ERC7579MultisigWeightChanged-address-bytes-uint64-) for each signer.
+
+
+
+Override to add weight tracking. See [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---).
+Each new signer has a default weight of 1.
+
+In cases where [`ERC7579MultisigWeighted.totalWeight`](#ERC7579MultisigWeighted-totalWeight-address-) is almost `type(uint64).max` (due to a large `_totalExtraWeight`), adding new
+signers could cause the [`ERC7579MultisigWeighted.totalWeight`](#ERC7579MultisigWeighted-totalWeight-address-) computation to overflow. Adding a [`ERC7579MultisigWeighted.totalWeight`](#ERC7579MultisigWeighted-totalWeight-address-) call after the new
+signers are added ensures no such overflow happens.
+
+
+
+Override to handle weight tracking during removal. See [`ERC7579Multisig._removeSigners`](#ERC7579Multisig-_removeSigners-address-bytes---).
+
+Just like [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---), this function does not emit [`ERC7579MultisigWeighted.ERC7579MultisigWeightChanged`](#ERC7579MultisigWeighted-ERC7579MultisigWeightChanged-address-bytes-uint64-) events. The
+[`ERC7579Multisig.ERC7913SignerRemoved`](#ERC7579Multisig-ERC7913SignerRemoved-address-bytes-) event emitted by [`ERC7579Multisig._removeSigners`](#ERC7579Multisig-_removeSigners-address-bytes---) is enough to track weights here.
+
+
+
+Override to validate threshold against total weight instead of signer count.
+
+
+This function intentionally does not call `super._validateReachableThreshold` because the base implementation
+assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple
+implementations of this function may exist in the contract, so important side effects may be missed
+depending on the linearization order.
+
+
+
+
+Validates that the total weight of signers meets the [`ERC7579Multisig.threshold`](#ERC7579Multisig-threshold-address-) requirement.
+Overrides the base implementation to use weights instead of count.
+
+
+This function intentionally does not call `super._validateThreshold` because the base implementation
+assumes each signer has a weight of 1, which is incompatible with this weighted implementation.
+
+
+
+
+Emitted when a signer's weight is changed.
+
+
+Not emitted in [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---) or [`ERC7579Multisig._removeSigners`](#ERC7579Multisig-_removeSigners-address-bytes---). Indexers must rely on [`ERC7579Multisig.ERC7913SignerAdded`](#ERC7579Multisig-ERC7913SignerAdded-address-bytes-)
+and [`ERC7579Multisig.ERC7913SignerRemoved`](#ERC7579Multisig-ERC7913SignerRemoved-address-bytes-) to index a default weight of 1. See [`ERC7579MultisigWeighted.signerWeight`](#ERC7579MultisigWeighted-signerWeight-address-bytes-).
+
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579MultisigWeightedConfidential.sol";
+```
+
+Extension of [`ERC7579MultisigConfidential`](#ERC7579MultisigConfidential) that supports weighted signatures.
+
+This module extends the confidential multisignature module to allow assigning different weights
+to each signer, enabling more flexible governance schemes. For example, some guardians
+could have higher weight than others, allowing for weighted voting or prioritized authorization.
+
+Unlike the standard [`ERC7579MultisigWeighted`](#ERC7579MultisigWeighted) module, this confidential version uses Fully
+Homomorphic Encryption (FHE) to keep signer identities (as uint256), weights, and the threshold
+value encrypted on-chain. This provides privacy benefits by hiding the identity of authorized
+signers (whether addresses or ERC-7913 keys), their individual weights, and the number of
+signatures required to approve operations.
+
+A smart account with this module installed can schedule social recovery operations
+after obtaining approval from guardians with sufficient total weight (e.g., requiring
+a total weight of 10, with 3 guardians weighted as 5, 3, and 2), while keeping both the
+signer identities, weights, and threshold confidential.
+
+
+When setting a threshold value, ensure it matches the scale used for signer weights.
+For example, if signers have weights like 1, 2, or 3, then a threshold of 4 would require
+signatures with a total weight of at least 4 (e.g., one with weight 1 and one with weight 3).
+
+
+
+The threshold can be set to 0 given it can't be validated in runtime. The account
+operators must rely on external validation to ensure the threshold is not 0, which allows
+arbitrary execution.
+
+
+
+
+Sets up the module's initial configuration when installed by an account.
+Besides the standard signer and threshold configuration, this can also include
+encrypted signer weights.
+
+See [`ERC7579MultisigWeighted._decodeMultisigWeightedInitData`](#ERC7579MultisigWeighted-_decodeMultisigWeightedInitData-bytes-) for the `initData` encoding details.
+
+
+An account can only call `onInstall` once. If called directly by the account,
+the signers, weights, and threshold will be set to the provided data. Future
+installations will revert with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--).
+
+
+Requirements:
+
+* The `account` must have no signers installed. Reverts with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--) if not.
+* The `signers` array must be non-empty. Reverts with [`ERC7579Multisig.ERC7579MultisigEmptySigners`](#ERC7579Multisig-ERC7579MultisigEmptySigners--) if not.
+* Those of [`ERC7579MultisigWeighted._decodeMultisigWeightedInitData`](#ERC7579MultisigWeighted-_decodeMultisigWeightedInitData-bytes-) for the decoding requirements.
+* Those of [`ERC7579Multisig.addSigners`](#ERC7579Multisig-addSigners-bytes---) and [`ERC7579MultisigWeighted.setSignerWeights`](#ERC7579MultisigWeighted-setSignerWeights-bytes---uint64---) for signer and weight configuration requirements.
+
+
+
+Cleans up module's configuration when uninstalled from an account.
+Clears all signers, weights, and total weights.
+
+See [`ERC7579MultisigConfidential.onUninstall`](#ERC7579MultisigConfidential-onUninstall-bytes-).
+
+
+
+Sets encrypted weights for signers for the calling account.
+Can only be called by the account itself.
+
+Increments the account's configuration nonce, invalidating any outstanding disclosure entries
+made under the previous nonce.
+
+Requirements:
+
+* Those of [`ERC7579MultisigWeighted._setSignerWeights`](#ERC7579MultisigWeighted-_setSignerWeights-address-bytes---uint64---) for additional requirements.
+
+
+
+Decodes weighted multisig initialization data from calldata without memory allocation.
+
+The initData should be encoded as:
+`abi.encode(bytes inputProof, externalEuint256[] signers, externalEuint32 threshold, externalEuint16[] weights)`
+
+Requirements:
+
+* The `initData` must be at least 224 bytes long to contain the minimum structure.
+* All offsets must be properly aligned and within bounds.
+* All array length fields must be accessible within the provided data.
+* The proof data must be fully contained within the provided data.
+
+
+
+Override to add weight tracking and restrict signer count. See [`ERC7579MultisigConfidential._addSigners`](#ERC7579MultisigConfidential-_addSigners-address-euint256---).
+Each new signer has a default weight of 1.
+
+Requirements:
+
+* The number of signers must not exceed `type(uint16).max`. Reverts with [`ERC7579MultisigConfidential.ERC7579MultisigTooManySigners`](#ERC7579MultisigConfidential-ERC7579MultisigTooManySigners--) if exceeded.
+
+
+
+Sets encrypted weights for multiple signers at once. Internal version without access control.
+
+Requirements:
+
+* `signers` and `weights` arrays must have the same length. Reverts with [`ERC7579MultisigWeightedConfidential.ERC7579MultisigMismatchedSignerWeights`](#ERC7579MultisigWeightedConfidential-ERC7579MultisigMismatchedSignerWeights--) on mismatch.
+* Each signer must exist in the set of authorized signers. Reverts with [`ERC7579Multisig.ERC7579MultisigNonexistentSigner`](#ERC7579Multisig-ERC7579MultisigNonexistentSigner-bytes-) if not.
+
+
+This function does not increment the account's configuration nonce.
+Callers that change the configuration must call `Nonces-_useNonce` to invalidate
+outstanding disclosure entries.
+
+
+
+The threshold will be automatically capped at the total weight to ensure it's always reachable.
+
+
+
+Weights of 0 are automatically converted to weight 1 (base weight). See [`ERC7579MultisigWeighted.signerWeight`](#ERC7579MultisigWeighted-signerWeight-address-bytes-).
+
+
+Emits [`ERC7579MultisigWeighted.ERC7579MultisigWeightChanged`](#ERC7579MultisigWeighted-ERC7579MultisigWeightChanged-address-bytes-uint64-) for each signer.
+
+
+
+Override to handle weight tracking during removal. See [`ERC7579MultisigConfidential._removeSigners`](#ERC7579MultisigConfidential-_removeSigners-address-euint256---).
+
+Cleans up weights for removed signers and updates the total extra weight accordingly.
+
+
+
+Override to return the maximum threshold based on total weight instead of signer count.
+See [`ERC7579MultisigConfidential._maxThreshold`](#ERC7579MultisigConfidential-_maxThreshold-address-).
+
+
+
+Override to return the encrypted weight of a signer instead of a fixed weight of 1.
+See [`ERC7579MultisigConfidential._signerWeight`](#ERC7579MultisigConfidential-_signerWeight-address-euint256-).
+
+Returns 0 if the signer is not authorized, otherwise returns 1 plus the signer's extra weight.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579P256.sol";
+```
+
+Implementation of an `IERC7579Module` that uses an P256 signer for signature
+validation.
+
+The P256 signer consists of a P256 public key pair (qx, qy coordinates) that
+is registered during module installation. Signatures must be valid P256 signatures
+that can be verified against this public key.
+
+This module provides a base implementation for P256 signer validation that can be
+attached to any function through the [`ERC7579P256._validateP256`](#ERC7579P256-_validateP256-address-bytes32-bytes-) internal function.
+
+
+
+Sets up the module's initial configuration when installed by an account.
+The `data` parameter should contain the P256 public key coordinates (64 bytes minimum).
+
+If the account doesn't already have a signer configured, the first 64 bytes
+of `data` will be used as the authorized P256 signer (qx, qy).
+See [`ERC7579ECDSA._extractSigner`](#ERC7579ECDSA-_extractSigner-bytes-) for more details about the encoding.
+
+Requirements:
+
+* The `data` must be at least 64 bytes. Reverts with [`ERC7579P256.ERC7579P256InvalidSignerLength`](#ERC7579P256-ERC7579P256InvalidSignerLength--) if not.
+* The `account` must have no signer installed. Reverts with [`ERC7579P256.ERC7579P256AlreadyInstalled`](#ERC7579P256-ERC7579P256AlreadyInstalled--) if not.
+* Decode `data` into `abi.encode(bytes32 qx, bytes32 qy)`.
+* The `data` must contain valid P256 public key coordinates. Reverts with [`ERC7579P256.ERC7579P256InvalidPublicKey`](#ERC7579P256-ERC7579P256InvalidPublicKey-bytes32-bytes32-) if not.
+
+Emits a [`ERC7579P256.ERC7579P256SignerSet`](#ERC7579P256-ERC7579P256SignerSet-address-bytes32-bytes32-) event if the signer is set.
+
+
+
+Cleans up the module's configuration when uninstalled from an account.
+Removes the authorized P256 signer for the calling account.
+
+If the account has a configured signer, it will be cleared. If no signer
+is configured, this function will behave as a no-op.
+
+Emits a [`ERC7579P256.ERC7579P256SignerSet`](#ERC7579P256-ERC7579P256SignerSet-address-bytes32-bytes32-) event if the signer is cleared.
+
+
+
+Sets the P256 signer for the specified account.
+
+Requirements:
+
+* The `qx` and `qy` coordinates must not be zero.
+* The `qx` and `qy` coordinates must be valid P256 public key coordinates.
+* The caller must be the specified account.
+
+Emits a [`ERC7579P256.ERC7579P256SignerSet`](#ERC7579P256-ERC7579P256SignerSet-address-bytes32-bytes32-) event.
+
+
+
+Internal version of [`ERC7579ECDSA.setSigner`](#ERC7579ECDSA-setSigner-address-address-) without access control.
+
+Requirements:
+
+* The `qx` and `qy` coordinates must not be zero.
+* The `qx` and `qy` coordinates must be valid P256 public key coordinates.
+
+Emits a [`ERC7579P256.ERC7579P256SignerSet`](#ERC7579P256-ERC7579P256SignerSet-address-bytes32-bytes32-) event.
+
+
+
+Validates a P256 signature against the configured signer for the account.
+
+This function extracts the r and s components from the signature and verifies
+them against the P256 public key stored for the given account.
+
+The signature is expected to be at least 64 bytes containing the P256 signature
+components (r, s) as 32-byte values.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579SessionKeys.sol";
+```
+
+Abstract base for ERC-7579 session keys.
+
+This module allows registering ERC-7913 keys as session keys on a smart account, each with
+a configurable validity range. A session key is any ERC-7913 signer (an arbitrary
+cryptographic key identified by a `(verifier, key)` pair) that is registered for a
+specific account. The key is only considered active between its `from` and `until`
+timepoints.
+
+The key is encoded in the `signature` field of the user operations and ERC-1271 requests for
+identification and cryptographic verification. See [`ERC7579SessionKeys._tryDecodeSignature`](#ERC7579SessionKeys-_tryDecodeSignature-bytes-) for the encoding details.
+
+This module provides session key management through the [`ERC7579SessionKeys._validateSessionKey`](#ERC7579SessionKeys-_validateSessionKey-address-uint256-bytes32-bytes-) and
+[`ERC7579SessionKeys.isValid`](#ERC7579SessionKeys-isValid-address-bytes-) internal functions. Concrete implementations should attach it to validation flows such as
+[`ERC7579Validator._rawERC7579Validation`](#ERC7579Validator-_rawERC7579Validation-address-bytes32-bytes-).
+
+
+
+Machine-readable description of the clock as specified in ERC-6372.
+Can be overridden to return `"mode=blocknumber&from=default"` if using block numbers.
+
+
+
+Returns a slice of the registered session keys for an `account`.
+
+Using `startIdx = 0` and `endIdx = type(uint256).max` will return all keys.
+
+
+Depending on the `startIdx` and `endIdx`, this operation can copy a large amount
+of data to memory, which can be expensive. This is designed for view accessors queried
+without gas fees. Using it in state-changing functions may become uncallable if the set
+grows too large.
+
+
+
+
+Sets up the module's initial configuration when installed by an account.
+See `IERC7579Module-onInstall`. The `initData` encodes arrays of keys and their
+corresponding validity ranges.
+
+See [`ERC7579SessionKeys._decodeKeysInitData`](#ERC7579SessionKeys-_decodeKeysInitData-bytes-) for the encoding details.
+
+Requirements:
+
+* Those from [`ERC7579SessionKeys._decodeKeysInitData`](#ERC7579SessionKeys-_decodeKeysInitData-bytes-) for the decoding requirements.
+* Those from [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---) for the key configuration requirements.
+
+
+Set `starts[i]` to `0` to activate the key immediately at the current timepoint.
+
+
+
+Set `ends[i]` to `0` to register a key with no expiry.
+
+
+
+
+Cleans up the module's configuration when uninstalled from an account.
+Increments the account's nonce to invalidate all registered session keys.
+
+See `IERC7579Module-onUninstall`.
+
+
+This approach avoids unbounded gas costs by not iterating over keys.
+Previous key data remains in storage but becomes inaccessible due to the nonce increment.
+Upon reinstallation, the module will use a new nonce and ignore old keys.
+
+
+
+
+Adds new session keys to the module for the calling account.
+Can only be called by the account itself.
+
+Requirements:
+
+* Those from [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---) for the key configuration requirements.
+
+
+
+Removes session keys from the module for the calling account.
+Can only be called by the account itself.
+
+Requirements:
+
+* Those from [`ERC7579SessionKeys._removeKeys`](#ERC7579SessionKeys-_removeKeys-address-uint256-bytes---) for the key configuration requirements.
+
+
+
+Internal function to register session keys for an `account`.
+
+For each key, stores the ERC-7913 key bytes in the enumerable set and records its
+validity range. Keys are identified by `keccak256(key)` in the validity mapping.
+
+Requirements:
+
+* All arrays must have equal length. Reverts with [`ERC7579SessionKeys.ERC7579SessionKeysMismatchedInitDataLengths`](#ERC7579SessionKeys-ERC7579SessionKeysMismatchedInitDataLengths--) if not.
+* Each of `keys_` must not be registered. Reverts with [`ERC7579SessionKeys.ERC7579SessionKeysAlreadyRegisteredKey`](#ERC7579SessionKeys-ERC7579SessionKeysAlreadyRegisteredKey-bytes-) if it is.
+
+
+Setting `starts[i]` to `0` resolves to the current timepoint.
+
+
+
+Setting `ends[i]` to `0` stores `type(uint48).max`, meaning no expiry.
+
+
+Emits an [`ERC7579SessionKeys.ERC7579SessionKeysERC7913KeySet`](#ERC7579SessionKeys-ERC7579SessionKeysERC7913KeySet-address-bytes-uint48-uint48-) event for each key.
+
+
+
+Internal function to remove session keys from an `account`.
+
+Removes the key from the enumerable set and clears its validity range in storage.
+
+Requirements:
+
+* Each of `keys_` must be registered. Reverts with [`ERC7579SessionKeys.ERC7579SessionKeysNonexistentKey`](#ERC7579SessionKeys-ERC7579SessionKeysNonexistentKey-bytes-) if not.
+
+Emits an [`ERC7579SessionKeys.ERC7579SessionKeysERC7913KeyUnset`](#ERC7579SessionKeys-ERC7579SessionKeysERC7913KeyUnset-address-bytes-) event for each key.
+
+
+
+Validates an ERC-7913 session key signature against the registered keys for the account.
+
+Decodes the ERC-7913 key and its cryptographic signature from the encoded `signature`
+field, then checks that the key is registered and that the cryptographic signature is
+valid. Does not validate the [`ERC7579SessionKeys.validity`](#ERC7579SessionKeys-validity-address-bytes-) range.
+
+See [`ERC7579SessionKeys._tryDecodeSignature`](#ERC7579SessionKeys-_tryDecodeSignature-bytes-) for the encoding details.
+
+
+Does not validate the [`ERC7579SessionKeys.validity`](#ERC7579SessionKeys-validity-address-bytes-) range.
+
+
+
+
+Variant of [`ERC7579SessionKeys._validateSessionKey`](#ERC7579SessionKeys-_validateSessionKey-address-uint256-bytes32-bytes-) that receives an `installation_` nonce.
+
+
+
+Decodes a session key signature into its ERC-7913 `key` and cryptographic
+`keySignature` components.
+
+The `signature` must be ABI-encoded as `abi.encode(bytes key, bytes keySignature)`.
+
+Returns `(false, emptyBytes, emptyBytes)` without reverting if the encoding is invalid
+or if any offset or length would read out of bounds.
+
+
+
+Decodes the initialization data for the session keys module.
+
+The `initData` must be ABI-encoded as `abi.encode(bytes[] keys, uint48[] starts, uint48[] ends)`
+where each array has the same length.
+
+Requirements:
+
+* `initData` must be at least 192 bytes (6 Γ 32 bytes for the 3 ABI offsets and 3 array lengths).
+* All offsets must point within the `initData` bounds.
+
+Reverts with [`ERC7579SessionKeys.ERC7579SessionKeysInvalidInitData`](#ERC7579SessionKeys-ERC7579SessionKeysInvalidInitData--) if the encoding is invalid.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/ERC7579WebAuthn.sol";
+```
+
+Implementation of an `IERC7579Module` that uses a WebAuthn credential for signature
+validation.
+
+This module provides a base implementation for WebAuthn signature validation that can be
+attached to any function through the [`ERC7579WebAuthn._validateWebAuthn`](#ERC7579WebAuthn-_validateWebAuthn-address-bytes32-bytes-) internal function.
+
+
+
+Validates a WebAuthn authentication assertion against the configured signer for the account.
+
+Returns `false` if the signature is not a valid WebAuthn authentication assertion.
+
+The signature is expected to be an abi-encoded `WebAuthn-WebAuthnAuth` struct.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/executors/ERC7579DelayedExecutor.sol";
+```
+
+Extension of [`ERC7579Executor`](#ERC7579Executor) that provides time-delayed execution patterns
+for smart accounts with scheduling and expiration capabilities.
+
+This contract provides the basic scheduling functionality through the [`ERC7579DelayedExecutor._schedule`](#ERC7579DelayedExecutor-_schedule-address-bytes32-bytes32-bytes-),
+[`ERC7579DelayedExecutor._scheduleAt`](#ERC7579DelayedExecutor-_scheduleAt-address-bytes32-bytes32-bytes-uint48-uint32-), and [`ERC7579DelayedExecutor._cancel`](#ERC7579DelayedExecutor-_cancel-address-bytes32-bytes32-bytes-) functions. Developers must implement their own public
+`schedule`, `cancel`, and `execute` functions in derived contracts to define the
+specific validation and access control logic for each operation.
+
+#### Operation Lifecycle
+
+1. Scheduling: Operations are scheduled via [`ERC7579DelayedExecutor._schedule`](#ERC7579DelayedExecutor-_schedule-address-bytes32-bytes32-bytes-) with a specified delay period.
+The delay period is set during [`ERC7579ECDSA.onInstall`](#ERC7579ECDSA-onInstall-bytes-) and can be customized via [`ERC7579DelayedExecutor.setDelay`](#ERC7579DelayedExecutor-setDelay-uint32-). Each
+operation enters a `Scheduled` state and must wait for its delay period to elapse.
+
+2. Security Window: During the delay period, operations remain in `Scheduled` state but
+cannot be executed. Through this period, suspicious operations can be monitored and
+canceled via [`ERC7579DelayedExecutor._cancel`](#ERC7579DelayedExecutor-_cancel-address-bytes32-bytes32-bytes-) if appropriate.
+
+3. Execution & Expiration: Once the delay period elapses, operations transition to `Ready` state.
+Operations can be executed via [`ERC7579DelayedExecutor._execute`](#ERC7579DelayedExecutor-_execute-address-bytes32-bytes32-bytes-) and have an expiration period after becoming
+executable. If an operation is not executed within the expiration period, it becomes `Expired`
+and can't be executed. Expired operations must be rescheduled with a different salt.
+
+#### Delay Management
+
+Accounts can set their own delay periods during installation or via [`ERC7579DelayedExecutor.setDelay`](#ERC7579DelayedExecutor-setDelay-uint32-).
+The delay period is enforced even between installs and uninstalls to prevent
+immediate downgrades. When setting a new delay period, the transition period before
+the new delay takes effect depends on the direction of the change:
+
+* When _increasing_ the delay, the new value takes effect after `min(newDelay, delayChangeWindow())`.
+* When _decreasing_ the delay, the new value takes effect after `currentDelay - newDelay`.
+
+
+Use [`ERC7579DelayedExecutor._scheduleAt`](#ERC7579DelayedExecutor-_scheduleAt-address-bytes32-bytes32-bytes-uint48-uint32-) to schedule operations at specific points in time. This is
+useful to pre-schedule operations for non-deployed accounts (e.g. subscriptions).
+
+
+
+Scheduling, executing and cancelling doesn't have any access control by default
+and should be validated accordingly while extending this contract.
+
+
+
+Target addresses of an operation may return an arbitrary large amount of data, which
+may cause the operation to fail due to insufficient gas. Consider that a target could return
+more data than initially expected (e.g. after being upgraded, or when their return data
+depends on state), potentially blocking the operation's execution until they expire.
+
+
+
+
+Maximum transition period applied when [`ERC7579DelayedExecutor.setDelay`](#ERC7579DelayedExecutor-setDelay-uint32-) increases the delay, which takes effect
+after `min(newDelay, delayChangeWindow())`. Delay decreases are unaffected and take effect after
+`currentDelay - newDelay`. Also used as the default delay if not provided during [`ERC7579ECDSA.onInstall`](#ERC7579ECDSA-onInstall-bytes-).
+
+
+
+Sets up the module's initial configuration when installed by an account.
+The account calling this function becomes registered with the module.
+
+The `initData` must be `abi.encodePacked(uint32(initialDelay), uint32(initialExpiration))`.
+Otherwise, the delay will be set to [`ERC7579DelayedExecutor.delayChangeWindow`](#ERC7579DelayedExecutor-delayChangeWindow--) and [`ERC7579DelayedExecutor.defaultExpiration`](#ERC7579DelayedExecutor-defaultExpiration--) respectively.
+
+If a previous delay is still active (e.g. from a prior installation that was uninstalled before its
+delay reset completed), the same asymmetric transition rules as [`ERC7579DelayedExecutor.setDelay`](#ERC7579DelayedExecutor-setDelay-uint32-) apply to the new delay,
+preventing an uninstall/reinstall cycle from being used to bypass the security window.
+
+Requirements:
+
+* The module must not be already installed on the account. Reverts with [`ERC7579DelayedExecutor.ERC7579DelayedExecutorAlreadyInstalled`](#ERC7579DelayedExecutor-ERC7579DelayedExecutorAlreadyInstalled--) if not.
+* `initData` must be empty or decode correctly to `(uint32, uint32)`.
+
+
+
+Allows an account to update its execution delay (see [`ERC7579DelayedExecutor.getDelay`](#ERC7579DelayedExecutor-getDelay-address-)).
+
+The transition period before the new delay takes effect depends on the direction of the change:
+
+* When _increasing_ the delay, the new value takes effect after `min(newDelay, delayChangeWindow())`.
+ For example, increasing from 1 day to 3 days takes effect after 3 days. Increasing from
+ 1 day to 10 days takes effect after [`ERC7579DelayedExecutor.delayChangeWindow`](#ERC7579DelayedExecutor-delayChangeWindow--) (5 days). This cap prevents the account
+ from being locked out by an accidental large increase (e.g. using milliseconds instead of
+ seconds).
+* When _decreasing_ the delay, the new value takes effect after `currentDelay - newDelay`.
+ This guarantees that a recovery operation cannot execute faster than the delay that was
+ active at the moment the change was scheduled. For example, decreasing from 10 days to
+ 3 days takes effect after 7 days.
+
+Can only be called by the account itself.
+
+
+
+Allows an account to update its execution expiration (see [`ERC7579DelayedExecutor.getExpiration`](#ERC7579DelayedExecutor-getExpiration-address-)).
+
+
+
+Cleans up the [`ERC7579DelayedExecutor.getDelay`](#ERC7579DelayedExecutor-getDelay-address-) and [`ERC7579DelayedExecutor.getExpiration`](#ERC7579DelayedExecutor-getExpiration-address-) values by scheduling them to `0`
+and respecting the previous delay and expiration values.
+
+
+This function does not clean up scheduled operations. This means operations
+could potentially be re-executed if the module is reinstalled later. This is a deliberate
+design choice for efficiency, but module implementations may want to override this behavior
+to clear scheduled operations during uninstallation for their specific use cases.
+
+
+
+Uninstalling the module schedules the delay to reset to `0` after the current delay
+(equivalent to calling `setDelay(0)`), and immediately removes the expiration. If the module
+is reinstalled before the delay reset completes, the same asymmetric transition rules as
+[`ERC7579DelayedExecutor.setDelay`](#ERC7579DelayedExecutor-setDelay-uint32-) apply to prevent bypassing the security window through an uninstall/reinstall cycle.
+
+
+
+
+Internal implementation for setting an account's delay. See [`ERC7579DelayedExecutor.getDelay`](#ERC7579DelayedExecutor-getDelay-address-).
+
+Emits an [`ERC7579DelayedExecutor.ERC7579ExecutorDelayUpdated`](#ERC7579DelayedExecutor-ERC7579ExecutorDelayUpdated-address-uint32-uint48-) event.
+
+
+
+Internal implementation for setting an account's expiration. See [`ERC7579DelayedExecutor.getExpiration`](#ERC7579DelayedExecutor-getExpiration-address-).
+
+Emits an [`ERC7579DelayedExecutor.ERC7579ExecutorExpirationUpdated`](#ERC7579DelayedExecutor-ERC7579ExecutorExpirationUpdated-address-uint32-) event.
+
+
+
+Schedules an operation to be executed after the account's delay period (see [`ERC7579DelayedExecutor.getDelay`](#ERC7579DelayedExecutor-getDelay-address-)).
+Operations are uniquely identified by the combination of `account`, `salt`, `mode`, and `data`.
+This should be called by derived contracts after performing their custom validation logic.
+
+Requirements:
+
+* The module must be installed on the account. Reverts with [`ERC7579DelayedExecutor.ERC7579ExecutorModuleNotInstalled`](#ERC7579DelayedExecutor-ERC7579ExecutorModuleNotInstalled--) if not.
+
+
+This function does not have any access control validation.
+
+
+
+
+Internal function to schedule an operation at a specific timepoint with a custom delay.
+The operation starts its security window at `timepoint` and becomes executable after `delay`.
+
+Requirements:
+
+* The operation must be `Unknown`.
+
+Emits an [`ERC7579DelayedExecutor.ERC7579ExecutorOperationScheduled`](#ERC7579DelayedExecutor-ERC7579ExecutorOperationScheduled-address-bytes32-bytes32-bytes32-bytes-uint48-) event.
+
+
+This function does not have any access control validation.
+
+
+
+
+See [`ERC7579Executor._execute`](#ERC7579Executor-_execute-address-bytes32-bytes32-bytes-).
+
+Requirements:
+
+* The operation must be `Ready`.
+
+
+This function does not have any access control validation.
+
+
+
+
+Internal function to cancel a scheduled operation.
+This should be called by derived contracts after performing their custom validation logic.
+
+Requirements:
+
+* The operation must be `Scheduled` or `Ready`.
+
+Canceled operations can't be rescheduled. Emits an [`ERC7579DelayedExecutor.ERC7579ExecutorOperationCanceled`](#ERC7579DelayedExecutor-ERC7579ExecutorOperationCanceled-address-bytes32-) event.
+
+
+This function does not have any access control validation.
+
+
+
+
+Check that the current state of an operation matches the requirements described by the `allowedStates` bitmap.
+This bitmap should be built using [`ERC7579DelayedExecutor._encodeStateBitmap`](#ERC7579DelayedExecutor-_encodeStateBitmap-enum-ERC7579DelayedExecutor-OperationState-).
+
+If requirements are not met, reverts with a [`ERC7579DelayedExecutor.ERC7579ExecutorUnexpectedOperationState`](#ERC7579DelayedExecutor-ERC7579ExecutorUnexpectedOperationState-bytes32-enum-ERC7579DelayedExecutor-OperationState-bytes32-) error.
+
+
+Passing a `bytes32(0)` for `allowedStates` will always revert.
+
+
+
+
+Encodes a `OperationState` into a `bytes32` representation where each bit enabled corresponds to
+the underlying position in the `OperationState` enum. For example:
+
+```
+0x00...100000
+ ^^^^^^------ ...
+ ^------ Canceled
+ ^----- Executed
+ ^---- Expired
+ ^--- Ready
+ ^-- Scheduled
+ ^- Unknown
+```
+
+
+
+The current state of an operation is not the expected. The `allowedStates` is a bitmap with the
+bits enabled for each OperationState enum position counting from right to left. See [`ERC7579DelayedExecutor._encodeStateBitmap`](#ERC7579DelayedExecutor-_encodeStateBitmap-enum-ERC7579DelayedExecutor-OperationState-).
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/executors/ERC7579ERC20Spender.sol";
+```
+
+Extension of [`ERC7579SessionKeysValidator`](#ERC7579SessionKeysValidator) that provides ERC20 token spending limits
+granted to session keys with automatic renewal periods for ERC-7579 smart accounts.
+
+This module allows accounts to configure spending allowances for external spenders,
+enabling use cases like subscription services, recurring payments, or delegated spending
+with predefined limits.
+
+Same dual-role design as [`ERC7579NativeSpender`](#ERC7579NativeSpender): a session key authorizes a UserOp whose
+`callData` targets [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) or [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---) on this module, and the module
+spends through `executeFromExecutor` on the account.
+
+Each `(session key, ERC-20 token)` pair carries its own `(limit, renewPeriod)` spending
+policy. A single key can have policies for multiple tokens by listing it multiple times
+(with different tokens) in the executor install initData.
+
+
+The module should be installed under both module types, in this order:
+
+1. **Validator install (first)**: `initData` is `abi.encode(bytes[] keys, uint48[] starts, uint48[] ends)`.
+ Registers session keys with their validity ranges through [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---).
+ At least one key must be registered, otherwise the install reverts.
+2. **Executor install (second)**: `initData` is
+ `abi.encode(bytes[] keys, address[] tokens, uint160[] limits, uint48[] renewPeriods)`.
+ Writes a spending policy for each `(keys[i], tokens[i])` row. Each row's `keys[i]` MUST
+ already be registered. Keys MAY repeat across rows to configure multiple tokens for the
+ same key. The `keys` array MUST be non-empty.
+
+Once any `(key, token)` config is written, the module is "installed" and further `onInstall`
+calls revert with [`ERC7579ERC20Spender.ERC7579ERC20SpenderAlreadyInstalled`](#ERC7579ERC20Spender-ERC7579ERC20SpenderAlreadyInstalled--). To change the spending policy, the
+account must uninstall and reinstall (which bumps the installation nonce and resets state).
+
+
+
+This module deliberately disables ERC-1271 signing
+([`ERC7579ERC20Spender.isValidSignatureWithSender`](#ERC7579ERC20Spender-isValidSignatureWithSender-address-bytes32-bytes-) always returns `0xffffffff`) so a spending session key cannot be
+repurposed to authorize arbitrary off-chain messages on behalf of the account.
+
+
+
+Removing a session key via [`ERC7579SessionKeys.removeKeys`](#ERC7579SessionKeys-removeKeys-bytes---) clears its validity range but
+does NOT clear the `(key, token)` spending configs. If the same key bytes are re-registered via
+[`ERC7579SessionKeys.addKeys`](#ERC7579SessionKeys-addKeys-bytes---uint48---uint48---) (without `addKeysWithConfig`), the previously-stored configs become
+active again. To re-register a key with a fresh policy, call [`ERC7579ERC20Spender.addKeysWithConfig`](#ERC7579ERC20Spender-addKeysWithConfig-bytes---uint48---uint48---address---uint160---uint48---) instead.
+
+
+
+Set `starts[i] == 0` to schedule a key from the current timepoint.
+
+
+
+Set `renewPeriod == type(uint48).max` to effectively disable the renewal period.
+
+
+
+
+Returns a paginated slice of tokens with at least one spending config for `account`.
+
+
+Depending on `startIdx` and `endIdx`, this operation can copy a large amount of
+data to memory. Designed for view accessors queried without gas fees.
+
+
+
+
+Same as [`ERC7579ERC20Spender.spent`](#ERC7579ERC20Spender-spent-address-bytes-address-uint256-uint256-) but reads from an explicit `start` timepoint.
+
+
+
+Returns `(start, period)` for `(key, token)` at `timepoint`.
+
+Requirements:
+
+* `timepoint` must be `>=` the key's `from`. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpendingConfigNotStarted`](#ERC7579ERC20Spender-ERC7579ERC20SpendingConfigNotStarted-address-bytes32-address-uint256-uint256-) otherwise.
+* The spending config must be set. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpendingConfigNotSet`](#ERC7579ERC20Spender-ERC7579ERC20SpendingConfigNotSet-address-bytes32-address-) otherwise.
+
+
+
+Sets up the module's initial configuration when installed by an account.
+
+This module is installed under BOTH module types and `onInstall` is called twice. The
+shape of `initData` differs between the two calls:
+
+* **First call** (validator install, `installed(msg.sender) == false`):
+ `abi.encode(bytes[] keys, uint48[] starts, uint48[] ends)`.
+* **Second call** (executor install, `installed(msg.sender) == true`):
+ `abi.encode(bytes[] keys, address[] tokens, uint160[] limits, uint48[] renewPeriods)`.
+
+First call requirements:
+
+* The `keys` array must be non-empty. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpenderEmptyKeys`](#ERC7579ERC20Spender-ERC7579ERC20SpenderEmptyKeys--) otherwise.
+* Those from the parent's [`ERC7579SessionKeys.onInstall`](#ERC7579SessionKeys-onInstall-bytes-) (key registration).
+
+Second call requirements:
+
+* No `(key, token)` config has been written yet under this installation. Reverts with
+ [`ERC7579ERC20Spender.ERC7579ERC20SpenderAlreadyInstalled`](#ERC7579ERC20Spender-ERC7579ERC20SpenderAlreadyInstalled--) otherwise.
+* `initData.length` must be at least 256 bytes (4 ABI offsets + 4 array lengths). Reverts
+ with [`ERC7579ERC20Spender.ERC7579ERC20SpenderInvalidInitData`](#ERC7579ERC20Spender-ERC7579ERC20SpenderInvalidInitData--) otherwise. All offsets must point within bounds.
+* All four arrays must have equal length.
+* The `keys` array must be non-empty.
+* Each `keys[i]` must already be registered. Reverts with [`ERC7579SessionKeys.ERC7579SessionKeysNonexistentKey`](#ERC7579SessionKeys-ERC7579SessionKeysNonexistentKey-bytes-) otherwise.
+* Those from [`ERC7579ERC20Spender._setConfig`](#ERC7579ERC20Spender-_setConfig-address-uint256-bytes-address-uint160-uint48-) for the spending config requirements.
+
+Emits [`ERC7579SessionKeys.ERC7579SessionKeysERC7913KeySet`](#ERC7579SessionKeys-ERC7579SessionKeysERC7913KeySet-address-bytes-uint48-uint48-) on the first call and [`ERC7579ERC20Spender.ERC7579ERC20SpendingConfigSet`](#ERC7579ERC20Spender-ERC7579ERC20SpendingConfigSet-address-bytes-address-uint256-uint256-)
+for each row on the second call.
+
+
+Each start will be set to the current timepoint if it's 0.
+
+
+
+
+Adds new session keys with one spending configuration each, in a single call. Can only
+be called by the account itself.
+
+Requirements:
+
+* All six arrays must have equal length.
+* Those from [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---) (and the [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---) override's past-start ban).
+* Those from [`ERC7579ERC20Spender._setConfig`](#ERC7579ERC20Spender-_setConfig-address-uint256-bytes-address-uint160-uint48-) for each row.
+
+
+This helper only supports one `(token, limit, renewPeriod)` per new key. To configure
+multiple tokens for a single key, list it multiple times in the executor install initData.
+
+
+
+
+
+
+
+
+
+
transferFrom(address account, bytes key, address token, address to, uint256 amount)
+
+Transfers `token` from `account` to `to` on behalf of `key`.
+
+Tracks the spending limit for the `(key, token)` pair on `account`, updates the spent
+amount for the current period, and executes the transfer through the account's
+`IERC7579Execution-executeFromExecutor`.
+
+Two authorization paths are accepted:
+
+1. **Executor flow**: `msg.sender == account`. The validator has already verified the session
+ key's signature and the calldata shape, so no further checks are performed here.
+2. **EOA self-call**: `key.length == 20` and `address(bytes20(key)) == msg.sender`. The key's
+ validity range is enforced manually.
+
+Requirements:
+
+* The caller is authorized via one of the two paths above. Reverts with
+ [`ERC7579ERC20Spender.ERC7579ERC20SpenderUnauthorized`](#ERC7579ERC20Spender-ERC7579ERC20SpenderUnauthorized-address-bytes-) otherwise.
+* `amount` must not exceed the available limit for the current period. Reverts with
+ [`ERC7579ERC20Spender.ERC7579ERC20InsufficientAvailable`](#ERC7579ERC20Spender-ERC7579ERC20InsufficientAvailable-address-bytes-address-uint256-) otherwise.
+* The token's `transfer` must return `true` or have no return value. Reverts with
+ [`ERC7579ERC20Spender.ERC7579ERC20SpenderFailedTransfer`](#ERC7579ERC20Spender-ERC7579ERC20SpenderFailedTransfer-address-) otherwise.
+* Those from [`ERC7579ERC20Spender._periodAt`](#ERC7579ERC20Spender-_periodAt-address-uint256-bytes32-address-uint48-) for the current timepoint.
+
+
+
+Transfers `token` from `account` to multiple recipients in a single batch.
+
+Same authorization paths as [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-). The available limit check is performed once
+against the total of `amounts`.
+
+Requirements:
+
+* `tos.length == amounts.length`. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpenderMismatchedBatchLengths`](#ERC7579ERC20Spender-ERC7579ERC20SpenderMismatchedBatchLengths-uint256-uint256-) otherwise.
+* The caller is authorized via one of the two paths described in [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-).
+* The sum of `amounts` must not exceed the available limit.
+* Each ERC20 `transfer` must succeed (return value handled identically to [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-)).
+
+
+
+Validates a user operation. Adds a callData shape gate on top of session-key validation:
+
+`userOp.callData` MUST call `IERC7579Execution.execute(mode, executionCalldata)` where:
+
+* `mode` is `CALLTYPE_SINGLE`.
+* `executionCalldata` decodes to `(target=address(this), value=0, data)`.
+* `data` calls [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) or [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---).
+* The `key` parameter inside `data` matches the key in `userOp.signature`.
+
+Returns `ERC4337Utils.SIG_VALIDATION_FAILED` for any shape or key mismatch, otherwise delegates
+to the parent for cryptographic verification and validity-range packing.
+
+
+
+Override of [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---) that disallows registering a key with a past
+`start` timepoint. See [`ERC7579NativeSpender._addKeys`](#ERC7579NativeSpender-_addKeys-address-uint256-bytes---uint48---uint48---) for the full rationale.
+
+Requirements:
+
+* Each `starts[i]` must be either `0` or `>= clock()`. Reverts with
+ [`ERC7579ERC20Spender.ERC7579ERC20SpenderInvalidStart`](#ERC7579ERC20Spender-ERC7579ERC20SpenderInvalidStart-uint256-) otherwise.
+* Those from [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---).
+
+
+Pass `starts[i] == 0` to resolve the start to the current timepoint.
+
+
+
+
+Internal version of [`ERC7579ERC20Spender.periodAt`](#ERC7579ERC20Spender-periodAt-address-bytes-address-uint48-) with explicit `installation_`.
+
+Requirements:
+
+* `timepoint` must be `>=` the key's `from`. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpendingConfigNotStarted`](#ERC7579ERC20Spender-ERC7579ERC20SpendingConfigNotStarted-address-bytes32-address-uint256-uint256-) otherwise.
+* The spending config must be set. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpendingConfigNotSet`](#ERC7579ERC20Spender-ERC7579ERC20SpendingConfigNotSet-address-bytes32-address-) otherwise.
+
+
+
+Internal version of [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) without access control.
+
+
+
+Internal version of [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---) without access control.
+
+
+
+Writes a spending config for `(keyId, token)` and adds the token to the per-installation
+token set.
+
+Requirements:
+
+* `newRenewPeriod > 0`. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpenderInvalidRenewPeriod`](#ERC7579ERC20Spender-ERC7579ERC20SpenderInvalidRenewPeriod-uint256-) otherwise.
+* `newLimit > 0`. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpenderInvalidLimit`](#ERC7579ERC20Spender-ERC7579ERC20SpenderInvalidLimit-uint256-) otherwise.
+* `token` must be a contract. Reverts with [`ERC7579ERC20Spender.ERC7579ERC20SpenderInvalidToken`](#ERC7579ERC20Spender-ERC7579ERC20SpenderInvalidToken-address-) otherwise.
+
+Emits [`ERC7579ERC20Spender.ERC7579ERC20SpendingConfigSet`](#ERC7579ERC20Spender-ERC7579ERC20SpendingConfigSet-address-bytes-address-uint256-uint256-).
+
+
+
+Returns whether `userOp.callData` is a permitted call into [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) or
+[`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---) on this module, and whether its `key` argument matches the one
+decoded from `userOp.signature`.
+
+
+
+See [`ERC7579NativeSpender._validateTransferKey`](#ERC7579NativeSpender-_validateTransferKey-bytes-). The key argument is the second
+parameter of both [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) and [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---), so the offset slot lies at a
+fixed position regardless of which selector is used.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/executors/ERC7579Executor.sol";
+```
+
+Abstract executor module for ERC-7579 accounts.
+
+This contract provides the base execution functionality for ERC-7579 accounts.
+Developers must implement the [`ERC7579ECDSA.onInstall`](#ERC7579ECDSA-onInstall-bytes-) and [`ERC7579ECDSA.onUninstall`](#ERC7579ECDSA-onUninstall-bytes-) functions, as well as
+wrap [`ERC7579DelayedExecutor._execute`](#ERC7579DelayedExecutor-_execute-address-bytes32-bytes32-bytes-) in a custom external function in derived contracts to define the
+specific validation and access control logic.
+
+Example implementation:
+
+```solidity
+contract MyExecutor is ERC7579Executor {
+ function execute(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public virtual returns (bytes[] memory) {
+ // Implement your validation logic here
+ // e.g., signature verification, access control, time delays, etc.
+
+ // Then call the internal _execute function
+ return _execute(account, salt, mode, data);
+ }
+}
+```
+
+
+For a more advanced implementation with time-delayed execution patterns and
+security features, see [`ERC7579DelayedExecutor`](#ERC7579DelayedExecutor).
+
+
+
+
+Internal function to execute an operation through the account's executor interface.
+
+Emits [`ERC7579Executor.ERC7579ExecutorOperationExecuted`](#ERC7579Executor-ERC7579ExecutorOperationExecuted-address-bytes32-bytes32-bytes-) event.
+
+
+Target addresses included in the `executionCalldata` may return a large amount of data,
+which may cause the operation to fail due to insufficient gas given `executeFromExecutor` copies
+all return datas to memory.
+
+
+Requirements:
+
+* The `account` must implement the `IERC7579Execution-executeFromExecutor` function.
+
+
+This function does not have any access control validation.
+
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/executors/ERC7579NativeSpender.sol";
+```
+
+Extension of [`ERC7579SessionKeysValidator`](#ERC7579SessionKeysValidator) that provides native token (ETH) spending
+limits granted to session keys with automatic renewal periods for ERC-7579 smart accounts.
+
+This module allows accounts to configure spending allowances for external spenders,
+enabling use cases like subscription services, recurring payments, or delegated spending
+with predefined limits.
+
+The module is both a validator and an executor:
+
+* As a **validator**, it authorizes user operations signed by a registered session key whose
+ `callData` is a single call into this module's [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) or [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---). The
+ key's validity range is packed into `validationData` so the EntryPoint enforces it natively.
+* As an **executor**, it spends from the account through `executeFromExecutor`, decrementing
+ the per-key, per-period limit.
+
+Each session key carries its own `(limit, renewPeriod)` spending policy. The start of the
+spending periods is the session key's `from` timepoint. Periods automatically renew every
+`renewPeriod` time units, allowing the same spending capacity to be used again without
+requiring manual intervention. It implements ERC-6372 clock functions.
+
+
+The module should be installed under both module types, in this order:
+
+1. **Validator install (first)**: `initData` is `abi.encode(bytes[] keys, uint48[] starts, uint48[] ends)`.
+ This registers session keys with their validity ranges through [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---).
+ At least one key must be registered, otherwise the install reverts.
+2. **Executor install (second)**: `initData` is `abi.encode(bytes[] keys, uint160[] limits, uint48[] renewPeriods)`.
+ This writes a spending policy for each already-registered key. The `keys` array MUST be
+ non-empty and reconfiguration is banned: once a key has been configured, neither it nor any
+ other key can be reconfigured under the current installation. To change the spending policy,
+ the account must uninstall and reinstall the module (which bumps the installation nonce and
+ resets state).
+
+
+
+This module deliberately disables ERC-1271 signing
+([`ERC7579ERC20Spender.isValidSignatureWithSender`](#ERC7579ERC20Spender-isValidSignatureWithSender-address-bytes32-bytes-) always returns `0xffffffff`) so a spending session key cannot be
+repurposed to authorize arbitrary off-chain messages on behalf of the account.
+
+
+
+Set `start` to a future timepoint to schedule a spending limit that activates later.
+
+
+
+Set [`ERC7579ERC20Spender.renewPeriod`](#ERC7579ERC20Spender-renewPeriod-address-bytes-address-) to `type(uint48).max` to disable the renewal period.
+
+
+
+
+Same as [`ERC7579ERC20Spender.spent`](#ERC7579ERC20Spender-spent-address-bytes-address-uint256-uint256-) but reads from an explicit `start` timepoint (useful for inspecting history).
+
+
+
+Returns `(start, period)` for `key` on `account` at `timepoint`.
+
+Requirements:
+
+* `timepoint` must be `>=` the key's `from`. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpendingConfigNotStarted`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigNotStarted-address-bytes32-uint256-uint256-) otherwise.
+* The spending config must be set. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpendingConfigNotSet`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigNotSet-address-bytes32-) otherwise.
+
+
+
+Sets up the module's initial configuration when installed by an account.
+
+This module is installed under BOTH module types and `onInstall` is called twice. The
+shape of `initData` differs between the two calls:
+
+* **First call** (validator install, `installed(msg.sender) == false`):
+ `abi.encode(bytes[] keys, uint48[] starts, uint48[] ends)`.
+* **Second call** (executor install, `installed(msg.sender) == true`):
+ `abi.encode(bytes[] keys, uint160[] limits, uint48[] renewPeriods)`.
+
+First call requirements:
+
+* The `keys` array must be non-empty. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderEmptyKeys`](#ERC7579NativeSpender-ERC7579NativeSpenderEmptyKeys--) otherwise.
+* Those from the parent's [`ERC7579SessionKeys.onInstall`](#ERC7579SessionKeys-onInstall-bytes-) (key registration).
+
+Second call requirements:
+
+* The account must not have configured the keys yet. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderAlreadyInstalled`](#ERC7579NativeSpender-ERC7579NativeSpenderAlreadyInstalled--) otherwise.
+* Those of [`ERC7579NativeSpender._decodeKeyConfigsInitData`](#ERC7579NativeSpender-_decodeKeyConfigsInitData-bytes-) for the decoding requirements.
+* All arrays must have equal length. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderMismatchedInitDataLengths`](#ERC7579NativeSpender-ERC7579NativeSpenderMismatchedInitDataLengths-uint256-uint256-uint256-) if not.
+* The `keys` array must be non-empty. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderEmptyKeys`](#ERC7579NativeSpender-ERC7579NativeSpenderEmptyKeys--) otherwise.
+* Each key must be registered. Reverts with [`ERC7579SessionKeys.ERC7579SessionKeysNonexistentKey`](#ERC7579SessionKeys-ERC7579SessionKeysNonexistentKey-bytes-) otherwise.
+* Those from [`ERC7579ERC20Spender._setConfig`](#ERC7579ERC20Spender-_setConfig-address-uint256-bytes-address-uint160-uint48-) for the spending config requirements.
+
+Emits [`ERC7579SessionKeys.ERC7579SessionKeysERC7913KeySet`](#ERC7579SessionKeys-ERC7579SessionKeysERC7913KeySet-address-bytes-uint48-uint48-) on the first call and [`ERC7579NativeSpender.ERC7579NativeSpendingConfigSet`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigSet-address-bytes-uint256-uint256-)
+for each entry on the second call.
+
+
+Each start will be set to the current timepoint if it's 0.
+
+
+
+
+Adds new session keys with their spending configurations in a single call. Can only be
+called by the account itself. See [`ERC7579NativeSpender._addKeysWithConfig`](#ERC7579NativeSpender-_addKeysWithConfig-address-uint256-bytes---uint48---uint48---uint160---uint48---) for requirements.
+
+
+
+
+
+
+
+
+
transferFrom(address account, bytes key, address to, uint256 amount)
+
+Transfers native tokens from `account` to `to` on behalf of `key`.
+
+This function tracks the spending limit for the `key` on the `account` and updates
+the spent amount for the current period before executing the transfer through the account's
+`IERC7579Execution-executeFromExecutor` function.
+
+Two authorization paths are accepted:
+
+1. **Executor flow**: `msg.sender == account`. The validator has already verified the session
+ key's signature and the calldata shape, so no further checks are performed here.
+2. **EOA self-call**: `key.length == 20` and `address(bytes20(key)) == msg.sender`. The key's
+ validity range is enforced manually.
+
+Requirements:
+
+* The caller is authorized via one of the two paths above. Reverts with
+ [`ERC7579NativeSpender.ERC7579NativeSpenderUnauthorized`](#ERC7579NativeSpender-ERC7579NativeSpenderUnauthorized-address-bytes-) otherwise.
+* `amount` must not exceed the available limit for the current period. Reverts with
+ [`ERC7579NativeSpender.ERC7579NativeInsufficientAvailable`](#ERC7579NativeSpender-ERC7579NativeInsufficientAvailable-address-bytes-uint256-) otherwise.
+* Those from [`ERC7579ERC20Spender._periodAt`](#ERC7579ERC20Spender-_periodAt-address-uint256-bytes32-address-uint48-) for the current timepoint.
+
+
+
+Transfers native tokens from `account` to multiple recipients in a single batch.
+
+Same authorization paths as [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-). The available limit check is performed once
+against the total of `amounts`.
+
+Requirements:
+
+* `tos.length == amounts.length`. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderMismatchedBatchLengths`](#ERC7579NativeSpender-ERC7579NativeSpenderMismatchedBatchLengths-uint256-uint256-) otherwise.
+* The caller is authorized via one of the two paths described in [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-).
+* The sum of `amounts` must not exceed the available limit.
+* Those from [`ERC7579ERC20Spender._periodAt`](#ERC7579ERC20Spender-_periodAt-address-uint256-bytes32-address-uint48-) for the current timepoint.
+
+
+
+Disables ERC-1271 signing by session keys.
+
+A spending session key is scoped to authorize [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) / [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---) user
+operations on this module. Letting it sign arbitrary ERC-1271 digests (e.g. EIP-2612
+permits) would let a compromised key bypass the spending bound entirely.
+
+
+
+Validates a user operation. Adds a callData shape gate on top of session-key validation:
+
+`userOp.callData` MUST call `IERC7579Execution.execute(mode, executionCalldata)` where:
+
+* `mode` is `CALLTYPE_SINGLE` (other call types are rejected).
+* `executionCalldata` decodes to `(target=address(this), value=0, data)`.
+* `data` calls [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) or [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---).
+* The `key` parameter inside `data` matches the key in `userOp.signature` (prevents one key's
+ signature from charging another key's allowance).
+
+Returns `ERC4337Utils.SIG_VALIDATION_FAILED` for any shape or key mismatch, otherwise delegates
+to the parent for cryptographic verification and validity-range packing.
+
+
+
+Override of [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---) that disallows registering a key with a past
+`start` timepoint.
+
+The spending bookkeeping in [`ERC7579ERC20Spender._spentFor`](#ERC7579ERC20Spender-_spentFor-mapping-address----mapping-uint256----mapping-bytes32----mapping-address----mapping-uint256----mapping-uint256----uint256------) is indexed by `(keyId, start, period)`. Letting a
+removed-then-re-registered key pick a past `start` would let it inherit `_spentFor` slots
+written under a previous validity range, which (combined with a different `renewPeriod`)
+can collide with previously-used period slots. Strictly incremental `start` values keep
+each registration's accounting in its own slot space.
+
+Requirements:
+
+* Each `starts[i]` must be either `0` or `>= clock()`. Reverts with
+ [`ERC7579NativeSpender.ERC7579NativeSpenderInvalidStart`](#ERC7579NativeSpender-ERC7579NativeSpenderInvalidStart-uint256-) otherwise.
+* Those from [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---).
+
+
+Pass `starts[i] == 0` to resolve the start to the current timepoint.
+
+
+
+
+Removes session keys from the calling account. Also clears the spending configuration
+for each key.
+
+
+Spending history (per-period `spent` values) is preserved in storage at the historical
+start timepoint. It is no longer surfaced through `spent-address-bytes-uint256--` (which uses
+the current [`ERC7579SessionKeys.validity`](#ERC7579SessionKeys-validity-address-bytes-)'s `from`, now zero) but can still be read via
+`spent-address-bytes-uint256-uint256--` with the historical start.
+
+
+Emits [`ERC7579SessionKeys.ERC7579SessionKeysERC7913KeyUnset`](#ERC7579SessionKeys-ERC7579SessionKeysERC7913KeyUnset-address-bytes-) (via the parent) and [`ERC7579NativeSpender.ERC7579NativeSpendingConfigUnset`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigUnset-address-bytes-)
+for each key.
+
+
+
+Internal version of [`ERC7579ERC20Spender.periodAt`](#ERC7579ERC20Spender-periodAt-address-bytes-address-uint48-) with explicit `installation_`.
+
+Requirements:
+
+* `timepoint` must be `>=` the key's `from`. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpendingConfigNotStarted`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigNotStarted-address-bytes32-uint256-uint256-) otherwise.
+* The spending config must be set. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpendingConfigNotSet`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigNotSet-address-bytes32-) otherwise.
+
+
+
+Internal version of [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) without access control.
+
+
+
+Internal version of [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---) without access control.
+
+
+
+Internal function to set the spending configuration for a key.
+
+Reconfiguration is banned at the [`ERC7579ECDSA.onInstall`](#ERC7579ECDSA-onInstall-bytes-) level, so this function is only ever invoked
+with a freshly-registered (or freshly-reinstalled) key whose existing config slot is zero.
+
+Requirements:
+
+* Renewal periods must be greater than zero. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderInvalidRenewPeriod`](#ERC7579NativeSpender-ERC7579NativeSpenderInvalidRenewPeriod-uint256-) if not.
+* Limits must be greater than zero. Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderInvalidLimit`](#ERC7579NativeSpender-ERC7579NativeSpenderInvalidLimit-uint256-) if not.
+
+Emits a [`ERC7579NativeSpender.ERC7579NativeSpendingConfigSet`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigSet-address-bytes-uint256-uint256-) event.
+
+
+
+Registers keys with validity range AND spending config in one pass.
+
+Requirements:
+
+* `newKeys.length == limits.length == renewPeriods.length`. Reverts with
+ [`ERC7579NativeSpender.ERC7579NativeSpenderMismatchedInitDataLengths`](#ERC7579NativeSpender-ERC7579NativeSpenderMismatchedInitDataLengths-uint256-uint256-uint256-) otherwise.
+* Those from the parent's [`ERC7579SessionKeys._addKeys`](#ERC7579SessionKeys-_addKeys-address-uint256-bytes---uint48---uint48---) (key registration).
+* Those from [`ERC7579ERC20Spender._setConfig`](#ERC7579ERC20Spender-_setConfig-address-uint256-bytes-address-uint160-uint48-) for the spending config requirements.
+
+Emits [`ERC7579SessionKeys.ERC7579SessionKeysERC7913KeySet`](#ERC7579SessionKeys-ERC7579SessionKeysERC7913KeySet-address-bytes-uint48-uint48-) and [`ERC7579NativeSpender.ERC7579NativeSpendingConfigSet`](#ERC7579NativeSpender-ERC7579NativeSpendingConfigSet-address-bytes-uint256-uint256-) for each key.
+
+
+
+Returns whether `userOp.callData` is a permitted call into [`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) or
+[`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---) on this module, and whether its `key` argument matches the one
+decoded from `userOp.signature`.
+
+
+
+Decodes the outer `IERC7579Execution.execute(SINGLE, executionCalldata)` call.
+
+Returns `(false, "")` if the call doesn't match the expected shape.
+
+
+
+Verifies the inner single execution targets this module with zero value, calls
+[`ERC7579ERC20Spender.transferFrom`](#ERC7579ERC20Spender-transferFrom-address-bytes-address-address-uint256-) or [`ERC7579ERC20Spender.batchTransferFrom`](#ERC7579ERC20Spender-batchTransferFrom-address-bytes-address-address---uint256---), and returns the `key` argument extracted from the
+inner calldata.
+
+Returns `(false, "")` if any check fails.
+
+
+
+Decodes the initialization data for the second install of the module.
+
+The `initData` must be ABI-encoded as `abi.encode(bytes[] keys, uint160[] limits, uint48[] renewPeriods)`
+where each array has the same length.
+
+Requirements:
+
+* `initData` must be at least 192 bytes (6 Γ 32 bytes for the 3 ABI offsets and 3 array lengths).
+* All offsets must point within the `initData` bounds.
+
+Reverts with [`ERC7579NativeSpender.ERC7579NativeSpenderInvalidInitData`](#ERC7579NativeSpender-ERC7579NativeSpenderInvalidInitData--) if the encoding is invalid.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/executors/ERC7579SocialRecoveryExecutor.sol";
+```
+
+Social recovery executor module for ERC-7579 accounts.
+
+This module combines weighted multisignatures with time-delayed execution to provide
+a secure and flexible social recovery mechanism. Trusted guardians can collectively schedule
+recovery operations, which execute after a security delay, giving the legitimate account owner
+time to detect and cancel malicious recovery attempts.
+
+To schedule operations, guardians must sign an EIP-712 typed data hash of the operation using
+the account's domain separator and provide it to the [`ERC7579SocialRecoveryExecutor.startRecovery`](#ERC7579SocialRecoveryExecutor-startRecovery-address-bytes32-bytes32-bytes-bytes-) function. Once scheduled,
+operations must wait for the configured delay period before becoming executable via [`ERC7579SocialRecoveryExecutor.executeRecovery`](#ERC7579SocialRecoveryExecutor-executeRecovery-address-bytes32-bytes32-bytes-).
+
+Operations can be canceled either by the account itself or by guardians via [`ERC7579SocialRecoveryExecutor.cancelRecovery`](#ERC7579SocialRecoveryExecutor-cancelRecovery-address-bytes32-bytes32-bytes-)
+with a valid multisignature.
+
+See [`ERC7579DelayedExecutor`](#ERC7579DelayedExecutor) and [`ERC7579MultisigWeighted`](#ERC7579MultisigWeighted) for more details.
+
+
+This module does not restrict execution by default once the social recovery operation
+is ready. Anyone can call [`ERC7579SocialRecoveryExecutor.executeRecovery`](#ERC7579SocialRecoveryExecutor-executeRecovery-address-bytes32-bytes32-bytes-) once the delay period has elapsed.
+
+
+
+
+Installs the social recovery executor module on an account.
+
+This function configures both the delayed executor and weighted multisig components
+required for the social recovery mechanism. The installation data must contain
+configuration for both components in the specified encoding format.
+
+Data encoding format: `[uint16(executorArgsLength), executorArgs, uint16(multisigArgsLength), multisigArgs]`
+
+Requirements:
+
+* See [`ERC7579DelayedExecutor.onInstall`](#ERC7579DelayedExecutor-onInstall-bytes-) for delay/expiration requirements
+* See [`ERC7579MultisigWeighted.onInstall`](#ERC7579MultisigWeighted-onInstall-bytes-) for guardian configuration requirements
+
+
+
+Uninstalls the social recovery executor module from an account.
+
+This cleans up both the delayed executor configuration and all guardian-related state.
+After uninstallation, all social recovery capabilities are removed.
+
+
+Ensure alternative recovery mechanisms or access methods are in place
+before uninstalling this module. Uninstalling social recovery without a backup
+access method could result in account lockout if primary credentials are lost.
+
+
+See [`ERC7579DelayedExecutor.onUninstall`](#ERC7579DelayedExecutor-onUninstall-bytes-) and [`ERC7579MultisigWeighted.onUninstall`](#ERC7579MultisigWeighted-onUninstall-bytes-).
+
+
+
+Returns a slice of the set of guardians for the specified account. See [`ERC7579Multisig.getSigners`](#ERC7579Multisig-getSigners-address-uint64-uint64-).
+
+
+
+Returns whether the `guardian` is an authorized guardian for the specified account. See [`ERC7579Multisig.isSigner`](#ERC7579Multisig-isSigner-address-bytes-).
+
+
+
+Removes guardians from the authorized set for the calling account. See [`ERC7579Multisig.removeSigners`](#ERC7579Multisig-removeSigners-bytes---).
+
+
+
+Schedules a social recovery operation for the specified account.
+
+The `recoveryData` is the encoded execution that will be sent to the account, and it
+follows the ERC-7579 standard encoding. The `multisignature` is a valid authorization
+from the guardians to schedule the operation and must be signed over the
+[`ERC7579SocialRecoveryExecutor.SCHEDULE_RECOVER_TYPEHASH`](#ERC7579SocialRecoveryExecutor-SCHEDULE_RECOVER_TYPEHASH-bytes32) typed data hash of the operation using the account's domain
+separator.
+
+Requirements:
+
+* The multisignature must be valid and meet the threshold requirements.
+ Reverts with [`ERC7579SocialRecoveryExecutor.ERC7579SocialRecoveryExecutorInvalidSchedule`](#ERC7579SocialRecoveryExecutor-ERC7579SocialRecoveryExecutorInvalidSchedule--) if not.
+* The signature must be encoded properly. See `ERC7579MultisigWeighted-_validateMultisig`.
+* See [`ERC7579DelayedExecutor._schedule`](#ERC7579DelayedExecutor-_schedule-address-bytes32-bytes32-bytes-) for scheduling requirements.
+* The `account` must implement `IERC5267`.
+
+
+
+Executes a social recovery operation for the specified account.
+
+This function can be called by any account once the operation is ready (i.e., the delay
+period has elapsed). The operation must be in the `Ready` state to be executed.
+
+Requirements:
+
+* See [`ERC7579DelayedExecutor._execute`](#ERC7579DelayedExecutor-_execute-address-bytes32-bytes32-bytes-) for execution requirements.
+
+
+
+Cancels a social recovery operation.
+
+This function allows either the account itself or guardians with a valid multisignature
+authorization to cancel a scheduled recovery operation. If the caller is not the account,
+the `multisignature` must contain valid guardian signatures over the [`ERC7579SocialRecoveryExecutor.CANCEL_RECOVER_TYPEHASH`](#ERC7579SocialRecoveryExecutor-CANCEL_RECOVER_TYPEHASH-bytes32)
+typed data hash using the account's domain separator.
+
+Uninstalling the module resets the threshold to zero while scheduled operations are retained
+(see [`ERC7579DelayedExecutor.onUninstall`](#ERC7579DelayedExecutor-onUninstall-bytes-)). Guardian-authorized cancellation is therefore only
+available while a guardian configuration is active. The account keeps its own cancellation path
+in either case.
+
+Requirements:
+
+* The caller must be the account or a valid `multisignature` must be provided.
+* A `multisignature` only authorizes the cancellation while the [`ERC7579Multisig.threshold`](#ERC7579Multisig-threshold-address-) is nonzero.
+* See [`ERC7579DelayedExecutor._cancel`](#ERC7579DelayedExecutor-_cancel-address-bytes32-bytes32-bytes-) for cancellation requirements.
+* The `account` must implement `IERC5267`.
+
+
+
+Same as [`ERC7579SocialRecoveryExecutor.cancelRecovery`](#ERC7579SocialRecoveryExecutor-cancelRecovery-address-bytes32-bytes32-bytes-), but without a `multisignature`.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/validators/ERC7579ECDSAValidator.sol";
+```
+
+Implementation of an [`ERC7579ECDSA`](#ERC7579ECDSA) as an [`ERC7579Validator`](#ERC7579Validator).
+
+This module provides ECDSA signature validation for ERC-7579 accounts. Each account
+can have a single authorized signer (EOA address) that can approve operations through
+ECDSA signatures.
+
+Example usage:
+
+```solidity
+// Install the validator with a signer address
+bytes memory initData = abi.encodePacked(signerAddress);
+account.installModule(MODULE_TYPE_VALIDATOR, validatorAddress, initData);
+```
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/validators/ERC7579MultisigConfidentialValidator.sol";
+```
+
+Implementation of [`ERC7579Validator`](#ERC7579Validator) that uses [`ERC7579MultisigConfidential`](#ERC7579MultisigConfidential)
+for confidential multisignature validation of user operations (see ERC-4337).
+
+This extension bypasses the explicit [`ERC7579MultisigConfidential.discloseOperation`](#ERC7579MultisigConfidential-discloseOperation-address-bytes32-bytes-) call
+and performs disclosure implicitly inside `_rawERC7579Validation`: when the user op
+signature includes a valid FHE decryption proof for the multisig result (relayer address
+plus proof after the first 20 bytes), the validator accepts the user op without requiring
+a prior call to `discloseOperation`. This reduces the flow to two steps:
+
+1. Call [`ERC7579MultisigConfidential.requestDiscloseOperation`](#ERC7579MultisigConfidential-requestDiscloseOperation-address-bytes32-bytes---euint256---bytes---) to validate the multisig
+ and make the encrypted result publicly decryptable.
+2. Submit the user operation to the entrypoint with the decryption proof in the signature;
+ the entrypoint calls `validateUserOp`, which invokes `_rawERC7579Validation` to verify
+ the proof (or that the operation was explicitly disclosed via [`ERC7579MultisigConfidential.disclosed`](#ERC7579MultisigConfidential-disclosed-address-bytes32-)).
+
+
+The operation is not marked as disclosed in storage when validation succeeds via the
+inline proof. That is intentional: the fact that the user op executed implies the
+multisig was valid and effectively disclosed, and offchain indexers can infer it by
+watching entrypoint events instead of relying on the [`ERC7579MultisigConfidential.disclosed`](#ERC7579MultisigConfidential-disclosed-address-bytes32-) boolean.
+
+
+
+By definition, the user operation is public in the bundler mempool the moment it's
+submitted. Users must not assume that the user operation is confidential, and should consider
+front running risks associated with the user operation payload.
+
+
+
+
+Validates the operation by either verifying an inline FHE decryption proof
+in the signature (relayer address in the first 20 bytes, proof thereafter) or by
+checking [`ERC7579MultisigConfidential.disclosed`](#ERC7579MultisigConfidential-disclosed-address-bytes32-). Does not call [`ERC7579MultisigConfidential.discloseOperation`](#ERC7579MultisigConfidential-discloseOperation-address-bytes32-bytes-)
+or set the disclosed flag when the proof is valid; disclosure is implicit in execution.
+
+
+
+Returns whether the number of valid signatures meets or exceeds the
+threshold set for the target account.
+
+See [`Multisig.tryDecodeMultisignatureCalldata`](/accounts/api/utils#Multisig-tryDecodeMultisignatureCalldata-bytes-) for the encoding details.
+
+Where `signingSigners` are the authorized signers and signatures are their corresponding
+signatures of the operation `hash`.
+
+
+
+Sets up the module's initial configuration when installed by an account.
+Besides the standard signer and threshold configuration, this can also include
+encrypted signer weights.
+
+See [`ERC7579MultisigWeighted._decodeMultisigWeightedInitData`](#ERC7579MultisigWeighted-_decodeMultisigWeightedInitData-bytes-) for the `initData` encoding details.
+
+
+An account can only call `onInstall` once. If called directly by the account,
+the signers, weights, and threshold will be set to the provided data. Future
+installations will revert with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--).
+
+
+Requirements:
+
+* The `account` must have no signers installed. Reverts with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--) if not.
+* The `signers` array must be non-empty. Reverts with [`ERC7579Multisig.ERC7579MultisigEmptySigners`](#ERC7579Multisig-ERC7579MultisigEmptySigners--) if not.
+* Those of [`ERC7579MultisigWeighted._decodeMultisigWeightedInitData`](#ERC7579MultisigWeighted-_decodeMultisigWeightedInitData-bytes-) for the decoding requirements.
+* Those of [`ERC7579Multisig.addSigners`](#ERC7579Multisig-addSigners-bytes---) and [`ERC7579MultisigWeighted.setSignerWeights`](#ERC7579MultisigWeighted-setSignerWeights-bytes---uint64---) for signer and weight configuration requirements.
+
+
+
+Cleans up module's configuration when uninstalled from an account.
+Clears all signers, weights, and total weights.
+
+See [`ERC7579MultisigConfidential.onUninstall`](#ERC7579MultisigConfidential-onUninstall-bytes-).
+
+
+
+Override to add weight tracking and restrict signer count. See [`ERC7579MultisigConfidential._addSigners`](#ERC7579MultisigConfidential-_addSigners-address-euint256---).
+Each new signer has a default weight of 1.
+
+Requirements:
+
+* The number of signers must not exceed `type(uint16).max`. Reverts with [`ERC7579MultisigConfidential.ERC7579MultisigTooManySigners`](#ERC7579MultisigConfidential-ERC7579MultisigTooManySigners--) if exceeded.
+
+
+
+Override to handle weight tracking during removal. See [`ERC7579MultisigConfidential._removeSigners`](#ERC7579MultisigConfidential-_removeSigners-address-euint256---).
+
+Cleans up weights for removed signers and updates the total extra weight accordingly.
+
+
+
+Override to return the maximum threshold based on total weight instead of signer count.
+See [`ERC7579MultisigConfidential._maxThreshold`](#ERC7579MultisigConfidential-_maxThreshold-address-).
+
+
+
+Override to return the encrypted weight of a signer instead of a fixed weight of 1.
+See [`ERC7579MultisigConfidential._signerWeight`](#ERC7579MultisigConfidential-_signerWeight-address-euint256-).
+
+Returns 0 if the signer is not authorized, otherwise returns 1 plus the signer's extra weight.
+
+
+
+Sets up the module's initial configuration when installed by an account.
+Besides the standard delay and signer configuration, this can also include
+signer weights.
+
+The initData should be encoded as:
+`abi.encode(bytes[] signers, uint64 threshold, uint64[] weights)`
+
+
+An account can only call onInstall once. If called directly by the account,
+the signer will be set to the provided data. Future installations will revert with
+[`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--).
+
+
+
+This function skips `super.onInstall` to avoid setting the threshold during installation
+since it can be greater than the total amount of signers. This function adds the signers, sets the weights,
+and sets the threshold.
+
+
+Requirements:
+
+* The `account` must have no signers installed. Reverts with [`ERC7579Multisig.ERC7579MultisigAlreadyInstalled`](#ERC7579Multisig-ERC7579MultisigAlreadyInstalled--) if not.
+* Those of [`ERC7579MultisigWeighted._decodeMultisigWeightedInitData`](#ERC7579MultisigWeighted-_decodeMultisigWeightedInitData-bytes-) for the decoding requirements.
+
+
+
+Cleans up module's configuration when uninstalled from an account.
+Clears all signers, weights, and total weights.
+
+See [`ERC7579Multisig.onUninstall`](#ERC7579Multisig-onUninstall-bytes-).
+
+
+
+Override to add weight tracking. See [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---).
+Each new signer has a default weight of 1.
+
+In cases where [`ERC7579MultisigWeighted.totalWeight`](#ERC7579MultisigWeighted-totalWeight-address-) is almost `type(uint64).max` (due to a large `_totalExtraWeight`), adding new
+signers could cause the [`ERC7579MultisigWeighted.totalWeight`](#ERC7579MultisigWeighted-totalWeight-address-) computation to overflow. Adding a [`ERC7579MultisigWeighted.totalWeight`](#ERC7579MultisigWeighted-totalWeight-address-) call after the new
+signers are added ensures no such overflow happens.
+
+
+
+Override to handle weight tracking during removal. See [`ERC7579Multisig._removeSigners`](#ERC7579Multisig-_removeSigners-address-bytes---).
+
+Just like [`ERC7579Multisig._addSigners`](#ERC7579Multisig-_addSigners-address-bytes---), this function does not emit [`ERC7579MultisigWeighted.ERC7579MultisigWeightChanged`](#ERC7579MultisigWeighted-ERC7579MultisigWeightChanged-address-bytes-uint64-) events. The
+[`ERC7579Multisig.ERC7913SignerRemoved`](#ERC7579Multisig-ERC7913SignerRemoved-address-bytes-) event emitted by [`ERC7579Multisig._removeSigners`](#ERC7579Multisig-_removeSigners-address-bytes---) is enough to track weights here.
+
+
+
+Override to validate threshold against total weight instead of signer count.
+
+
+This function intentionally does not call `super._validateReachableThreshold` because the base implementation
+assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple
+implementations of this function may exist in the contract, so important side effects may be missed
+depending on the linearization order.
+
+
+
+
+Validates that the total weight of signers meets the [`ERC7579Multisig.threshold`](#ERC7579Multisig-threshold-address-) requirement.
+Overrides the base implementation to use weights instead of count.
+
+
+This function intentionally does not call `super._validateThreshold` because the base implementation
+assumes each signer has a weight of 1, which is incompatible with this weighted implementation.
+
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/validators/ERC7579P256Validator.sol";
+```
+
+Implementation of an [`ERC7579P256`](#ERC7579P256) as an [`ERC7579Validator`](#ERC7579Validator).
+
+This module provides P256 signature validation for ERC-7579 accounts. Each account
+can have a single authorized P256 signer (public key coordinates) that can approve
+operations through P256 signatures.
+
+Example usage:
+
+```solidity
+// Install the validator with P256 public key coordinates
+bytes memory initData = abi.encodePacked(qx, qy);
+account.installModule(MODULE_TYPE_VALIDATOR, validatorAddress, initData);
+```
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/validators/ERC7579SessionKeysValidator.sol";
+```
+
+Implementation of [`ERC7579SessionKeys`](#ERC7579SessionKeys) as an [`ERC7579Validator`](#ERC7579Validator).
+
+This module provides session key validation for ERC-7579 accounts. Each account can have
+multiple registered ERC-7913 session keys, each with a configurable validity range. A
+session key is only authorized to validate operations within its `from` to `until` timepoints.
+
+
+By default, any registered key within its validity range can validate any user
+operation and sign any ERC-1271 message on behalf of the account. Derived contracts
+should restrict the scope of what a session key is authorized to do.
+
+
+Example usage:
+
+```solidity
+// Install the validator with session keys
+bytes memory initData = abi.encode(keys, starts, ends);
+account.installModule(MODULE_TYPE_VALIDATOR, validatorAddress, initData);
+```
+
+
+
+See `IERC7579Validator-isValidSignatureWithSender`.
+
+Enforces the validity range of the session key manually since the EntryPoint is not
+involved in ERC-1271 signature validation. See [`ERC7579SessionKeys._tryDecodeSignature`](#ERC7579SessionKeys-_tryDecodeSignature-bytes-) for the encoding details.
+
+
+
+See [`ERC7579Validator._validateUserOp`](#ERC7579Validator-_validateUserOp-struct-PackedUserOperation-bytes32-).
+
+Decodes the session key from `userOp.signature` and reads its validity range. The range
+is packed into the returned `validationData` so the EntryPoint enforces it without
+requiring an explicit timestamp check in the validator.
+See [`ERC7579SessionKeys._tryDecodeSignature`](#ERC7579SessionKeys-_tryDecodeSignature-bytes-) for the signature encoding details.
+
+
+Returns `ERC4337Utils.SIG_VALIDATION_FAILED` immediately if [`ERC7579SessionKeys.CLOCK_MODE`](#ERC7579SessionKeys-CLOCK_MODE--) returns a mode
+other than timestamp or blocknumber from the same chain (default), as the EntryPoint does not support
+other validity range formats.
+
+
+
+
+See `IERC7579Validator-isValidSignatureWithSender`.
+
+Ignores the `sender` parameter and validates using [`ERC7579ECDSAValidator._rawERC7579Validation`](#ERC7579ECDSAValidator-_rawERC7579Validation-address-bytes32-bytes-).
+Consider overriding this function to implement custom validation logic
+based on the original sender.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/modules/validators/ERC7579WebAuthnValidator.sol";
+```
+
+Implementation of an [`ERC7579WebAuthn`](#ERC7579WebAuthn) as an [`ERC7579P256Validator`](#ERC7579P256Validator).
+
+This module provides WebAuthn signature validation for ERC-7579 accounts. Each account
+can have a single authorized WebAuthn credential (P256 public key) that can approve
+operations through WebAuthn authentication assertions.
+
+Example usage:
+
+```solidity
+// Install the validator with WebAuthn public key coordinates
+bytes memory initData = abi.encodePacked(qx, qy);
+account.installModule(MODULE_TYPE_VALIDATOR, validatorAddress, initData);
+```
+
+
+
diff --git a/content/accounts/api/utils.mdx b/content/accounts/api/utils.mdx
new file mode 100644
index 00000000..40b53e44
--- /dev/null
+++ b/content/accounts/api/utils.mdx
@@ -0,0 +1,738 @@
+---
+title: "Utils"
+description: "Smart contract utils utilities and implementations"
+---
+
+Libraries and helpers used by the account and its modules.
+
+- [`Multisig`](#Multisig): Encoding and decoding of multisignatures and their signer sets.
+- [`CrossChainSignatureChecker`](#CrossChainSignatureChecker): Verification of ERC-7964 crosschain signatures.
+- [`ERC8043Utils`](#ERC8043Utils): Encoding helpers for the ERC-8043 init mode.
+- [`MultisigCoordinator`](#MultisigCoordinator): Coordinates FHE inputs and decryption rights between the signers of a multisig account.
+
+## Libraries
+
+[`Multisig`](#Multisig)
+
+[`CrossChainSignatureChecker`](#CrossChainSignatureChecker)
+
+[`ERC8043Utils`](#ERC8043Utils)
+
+## Coordination
+
+[`MultisigCoordinator`](#MultisigCoordinator)
+
+
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/utils/CrossChainSignatureChecker.sol";
+```
+
+Crosschain signature verification helper for ERC-7964 compliant signatures.
+
+[ERC-7964](https://eips.ethereum.org/EIPS/eip-7964) enables a single signature to
+be valid across multiple chains by omitting the `chainId` from the EIP-712 domain
+and encoding chain-specific operations as an array of struct hashes. This helper
+provides functions to parse and verify such crosschain signatures.
+
+The signature encoding includes metadata that allows contracts to:
+* Detect crosschain signatures via a magic prefix
+* Identify which operation in the array corresponds to the current chain
+* Reconstruct the full EIP-712 hash for verification
+* Validate the signature using standard ECDSA or ERC-1271
+
+
+
+Verifies an ERC-7964 crosschain signature for a given `signer`, `hash` and `erc7964Signature`.
+Uses the provided `structHash` function to compute the main struct hash from the array of
+chain-specific struct hashes.
+
+This function:
+
+1. Parses the ERC-7964 signature encoding to extract metadata
+2. Verifies that the provided `hash` matches the struct at `structIndex` in the array
+3. Queries the application contract for EIP-712 domain information
+4. Reconstructs the full typed hash using the domain and array of struct hashes
+5. Validates the signature against the reconstructed hash
+
+
+Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
+change through time. It could return true at block N and false at block N+1 (or the opposite).
+
+
+
+Make sure the `structHash` function pointer manages memory correctly. Otherwise,
+this function may behave unexpectedly.
+
+
+
+
+Variant of [`CrossChainSignatureChecker.isValidCrossChainSignatureNow`](#CrossChainSignatureChecker-isValidCrossChainSignatureNow-address-bytes32-bytes-function--bytes32--view-returns--bytes32--) that takes the signature in calldata.
+
+
+
+Parses an ERC-7964 encoded crosschain signature into its components:
+
+* `success` True if parsing succeeded, false if the signature is malformed or the magic value is incorrect
+* `fields` The EIP-712 domain fields byte from ERC-5267
+* `structIndex` The index of the current chain's operation in the array
+* `application` The address of the contract providing EIP-712 domain info
+* `structsArray` The array of struct hashes for all chains
+* `crossChainSignature` The actual signature bytes
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/utils/ERC8043Utils.sol";
+```
+
+Utility functions for the [ERC-8043](https://eip.tools/eip/8043) init mode.
+
+Init mode allows an ERC-4337 user operation to install one or more ERC-7579 modules
+atomically alongside its regular execution. It is triggered by prefixing the user
+operation's `initCode` with [`ERC8043Utils.INIT_MODE_MAGIC_VALUE`](#ERC8043Utils-INIT_MODE_MAGIC_VALUE-bytes4); the remaining bytes carry an
+ABI-encoded installation payload followed by an EIP-712 signature over the
+`InitModules` struct authorizing the installation.
+
+
+
+Computes the EIP-712 struct hash of the `InitModules` message.
+
+```
+InitModules(uint256[] moduleTypeIds, address[] modules, bytes[] initDatas, uint256 nonce)
+```
+
+`nonce` is the user operation's nonce; combining it into the signed payload binds
+the installation authorization to a specific user operation. Callers must wrap the
+returned struct hash with the account's EIP-712 domain separator (e.g.
+`_hashTypedDataV4`) before verifying `installationSignature`.
+
+
+
+Attempts to decode an ERC-8043 init mode payload from `data`.
+
+`data` must be the user operation's `initCode` with the [`ERC8043Utils.INIT_MODE_MAGIC_VALUE`](#ERC8043Utils-INIT_MODE_MAGIC_VALUE-bytes4)
+4-byte prefix already stripped. Callers are expected to detect init mode
+themselves β typically by comparing `initCode[:4]` against [`ERC8043Utils.INIT_MODE_MAGIC_VALUE`](#ERC8043Utils-INIT_MODE_MAGIC_VALUE-bytes4)
+β before invoking this function.
+
+`valid` is `true` when the payload decodes into four consistent, non-empty
+arrays: `moduleTypeIds.length == modules.length == initDatas.length > 0`. When
+`valid` is `false` the returned slices should not be relied on; individual
+fields whose sub-decoder failed are returned as empty slices, but fields that
+decoded before the inconsistency was detected may be populated. Callers that
+want strict init-mode semantics should reject the user operation in that case.
+
+The decoding never reverts on malformed input: length and offset checks are
+performed inline before any calldata slice is materialized.
+
+
+
+Decodes a multisignature data from calldata without memory allocation.
+
+The multisignature should be encoded as:
+`abi.encode(bytes[] signingSigners, bytes[] signatures)`
+
+Where `signingSigners` are the authorized signers and `signatures` are their corresponding
+signatures of the operation hash. This function is equivalent to
+`abi.decode(multisignature, (bytes[], bytes[]))` but operates directly on calldata to avoid
+memory allocation, improving gas efficiency during multisignature validation.
+
+
+This function does not validate the returned signers and signatures have the same length.
+
+
+
+
+Same as [`Multisig.tryDecodeMultisignatureCalldata`](#Multisig-tryDecodeMultisignatureCalldata-bytes-), but reverts if the multisignature is invalid.
+
+Requirements:
+
+* The `multisignature` must be at least 64 bytes long to contain the two offset words.
+* Both array offsets must be properly aligned and within bounds.
+* Both array length fields must be accessible within the provided data.
+* The arrays should have the same length for proper signature validation.
+
+
+
+```solidity
+import "@openzeppelin-accounts/contracts/utils/MultisigCoordinator.sol";
+```
+
+Utility contract that bridges multisig accounts with Fully Homomorphic Encryption (FHE).
+
+When a multisig account interacts with a confidential token (e.g. ERC-7984), one signer
+typically constructs the encrypted input off-chain and packages it with an input proof. The
+remaining signers cannot independently inspect or reconstruct the encrypted value, they must
+trust the submitting signer's off-chain preparation.
+
+Additionally, Zama's FHE Access Control List (ACL) only supports EOA addresses as decryption
+recipients; it does not natively support smart-contract multisigs. This means signers that are
+not EOAs cannot receive decryption rights that their signers can act on.
+
+### Solution
+
+This contract provides two capabilities:
+
+1. **Encrypted value creation**: Any caller can submit an externally-encrypted FHE value along
+ with its input proof. The contract verifies the proof and stores ACL permissions so that both
+ this coordinator and the target account can subsequently use or re-share the handle.
+ The resulting handle is returned for use in downstream calls (e.g. `confidentialTransferFrom`).
+
+2. **Handle allowance granting**: Any authorized signer (or the account itself) can grant
+ ACL permission over an existing encrypted handle to an arbitrary EOA or contract address.
+ This lets signers individually expose a confidential value to their own EOA for decryption,
+ without the account contract having to approve anything.
+
+### Setup
+
+This is an abstract contract. Concrete deployments must also inherit an fhEVM coprocessor
+configuration. For Zama's fhEVM this is done by inheriting `ZamaEthereumConfig` from the
+[FHEVM Solidity package](https://github.com/zama-ai/fhevm/tree/main/library-solidity) alongside
+this contract:
+
+```solidity
+contract MyMultisigCoordinator is MultisigCoordinator, ZamaEthereumConfig {}
+```
+
+A single deployment can serve multiple accounts: each account is identified by its address.
+Before using [`MultisigCoordinator.allowHandle`](#MultisigCoordinator-allowHandle-address-bytes-bytes-bytes32-address-), an account must register its associated
+[`ERC7579Multisig`](/accounts/api/modules#ERC7579Multisig) module by calling [`MultisigCoordinator.setERC7579Multisig`](#MultisigCoordinator-setERC7579Multisig-address-) from the account itself (e.g. via a
+UserOperation's `execute` call).
+
+### Signature scheme
+
+When an explicit signer and signature are supplied, the signed digest is an EIP-712 typed
+data hash anchored to the `account`'s own domain separator. The domain is retrieved at
+call-time via `IERC5267-eip712Domain`, so no additional configuration of this coordinator
+is needed.
+
+
+[`MultisigCoordinator.fromExternal`](#MultisigCoordinator-fromExternal-address-bytes32-bytes-enum-FheType-) does not require prior registration; it grants ACL permissions to
+any address provided as `account`, regardless of whether a multisig has been configured.
+
+
+
+
+Returns the address of the [`ERC7579Multisig`](/accounts/api/modules#ERC7579Multisig) module associated with `account`.
+Returns `address(0)` if no multisig has been registered.
+
+
+
+Registers or updates the [`ERC7579Multisig`](/accounts/api/modules#ERC7579Multisig) module address for the calling account.
+
+This should be called by the account itself, typically via a UserOperation's `execute` call,
+so that [`MultisigCoordinator.allowHandle`](#MultisigCoordinator-allowHandle-address-bytes-bytes-bytes32-address-) can verify signer authority.
+
+Requirements:
+
+* `multisig_` must not be the zero address.
+
+
+
+Verifies an externally-encrypted handle input proof and registers ACL permissions.
+
+The proof is validated by `FHE-fromExternal`, after which both this coordinator (`address(this)`)
+and the target `account` are permanently allowed to use the resulting handle.
+
+The returned handle can be cast to the appropriate type using with `ebool.wrap(externalHandle)`
+for `ebool`, `eaddress.wrap(externalHandle)` for `eaddress`, `euint8.wrap(externalHandle)` for
+`euint8` and so on.
+
+
+Anyone can call this function for any `account` address. ACL permissions are additive
+and the caller has no ability to revoke them, so there is no griefing risk.
+
+
+
+
+Grants ACL permission over an encrypted `handle` to address `to`.
+
+`msg.sender` must be `account` itself or a registered signer EOA.
+See `allowHandle-address-bytes-bytes-bytes32-address-` for the full authorization
+rules and requirements.
+
+
+
+Grants a permanent ACL permission over an encrypted `handle` to address `to`, verified
+against the account's registered [`ERC7579Multisig`](/accounts/api/modules#ERC7579Multisig).
+
+See [`MultisigCoordinator._validateAccess`](#MultisigCoordinator-_validateAccess-address-bytes-bytes-bytes32-) for the full authorization rules.
+
+Once authorization is confirmed, the `handle` is permanently allowed to `to` via the FHE
+ACL. This enables `to` (typically an EOA) to request off-chain decryption of the value.
+
+
+This grant is permanent and irrevocable. If `to` is later removed from the
+multisig, they will still be able to decrypt any handle they were previously allowed. To
+remove a former signer's access, re-encrypt the sensitive values using fresh FHE inputs
+and grant access only to current signers.
+
+
+Requirements:
+
+* `account` must have a multisig registered via [`MultisigCoordinator.setERC7579Multisig`](#MultisigCoordinator-setERC7579Multisig-address-) and installed as a validator.
+* `account` must implement `IERC5267`.
+* The EIP-712 domain returned by `account` must not set the extensions flag (bit `0x20` of `fields`),
+
+
+
+Validates that `signer` is authorized to act for `account` over the given `hash`.
+
+First confirms that the registered multisig module is actually installed as a validator on
+the account (via `IERC7579ModuleConfig-isModuleInstalled`), then applies the following
+authorization rules:
+
+- If `signer` is empty: `msg.sender` must be `account` itself or a registered signer EOA.
+- If `signer` is exactly 20 bytes: treated as an EOA; must be a registered signer and
+ `signature` must be a valid EIP-712 signature over `hash`.
+- If `signer` is more than 20 bytes: treated as an ERC-7913 signer (`verifier || key`);
+ must be a registered signer and `signature` is forwarded to the verifier via
+ `IERC7913SignatureVerifier-verify`.
+
+
+Existing FHE ACL permissions (`FHE.isAllowed`) are intentionally not checked here.
+Authorization is strictly tied to the account's multisig membership. Callers that already
+hold ACL access to a handle and wish to extend it to another address can do so directly via
+the FHE ACL (e.g. `FHE.allow`) without involving this coordinator.
+
+
+Requirements:
+
+* `account` must have a multisig registered via [`MultisigCoordinator.setERC7579Multisig`](#MultisigCoordinator-setERC7579Multisig-address-) and installed as a validator.
+
+
+
+The provided account is not allowed to use the handle.
+
+
+
+
diff --git a/content/accounts/contracts/erc7579.mdx b/content/accounts/contracts/erc7579.mdx
new file mode 100644
index 00000000..73f243fc
--- /dev/null
+++ b/content/accounts/contracts/erc7579.mdx
@@ -0,0 +1,290 @@
+---
+title: ERC-7579 Account Modules
+description: A developer's guide to the ERC-7579 standard for modular smart accounts
+---
+
+ERC-7579 (Minimal Modular Smart Accounts) is the standard that enables interoperability between smart accounts and modules across the ecosystem. This guide explains the key concepts you need to know when working with OpenZeppelin Accounts.
+
+## What is ERC-7579?
+
+ERC-7579 defines the minimal interfaces and behavior required for modular smart accounts and their modules to work together. The standard ensures that:
+
+- **Modules work across different account implementations**: Write a module once, use it with Kernel, Safe, or any ERC-7579 account
+- **Accounts work with any ERC-7579 module**: Install modules from different vendors without vendor lock-in
+- **SDKs and tooling are interoperable**: Build tooling that works with all ERC-7579 accounts
+
+The standard is intentionally minimal, allowing account vendors to innovate while ensuring basic interoperability.
+
+### ERC-7579 + ERC-4337
+
+ERC-7579 accounts integrate seamlessly with [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337). The following sequence illustrates how a UserOperation flows through an ERC-7579 account with validator and executor modules:
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Bundler
+ participant EntryPoint
+ participant Account
+ participant Validator
+ participant Executor
+ participant Target
+
+ User->>Bundler: Send UserOperation
+ Bundler->>EntryPoint: handleOps(UserOperation)
+
+ Note over EntryPoint: Verification Loop
+
+ EntryPoint->>Account: validateUserOp(userOp, userOpHash, missingFunds)
+ Account->>Validator: validateUserOp(userOp, userOpHash)
+ Validator-->>Account: validationData (SIG_VALIDATION_SUCCESS)
+ Account->>EntryPoint: Pay fees (if needed)
+ Account-->>EntryPoint: validationData
+
+ Note over EntryPoint: Execution Loop
+
+ EntryPoint->>Account: execute(userOp.callData)
+ alt Executor Module Used
+ Account->>Executor: execute(account, salt, mode, data)
+ Executor->>Account: executeFromExecutor(mode, executionCalldata)
+ end
+ Account->>Target: call(target, value, callData)
+ Target-->>Account: returnData
+ Account-->>EntryPoint: success
+ EntryPoint-->>Bundler: Bundle executed
+ Bundler-->>User: Transaction confirmed
+```
+
+This flow assumes both the validator and executor modules are installed already
+
+In this flow:
+
+1. **User sends UserOperation**: The user creates and signs a UserOperation, which is sent to a bundler
+2. **Bundler calls EntryPoint**: The bundler packages UserOperations and calls `EntryPoint.handleOps()`
+3. **Validation phase**: The EntryPoint calls `account.validateUserOp()`, which forwards to the installed validator module
+4. **Validator verifies signature**: The validator module checks the signature and returns validation data
+5. **Execution phase**: The EntryPoint calls `account.execute()` with the UserOperation's callData
+6. **Optional executor**: If an executor module is used, it can execute operations on behalf of the account
+7. **Target execution**: The account executes the actual operation (e.g., token transfer, contract call)
+8. **Completion**: The EntryPoint refunds excess gas and the transaction is confirmed
+
+
+Modules in ERC-7579 are not limited to use within the ERC-4337 flow.
+
+- Validators could be invoked by calling `isValidSignature` on the target account using ERC-1271
+- Executors may be called independently of the `EntryPoint` and without an `UserOperation` since they're designed to facilitate execution flows. You can call `executeFromExecutor` directly to execute from the account without involving the full ERC-4337 execution sequence.
+- Fallback Handlers would be called whenever the account receives an unrecognized selector as part of the calldata.
+
+
+
+## Core Concepts
+
+### Smart Accounts
+
+A **smart account** is a smart contract account with a modular architecture. ERC-7579 accounts can:
+
+- Install and uninstall modules dynamically
+- Execute operations through standardized interfaces
+- Support multiple execution modes (single calls, batches, delegatecalls)
+- Forward signature validation to validator modules
+
+ERC-7579 accounts implement several core interfaces that expose an interface to get relevant information from the account, perform execution and configure modules:
+
+```solidity
+interface IERC7579ModuleConfig {
+ /// @dev Installs a Module of a certain type on the smart account
+ function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external;
+
+ /// @dev Uninstalls a Module of a certain type on the smart account
+ function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external;
+
+ /// @dev Returns whether a module is installed on the smart account
+ function isModuleInstalled(
+ uint256 moduleTypeId,
+ address module,
+ bytes calldata additionalContext
+ ) external view returns (bool);
+}
+
+interface IERC7579Execution {
+ /// @dev Executes a transaction on behalf of the account.
+ function execute(bytes32 mode, bytes calldata executionCalldata) external payable;
+
+ /// @dev Executes a transaction on behalf of the account.
+ /// This function is intended to be called by Executor Modules
+ function executeFromExecutor(
+ bytes32 mode,
+ bytes calldata executionCalldata
+ ) external payable returns (bytes[] memory returnData);
+}
+
+interface IERC7579AccountConfig {
+ /// @dev Returns a unique identifier for the account implementation,
+ /// typically structured as `"vendorname.accountname.semver"` (e.g., `"openzeppelin.openaccount.1.0"`).
+ /// This helps SDKs and tooling identify the account type and version.
+ function accountId() external view returns (string memory);
+
+ /// @dev Checks whether the account supports a specific execution mode.
+ /// Use this before executing operations to ensure compatibility.
+ function supportsExecutionMode(bytes32 encodedMode) external view returns (bool);
+
+ /// @dev Checks whether the account supports installing a specific module type.
+ /// This allows you to verify module compatibility before attempting installation.
+ function supportsModule(uint256 moduleTypeId) external view returns (bool);
+}
+```
+
+### Modules
+
+A **module** is a smart contract that provides self-contained functionality for a smart account. Modules come in different types:
+
+- **Validators** (Type ID: 1): Determine if a transaction should be executed
+- **Executors** (Type ID: 2): Execute transactions on behalf of the account
+- **Fallback Handlers** (Type ID: 3): Extend the account's fallback functionality
+- **Hooks** (Type ID: 4): Execute logic before/after operations
+
+#### Module Lifecycle
+
+Every module implements the core `IERC7579Module` interface:
+
+```solidity
+interface IERC7579Module {
+ function onInstall(bytes calldata data) external;
+ function onUninstall(bytes calldata data) external;
+ function isModuleType(uint256 moduleTypeId) external view returns (bool);
+}
+```
+
+When a module is installed:
+
+1. The `installModule(moduleTypeId, module, initData)` function is called in the smart account.
+2. The account calls `module.onInstall(initData)` to initialize the module
+3. The account stores the module and its type
+
+When uninstalled, the account calls `module.onUninstall(deInitData)` to clean up.
+
+#### Building Custom Modules
+
+The library provides composable abstract modules with an internal API for developers. A good starting point is [`ERC7579Validator`](/accounts/api/modules#ERC7579Validator) or [`ERC7579Executor`](/accounts/api/modules#ERC7579Executor), which include an opinionated base layer that combines with the other abstract modules. Hooks and fallback handlers are more straightforward to implement directly from their interfaces:
+
+```solidity
+import {IERC7579Module, IERC7579Hook} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579Executor} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579Executor.sol";
+import {ERC7579Validator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579Validator.sol";
+
+// Basic validator module
+abstract contract MyValidator is ERC7579Validator {}
+
+// Basic executor module
+abstract contract MyExecutor is ERC7579Executor {}
+
+// Basic fallback handler
+abstract contract MyFallback is IERC7579Module {}
+
+// Basic hook
+abstract contract MyHook is IERC7579Hook {}
+```
+
+See the [Modules API reference](/accounts/api/modules) for the full set of abstract modules.
+
+## Execution Modes
+
+ERC-7579 uses a flexible execution mode system encoded in a `bytes32` value. The execution mode specifies:
+
+| Field | Size | Description |
+| -------------- | -------- | ----------------------------------------------------------------------------------------- |
+| `callType` | 1 byte | How to execute: `0x00` (call), `0x01` (batch), `0xfe` (staticcall), `0xff` (delegatecall) |
+| `execType` | 1 byte | Error handling: `0x00` (revert on failure), `0x01` (continue on failure) |
+| `unused` | 4 bytes | Reserved for future standardization |
+| `modeSelector` | 4 bytes | Custom mode selector for account-specific modes |
+| `modePayload` | 22 bytes | Additional data for the execution mode |
+
+### Execution Data Format
+
+The execution data passed along with the mode depends on the call type:
+
+- Single call: `abi.encodePacked(target, value, callData)`
+- Batch call: `abi.encode(Execution[])`, where each `Execution` contains `target`, `value` and `callData`
+- Delegate call: `abi.encodePacked(target, callData)`
+
+### Single Call
+
+Using OpenZeppelin's `ERC7579Utils` library:
+
+```solidity
+import {ERC7579Utils, Mode, ModeSelector, ModePayload} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
+
+using ERC7579Utils for *;
+
+// Encode the execution mode and data
+Mode mode = ERC7579Utils.encodeMode(
+ ERC7579Utils.CALLTYPE_SINGLE,
+ ERC7579Utils.EXECTYPE_DEFAULT,
+ ModeSelector.wrap(0x00000000),
+ ModePayload.wrap(0x00000000000000000000000000000000000000000000)
+);
+bytes memory executionData = ERC7579Utils.encodeSingle(target, value, callData);
+account.execute(Mode.unwrap(mode), executionData);
+```
+
+Or using the SDK:
+
+```typescript
+import { encodeMode, encodeSingle, CALL_TYPE_CALL, EXEC_TYPE_DEFAULT } from '@openzeppelin-accounts/sdk';
+
+const mode = encodeMode({
+ callType: CALL_TYPE_CALL,
+ execType: EXEC_TYPE_DEFAULT,
+});
+const executionData = encodeSingle(target, value, callData);
+await account.write.execute([mode, executionData]);
+```
+
+### Batch Call
+
+Using OpenZeppelin's `ERC7579Utils` library:
+
+```solidity
+import {ERC7579Utils, Mode, ModeSelector, ModePayload} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
+import {Execution} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+
+using ERC7579Utils for *;
+
+// Prepare the execution batch
+Execution[] memory executions = new Execution[](2);
+executions[0] = Execution(target1, value1, callData1);
+executions[1] = Execution(target2, value2, callData2);
+
+// Encode the execution mode and data
+Mode mode = ERC7579Utils.encodeMode(
+ ERC7579Utils.CALLTYPE_BATCH,
+ ERC7579Utils.EXECTYPE_DEFAULT,
+ ModeSelector.wrap(0x00000000),
+ ModePayload.wrap(0x00000000000000000000000000000000000000000000)
+);
+bytes memory executionData = ERC7579Utils.encodeBatch(executions);
+account.execute(Mode.unwrap(mode), executionData);
+```
+
+Or using the SDK:
+
+```typescript
+import { encodeMode, encodeBatch, CALL_TYPE_BATCH, EXEC_TYPE_DEFAULT } from '@openzeppelin-accounts/sdk';
+
+const mode = encodeMode({
+ callType: CALL_TYPE_BATCH,
+ execType: EXEC_TYPE_DEFAULT,
+});
+const executionData = encodeBatch(
+ { target: target1, value: value1, callData: callData1 },
+ { target: target2, value: value2, callData: callData2 },
+);
+await account.write.execute([mode, executionData]);
+```
+
+
+ The batching interface used in ERC-7579 is shared with [ERC-7802](https://eips.ethereum.org/EIP-7802), which defines a
+ standard interface for crosschain token transfers. This shared format ensures consistency across different use cases
+ and enables interoperability between standards.
+
+
+Not all accounts support all execution modes. Check `supportsExecutionMode(mode)` before executing.
diff --git a/content/accounts/contracts/erc7913.mdx b/content/accounts/contracts/erc7913.mdx
new file mode 100644
index 00000000..6f1989a0
--- /dev/null
+++ b/content/accounts/contracts/erc7913.mdx
@@ -0,0 +1,106 @@
+---
+title: ERC-7913 Signature Verifiers
+description: A developer's guide to signature verifiers for address-less keys
+---
+
+[ERC-7913](https://eips.ethereum.org/EIP-7913) extends the concept of signature verification beyond Ethereum addresses. While EOAs sign with secp256k1 and ERC-1271 contracts verify on behalf of a smart account, neither covers keys that have no Ethereum address of their own: P256 keys on a mobile device, a hardware security key, an email address. ERC-7913 introduces a unified interface so any cryptographic key type can be used as a signer without deploying a dedicated on-chain identity contract per key.
+
+## Signer Representation
+
+An ERC-7913 signer is a `bytes` object formed by concatenating a **verifier address** and an optional **key**:
+
+```
+signer = verifier (20 bytes) || key (0+ bytes)
+```
+
+| Signer length | Interpretation |
+| ------------- | --------------------------------------- |
+| < 20 bytes | Invalid |
+| = 20 bytes | `key` is empty β EOA or ERC-1271 contract at `verifier` |
+| > 20 bytes | `key` is non-empty β ERC-7913 verifier |
+
+This encoding is backward-compatible: a plain EOA address is a valid 20-byte signer (key empty, `verifier` is the EOA address itself).
+
+## Verification Algorithm
+
+Given a `signer`, a `hash`, and a `signature`, verification resolves as follows:
+
+```mermaid
+flowchart TD
+ A["signer (bytes)"] --> B{"signer.length < 20?"}
+ B -->|Yes| FAIL["β Invalid"]
+ B -->|No| C["split: verifier = signer[0:20]\n key = signer[20:]"]
+ C --> D{"key empty?"}
+
+ D -->|"Yes (EOA or ERC-1271)"| E{"code at verifier?"}
+ E -->|No| F["ecrecover\nstandard ECDSA"]
+ E -->|Yes| G["ERC-1271\nisValidSignature"]
+
+ D -->|"No (ERC-7913)"| H["IERC7913SignatureVerifier(verifier)\n .verify(key, hash, signature)"]
+
+ H --> I{"returns 0x024ad318?"}
+ I -->|Yes| OK["β Valid"]
+ I -->|No / revert| FAIL2["β Invalid"]
+
+ F --> OK
+ F --> FAIL2
+ G --> OK
+ G --> FAIL2
+```
+
+The ERC-7913 path calls a shared, stateless **verifier contract** that understands the encoding of a particular key type. Any number of keys of the same type share one verifier: no per-key deployment required.
+
+## Verifier Interface
+
+A verifier contract must implement:
+
+```solidity
+interface IERC7913SignatureVerifier {
+ /**
+ * @dev Verifies `signature` as a valid signature of `hash` by `key`.
+ *
+ * MUST return 0x024ad318 (verify.selector) if valid.
+ * SHOULD return 0xffffffff or revert otherwise.
+ */
+ function verify(
+ bytes calldata key,
+ bytes32 hash,
+ bytes calldata signature
+ ) external view returns (bytes4);
+}
+```
+
+Verifiers **should be stateless**, meaning that all parameters relevant to verification must be embedded in `key` or compiled into the verifier contract as immutables. This keeps verifiers permissionless, trustless, and compatible with the [ERC-7562](https://eips.ethereum.org/EIP-7562) storage access rules used in ERC-4337 validation.
+
+## Signer Types
+
+The table below shows how different key types are represented using the `verifier || key` encoding:
+
+| Key type | `verifier` | `key` |
+| ----------- | ----------------------------------- | ---------------------------- |
+| EOA | EOA address | empty |
+| ERC-1271 | contract address | empty |
+| P256 | `P256Verifier` contract | 64-byte `(qx, qy)` |
+| WebAuthn | `WebAuthnVerifier` contract | 64-byte `(qx, qy)` |
+| ZKEmail | `ZKEmailVerifier` contract | email-specific key bytes |
+| RSA | `RSAVerifier` contract | DER-encoded public key |
+
+OpenZeppelin Contracts ships a [`P256`](/contracts/5.x/api/utils/cryptography#P256) library and a [`WebAuthn`](/contracts/5.x/api/utils/cryptography#WebAuthn) library that can serve as the core of verifier contracts for those key types.
+
+## In OpenZeppelin Accounts
+
+ERC-7913 is leveraged by the [multisig base modules](/accounts/contracts/multisig) (`ERC7579Multisig`, `ERC7579MultisigWeighted`) to support heterogeneous signer sets. A single multisig can hold a mix of EOAs, P256 hardware keys, WebAuthn credentials, and ZKEmail addresses:
+
+```solidity
+bytes[] memory signers = new bytes[](3);
+signers[0] = abi.encodePacked(alice); // EOA
+signers[1] = abi.encodePacked(p256Verifier, qx, qy); // P256
+signers[2] = abi.encodePacked(zkEmailVerifier, emailKeyBytes); // ZKEmail
+```
+
+The `ERC7579SocialRecoveryExecutor` and the multisig validators all consume signers in this format. See [Base Modules](/accounts/contracts/multisig) and [Validators](/accounts/contracts/validators) for usage details.
+
+## Security Considerations
+
+- **Verifiers must be trustless**: because signers may be configured years before they are used (e.g., social recovery guardians), a verifier that is upgradeable or depends on mutable state could be compromised. Verifiers should be immutable contracts.
+- **Verifiers should be stateless**: the `verify` function should behave like a `pure` function. All verification parameters come from `key` or hardcoded logic. This also ensures compliance with ERC-4337's storage access restrictions during UserOperation validation.
diff --git a/content/accounts/contracts/erc7964.mdx b/content/accounts/contracts/erc7964.mdx
new file mode 100644
index 00000000..e3cd8d53
--- /dev/null
+++ b/content/accounts/contracts/erc7964.mdx
@@ -0,0 +1,313 @@
+---
+title: ERC-7964 Crosschain Signatures
+description: Crosschain signature verification for ERC-7964 compliant signatures
+---
+
+import { Step, Steps } from 'fumadocs-ui/components/steps';
+
+[ERC-7964](https://eips.ethereum.org/EIPS/eip-7964) enables a single EIP-712 signature to be valid across multiple chains simultaneously. Rather than asking users to sign one message per chain, it omits `chainId` from the top-level domain and encodes chain-specific operations as an array of nested structs (one per chain), so wallets display all authorized operations in a single, fully human-readable signing prompt.
+
+## Background
+
+Interacting with a chain requires the signature of a private key to validate the account's identity. Natively, the EVM supports ECDSA signature validation for EOAs, while smart accounts often validate signatures using [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) to provide a `hash` and a `bytes` buffer representing the signature to be validated.
+
+Smart contracts are able to execute functionality on behalf of the signing account by validating the signature. In fact, this approach is commonly used by applications to enable operations without requiring the signing account to execute a transaction.
+
+In practice, signing a simple Ethereum message is not enough for more complex applications that require structured data to be represented by the signature while still being readable for the end user. To solve this, [EIP-712](https://eips.ethereum.org/EIPS/eip-712) standardizes signing messages whose content is a struct of values that can be used by the EVM. Each message is bound to a `domainSeparator` hash by design, which includes the `verifyingContract` and a `chainId` to avoid replayability attacks (i.e., the same signature used in different chains or contracts).
+
+### Selective Replayability
+
+As a consequence of the EIP-712 domain, to coordinate signatures across different chains applications must decide whether they ask users to sign one EIP-712 message per chain to preserve readability (degrading UX), or request a single signature with a different encoding scheme, sacrificing readability and introducing trust assumptions on the UI that requests the signature.
+
+Ideally, users should sign a single human-readable message that is selectively replayable in certain domains to preserve security. Fortunately, EIP-712 has the following properties:
+
+- All domain fields are optional and can be omitted at convenience, enabling developers to create signatures that are not bound by either `chainId` or `verifyingContract` and can be reused in a different chain.
+- Nested structs are supported
+
+### How ERC-7964 Works
+
+ERC-7964 omits `chainId` and `verifyingContract` from the outer domain and instead encodes them inside a `ChainOperation` struct that also carries the chain-specific parameters:
+
+```solidity
+ChainOperation(EIP712ChainDomain domain,...)
+EIP712ChainDomain(uint256 chainId,address verifyingContract)
+```
+
+Per EIP-712, arrays are encoded as `keccak256(encodeData(element[0]) β encodeData(element[1]) β ... β encodeData(element[n]))` and structs are hashed as `hashStruct(s : π) = keccak256(typeHash β encodeData(s))`. This means that an array of structs would be encoded as `keccak256(hashStruct(s0) β hashStruct(s1) β ... β hashStruct(sn))`. Therefore, an array of structs that represent chain-specific operations would look like the following:
+
+```solidity
+// Each operation is hashed individually
+operationHashes = [
+ keccak256(abi.encode(CHAIN_OPERATION_TYPEHASH, chainDomainHash0, ...)),
+ keccak256(abi.encode(CHAIN_OPERATION_TYPEHASH, chainDomainHash1, ...)),
+ keccak256(abi.encode(CHAIN_OPERATION_TYPEHASH, chainDomainHash2, ...)),
+ // ...
+]
+
+// The array hash is computed by concatenating all operation hashes
+operationsArrayHash = keccak256(abi.encodePacked(operationHashes))
+
+// The main struct hash uses the array hash
+mainStructHash = keccak256(abi.encode(CROSS_CHAIN_OPERATION_TYPEHASH, operationsArrayHash))
+```
+
+Smart contracts that support this signature verification scheme request the user to sign an EIP-712 type that includes all the operations that the signature enables without requiring special wallet support. During verification, the smart contract receives only the operation relevant to it and the array of operation hashes (i.e., not the full list of structs) to reconstruct the final signed hash.
+
+While several solutions exist for enabling crosschain signatures, ERC-7964 provides **full transparency**. Users see all chain operations in any EIP-712 compatible wallet without custom support, prioritizing readability, security and widespread wallet compatibility.
+
+## Signature Encoding and Verification
+
+Smart contracts must be able to include metadata to reconstruct the final type hash and detect when a signature is intended for crosschain validation. To solve this, ERC-7964 defines a standard encoding that packs all metadata into the leading 32 bytes of the signature:
+
+```solidity
+bytes9 ERC7964_MAGIC = 0x796479647964796479; // bytes 0β8
+bytes1 fields = 0x03; // byte 9 (ERC-5267 domain field flags)
+uint16 structIndex = <...>; // bytes 10β11 (index of this chain's operation)
+address application = 0x...; // bytes 12β31 (ERC-5267 provider for the domain)
+
+bytes32 header = bytes32(abi.encodePacked(ERC7964_MAGIC, fields, structIndex, application));
+
+bytes memory erc7964Signature = abi.encode(header, structsArray, actualSignature);
+```
+
+The `fields` byte follows [ERC-5267](https://eips.ethereum.org/EIPS/eip-5267): each bit signals which domain fields (`name`, `version`, `chainId`, `verifyingContract`, `salt`) the domain separator includes. The `application` address is queried at verification time via `IERC5267.eip712Domain()` to reconstruct the chain-specific domain separator on the fly.
+
+The `CrossChainSignatureChecker` library provides utilities for parsing and verifying ERC-7964 signatures in your contracts. The full verification sequence is:
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Contract
+ participant CrossChainSignatureChecker
+ participant Application
+
+ Caller->>Contract: setValue(signer, value, erc7964Signature)
+ Contract->>CrossChainSignatureChecker: isValidCrossChainSignatureNowCalldata(signer, currentOpHash, sig, structHashFn)
+ CrossChainSignatureChecker->>CrossChainSignatureChecker: parse header (magic, fields, structIndex, application)
+ CrossChainSignatureChecker->>CrossChainSignatureChecker: assert structsArray[structIndex] == currentOpHash
+ CrossChainSignatureChecker->>Application: eip712Domain() [ERC-5267]
+ Application-->>CrossChainSignatureChecker: name, version, chainId, verifyingContract, ...
+ CrossChainSignatureChecker->>CrossChainSignatureChecker: domainSeparator = toDomainSeparator(fields, ...)
+ CrossChainSignatureChecker->>CrossChainSignatureChecker: structHash = structHashFn(keccak256(structsArray))
+ CrossChainSignatureChecker->>CrossChainSignatureChecker: digest = toTypedDataHash(domainSeparator, structHash)
+ CrossChainSignatureChecker->>CrossChainSignatureChecker: isValidSignatureNow(signer, digest, actualSig)
+ CrossChainSignatureChecker-->>Contract: bool
+ Contract-->>Caller: success or revert
+```
+
+## Writing Crosschain Applications
+
+Smart contracts can support ERC-7964 signatures using the `CrossChainSignatureChecker` library. Here's how to implement an application that accepts crosschain signatures:
+
+
+
+### Define Your Type Hashes
+
+First, define the type hashes for your message structures. You'll need:
+
+1. A type hash for the chain-specific domain (including `chainId`)
+2. A type hash for each chain operation struct
+3. A type hash for the main crosschain struct (wrapping the operations array)
+
+```solidity
+import { EIP712 } from '@openzeppelin/contracts/utils/cryptography/EIP712.sol';
+import { CrossChainSignatureChecker } from '@openzeppelin-accounts/contracts/utils/CrossChainSignatureChecker.sol';
+
+contract CrossChainApp is EIP712 {
+ using CrossChainSignatureChecker for address;
+
+ // Chain-specific domain type hash (includes chainId)
+ bytes32 public constant EIP712_CHAIN_DOMAIN_TYPEHASH =
+ keccak256('EIP712ChainDomain(uint256 chainId,address verifyingContract)');
+
+ // Chain operation type hash (includes nested EIP712ChainDomain type)
+ bytes32 public constant CHAIN_OPERATION_TYPEHASH =
+ keccak256(
+ 'ChainOperation(EIP712ChainDomain domain,uint256 value)EIP712ChainDomain(uint256 chainId,address verifyingContract)'
+ );
+
+ // Main crosschain struct type hash (includes all nested types)
+ bytes32 public constant CROSS_CHAIN_SET_VALUE_TYPEHASH =
+ keccak256(
+ 'SetValue(ChainOperation[] operations)ChainOperation(EIP712ChainDomain domain,uint256 value)EIP712ChainDomain(uint256 chainId,address verifyingContract)'
+ );
+
+ constructor(string memory name, string memory version) EIP712(name, version) {}
+}
+```
+
+
+
+
+### Implement Struct Hash Functions
+
+Implement functions to compute the struct hash for the current chain's operation and the main crosschain struct:
+
+```solidity
+function _structHash(uint256 value_) internal view returns (bytes32) {
+ bytes32 chainDomainHash = keccak256(abi.encode(EIP712_CHAIN_DOMAIN_TYPEHASH, block.chainid, address(this)));
+ return keccak256(abi.encode(CHAIN_OPERATION_TYPEHASH, chainDomainHash, value_));
+}
+
+function _crossChainStructHash(bytes32 operationsHash) internal pure returns (bytes32) {
+ return keccak256(abi.encode(CROSS_CHAIN_SET_VALUE_TYPEHASH, operationsHash));
+}
+```
+
+The `_structHash` function computes the hash of the current chain's operation, including the `chainId` and `verifyingContract` in the chain domain. The `_crossChainStructHash` function computes the hash of the main crosschain struct, which wraps the array of operations.
+
+
+
+
+### Verify Signatures
+
+Use `CrossChainSignatureChecker` to verify signatures. The library provides two variants: one that takes `bytes memory` and one that takes `bytes calldata` (more gas-efficient):
+
+```solidity
+function setValue(address account, uint256 value_, bytes calldata erc7964Signature) public {
+ bytes32 currentOpHash = _structHash(value_);
+
+ require(
+ account.isValidCrossChainSignatureNowCalldata(currentOpHash, erc7964Signature, _crossChainStructHash),
+ 'Invalid signature'
+ );
+
+ // Execute the operation
+ _setValue(account, value_);
+}
+```
+
+The `isValidCrossChainSignatureNowCalldata` function:
+
+1. Parses the ERC-7964 signature encoding to extract metadata (struct index, application address, array of struct hashes)
+2. Verifies that the provided `currentOpHash` matches the struct at the correct index in the array
+3. Queries the application contract (via ERC-5267) for the EIP-712 domain information
+4. Reconstructs the full typed hash using the domain and the array of struct hashes
+5. Validates the signature against the reconstructed hash
+
+
+
+
+
+## Complete Example
+
+Here's a complete example that puts it all together:
+
+```solidity
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import { EIP712 } from '@openzeppelin/contracts/utils/cryptography/EIP712.sol';
+import { CrossChainSignatureChecker } from '@openzeppelin-accounts/contracts/utils/CrossChainSignatureChecker.sol';
+
+contract CrossChainApp is EIP712 {
+ using CrossChainSignatureChecker for address;
+
+ bytes32 public constant EIP712_CHAIN_DOMAIN_TYPEHASH =
+ keccak256('EIP712ChainDomain(uint256 chainId,address verifyingContract)');
+ bytes32 public constant CHAIN_OPERATION_TYPEHASH =
+ keccak256(
+ 'ChainOperation(EIP712ChainDomain domain,uint256 value)EIP712ChainDomain(uint256 chainId,address verifyingContract)'
+ );
+ bytes32 public constant CROSS_CHAIN_SET_VALUE_TYPEHASH =
+ keccak256(
+ 'SetValue(ChainOperation[] operations)ChainOperation(EIP712ChainDomain domain,uint256 value)EIP712ChainDomain(uint256 chainId,address verifyingContract)'
+ );
+
+ mapping(address account => uint256) private _values;
+
+ constructor(string memory name, string memory version) EIP712(name, version) {}
+
+ function value(address account) public view returns (uint256) {
+ return _values[account];
+ }
+
+ function setValue(address account, uint256 value_, bytes calldata erc7964Signature) public {
+ bytes32 currentOpHash = _structHash(value_);
+
+ require(
+ account.isValidCrossChainSignatureNowCalldata(currentOpHash, erc7964Signature, _crossChainStructHash),
+ 'Invalid signature'
+ );
+
+ _values[account] = value_;
+ }
+
+ function _crossChainStructHash(bytes32 operationsHash) internal pure returns (bytes32) {
+ return keccak256(abi.encode(CROSS_CHAIN_SET_VALUE_TYPEHASH, operationsHash));
+ }
+
+ function _structHash(uint256 value_) internal view returns (bytes32) {
+ bytes32 chainDomainHash = keccak256(abi.encode(EIP712_CHAIN_DOMAIN_TYPEHASH, block.chainid, address(this)));
+ return keccak256(abi.encode(CHAIN_OPERATION_TYPEHASH, chainDomainHash, value_));
+ }
+}
+```
+
+OpenZeppelin's EIP712 contract already implements ERC-5267, so it can be passed directly as the `application` address in the signature header.
+
+## Message Structure for Users
+
+When users sign crosschain messages, the EIP-712 structure should look like this:
+
+```javascript
+{
+ types: {
+ EIP712Domain: [
+ { name: "name", type: "string" },
+ { name: "version", type: "string" }
+ // chainId is omitted for crosschain validity
+ ],
+ SetValue: [
+ { name: "operations", type: "ChainOperation[]" }
+ ],
+ ChainOperation: [
+ { name: "domain", type: "EIP712ChainDomain" },
+ { name: "value", type: "uint256" }
+ ],
+ EIP712ChainDomain: [
+ { name: "chainId", type: "uint256" },
+ { name: "verifyingContract", type: "address" }
+ ]
+ },
+ primaryType: "SetValue",
+ domain: {
+ name: "CrossChainApp",
+ version: "1"
+ },
+ message: {
+ operations: [
+ {
+ domain: { chainId: 1, verifyingContract: "0x..." },
+ value: 42
+ },
+ {
+ domain: { chainId: 42161, verifyingContract: "0x..." },
+ value: 42
+ }
+ ]
+ }
+}
+```
+
+The operations array encodes to a single `bytes32` hash per EIP-712, and users can see all operations in their wallet. Each chain validates only its relevant operation from the signature.
+
+## In OpenZeppelin Accounts
+
+`CrossChainSignatureChecker` is a standalone library that any contract can import. It is used by the included ERC-7579 validators to support ERC-7964 signatures natively, so smart account users can authorize operations on multiple chains with a single signature through their installed validator.
+
+See [Validators](/accounts/contracts/validators) for usage details.
+
+## Security Considerations
+
+### Signature Replay
+
+ERC-7964 intentionally enables replay across chains, the same signature is designed to be used on multiple chains. The `verifyingContract` field inside each `ChainOperation` binds that operation to a specific contract address, but the outer signature itself has no chain binding. Applications must implement their own replay protection mechanisms such as including nonces and deadlines in the chain-specific operation struct.
+
+### Partial Execution
+
+Crosschain signatures do not guarantee atomic execution across chains. A signature may be successfully validated on some chains but not others, resulting in partial fulfillment. Applications that require atomicity should implement complementary mechanisms such as refund paths or escrow patterns to allow users to recover from failed partial executions.
+
+### Verifier Trust
+
+The `application` address embedded in the signature header is queried at verification time via `IERC5267.eip712Domain()` to reconstruct the domain separator. If the contract at that address is upgradeable or mutable, it could return a different domain on a later call, potentially allowing a signature to verify against an unintended domain. Prefer immutable contracts as the `application` address, or validate that the returned domain matches the expected values before accepting the signature.
diff --git a/content/accounts/contracts/erc8043.mdx b/content/accounts/contracts/erc8043.mdx
new file mode 100644
index 00000000..973d4315
--- /dev/null
+++ b/content/accounts/contracts/erc8043.mdx
@@ -0,0 +1,309 @@
+---
+title: ERC-8043 Init Mode
+description: A developer's guide to atomic module installation and execution in a single user operation
+---
+
+import { Step, Steps } from 'fumadocs-ui/components/steps';
+
+[ERC-8043](https://eips.ethereum.org/EIP-8043) (Atomic ERC-7579 Module Install and Execute) solves a common friction point in modular smart accounts: having to send two separate user operations: one to install a module and another to use it. Init mode collapses both into one by piggybacking on the `initCode` field of an ERC-4337 user operation.
+
+## How It Works
+
+[ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) EntryPoint v0.9 introduced a subtle but powerful behavior: when `initCode` is non-empty but the sender account is **already deployed**, the EntryPoint silently skips the factory call and emits an `IgnoredInitCode` event. Crucially, `initCode` is still included in the `userOpHash` computation, so its contents are cryptographically committed to by the user operation signature.
+
+ERC-8043 exploits this: a 4-byte magic value at the start of `initCode` signals the account to enter _init mode_ during validation, decode the payload, verify the installation authorization, install the modules, and then continue with normal user operation validation.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Bundler
+ participant EntryPoint
+ participant Account
+ participant Validator (new)
+ participant Target
+
+ User->>Bundler: UserOperation (initCode = MAGIC ++ payload)
+ Bundler->>EntryPoint: handleOps(userOps)
+
+ Note over EntryPoint: initCode non-empty but account exists β IgnoredInitCode
+
+ EntryPoint->>Account: validateUserOp(userOp, userOpHash)
+ Note over Account: initCode[0:4] == 0x80438043 β init mode
+
+ Account->>Account: verify installationSignature (EIP-712)
+ Account->>Account: installModule(newValidator)
+ Account->>Validator (new): validateUserOp(userOp, userOpHash)
+ Validator (new)-->>Account: SIG_VALIDATION_SUCCESS
+ Account-->>EntryPoint: validationData
+
+ EntryPoint->>Account: execute(callData)
+ Account->>Target: call(...)
+ Target-->>Account: returnData
+```
+
+In this flow:
+
+1. The user bundles module installation data into `initCode` alongside the 4-byte magic value
+2. The EntryPoint ignores `initCode` (account already exists) but keeps it in the hash
+3. The account detects the magic, decodes the payload, and validates the EIP-712 `installationSignature`
+4. Modules are installed, and the user operation proceeds with normal validation through the newly installed module
+
+## The Magic Value
+
+Init mode is triggered when the first 4 bytes of `userOp.initCode` equal:
+
+```solidity
+bytes4 constant INIT_MODE_MAGIC_VALUE = 0x80438043;
+```
+
+The value `0x8043` embeds the ERC number for self-documentation. A 4-byte prefix is sufficient because a valid init mode `initCode` payload is always far longer than 20 bytesβ: he minimum ABI-encoded `(uint256[], address[], bytes[], bytes)` is 256 bytes (four 32-byte head offsets plus four 32-byte length words), so there is no ambiguity with a real factory address.
+
+
+ The EntryPoint v0.9 only ignores `initCode` for **already-deployed** accounts. Bundlers will reject a user operation
+ with non-empty `initCode` for an undeployed account if the factory address is not a valid factory. ERC-8043 applies
+ exclusively to accounts that are already deployed.
+
+
+## `initCode` Encoding Format
+
+The bytes after the magic value (`userOp.initCode[4:]`) are ABI-encoded as:
+
+```solidity
+abi.encode(
+ uint256[] moduleTypeIds,
+ address[] modules,
+ bytes[] initDatas,
+ bytes installationSignature
+)
+```
+
+| Field | Type | Description |
+| ----------------------- | ----------- | ----------------------------------------------------------- |
+| `moduleTypeIds` | `uint256[]` | ERC-7579 module type identifier for each module |
+| `modules` | `address[]` | Address of each module to install |
+| `initDatas` | `bytes[]` | Initialization data passed to each module's `onInstall` |
+| `installationSignature` | `bytes` | EIP-712 signature authorizing the installations (see below) |
+
+All three arrays must have the same length. Passing an empty `installationSignature` skips both signature verification and module installation, allowing the user operation to proceed with normal validation only.
+
+## EIP-712 Installation Authorization
+
+The `installationSignature` is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) signature over the following typed struct, using the **account's own domain separator**:
+
+```solidity
+bytes32 constant INIT_MODULES_TYPEHASH = keccak256(
+ 'InitModules(uint256[] moduleTypeIds,address[] modules,bytes[] initDatas,uint256 nonce)'
+);
+```
+
+The struct hash is computed as:
+
+```solidity
+bytes32 structHash = keccak256(abi.encode(
+ INIT_MODULES_TYPEHASH,
+ keccak256(abi.encodePacked(moduleTypeIds)),
+ keccak256(abi.encodePacked(modules)),
+ keccak256(abi.encodePacked(initDataHashes)), // element-by-element keccak256 of each bytes
+ userOp.nonce
+));
+```
+
+The account verifies it using its own `isValidSignature`, which routes the signature to the currently installed validator:
+
+```solidity
+isValidSignature(_hashTypedDataV4(structHash), installationSignature)
+```
+
+### Why EIP-712?
+
+EIP-712 lets hardware wallets and browser signers display exactly which modules at which addresses are being authorized, with what initialization data, and at which nonce. This separates the owner's **installation authorization** (what modules are being granted access to this account) from the **execution authorization** (who signs the user operation itself, e.g. a session key that is being installed in the same operation).
+
+### Replay Protection
+
+The `userOp.nonce` is included in the `InitModules` struct. Once the user operation is executed and its nonce consumed, the same `installationSignature` cannot be replayed. The EIP-712 domain separator binds the signature to the specific account address and chain ID.
+
+## Validation Flow
+
+The full init mode validation flow implemented by `AccountERC8043Upgradeable`:
+
+
+
+
+**Detect init mode**
+
+Check whether `userOp.initCode` starts with the magic value. If not, fall through to normal `_validateUserOp`.
+
+```solidity
+if (userOp.initCode.length >= 4 && bytes4(userOp.initCode[:4]) == ERC8043Utils.INIT_MODE_MAGIC_VALUE) {
+ // ...decode, verify, and install (steps below)...
+}
+return super._validateUserOp(userOp, userOpHash, signature);
+```
+
+
+
+
+**Decode the payload**
+
+Decode `(moduleTypeIds, modules, initDatas, installationSignature)` from `userOp.initCode[4:]`. Return `SIG_VALIDATION_FAILED` on any decoding error or array length mismatch.
+
+
+
+
+**Verify the installation signature**
+
+Compute the EIP-712 struct hash and validate `installationSignature` via `isValidSignature`. If the signature is empty, skip this step and proceed without installing any modules.
+
+```solidity
+bytes32 structHash = ERC8043Utils.initModulesStructHash(moduleTypeIds, modules, initDatas, userOp.nonce);
+if (isValidSignature(_hashTypedDataV4(structHash), installationSignature) != IERC1271.isValidSignature.selector) {
+ return ERC4337Utils.SIG_VALIDATION_FAILED;
+}
+```
+
+
+
+
+**Install modules**
+
+Call `_installModule` for each `(moduleTypeId, module, initData)` tuple.
+
+```solidity
+for (uint256 i = 0; i < modules.length; i++) {
+ _installModule(moduleTypeIds[i], modules[i], initDatas[i]);
+}
+```
+
+
+
+
+**Delegate to normal validation**
+
+Continue with `super._validateUserOp` using the forwarded `signature`. This validates the user operation itself through the appropriate installed validator (e.g. the one just installed).
+
+```solidity
+return super._validateUserOp(userOp, userOpHash, signature);
+```
+
+
+
+
+
+## Using `AccountERC8043Upgradeable`
+
+`AccountERC8043Upgradeable` is an abstract extension of `AccountERC7579Upgradeable`. It is already included in `OpenAccountImplementation`, the contract deployed by [`OpenAccountFactory`](/accounts/contracts#using-openaccountfactory), so every account created through the factory supports init mode out of the box with no extra setup.
+
+### Building an Init Mode User Operation
+
+The steps below show how to build a user operation that installs a new validator and uses it in the same operation.
+
+
+
+
+**Compute the EIP-712 installation signature**
+
+```solidity
+bytes32[] memory initDataHashes = new bytes32[](1);
+initDataHashes[0] = keccak256(initData);
+
+bytes32 structHash = keccak256(abi.encode(
+ INIT_MODULES_TYPEHASH,
+ keccak256(abi.encodePacked(moduleTypeIds)),
+ keccak256(abi.encodePacked(modules)),
+ keccak256(abi.encodePacked(initDataHashes)),
+ nonce
+));
+
+bytes32 digest = MessageHashUtils.toTypedDataHash(accountDomainSeparator, structHash);
+(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
+
+// Prefix with the validator module address for ERC-7579 isValidSignature routing
+bytes memory installationSignature = abi.encodePacked(address(currentValidator), r, s, v);
+```
+
+
+
+
+**Encode `initCode`**
+
+```solidity
+uint256[] memory moduleTypeIds = new uint256[](1);
+moduleTypeIds[0] = MODULE_TYPE_VALIDATOR;
+address[] memory modules = new address[](1);
+modules[0] = address(newValidator);
+bytes[] memory initDatas = new bytes[](1);
+initDatas[0] = abi.encodePacked(newValidatorSigner);
+
+bytes memory initCode = abi.encodePacked(
+ bytes4(0x80438043), // INIT_MODE_MAGIC_VALUE
+ abi.encode(moduleTypeIds, modules, initDatas, installationSignature)
+);
+```
+
+
+
+
+**Build and sign the user operation**
+
+Route the nonce through the newly installed validator (upper 160 bits = validator address) and sign `userOpHash` with the new validator's key:
+
+```solidity
+uint256 nonce = uint256(uint160(address(newValidator))) << 96;
+
+PackedUserOperation memory userOp = PackedUserOperation({
+ sender: address(account),
+ nonce: nonce,
+ initCode: initCode,
+ // ... callData, gas fields, etc.
+ signature: bytes("") // filled in after hashing
+});
+
+bytes32 userOpHash = entryPoint.getUserOpHash(userOp);
+(uint8 v, bytes32 r, bytes32 s) = vm.sign(newValidatorPrivateKey, userOpHash);
+userOp.signature = abi.encodePacked(r, s, v);
+```
+
+
+
+
+**Submit**
+
+```solidity
+PackedUserOperation[] memory ops = new PackedUserOperation[](1);
+ops[0] = userOp;
+entryPoint.handleOps(ops, payable(beneficiary));
+```
+
+After execution, the new module is installed and the call in `callData` has been executed, all in one user operation.
+
+
+
+
+
+### Installing Multiple Modules
+
+Init mode supports installing any number of modules atomically. Pass parallel arrays with one entry per module:
+
+```solidity
+uint256[] memory moduleTypeIds = new uint256[](3);
+moduleTypeIds[0] = MODULE_TYPE_VALIDATOR;
+moduleTypeIds[1] = MODULE_TYPE_VALIDATOR;
+moduleTypeIds[2] = MODULE_TYPE_EXECUTOR;
+
+address[] memory modules = new address[](3);
+bytes[] memory initDatas = new bytes[](3);
+// ... populate each entry ...
+
+bytes memory initCode = abi.encodePacked(
+ bytes4(0x80438043),
+ abi.encode(moduleTypeIds, modules, initDatas, installationSignature)
+);
+```
+
+Modules are installed in array order. If any installation reverts, the entire user operation fails.
+
+## Security Considerations
+
+- **EntryPoint version**: Init mode requires EntryPoint **v0.9** (`0x433709009B8330FDa32311DF1C2AFA402eD8D009`). On earlier versions, non-empty `initCode` for an already-deployed account is rejected by bundlers.
+- **`IgnoredInitCode` event**: The v0.9 EntryPoint emits `IgnoredInitCode` for every init mode user operation. Monitoring tools should not treat this event as an anomaly.
+- **Empty `installationSignature`**: Passing an empty signature skips module installation entirely. The user operation still proceeds with normal validation. This is useful for no-op init mode triggers but does not install any modules.
diff --git a/content/accounts/contracts/executors/index.mdx b/content/accounts/contracts/executors/index.mdx
new file mode 100644
index 00000000..d1a7d50d
--- /dev/null
+++ b/content/accounts/contracts/executors/index.mdx
@@ -0,0 +1,468 @@
+---
+title: Executors
+description: Execution modules for ERC-7579 accounts
+---
+
+Executor modules enable accounts to execute operations through a callback mechanism. Unlike direct execution, executors call `executeFromExecutor` on the account, allowing for additional validation, scheduling, and access control logic.
+
+## Base Executor
+
+**`ERC7579Executor`** is the abstract base contract for all executor modules. It provides:
+
+- Core execution functionality via the internal `_execute` function
+- Integration with ERC-7579's `executeFromExecutor` interface
+- Full flexibility for implementing custom validation and access control
+
+Developers extending this base contract must implement:
+
+- `onInstall(bytes calldata data)`: Initialize the executor with account-specific configuration
+- `onUninstall(bytes calldata data)`: Clean up executor state
+- A public `execute` function (or similar) that implements validation logic and calls `_execute`
+
+## Available Executors
+
+OpenZeppelin Accounts provides the following executor implementations:
+
+### ERC7579DelayedExecutor
+
+An executor base contract that provides scheduling infrastructure for time-delayed execution with expiration. Operations are scheduled and must wait for a delay period before becoming executable. This provides a security window to cancel suspicious operations.
+
+Developers must implement their own `schedule`, `cancel`, and `execute` functions with appropriate validation and access control logic, then call the internal `_schedule`, `_cancel`, and `_execute` functions.
+
+**Key Features:**
+
+- Internal functions for scheduling (`_schedule`, `_scheduleAt`) and cancellation (`_cancel`)
+- Time-delayed execution with configurable delay periods
+- Expiration mechanism (operations expire if not executed in time)
+- Security window for canceling operations
+- Operation state tracking (Unknown, Scheduled, Ready, Expired, Executed, Canceled)
+- Full flexibility for custom validation and access control
+
+**Operation Lifecycle:**
+
+```mermaid
+stateDiagram-v2
+ [*] --> Unknown: Operation not scheduled
+ Unknown --> Scheduled: _schedule()
+ Scheduled --> Ready: Delay period elapses
+ Scheduled --> Canceled: _cancel()
+ Ready --> Executed: _execute()
+ Ready --> Expired: Expiration period elapses
+ Ready --> Canceled: _cancel()
+ Executed --> [*]
+ Expired --> [*]
+ Canceled --> [*]
+
+ note right of Scheduled
+ Security window:
+ Operations can be
+ canceled but not executed
+ end note
+
+ note right of Ready
+ Executable window:
+ Operations can be
+ executed or canceled
+ end note
+```
+
+
+This is an abstract contract. You need to create a concrete implementation:
+
+```solidity
+contract MyDelayedExecutor is ERC7579DelayedExecutor {
+ function schedule(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public {
+ // Implement authorization logic for scheduling
+ // e.g., require(msg.sender == account, "unauthorized");
+
+ // Call the internal _schedule function
+ _schedule(account, salt, mode, data);
+ }
+
+ function cancel(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public {
+ // Implement authorization logic for canceling
+ // e.g., require(msg.sender == account, "unauthorized");
+
+ // Call the internal _cancel function
+ _cancel(account, salt, mode, data);
+ }
+
+ function execute(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public returns (bytes[] memory) {
+ // Optionally implement authorization logic for execution
+ // Note: _execute already checks that the operation is Ready
+
+ // Call the internal _execute function
+ return _execute(account, salt, mode, data);
+ }
+}
+```
+
+
+
+To install an ERC7579DelayedExecutor:
+
+```solidity
+import {MODULE_TYPE_EXECUTOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579DelayedExecutor} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579DelayedExecutor.sol";
+
+// Create a concrete dummy implementation
+// WARNING: Do not use in production since it doesn't implement access control
+contract MyDelayedExecutor is ERC7579DelayedExecutor {
+ function schedule(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public {
+ // No access control for educational purposes.
+ _schedule(account, salt, mode, data);
+ }
+
+ function cancel(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public {
+ // No access control for educational purposes.
+ _cancel(account, salt, mode, data);
+ }
+
+ function execute(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public returns (bytes[] memory) {
+ // No access control for educational purposes.
+ return _execute(account, salt, mode, data);
+ }
+}
+
+// Deploy the executor
+MyDelayedExecutor executor = new MyDelayedExecutor();
+
+// Prepare initialization data: delay (4 bytes) + expiration (4 bytes)
+uint32 delay = 7 days; // Operations must wait 7 days before execution
+uint32 expiration = 30 days; // Operations expire 30 days after becoming ready
+bytes memory initData = abi.encodePacked(delay, expiration);
+
+// Install the module
+account.installModule(MODULE_TYPE_EXECUTOR, address(executor), initData);
+```
+
+To schedule a delayed operation, provide a unique `salt`, the `mode`, and the `executionData`. Scheduling registers the operation and enforces the configured delay before it can be executed.
+
+```solidity
+// Generate a salt (unique identifier for this operation)
+bytes32 salt = keccak256("recovery-operation");
+bytes32 mode = /* ERC-7579 execution mode */;
+bytes calldata executionData = /* calldata to execute the operation */;
+
+// Schedule the operation
+executor.schedule(address(account), salt, mode, executionData);
+```
+
+You can check the current state of the scheduled operation and retrieve timestamps:
+
+```solidity
+// Query the state of the operation
+ERC7579DelayedExecutor.OperationState operationState =
+ executor.state(address(account), salt, mode, executionData);
+// State will be one of: Unknown, Scheduled, Ready, Expired, Executed, or Canceled
+
+// Get timestamps for scheduled, executable, and expiration times
+(uint48 scheduledAt, uint48 executableAt, uint48 expiresAt) =
+ executor.getSchedule(address(account), salt, mode, executionData);
+```
+
+Once the delay has elapsed and the state becomes `Ready`, you can execute the scheduled operation. Attempting to execute too early or after expiration will revert.
+
+```solidity
+// If the operation is ready, execute it
+if (operationState == ERC7579DelayedExecutor.OperationState.Ready) {
+ bytes[] memory returnData = executor.execute(address(account), salt, mode, executionData);
+}
+```
+
+If you need to cancel a scheduled operation (e.g., due to mistake or a change of plans), you can do so as long as the operation is still in the `Scheduled` or `Ready` state and not yet executed.
+
+```solidity
+// Cancel the operation
+if (operationState == ERC7579DelayedExecutor.OperationState.Scheduled ||
+ operationState == ERC7579DelayedExecutor.OperationState.Ready) {
+ executor.cancel(address(account), salt, mode, executionData);
+}
+```
+
+You can update the delay and expiration settings for scheduled operations, allowing you to control how long actions must wait before execution and how long they remain valid.
+
+```solidity
+// Update the delay (takes effect after a transition period)
+executor.setDelay(14 days); // New delay will be 14 days
+
+// Update the expiration (takes effect immediately)
+executor.setExpiration(60 days); // Operations expire after 60 days
+
+// Query current settings
+(uint32 delay, uint32 pendingDelay, uint48 effectTime) = executor.getDelay(account);
+uint32 expiration = executor.getExpiration(account);
+```
+
+
+ The transition period for `setDelay` depends on the direction of the change:
+
+ - **Increasing** the delay: takes effect after `min(newDelay, delayChangeWindow())` (defaults to 5 days). This means when the new delay is below `delayChangeWindow()`, the transition period is the new delay itself; when above, it is capped at `delayChangeWindow()` to guard against accidental lock-out (e.g. passing milliseconds instead of seconds).
+ - **Decreasing** the delay: takes effect after `currentDelay - newDelay`. This ensures no already-scheduled operation can execute under a shorter window than was active when the change was requested.
+
+ Changing the expiration or delay settings only affects new operations scheduled after the update. Operations that were
+ already scheduled retain their original delay and expiration timesβthey are not retroactively updated.
+
+
+#### Delayed Execution
+
+To maximize security when safeguarding account recovery, combine multisig with `ERC7579DelayedExecutor` so that recovery operations require approval from multiple guardians and then are subject to a configurable time delay before execution. This layered approach brings several security benefits:
+
+- **Guardian compromise protection**: Even if some guardians are compromised, the enforced delay gives honest parties time to detect and cancel malicious recovery attempts.
+- **Social engineering protection**: The delay window allows the account owner to notice and respond to unauthorized or suspicious recovery actions, helping to prevent attacks using social engineering tactics.
+- **Flexible cancellation**: During the security window, the account owner (or other authorized parties) can cancel scheduled recoveries, reducing the risk from accidental or malicious actions.
+
+By requiring both multisig approval and a delay, this design creates a robust security window that enables rapid response to threats, even in scenarios involving compromised guardians or possible trickery.
+
+This pattern requires guardians to:
+
+1. Approve the recovery operation (multisig validation)
+2. Wait for the delay period (security window)
+3. Execute the recovery after the delay
+
+To implement this, you would extend both `ERC7579Multisig` and `ERC7579DelayedExecutor`:
+
+```solidity
+import { ERC7579Multisig } from '@openzeppelin-accounts/contracts/modules/ERC7579Multisig.sol';
+import { ERC7579DelayedExecutor } from '@openzeppelin-accounts/contracts/modules/executors/ERC7579DelayedExecutor.sol';
+
+contract DelayedRecoveryExecutor is ERC7579Multisig, ERC7579DelayedExecutor {
+ function onInstall(bytes calldata data) public override(ERC7579Multisig, ERC7579DelayedExecutor, IERC7579Module) {
+ // Decode: guardians, threshold, delay, expiration
+ (bytes[] memory guardians, uint64 threshold, uint32 delay, uint32 expiration) = abi.decode(
+ data,
+ (bytes[], uint64, uint32, uint32)
+ );
+
+ // Set up multisig
+ _addSigners(msg.sender, guardians);
+ _setThreshold(msg.sender, threshold);
+
+ // Set up delayed executor
+ _configs[msg.sender].installed = true;
+ _setDelay(msg.sender, delay, 0);
+ _setExpiration(msg.sender, expiration);
+ }
+
+ function onUninstall(bytes calldata data) public override(ERC7579Multisig, ERC7579DelayedExecutor, IERC7579Module) {
+ super.onUninstall(data);
+ }
+
+ function schedule(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public {
+ // Decode the multisig signature and execution calldata
+ (bytes memory multisignature, bytes memory executionCalldata) = abi.decode(data, (bytes, bytes));
+ (bytes[] memory signingSigners, bytes[] memory signatures) =
+ abi.decode(multisignature, (bytes[], bytes[]));
+
+ // NOTE: EIP-712 typed hashes are a better recommendation, but using a
+ // simplified hash is better for educational purposes
+ bytes32 hash = keccak256(abi.encode(account, salt, mode, executionCalldata));
+ require(_validateMultisig(account, hash, signingSigners, signatures), "Invalid multisig");
+
+ // Call the internal _schedule function
+ _schedule(account, salt, mode, executionCalldata);
+ }
+
+ function cancel(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public {
+ // Only the account owner can cancel recovery operations
+ require(msg.sender == account, "unauthorized");
+
+ // Call the internal _cancel function
+ _cancel(account, salt, mode, data);
+ }
+
+ function execute(
+ address account,
+ bytes32 salt,
+ bytes32 mode,
+ bytes calldata data
+ ) public returns (bytes[] memory) {
+ // No authorization needed for execution. Operation state is checked in _execute
+ return _execute(account, salt, mode, data);
+ }
+}
+```
+
+To set it up:
+
+```solidity
+// Deploy the delayed recovery executor
+DelayedRecoveryExecutor recovery = new DelayedRecoveryExecutor();
+
+// Set up guardians
+bytes[] memory guardians = new bytes[](5);
+guardians[0] = abi.encodePacked(trustedFriend1);
+guardians[1] = abi.encodePacked(trustedFriend2);
+guardians[2] = abi.encodePacked(familyMember);
+guardians[3] = abi.encodePacked(backupDevice);
+guardians[4] = abi.encodePacked(secureLocation);
+
+uint64 threshold = 3; // 3-of-5 guardians required
+uint32 delay = 7 days; // 7-day security window
+uint32 expiration = 30 days; // Operations expire 30 days after becoming ready
+
+// Install the executor
+bytes memory initData = abi.encode(guardians, threshold, delay, expiration);
+account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), initData);
+```
+
+When guardians need to recover the account:
+
+1. Guardians sign the recovery operation
+2. Call `recovery.schedule()` with the account address, salt, mode, and combined data (multisig signature + recovery calldata)
+3. The executor validates that at least 3 guardians have signed
+4. The operation enters `Scheduled` state and waits for the 7-day delay
+5. During the delay, the account owner can cancel if the recovery is suspicious
+6. After 7 days, the operation becomes `Ready`
+7. Anyone can then call `recovery.execute()` to complete the recovery
+
+### ERC7579SocialRecoveryExecutor
+
+A production-ready implementation of the delayed social recovery pattern. It combines `ERC7579MultisigWeighted` with `ERC7579DelayedExecutor`, exposing dedicated `startRecovery`, `executeRecovery`, and `cancelRecovery` functions. Guardian signing uses EIP-712 typed data over the account's domain separator.
+
+See [Multisig as Executors](/accounts/contracts/multisig#as-executors) for the underlying pattern.
+
+Installation data is length-prefixed to separate the two components:
+
+```solidity
+import {MODULE_TYPE_EXECUTOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579SocialRecoveryExecutor} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579SocialRecoveryExecutor.sol";
+
+ERC7579SocialRecoveryExecutor recovery = new ERC7579SocialRecoveryExecutor();
+
+// Encode each component separately
+bytes memory executorArgs = abi.encodePacked(delay, expiration); // uint32 each
+bytes memory multisigArgs = abi.encode(guardians, threshold, weights); // bytes[], uint64, uint64[]
+
+// Pack with length prefixes: [uint16 executorArgsLen][executorArgs][uint16 multisigArgsLen][multisigArgs]
+bytes memory initData = abi.encodePacked(
+ uint16(executorArgs.length), executorArgs,
+ uint16(multisigArgs.length), multisigArgs
+);
+
+account.installModule(MODULE_TYPE_EXECUTOR, address(recovery), initData);
+```
+
+After installation, guardians use `startRecovery` to schedule and `cancelRecovery` to abort:
+
+```solidity
+// Guardians sign the SCHEDULE_RECOVER_TYPEHASH typed hash and submit
+recovery.startRecovery(account, salt, mode, recoveryData, multisignature);
+
+// Account owner (or guardians) can cancel during the delay window
+recovery.cancelRecovery(account, salt, mode, recoveryData);
+
+// Anyone can execute once ready
+recovery.executeRecovery(account, salt, mode, recoveryData);
+```
+
+Guardian management mirrors `ERC7579MultisigWeighted`, with aliased helpers:
+
+```solidity
+recovery.addGuardians(newGuardians);
+recovery.removeGuardians(oldGuardians);
+bool ok = recovery.isGuardian(account, guardian);
+```
+
+### ERC7579NativeSpender
+
+Extends `ERC7579Executor` with native token (ETH) spending limits and automatic renewal periods. Authorized spenders can call `transferFrom` to move ETH from an account up to a configurable per-period limit. Limits reset automatically at the end of each period without requiring manual intervention.
+
+```solidity
+import {MODULE_TYPE_EXECUTOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579NativeSpender} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579NativeSpender.sol";
+
+ERC7579NativeSpender nativeSpender = new ERC7579NativeSpender();
+
+// initData: abi.encode(spenders, starts, limits, renewPeriods)
+// starts: pass 0 to start immediately; all arrays must be equal length
+bytes memory initData = abi.encode(spenders, starts, limits, renewPeriods);
+account.installModule(MODULE_TYPE_EXECUTOR, address(nativeSpender), initData);
+```
+
+Spenders call `transferFrom` or `batchTransferFrom` to move funds within their limit:
+
+```solidity
+nativeSpender.transferFrom(account, recipient, amount);
+nativeSpender.batchTransferFrom(account, recipients, amounts);
+```
+
+The account can adjust or revoke spending configurations at any time:
+
+```solidity
+nativeSpender.setSpendingConfig(spender, newLimit, newRenewPeriod);
+nativeSpender.unsetSpendingConfig(spender);
+```
+
+
+ Setting `renewPeriod` to `type(uint48).max` effectively disables automatic renewal (one-time limit).
+
+
+### ERC7579ERC20Spender
+
+Mirrors `ERC7579NativeSpender` for ERC20 tokens. Spending configurations are tracked per tokenβspender pair, and the clock follows the token's ERC-6372 implementation when available, falling back to timestamps.
+
+```solidity
+import {MODULE_TYPE_EXECUTOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579ERC20Spender} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579ERC20Spender.sol";
+
+ERC7579ERC20Spender erc20Spender = new ERC7579ERC20Spender();
+
+// initData: abi.encode(tokens, spenders, starts, limits, renewPeriods)
+bytes memory initData = abi.encode(tokens, spenders, starts, limits, renewPeriods);
+account.installModule(MODULE_TYPE_EXECUTOR, address(erc20Spender), initData);
+```
+
+Spenders transfer tokens via `transferFrom` or `batchTransferFrom`:
+
+```solidity
+erc20Spender.transferFrom(account, token, recipient, amount);
+erc20Spender.batchTransferFrom(account, token, recipients, amounts);
+```
+
+The account manages configurations per tokenβspender pair:
+
+```solidity
+erc20Spender.setSpendingConfig(token, spender, newLimit, newRenewPeriod);
+erc20Spender.unsetSpendingConfig(token, spender);
+```
diff --git a/content/accounts/contracts/executors/social-recovery.mdx b/content/accounts/contracts/executors/social-recovery.mdx
new file mode 100644
index 00000000..17f72d0a
--- /dev/null
+++ b/content/accounts/contracts/executors/social-recovery.mdx
@@ -0,0 +1,322 @@
+---
+title: Social Recovery
+description: Secure account recovery through trusted guardians with time-delayed execution
+---
+
+import { Step, Steps } from 'fumadocs-ui/components/steps';
+
+Social recovery enables users to regain control of their smart accounts through trusted guardians when access to primary credentials is lost. The OpenZeppelin Accounts provides a production-ready social recovery module that combines weighted multisignatures with time-delayed execution, offering both flexibility and security. See also [Social Recovery use cases in Executors](/accounts/contracts/executors#erc7579socialrecoveryexecutor).
+
+## Overview
+
+Social recovery fundamentally combines two features:
+
+1. **Multisignature validation**: Require multiple guardians to approve a recovery operation
+2. **Execution scheme**: Define how and when approved operations execute
+
+While you can compose these features in various ways (combining [`ERC7579Multisig`](/accounts/contracts/multisig) or [`ERC7579MultisigWeighted`](/accounts/contracts/multisig) with [`ERC7579Executor`](/accounts/contracts/executors#base-executor), [`ERC7579DelayedExecutor`](/accounts/contracts/executors#erc7579delayedexecutor), or even `ERC7579Validator` for ERC-4337-based execution), **`ERC7579SocialRecoveryExecutor`** is the recommended implementation, with the following features:
+
+- Uses [`ERC7579MultisigWeighted`](/accounts/contracts/multisig) to implement a weighted list of guardians
+- Uses [`ERC7579DelayedExecutor`](/accounts/contracts/executors#erc7579delayedexecutor) to enforce a security window in which the recovery operations can be cancelled
+- Defines an EIP-712 type that needs to be signed using the account's domain by the account guardians
+- Allows executing arbitrary calldata from the account
+
+The module is implemented as an **executor**, rather than as a validator as specified in [ERC-7579](https://eips.ethereum.org/EIPS/eip-7579). [Executors](/accounts/contracts/executors) allow delayed and flexible execution, whereas a validator would execute through the ERC-4337 entrypoint requiring either 2 user operations or an external `queue` call (or similar) validator to support a cancellation window and wouldn't allow to customize the EIP-712 message that guardians authorize.
+
+```mermaid
+sequenceDiagram
+ participant Owner as Account Owner
+ participant Account as Smart Account
+ participant Module as Social Recovery Module
+ participant G1 as Guardian 1
+ participant G2 as Guardian 2
+ participant G3 as Guardian 3
+ participant Anyone as Anyone
+
+ Note over Owner,Anyone: Phase 1: Scheduling (Guardians coordinate)
+
+ G1->>G1: Sign EIP-712 typed data (ScheduleRecover)
+ G2->>G2: Sign EIP-712 typed data (ScheduleRecover)
+ G3->>G3: Sign EIP-712 typed data (ScheduleRecover)
+
+ Anyone->>Module: startRecovery(account, salt, mode, recoveryCalldata, multisignature)
+ Module->>Module: Validate guardians signatures Check threshold met
+ Module->>Module: Schedule operation
+ Module-->>Anyone: β Operation Scheduled
+
+ Note over Owner,Anyone: Phase 2: Security Window (e.g., 5 days)
+
+ rect rgba(255, 240, 240, 0.11)
+ Note over Owner,Module: Owner can cancel if malicious
+ Owner->>Module: cancelRecovery(account, salt, mode, recoveryCalldata)
+ Module->>Module: Verify caller is account
+ Module-->>Owner: β Operation Canceled
+ end
+
+ Note over Owner,Anyone: Phase 3: Execution (after delay)
+
+ rect rgba(240, 255, 240, 0.19)
+ Note over Module: Delay period elapsed Operation is Ready
+ Anyone->>Module: executeRecovery(account, salt, mode, recoveryCalldata)
+ Module->>Module: Verify operation is Ready
+ Module->>Account: executeFromExecutor(mode, recoveryCalldata)
+ Account->>Account: Execute recovery (e.g., install new validator)
+ Account-->>Module: β Executed
+ Module-->>Anyone: β Recovery Complete
+ end
+```
+
+### Weighted Guardians
+
+Unlike simple threshold-based multisigs (N-of-M), [weighted multisigs](/accounts/contracts/multisig#weighted-multisig-details) may support more sophisticated recovery setups. For example, social recovery could involve not only your friends and family but also other multisig. A recovery setup in which an external multisig has the same weight as 3 of your friends or 2 members of your family would be a valid configuration.
+
+### Delayed Execution
+
+A valid concern when setting up a social recovery mechanism is that guardians may collude maliciously or get compromised, in which case the legitimate owner of the account must be able to intervene. Using a 2-step configuration to execute a recovery action on the account provides a security window in which the owner can react. See the [operation lifecycle](/accounts/contracts/executors#erc7579delayedexecutor) for details on operation states.
+
+
+ The owner of the account must monitor the social recovery module periodically to detect unauthorized schedule
+ operations.
+
+
+### EIP-712 Authorization
+
+To preserve readability during recovery authorization, the executor module defines an EIP-712 type to start and cancel a recovery operation and uses the account's domain following [ERC-5267](https://eips.ethereum.org/EIPS/eip-5267). Guardians would see a request to sign a typed `StartRecovery` (or `CancelRecovery`) where the application is the account where the recovery will be executed.
+
+
+ The module assumes that 1) accounts define an EIP-712 domain and 2) they expose it following
+ [ERC-5267](https://eips.ethereum.org/EIPS/eip-5267).
+
+
+### Arbitrary Calldata
+
+Ideally, all accounts would implement a unified interface for recovery, however, most production-level smart accounts are already deployed and follow different schemes to set and reset the owner. In the case of ERC-7579 accounts one would argue that a recovery operation consists of adding a new validator module and optionally removing old ones, but in practice, recovery may be a call from the account to a validator to reset their key or replace signers.
+
+From a security standpoint, the benefits of a unified interface for recovery don't see to change dramatically. In any case, the guardians are still able to change the owner and start controlling the account themselves. The diversity of ownership schemes and the lack of security benefits from a unified interface makes executing arbitrary calls from the social recovery executor the simplest approach for generalized recovery.
+
+## Installation
+
+To install the social recovery executor on your ERC-7579 account:
+
+```solidity
+import {MODULE_TYPE_EXECUTOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579SocialRecoveryExecutor} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579SocialRecoveryExecutor.sol";
+
+// Deploy the executor (singleton, can be reused)
+ERC7579SocialRecoveryExecutor socialRecovery = new ERC7579SocialRecoveryExecutor();
+
+// Configure delay and expiration (executor settings)
+uint32 delay = 5 days; // Time before recovery can execute
+uint32 expiration = 60 days; // Time after which scheduled operations expire
+bytes memory executorArgs = abi.encodePacked(delay, expiration);
+
+// Configure guardians and threshold (multisig settings)
+bytes[] memory guardians = new bytes[](3);
+guardians[0] = abi.encodePacked(trustedFriend1);
+guardians[1] = abi.encodePacked(trustedFriend2);
+guardians[2] = abi.encodePacked(familyMember);
+
+uint64 threshold = 3; // All 3 guardians must approve
+
+uint64[] memory weights = new uint64[](3);
+weights[0] = 1; // Friend 1
+weights[1] = 1; // Friend 2
+weights[2] = 1; // Family member
+
+bytes memory multisigArgs = abi.encode(guardians, threshold, weights);
+
+// Combine both configurations
+bytes memory initData = abi.concat(
+ bytes2(uint16(executorArgs.length)), executorArgs,
+ bytes2(uint16(multisigArgs.length)), multisigArgs
+);
+
+// Install the module
+account.installModule(MODULE_TYPE_EXECUTOR, address(socialRecovery), initData);
+```
+
+## Configuration
+
+### Managing Guardians
+
+Guardians are represented as [ERC-7913](https://eips.ethereum.org/EIPS/eip-7913) signers (see [ERC-7913](/accounts/contracts/erc7913) for details). This allows mixing traditional EOAs with modern signature schemes like P256, WebAuthn, or ZKEmail. After installation, you can manage guardians dynamically:
+
+```solidity
+// Add new guardians
+bytes[] memory newGuardians = new bytes[](1);
+newGuardians[0] = abi.encodePacked(newGuardianAddress);
+multisig.addGuardians(newGuardians);
+
+// Remove guardians
+bytes[] memory oldGuardians = new bytes[](1);
+oldGuardians[0] = abi.encodePacked(oldGuardianAddress);
+multisig.removeGuardians(oldGuardians);
+
+// Update threshold (must remain reachable)
+multisig.setThreshold(4); // Now requires 4-of-N signatures
+
+// Update guardian weights
+bytes[] memory guardians = new bytes[](1);
+guardians[0] = abi.encodePacked(multisig);
+uint64[] memory weights = new uint64[](1);
+weights[0] = 5; // Increase external multisig weight to 5
+socialRecovery.setSignerWeights(guardians, weights);
+```
+
+
+ The threshold must always be reachable with the current number of guardians. Removing guardians will fail if it would
+ make the threshold unreachable.
+
+
+To query guardians:
+
+```solidity
+// Get all guardians (paginated)
+bytes[] memory allGuardians = socialRecovery.getGuardians(account, 0, type(uint64).max);
+
+// Check if someone is a guardian
+bool isGuardian = socialRecovery.isGuardian(account, guardianSigner);
+
+// Get guardian count
+uint256 count = socialRecovery.getGuardianCount(account);
+
+// Get guardian weight
+uint64 weight = socialRecovery.signerWeight(account, guardianSigner);
+
+// Get total weight and threshold
+uint64 total = socialRecovery.totalWeight(account);
+uint64 threshold = socialRecovery.threshold(account);
+```
+
+### Updating Delay and Expiration
+
+Accounts can adjust their recovery timing parameters:
+
+```solidity
+// Update delay (takes effect after transition period)
+socialRecovery.setDelay(7 days); // Increase security window to 7 days
+
+// Update expiration
+socialRecovery.setExpiration(90 days); // Extend operation lifetime to 90 days
+```
+
+
+ Delay changes do not take effect immediately. The transition period depends on the direction of the change:
+
+ - **Increasing** the delay: takes effect after `min(newDelay, delayChangeWindow())` (5 days by default). When the new delay is below `delayChangeWindow()`, the transition period is the new delay itself; very large ones are capped at `delayChangeWindow()` to guard against accidental lock-out.
+ - **Decreasing** the delay: takes effect after `currentDelay - newDelay`. This ensures no already-scheduled operation can execute under a shorter security window than was active when the change was requested.
+
+
+Query current and pending delays:
+
+```solidity
+(uint32 currentDelay, uint32 pendingDelay, uint48 effectTime) =
+ socialRecovery.getDelay(account);
+
+uint32 currentExpiration = socialRecovery.getExpiration(account);
+```
+
+## Recovery Process
+
+The recovery process consists of three phases: [scheduling, execution, and optional cancellation](/accounts/contracts/executors#erc7579delayedexecutor).
+
+
+
+### Scheduling Recovery
+
+Guardians must coordinate off-chain to sign an EIP-712 typed data message, then anyone can submit the multisignature to schedule the recovery:
+
+```solidity
+// Define the recovery operation (arbitrary calldata to execute on the account)
+// Example: Install a new validator module to regain access
+bytes memory recoveryCalldata = abi.encodeCall(
+ account.installModule,
+ (MODULE_TYPE_VALIDATOR, newValidatorAddress, newValidatorInitData)
+);
+
+// Guardians sign off-chain using EIP-712 typed data:
+// - Domain: account's EIP-712 domain
+// - Type: ScheduleRecover(bytes32 salt, bytes32 mode, bytes recoveryCalldata)
+// - Values: random salt, execution mode, recovery calldata
+
+// The multisignature is automatically sorted and encoded by guardian wallets
+// See: /accounts/contracts/multisig#signature-format
+bytes memory multisignature = /* ... guardian signatures ... */;
+
+// Submit the recovery operation
+socialRecovery.startRecovery(
+ accountAddress,
+ salt,
+ mode,
+ recoveryCalldata,
+ multisignature
+);
+```
+
+See [multisignature format](/accounts/contracts/multisig#signature-format)
+
+The operation enters the **Scheduled** state and cannot execute until the delay period elapses.
+
+
+
+
+### Executing Recovery
+
+After the delay period passes, anyone can execute the recovery:
+
+```solidity
+// Wait for delay to elapse (e.g., 5 days)
+// Then anyone can execute:
+
+socialRecovery.executeRecovery(
+ accountAddress,
+ salt,
+ mode,
+ recoveryCalldata
+);
+```
+
+The operation transitions to **Executed** state and the account's `executeFromExecutor` function is called with the recovery calldata.
+
+
+ Recovery operations expire if not executed within the expiration period (e.g., 60 days after becoming ready). Expired
+ operations must be rescheduled with a different salt.
+
+
+
+
+### Canceling Recovery (Optional)
+
+The legitimate account owner can cancel malicious or erroneous recovery operations during the delay period:
+
+```solidity
+// Called by the account itself
+socialRecovery.cancelRecovery(
+ accountAddress,
+ salt,
+ mode,
+ recoveryCalldata
+);
+```
+
+Guardians can also coordinate to cancel recovery by signing a cancellation message:
+
+```solidity
+// Guardians sign off-chain using EIP-712 typed data:
+// - Type: CancelRecover(bytes32 salt, bytes32 mode, bytes recoveryCalldata)
+
+bytes memory cancelMultisignature = /* ... guardian signatures ... */;
+
+socialRecovery.cancelRecovery(
+ accountAddress,
+ salt,
+ mode,
+ recoveryCalldata,
+ cancelMultisignature
+);
+```
+
+Canceled operations cannot be executed and enter the **Canceled** state permanently.
+
+
+
diff --git a/content/accounts/contracts/executors/spending-limits.mdx b/content/accounts/contracts/executors/spending-limits.mdx
new file mode 100644
index 00000000..d2812352
--- /dev/null
+++ b/content/accounts/contracts/executors/spending-limits.mdx
@@ -0,0 +1,227 @@
+---
+title: Spending Limits
+description: Session-key-driven spending policies for native ETH and ERC20 tokens with automatic renewal periods
+---
+
+`ERC7579NativeSpender` and `ERC7579ERC20Spender` are concrete [session-key](/accounts/contracts/session-keys) policy modules that grant per-key spending allowances on a smart account. They are useful for subscriptions, recurring payments, agent budgets, gaming sessions, or any case where the account owner wants to delegate a bounded amount of value to a session key without exposing the primary key.
+
+Each session key carries its own `(limit, renewPeriod)` policy. The window starts at the session key's `from` timepoint (managed by [`ERC7579SessionKeys`](/accounts/contracts/session-keys)) and renews automatically every `renewPeriod` time units, so the same allowance is available again in the next period without owner intervention.
+
+## Architecture
+
+Both modules are simultaneously **validators and executors**:
+
+- As a **validator**, the module authorizes a UserOp signed by a registered session key, provided `userOp.callData` is a single call into the module's own `transferFrom` or `batchTransferFrom`. The session key's validity range is packed into `validationData` so the EntryPoint enforces it natively β the validator does not need to re-check the range itself.
+- As an **executor**, the module spends from the account through `executeFromExecutor`, decrementing the per-key, per-period limit before the transfer.
+
+This means the module MUST be installed under **both** module types. The install is a two-step process:
+
+```
+First install (MODULE_TYPE_VALIDATOR): register session keys
+Second install (MODULE_TYPE_EXECUTOR): write the spending policies
+```
+
+The two installs commit one half of the state each. Once any policy is written, the module is "fully installed" and further `onInstall` calls revert with `AlreadyInstalled`. To change a policy, the account must **uninstall and reinstall** (which bumps the installation nonce and clears all state).
+
+
+ Session keys are immutable once configured: you cannot change a key's `(limit, renewPeriod)` or add new tokens to an
+ already-installed key. Plan all policies up-front in the second install, or use `addKeysWithConfig` to register fresh
+ session keys with their own policies.
+
+
+## ERC7579NativeSpender (ETH)
+
+One session key has one `(limit, renewPeriod)` policy. The session key authorizes ETH transfers from the account up to its per-period limit.
+
+### Installing
+
+Install the module under both module types in the same atomic flow. The two `initData` payloads have different shapes:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579NativeSpender} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579NativeSpender.sol";
+
+ERC7579NativeSpender nativeSpender = new ERC7579NativeSpender(); // singleton
+
+bytes[] memory keys = new bytes[](2);
+keys[0] = abi.encodePacked(eoaSessionKey); // 20-byte EOA key
+keys[1] = abi.encodePacked(p256Verifier, qx, qy); // ERC-7913 P256 key
+
+uint48[] memory starts = new uint48[](2);
+uint48[] memory ends = new uint48[](2);
+starts[0] = 0; // 0 β resolved to clock()
+ends[0] = uint48(block.timestamp + 7 days); // expires in 7 days
+starts[1] = 0;
+ends[1] = 0; // 0 β type(uint48).max (no expiry)
+
+uint160[] memory limits = new uint160[](2);
+limits[0] = 1 ether;
+limits[1] = 0.5 ether;
+
+uint48[] memory renewPeriods = new uint48[](2);
+renewPeriods[0] = 1 days;
+renewPeriods[1] = 30 days;
+
+// First install: validator role - registers the session keys
+account.installModule(
+ MODULE_TYPE_VALIDATOR,
+ address(nativeSpender),
+ abi.encode(keys, starts, ends)
+);
+
+// Second install: executor role - writes the spending policies
+account.installModule(
+ MODULE_TYPE_EXECUTOR,
+ address(nativeSpender),
+ abi.encode(keys, limits, renewPeriods)
+);
+```
+
+
+ Both installs can be bundled into a single transaction via the account's batch `execute`, or executed atomically
+ alongside the first session-key UserOp via [ERC-8043 init mode](/accounts/contracts/erc8043).
+
+
+### Spending via UserOp (recommended)
+
+A session key signs a `UserOperation` whose `callData` calls the module's own `transferFrom` (or `batchTransferFrom`):
+
+```solidity
+// Inner call: the module's transferFrom
+bytes memory innerCall = abi.encodeCall(
+ ERC7579NativeSpender.transferFrom,
+ (address(account), key, recipient, 0.3 ether)
+);
+
+// Outer call: account.execute(SINGLE, encodeSingle(module, 0, innerCall))
+bytes memory execCalldata = abi.encodePacked(address(nativeSpender), uint256(0), innerCall);
+bytes memory outerCallData = abi.encodeCall(
+ IERC7579Execution.execute,
+ (encodeMode(CALLTYPE_SINGLE, EXECTYPE_DEFAULT, 0, 0), execCalldata)
+);
+
+// Route the nonce through the spender as validator
+uint256 nonce = uint256(uint160(address(nativeSpender))) << 96;
+
+// Session key signs the UserOpHash
+PackedUserOperation memory userOp = /* ... fill, sign with key ... */;
+userOp.signature = abi.encode(key, sessionKeySignatureOverUserOpHash);
+```
+
+The validator gate accepts only this exact shape:
+
+- The outer call **must** be `IERC7579Execution.execute(...)` with `CALLTYPE_SINGLE` mode.
+- The single execution's `target` must be the module itself and `value` must be `0`.
+- The inner call's selector must be `transferFrom` or `batchTransferFrom`.
+- The `key` parameter inside the inner call must match the key in `userOp.signature` (prevents one key's signature from charging another key's allowance).
+
+Any deviation returns `SIG_VALIDATION_FAILED` without reverting.
+
+### EOA self-call (when the key is a plain EOA)
+
+If the session key is a 20-byte EOA, its holder can call `transferFrom` directly from that EOA β skipping the EntryPoint entirely. The module verifies `msg.sender == address(bytes20(key))` and the key's validity range manually:
+
+```solidity
+// The EOA whose address bytes match `key` calls the module directly.
+nativeSpender.transferFrom(address(account), key, recipient, 0.3 ether);
+```
+
+This path still requires the module to be installed as `MODULE_TYPE_EXECUTOR`, since it eventually calls `executeFromExecutor` on the account. For non-EOA keys (P256, WebAuthn, ZKEmail, β¦) the UserOp flow is the only option.
+
+### Removing keys
+
+`removeKeys` clears both the validity range and the per-key spending config. Spending history at the historical `from` timepoint is preserved in storage and can still be read via `spent(account, key, period, historicalStart)`:
+
+```solidity
+bytes[] memory oldKeys = new bytes[](1);
+oldKeys[0] = abi.encodePacked(eoaSessionKey);
+nativeSpender.removeKeys(oldKeys); // called by the account
+```
+
+## ERC7579ERC20Spender
+
+Same dual-role design, with an explicit `token` axis. A single session key can hold spending policies for **multiple tokens** by listing it more than once in the executor install bundle.
+
+### Installing
+
+```solidity
+import {ERC7579ERC20Spender} from "@openzeppelin-accounts/contracts/modules/executors/ERC7579ERC20Spender.sol";
+
+ERC7579ERC20Spender erc20Spender = new ERC7579ERC20Spender(); // singleton
+
+// Register one key
+bytes[] memory keys = new bytes[](1);
+keys[0] = abi.encodePacked(sessionKey);
+uint48[] memory starts = new uint48[](1);
+uint48[] memory ends = new uint48[](1);
+starts[0] = 0;
+ends[0] = uint48(block.timestamp + 30 days);
+
+account.installModule(
+ MODULE_TYPE_VALIDATOR,
+ address(erc20Spender),
+ abi.encode(keys, starts, ends)
+);
+
+// Configure the same key for TWO tokens by repeating it across rows
+bytes[] memory cfgKeys = new bytes[](2);
+address[] memory cfgTokens = new address[](2);
+uint160[] memory cfgLimits = new uint160[](2);
+uint48[] memory cfgRenews = new uint48[](2);
+cfgKeys[0] = keys[0]; cfgTokens[0] = USDC; cfgLimits[0] = 100e6; cfgRenews[0] = 30 days;
+cfgKeys[1] = keys[0]; cfgTokens[1] = DAI; cfgLimits[1] = 1000e18; cfgRenews[1] = 30 days;
+
+account.installModule(
+ MODULE_TYPE_EXECUTOR,
+ address(erc20Spender),
+ abi.encode(cfgKeys, cfgTokens, cfgLimits, cfgRenews)
+);
+```
+
+
+ The `tokens` parameter MUST be a contract address. The module verifies `token.code.length > 0` to catch typos at
+ install time. ERC20 transfers that return `false` (or that revert) trigger `ERC7579ERC20SpenderFailedTransfer`. Tokens
+ with no return value (older USDT-style) are supported.
+
+
+### Spending
+
+The public surface mirrors the native module, with an extra `token` parameter:
+
+```solidity
+erc20Spender.transferFrom(address(account), key, USDC, recipient, 50e6);
+erc20Spender.batchTransferFrom(address(account), key, DAI, [r1, r2], [100e18, 200e18]);
+```
+
+The validator gate is identical: outer `execute(SINGLE, ...)` into the module, inner selector must be one of `transferFrom`/`batchTransferFrom`, key consistency between signature and inner call.
+
+### Removing keys (different from native)
+
+`removeKeys` on the ERC20 module clears the validity range but **does not** clear the `(key, token)` spending configs. If the same key bytes are re-registered later via plain `addKeys`, the previously-stored configs become active again. To re-register with a fresh policy, use `addKeysWithConfig`:
+
+```solidity
+// Inherits the old configs:
+erc20Spender.addKeys([key], [0], [0]);
+
+// Overrides the old config for (key, USDC):
+erc20Spender.addKeysWithConfig([key], [0], [0], [USDC], [200e6], [30 days]);
+```
+
+The rationale is that multi-token configs would otherwise require unbounded gas to clear on `removeKeys` (one config per `(key, token)` pair). Document this for your operational runbook if you uninstall keys at scale.
+
+## Common security properties
+
+- **ERC-1271 disabled.** Both modules deliberately make `isValidSignatureWithSender` return `0xffffffff`. A spending session key cannot be repurposed to sign EIP-2612 permits or other arbitrary off-chain digests on the account's behalf.
+- **Past starts banned.** Both modules override `_addKeys` to reject any `start` timepoint older than `clock()`. This prevents a removed-then-re-registered key from inheriting stale `_spentFor` storage slots written under a previous validity range. Pass `starts[i] == 0` to resolve to the current timepoint.
+- **Reconfiguration banned.** Once a policy is written, neither it nor any sibling policy can be reconfigured under the current installation. Change requires uninstall + reinstall, which bumps the installation nonce and resets state.
+- **No external `value` channel.** The validator gate rejects user operations where the outer execution carries non-zero `value` to the module, so the only ETH ever leaving the account is the one the module itself sends via `_execute`.
+
+## Atomic install + first use with ERC-8043
+
+Because both module-type installs are routine `installModule` calls on the account, they compose with [ERC-8043 init mode](/accounts/contracts/erc8043). A dapp can:
+
+1. Have the account owner sign an EIP-712 `InitModules` authorization for `[VALIDATOR, EXECUTOR]` with `[validatorInitData, executorInitData]`.
+2. Have the session key sign the first `transferFrom`/`batchTransferFrom` user operation.
+3. Bundle both signatures into a single UserOp β the account installs both module roles inline, then validates the session key's signature through the freshly-installed validator.
+
+This is the recommended onboarding path for end-user dapps: the owner authorizes once (off-chain), the session key handles everything else (on-chain) for the duration of its validity window.
diff --git a/content/accounts/contracts/index.mdx b/content/accounts/contracts/index.mdx
new file mode 100644
index 00000000..3ad7af39
--- /dev/null
+++ b/content/accounts/contracts/index.mdx
@@ -0,0 +1,168 @@
+---
+title: Overview
+description: Architecture and key concepts of OpenZeppelin Accounts
+---
+
+import { File, Folder, Files } from 'fumadocs-ui/components/files';
+
+The OpenZeppelin Accounts is designed to provide compatibility with ERC-7579 accounts. Previous developments in major smart account contracts have provided with a layer of compatibility with ERC-7579, or even native support, which suits the standard to distribute standardized functionality that developers can use safely with popular account implementations like [OpenZeppelin's ERC-7579 account](/contracts/5.x/api/account#AccountERC7579) or [Kernel](https://github.com/zerodevapp/kernel).
+
+The contracts are organized with a clear structure that separates account implementations, and modules. This overview explains the architecture and key components.
+
+## Directory Structure
+
+The repository's source code is organized as follows:
+
+```files
+src/
+βββ OpenAccount.sol # Base ERC-7579 account implementation
+βββ modules/
+β βββ ERC7579Multisig.sol # Multisig module (ERC-7913)
+β βββ ERC7579MultisigWeighted.sol # Weighted multisig module
+β βββ executors/ # Execution modules
+β βββ validators/ # Validation modules
+βββ extensions/ # Account extensions
+```
+
+## Setting up an account
+
+Since these contracts are designed to provide compatibility with major ERC-7579 account implementations, it's assumed the developer has picked one already. Otherwise, `OpenAccount` is provided as a starting point.
+
+### OpenAccount
+
+`OpenAccount` is a production-ready, ERC-7579 compliant smart account implementation built on top of [OpenZeppelin Contracts](/contracts). It provides:
+
+- Full ERC-7579 compliance (module installation, execution modes, validation)
+- ERC-4337 support for account abstraction
+- EIP-712 signature support
+- NFT holder capabilities (ERC721 and ERC1155)
+
+#### Using OpenAccountFactory
+
+The `OpenAccountFactory` is deployed across all major EVM chains at the same deterministic address:
+
+```
+0x9045eF209067701bE7b78AaC216C04Ffa6543330
+```
+
+This factory creates minimal ERC-1967 proxy clones of the `OpenAccountImplementation` contract, providing deterministic addresses and efficient ERC-1967 proxy deployment. By design, the account is upgradeable, allowing the account itself (or the entrypoint) to upgrade the implementation contract.
+
+`OpenAccountImplementation` inherits from both `OpenAccount` and `AccountERC8043Upgradeable`, so every account deployed through this factory supports [init mode (ERC-8043)](/accounts/contracts/erc8043) out of the box. No extra setup is required to install and use modules atomically in a single user operation.
+
+
+ ZKSync has a different CREATE2 derivation scheme making it impossible to deploy deterministically in chains using the
+ ZKStack.
+
+
+#### Creating a Custom Factory
+
+If you need custom account logic or want to extend `OpenAccount`, you can create your own account factory using OpenZeppelin's [Clones library](/contracts/5.x/api/proxy#Clones):
+
+```solidity
+// contracts/MyFactoryAccount.sol
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import { Clones } from '../../../proxy/Clones.sol';
+import { Address } from '../../../utils/Address.sol';
+
+/**
+ * @dev A factory contract to create accounts on demand.
+ */
+contract MyFactoryAccount {
+ using Clones for address;
+ using Address for address;
+
+ address private immutable _impl;
+
+ constructor(address impl_) {
+ require(impl_.code.length > 0);
+ _impl = impl_;
+ }
+
+ /// @dev Predict the address of the account
+ function predictAddress(bytes calldata callData) public view returns (address) {
+ return _impl.predictDeterministicAddress(keccak256(callData), address(this));
+ }
+
+ /// @dev Create clone accounts on demand
+ function cloneAndInitialize(bytes calldata callData) public returns (address) {
+ address predicted = predictAddress(callData);
+ if (predicted.code.length == 0) {
+ _impl.cloneDeterministic(keccak256(callData));
+ predicted.functionCall(callData);
+ }
+ return predicted;
+ }
+}
+```
+
+
+ Most developers are fine using deployed `OpenAccountFactory` at `0x9045eF209067701bE7b78AaC216C04Ffa6543330`. Only
+ create a custom factory if you need to extend `OpenAccount` with custom logic or implement a different account
+ contract.
+
+
+
+ Account factories should be carefully implemented to ensure the account address is deterministically tied to the
+ initial owners or signers. This prevents frontrunning attacks where a malicious actor could deploy the account with
+ their own owners before the intended owner does. The factory should include the owner's address (or other identifying
+ information) in the salt used for address calculation.
+
+
+#### Handling Initialization
+
+Since `OpenAccount` implements `Initializable`, it's designed to work with factories. The account must be initialized with at least one validator or executor module right after deployment:
+
+```solidity
+function initialize(
+ uint256[] calldata moduleTypeIds,
+ address[] calldata modules,
+ bytes[] calldata initDatas,
+ Mode configMode,
+ bytes calldata configCalldata
+) public
+```
+
+In case an extra configuration call is needed from the account, the `configMode` and `configCalldata` arguments following ERC-7579 `Mode` and `executionCalldata` definitions, which allow to execute arbitrary calls during initialization and after module installation:
+
+```solidity
+// Single configuration call during initialization
+Mode configMode = CALLTYPE_SINGLE.encodeMode(EXECTYPE_DEFAULT, ...);
+bytes memory configCalldata = abi.encodePacked(
+ targetAddress,
+ uint256(0), // value
+ abi.encodeCall(target.someFunction, (arg1, arg2))
+);
+
+account.initialize(moduleTypeIds, modules, initDatas, configMode, configCalldata);
+```
+
+
+ Factories will normally include the initialization call as part of the address derivation to ensure that each address
+ is specific to the owners and configuration set. For this reason, accounts who want to support `value` during a
+ configuration call must receive it first to an address that includes the transfer and native value in the calldata;
+ any change to the `configCalldata` will change the address of the account.
+
+
+
+ Accounts must be initialized with at least one validator or executor module. Leaving an account uninitialized or
+ without these modules will make it unusable since no module will be installed to authorize operations.
+
+
+## Installing Modules
+
+To install a module on an account:
+
+```solidity
+// Install an ECDSA validator
+address validator = address(new ERC7579ECDSAValidator());
+bytes memory initData = abi.encodePacked(signerAddress);
+account.installModule(MODULE_TYPE_VALIDATOR, validator, initData);
+```
+
+The `initData` format depends on the module. For example:
+
+- **ECDSA Validator**: `abi.encodePacked(signerAddress)` (20 bytes)
+- **Multisig**: `abi.encode(signers, threshold)` where `signers` is `bytes[]` and `threshold` is `uint64`
diff --git a/content/accounts/contracts/multisig.mdx b/content/accounts/contracts/multisig.mdx
new file mode 100644
index 00000000..0fbc7212
--- /dev/null
+++ b/content/accounts/contracts/multisig.mdx
@@ -0,0 +1,531 @@
+---
+title: Multisig
+description: Threshold and weighted multisignature modules and utilities, with optional on-chain confidentiality
+---
+
+import { Step, Steps } from 'fumadocs-ui/components/steps';
+
+OpenZeppelin Accounts provides multisignature (multisig) modules and utilities that enable accounts to require multiple signatures before executing operations. Unlike traditional accounts controlled by a single private key, multisigs distribute control among multiple parties, eliminating single points of failure.
+
+It also provides base multisig modules that can be combined with different module types (validators or executors) to create various use cases. This flexible design allows multisig functionality to be used for signature validation, execution authorization, or both.
+
+Two main multisig modules are available:
+
+**`ERC7579Multisig`**: A threshold-based multisig module that requires a fixed number of signatures. For example, a 3-of-5 multisig requires signatures from at least 3 out of 5 possible signers.
+
+**`ERC7579MultisigWeighted`**: An extension that supports weighted voting, where different signers can have different weights. This enables more sophisticated governance and authorization schemes.
+
+**Key Features of Base Modules:**
+
+- Threshold-based validation (e.g., 3-of-5 signers required)
+- [ERC-7913](/accounts/contracts/erc7913) signer support (P256, WebAuthn, ECDSA, etc.)
+- Dynamic signer management (add/remove signers)
+- Configurable thresholds
+- Weighted voting support (in `ERC7579MultisigWeighted`)
+
+Confidential versions of the multisig modules are also available. They use Fully Homomorphic Encryption (FHE) to keep signer addresses, weights, and thresholds encrypted on-chain, hiding sensitive information from public view.
+
+**`ERC7579MultisigConfidential`**: A confidential threshold-based multisig module that keeps signer addresses and threshold values encrypted.
+
+**`ERC7579MultisigWeightedConfidential`**: A confidential weighted multisig module that keeps signer addresses, individual weights, and threshold values encrypted.
+
+## Understanding Confidential Multisig
+
+Confidential multisig modules provide enhanced privacy by keeping critical multisig parameters encrypted on-chain using Fully Homomorphic Encryption (FHE). This approach offers several privacy properties:
+
+**Signer Privacy**: Signer addresses are stored as encrypted values (ciphertexts) on-chain. While signers must reveal their addresses when providing valid signatures for an operation, the confidential nature provides obfuscation benefits:
+
+- **Weight Obfuscation** (weighted variant): Even when a signer reveals their address, their individual weight remains encrypted. Observers cannot determine how much voting power each participant has.
+- **Threshold Ambiguity**: The exact threshold required remains encrypted. Valid signatures are accepted, but observers cannot determine which specific signatures counted toward the threshold, or how many signatures were actually needed.
+- **Dynamic Privacy**: Signers can be rotated at the end of any ERC-4337 user operation execution. Even if the same signers are set again, FHE's non-deterministic encryption produces different ciphertexts (handles) for the same addresses, effectively re-establishing their confidentiality. This makes it difficult for observers to track whether the signer set has changed or remained constant.
+
+The confidential module variants don't support ERC-7913 signers, only addresses.
+
+
+ Due to FHE's non-deterministic nature, encrypting the same address multiple times produces different ciphertexts. This
+ means the same signer can be added multiple times with different encrypted handles. The validation logic handles
+ duplicates by checking plaintext addresses.
+
+
+
+ While signers must reveal their addresses when signing valid operations, they can maintain long-term privacy by rotating
+ their encrypted handles (even to the same addresses) after each sensitive operation, particularly at the end of user
+ operations where the signer update won't be immediately observable.
+
+
+## As validators
+
+The following validator implementations combine the base multisig modules with validator functionality:
+
+- **`ERC7579MultisigValidator`**: Combines `ERC7579Multisig` with validator functionality for signature validation
+- **`ERC7579MultisigWeightedValidator`**: Combines `ERC7579MultisigWeighted` with validator functionality
+- **`ERC7579MultisigConfidentialValidator`**: Combines `ERC7579MultisigConfidential` with validator functionality for confidential signature validation
+- **`ERC7579MultisigWeightedConfidentialValidator`**: Combines `ERC7579MultisigWeightedConfidential` with validator functionality for confidential weighted signature validation
+
+These validators enable accounts to require multiple signatures before executing operations through ERC-4337 UserOperations.
+
+### Multisig Validator
+
+To install a multisig validator on your account:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579MultisigValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579MultisigValidator.sol";
+
+// Deploy the validator (singleton, can be reused)
+ERC7579MultisigValidator multisig = new ERC7579MultisigValidator();
+
+// Prepare signers (ERC-7913 format: verifier || key)
+bytes[] memory signers = new bytes[](3);
+signers[0] = abi.encodePacked(alice); // EOA address (20 bytes)
+signers[1] = abi.encodePacked(p256Verifier, p256PublicKeyX, p256PublicKeyY); // P256 signer
+signers[2] = abi.encodePacked(zkEmailVerifier, emailKey); // ZKEmail signer
+
+// Set threshold (3-of-3 multisig)
+uint64 threshold = 3;
+
+// Encode initialization data
+bytes memory initData = abi.encode(signers, threshold);
+
+// Install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(multisig), initData);
+```
+
+The `initData` must be encoded as:
+
+```solidity
+bytes memory initData = abi.encode(bytes[] signers, uint64 threshold);
+```
+
+### Multisig Weighted Validator
+
+To install a multisig validator that allows for weighted signers:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579MultisigWeightedValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579MultisigWeightedValidator.sol";
+
+// Deploy the weighted validator (singleton, can be reused)
+ERC7579MultisigWeightedValidator multisigWeighted = new ERC7579MultisigWeightedValidator();
+
+// Prepare signers (ERC-7913 format: verifier || key)
+bytes[] memory signers = new bytes[](3);
+signers[0] = abi.encodePacked(alice); // EOA address (20 bytes)
+signers[1] = abi.encodePacked(p256Verifier, p256PublicKeyX, p256PublicKeyY); // P256 signer
+signers[2] = abi.encodePacked(zkEmailVerifier, emailKey); // ZKEmail signer
+
+// Set threshold (sum of weights needed to approve)
+uint64 threshold = 5;
+
+// Set individual signer weights
+uint64[] memory weights = new uint64[](3);
+weights[0] = 2; // alice has weight 2
+weights[1] = 2; // p256 has weight 2
+weights[2] = 2; // zkEmail has weight 2
+
+// Encode initialization data
+bytes memory initData = abi.encode(signers, threshold, weights);
+
+// Install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(multisigWeighted), initData);
+```
+
+The `initData` must be encoded as:
+
+```solidity
+bytes memory initData = abi.encode(bytes[] signers, uint64 threshold, uint64[] weights);
+```
+
+### Multisig Confidential Validator
+
+The confidential validators use FHE to keep signer addresses and thresholds encrypted on-chain, providing enhanced privacy for multisig operations. Due to FHE's asynchronous decryption model, validating a UserOperation with confidential multisig requires a three-step process:
+
+
+
+
+**Request Disclosure**
+
+Call `requestDiscloseOperation` to validate the multisig using encrypted values and make the result publicly decryptable:
+
+```solidity
+import {ERC7579MultisigConfidentialValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579MultisigConfidentialValidator.sol";
+
+// Prepare the UserOperation hash and signature data
+bytes32 userOpHash = ...; // Hash of the UserOperation
+bytes[] memory signingSigners = new bytes[](2);
+signingSigners[0] = abi.encodePacked(alice);
+signingSigners[1] = abi.encodePacked(bob);
+
+// Provide the encrypted signer handles (obtained from the module)
+euint256[] memory signerIdHandles = new euint256[](2);
+signerIdHandles[0] = ...; // Alice's encrypted handle
+signerIdHandles[1] = ...; // Bob's encrypted handle
+
+bytes[] memory signatures = new bytes[](2);
+signatures[0] = aliceSignature;
+signatures[1] = bobSignature;
+
+// Request disclosure (can be called by anyone)
+multisigConfidential.requestDiscloseOperation(
+ address(account),
+ userOpHash,
+ signingSigners,
+ signerIdHandles,
+ signatures
+);
+```
+
+
+
+**Disclose Operation**
+
+After requesting disclosure, call `discloseOperation` with the decryption proof to verify and mark the operation as disclosed:
+
+```solidity
+// Obtain the decryption proof (from FHE network/oracle)
+bytes memory decryptionProof = ...;
+
+// Disclose the operation result
+multisigConfidential.discloseOperation(userOpHash, decryptionProof);
+```
+
+
+
+**Submit UserOperation**
+
+Once the operation is disclosed, submit the UserOperation to the EntryPoint. The validator will check that the operation hash has been properly disclosed:
+
+```solidity
+// The EntryPoint will call validateUserOp, which checks:
+// - That the operation hash has been disclosed
+// - The disclosed result indicates valid signatures
+
+// Submit through EntryPoint as normal
+entryPoint.handleOps(userOps, beneficiary);
+```
+
+
+
+
+To install a confidential multisig validator on your account:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579MultisigConfidentialValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579MultisigConfidentialValidator.sol";
+import {FHE, externalEuint256, externalEuint32} from "@fhevm/solidity/lib/FHE.sol";
+
+// Deploy the confidential validator (singleton, can be reused)
+ERC7579MultisigConfidentialValidator multisigConfidential = new ERC7579MultisigConfidentialValidator();
+
+// Encrypt signer addresses off-chain using FHE
+// (signers must be encrypted as uint256 addresses)
+externalEuint256[] memory encryptedSigners = new externalEuint256[](3);
+encryptedSigners[0] = ...; // Encrypted alice address
+encryptedSigners[1] = ...; // Encrypted bob address
+encryptedSigners[2] = ...; // Encrypted charlie address
+
+// Encrypt the threshold
+externalEuint32 encryptedThreshold = ...; // Encrypted threshold value (e.g., 2)
+
+// Prepare the FHE input proof
+bytes memory inputProof = ...;
+
+// Encode initialization data
+bytes memory initData = abi.encode(inputProof, encryptedSigners, encryptedThreshold);
+
+// Install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(multisigConfidential), initData);
+```
+
+The `initData` must be encoded as:
+
+```solidity
+bytes memory initData = abi.encode(
+ bytes inputProof,
+ externalEuint256[] signerIds,
+ externalEuint32 threshold
+);
+```
+
+### Multisig Weighted Confidential Validator
+
+The weighted confidential validator extends the confidential validator to support weighted signatures while keeping weights encrypted:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579MultisigWeightedConfidentialValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579MultisigWeightedConfidentialValidator.sol";
+import {FHE, externalEuint256, externalEuint32, externalEuint16} from "@fhevm/solidity/lib/FHE.sol";
+
+// Deploy the weighted confidential validator (singleton, can be reused)
+ERC7579MultisigWeightedConfidentialValidator multisigWeightedConfidential =
+ new ERC7579MultisigWeightedConfidentialValidator();
+
+// Encrypt signer addresses off-chain
+externalEuint256[] memory encryptedSigners = new externalEuint256[](3);
+encryptedSigners[0] = ...; // Encrypted alice address
+encryptedSigners[1] = ...; // Encrypted bob address
+encryptedSigners[2] = ...; // Encrypted charlie address
+
+// Encrypt the threshold (total weight needed)
+externalEuint32 encryptedThreshold = ...; // Encrypted threshold (e.g., 5)
+
+// Encrypt individual signer weights
+externalEuint16[] memory encryptedWeights = new externalEuint16[](3);
+encryptedWeights[0] = ...; // Encrypted weight for alice (e.g., 2)
+encryptedWeights[1] = ...; // Encrypted weight for bob (e.g., 2)
+encryptedWeights[2] = ...; // Encrypted weight for charlie (e.g., 2)
+
+// Prepare the FHE input proof
+bytes memory inputProof = ...;
+
+// Encode initialization data
+bytes memory initData = abi.encode(
+ inputProof,
+ encryptedSigners,
+ encryptedThreshold,
+ encryptedWeights
+);
+
+// Install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(multisigWeightedConfidential), initData);
+```
+
+The `initData` must be encoded as:
+
+```solidity
+bytes memory initData = abi.encode(
+ bytes inputProof,
+ externalEuint256[] signerIds,
+ externalEuint32 threshold,
+ externalEuint16[] weights
+);
+```
+
+
+ The three-step disclosure process (request, disclose, submit) is required because FHE operations are asynchronous.
+ The decryption proof must be obtained from the FHE network between steps 1 and 2.
+
+
+
+ UserOperations are public in the bundler mempool once submitted. Do not assume the UserOperation payload is confidential.
+ Consider front-running risks associated with your UserOperation data.
+
+
+
+ To maintain signer privacy after an operation, rotate the encrypted signer handles at the end of the UserOperation
+ execution. Even if setting the same addresses, new handles will be generated due to FHE's non-deterministic encryption.
+
+
+## As executors
+
+The base multisig modules can also be extended to work with executors, enabling different execution patterns. For example:
+
+- **Social Recovery with Immediate Execution**: Combine multisig with `ERC7579Executor` to allow guardians to immediately execute recovery operations once the threshold is met.
+
+- **Social Recovery with Delayed Execution**: Combine multisig with `ERC7579DelayedExecutor` to require guardians to approve recovery operations, which then execute after a time delay. This provides a security window to cancel suspicious operations.
+
+This design pattern allows multisig to be used flexibly across different use cases, not just for signature validation. `ERC7579SocialRecoveryExecutor` is a ready-to-use implementation of the delayed social recovery pattern. See [Executors](/accounts/contracts/executors#erc7579socialrecoveryexecutor) for details.
+
+## Managing Signers
+
+After installation, you can manage signers dynamically:
+
+```solidity
+// Add new signers
+bytes[] memory newSigners = new bytes[](1);
+newSigners[0] = abi.encodePacked(newSignerAddress);
+multisig.addSigners(newSigners);
+
+// Remove signers
+bytes[] memory oldSigners = new bytes[](1);
+oldSigners[0] = abi.encodePacked(oldSignerAddress);
+multisig.removeSigners(oldSigners);
+
+// Update threshold (must remain reachable)
+multisig.setThreshold(4); // Now requires 4-of-N signatures
+```
+
+
+ The threshold must always be reachable with the current number of signers. Removing signers will fail if it would make
+ the threshold unreachable.
+
+
+## Signature Format
+
+When signing a UserOperation or message, the signature must be encoded as:
+
+```solidity
+bytes memory signature = abi.encode(
+ bytes[] signingSigners, // Array of signers participating (sorted by keccak256)
+ bytes[] signatures // Array of signatures corresponding to each signer
+);
+```
+
+The `signingSigners` array must be sorted in ascending order by `keccak256(signer)` to prevent duplicates and optimize verification.
+
+## Weighted Multisig Details
+
+When using `ERC7579MultisigWeightedValidator`, each signer has a weight (default is 1). The threshold is based on total weight, not signer count.
+
+**Important Considerations:**
+
+- **Weight scale**: Ensure the threshold matches the scale used for signer weights. If signers have weights 1, 2, or 3, a threshold of 4 requires at least two signers.
+- **Base weight**: All signers have a base weight of 1. Additional weight is stored separately, so a signer with weight 3 has base weight 1 + extra weight 2.
+- **Threshold validation**: The threshold must be reachable with the total weight of all signers.
+
+Example:
+
+```solidity
+// Signers with weights: Alice=1, Bob=2, Charlie=3
+// Total weight = 6
+// Threshold = 4 means: Bob+Charlie (5) or Alice+Bob+Charlie (6) or Alice+Charlie (4)
+```
+
+## Querying Multisig State
+
+You can query the multisig module to check signers and thresholds:
+
+```solidity
+// Check if a signer is authorized
+bool isAuthorized = multisig.isSigner(account, signer);
+
+// Get the number of signers
+uint256 signerCount = multisig.getSignerCount(account);
+
+// Get signers (paginated)
+bytes[] memory signers = multisig.getSigners(account, 0, 10);
+
+// Get the threshold
+uint64 currentThreshold = multisig.threshold(account);
+
+// For weighted multisig, get signer weight
+uint64 weight = weightedMultisig.signerWeight(account, signer);
+
+// Get total weight
+uint64 total = weightedMultisig.totalWeight(account);
+```
+
+## MultisigCoordinator
+
+`MultisigCoordinator` is a utility contract (not an ERC-7579 module) that bridges multisig accounts with [confidential tokens (ERC-7984)](https://ercs.ethereum.org/ERCS/erc-7984). It is deployed independently and works alongside any installed `ERC7579Multisig` module.
+
+Two problems arise when a multisig account interacts with confidential tokens:
+
+1. **Off-chain input preparation**: The encrypted input (ciphertext + ZK proof) is typically constructed by a single signer off-chain. The remaining signers cannot independently verify it before execution and must trust the submitting signer's preparation.
+
+2. **ACL EOA-only restriction**: Zama's [FHE ACL](https://docs.zama.org/protocol/solidity-guides/smart-contract/acl) only supports EOA addresses as decryption recipients. A smart-contract multisig cannot be directly granted decryption rights in a way its signers can act on off-chain.
+
+`MultisigCoordinator` solves both by providing:
+
+1. **Encrypted value creation**: Any caller can submit an externally-encrypted FHE value with its input proof. The contract validates the proof and registers ACL permissions so that the coordinator and the account can subsequently use the resulting handle (e.g. pass it to `confidentialTransferFrom`).
+
+2. **Handle allowance delegation**: Any authorized signer can grant ACL permission over an existing encrypted handle to any address. This allows individual signers to expose a value to their own EOA address for off-chain decryption, without the account needing to approve anything. Note that any other contract that performs FHE operations on the handle (e.g. a token contract that performs a transfer using the handle) must also be granted ACL access via `allowHandle` β see the example below.
+
+
+ `MultisigCoordinator` is abstract and must be extended with an fhEVM configuration before deployment. Once deployed, a single instance can serve multiple accounts, and each account registers its own multisig address independently via `setERC7579Multisig`.
+
+
+### Setting up the coordinator
+
+Before using `allowHandle`, the account must register its `ERC7579Multisig` module with the coordinator. This is done by calling `setERC7579Multisig` from the account itself, typically as part of a UserOperation:
+
+```solidity
+import {MultisigCoordinator} from "@openzeppelin-accounts/contracts/utils/MultisigCoordinator.sol";
+
+// Called from the account (e.g. via account.execute in a UserOperation)
+coordinator.setERC7579Multisig(address(multisigModule));
+```
+
+### Creating an encrypted value
+
+Any caller can submit an FHE-encrypted value together with its input proof. The coordinator verifies the proof and grants ACL access to both itself and the target account:
+
+```solidity
+import {FheType} from "@fhevm/solidity/lib/FheType.sol";
+
+// Off-chain: one signer encrypts the transfer amount using the FHE public key
+// and generates the input proof
+bytes32 encryptedAmount = ...; // handle from input.encrypt()
+bytes memory inputProof = ...; // proof from input.encrypt()
+
+// On-chain: any caller registers the encrypted value for the account
+bytes32 handle = coordinator.fromExternal(address(account), encryptedAmount, inputProof, FheType.Uint64);
+
+// Grant the token contract ACL access for the handle, so that its transfer logic
+// can perform FHE operations using it.
+coordinator.allowHandle(address(account), handle, address(token));
+
+// The handle can now be cast and forwarded to e.g. confidentialTransferFrom
+token.confidentialTransferFrom(address(account), recipient, euint64.wrap(handle));
+```
+
+All FHE types are supported via the `fheType` parameter: `FheType.Bool`, `FheType.Uint8`, `FheType.Uint16`, `FheType.Uint32`, `FheType.Uint64`, `FheType.Uint128`, `FheType.Uint256`, and `FheType.Address`.
+
+### Granting handle access to a signer's EOA
+
+Because the FHE ACL does not support smart contracts as decryption recipients, a signer can call `allowHandle` to grant their own EOA the ability to decrypt a value:
+
+```solidity
+// Option 1: msg.sender is the account itself or a registered signer EOA (no signature needed)
+coordinator.allowHandle(address(account), handle, signerEOA);
+
+// Option 2: an EOA signer provides an EIP-712 signature
+bytes memory signerBytes = abi.encodePacked(signerAddress); // 20 bytes
+bytes memory sig = ...; // EIP-712 signature over AllowHandle(account, handle, to)
+coordinator.allowHandle(address(account), signerBytes, sig, handle, signerEOA);
+
+// Option 3: an ERC-7913 signer (e.g. P256) provides its key and a signature
+bytes memory signerBytes = abi.encodePacked(verifierAddress, publicKey);
+bytes memory sig = ...; // signature from the ERC-7913 verifier
+coordinator.allowHandle(address(account), signerBytes, sig, handle, signerEOA);
+```
+
+For options 2 and 3, the signed digest follows the same EIP-712 structure anchored to the account's own domain:
+
+```
+AllowHandle(address account, bytes32 handle, address to)
+```
+
+Once `allowHandle` is called, `signerEOA` can request off-chain decryption of the FHE value through the Zama gateway.
+
+
+ ACL grants made via `allowHandle` are **permanent and irrevocable**. A signer removed from the multisig retains the ability to decrypt any handle they were previously allowed. To remove a former signer's access, re-encrypt the sensitive values using fresh FHE inputs and grant access only to current signers.
+
+
+### Batching encrypted value creation and handle delegation
+
+`fromExternal` and `allowHandle` can be combined into a single UserOperation to register an encrypted value and immediately delegate decryption rights to a signer's EOA in one atomic step:
+
+```solidity
+import {FheType} from "@fhevm/solidity/lib/FheType.sol";
+
+// Prepare off-chain
+bytes32 encryptedAmount = ...; // handle from input.encrypt()
+bytes memory inputProof = ...; // proof from input.encrypt()
+
+// Batched on-chain calls (e.g. via account.execute with CALLTYPE_BATCH in a UserOperation)
+bytes32 handle = coordinator.fromExternal(address(account), encryptedAmount, inputProof, FheType.Uint64);
+coordinator.allowHandle(address(account), handle, signerEOA);
+```
+
+`fromExternal` is open to any caller and it does not check `msg.sender` and will register the encrypted value for any `account` address. This means the party building the UserOperation does not need to be the account itself just to submit the encrypted input.
+
+`allowHandle` is different: it checks that the caller is actually authorized to act for the account. Concretely, when using the `allowHandle(address,bytes32,address)` function in the batch, `msg.sender` must be either the account itself or an EOA that is registered as a signer in the account's multisig. Otherwise, the `allowHandle(address,bytes,bytes,bytes32,adddress)` overload must be used instead, carrying a pre-signed EIP-712 authorization from a registered signer.
+
+In practice, the most common batching patterns are:
+
+- **A signer that is itself a smart account** calls both functions in a single transaction. Since `msg.sender` is a registered signer, the 3-parameter overload is accepted directly.
+- **A signer EOA using EIP-7702 delegation**. A bare EOA cannot atomically issue two external calls, but if the signer EOA has delegated its code to a batching contract via [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), the delegation allows it to batch both calls in one transaction. `msg.sender` remains the EOA address, so the 3-parameter overload is still accepted.
+- **A third-party relayer submits the batch**. The `fromExternal` call goes through freely, but the `allowHandle` call must use the 5-parameter overload with an EIP-712 signature obtained off-chain from a registered signer before the batch is submitted.
+
+### Batching token ACL grant with confidential transfer
+
+When executing a confidential transfer, the token contract needs ACL access to the handle (see [Creating an encrypted value](#creating-an-encrypted-value)). This grant can be batched with the transfer in the same UserOperation for convenience:
+
+```solidity
+// Executed by the account itself via account.execute with CALLTYPE_BATCH in a UserOperation:
+// 1. Grant the token contract ACL access to the handle
+coordinator.allowHandle(address(account), handle, address(token));
+// 2. Execute the confidential transfer
+token.confidentialTransfer(recipient, euint64.wrap(handle));
+```
diff --git a/content/accounts/contracts/session-keys.mdx b/content/accounts/contracts/session-keys.mdx
new file mode 100644
index 00000000..e74a61b3
--- /dev/null
+++ b/content/accounts/contracts/session-keys.mdx
@@ -0,0 +1,285 @@
+---
+title: Session Keys
+description: Scoped, time-limited signing authority for dapps, games, and automated services
+---
+
+Session keys grant a temporary, scoped signing authority to an additional key so that a dapp, game engine, or automated service can act on behalf of an account without exposing the owner's primary key. Rather than implementing a single, opinionated session-key contract, OpenZeppelin Accounts decomposes the problem into two clearly separated concerns.
+
+**What every session key needs:**
+
+- A configurable **validity range**: the key is only active between two timepoints.
+- **Policy enforcement**: the key must be constrained to a specific set of actions, otherwise a compromised session key would have the same power as the owner.
+- **Arbitrary signature schemes**: session keys may be EOAs (secp256k1), P256 hardware keys, WebAuthn credentials, or any other key type supported by [ERC-7913](/accounts/contracts/erc7913).
+
+**Why a unified design is hard:**
+
+Policy enforcement is use-case specific. A gaming session may need only a timer; a DeFi integration may need a spending-limit tracker that renews on a schedule; a selector-whitelist module rejects calls to functions not on an allow-list. Each policy requires its own storage and logic, and there is no one-size-fits-all implementation.
+
+This is resolved by splitting the responsibility: `ERC7579SessionKeys` handles the generic concerns (key registry + validity) while concrete extensions provide their own policy logic as ERC-7579 modules.
+
+## Architecture
+
+```
+ERC7579SessionKeys (abstract)
+ β
+ βββ ERC7579SessionKeysValidator (abstract β adds ERC-4337 / ERC-1271 validation)
+ β β
+ β βββ YourPolicyModule (concrete β add policy enforcement here)
+ β
+ βββ YourCustomModule (can also mix in ERC7579Executor for execution constraints)
+```
+
+### `ERC7579SessionKeys`
+
+`ERC7579SessionKeys` is an abstract ERC-7579 module that manages a per-account registry of [ERC-7913](/accounts/contracts/erc7913) session keys and their validity ranges. It deliberately does **not** implement any policy: a concrete subclass must decide what a valid key is allowed to do.
+
+Key properties:
+
+- **ERC-7913 keys**: any `(verifier, key)` pair is accepted: EOA addresses, P256 hardware keys, WebAuthn credentials, ZKEmail addresses, etc.
+- **Validity ranges**: each key has a `from` and an `until` timepoint. Passing `0` for `from` resolves to the current timestamp; passing `0` for `until` stores `type(uint48).max` (no expiry).
+- **ERC-6372 clock**: defaults to `block.timestamp`. Override `clock()` and `CLOCK_MODE()` to use block numbers instead.
+- **Cheap uninstall**: rather than iterating over all registered keys on uninstall, the module increments an installation nonce. All previous key data becomes unreachable instantly. Storage from old installations remains but is never accessed again.
+
+```solidity
+// Key registration state accessors
+bool registered = module.isKey(account, key); // is the key in the registry?
+bool active = module.isValid(account, key); // registered AND within validity range?
+(uint48 from, uint48 until) = module.validity(account, key);
+bytes[] memory allKeys = module.keys(account, 0, type(uint256).max);
+
+// Lifecycle (called by the account)
+module.addKeys(newKeys, starts, ends);
+module.removeKeys(oldKeys);
+```
+
+### `ERC7579SessionKeysValidator`
+
+`ERC7579SessionKeysValidator` extends `ERC7579SessionKeys` with ERC-4337 user-operation validation and ERC-1271 message signing. It is still **abstract**: it does not call `_validateSessionKey` unconditionallyβconcrete subclasses inherit the full validation plumbing and add their own policy gate before or after the key check.
+
+Key behaviors:
+
+- **User-operation validation**: decodes the session key from `userOp.signature`, reads its `(from, until)` range, and packs both the signature validity bit and the time range into a single `validationData` word using `ERC4337Utils.packValidationData`. The EntryPoint enforces the time range natively without requiring an explicit on-chain timestamp check.
+- **ERC-1271 validation**: the EntryPoint is not involved in off-chain message signing, so validity is checked manually: if the clock is outside `[from, until]`, the function returns `0xffffffff` immediately.
+
+## Implementing Custom Policies
+
+Because `ERC7579SessionKeysValidator` is abstract, the developer is responsible for providing the policy logic. The simplest concrete implementationβwhich grants full account access to any registered key within its validity windowβlooks like:
+
+```solidity
+import {ERC7579SessionKeysValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579SessionKeysValidator.sol";
+
+contract UnrestrictedSessionKeyValidator is ERC7579SessionKeysValidator {
+ // No additional policy: any registered + valid key can sign anything.
+ // _rawERC7579Validation is already implemented in ERC7579SessionKeysValidator.
+}
+```
+
+
+ Without additional restrictions, any registered session key within its validity range can validate any user operation
+ and sign any ERC-1271 message on behalf of the account. Always add policy enforcement for production use.
+
+
+A more realistic policy overrides `_validateUserOp` to inspect `userOp.callData` before delegating to the parent. For example, a selector-whitelist policy:
+
+```solidity
+import {ERC7579SessionKeysValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579SessionKeysValidator.sol";
+import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
+
+contract SelectorWhitelistValidator is ERC7579SessionKeysValidator {
+ // account => key id => selector => allowed
+ mapping(address => mapping(bytes32 => mapping(bytes4 => bool))) private _allowed;
+
+ function allowSelector(bytes calldata key, bytes4 selector) external {
+ _allowed[msg.sender][keccak256(key)][selector] = true;
+ }
+
+ function _validateUserOp(
+ PackedUserOperation calldata userOp,
+ bytes32 userOpHash
+ ) internal override returns (uint256) {
+ // Enforce the selector whitelist by inspecting userOp.callData before
+ // delegating to the parent for cryptographic verification and validity packing.
+ // ...
+ }
+}
+```
+
+## Validator and Executor Combined
+
+Some policies cannot be enforced at the ERC-4337 validation phase because the phase is stateless and cannot track evolving state like remaining spend allowance. In these cases the module can be **both a validator and an executor**: the session key validates a user operation that calls a specific function on the module itself, and that function uses `executeFromExecutor` on the account. This pattern allows execution-time constraintsβsuch as decrementing a spend counter or reverting if a call target is not whitelistedβto be enforced outside the validation phase.
+
+```
+UserOp (signed by session key)
+ ββ callData β SpendLimitModule.transferFrom(account, key, ..., amount)
+ β
+ ββ checks & decrements per-key remaining limit
+ ββ account.executeFromExecutor(target, value, data)
+```
+
+`ERC7579NativeSpender` and `ERC7579ERC20Spender` are the canonical implementations of this pattern. They:
+
+- Install under BOTH `MODULE_TYPE_VALIDATOR` and `MODULE_TYPE_EXECUTOR`, in that order.
+- Override `_validateUserOp` to require that `userOp.callData` is exactly `IERC7579Execution.execute(SINGLE, encodeSingle(address(this), 0, transferFromOrBatchTransferFrom(...)))`.
+- Disable ERC-1271 signing (`isValidSignatureWithSender` returns `0xffffffff`) so a spending session key cannot be repurposed to sign arbitrary off-chain digests.
+
+See [Spending Limits](/accounts/contracts/executors/spending-limits) for the full install/use guide. This is the recommended pattern for any policy with mutable state (spending limits, rate limits, per-session counters).
+
+## Installing Session Keys
+
+### At Account Deployment
+
+Pass the session key module as one of the initial modules when deploying through `OpenAccountFactory`:
+
+```solidity
+bytes[] memory keys = new bytes[](1);
+keys[0] = abi.encodePacked(sessionKeyAddress); // EOA session key
+
+uint48[] memory starts = new uint48[](1);
+uint48[] memory ends = new uint48[](1);
+starts[0] = 0; // 0 β current timestamp
+ends[0] = uint48(block.timestamp + 7 days);
+
+bytes memory initData = abi.encode(keys, starts, ends);
+
+uint256[] memory moduleTypeIds = new uint256[](1);
+moduleTypeIds[0] = MODULE_TYPE_VALIDATOR;
+address[] memory modules = new address[](1);
+modules[0] = address(sessionKeyValidator);
+bytes[] memory initDatas = new bytes[](1);
+initDatas[0] = initData;
+
+address account = factory.createAndInitialize(moduleTypeIds, modules, initDatas, mode, callData, salt);
+```
+
+### On an Existing Account (standard two-step)
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+
+// Step 1 β install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(sessionKeyValidator), initData);
+
+// Step 2 β use the session key in a subsequent UserOperation
+```
+
+### Atomic Installation and First Use with ERC-8043
+
+[ERC-8043](/accounts/contracts/erc8043) collapses module installation and the first use of the new module into a single user operation. This is particularly valuable for session keys because the dapp can install its session key and immediately perform the first action without requiring a separate owner-signed transaction.
+
+The owner signs an EIP-712 `InitModules` struct authorizing the installation, and the session key signs the user operation itself. Both authorizations travel in the same `UserOperation`:
+
+```solidity
+// 1. Owner signs the EIP-712 installation authorization
+bytes32[] memory initDataHashes = new bytes32[](1);
+initDataHashes[0] = keccak256(initData);
+
+bytes32 structHash = keccak256(abi.encode(
+ INIT_MODULES_TYPEHASH,
+ keccak256(abi.encodePacked(moduleTypeIds)),
+ keccak256(abi.encodePacked(modules)),
+ keccak256(abi.encodePacked(initDataHashes)),
+ userOpNonce
+));
+bytes32 digest = MessageHashUtils.toTypedDataHash(account.domainSeparator(), structHash);
+bytes memory installationSignature = ownerSign(digest); // prefixed with current validator address
+
+// 2. Encode initCode with the ERC-8043 magic value
+bytes memory initCode = abi.encodePacked(
+ bytes4(0x80438043),
+ abi.encode(moduleTypeIds, modules, initDatas, installationSignature)
+);
+
+// 3. Session key signs the userOpHash (routing nonce through the new validator)
+uint256 nonce = uint256(uint160(address(sessionKeyValidator))) << 96;
+PackedUserOperation memory userOp = PackedUserOperation({
+ sender: address(account),
+ nonce: nonce,
+ initCode: initCode,
+ callData: callData, // the action to execute in the same operation
+ // ... gas fields ...
+ signature: sessionKeySign(userOpHash) // abi.encode(key, cryptoSignature)
+});
+```
+
+See [ERC-8043](/accounts/contracts/erc8043) for the complete encoding details and the full step-by-step guide.
+
+## Managing Session Keys
+
+After installation, the account can add or remove keys at any time by calling the module directly (only the account itself may call these functions):
+
+```solidity
+// Add new keys
+bytes[] memory newKeys = new bytes[](2);
+newKeys[0] = abi.encodePacked(eoa); // EOA
+newKeys[1] = abi.encodePacked(p256Verifier, qx, qy); // P256 hardware key
+
+uint48[] memory starts = new uint48[](2);
+uint48[] memory ends = new uint48[](2);
+starts[0] = 0; // active immediately
+ends[0] = uint48(block.timestamp + 1 days);
+starts[1] = uint48(block.timestamp + 1 hours); // delayed activation
+ends[1] = 0; // no expiry
+
+sessionKeyValidator.addKeys(newKeys, starts, ends);
+
+// Remove keys
+bytes[] memory oldKeys = new bytes[](1);
+oldKeys[0] = abi.encodePacked(eoa);
+sessionKeyValidator.removeKeys(oldKeys);
+```
+
+
+ Setting `starts[i] = 0` activates the key from the current timepoint. Setting `ends[i] = 0` stores
+ `type(uint48).max`, meaning the key never expires.
+
+
+## Querying Session Key State
+
+```solidity
+// Is the key registered (regardless of validity range)?
+bool registered = sessionKeyValidator.isKey(account, key);
+
+// Is the key registered AND currently within its validity range?
+bool active = sessionKeyValidator.isValid(account, key);
+
+// Read the exact validity range
+(uint48 from, uint48 until) = sessionKeyValidator.validity(account, key);
+
+// Enumerate all keys (paginated)
+bytes[] memory allKeys = sessionKeyValidator.keys(account, 0, type(uint256).max);
+
+// Current installation nonce (incremented on each uninstall)
+uint256 nonce = sessionKeyValidator.installation(account);
+```
+
+## Signature Format
+
+When signing a user operation or ERC-1271 message with a session key, the `signature` field must be ABI-encoded as:
+
+```solidity
+bytes memory signature = abi.encode(
+ bytes key, // ERC-7913 key bytes (verifier || key)
+ bytes keySignature // cryptographic signature produced by that key
+);
+```
+
+For an EOA session key this looks like:
+
+```solidity
+(uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKeyPrivateKey, userOpHash);
+bytes memory keySignature = abi.encodePacked(r, s, v);
+bytes memory signature = abi.encode(
+ abi.encodePacked(sessionKeyAddress), // 20-byte EOA key
+ keySignature
+);
+```
+
+For a P256 session key (e.g. a hardware security key or mobile enclave):
+
+```solidity
+bytes memory key = abi.encodePacked(p256Verifier, qx, qy);
+bytes memory keySignature = ...; // P256 signature bytes
+bytes memory signature = abi.encode(key, keySignature);
+```
+
+The session key is identified and verified through the [ERC-7913](/accounts/contracts/erc7913) `SignatureChecker` path, so any key type supported by ERC-7913 works out of the box.
diff --git a/content/accounts/contracts/signers.mdx b/content/accounts/contracts/signers.mdx
new file mode 100644
index 00000000..4bf6309c
--- /dev/null
+++ b/content/accounts/contracts/signers.mdx
@@ -0,0 +1,28 @@
+---
+title: Signers
+description: Abstract modules for single-signer storage and signature validation primitives
+---
+
+These abstract modules handle signer storage, the installation lifecycle, and core signature validation primitives. See the [Validators](/accounts/contracts/validators) page for the ready-to-use validator implementations built on top of them.
+
+### ERC7579ECDSA
+
+Manages a single EOA signer per account and exposes `_validateECDSA` for downstream validation logic. Installation data encodes the signer as a packed 20-byte address.
+
+```solidity
+address currentSigner = module.signer(account);
+module.setSigner(account, newSigner); // must be called by the account
+```
+
+### ERC7579P256
+
+Manages a single P256 (secp256r1) public key per account as `(qx, qy)` coordinates and exposes `_validateP256`. Installation data encodes the key as 64 packed bytes.
+
+```solidity
+(bytes32 qx, bytes32 qy) = module.signer(account);
+module.setSigner(account, newQx, newQy); // must be called by the account
+```
+
+### ERC7579WebAuthn
+
+Extends `ERC7579P256` with WebAuthn authentication by adding `_validateWebAuthn`. Signatures must be ABI-encoded [`WebAuthn.WebAuthnAuth`](/contracts/5.x/api/utils/cryptography#WebAuthn) structs. Signer storage and management are inherited from `ERC7579P256`.
diff --git a/content/accounts/contracts/validators.mdx b/content/accounts/contracts/validators.mdx
new file mode 100644
index 00000000..1900e869
--- /dev/null
+++ b/content/accounts/contracts/validators.mdx
@@ -0,0 +1,252 @@
+---
+title: Validators
+description: Signature validation modules for ERC-7579 accounts
+---
+
+Validator modules determine whether a transaction should be executed by validating signatures. They implement the `IERC7579Validator` interface and are installed as `MODULE_TYPE_VALIDATOR` modules on ERC-7579 accounts.
+
+## Base Validator
+
+**`ERC7579Validator`** is the abstract base contract for all validator modules. It provides:
+
+- Integration with ERC-4337 UserOperations via `validateUserOp`
+- ERC-1271 signature verification via `isValidSignatureWithSender`
+- A hook for custom validation logic via `_rawERC7579Validation`
+
+Developers extending this base contract must implement:
+
+- `onInstall(bytes calldata data)`: Initialize the validator with account-specific configuration
+- `onUninstall(bytes calldata data)`: Clean up validator state
+- `_rawERC7579Validation(address account, bytes32 hash, bytes calldata signature)`: Core signature validation logic
+
+The single-signer validators below (`ERC7579ECDSAValidator`, `ERC7579P256Validator`, `ERC7579WebAuthnValidator`) are thin wrappers that compose `ERC7579Validator` with the corresponding [signer base module](/accounts/contracts/signers). Signer storage and management logic live in those base modules.
+
+## Available Validators
+
+OpenZeppelin Accounts provides several validator implementations:
+
+### ERC7579ECDSAValidator
+
+Validates ECDSA signatures from Ethereum addresses (EOAs). This is the most common validator for traditional Ethereum wallets.
+
+**Key Features:**
+
+- Single signer per account (EOA address)
+- Standard ECDSA signature validation
+- Simple installation with just a 20-byte address
+
+
+This is an abstract contract. You need to create a concrete implementation:
+
+```solidity
+contract MyECDSAValidator is ERC7579ECDSAValidator {}
+```
+
+
+
+The ECDSA validator can be installed as follows:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579ECDSAValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579ECDSAValidator.sol";
+
+// Create a concrete implementation
+contract MyECDSAValidator is ERC7579ECDSAValidator {}
+
+// Deploy the validator (singleton, can be reused)
+MyECDSAValidator validator = new MyECDSAValidator();
+
+// Prepare initialization data (20-byte address)
+bytes memory initData = abi.encodePacked(signerAddress);
+
+// Install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(validator), initData);
+```
+
+After installation, you can update the signer:
+
+```solidity
+// Update the signer (must be called by the account)
+validator.setSigner(account, newSignerAddress);
+
+// Query the current signer
+address currentSigner = validator.signer(account);
+```
+
+### ERC7579P256Validator
+
+Validates P256 (secp256r1) signatures, which are commonly used by mobile devices and hardware security modules. This enables signing with keys that don't have Ethereum addresses.
+
+**Key Features:**
+
+- Single P256 public key per account (qx, qy coordinates)
+- Native support for mobile device signing
+- Hardware security module compatibility
+
+
+This is an abstract contract. You need to create a concrete implementation:
+
+```solidity
+contract MyP256Validator is ERC7579P256Validator {}
+```
+
+
+
+The P256 validator can be installed as follows:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579P256Validator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579P256Validator.sol";
+
+// Create a concrete implementation
+contract MyP256Validator is ERC7579P256Validator {}
+
+// Deploy the validator (singleton, can be reused)
+MyP256Validator validator = new MyP256Validator();
+
+// Prepare P256 public key coordinates (64 bytes: qx + qy)
+bytes32 qx = /* P256 public key x-coordinate */;
+bytes32 qy = /* P256 public key y-coordinate */;
+bytes memory initData = abi.encodePacked(qx, qy);
+
+// Install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(validator), initData);
+```
+
+After installation, you can update the signer:
+
+```solidity
+// Update the P256 signer (must be called by the account)
+validator.setSigner(account, newQx, newQy);
+
+// Query the current signer
+(bytes32 qx, bytes32 qy) = validator.signer(account);
+```
+
+### ERC7579WebAuthnValidator
+
+Validates WebAuthn/FIDO2 signatures, providing passwordless authentication support. This validator extends [`ERC7579WebAuthn`](/accounts/contracts/signers#erc7579webauthn) and `ERC7579P256Validator`.
+
+**Key Features:**
+
+- WebAuthn/FIDO2 authentication support
+- Passwordless login capabilities
+- Compatible with hardware security keys (YubiKey, etc.)
+
+
+This is an abstract contract. You need to create a concrete implementation:
+
+```solidity
+contract MyWebAuthnValidator is ERC7579WebAuthnValidator {}
+```
+
+
+
+The WebAuthn validator can be installed as follows:
+
+```solidity
+import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
+import {ERC7579WebAuthnValidator} from "@openzeppelin-accounts/contracts/modules/validators/ERC7579WebAuthnValidator.sol";
+
+// Create a concrete implementation
+contract MyWebAuthnValidator is ERC7579WebAuthnValidator {}
+
+// Deploy the validator (singleton, can be reused)
+MyWebAuthnValidator validator = new MyWebAuthnValidator();
+
+// Prepare P256 public key coordinates (same as P256 validator)
+bytes32 qx = /* WebAuthn credential public key x-coordinate */;
+bytes32 qy = /* WebAuthn credential public key y-coordinate */;
+bytes memory initData = abi.encodePacked(qx, qy);
+
+// Install the module
+account.installModule(MODULE_TYPE_VALIDATOR, address(validator), initData);
+```
+
+**Signature Format:**
+
+Signatures must be ABI-encoded `WebAuthn.WebAuthnAuth` structs. See [`WebAuthn`](/contracts/5.x/api/utils/cryptography#WebAuthn) for the struct definition.
+
+### ERC7579MultisigValidator
+
+A validator that requires multiple signatures before executing operations. See the [Multisig documentation](/accounts/contracts/multisig) for more details.
+
+### ERC7579MultisigWeightedValidator
+
+A validator that supports weighted voting with multiple signers. See the [Multisig documentation](/accounts/contracts/multisig) for more details.
+
+### ERC7579MultisigConfidentialValidator
+
+A validator combining confidential multisig (FHE-encrypted signer addresses and threshold) with ERC-7579 validation. See the [Multisig documentation](/accounts/contracts/multisig) for installation and usage details.
+
+### ERC7579MultisigWeightedConfidentialValidator
+
+A validator combining confidential weighted multisig (FHE-encrypted signer addresses, weights, and threshold) with ERC-7579 validation. See the [Multisig documentation](/accounts/contracts/multisig) for installation and usage details.
+
+### Considerations
+
+When an account has multiple validators installed:
+
+- **UserOperations**: The validator is selected via the nonce (or account-specific mechanism) (see [validator selection](#validator-selection))
+- **ERC-1271**: The validator is selected via the first 20 bytes of the signature
+- **Validation**: Only the selected validator is called, not all installed validators
+
+This design allows accounts to support multiple signature types while explicitly selecting which validator to use for each operation.
+
+## Validator Selection
+
+ERC-7579 does not standardize how accounts select which validator to use for a given operation. Different account implementations use different mechanisms, with the most common approach being to encode the validator address in the UserOperation's `nonce` field.
+
+
+ When using the SDK's `toOpenAccount`, pass the `validator` address once at account creation time. The nonce is
+ constructed automatically for every user operation, so you never need to encode the validator address manually.
+
+
+#### OpenZeppelin's AccountERC7579
+
+[OpenZeppelin's AccountERC7579](/contracts/5.x/api/account#AccountERC7579) extracts the validator address from the first 20 bytes of the nonce:
+
+```solidity
+// Nonce format: | |
+address validator = address(bytes32(userOp.nonce).extract_32_20(0));
+```
+
+To construct a nonce for multi-validator setup:
+
+```solidity
+// You have multiple validators installed
+address ecdsaValidator = address(0x1234...);
+address p256Validator = address(0x5678...);
+address multisigValidator = address(0x9abc...);
+
+// To use the ECDSA validator, construct the nonce:
+// Nonce format: | |
+uint192 key = 0; // Optional: sequence key for parallel nonces (allows multiple nonce sequences)
+uint64 sequence = 1; // The actual nonce sequence
+
+// Pack the nonce: validator address in bytes 0-19, key in bytes 20-23, sequence in bytes 24-31
+bytes32 nonce = bytes32(
+ uint256(uint160(ecdsaValidator)) << 96 | // validator at bytes 0-19 (left-shift by 96 bits)
+ uint256(key) << 64 | // key at bytes 20-23 (left-shift by 64 bits)
+ uint256(sequence) // sequence at bytes 24-31 (no shift needed)
+);
+
+// Use this nonce in your UserOperation
+userOp.nonce = nonce;
+
+// To use a different validator (e.g., P256), just change the validator address:
+nonce = bytes32(
+ uint256(uint160(p256Validator)) << 96 |
+ uint256(key) << 64 |
+ uint256(sequence)
+);
+```
+
+#### Kernel
+
+[Kernel](https://github.com/zerodevapp/kernel) by ZeroDev also uses the nonce to select validators, but uses a different offset. Kernel extracts the validator from `userOp.nonce[2:22]` (bytes 2-21), allowing the first 2 bytes to encode validation mode information.
+
+
+ When working with multiple account implementations, check each account's documentation for its specific validator
+ selection mechanism, as they may be incompatible.
+
diff --git a/content/accounts/index.mdx b/content/accounts/index.mdx
new file mode 100644
index 00000000..6285717b
--- /dev/null
+++ b/content/accounts/index.mdx
@@ -0,0 +1,43 @@
+---
+title: Introduction
+description: Standardized modular tooling and SDK for ERC-7579 smart accounts
+---
+
+The **OpenZeppelin Accounts** is a comprehensive collection of contracts, modules, and SDK utilities for building and working with [ERC-7579](https://eips.ethereum.org/EIP-7579) compliant modular smart accounts.
+
+## Overview
+
+OpenZeppelin Accounts consists of three main packages:
+
+
+
+
+
+
+## Getting Started
+
+Ready to build with modular smart accounts? Check out our guides:
+
+
+
+
+
+
+## Why ERC-7579?
+
+ERC-7579 (Minimal Modular Smart Accounts) has emerged as the leading standard for modular smart account architecture. OpenZeppelin Accounts focuses exclusively on ERC-7579 because:
+
+- **Wide Adoption**: The modular standard is well-adopted across the ecosystem
+- **Module-Based Architecture**: Install functionality as modules rather than through inheritance
+- **No Redeployment Required**: Add new features to existing accounts without redeploying
+- **Wallet Provider Friendly**: Most wallet providers support module installation without requiring account redeployment
+
+Unlike inheritance-based approaches that require redeploying the entire account contract, ERC-7579's modular design allows you to install, update, and remove functionality dynamically, making it the ideal choice for production smart account implementations.
diff --git a/content/accounts/sdk/actions.mdx b/content/accounts/sdk/actions.mdx
new file mode 100644
index 00000000..774cb92f
--- /dev/null
+++ b/content/accounts/sdk/actions.mdx
@@ -0,0 +1,211 @@
+---
+title: ERC-7579 Actions
+description: Bundler client extension for managing ERC-7579 modules and executing user operations
+---
+
+`erc7579Actions` is a Viem client extension that adds a set of typed convenience methods for interacting with any ERC-7579 smart account. It is built on top of the core utilities in `erc7579.utils` and wraps Viem's `sendUserOperation` and `readContract` actions.
+
+## Setup
+
+Extend any Viem bundler client with `erc7579Actions()` after creating it:
+
+```typescript
+import { createBundlerClient } from 'viem/account-abstraction';
+import { http } from 'viem';
+import { erc7579Actions } from '@openzeppelin-accounts/sdk';
+
+const bundlerClient = createBundlerClient({
+ account,
+ client,
+ transport: http('https://bundler.example.com'),
+}).extend(erc7579Actions());
+```
+
+All actions resolve the smart account from either the client's attached `account` or an explicit `account` field in the parameters object.
+
+## Actions
+
+### accountId
+
+Reads the ERC-7579 account identifier string from the on-chain contract. Useful for verifying the account implementation and version before interacting with it.
+
+```typescript
+const id = await bundlerClient.accountId();
+// '@openzeppelin/contracts.AccountERC7579.v1.0.0'
+```
+
+### supportsModule
+
+Returns `true` if the account declares support for a given module type.
+
+```typescript
+const supported = await bundlerClient.supportsModule({ type: 'validator' });
+```
+
+Valid values for `type` are `'validator'`, `'executor'`, `'fallback'`, `'hook'`, or their numeric equivalents `1`β`4`.
+
+### supportsExecutionMode
+
+Returns `true` if the account supports a given execution mode. Execution modes combine a call type and an execution type.
+
+```typescript
+const supported = await bundlerClient.supportsExecutionMode({
+ callType: 'batchcall', // 'call' | 'batchcall' | 'delegatecall'
+ execType: 'try', // 'default' | 'try'
+});
+```
+
+An optional `selector` (4 bytes) and `payload` (22 bytes) can also be specified for accounts that expose fallback handlers gated by function selector.
+
+### isModuleInstalled
+
+Returns `true` if a specific module address is installed under a given type.
+
+```typescript
+const installed = await bundlerClient.isModuleInstalled({
+ type: 'validator',
+ address: '0x',
+ additionalContext: '0x', // optional, passed to the account's isModuleInstalled check
+});
+```
+
+### installModules
+
+Sends a user operation that installs one or more modules. When multiple modules are provided they are automatically batched into a single `CALLTYPE_BATCH` execution so the account pays for only one user operation.
+
+```typescript
+import { encodePacked } from 'viem';
+
+const hash = await bundlerClient.installModules({
+ modules: [
+ {
+ type: 'validator',
+ address: '0x',
+ initData: encodePacked(['address'], [signer.address]),
+ },
+ ],
+});
+
+await bundlerClient.waitForUserOperationReceipt({ hash });
+```
+
+The `initData` format is module-specific. See the [Validators](/accounts/contracts/validators) and [Executors](/accounts/contracts/executors) pages for the expected encoding of each module.
+
+An optional `execType` field (`'default'` | `'try'`) controls whether individual installations revert the entire batch or are silently skipped on failure.
+
+### uninstallModules
+
+Sends a user operation that uninstalls one or more modules. Like `installModules`, multiple modules are batched automatically.
+
+```typescript
+const hash = await bundlerClient.uninstallModules({
+ modules: [
+ {
+ type: 'validator',
+ address: '0x',
+ deInitData: '0x', // module-specific cleanup data, defaults to '0x'
+ },
+ ],
+});
+
+await bundlerClient.waitForUserOperationReceipt({ hash });
+```
+
+
+ Uninstalling the last validator module from an account will leave it without any authorization mechanism, making it
+ permanently unusable. Always ensure at least one validator remains installed.
+
+
+### execute
+
+Sends a user operation that executes one or more arbitrary calls through the account. The call type is selected automatically: a single entry uses `CALLTYPE_SINGLE`, two or more entries use `CALLTYPE_BATCH`.
+
+```typescript
+import { parseEther } from 'viem';
+
+// Single call
+const hash = await bundlerClient.execute({
+ calls: [{ to: '0x', data: '0x', value: parseEther('1') }],
+});
+
+// Batch call
+const hash = await bundlerClient.execute({
+ calls: [
+ { to: '0x', data: '0x', value: parseEther('1') },
+ { to: '0x', data: '0x', value: parseEther('2') },
+ ],
+});
+
+await bundlerClient.waitForUserOperationReceipt({ hash });
+```
+
+Pass `execType: 'try'` to continue processing remaining calls even if one fails:
+
+```typescript
+const hash = await bundlerClient.execute({
+ calls: [/* ... */],
+ execType: 'try',
+});
+```
+
+## ERC-7579 Utilities
+
+The SDK also exports the low-level encoding helpers used internally by `erc7579Actions` and `toERC7579Account`. These are useful when you need to build execution calldata manually.
+
+### Execution Mode
+
+ERC-7579 execution modes are 32-byte packed values that combine a call type, an execution type, an optional selector, and optional payload.
+
+```typescript
+import { encodeMode, decodeMode } from '@openzeppelin-accounts/sdk';
+
+const mode = encodeMode({
+ callType: 'batchcall', // 'call' | 'batchcall' | 'delegatecall'
+ execType: 'try', // 'default' | 'try'
+});
+
+const decoded = decodeMode(mode);
+// { callType: '0x01', execType: '0x01', selector: '0x00000000', payload: '0x...' }
+```
+
+
+Omitted fields default to `CALLTYPE_CALL`, `EXECTYPE_DEFAULT`, a zero selector, and a zero payload.
+
+
+### Call Encoding / Decoding
+
+`encodeCalls` wraps an array of Viem `Call` objects into ERC-7579 `execute` calldata, choosing single or batch encoding automatically:
+
+```typescript
+import { encodeCalls, decodeCalls } from '@openzeppelin-accounts/sdk';
+
+const calldata = encodeCalls([
+ { to: '0x', value: 0n, data: '0x1234' },
+]);
+
+const calls = await decodeCalls(calldata);
+```
+
+Lower-level helpers for each encoding are also exported:
+
+| Function | Description |
+|---|---|
+| `encodeSingle` / `decodeSingle` | Pack/unpack a single `(address, uint256, bytes)` execution |
+| `encodeBatch` / `decodeBatch` | ABI-encode/decode an array of `(address, uint256, bytes)` executions |
+| `encodeDelegate` / `decodeDelegate` | Pack/unpack a `delegatecall` execution (no value field) |
+
+### Module Type Identifiers
+
+`ModuleType` values can be passed as either string or numeric literals throughout the SDK:
+
+```typescript
+import { ModuleTypes, parseModuleTypeId } from '@openzeppelin-accounts/sdk';
+
+ModuleTypes.VALIDATOR // 1
+ModuleTypes.EXECUTOR // 2
+ModuleTypes.FALLBACK // 3
+ModuleTypes.HOOK // 4
+
+parseModuleTypeId('validator') // 1n
+parseModuleTypeId(2) // 2n
+```
diff --git a/content/accounts/sdk/index.mdx b/content/accounts/sdk/index.mdx
new file mode 100644
index 00000000..8fd8022f
--- /dev/null
+++ b/content/accounts/sdk/index.mdx
@@ -0,0 +1,129 @@
+---
+title: Overview
+description: TypeScript SDK for interacting with OpenZeppelin Accounts
+---
+
+The OpenZeppelin Accounts SDK is a TypeScript library that provides a typed, Viem-native interface for interacting with ERC-7579 smart accounts. It is the off-chain counterpart to the [contracts package](/accounts/contracts), which delivers ERC-7579 modules for smart account functionality.
+
+The SDK is built on top of [Viem's account abstraction primitives](https://viem.sh/account-abstraction/accounts/smart/toSmartAccount), extending them with ERC-7579-specific encoding, module management, and account lifecycle helpers. Because the contracts are ERC-7579 modules and can be installed on _any_ ERC-7579 compliant account, the SDK is designed to work both with the bundled `OpenAccount` contract and with third-party account implementations.
+
+## Installation
+
+```bash
+npm install @openzeppelin-accounts/sdk viem
+```
+
+## Account Setup
+
+The SDK exposes two ways to create a smart account client depending on whether you are using the `OpenAccount` contract or another ERC-7579 account.
+
+### toOpenAccount
+
+`toOpenAccount` is the primary entry point for users targeting the `OpenAccount` implementation provided by the contracts package. It wraps `toERC7579Account` with account-specific address derivation and factory interaction, and returns a Viem `SmartAccount` instance.
+
+```typescript
+import { toOpenAccount } from '@openzeppelin-accounts/sdk';
+```
+
+It accepts two mutually exclusive modes:
+
+#### Creating a new account via factory
+
+Pass a `factory` contract instance together with the modules you want to install during initialization. The account address is derived deterministically via CREATE2, so it can be known before deployment.
+
+```typescript
+import { http, createPublicClient, getContract, encodePacked } from 'viem';
+import { mainnet } from 'viem/chains';
+import { privateKeyToAccount } from 'viem/accounts';
+import { toOpenAccount } from '@openzeppelin-accounts/sdk';
+import OpenAccountFactoryABI from '@openzeppelin-accounts/contracts/out/OpenAccountFactory.sol/OpenAccountFactory.json' assert { type: 'json' };
+
+const client = createPublicClient({ chain: mainnet, transport: http() });
+const signer = privateKeyToAccount('0x...');
+
+const factory = getContract({
+ address: '0x9045eF209067701bE7b78AaC216C04Ffa6543330',
+ abi: OpenAccountFactoryABI.abi,
+ client,
+});
+
+const account = await toOpenAccount({
+ client,
+ signer,
+ validator: '0x',
+ factory,
+ moduleTypeIds: ['validator'],
+ modules: ['0x'],
+ initDatas: [encodePacked(['address'], [signer.address])],
+ getStubSignature: async () => '0x' + 'ff'.repeat(65),
+});
+```
+
+The `OpenAccountFactory` is deployed at the same deterministic address on all major EVM chains:
+
+```
+0x9045eF209067701bE7b78AaC216C04Ffa6543330
+```
+
+
+ The factory encodes the module list, init data, and salt into the CREATE2 salt, so every unique combination of modules
+ and initialization arguments produces a unique account address. Include the signer address in the `initData` to
+ prevent frontrunning.
+
+
+#### Using an existing deployed account
+
+If the account is already on-chain, pass its `address` directly. `toOpenAccount` validates that the contract at that address is an `OpenAccount` by reading its `accountId` string.
+
+```typescript
+const account = await toOpenAccount({
+ client,
+ signer,
+ validator: '0x',
+ address: '0x',
+ getStubSignature: async () => '0x' + 'ff'.repeat(65),
+});
+```
+
+### toERC7579Account
+
+For users who already have a different ERC-7579 account deployed (e.g. Kernel, Safe with ERC-7579 plugin, OpenZeppelin's `AccountERC7579`), `toERC7579Account` provides the same ERC-7579-aware signing and call encoding without any `OpenAccount`-specific logic.
+
+```typescript
+import { toERC7579Account } from '@openzeppelin-accounts/sdk';
+```
+
+You supply `getAddress` and `getFactoryArgs` yourself:
+
+```typescript
+const account = await toERC7579Account({
+ client,
+ signer,
+ async getAddress() {
+ return '0x';
+ },
+ async getFactoryArgs() {
+ return { factory: '0x...', factoryData: '0x...' };
+ },
+ getStubSignature: async () => '0x' + 'ff'.repeat(65),
+});
+```
+
+Both functions return a Viem [`SmartAccount`](https://viem.sh/account-abstraction/accounts/smart/toSmartAccount) and are compatible with Viem's `createBundlerClient`.
+
+### Signers
+
+Both `toOpenAccount` and `toERC7579Account` accept a `signer` argument that determines how user operations and messages are signed. Viem's `LocalAccount` (ECDSA) and `WebAuthnAccount` (passkey) are accepted directly:
+
+```typescript
+import { privateKeyToAccount } from 'viem/accounts';
+import { toWebAuthnAccount } from 'viem/account-abstraction';
+
+// ECDSA signer
+const ecdsaSigner = privateKeyToAccount('0x...');
+
+// WebAuthn signer (passkey)
+const webAuthnSigner = toWebAuthnAccount({ credential: { id: '...', publicKey: '0x...' } });
+```
+
+The SDK also exposes a narrow `Signer` interface and a set of composable wrappers (`toERC7913Signer`, `toCrossChainSigner`, `toMultisigner`) that produce the wire formats expected by the on-chain validators β ERC-7913 envelopes, ERC-7964 cross-chain envelopes, and the multisig `bytes[]` layout. See the [Signers](/accounts/sdk/signers) page for the full API.
diff --git a/content/accounts/sdk/open-account.mdx b/content/accounts/sdk/open-account.mdx
new file mode 100644
index 00000000..b05661ba
--- /dev/null
+++ b/content/accounts/sdk/open-account.mdx
@@ -0,0 +1,303 @@
+---
+title: toOpenAccount
+description: Detailed parameter reference for toOpenAccount
+---
+
+`toOpenAccount` creates a Viem `SmartAccount` instance configured for the `OpenAccount` contract. It is a thin wrapper around `toERC7579Account` that handles the account's address derivation, factory interaction, and on-chain identity validation.
+
+```typescript
+import { toOpenAccount } from '@openzeppelin-accounts/sdk';
+
+const account = await toOpenAccount({ client, signer, ...parameters });
+```
+
+The function accepts two mutually exclusive shapes of parameters depending on whether you are creating a new account or connecting to one that is already deployed.
+
+## Parameters
+
+### Shared parameters
+
+These parameters apply in both factory and existing-account modes.
+
+#### `client`
+
+A Viem public (or wallet) client connected to the target chain. Used to read on-chain state such as the entry point nonce and the account's `accountId` string.
+
+```typescript
+import { createPublicClient, http } from 'viem';
+import { mainnet } from 'viem/chains';
+
+const client = createPublicClient({ chain: mainnet, transport: http() });
+```
+
+#### `signer`
+
+The signing primitive used to authorize user operations, ERC-1271 signature requests, and typed data. The signer determines the on-chain validator that must be installed on the account.
+
+Two shapes are accepted:
+
+**ECDSA (`LocalAccount`)**
+
+```typescript
+import { privateKeyToAccount } from 'viem/accounts';
+
+const signer = privateKeyToAccount('0x');
+```
+
+The `address` property of the returned account must correspond to the signer address registered in the ECDSA validator module.
+
+**WebAuthn (`WebAuthnAccount`)**
+
+```typescript
+import { toWebAuthnAccount } from 'viem/account-abstraction';
+
+const signer = toWebAuthnAccount({
+ credential: { id: '', publicKey: '0x' },
+});
+```
+
+The public key coordinates must match those stored in the WebAuthn validator module's `initData`.
+
+
+ The `signer` field controls how signatures are _produced_ off-chain. The corresponding on-chain validator module
+ controls how they are _verified_. Both must be consistent: an ECDSA signer paired with a WebAuthn validator (or vice
+ versa) will produce signatures that the account cannot verify.
+
+
+#### `validator`
+
+The address of the ERC-7579 validator module that authorises this signer. This is a **required** parameter that drives two automatic behaviors inside `toOpenAccount`:
+
+- **ERC-1271 routing**: `signMessage` and `signTypedData` prepend `validator` (20 bytes) to the raw signature so that the account's `isValidSignature` can extract and route to the correct module. You never need to prepend the validator address manually.
+- **UserOp nonce routing**: `getNonce` encodes `validator` in the upper 20 bytes of the 24-byte nonce key, matching the `_extractUserOpValidator` logic in `AccountERC7579`. The remaining 4 bytes of the key are available as an optional parallel-sequence counter via `getNonce({ key })`.
+
+```typescript
+validator: '0x'
+```
+
+Pass the same address that is in the `modules` array when creating a new account, or the address of the already-installed validator module when connecting to an existing one.
+
+
+ The `validator` address must correspond to an installed module. Passing a wrong address will cause every user
+ operation and ERC-1271 check to fail on-chain, since the account will look for a module that is not installed.
+
+
+#### `getStubSignature`
+
+Returns a dummy signature used during gas estimation. Bundlers call `eth_estimateUserOperationGas` before the real signature is available; `getStubSignature` provides a placeholder that passes signature length checks without needing to be cryptographically valid.
+
+`toOpenAccount` uses this value as the raw UserOp signature, which the validator module receives directly (no validator-address prefix that only applies to ERC-1271 calls). The stub must have the same byte length as a real inner signature for the installed validator. Providing a stub that is too short or too long will produce incorrect gas estimates.
+
+**ECDSA (65 bytes)**
+
+```typescript
+getStubSignature: async () => '0x' + 'ff'.repeat(65)
+```
+
+**WebAuthn**
+
+The expected encoding is larger because it packs the P-256 `r` and `s` values, authenticator data, and the clientDataJSON string. Use a randomized buffer of the correct size:
+
+```typescript
+getStubSignature: async () =>
+ bytesToHex(
+ crypto.getRandomValues(
+ new Uint8Array(
+ 6 * 32 + // ABI offset words
+ 2 * 32 + // r, s
+ 37 + // authenticatorData
+ 81, // clientDataJSON
+ ),
+ ),
+ )
+```
+
+---
+
+## Factory mode
+
+Use this mode when no account has been deployed yet. The account address is computed deterministically off-chain before any transaction is sent, so you can fund it in advance.
+
+```typescript
+import { getContract, encodePacked } from 'viem';
+import OpenAccountFactoryABI from '@openzeppelin-accounts/contracts/out/OpenAccountFactory.sol/OpenAccountFactory.json' assert { type: 'json' };
+
+const factory = getContract({
+ address: '0x9045eF209067701bE7b78AaC216C04Ffa6543330',
+ abi: OpenAccountFactoryABI.abi,
+ client,
+});
+
+const account = await toOpenAccount({
+ client,
+ signer,
+ validator: '0x',
+ factory,
+ moduleTypeIds: ['validator'],
+ modules: ['0x'],
+ initDatas: [encodePacked(['address'], [signer.address])],
+ getStubSignature: async () => '0x' + 'ff'.repeat(65),
+});
+```
+
+### `factory`
+
+A Viem contract instance pointing to the `OpenAccountFactory`. The canonical deployment is at `0x9045eF209067701bE7b78AaC216C04Ffa6543330` on all major EVM chains.
+
+### `moduleTypeIds`
+
+Parallel array of module type identifiers that declares what role each entry in `modules` plays. Values can be strings (`'validator'`, `'executor'`, `'fallback'`, `'hook'`) or their numeric equivalents (`1`β`4`).
+
+```typescript
+moduleTypeIds: ['validator', 'executor']
+```
+
+The array must be the same length as `modules` and `initDatas`.
+
+### `modules`
+
+Addresses of the module contracts to install during account initialization, in the same order as `moduleTypeIds`.
+
+```typescript
+modules: [
+ '0x',
+ '0x',
+]
+```
+
+
+ At least one validator module must be included. An account initialized without any validator will be permanently
+ locked: no user operation will pass the entry point's validation step.
+
+
+### `initDatas`
+
+ABI-encoded initialization payloads passed to each module's `onInstall` function, in the same order as `modules`. The expected encoding varies by module:
+
+| Module | `initData` encoding |
+|---|---|
+| `ERC7579ECDSAValidator` | `encodePacked(['address'], [signerAddress])` (20 bytes) |
+| `ERC7579WebAuthnValidator` | `encodePacked(['bytes32', 'bytes32'], [qx, qy])` (64 bytes) (uncompressed P-256 public key coordinates) |
+| `ERC7579Multisig` | `abi.encode(signers, threshold)` where `signers` is `bytes[]` and `threshold` is `uint64` |
+
+Refer to the individual module pages under [Validators](/accounts/contracts/validators) and [Executors](/accounts/contracts/executors) for the exact encoding each module expects.
+
+### `salt`
+
+A 32-byte value mixed into the CREATE2 salt alongside the module configuration. Use a random salt when you need multiple distinct accounts for the same signer and module combination:
+
+```typescript
+import { bytesToHex } from 'viem';
+
+salt: bytesToHex(crypto.getRandomValues(new Uint8Array(32)))
+```
+
+The full CREATE2 salt is `keccak256(abi.encode(moduleTypeIds, modules, initDatas, configMode, configCalldata, salt))`, so changing any of those fields also changes the resulting address.
+
+### `configMode` and `configCalldata`
+
+An additional call executed _after_ all modules are installed, in the same initialization transaction. Useful when the account itself must call another contract as part of setup (e.g. to approve a token allowance).
+
+`configMode` is an `EncodeMode` object (same shape as passed to `encodeMode`) that describes how `configCalldata` should be executed. `configCalldata` follows the ERC-7579 `executionCalldata` format for the chosen mode.
+
+```typescript
+import { encodeMode } from '@openzeppelin-accounts/sdk';
+import { encodePacked } from 'viem';
+
+configMode: { callType: 'call', execType: 'default' },
+configCalldata: encodePacked(
+ ['address', 'uint256', 'bytes'],
+ ['0x', 0n, '0x'],
+),
+```
+
+
+ Because `configMode` and `configCalldata` are included in the CREATE2 salt computation, any change to these fields
+ will produce a different account address. Design the initialization calldata carefully before funding the predicted
+ address.
+
+
+---
+
+## Existing account mode
+
+Use this mode when the `OpenAccount` contract is already deployed. Pass the on-chain address instead of factory arguments:
+
+```typescript
+const account = await toOpenAccount({
+ client,
+ signer,
+ validator: '0x',
+ address: '0x',
+ getStubSignature: async () => '0x' + 'ff'.repeat(65),
+});
+```
+
+### `address`
+
+The address of the deployed `OpenAccount` contract. `toOpenAccount` reads the contract's `accountId()` function on construction and throws if the returned string does not start with `@openzeppelin/contracts.AccountERC7579.`, ensuring you do not accidentally point the SDK at a different contract type.
+
+---
+
+## Complete example
+
+The following example deploys an account with an ECDSA validator and immediately uses a second validator for a subsequent operation:
+
+```typescript
+import {
+ createPublicClient,
+ createWalletClient,
+ http,
+ getContract,
+ encodePacked,
+ bytesToHex,
+} from 'viem';
+import { mainnet } from 'viem/chains';
+import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
+import { createBundlerClient } from 'viem/account-abstraction';
+import { toOpenAccount, erc7579Actions } from '@openzeppelin-accounts/sdk';
+import OpenAccountFactoryABI from '@openzeppelin-accounts/contracts/out/OpenAccountFactory.sol/OpenAccountFactory.json' assert { type: 'json' };
+
+const client = createPublicClient({ chain: mainnet, transport: http() });
+const signer = privateKeyToAccount(generatePrivateKey());
+
+const factory = getContract({
+ address: '0x9045eF209067701bE7b78AaC216C04Ffa6543330',
+ abi: OpenAccountFactoryABI.abi,
+ client,
+});
+
+// Predict the address and fund it before deployment
+const account = await toOpenAccount({
+ client,
+ signer,
+ validator: '0x',
+ factory,
+ moduleTypeIds: ['validator'],
+ modules: ['0x'],
+ initDatas: [encodePacked(['address'], [signer.address])],
+ salt: bytesToHex(crypto.getRandomValues(new Uint8Array(32))),
+ getStubSignature: async () => '0x' + 'ff'.repeat(65),
+});
+
+console.log('Predicted address:', await account.getAddress());
+
+// The first user operation deploys the account automatically
+const bundlerClient = createBundlerClient({
+ account,
+ client,
+ transport: http('https://bundler.example.com'),
+}).extend(erc7579Actions());
+
+const hash = await bundlerClient.installModules({
+ modules: [
+ {
+ type: 'validator',
+ address: '0x',
+ initData: encodePacked(['address'], [signer.address]),
+ },
+ ],
+});
+
+await bundlerClient.waitForUserOperationReceipt({ hash });
+```
diff --git a/content/accounts/sdk/signers.mdx b/content/accounts/sdk/signers.mdx
new file mode 100644
index 00000000..f71848b1
--- /dev/null
+++ b/content/accounts/sdk/signers.mdx
@@ -0,0 +1,364 @@
+---
+title: Signers
+description: Composable off-chain signer wrappers that produce ERC-7913, ERC-7964, and multisig envelopes
+---
+
+The `signers/` module is the off-chain counterpart to the on-chain signature-verification primitives shipped by the contracts package. It provides a narrow `Signer` interface that `toERC7579Account` and `toOpenAccount` accept alongside Viem's `LocalAccount | WebAuthnAccount`, and a set of composable wrappers that produce the exact wire formats the on-chain validators expect.
+
+Each wrapper is a pure function that takes an inner `Signer` (and a config object) and returns a new `Signer`. Wrappers compose β the outermost wrapper is the outermost envelope, matching the order the on-chain verifier strips.
+
+## The Signer interface
+
+```typescript
+import type { Address, Hex, SignableMessage, TypedDataDefinition } from 'viem';
+
+type Signer = {
+ signMessage: (parameters: { message: SignableMessage }) => Promise;
+ signTypedData: (parameters: TypedDataDefinition) => Promise;
+ address?: Address;
+ signerBytes?: Hex;
+};
+```
+
+`Signer` is a structural subset of Viem's `LocalAccount`, so any `LocalAccount` already satisfies it without an adapter. `WebAuthnAccount` returns a `{ signature, webauthn, ... }` object from `signMessage`, so it needs a small normalization step β that's what `toSigner` does.
+
+The two optional fields carry cross-wrapper metadata:
+
+| Field | Purpose |
+|---|---|
+| `address` | Underlying EOA / smart-account address, when one exists. Used by the ERC-1271 fallback path. |
+| `signerBytes` | ERC-7913 signer bytes (`verifier(20) \|\| key(rest)`), present on wrappers that change the on-chain identity of the signer. See [ERC-7913 Signature Verifiers](/accounts/contracts/erc7913) for the encoding. |
+
+## toSigner
+
+Normalizes Viem's signer types into the SDK's `Signer` interface.
+
+```typescript
+import { toSigner } from '@openzeppelin-accounts/sdk';
+import { privateKeyToAccount } from 'viem/accounts';
+import { toWebAuthnAccount } from 'viem/account-abstraction';
+
+const eoaSigner = toSigner(privateKeyToAccount('0x...'));
+const passkeySigner = toSigner(toWebAuthnAccount({ credential: { id: '...', publicKey: '0x...' } }));
+```
+
+`toSigner` is idempotent for `LocalAccount` (the structural fields are already present) and unwraps the WebAuthn discriminated return shape so downstream code only ever handles `Hex`.
+
+
+ Passing a raw `LocalAccount` or `WebAuthnAccount` directly to `toERC7579Account` / `toOpenAccount` still works β
+ the SDK calls `toSigner` internally at the boundary. Call `toSigner` yourself when you're composing wrappers and
+ want the input type-narrowed to `Signer` up front.
+
+
+## toERC7913Signer
+
+Wraps an inner signer so its output matches the [ERC-7913](/accounts/contracts/erc7913) wire format:
+`abi.encode(bytes signer, bytes rawSig)`. This is the format `SignatureChecker.isValidSignatureNow` decodes when the signer length differs from 20.
+
+```typescript
+import { toERC7913Signer } from '@openzeppelin-accounts/sdk';
+```
+
+Two config shapes are accepted.
+
+**Verifier-less shortcut** β for EOAs (or ERC-1271 contracts) registered into an ERC-7913 key store by address:
+
+```typescript
+const signer = toERC7913Signer(toSigner(eoa), { address: eoa.address });
+// signer.signerBytes === encodePacked(['address'], [eoa.address]) // 20 bytes
+// signer.signMessage(msg) === abi.encode(bytes signer, bytes rawSig)
+```
+
+**Verifier + key** β for algorithm-specific signers (P-256, WebAuthn, RSA, ...) whose public key material lives behind a stateless verifier contract:
+
+```typescript
+import { encodeAbiParameters } from 'viem';
+
+const signer = toERC7913Signer(toSigner(passkey), {
+ verifier: '0x',
+ key: encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }], [qx, qy]),
+});
+// signer.signerBytes === concat([verifier, key]) // 20 + N bytes
+```
+
+See the [Signer Types table](/accounts/contracts/erc7913#signer-types) for the `verifier || key` layout of each key type.
+
+### Reaching the pre-wrap signer
+
+`toERC7913Signer` also exposes `unwrap()`, which returns the original inner `Signer` passed at construction. Use it when a downstream consumer needs the pre-ERC-7913 signature bytes β most notably `toMultisigner`, which packs raw signatures alongside each signer's ERC-7913 `signerBytes`.
+
+```typescript
+const wrapped = toERC7913Signer(toSigner(eoa), { address: eoa.address });
+const envelope = await wrapped.signMessage({ message: 'hi' }); // abi.encode(signer, rawSig)
+const raw = await wrapped.unwrap().signMessage({ message: 'hi' }); // just the ECDSA rawSig
+```
+
+
+ `unwrap()` returns the exact signer instance you passed in β no reconstruction, no defensive copy. It only strips
+ the immediate ERC-7913 layer, so `toERC7913Signer(toCrossChainSigner(...)).unwrap()` returns the cross-chain
+ wrapper, not the raw EOA. Compose flatly (raw signer β ERC-7913 β multisig) unless you know the on-chain verifier
+ can decode whatever layered envelope you're producing.
+
+
+## toCrossChainSigner
+
+Wraps an inner signer so it produces [ERC-7964](/accounts/contracts/erc7964) cross-chain envelopes.
+
+```typescript
+import { toCrossChainSigner } from '@openzeppelin-accounts/sdk';
+```
+
+The wrapper takes the full list of chain-specific operations, hashes each one off-chain via viem's `hashStruct`, asks the inner signer to sign a `chainId`-free outer typed data, and packs the result into the canonical envelope:
+
+```
+abi.encode(
+ bytes32 header, // ERC7964_MAGIC || fields || structIndex || application
+ bytes32[] structsArray,
+ bytes crossChainSignature
+)
+```
+
+```typescript
+import { encodeAbiParameters } from 'viem';
+
+const setValueOp = (chainId: bigint, verifyingContract: Address, value: bigint) => ({
+ types: {
+ ChainOperation: [
+ { name: 'domain', type: 'EIP712ChainDomain' },
+ { name: 'value', type: 'uint256' },
+ ],
+ EIP712ChainDomain: [
+ { name: 'chainId', type: 'uint256' },
+ { name: 'verifyingContract', type: 'address' },
+ ],
+ },
+ primaryType: 'ChainOperation' as const,
+ domain: { name: 'CrossChainApp', version: '1' },
+ message: { domain: { chainId, verifyingContract }, value },
+});
+
+const operations = [
+ setValueOp(1n, app, 42n),
+ setValueOp(42161n, app, 42n),
+];
+
+// Select the chain by its id β the wrapper scans operations for a matching `chainId` field.
+const signer = toCrossChainSigner(toSigner(eoa), {
+ application: app,
+ chainId: 1n,
+ operations,
+ domain: { name: 'CrossChainApp', version: '1' },
+ primaryType: 'SetValue',
+ types: {
+ SetValue: [{ name: 'operations', type: 'ChainOperation[]' }],
+ ChainOperation: [
+ { name: 'domain', type: 'EIP712ChainDomain' },
+ { name: 'value', type: 'uint256' },
+ ],
+ EIP712ChainDomain: [
+ { name: 'chainId', type: 'uint256' },
+ { name: 'verifyingContract', type: 'address' },
+ ],
+ },
+});
+
+const envelope = await signer.signTypedData(operations[0]);
+```
+
+`ChainOperation` is a type alias for viem's `TypedDataDefinition`, so each entry in `operations` is a plain typed-data payload β no wrapping object around it.
+
+### Selecting the current chain
+
+The wrapper needs to know which entry in `operations` corresponds to the chain it's signing for. Two selectors are accepted; the first non-`undefined` one wins:
+
+| Selector | Behaviour |
+|---|---|
+| `config.currentChainIndex` | Explicit index into `operations` |
+| `config.chainId` | Scans `operations` for a message field named `chainId` matching this value |
+
+Per [ERC-7964 Β§Message Array Encoding](/accounts/contracts/erc7964#how-erc-7964-works), each operation MUST include the target `chainId` under a field named exactly `chainId` at any depth within the struct. That canonical field name is what lets `chainId`-driven lookup work regardless of the application-specific struct layout β the wrapper walks the operation's message recursively for the field.
+
+```typescript
+// (a) Explicit index β v1 escape hatch, still supported.
+toCrossChainSigner(inner, { currentChainIndex: 1, ...rest });
+
+// (b) By chainId β wrapper finds the operation whose message includes `chainId: 42161n`.
+toCrossChainSigner(inner, { chainId: 42161n, ...rest });
+```
+
+
+ The recursive `chainId` scan returns the **first match** in DFS insertion order β if an application struct embeds
+ more than one field named `chainId` for its own reasons, the outermost one wins. Fall back to an explicit
+ `currentChainIndex` if you need to disambiguate.
+
+
+
+ `signTypedData(parameters)` asserts that the incoming payload's `hashStruct` matches the resolved operation.
+ This catches the common foot-gun where the caller's `operations` array drifts from the per-chain typed data the
+ smart account is being asked to authorize β the wrapper throws instead of producing an envelope that would
+ silently fail on-chain.
+
+
+### signCrossChainEnvelopes
+
+`toCrossChainSigner` returns a `CrossChainSigner` β a `Signer & { signCrossChainEnvelopes: () => Promise }`. When you have a wrapper instance and want to dispatch to every chain in the config at once, call the method:
+
+```typescript
+const wrapper = toCrossChainSigner(toSigner(eoa), config);
+
+const envelopeForCurrentChain = await wrapper.signTypedData(operations[0]);
+const envelopesForEveryChain = await wrapper.signCrossChainEnvelopes();
+```
+
+The inner signer is asked for the cross-chain signature exactly once β the N envelopes share it and differ only in their header's `structIndex`. This matters for passkey and hardware-wallet flows where each `signTypedData` call prompts the user.
+
+A standalone `signCrossChainEnvelopes(inner, config)` is also exported for the case where you don't have (or don't want) a wrapper instance:
+
+```typescript
+import { signCrossChainEnvelopes } from '@openzeppelin-accounts/sdk';
+
+const envelopes = await signCrossChainEnvelopes(toSigner(passkey), {
+ application: app,
+ operations,
+ domain: { name: 'CrossChainApp', version: '1' },
+ primaryType: 'SetValue',
+ types,
+});
+```
+
+### Outer domain and the `fields` byte
+
+The outer `domain` config accepts `name`, `version`, and optionally `verifyingContract`. `chainId` is intentionally omitted per ERC-7964 β that's what makes the signature crosschain-reusable.
+
+```typescript
+domain: {
+ name: 'CrossChainApp',
+ version: '1',
+ verifyingContract: '0x', // optional, spec's Crosschain Domain Semantics
+},
+```
+
+Include `verifyingContract` when the application is deployed deterministically at the same address on every target chain β this binds the outer signature to that address so per-chain operation structs can omit `verifyingContract`.
+
+The ERC-5267 `fields` byte is derived automatically from the domain shape:
+
+| Domain shape | Auto-computed `fields` |
+| -------------------------------------------- | ---------------------- |
+| `{ name, version }` | `0x03` (name + version) |
+| `{ name, version, verifyingContract }` | `0x0b` (name + version + verifyingContract) |
+
+Callers can pass an explicit `fields` byte, but only bits inside `0x0b` (name / version / verifyingContract) are accepted β the SDK's outer typed data doesn't emit `chainId` (`0x04`, forbidden by ERC-7964) or `salt` (`0x10`), so setting either would make the on-chain domain separator diverge from the SDK's signed digest.
+
+If bit `0x08` is set in the explicit `fields` but `domain.verifyingContract` is omitted, it defaults to `application`. The wrapper still throws when an explicit `fields` disagrees with an explicit `domain.verifyingContract` (bit set vs value missing, or bit unset vs value present).
+
+
+ `signMessage` is intentionally unsupported on a cross-chain signer β ERC-7964 is EIP-712-only by spec. Calling it
+ throws a typed error rather than returning a signature that no on-chain verifier would accept.
+
+
+## toMultisigner
+
+Wraps a set of inner signers into a single signer whose output matches the encoding consumed by `Multisig.tryDecodeMultisignatureCalldata` and the multisig validators β see [Multisig Modules](/accounts/contracts/multisig) for the on-chain side:
+
+```
+abi.encode(bytes[] signers, bytes[] signatures)
+```
+
+```typescript
+import { toMultisigner } from '@openzeppelin-accounts/sdk';
+```
+
+Two input shapes are accepted and can be mixed freely in the same call:
+
+| Shape | When to use |
+|---|---|
+| `{ signer: Signer, signerBytes: Hex }` | Explicit pair. Use when you're wiring an unwrapped inner signer with an externally-known ERC-7913 identity. |
+| `ERC7913Signer` (returned by `toERC7913Signer`) | Already-wrapped signer. `toMultisigner` calls `.unwrap()` to reach the pre-ERC-7913 signer and pairs its raw signature with the wrapper's `signerBytes`, so the multisig envelope carries raw inner signatures rather than nested envelopes. |
+
+```typescript
+const multisig = toMultisigner([
+ // Verifier-less EOA in pair form
+ { signer: toSigner(alice), signerBytes: alice.address },
+ // ERC-7913-wrapped passkey
+ toERC7913Signer(toSigner(passkey), {
+ verifier: webAuthnVerifier,
+ key: encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }], [qx, qy]),
+ }),
+]);
+
+const envelope = await multisig.signTypedData(typedData);
+// abi.encode(
+// [alice.address, webAuthnVerifier || key],
+// [aliceEcdsaSig, passkeyWebAuthnSig],
+// )
+```
+
+Signer/signature pairs are sorted by `keccak256(signerBytes)` before encoding. The on-chain `SignatureChecker.areValidSignaturesNow` uses that same key for its efficient duplicate check, so a sorted input skips the O(nΒ²) fallback.
+
+## Composition
+
+Wrappers are pure functions of shape `(inner, config) => Signer`; the composition order matches the order the on-chain verifier strips envelopes. The design doc's canonical example β an EOA whose cross-chain envelope is further wrapped for an ERC-7913 validator β reads left-to-right from inner to outer:
+
+```typescript
+const signer = toERC7913Signer(
+ toCrossChainSigner(toSigner(eoa), {
+ application,
+ chainId,
+ operations,
+ domain: { name: 'App', version: '1' },
+ primaryType: 'SetValue',
+ types,
+ }),
+ { address: eoa.address },
+);
+
+const envelope = await signer.signTypedData(operationForChain);
+```
+
+Verifying it on-chain unwraps in the reverse order: the ERC-7913 layer first (`abi.decode(bytes signer, bytes rawSig)`), then the cross-chain layer via `CrossChainSignatureChecker.isValidCrossChainSignatureNow`.
+
+Fields like `signerBytes` propagate through outer wrappers via a spread. `toCrossChainSigner` doesn't change the signer identity β only the envelope around the signature bytes β so composing it inside an ERC-7913 wrapper preserves the outer wrapper's `signerBytes` for downstream consumers.
+
+## Using signers with toOpenAccount / toERC7579Account
+
+`toOpenAccount` and `toERC7579Account` accept `Signer | LocalAccount | WebAuthnAccount`. Passing an SDK `Signer` (from any wrapper) works the same way as passing a raw Viem account:
+
+```typescript
+import { toOpenAccount, toMultisigner, toERC7913Signer, toSigner } from '@openzeppelin-accounts/sdk';
+
+const signer = toMultisigner([
+ { signer: toSigner(alice), signerBytes: alice.address },
+ { signer: toSigner(bob), signerBytes: bob.address },
+]);
+
+const account = await toOpenAccount({
+ client,
+ signer,
+ validator: '0x',
+ address: '0x',
+ getStubSignature: async () =>
+ // Match the shape of a real multisig envelope for gas estimation
+ encodeAbiParameters(
+ [{ type: 'bytes[]' }, { type: 'bytes[]' }],
+ [[alice.address, bob.address], [`0x${'ff'.repeat(65)}`, `0x${'ff'.repeat(65)}`]],
+ ),
+});
+```
+
+The `getStubSignature` must have the byte layout of the real envelope the validator expects β the outer multisig wrapper for multisig validators, an ERC-7913 envelope for `ERC7579Signature`, and so on.
+
+
+ `toCrossChainSigner` is **not** a valid top-level signer for `toOpenAccount` / `toERC7579Account`. It produces
+ application-level cross-chain envelopes bound to a fixed set of `operations`, not per-UserOp signatures. If you
+ hand a `CrossChainSigner` to `toOpenAccount`, every `signUserOperation` and `signMessage` call throws β the
+ wrapper asserts the incoming payload against `operations[currentChainIndex]`, and a `PackedUserOperation` never
+ matches. The correct composition is `toERC7913Signer(toCrossChainSigner(...), ...)` handed to your application's
+ own verification path (e.g. `app.setValue(sig)`), not to the smart account.
+
+
+## Security Considerations
+
+- **Signer identity is data**: `signerBytes` is passed through to the on-chain multisig / validator verbatim. Make sure the value you use matches what was installed at `onInstall` time. For verifier-less EOAs this is `abi.encodePacked(address)`; for ERC-7913 signers, `verifier || key`. See [ERC-7913 Signer Types](/accounts/contracts/erc7913#signer-types).
+- **Sort by identity, encode raw**: `toMultisigner` sorts pairs by `keccak256(signerBytes)` for on-chain gas efficiency but encodes the raw `signerBytes` and raw inner signatures into the output. Do not pre-hash `signerBytes` in the input.
+- **Replay protection is the application's job**: `toCrossChainSigner` implements the wire format only. Nonces, deadlines, and per-application replay scope must be part of the operation's typed data (see [ERC-7964 Security Considerations](/accounts/contracts/erc7964#security-considerations)).
+- **Wrappers do not touch keys**: none of the wrappers persist, encrypt, or rotate key material. WebAuthn ceremonies, KMS adapters, and secure enclaves live behind the inner signer.
diff --git a/docgen/config-md.js b/docgen/config-md.js
index afbf7c6a..4ad989ee 100644
--- a/docgen/config-md.js
+++ b/docgen/config-md.js
@@ -1,6 +1,9 @@
const path = require('path');
const fs = require('fs');
+// Page files that group contracts into API pages. README.mdx is used as-is; README.adoc is converted.
+const READMES = ['README.mdx', 'README.adoc'];
+
/** @type import('solidity-docgen/dist/config').UserConfig */
module.exports = {
outputDir: 'docs/modules/api/pages',
@@ -12,8 +15,9 @@ module.exports = {
let dir = path.resolve(config.root, file.absolutePath);
while (dir.startsWith(sourcesDir)) {
dir = path.dirname(dir);
- if (fs.existsSync(path.join(dir, 'README.adoc'))) {
- return path.relative(sourcesDir, dir) + config.pageExtension;
+ if (READMES.some(readme => fs.existsSync(path.join(dir, readme)))) {
+ // A README at the sources root documents root-level contracts on the index page
+ return (path.relative(sourcesDir, dir) || 'index') + config.pageExtension;
}
}
},
diff --git a/docgen/templates-md/helpers.js b/docgen/templates-md/helpers.js
index 1c679f5f..2c3619ee 100644
--- a/docgen/templates-md/helpers.js
+++ b/docgen/templates-md/helpers.js
@@ -8,17 +8,31 @@ const API_DOCS_PATH = 'contracts/5.x/api';
module.exports['oz-version'] = () => version;
-module.exports['readme-path'] = opts => {
- const pageId = opts.data.root.id;
+// Page path without extension. The index page (root-level README.adoc) maps to the sources root.
+function pageBasePath(pageId) {
const basePath = pageId.replace(/\.(adoc|mdx)$/, '');
- return 'contracts/' + basePath + '/README.adoc';
+ return basePath === 'index' ? '' : basePath;
+}
+
+// Sources live under contracts/ (single-package repos) or src/ (e.g. monorepo packages published from src/)
+const SOURCES_DIRS = ['contracts', 'src'];
+
+module.exports['readme-path'] = opts => {
+ const basePath = pageBasePath(opts.data.root.id);
+ const candidates = SOURCES_DIRS.flatMap(dir =>
+ ['README.mdx', 'README.adoc'].map(readme => path.join(dir, basePath, readme)),
+ );
+ return candidates.find(candidate => fs.existsSync(candidate)) ?? candidates[0];
};
module.exports.readme = readmePath => {
try {
if (fs.existsSync(readmePath)) {
const readmeContent = fs.readFileSync(readmePath, 'utf8');
- return processAdocContent(readmeContent);
+ // Markdown page files are used as-is, minus the leading title (the page frontmatter provides it)
+ return readmePath.endsWith('.mdx')
+ ? readmeContent.replace(/^#[ \t]+.*\n+/, '')
+ : processAdocContent(readmeContent);
}
} catch (error) {
console.warn(`Warning: Could not process README at ${readmePath}:`, error.message);
@@ -48,12 +62,12 @@ module.exports['reset-function-counts'] = function () {
module.exports.eq = (a, b) => a === b;
-// Strip the leading "contracts/" segment from a path. Used to derive the npm
+// Strip the leading "contracts/" or "src/" segment from a path. Used to derive the npm
// import specifier for non-`contracts` packages, where the file is published
// at the package root (e.g. @openzeppelin/community-contracts/account/X.sol)
// but the source lives at contracts/account/X.sol in the repo.
module.exports['strip-contracts-prefix'] = function (p) {
- return typeof p === 'string' ? p.replace(/^contracts\//, '') : p;
+ return typeof p === 'string' ? p.replace(/^(contracts|src)\//, '') : p;
};
module.exports['starts-with'] = (str, prefix) => str && str.startsWith(prefix);
@@ -150,8 +164,8 @@ function generateLinkPath(pagePath, currentPagePath, anchor) {
return `#${anchor}`;
}
- // Cross-page: use absolute path with API_DOCS_PATH prefix
- return `/${API_DOCS_PATH}/${pagePath}#${anchor}`;
+ // Cross-page: use absolute path with API_DOCS_PATH prefix (the index page is served at the API root)
+ return pagePath === 'index' ? `/${API_DOCS_PATH}#${anchor}` : `/${API_DOCS_PATH}/${pagePath}#${anchor}`;
}
// Process {REF} and other references
@@ -216,7 +230,7 @@ function processReferences(content, links) {
return replacement || `\`${key}\``;
});
- result = cleanupContent(result);
+ result = cleanupContent(result, new Set(Object.values(links.__linkPages ?? {})));
// Escape bare < that aren't HTML/JSX tags (e.g., "< 0x80", "< 128")
// Must run after cleanupContent (which decodes < to <) and before processCallouts
@@ -297,8 +311,14 @@ function findBestMatch(key, links) {
return undefined;
}
-function cleanupContent(content) {
+// Bare xrefs in NatSpec inherited from OpenZeppelin Contracts (e.g. `xref:access.adoc[...]`) point to its API
+// pages. Outside of OpenZeppelin Contracts, link them there unless the current library has a page with that name.
+const OZ_CONTRACTS_API_BASE = '/contracts/5.x/api';
+
+function cleanupContent(content, localPages) {
const apiDocsBase = '/' + API_DOCS_PATH;
+ const bareXrefBase = file =>
+ localPages && !localPages.has(file) && !API_DOCS_PATH.startsWith('contracts/') ? OZ_CONTRACTS_API_BASE : apiDocsBase;
const docsBase = apiDocsBase.replace(/\/api$/, '');
return content
.replace(/</g, '<')
@@ -330,7 +350,7 @@ function cleanupContent(content) {
})
// xref:filename.adoc#anchor[text] -> [text](apiDocsBase/filename#anchor) (bare, within API context)
.replace(/xref:([^:[\s]+)\.adoc(?:#([^[]*))?\[([^\]]*)\]/g, (_, file, anchor, text) =>
- `[${text}](${apiDocsBase}/${file}${anchor ? '#' + anchor : ''})`)
+ `[${text}](${bareXrefBase(file)}/${file}${anchor ? '#' + anchor : ''})`)
// Strip /index from links
.replace(/\/index([#)])/g, '$1');
}
@@ -401,7 +421,22 @@ function processAdocContent(content) {
}
}
+// GitHub alert types mapped to Callout props
+const ALERT_TYPES = { NOTE: '', TIP: '', IMPORTANT: ' type="warn"', WARNING: ' type="warn"', CAUTION: ' type="warn"' };
+
+// Convert GitHub alerts (`> [!NOTE]` followed by `> ...` lines) to Callouts
+function processAlerts(content) {
+ return content.replace(
+ /^>[ \t]*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*\n((?:>.*(?:\n|$))*)/gm,
+ (_, type, body) => {
+ const text = body.replace(/^>[ \t]?/gm, '').trimEnd();
+ return `\n${text}\n\n`;
+ },
+ );
+}
+
function processCallouts(content) {
+ content = processAlerts(content);
// Convert AsciiDoc headings in natspec (==== heading -> #### heading)
// Use [ \t]+ instead of \s+ so the regex does not consume the following
// newline and pull the next line's text up β that bug was eating the
@@ -479,8 +514,8 @@ function processCallouts(content) {
}
module.exports.title = opts => {
- const pageId = opts.data.root.id;
- const basePath = pageId.replace(/\.(adoc|mdx)$/, '');
+ const basePath = pageBasePath(opts.data.root.id);
+ if (basePath === '') return 'API Reference';
const parts = basePath.split('/');
const dirName = parts[parts.length - 1] || 'Contracts';
return dirName
@@ -490,8 +525,8 @@ module.exports.title = opts => {
};
module.exports.description = opts => {
- const pageId = opts.data.root.id;
- const basePath = pageId.replace(/\.(adoc|mdx)$/, '');
+ const basePath = pageBasePath(opts.data.root.id);
+ if (basePath === '') return 'Smart contract API reference';
const parts = basePath.split('/');
const dirName = parts[parts.length - 1] || 'contracts';
return `Smart contract ${dirName.replace('-', ' ')} utilities and implementations`;
diff --git a/scripts/generate-api-docs.js b/scripts/generate-api-docs.js
index b4bfff0b..762463f8 100755
--- a/scripts/generate-api-docs.js
+++ b/scripts/generate-api-docs.js
@@ -22,6 +22,9 @@ function parseArgs() {
skipTemplateInject: false,
preGenerated: null,
guidesOutputDir: null,
+ guidesMdx: null,
+ sourcePackage: "",
+ packageName: null,
};
for (let i = 0; i < args.length; i++) {
@@ -63,6 +66,16 @@ function parseArgs() {
case "-g":
options.guidesOutputDir = args[++i];
break;
+ case "--guides-mdx":
+ options.guidesMdx = args[++i];
+ break;
+ case "--source-package":
+ case "-s":
+ options.sourcePackage = args[++i];
+ break;
+ case "--package-name":
+ options.packageName = args[++i];
+ break;
default:
console.error(`Unknown option: ${arg}`);
showHelp();
@@ -87,6 +100,9 @@ Options:
-e, --examples-output Examples output directory (default: examples)
-p, --pre-generated Path within repo containing pre-generated MDX files (skips docgen)
-g, --guides-output Convert .adoc guides from docs/modules/ROOT/pages/ to MDX into
+ --guides-mdx Path within repo containing MDX guides to copy into --guides-output (skips .adoc conversion)
+ -s, --source-package Path within repo of the Solidity package to run docgen in (default: repo root)
+ --package-name npm package name used in import paths (default: derived from the repo name)
--skip-template-inject Skip injecting canonical templates (use source repo's own)
-h, --help Show this help message
@@ -98,6 +114,9 @@ Examples:
# Solidity repo (injects templates, runs docgen):
node generate-api-docs.js --repo https://github.com/OpenZeppelin/openzeppelin-contracts.git --api-output content/contracts/5.x/api
+ # Solidity package in a monorepo, with MDX guides:
+ node generate-api-docs.js --repo https://github.com/OpenZeppelin/openzeppelin-accounts.git --api-output content/accounts/api --source-package packages/contracts --package-name @openzeppelin-accounts/contracts --guides-output content/accounts --guides-mdx docs
+
# Non-Solidity repo (copies pre-generated MDX):
node generate-api-docs.js --repo https://github.com/OpenZeppelin/cairo-contracts.git --api-output content/contracts-cairo/3.x/api --pre-generated docs/api
`);
@@ -127,18 +146,20 @@ function derivePackageName(repoName) {
}
async function injectTemplates(tempDir, options) {
- const { contractsRepo, contractsBranch, apiOutputDir } = options;
+ const { contractsRepo, contractsBranch, apiOutputDir, sourcePackage } =
+ options;
const repoInfo = extractRepoInfo(contractsRepo);
- const packageName = derivePackageName(repoInfo.repo);
+ const packageName = options.packageName ?? derivePackageName(repoInfo.repo);
+ const packageDir = path.join(tempDir, sourcePackage);
console.log("π Injecting canonical MDX templates...");
- const templatesTarget = path.join(tempDir, "docs", "templates-md");
+ const templatesTarget = path.join(packageDir, "docs", "templates-md");
// Overwrite the cloned repo's docs/config.js with our canonical config.
// The cloned repo's hardhat config and prepare-docs.sh scripts both load
// `./docs/config`, so this single overwrite covers both paths without
// needing a separate config-md.js write or a hardhat-config regex patch.
- const configTarget = path.join(tempDir, "docs", "config.js");
+ const configTarget = path.join(packageDir, "docs", "config.js");
await fs.mkdir(templatesTarget, { recursive: true });
await copyDirRecursive(
@@ -177,16 +198,27 @@ async function injectTemplates(tempDir, options) {
`blob/${contractsBranch}`,
);
+ // Source paths are relative to the package, so prefix them with its location in the repo
+ if (sourcePackage) {
+ const prefix = sourcePackage.replace(/^\/+|\/+$/g, "");
+ contract = contract.replace(
+ /(blob\/[^"]*?\/)(\{\{__item_context\.file\.absolutePath\}\})/g,
+ `$1${prefix}/$2`,
+ );
+ }
+
// Canonical template has @openzeppelin/{{absolutePath}} with absolutePath
// starting "contracts/...". For openzeppelin-contracts that's correct
// (npm package is `@openzeppelin/contracts`, file path keeps the prefix).
// For other packages (community-contracts, confidential-contracts), the
// npm package is published with `contracts/` as the package root, so the
// import skips that prefix β strip it via the strip-contracts-prefix helper.
+ // An explicit --package-name is used verbatim (e.g. @openzeppelin-accounts/contracts).
+ const importPrefix = options.packageName ?? `@openzeppelin/${packageName}`;
if (packageName !== "contracts") {
contract = contract.replace(
/import "@openzeppelin\/\{\{__item_context\.file\.absolutePath\}\}";/g,
- `import "@openzeppelin/${packageName}/{{strip-contracts-prefix __item_context.file.absolutePath}}";`,
+ `import "${importPrefix}/{{strip-contracts-prefix __item_context.file.absolutePath}}";`,
);
}
await fs.writeFile(contractPath, contract, "utf8");
@@ -194,7 +226,7 @@ async function injectTemplates(tempDir, options) {
` β GitHub link set to ${repoInfo.org}/${repoInfo.repo}`,
);
console.log(
- ` β Import path set to @openzeppelin/${packageName}/`,
+ ` β Import path set to ${packageName === "contracts" ? "@openzeppelin/contracts" : importPrefix}/`,
);
}
@@ -208,6 +240,8 @@ async function generateApiDocs(options) {
skipTemplateInject,
preGenerated,
guidesOutputDir,
+ guidesMdx,
+ sourcePackage,
} = options;
console.log("π Generating OpenZeppelin API documentation...");
@@ -284,17 +318,31 @@ async function generateApiDocs(options) {
await injectTemplates(tempDir, options);
}
- // Navigate to contracts directory and install dependencies
+ // Install dependencies at the repo root (pnpm workspaces) and run docgen in the package directory
console.log("π Installing dependencies...");
const originalDir = process.cwd();
- process.chdir(tempDir);
+ const usePnpm = await fs
+ .access(path.join(tempDir, "pnpm-lock.yaml"))
+ .then(() => true)
+ .catch(() => false);
+ if (usePnpm) {
+ execSync("pnpm install --frozen-lockfile", {
+ cwd: tempDir,
+ stdio: "inherit",
+ });
+ }
+ process.chdir(path.join(tempDir, sourcePackage));
try {
- execSync("npm install --silent", { stdio: "inherit" });
+ if (!usePnpm) {
+ execSync("npm install --silent", { stdio: "inherit" });
+ }
// Generate MDX documentation
console.log("ποΈ Generating MDX documentation...");
- execSync("npm run prepare-docs", { stdio: "inherit" });
+ execSync(`${usePnpm ? "pnpm" : "npm"} run prepare-docs`, {
+ stdio: "inherit",
+ });
// Copy generated files
console.log("π Copying generated documentation...");
@@ -335,14 +383,20 @@ async function generateApiDocs(options) {
process.chdir(originalDir);
}
- // Restore index.mdx if it was backed up
- if (indexBackup) {
+ // Restore index.mdx if it was backed up, unless docgen generated one (root-level README.adoc)
+ const indexGenerated = await fs
+ .access(indexPath)
+ .then(() => true)
+ .catch(() => false);
+ if (indexBackup && !indexGenerated) {
console.log("β»οΈ Restoring index.mdx...");
await fs.writeFile(indexPath, indexBackup, "utf8");
}
- // Convert AsciiDoc guides (docs/modules/ROOT/pages) to MDX
- if (guidesOutputDir) {
+ // Copy MDX guides as-is, or convert AsciiDoc guides (docs/modules/ROOT/pages) to MDX
+ if (guidesOutputDir && guidesMdx) {
+ await copyGuides(tempDir, guidesMdx, guidesOutputDir);
+ } else if (guidesOutputDir) {
await convertGuides(tempDir, guidesOutputDir, apiOutputDir);
}
@@ -381,6 +435,22 @@ async function convertGuides(tempDir, guidesOutputDir, apiOutputDir) {
console.log(`β Guides copied to ${guidesOutputDir}`);
}
+async function copyGuides(tempDir, guidesMdx, guidesOutputDir) {
+ const guidesPath = path.join(tempDir, guidesMdx);
+
+ try {
+ await fs.access(guidesPath);
+ } catch {
+ console.log(`β Error: MDX guides not found at ${guidesMdx}`);
+ process.exit(1);
+ }
+
+ console.log("π Copying MDX guides...");
+ await fs.mkdir(guidesOutputDir, { recursive: true });
+ await copyMdxRecursive(guidesPath, guidesOutputDir);
+ console.log(`β Guides copied to ${guidesOutputDir}`);
+}
+
async function copyMdxRecursive(srcDir, destDir) {
const entries = await fs.readdir(srcDir, { withFileTypes: true });
for (const entry of entries) {
diff --git a/src/navigation/ethereum-evm.json b/src/navigation/ethereum-evm.json
index b3c89e67..e2c8489b 100644
--- a/src/navigation/ethereum-evm.json
+++ b/src/navigation/ethereum-evm.json
@@ -661,6 +661,147 @@
}
]
},
+ {
+ "type": "folder",
+ "name": "OpenZeppelin Accounts",
+ "index": {
+ "type": "page",
+ "name": "Overview",
+ "url": "/accounts"
+ },
+ "children": [
+ {
+ "type": "folder",
+ "name": "Contracts",
+ "index": {
+ "type": "page",
+ "name": "Overview",
+ "url": "/accounts/contracts"
+ },
+ "children": [
+ {
+ "type": "page",
+ "name": "ERC-7579",
+ "url": "/accounts/contracts/erc7579"
+ },
+ {
+ "type": "page",
+ "name": "ERC-7913",
+ "url": "/accounts/contracts/erc7913"
+ },
+ {
+ "type": "page",
+ "name": "ERC-7964",
+ "url": "/accounts/contracts/erc7964"
+ },
+ {
+ "type": "page",
+ "name": "ERC-8043",
+ "url": "/accounts/contracts/erc8043"
+ },
+ {
+ "type": "folder",
+ "name": "Modules",
+ "children": [
+ {
+ "type": "page",
+ "name": "Signers",
+ "url": "/accounts/contracts/signers"
+ },
+ {
+ "type": "page",
+ "name": "Multisig",
+ "url": "/accounts/contracts/multisig"
+ },
+ {
+ "type": "page",
+ "name": "Session Keys",
+ "url": "/accounts/contracts/session-keys"
+ },
+ {
+ "type": "page",
+ "name": "Validators",
+ "url": "/accounts/contracts/validators"
+ },
+ {
+ "type": "folder",
+ "name": "Executors",
+ "index": {
+ "type": "page",
+ "name": "Overview",
+ "url": "/accounts/contracts/executors"
+ },
+ "children": [
+ {
+ "type": "page",
+ "name": "Social Recovery",
+ "url": "/accounts/contracts/executors/social-recovery"
+ },
+ {
+ "type": "page",
+ "name": "Spending Limits",
+ "url": "/accounts/contracts/executors/spending-limits"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "folder",
+ "name": "SDK",
+ "index": {
+ "type": "page",
+ "name": "Overview",
+ "url": "/accounts/sdk"
+ },
+ "children": [
+ {
+ "type": "page",
+ "name": "ERC-7579 Actions",
+ "url": "/accounts/sdk/actions"
+ },
+ {
+ "type": "page",
+ "name": "Signers",
+ "url": "/accounts/sdk/signers"
+ },
+ {
+ "type": "page",
+ "name": "toOpenAccount",
+ "url": "/accounts/sdk/open-account"
+ }
+ ]
+ },
+ {
+ "type": "folder",
+ "name": "API Reference",
+ "index": {
+ "type": "page",
+ "name": "Overview",
+ "url": "/accounts/api"
+ },
+ "children": [
+ {
+ "type": "page",
+ "name": "Modules",
+ "url": "/accounts/api/modules"
+ },
+ {
+ "type": "page",
+ "name": "Extensions",
+ "url": "/accounts/api/extensions"
+ },
+ {
+ "type": "page",
+ "name": "Utilities",
+ "url": "/accounts/api/utils"
+ }
+ ]
+ }
+ ]
+ },
{
"type": "folder",
"name": "Upgrades Plugins",