Reference
JavaScript engine
The same code runs in the browser, the CLI and the tests.
Files: src/engine/marmot-engine.mjs (vault logic, digests, encodings, simulator) and src/engine/passkey.mjs (WebAuthn). Both need only ethers v6.
Constants
E.MIN_DELAY = 300 E.MAX_DELAY = 2592000 E.GRACE = 1209600
E.KIND = { Call, SetDelay, SetGuardian, SetOwner }
E.STATUS = { None, Pending, Executed, Vetoed, Cancelled }
Addresses and calldata
E.guardianAddress(addr) -> { addr, x: 0n, y: 0n }
E.guardianPasskey(x, y) -> { addr: 0x0, x, y }
E.predictVault(factory, { owner, guardian, delay, rpIdHash, salt }) -> address
E.call.create({...}) E.call.queueTransfer(token, to, amount) E.call.queueCall(to, value, data)
E.call.queueSetDelay(d) E.call.queueSetGuardian(g) E.call.queueSetOwner(o)
E.call.execute(id) E.call.cancel(id) E.call.veto(id) E.call.vetoWithSig(id, sig)
E.call.panic() E.call.panicWithSig(sig)
Digests and signing
E.vetoDigest(chainId, vault, id, epoch) E.panicDigest(chainId, vault, epoch)
E.vetoTypedData(...) E.panicTypedData(...) // for eth_signTypedData_v4
await E.signVeto(wallet, chainId, vault, id, epoch) // wallet guardian
P.softAuthenticator({ rpId, origin }) // a software passkey, same output as a browser
P.createPasskey({ rpId }) // real WebAuthn (browser)
P.webauthnAuthenticator({ rpId, x, y }) // sign with an existing passkey
await P.signWithPasskey(auth, digest) // -> abi.encode(Auth), ready for vetoWithSig
await P.verifyAssertion({ challenge32, auth, rpIdHash, x, y }) // mirror of WebAuthn.sol
Reading announcements
const op = E.decodeOp({ kind, to, value, data }) // { action: { type, ... } }
E.describeOp(op, { symbols, decimals }) // "Send 1.5 ETH to 0xA7E1...A6C1"
E.riskOf(op) // 'money' | 'drain' | 'control' | 'config'
The simulator
E.Vault is a line-by-line JavaScript mirror of the contract's state machine, with the same errors and the same rules. The website demo uses it, and a differential test proves it behaves exactly like the contract.
const sim = new E.Vault({ owner, guardian: E.guardianAddress(g), delay: 3600 });
sim.deposit(E.ZERO, 10n ** 19n);
const id = sim.queueTransfer(owner, E.ZERO, thief, 4n * 10n ** 18n);
sim.execute(id); // throws MarmotError('NotReady')
sim.warp(3600); // advance the clock
sim.phase(id); // 'ready'
sim.veto(g, id); // wallet guardian; or: await sim.vetoWithSig(id, sig)
sim.execute(id); // throws MarmotError('NotPending')
Execution is atomic: if a call fails the state is restored. The simulator runs ETH sends and ERC-20 transfer; other calls report CallFailed: unsupported in simulator. ERC-1271 guardians are not simulated.