Abstract
This ERC defines a mechanism for ERC-7579 compliant smart accounts to install and immediately use modules within a single user operation by reusing the initCode field when bundled in an ERC-4337 Entrypoint v9, which silently ignores initCode when the sender account is already deployed, making the initCode field available as a signaling and data channel for existing accounts.
This document specifies a magic value in the initCode field and an EIP-712 encoding format to trigger init mode: the installation of one or more modules during the validation phase of a user operation.
Motivation
Current ERC-7579 implementations require separate transactions for module installation and usage, which can hinder user experience. Use cases such as session key and spending limit modules would benefit from the ability to install and use modules in a single operation. This spec allows applications to opt in to install and use multiple modules under a single user operation, enabling greater flexibility—particularly for scenarios where multiple modules need to work together atomically.
This document addresses the following issues:
- User Experience: Reduces transaction count from N+1 to 1 for installing N modules and using them
- Consistency across implementations: Provides a standard pattern that works across different ERC-7579 account implementations
- Security: Module installation parameters are committed to by the
userOpHash(which coversinitCode) and authorized through EIP-712 typed signatures, ensuring both tamper-resistance and human-readable signing UX
Real-world usage in production systems has validated the need and feasibility of this pattern, particularly for session key management and dynamic module installation.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Definitions
- Init Mode: The logic branch triggered during the validation phase allowing to install and use one or more ERC-7579 modules within a single user operation
- Init Mode Magic Value: The 4-byte magic value at the start of
initCodethat signals init mode - Installation Signature: An EIP-712 signature by the account authorizing module installations
- User Operation Signature: The signature in the
signaturefield for validating the user operation itself
Init Mode Magic Value
Smart accounts implementing this standard MUST detect init mode by checking whether the first 4 bytes of userOp.initCode equal the magic value:
bytes4 constant INIT_MODE_MAGIC_VALUE = 0x80438043;
if (bytes4(userOp.initCode[0:4]) == INIT_MODE_MAGIC_VALUE) {
// Enter init mode
}initCode Encoding Format
When init mode is detected, the remaining bytes of userOp.initCode after the magic value (i.e., userOp.initCode[4:]) MUST be ABI-decoded as:
abi.decode(
userOp.initCode[4:],
(uint256[], address[], bytes[], bytes)
)
// (moduleTypeIds, modules, initDatas, installationSignature)Where:
moduleTypeIds: Array of ERC-7579 module type identifiersmodules: Array of module addresses to installinitDatas: Array of initialization data to pass to each module'sonInstallfunctioninstallationSignature: EIP-712 signature by the account owner authorizing the module installations
EIP-712 Message Structure
The installationSignature MUST be an EIP-712 signature over the following typed data, using the account's own EIP-712 domain:
bytes32 constant INIT_MODULES_TYPEHASH = keccak256(
"InitModules(uint256[] moduleTypeIds,address[] modules,bytes[] initDatas,uint256 nonce)"
);The struct hash is computed as:
bytes32 structHash = keccak256(abi.encode(
INIT_MODULES_TYPEHASH,
keccak256(abi.encodePacked(moduleTypeIds)),
keccak256(abi.encodePacked(modules)),
keccak256(abi.encode(initDatas)), // bytes[] requires element-by-element hashing
nonce
));Where nonce is userOp.nonce. The final digest passed to signature verification SHOULD be _hashTypedDataV4(structHash), i.e., the EIP-712 encoding with domain separator.
Validation Flow
Smart accounts MUST implement init mode on top of ERC-4337's validateUserOp function. Implementations MUST:
- Detect init mode by checking
bytes4(userOp.initCode[0:4]) == INIT_MODE_MAGIC_VALUE - Decode
moduleTypeIds,modules,initDatas, andinstallationSignaturefromuserOp.initCode[4:] - Compute the EIP-712 struct hash over
moduleTypeIds,modules,initDatas, anduserOp.nonce, then validateinstallationSignatureusingisValidSignature(_hashTypedDataV4(structHash), installationSignature) - Install each module using the standard ERC-7579
installModuleflow - Validate the user operation using
super._validateUserOp - Return the validation result
Implementations MUST return SIG_VALIDATION_FAILED (1) if decoding fails, the installation signature is invalid, any module installation fails, or user operation validation fails.
Rationale
Use of initCode as the Signaling and Data Channel
ERC-4337 EntryPoint v0.9 introduced the ability to include a non-empty initCode in a user operation for an already-deployed account, with the EntryPoint silently ignoring the factory call in that case. This makes initCode a natural fit for init mode:
- The
initCodefield is included in theuserOpHashcomputation per ERC-4337, so module installation parameters are automatically committed to by the user operation signature - The nonce field is left entirely available for standard 2D nonce use
The 4-byte magic value 0x8043 was chosen because a well-formed init mode initCode is always far longer than 20 bytes: the minimum ABI-encoded payload for (uint256[], address[], bytes[], bytes) is 256 bytes (4 _ 32-byte head offsets plus 4 _ 32-byte empty-length words), making a 4-byte prefix sufficient to unambiguously identify init mode without reserving the full 20-byte factory address slot. The value embeds the ERC number for self-documentation and avoids conflicts with Ethereum precompiles and real factory address prefixes.
EIP-712 for Installation Authorization
EIP-712 typed data is used for installationSignature to provide human-readable signing requests in wallets and hardware signers. A signer presented with an InitModules type can see precisely which modules at which addresses are being authorized for installation, with what initialization data, at which nonce—giving the signed data unambiguous semantic meaning. This separates the owner's installation authorization from the user operation's execution authorization: the owner signs only the module installation intent, not the full user operation (gas parameters, calldata, etc.), which may be controlled by a different key such as a session key being granted in the same operation.
Replay protection for installationSignature is provided by userOp.nonce included in the struct: once the user operation is executed and its nonce consumed, the same installationSignature cannot be replayed. Chain ID and account address binding are provided by the EIP-712 domain separator.
Compatibility with ERC-7579
This standard extends ERC-7579 without breaking existing functionality:
- Normal user operations continue to work unchanged
- Standard module installation flows remain available
- All ERC-7579 security guarantees are preserved
Backwards Compatibility
This standard requires ERC-4337 EntryPoint v0.9 or later (deployed at 0x433709009B8330FDa32311DF1C2AFA402eD8D009). On earlier EntryPoint versions, a user operation with non-empty initCode for an already-deployed account is rejected by bundlers before reaching the account.
Accounts not implementing this standard are unaffected: the EntryPoint does not call the factory when the account already exists, so the initCode contents are simply ignored by the runtime.
Note that the v0.9 EntryPoint emits an IgnoredInitCode event whenever initCode is silently skipped for an existing account. Tools and indexers that use this event to detect orphaned factory calls will also observe it for every init mode user operation. Implementations SHOULD document this behavior in off-chain tooling.
Reference Implementation
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.27;
import {AccountERC7579} from "@openzeppelin/contracts/account/extensions/draft-AccountERC7579.sol";
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
abstract contract InitModeAccount is AccountERC7579 {
bytes4 private constant INIT_MODE_MAGIC_VALUE = 0x80438043;
bytes32 private constant INIT_MODULES_TYPEHASH =
keccak256("InitModules(uint256[] moduleTypeIds,address[] modules,bytes[] initDatas,uint256 nonce)");
function _validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual override returns (uint256) {
if (bytes4(userOp.initCode[0:4]) == INIT_MODE_MAGIC_VALUE) {
return _validateInitMode(userOp, userOpHash);
}
return super._validateUserOp(userOp, userOpHash);
}
function _validateInitMode(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal returns (uint256) {
(
bool success,
uint256[] memory moduleTypeIds,
address[] memory modules,
bytes[] memory initDatas,
bytes memory installationSignature
) = _tryDecodeInitModeData(userOp.initCode[4:]);
if (!success) return ERC4337Utils.SIG_VALIDATION_FAILED;
if (moduleTypeIds.length != modules.length || modules.length != initDatas.length) {
return ERC4337Utils.SIG_VALIDATION_FAILED;
}
bytes32 structHash = _initModulesStructHash(moduleTypeIds, modules, initDatas, userOp.nonce);
if (
isValidSignature(_hashTypedDataV4(structHash), installationSignature) !=
IERC1271.isValidSignature.selector
) {
return ERC4337Utils.SIG_VALIDATION_FAILED;
}
for (uint256 i = 0; i < modules.length; i++) {
_installModule(moduleTypeIds[i], modules[i], initDatas[i]);
}
return super._validateUserOp(userOp, userOpHash);
}
function _initModulesStructHash(
uint256[] memory moduleTypeIds,
address[] memory modules,
bytes[] memory initDatas,
uint256 nonce
) internal pure returns (bytes32) {
bytes32[] memory initDataHashes = new bytes32[](initDatas.length);
for (uint256 i = 0; i < initDatas.length; i++) {
initDataHashes[i] = keccak256(initDatas[i]);
}
return keccak256(abi.encode(
INIT_MODULES_TYPEHASH,
keccak256(abi.encodePacked(moduleTypeIds)),
keccak256(abi.encodePacked(modules)),
keccak256(abi.encodePacked(initDataHashes)),
nonce
));
}
function _tryDecodeInitModeData(
bytes calldata data
)
internal
view
returns (
bool success,
uint256[] memory moduleTypeIds,
address[] memory modules,
bytes[] memory initDatas,
bytes memory installationSignature
)
{
try this._decodeInitModeData(data) returns (
uint256[] memory _moduleTypeIds,
address[] memory _modules,
bytes[] memory _initDatas,
bytes memory _installationSignature
) {
return (true, _moduleTypeIds, _modules, _initDatas, _installationSignature);
} catch {
return (false, new uint256[](0), new address[](0), new bytes[](0), "");
}
}
function _decodeInitModeData(
bytes calldata data
)
external
pure
returns (
uint256[] memory moduleTypeIds,
address[] memory modules,
bytes[] memory initDatas,
bytes memory installationSignature
)
{
return abi.decode(data, (uint256[], address[], bytes[], bytes));
}
}Security Considerations
- Authorization: Module installations are authorized by the account owner's EIP-712
installationSignature, providing human-readable wallet display of what is being authorized independently of whoever signs the user operation. - Replay Protection: The EIP-712
InitModulesstruct includesuserOp.nonce, ensuring theinstallationSignaturecannot be reused once the nonce is consumed. The EIP-712 domain separator additionally binds the signature to the specific account address and chain ID. - Installation Ordering: Modules should be installed only after
installationSignatureis successfully verified. This prevents unauthorized module installation in the event that signature verification fails. - Module Validation: Standard ERC-7579 module validation applies (type checking, duplicate prevention, etc.).
- Array Length Validation: Implementations should ensure that
moduleTypeIds,modules, andinitDatasarrays have the same length to prevent mismatched installations. - EntryPoint Version: This ERC requires the v0.9 EntryPoint. Accounts shouldn't assume init mode is available without confirming the EntryPoint version supports non-empty
initCodefor already-deployed accounts. IgnoredInitCodeEvent: The v0.9 EntryPoint emitsIgnoredInitCodefor every init mode user operation. Security monitoring tools should not treat this event alone as an anomaly.
Copyright
Copyright and related rights waived via CC0.
