Triggers
A trigger is the condition under which the vault becomes drainable. Two families today: price and time.
PRICE_BELOW · PRICE_ABOVE (Pyth Pull)
The protocol reads price from a Pyth Pull PriceUpdateV2 account.
Flow
client
↓ fetch latest update from Hermes
↓ post update to Pyth Solana Receiver
↓ invoke trigger_safe_room with the freshly-written PriceUpdateV2 account
program
↓ verify account owner == Pyth Receiver program
↓ verify embedded feed_id matches the feed_id stored in the room
↓ verify publish_time within 90s of clock.unix_timestamp
↓ compare price to room.trigger_threshold
All four steps are bundled into a single transaction by the SDK helper buildPythTriggerTransactions. The client does not need to write any custom bundle code.
Why no pubkey pinning
Pyth Pull creates a new ephemeral PriceUpdateV2 account per post. The pubkey is not deterministic, so the room cannot pin it at create time. Instead, the program validates two invariants:
- Ownership. Only accounts owned by the canonical Pyth Receiver program (
rec5EKMG…LtFJ, same address on mainnet and devnet) are accepted. - Feed match. The
feed_idin the account must match thefeed_idthe owner stored when the room was armed.
If both pass, the price is trusted.
Threshold scaling
Pyth prices are integers scaled by expo (typically -8). The SDK does the scaling for you: if you ask for "SOL below $244", the threshold stored on chain is 24_400_000_000.
The program reads the raw integer and the exponent from the same PriceUpdateV2 account, so the comparison is exact.
Feed ids
Common feeds shipped with the SDK:
| symbol | feed id (hex) |
|---|---|
| SOL/USD | ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d |
| BTC/USD | e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43 |
| ETH/USD | ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace |
| USDC/USD | eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a |
| JUP/USD | 0a0408d619e9380abad35060f9192039ed5042fa6f82301d0e48bb52be830996 |
| BONK/USD | 72b021217ca3fe68922a19aaf990109cb9d84e9ad004b4d2025ad6f529314419 |
Any Pyth Pull feed works. The SDK helper feedIdHexToBytes does the conversion.
TIME_AFTER
A simple slot comparison. The room is armed with trigger_threshold = some_slot_number. The trigger is true the moment clock.slot >= trigger_threshold.
No oracle account is required. The program passes ORACLE_KIND_NONE for time triggers and reads the slot directly from the Clock sysvar.
Use cases: scheduled redemption, vesting cliff abort, dead-man's deadline ("if I don't disarm this room by slot X, drain it").
Staleness
For Pyth, the program enforces a max age of 90 seconds. Older updates are rejected with OracleStale. The client can re-post a fresh update at any time — there is no penalty for posting stale data, only for trying to trigger on it.
For time triggers, there is no staleness concept.
Composing triggers
Today, a single room has exactly one trigger. Compound triggers (A and B, A or B) live in the SDK layer: just create multiple rooms with overlapping or complementary thresholds.
In a future release, the program may expose a compound_trigger_kind that consumes a list of sub-triggers. The IDL is designed to allow that without a breaking change.