Skip to content

Repository files navigation

Kernel v4

ERC-4337 / ERC-7702 modular smart account with pluggable validation, execution, and hook modules. Implements ERC-7579 for standardized module interfaces.

Key Features

Modular Architecture (ERC-7579)

Six pluggable module types that can be installed and uninstalled at runtime:

TypeRole
ValidatorValidates UserOps and signatures — owns a nonce key namespace
ExecutorCalls executeFromExecutor to perform actions on behalf of the account
FallbackExtends the account with new function selectors (call or delegatecall)
HookPre/post execution checks on validators, executors, and fallback selectors
PolicyPart of a permission — enforces rules (e.g. spending limits, target allowlists)
SignerPart of a permission — provides the signature verification (e.g. passkey, multisig)

Permission System

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.

Three Account Variants

  • KernelUUPS — UUPS upgradeable proxy. Standard deployment via KernelFactory.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 can setRoot to a different validator and the proxy is still UUPS-upgradeable. Deployed via KernelFactory.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.

Enable Mode

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.

Nonce-Encoded Validation

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.

Hook System

Hooks provide pre/post execution checks. They bind to validators, executors, and fallback selectors independently:

  • Validator hook — Runs around executeUserOp when a non-root validator with a hook is used
  • Executor hook — Runs around executeFromExecutor for any installed executor
  • Fallback hook — Runs around fallback selector dispatch

Two sentinel values: address(0) = not installed, address(1) = installed with no hook.

Signature Verification (ERC-1271 / ERC-7739)

Three signature modes for isValidSignature:

  1. Raw — Direct hash signing (only on Kernel7702 where the EOA is the signer)
  2. Chain-specific nested EIP-712 — Wraps the hash in a TypedDataSign struct bound to chain ID
  3. 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.

Standards

StandardSupport
ERC-4337Account abstraction via EntryPoint v0.9
ERC-7579Modular smart account interfaces
ERC-7702EOA code delegation (Kernel7702)
ERC-7739Nested EIP-712 for safe isValidSignature
ERC-7201Namespaced storage for upgrade safety
ERC-1271Smart contract signature validation

Data Encoding

UserOp Nonce

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):

ValueMeaning
0x00Standard — chain-specific, no inline install
0x08Enable — install modules inline, chain-specific enable signature
0x0CEnable + replayable enable signature
0x40Replayable — chain-agnostic userOp hash
0x48Enable + replayable userOp hash
0x4CEnable + replayable enable signature + replayable userOp hash

vType (ValidationType):

ValueMeaningvId contains
0x00Root / FallbackIgnored (uses stored root)
0x01Validator20-byte validator address
0x02Permission4-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.

UserOp Signature

Standard Mode (no enable flag)

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.

Enable Mode (enable flag set in vMode)

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).

ERC-1271 Signature (isValidSignature)

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:

vTypeNvId content
0x00 (root)0Uses stored root, inner = signature[2:]
0x01 (validator)20Validator address, inner = signature[22:]
0x02 (permission)4PermissionId, inner = signature[6:]

Standard Mode (no enable flag)

The inner signature is verified via _verifySignature against the installed validator or permission, same as UserOp standard mode.

Enable Mode for ERC-1271

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:

  1. Verifies the install signature is valid (same digest as UserOp enable mode)
  2. Checks the nonce is correct (view-only, no increment)
  3. Uses stateless verification — finds the validator/permission modules inside the packages array and calls IStatelessValidatorWithSender.validateSignatureWithDataWithSender instead 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.

Nested EIP-712 Wrapping

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.

installModule / uninstallModule

function installModule(uint256moduleType, addressmodule, bytescalldatainitData) externalpayable;
function uninstallModule(uint256moduleType, addressmodule, bytescalldatainitData) externalpayable;

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)
}))

internalData for Install

Module TypeinternalData 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:

AddressMeaning
address(0)Not installed / entry-point-only (for fallback: only EntryPoint can call)
address(1)Installed with no hook
OtherHook contract address (must be installed as hook module first)

callType for fallback (type 3):

ValueMeaning
0x00call — regular call, appends msg.sender to calldata
0xFFdelegatecall — executes in Kernel's storage context

internalData for Uninstall

Module TypeinternalData 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

Batch Install via Install[]

Three entry points for batch installation:

// 1. During account creation — first package becomes rootinitialize(Install[] calldatapackages)
// 2. From EntryPoint or selfinstallModule(Install[] calldatapackages)
// 3. With root validator signature (no EntryPoint needed)installModule(boolreplayable, uint256nonce, Install[] calldatapackages, bytescalldatasignature)

Each Install struct:

struct Install {
uint256 moduleType; // 1-6address module; // module contract addressbytes moduleData; // forwarded to onInstallbytes 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.

Architecture

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

Storage

All storage uses ERC-7201 namespaced slots to avoid collisions across modules and upgrades:

ManagerSlot
ValidationManagerkeccak256("kernel.v4.validation") - 1
ModuleManagerkeccak256("kernel.v4.module") - 1
ExecutorManagerkeccak256("kernel.v4.executor") - 1
HookManagerkeccak256("kernel.v4.hook") - 1
SelectorManagerkeccak256("kernel.v4.selector") - 1

Project Structure

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

Getting Started

Prerequisites

  • Foundry
  • Soldeer (dependency manager, bundled with Foundry)
  • Solidity 0.8.33+

Install

git clone git@github.com:zerodevapp/kernel_v4.git
cd kernel_v4
forge soldeer install
forge build

Testing

# 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)
halmos

Coverage

forge 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

Dependencies

PackageVersion
Solady0.1.26
account-abstractionv0.9.0
OpenZeppelin Contracts5.4.0
forge-std1.11.0

Configuration

Key settings in foundry.toml:

  • EVM version: Prague (transient storage support)
  • Solc: 0.8.33
  • Optimizer: 200 runs
  • Invariant: 1000 runs, 1000 depth

License

MIT

Releases

Packages

Used by

Contributors

Languages