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:

  1. Ownership. Only accounts owned by the canonical Pyth Receiver program (rec5EKMG…LtFJ, same address on mainnet and devnet) are accepted.
  2. Feed match. The feed_id in the account must match the feed_id the 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:

symbolfeed id (hex)
SOL/USDef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d
BTC/USDe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43
ETH/USDff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
USDC/USDeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a
JUP/USD0a0408d619e9380abad35060f9192039ed5042fa6f82301d0e48bb52be830996
BONK/USD72b021217ca3fe68922a19aaf990109cb9d84e9ad004b4d2025ad6f529314419

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.