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).