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
- Create a Kernel v3.3 account.
- 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.
- Install a guardian validator and
grantAccess(guardianVId, 0xac39fd0f, true).
- 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:
- 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
- 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.
Summary
The deployed only-entrypoint hook at
0xb230f0A1C7C95fa11001647383c8C7a8F316b900— exposed by@zerodev/sdkasONLY_ENTRYPOINT_HOOK_ADDRESS— does not conform to Kernel'sIHook.It implements
postCheck(bytes,bool,bytes)(0xaacbd72a), while Kernel callspostCheck(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 SDKships 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.3andrelease/v4ofzerodevapp/kerneldeclare the one-argument form insrc/interfaces/IERC7579Modules.sol:So Kernel is the conforming side in both versions; the deployed hook is the one that needs
updating (or replacing).
Environment
@zerodev/sdk5.5.10 (latest on npm)0xd6CEDDe84be40893d153Be9d467CD6aD37875b28(matches
KernelVersionToAddressesMap["0.3.3"].accountImplementationAddress)0x0000000071727De22E5E9d8BAf0edAc6f37da0320xe884C2868CC82c16177eC73a93f7D9E6F3A5DC6EReproduction
doRecovery(address,bytes)(
0xac39fd0f), usingconstants.ONLY_ENTRYPOINT_HOOK_ADDRESSas the hook — i.e. thestandard recovery setup.
grantAccess(guardianVId, 0xac39fd0f, true).doRecovery(webAuthnValidator, newKeyData).eth_estimateUserOperationGasfails withUserOperation reverted during simulation with reason: 0x.Trace (anvil fork of Polygon)
The recovery body works correctly — the intended new passkey is registered. Only
postCheckfails, and it rolls everything back.
Evidence for the signature mismatch
Direct calls against the deployed hook (
cast call,--fromthe account):postCheck(bytes)—0x173bf7dapostCheck(bytes,bool,bytes)—0xaacbd72apreCheck(address,uint256,bytes)only entrypointfor non-EntryPoint callers)Selector
0x173bf7dais absent from the hook's bytecode;0xaacbd72ais present.Why integrators cannot work around it
installModule(3, executor, selector ‖ 0x…01 ‖ abi.encode(selectorData, hookData))— usingaddress(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 emittingModuleInstalled(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.jsalso uses it as the defaulthook for action plugins (
action.hook?.address ?? ONLY_ENTRYPOINT_HOOK_ADDRESS), so anythingbuilt on
plugins.actionhits 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 closedwithout merging and targeted the
feat/callerbranch. Thehooks/sources are not on thedefault branch, so this cannot be fixed by an outside PR.
Suggested resolution
Either:
postCheck(bytes)and pointONLY_ENTRYPOINT_HOOK_ADDRESSat it (version-keyed if older Kernel versions need thethree-argument form), or
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.