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
| Contract | Address |
|---|---|
| EntryPoint v0.7 (shared standard, not ours) | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
| MerkiKeyholdForge | not deployed yet |
| MerkiKeyhold (implementation) | not deployed yet |
| USDG | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
| WETH | 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73 |
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
| Function | Who | What |
|---|---|---|
validateUserOp(op, hash, missing) | EntryPoint | Check the signature, rotate, pay the prefund if asked |
execute(target, value, data) | EntryPoint or self | One call; a revert reverts the operation's calls |
executeBatch(calls) | EntryPoint or self | Several calls, all or nothing |
replaceRecoveryKey(next) | self | New recovery key, e.g. after one was revealed |
withdrawDepositTo(to, amount) | self | Move EntryPoint deposit out |
keyState() | anyone | Main hash and index, recovery hash and index, nonce |
Events: KeyholdOpened, KeyRotated(mainIndex, mainKeyHash), KeyRecovered(mainIndex, mainKeyHash, recoveryIndex, recoveryKeyHash), RecoveryKeyReplaced.
MerkiKeyholdForge
| Function | What |
|---|---|
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.