Signing Due Signables from Kernel v3 Smart Accounts
This guide explains how to properly sign Due's Permit and PayoutIntent messages when your account is a Kernel v3 (ZeroDev) smart contract account, including accounts set up via EIP-7702.
Why Standard Signatures Don't Work
Both Due's contracts and modern token contracts (e.g. USDC) use SignatureChecker for signature verification, which supports:
- EOA accounts - standard ECDSA recovery (
ecrecover) - Smart contract accounts - ERC-1271
isValidSignature(hash, signature)
When your address has contract code (Kernel via EIP-7702), the verifying contract detects this and calls isValidSignature(hash, signature) on your account instead of using ecrecover. Kernel then wraps the original hash in its own EIP-712 domain before verifying the ECDSA signature. This means you must sign the wrapped hash, not the original one.
How Kernel's Signature Verification Works
When isValidSignature(bytes32 hash, bytes signature) is called on your Kernel account:
- The first byte of
signatureis stripped as the mode byte (validation routing) - The original
hashis wrapped in Kernel's EIP-712 typed data:wrappedHash = keccak256(0x19 0x01 || kernelDomainSeparator || keccak256(abi.encode(KERNEL_WRAPPER_TYPE_HASH, hash))) - The remaining signature bytes are verified via ECDSA against the
wrappedHash
The Kernel EIP-712 domain is:
{
name: "Kernel",
version: "0.3.3",
chainId: <chain ID>,
verifyingContract: <your Kernel account address>
}
The wrapper type:
Kernel(bytes32 hash)
Tip: You can query the domain parameters directly from your Kernel account by calling
eip712Domain()(EIP-5267). This is more robust than hardcoding values, especially if Kernel versions change.
Step-by-Step Signing Process
For each signable returned by Due's API:
1. Get the original hash
Due's API returns a TransferIntent with signables[]. Each signable has:
payload- the full EIP-712 typed data (domain, types, message)hash- the EIP-712 digest to sign
2. Sign the hash using Kernel's EIP-712 wrapper
Instead of signing hash directly, sign it as the hash field of a Kernel EIP-712 message:
import { TypedDataEncoder } from "ethers"
// The hash from Due's signable
const originalHash: string = signable.hash
// Sign using Kernel's wrapper domain
const kernelSignature = await signer.signTypedData(
{
name: "Kernel",
version: "0.3.3",
chainId: chainId,
verifyingContract: yourKernelAccountAddress,
},
{
Kernel: [{ name: "hash", type: "bytes32" }],
},
{
hash: originalHash,
},
)3. Prepend the mode byte
Kernel uses the first byte of the signature to determine the validation mode. For accounts using the root ECDSA validator (the default for most Kernel 7702 accounts), the mode byte is 0x00:
import { concat, hexlify } from "ethers"
const finalSignature = concat([
hexlify(new Uint8Array([0x00])), // sudo/root mode
kernelSignature,
])The resulting signature is 66 bytes: 1 byte mode + 65 bytes ECDSA.
4. Return as the signable's signature
Set signable.signature = finalSignature and submit the TransferIntent back to Due's API.
Complete Code Example
import { HDNodeWallet, concat, hexlify } from "ethers"
/**
* Signs a Due signable hash for a Kernel v3 smart account.
*
* @param signer EOA private key holder of the Kernel account
* @param kernelAccountAddress Your Kernel smart account address
* @param chainId Chain ID of the network
* @param originalHash The `hash` field from Due's signable
* @param modeByte Kernel validation mode (default 0x00 = sudo/root)
* @returns 66-byte signature ready to submit to Due
*/
async function signForKernel(
signer: HDNodeWallet,
kernelAccountAddress: string,
chainId: bigint,
originalHash: string,
modeByte: number = 0x00,
): Promise<string> {
const signature = await signer.signTypedData(
{
name: "Kernel",
version: "0.3.3",
chainId,
verifyingContract: kernelAccountAddress,
},
{
Kernel: [{ name: "hash", type: "bytes32" }],
},
{ hash: originalHash },
)
return concat([hexlify(new Uint8Array([modeByte])), signature])
}Usage with viem
import { hashTypedData, signTypedData, concat, toHex } from "viem"
import { privateKeyToAccount } from "viem/accounts"
async function signForKernelViem(
privateKey: `0x${string}`,
kernelAccountAddress: `0x${string}`,
chainId: number,
originalHash: `0x${string}`,
modeByte: number = 0x00,
): Promise<`0x${string}`> {
const account = privateKeyToAccount(privateKey)
const signature = await signTypedData({
privateKey,
domain: {
name: "Kernel",
version: "0.3.3",
chainId: BigInt(chainId),
verifyingContract: kernelAccountAddress,
},
types: {
Kernel: [{ name: "hash", type: "bytes32" }],
},
primaryType: "Kernel",
message: { hash: originalHash },
})
return concat([toHex(modeByte, { size: 1 }), signature])
}Common Pitfalls
-
Wrong
verifyingContract- Must be your Kernel account address (the smart account), not the Kernel implementation contract address. -
Wrong version string - Must exactly match what your Kernel contract returns from
_domainNameAndVersion(). Current version is"0.3.3". You can verify by callingeip712Domain()on your account. -
Missing mode byte - The signature must start with the mode byte (
0x00for sudo mode). Without it, Kernel will misparse the signature. -
Double-hashing - Do NOT compute the EIP-712 hash yourself from the typed data. Use the
hashfield from Due's signable directly. The token/payout contract computes the hash on-chain, andisValidSignaturereceives that exact hash. -
Signing the typed data directly - You must NOT call
signTypedDatawith the original permit/payout typed data. Instead, compute the hash first and sign it through Kernel's wrapper. The original typed data is only used to display the signing request to the user.
Validation Mode Bytes
Kernel v3 supports different validation modes via the first signature byte:
| Mode | Byte | Description |
|---|---|---|
| Sudo/Root | 0x00 | Uses the account's root validator (default ECDSA) |
| Validator | 0x01 | Routes to a specific validator (21-byte prefix) |
| Permission | 0x02 | Routes to a permission policy (5-byte prefix) |
Most EIP-7702 Kernel accounts use 0x00 (sudo mode) with the root ECDSA validator.
Reference
- Kernel v3 source
- Kernel signature wrapping (
_toWrappedHash) - Kernel constants (
KERNEL_WRAPPER_TYPE_HASH) - ZeroDev EIP-7702 quickstart
Updated 2 days ago