ERC-4337 / ERC-7702 modular smart account with pluggable validation and execution modules. Implements ERC-7579 for standardized module interfaces.
Six supported module types 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) |
| Policy | Part of a permission — enforces rules (e.g. spending limits, target allowlists) |
| Signer | Part of a permission — provides signature verification (e.g. passkey, multisig) |
| Scoped Execution Hook | Optional pre/post checks scoped to one validation, executor, or selector (type 11) |
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.
An optional type-11 scoped execution hook runs before and after execution in one of three scopes: validation, executor, or selector. Hooked non-root validations are routed through executeUserOp; root validations bypass hooks. Executor hooks wrap executeFromExecutor, and selector hooks wrap fallback selector dispatch. Selector hooks apply only to calls that reach Kernel's fallback; native Kernel function dispatch bypasses them, and built-in token-receiver selectors cannot be installed as fallback targets.
Fallback access control: a fallback selector with no scoped execution hook installed is callable only by the EntryPoint. Installing a selector-scoped execution hook makes the selector publicly callable, with the hook gating direct access via preCheck/postCheck.
preCheck and postCheck receive the same Kernel-generated bytes32 id. The ID layout is [bytes1 scope][target][zero padding], where the target is a 21-byte ValidationId, 20-byte executor address, or 4-byte selector. Utils.sol exposes generation and decoding helpers for every scope.
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
Structured signatures select root, validator, or permission validation through the leading vType and optional ID. Raw Kernel7702 signatures have no validation header.
| 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 | N bytes | remaining bytes |
| vType | vId | inner signature |
Where N depends on vType:
| vType | N | vId content |
|---|---|---|
0x00 (root) |
0 | Uses stored root, inner = signature[1:] |
0x01 (validator) |
20 | Validator address, inner = signature[21:] |
0x02 (permission) |
4 | PermissionId, inner = signature[5:] |
The inner signature is verified via _verifySignature against the installed validator or permission. ERC-1271 has no validation-mode byte and does not support enable mode; validation modes remain part of ERC-4337 UserOperation nonces only.
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) | [bytes4 selector₁][bytes4 selector₂]... |
| Executor (2) | Empty (required) |
| Fallback (3) | Exactly [bytes4 selector][bytes1 callType] |
| Policy (5) | [bytes4 permissionId] |
| Signer (6) | [bytes4 permissionId][bytes4 selector₁]... |
| Scoped Execution Hook (11) | [bytes1 scope][target] (validation: 21-byte ValidationId; executor: 20-byte address; selector: 4-byte selector) |
Executor lifecycle: installation requires the low-level onInstall call to succeed; a failed
callback reverts with ModuleInstallFailed before granting executor authority. Plain EOAs remain
supported because calling an address without code succeeds. Contract executors must accept the
callback and correctly initialize their authorization state; contracts that reject onInstall
cannot be installed. Uninstallation revokes Kernel authority before calling onUninstall, and
failed cleanup does not block removal. Reinstalling after failed cleanup still requires a
successful onInstall, preventing a failed installation from reactivating stale authorization
(TOB-KERNEL-11).
Execution-hook scopes are 0x01 for validation, 0x02 for executor, and 0x03 for selector.
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) | Empty (required) |
| Fallback (3) | Exactly [bytes4 selector] |
| Policy (5) | [bytes4 permissionId] — must uninstall in LIFO order (last installed first) |
| Signer (6) | [bytes4 permissionId] — all policies and its scoped execution hook must be removed first |
| Scoped Execution Hook (11) | [bytes1 scope][target] (same format as installation) |
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, 2, 3, 5, 6, or 11
address module; // module contract address
bytes moduleData; // forwarded to onInstall
bytes internalData; // kernel config (same format as table above)
}Permission install order: policies (type 5) come first, followed by exactly one signer (type 6), then an optional scoped execution hook (type 11), all sharing the same PermissionId. The hook targets the permission's full 21-byte ValidationId and requires the signer-completed permission to exist. Validator, executor, and selector hooks are installed after their respective targets.
Kernel (abstract)
├── ModuleManager
│ ├── ValidationManager — Validator/permission lifecycle, enable-mode, nonce mgmt
│ ├── ExecutorManager — Executor install/uninstall state
│ └── 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 |
| 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
- Revocation finality within a bundle (TOB-KERNEL-3, acknowledged): Kernel checks that the
selected validator or root is installed during
validateUserOpand does not re-verify it at execution time. Because the EntryPoint validates every operation in a bundle before executing any of them, a validator uninstall or root replacement that executes earlier in ahandleOpsbundle does not invalidate a later operation in that same bundle, which was already validated against the now-revoked authority. This grants no authority beyond what the revoked key already held and is equivalent to the revocation being ordered after the attacker's transaction; treat revocations of compromised keys as racing the key until the revoking transaction is mined.
MIT