marmot

Marmot docs

20 pages, from the idea to the error codes

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.