ERC-4337 / ERC-7702 modular smart account with pluggable validation, execution, and hook modules. Implements ERC-7579 for standardized module interfaces.
Six pluggable module types that can be installed and uninstalled at runtime:
| Type | Role |
|---|---|
| Validator | Validates UserOps and signatures — owns a nonce key namespace |
| Executor | Calls executeFromExecutor to perform actions on behalf of the account |
| Fallback | Extends the account with new function selectors (call or delegatecall) |
| Hook | Pre/post execution checks on validators, executors, and fallback selectors |
| Policy | Part of a permission — enforces rules (e.g. spending limits, target allowlists) |
| Signer | Part of a permission — provides the signature verification (e.g. passkey, multisig) |
Beyond simple validator modules, Kernel v4 supports permissions — a composition of one signer and one or more policies. This enables granular session keys: a dapp gets a signer that can only call specific selectors, under specific policies (spending caps, time windows, target restrictions), without touching the root validator.
KernelUUPS— UUPS upgradeable proxy. Standard deployment viaKernelFactory.deploy(). Supports full module lifecycle and proxy upgrades via UserOp.KernelImmutableECDSA— ERC-1967 clone with an ECDSA fallback signer in immutable args. The fallback signer is fixed at deploy time, but users cansetRootto a different validator and the proxy is still UUPS-upgradeable. Deployed viaKernelFactory.deployECDSA().Kernel7702— EIP-7702 variant for EOA delegation. The EOA itself is the fallback signer (ECDSA.recover == address(this)). No initialization needed. Supports raw ERC-1271 signatures.
Install modules atomically with the first UserOp — no separate setup transaction. The UserOp nonce encodes an enable-mode flag; the signature carries both the install payload (with root validator approval) and the UserOp signature. Supports chain-specific and replayable (chain-agnostic) variants.
The 32-byte ERC-4337 nonce encodes which validator to use, giving each validator/permission its own nonce namespace. See Data Encoding > UserOp Nonce for the full layout.
Hooks provide pre/post execution checks. They bind to validators, executors, and fallback selectors independently:
- Validator hook — Runs around
executeUserOpwhen a non-root validator with a hook is used - Executor hook — Runs around
executeFromExecutorfor any installed executor - Fallback hook — Runs around fallback selector dispatch
Two sentinel values: address(0) = not installed, address(1) = installed with no hook.
Three signature modes for isValidSignature:
- Raw — Direct hash signing (only on
Kernel7702where the EOA is the signer) - Chain-specific nested EIP-712 — Wraps the hash in a
TypedDataSignstruct bound to chain ID - Replayable nested EIP-712 — Same wrapping but without chain ID, valid across chains
All modes support both validator-based and permission-based signature verification, selected by the first 21 bytes of the signature.
| Standard | Support |
|---|---|
| ERC-4337 | Account abstraction via EntryPoint v0.9 |
| ERC-7579 | Modular smart account interfaces |
| ERC-7702 | EOA code delegation (Kernel7702) |
| ERC-7739 | Nested EIP-712 for safe isValidSignature |
| ERC-7201 | Namespaced storage for upgrade safety |
| ERC-1271 | Smart contract signature validation |
The 32-byte ERC-4337 nonce encodes the validation mode, type, and identifier:
| 1 byte | 1 byte | 20 bytes | 2 bytes | 8 bytes |
| vMode | vType | vId | nonceKey | seq |
vMode (ValidationMode flags):
| Value | Meaning |
|---|---|
0x00 |
Standard — chain-specific, no inline install |
0x08 |
Enable — install modules inline, chain-specific enable signature |
0x0C |
Enable + replayable enable signature |
0x40 |
Replayable — chain-agnostic userOp hash |
0x48 |
Enable + replayable userOp hash |
0x4C |
Enable + replayable enable signature + replayable userOp hash |
vType (ValidationType):
| Value | Meaning | vId contains |
|---|---|---|
0x00 |
Root / Fallback | Ignored (uses stored root) |
0x01 |
Validator | 20-byte validator address |
0x02 |
Permission | 4-byte PermissionId (left-aligned, rest zero) |
nonceKey + seq: Each (vType, vId) combination has independent nonce namespaces via the nonceKey. The seq is incremented by the EntryPoint per standard ERC-4337 nonce management.
For validator (vType=0x01 or root resolving to validator):
[raw signature bytes]
Passed directly to IValidator.validateUserOp(userOp, userOpHash).
For permission (vType=0x02 or root resolving to permission):
abi.encode(PermissionSignature({
signatures: [policy1Sig, policy2Sig, ..., signerSig]
}))
One signature per policy (in install order), plus one for the signer (last). Each policy's signature is passed to IPolicy.checkUserOpPolicy, and the signer's to ISigner.checkUserOpSignature.
abi.encode(EnableModeSignature({
nonce: uint256, // install nonce for replay protection
packages: Install[], // modules to install
enableSignature: bytes, // root validator's signature over the install digest
userOpSignature: bytes // the actual validation signature (standard or permission format)
}))
The install digest is:
EIP-712 hash of InstallPackages(uint256 nonce, Install[] packages)
If the enable-replayable flag (0x04) is set, the digest uses the chain-agnostic domain separator (no chainId).
After ERC-6492 unwrapping, the signature is parsed as:
| 1 byte | 1 byte | N bytes | remaining bytes |
| vMode | vType | vId | inner signature |
Where N depends on vType:
| vType | N | vId content |
|---|---|---|
0x00 (root) |
0 | Uses stored root, inner = signature[2:] |
0x01 (validator) |
20 | Validator address, inner = signature[22:] |
0x02 (permission) |
4 | PermissionId, inner = signature[6:] |
The inner signature is verified via _verifySignature against the installed validator or permission, same as UserOp standard mode.
Since isValidSignature is a view function, enable mode works differently than in UserOps — it cannot modify state (no module installation, no nonce increment). Instead it:
- Verifies the install signature is valid (same digest as UserOp enable mode)
- Checks the nonce is correct (view-only, no increment)
- Uses stateless verification — finds the validator/permission modules inside the
packagesarray and callsIStatelessValidatorWithSender.validateSignatureWithDataWithSenderinstead of the normal installed module
The inner signature format is the same EnableModeSignature:
abi.encode(EnableModeSignature({
nonce: uint256,
packages: Install[],
enableSignature: bytes, // root validator's signature over the install digest
userOpSignature: bytes // verified statelessly against modules in packages
}))
For permission-based enable mode, userOpSignature is a PermissionSignature — one signature per policy/signer found in the packages with the matching PermissionId.
Note: vType cannot be root (
0x00) in enable mode — it must specify an explicit validator or permission.
The above describes the validation layer (which validator/permission to use). Independently, ERC-7739 wraps the hash before it reaches the validation layer:
TypedDataSign — The inner signature is:
[r | s | v | APP_DOMAIN_SEPARATOR (32 bytes) | contents (32 bytes) | contentsDescription | uint16(contentsDescription.length)]
PersonalSign — The inner signature is just [r | s | v].
The replayable variant uses a TypedDataSign struct without chainId in the account domain.
function installModule(uint256 moduleType, address module, bytes calldata initData) external payable;
function uninstallModule(uint256 moduleType, address module, bytes calldata initData) external payable;initData is ABI-encoded as InstallModuleDataFormat:
abi.encode(InstallModuleDataFormat({
installData: bytes, // forwarded to module's onInstall / onUninstall
internalData: bytes // kernel-internal configuration (format varies by type)
}))| Module Type | internalData format |
|---|---|
| Validator (1) | [bytes20 hookAddress][bytes4 selector₁][bytes4 selector₂]... |
| Executor (2) | [bytes20 hookAddress] |
| Fallback (3) | [bytes4 selector][bytes1 callType][bytes20 hookAddress] |
| Hook (4) | Ignored (empty OK) |
| Policy (5) | [bytes4 permissionId] |
| Signer (6) | [bytes4 permissionId][bytes20 hookAddress][bytes4 selector₁]... |
hookAddress sentinel values:
| Address | Meaning |
|---|---|
address(0) |
Not installed / entry-point-only (for fallback: only EntryPoint can call) |
address(1) |
Installed with no hook |
| Other | Hook contract address (must be installed as hook module first) |
callType for fallback (type 3):
| Value | Meaning |
|---|---|
0x00 |
call — regular call, appends msg.sender to calldata |
0xFF |
delegatecall — executes in Kernel's storage context |
| Module Type | internalData format |
|---|---|
| Validator (1) | Ignored |
| Executor (2) | Ignored |
| Fallback (3) | [bytes4 selector] (first 4 bytes used) |
| Hook (4) | Ignored |
| Policy (5) | [bytes4 permissionId] — must uninstall in LIFO order (last installed first) |
| Signer (6) | [bytes4 permissionId] — all policies must be uninstalled first |
Three entry points for batch installation:
// 1. During account creation — first package becomes root
initialize(Install[] calldata packages)
// 2. From EntryPoint or self
installModule(Install[] calldata packages)
// 3. With root validator signature (no EntryPoint needed)
installModule(bool replayable, uint256 nonce, Install[] calldata packages, bytes calldata signature)Each Install struct:
struct Install {
uint256 moduleType; // 1-6
address module; // module contract address
bytes moduleData; // forwarded to onInstall
bytes internalData; // kernel config (same format as table above)
}Permission install order: When installing a permission, all policies (type 5) for that PermissionId must come first, followed by exactly one signer (type 6) with the same PermissionId. The signer finalizes the permission. Multiple permissions can be installed in a single batch — just ensure each permission's policies+signer are grouped together.
Kernel (abstract)
├── ModuleManager
│ ├── ValidationManager — Validator/permission lifecycle, enable-mode, nonce mgmt
│ ├── ExecutorManager — Executor install/uninstall with hook binding
│ ├── HookManager — Hook install/uninstall, pre/post check dispatch
│ └── SelectorManager — Fallback handler routing by function selector
├── ExecutionManager — ERC-7579 execution modes (single/batch/delegatecall)
└── ERC1271 — ERC-1271 / ERC-7739 signature verification
Concrete implementations:
├── KernelUUPS — UUPS upgradeable proxy
├── KernelImmutableECDSA — Minimal clone with immutable signer
└── Kernel7702 — EIP-7702 EOA delegation
Supporting contracts:
├── KernelFactory — Deterministic ERC-1967 proxy deployment
└── Staker — EntryPoint staking and factory approval management
All storage uses ERC-7201 namespaced slots to avoid collisions across modules and upgrades:
| Manager | Slot |
|---|---|
| ValidationManager | keccak256("kernel.v4.validation") - 1 |
| ModuleManager | keccak256("kernel.v4.module") - 1 |
| ExecutorManager | keccak256("kernel.v4.executor") - 1 |
| HookManager | keccak256("kernel.v4.hook") - 1 |
| SelectorManager | keccak256("kernel.v4.selector") - 1 |
src/
├── Kernel.sol — Abstract base account
├── Kernel7702.sol — EIP-7702 variant
├── KernelImmutableECDSA.sol — Immutable ECDSA variant
├── KernelUUPS.sol — UUPS upgradeable variant
├── KernelFactory.sol — Deterministic proxy deployer
├── Staker.sol — EntryPoint staking manager
├── core/ — Manager contracts
├── interfaces/ — ERC-7579 interfaces
├── lib/ — ERC1271, Lib4337, Utils
└── types/ — Types, Constants, Errors, Structs
test/
├── btt/ — Branching Tree Technique tests (bulloak)
├── unit/ — Unit tests and gas benchmarks
├── integration/ — End-to-end integration tests
├── invariant/ — Invariant tests (1000 runs / 1000 depth)
├── fuzz/ — Fuzz tests
├── halmos/ — Symbolic execution (formal verification)
└── mock/ — Test mocks and helpers
git clone git@github.com:zerodevapp/kernel_v4.git
cd kernel_v4
forge soldeer install
forge build# All tests
forge test
# BTT tests only
forge test --match-path "test/btt/*.t.sol"
# Unit tests only
forge test --match-path "test/unit/*.t.sol"
# Integration tests
forge test --match-path "test/integration/*.t.sol"
# Invariant tests
forge test --match-path "test/invariant/*.t.sol"
# Fuzz tests
forge test --match-path "test/fuzz/*.t.sol"
# Halmos (symbolic execution)
halmosforge coverage \
--no-match-coverage "(script|test|Foo|Bar|validator|sdk|signer)" \
--report lcov
genhtml lcov.info \
--output-directory coverage \
--ignore-errors inconsistent \
--ignore-errors corrupt
open coverage/index.html| Package | Version |
|---|---|
| Solady | 0.1.26 |
| account-abstraction | v0.9.0 |
| OpenZeppelin Contracts | 5.4.0 |
| forge-std | 1.11.0 |
Key settings in foundry.toml:
- EVM version: Prague (transient storage support)
- Solc: 0.8.33
- Optimizer: 200 runs
- Invariant: 1000 runs, 1000 depth
MIT