Skip to content

Deployed ONLY_ENTRYPOINT hook does not conform to Kernel's IHook: postCheck signature mismatch reverts every fallback-module call (v3.3 and v4) #62

Description

@naota70

Moved from zerodevapp/sdk#272 — the hook contract lives in this repo, so this is the better home.

Summary

The deployed only-entrypoint hook at 0xb230f0A1C7C95fa11001647383c8C7a8F316b900 — exposed by
@zerodev/sdk as ONLY_ENTRYPOINT_HOOK_ADDRESS — does not conform to Kernel's IHook.
It implements postCheck(bytes,bool,bytes) (0xaacbd72a), while Kernel calls
postCheck(bytes) (0x173bf7da).

Consequence: every call routed through a fallback module installed with this hook reverts,
after the fallback body has already executed successfully. Kernel refuses hook = address(1)
for MODULE_TYPE_FALLBACK, so a fallback module cannot be installed without a hook, and the SDK
ships no alternative hook address. This makes the documented
account recovery flow
unusable on Kernel v3.3.

Verified on Polygon mainnet (chain 137) and on an anvil fork.

This is not fixed by Kernel v4

Both release/v3.3 and release/v4 of zerodevapp/kernel declare the one-argument form in
src/interfaces/IERC7579Modules.sol:

interface IHook is IModule {
    function preCheck(address msgSender, uint256 msgValue, bytes calldata msgData)
        external payable returns (bytes memory hookData);
    function postCheck(bytes calldata hookData) external payable;
}

So Kernel is the conforming side in both versions; the deployed hook is the one that needs
updating (or replacing).

Environment

  • @zerodev/sdk 5.5.10 (latest on npm)
  • Kernel v3.3, account implementation 0xd6CEDDe84be40893d153Be9d467CD6aD37875b28
    (matches KernelVersionToAddressesMap["0.3.3"].accountImplementationAddress)
  • EntryPoint v0.7 0x0000000071727De22E5E9d8BAf0edAc6f37da032
  • Recovery executor 0xe884C2868CC82c16177eC73a93f7D9E6F3A5DC6E
  • Chain: Polygon mainnet

Reproduction

  1. Create a Kernel v3.3 account.
  2. Install the recovery executor as a fallback module bound to doRecovery(address,bytes)
    (0xac39fd0f), using constants.ONLY_ENTRYPOINT_HOOK_ADDRESS as the hook — i.e. the
    standard recovery setup.
  3. Install a guardian validator and grantAccess(guardianVId, 0xac39fd0f, true).
  4. Send a UserOperation from the guardian with calldata doRecovery(webAuthnValidator, newKeyData).

eth_estimateUserOperationGas fails with
UserOperation reverted during simulation with reason: 0x.

Trace (anvil fork of Polygon)

account::doRecovery(0x7ab16Ff3…, 0xe2b97072…)
 └ 0xd6CEDDe8…::doRecovery(...) [delegatecall]
    ├ 0xb230f0A1…::preCheck(0x00000000717…, 0, 0xac39fd0f…)   → returns hookData
    ├ 0xe884C286…::doRecovery(...) [delegatecall]
    │  ├ 0x7ab16Ff3…::onUninstall(0x)                          → ok
    │  └ 0x7ab16Ff3…::onInstall(0xe2b97072…)
    │     └ emit WebAuthnRegistered(account, pubX, pubY)       → correct new key installed
    │
    └ 0xb230f0A1…::postCheck(0x)                               → REVERT (empty)
 ← REVERT (whole UserOperation rolled back)

The recovery body works correctly — the intended new passkey is registered. Only postCheck
fails, and it rolls everything back.

Evidence for the signature mismatch

Direct calls against the deployed hook (cast call, --from the account):

call result
postCheck(bytes)0x173bf7da reverts
postCheck(bytes,bool,bytes)0xaacbd72a succeeds
preCheck(address,uint256,bytes) succeeds (correctly reverts with only entrypoint for non-EntryPoint callers)

Selector 0x173bf7da is absent from the hook's bytecode; 0xaacbd72a is present.

Why integrators cannot work around it

installModule(3, executor, selector ‖ 0x…01 ‖ abi.encode(selectorData, hookData)) — using
address(1) as the "no hook" sentinel — reverts immediately on Kernel v3.3
(gas 1442, no external calls). With a real hook address the same call succeeds, invoking
hook.isInitialized(account) and emitting ModuleInstalled(4, hook) / ModuleInstalled(3, executor).

So a fallback module requires a real, conforming hook contract, and the only hook address the
SDK exposes is the non-conforming one. toKernelPluginManager.js also uses it as the default
hook for action plugins (action.hook?.address ?? ONLY_ENTRYPOINT_HOOK_ADDRESS), so anything
built on plugins.action hits this.

Related

PR #29 ("feat: update CallerHook postCheck interface and update mappings", 2025-09-16) changes
exactly this interface and switches the kernel dependency to kernel_v3, but it was closed
without merging and targeted the feat/caller branch. The hooks/ sources are not on the
default branch, so this cannot be fixed by an outside PR.

Suggested resolution

Either:

  1. Deploy an only-entrypoint hook implementing postCheck(bytes) and point
    ONLY_ENTRYPOINT_HOOK_ADDRESS at it (version-keyed if older Kernel versions need the
    three-argument form), or
  2. Document that fallback modules / the SDK recovery flow are unsupported on Kernel v3.3+.

Note

The recovery mechanism itself is sound. Swapping the root passkey only needs
onUninstall() + onInstall(newKey) on the WebAuthn validator, called from the account —
verified on a fork. The fix is confined to the hook contract.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions