Start
How it works
Every step, with the data that moves.
The actors
| Actor | Holds | Can |
|---|---|---|
| Owner | A normal key (or a smart account) | Announce withdrawals and changes. Cancel their own announcements. |
| Guardian | A passkey (or a wallet, or an ERC-1271 contract) | Refuse one announcement, or all of them. Nothing else. |
| Anyone | Gas | Run 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