marmot

Marmot docs

20 pages, from the idea to the error codes

Trust

Verification

Everything we ran, so you can run it again.

1. Contract tests (Foundry)

43 tests in contracts/test/Marmot.t.sol, including 3 fuzz tests (256 runs each).

  • Nominal flows: ETH and token withdrawals wait, then run; generic calls; no double execution; expiry at the exact last second.
  • Access control: only the owner announces; the guardian cannot announce or move funds; nobody can target the vault itself.
  • Veto: direct, signed and relayed, signed for another id, from a stranger, after execution, in the last second.
  • Panic: kills every pending announcement; the signature cannot be replayed; new announcements work afterwards.
  • Settings: delay, guardian and owner changes wait; owner rotation kills old announcements.
  • Robustness: failing recipient, insufficient balance, reentrancy.
  • Passkey: valid, without user verification, on the wrong domain, signed for another announcement.
  • Fuzz: nothing executes early; a stranger cannot veto; the guardian can never increase a balance.
cd contracts && forge test

Foundry does not run the chain's P-256 precompile, so these tests use a stand-in that accepts only signatures the test just produced. The real precompile is exercised in steps 3 and 4.

2. Engine against the real contract

7 tests in tests-js/engine.test.mjs run the compiled contract on a local node (which does implement the P-256 precompile): predicted addresses match, EIP-712 digests match, a wallet veto relayed by a stranger works, a veto by another key is refused, and a real P-256 passkey signature is accepted for about 87,000 gas while an unknown passkey, a missing user-verification flag and a wrong domain are refused.

3. Differential test

tests-js/diff.test.mjs applies the same random sequences of operations (announce, execute, veto, cancel, panic, change settings, wait, and calls from strangers) to the Solidity contract on a node and to the JavaScript simulator, then compares the full state after every step: owner, guardian, delay, epoch, every announcement, every balance.

560 steps compared (376 accepted, 184 refused), identical states at every step

4. The real chain

scripts/real-chain-check.mjs injects the contract bytecode into a read-only eth_call on Robinhood Chain (state override), so nothing is deployed, and replays an attack against the chain's own P-256 precompile.

NOMINAL : a thief announces 2 withdrawals, the guardian's passkey refuses the 1st then panics
  withdrawal 1 : Vetoed
  withdrawal 2 : Pending (then killed by the panic, epoch 1)
  vault 1.0 ETH     thief 0.0 ETH

FORGERIES (the live chain refused all)
  one flipped bit in the signature
  a passkey the vault does not know
  the right passkey on another domain
  a valid signature for another announcement
  a signature without user verification
  a panic signed for another epoch

5. End to end with the CLI

scripts/anvil-e2e.mjs runs the real CLI as a subprocess: deploy, create, deposit, announce, status, veto, execute after the wait, a refused execution, an owner rotation, and the watcher auto-refusing an unknown destination while letting an allowed one pass.

What was not done

  • No third-party audit.
  • No deployment, so nothing has been run with real funds.
  • No test on real phones across browsers (the passkey flow is tested with a software authenticator in the same format).