Marmota

Marmota docs

20 pages, from the idea to the error codes

Reference

Contract reference

Marmot.sol and MarmotFactory.sol, function by function.

Solidity 0.8.26, optimizer 500 runs, via-IR, EVM cancun. Source in contracts/src/. The vault inherits OpenZeppelin EIP712 and uses SignatureChecker and the repository's WebAuthn library.

Constants and state

Name Value Notes
MIN_DELAY 5 minutes
MAX_DELAY 30 days
GRACE 14 days How long a ripe announcement stays runnable
owner address Changes only by an announcement
guardian (address addr, uint256 x, uint256 y) addr != 0: wallet or ERC-1271. Otherwise a P-256 passkey (x, y).
delay uint64
rpIdHash bytes32, immutable sha256 of the passkey domain
epoch uint64 Incremented by panic and by owner rotation
opCount uint256 Next announcement id

Owner functions

Function Effect
queueCall(to, value, data) returns (id) to may not be the vault or zero
queueTransfer(token, to, amount) returns (id) ETH if token == 0. to may not be zero or the vault.
queueSetDelay(newDelay) returns (id) Within MIN_DELAY and MAX_DELAY
queueSetGuardian(g) returns (id) Valid per the creation rules
queueSetOwner(o) returns (id) o != 0
cancel(id) Pending announcements only

Public functions

Function Effect
execute(id) Runs a pending, ripe, unexpired announcement of the current epoch. Reentrancy-locked.
vetoWithSig(id, sig) Verifies the guardian's signature on Veto(id, epoch), then refuses the announcement
panicWithSig(sig) Verifies the guardian's signature on Panic(epoch), then epoch + 1
receive() Accepts ETH, emits Deposited

Guardian functions (wallet guardian only)

Function Effect
veto(id) msg.sender must be guardian.addr
panic() Same, epoch + 1

Views

Function Returns
getOp(id) (kind, status, readyAt, epoch, to, value, data)
isLive(id) Pending, current epoch, not expired
vetoDigest(id), panicDigest() The EIP-712 digest to sign, for the current epoch

EIP-712 messages

domain: { name: "Marmot", version: "1", chainId, verifyingContract: vault }
Veto(uint256 id, uint64 epoch)
Panic(uint64 epoch)

For a passkey guardian the 32-byte digest is the WebAuthn challenge. For a wallet or ERC-1271 guardian the signature is the usual 65-byte (or contract-defined) signature over the digest.

Events

Event When
Deposited(from, amount) ETH received
Queued(id, kind, to, value, data, readyAt, expiresAt) Any announcement. Everything needed to explain it is here.
Executed(id, by) An announcement ran
Vetoed(id, by) The guardian refused it. by is the caller (a relayer for signed vetoes).
Cancelled(id) The owner withdrew it
Panicked(newEpoch) Panic, or owner rotation
OwnerChanged, GuardianChanged, DelayChanged At creation and when an announcement changes them

MarmotFactory

create(owner, guardian, delay, rpIdHash, salt) returns (Marmot)
predict(owner, guardian, delay, rpIdHash, salt)   returns (address)
event Created(vault, owner, delay)

The CREATE2 salt is keccak256(owner, salt), so nobody can squat an address meant for you. The factory has no owner, no fee and no authority over the vaults it creates.