MERKI

Build

Contracts & signer

MERKI contract reference: functions, events, signature byte layout and deployment check for the keyhold, the forge and the hash-chain verifier.

Addresses

ContractAddress
EntryPoint v0.7 (shared standard, not ours)0x0000000071727De22E5E9d8BAf0edAc6f37da032
MerkiKeyholdForgenot deployed yet
MerkiKeyhold (implementation)not deployed yet
USDG0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
WETH0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73

Signature layout

main      0x00 ‖ nextMain(32) ‖ seed(32) ‖ values(67 × 32)                    = 2,209 bytes
recovery  0x01 ‖ nextMain(32) ‖ nextRecovery(32) ‖ seed(32) ‖ values(67 × 32) = 2,241 bytes

digest(main)     = keccak256(abi.encode(MAIN_TAG, userOpHash, nextMain))
digest(recovery) = keccak256(abi.encode(RECOVERY_TAG, userOpHash, nextMain, nextRecovery))
MAIN_TAG         = keccak256("MERKI.keyhold.main.v1")
RECOVERY_TAG     = keccak256("MERKI.keyhold.recovery.v1")

The next key hashes must be non-zero and differ from the stored keys and from each other. A signature that fails returns SIG_VALIDATION_FAILED and changes nothing.

MerkiKeyhold

FunctionWhoWhat
validateUserOp(op, hash, missing)EntryPointCheck the signature, rotate, pay the prefund if asked
execute(target, value, data)EntryPoint or selfOne call; a revert reverts the operation's calls
executeBatch(calls)EntryPoint or selfSeveral calls, all or nothing
replaceRecoveryKey(next)selfNew recovery key, e.g. after one was revealed
withdrawDepositTo(to, amount)selfMove EntryPoint deposit out
keyState()anyoneMain hash and index, recovery hash and index, nonce

Events: KeyholdOpened, KeyRotated(mainIndex, mainKeyHash), KeyRecovered(mainIndex, mainKeyHash, recoveryIndex, recoveryKeyHash), RecoveryKeyReplaced.

MerkiKeyholdForge

FunctionWhat
createKeyhold(main, recovery, salt)Deploy the keyhold, or return it if it exists. Anyone may call it.
keyholdAddress(main, recovery, salt)The address, deployed or not.
implementation()The shared keyhold code (locked).

Tests

  • Unit and fuzz tests against a locally compiled EntryPoint v0.7: rotation, replay, retired keys, swapped next keys, bit flips anywhere in the signature, truncation, recovery, access control, batches, prefund.
  • Invariant tests: the stored key always matches its index, no used key ever becomes current again, funds leave only through accepted operations.
  • Fork tests on Robinhood Chain mainnet with the canonical EntryPoint, real WETH and real USDG.
  • Vectors written by the browser signer and replayed by the Solidity verifier, so both sides agree byte for byte.

Checking a deployment

cd contracts && ./setup.sh && forge build --force && cd ..
npm run contracts:artifacts -- --check
npm run verify-deployment -- --forge <address> [--forge-tx <hash>] [--keyhold <address>]

The check compares runtime code byte for byte, decodes every immutable (EntryPoint, implementation, forge), confirms the implementation is locked, the EntryPoint code is canonical and the creation input carries exactly this build. Any failure exits non-zero.