Architecture
Three account types. Six instructions. One trait. The whole program fits in a single Rust module.
Accounts
Registry PDA
A single global account. Holds the admin authority and lifetime counters for every room ever created (total_rooms, total_evacuated, total_disarmed, total_expired).
Seeds: ["saferoom_registry"].
SafeRoom PDA
One per (owner, room_id) pair. Stores the trigger configuration and action template that the room will execute on evacuation. Once armed, this account's fields are immutable except for status and triggered_slot.
Seeds: ["safe_room", owner_pubkey, room_id_le_bytes].
Key fields:
status: u8—ARMED(0),EVACUATED(1),DISARMED(2),EXPIRED(3).trigger_kind—PRICE_BELOW/PRICE_ABOVE/TIME_AFTER.trigger_oracle_kind—CEDA(custom devnet) /PYTH/NONE(time triggers).trigger_feed_id: [u8; 32]— for Pyth, the 32-byte feed id from Hermes.trigger_threshold: i64— price (Pyth-scaled) or slot number.action_kind—TOKEN_TRANSFERorTOKEN_BURN.action_destination: Pubkey— where the vault drains.action_amount: u64— exact amount the program will move.
Vault PDA
One per room. Owned by the SPL Token program at the chain level, but the authority is the room PDA. Holds the locked tokens.
Seeds: ["vault", room_pubkey].
Created by create_safe_room with init + token::mint = action_mint + token::authority = room. The room signs as the authority using its seeds.
Instructions
| ix | who | what |
|---|---|---|
initialize_registry | admin once | scaffolds the Registry PDA |
create_safe_room | room owner | deposits tokens into vault, arms the trigger |
trigger_safe_room | anyone | verifies trigger, drains vault, flips status to EVACUATED |
disarm_safe_room | room owner | cancels armed room, refunds vault |
expire_safe_room | anyone, after deadline | reclaims vault to owner once past expiry_slot |
close_safe_room | room owner, terminal only | closes accounts, returns rent |
Permission model
- The owner controls
disarmandclose. Nothing else. - The program controls the vault. There is no admin path that can move user funds.
- Anyone can trigger or expire. Permissionless by construction: the worst a malicious responder can do is fire a trigger that is already true — which is exactly what you wanted to happen.
Atomicity
trigger_safe_room does all of the following inside one Solana transaction:
- Validates the room is
ARMEDand not pastexpiry_slot. - Reads the oracle account, verifies ownership and
feed_id, checks staleness. - Compares the on-chain price (or slot) to
trigger_threshold. - Signs as the room PDA and CPI-calls
token::transferortoken::burn. - Flips status to
EVACUATED, stampstriggered_slot, bumps registry counters.
If any step fails, the entire transaction reverts. There is no halfway state where the vault is empty but the status is still ARMED.
Composability
trigger_safe_room does not care who calls it, and action_destination does not have to be the room owner. That makes the primitive composable:
- A lending market can register itself as the destination so liquidations route back into a repay action.
- A treasury contract can be the destination of a panic-button vault.
- A vesting protocol can use
TIME_AFTERtriggers as cliff aborts. - A keeper network can earn fees by being the first responder to trigger any room.