marmot

Marmot docs

20 pages, from the idea to the error codes

Start

How it works

Every step, with the data that moves.

The actors

ActorHoldsCan
OwnerA normal key (or a smart account)Announce withdrawals and changes. Cancel their own announcements.
GuardianA passkey (or a wallet, or an ERC-1271 contract)Refuse one announcement, or all of them. Nothing else.
AnyoneGasRun a ripe announcement. Carry a guardian signature on chain. Nothing else.

1. Create

A factory deploys your vault at an address that can be computed in advance (CREATE2). You give it the owner, the guardian, the waiting time and, for a passkey guardian, the hash of the website domain the passkey was made on.

factory.create(owner, guardian{addr,x,y}, delay, rpIdHash, salt) -> vault

2. Deposit

Send ETH to the vault address, or transfer tokens to it. There is nothing to approve and nothing to register.

3. Announce

The owner calls one of the queue* functions. The vault stores the action with readyAt = now + delay and the current epoch, and emits Queued. The announcement is public the moment the transaction lands.

queueTransfer(token, to, amount)      // ETH when token = address(0)
queueCall(to, value, data)            // anything else: approve, NFT, swap...
queueSetDelay(newDelay)
queueSetGuardian(newGuardian)
queueSetOwner(newOwner)

4. Wait

For delay seconds nothing can run it. The guardian can:

veto(id)                       // guardian is a wallet, calls directly
vetoWithSig(id, sig)           // anyone submits the guardian's signature
panic() / panicWithSig(sig)    // kill every pending announcement at once

The owner can also withdraw their own announcement with cancel(id).

5. Execute

execute(id) is open to everyone. It succeeds only if the announcement is still pending, belongs to the current epoch, is ripe (now >= readyAt) and has not expired (now <= readyAt + 14 days). Then the vault performs the call, or applies the setting.

The whole thing as a picture

owner key                guardian passkey              anyone
   |                          |                           |
   |-- queueTransfer(...) --> |                           |
   |      Queued(id, readyAt) emitted, public             |
   |                          |                           |
   |      ...waiting time...  |-- sign veto(id) --------> |
   |                          |      (Face ID)            |-- vetoWithSig(id, sig)
   |                                                      |
   |                 Vetoed: stays in the vault           |
   |                                                      |
   | (no veto)  after readyAt: ------------------------- >|-- execute(id)  -> money leaves