HOW IT WORKS

Detection, Scoring, and Response

SentinelGuard moves from raw transaction to on-chain pause inside a single Solana slot — no human in the loop.

Signal Intake

The watcher subscribes to Solana transaction activity via Helius Geyser gRPC. Every slot fires a callback. Each transaction is parsed into a ParsedTransaction struct containing token deltas, program IDs, log messages, and signer addresses.

parsed_transaction.rsrust
struct ParsedTransaction {
    signature: String,
    slot: u64,
    program_ids: Vec<Pubkey>,
    log_messages: Vec<String>,
    token_deltas: HashMap<Pubkey, i64>,
    signer: Option<Pubkey>,
}

Helius is used as a Geyser-compatible devnet substitute. In production, this would be a direct Yellowstone gRPC connection.

Rolling Window Engine

SentinelGuard maintains a 10-slot rolling window per monitored protocol. Each slot, TVL is recalculated from token delta aggregation. The window tracks:

  • peak_tvl — highest TVL seen since monitoring started
  • current_tvl — sum of all token deltas in latest slot
  • slot_history — ring buffer of last 10 TVL snapshots
  • bridge_outflow_avg — rolling average of bridge transfers

Key Invariant

TVL baseline is set from first observed slot with activity > $50k. This prevents cold-start false positives on protocol initialization.

Detection Rules

Three rules run simultaneously on every slot. The highest score is used — rules do not stack.

Rule 1FLASH_LOAN_DRAINScore: 40–99

Flash Loan Correlation

Detects flash loan instruction via known program IDs (Solend, Marginfi, Orca) or log keywords ('flash_loan', 'FlashLoan'), then checks for TVL drop >15% in the same 5-slot window using peak_tvl as baseline.

Score Formula

40 + (drop * 100 * confidence_factor) + same_signer_bonus
same-slot signer match: +15 bonusconfidence_factor: 0.5 – 1.0
Rule 2TVL_VELOCITYScore: 75–99

TVL Velocity Drop

TVL drops ≥20% within the last 3 slots regardless of flash loan presence. Guards: TVL must be above $50k and absolute drop must exceed $10k to filter low-liquidity noise.

Score Formula

75 + (drop - 0.20) * 100
min TVL: $50,000min abs drop: $10,000
Rule 3BRIDGE_SPIKEScore: 85–95

Bridge Outflow Spike

Bridge transfer volume exceeds 10x the rolling average in the current slot. Designed to catch exfiltration after a drain even if TVL impact is delayed cross-chain.

Score Formula

10–20x multiplier → 8520x+ multiplier → 95

Severity Threshold

After all three rules evaluate, the watcher compares the highest score against MIN_SEVERITY_TO_PAUSE (default: 60). Scores below this are logged but do not trigger any action.

Score RangeClassificationAction
40–59LOWLogged to Kafka only
60–74MEDIUMAlert published, no pause
75–89HIGHAlert + webhook
90–99CRITICALAlert + webhook + on-chain pause

Alert Lifecycle

Transaction Received

Geyser stream fires callback. ParsedTransaction built from slot data. Signer, deltas, program IDs extracted.

Rule Engine Scores

All 3 rules evaluate simultaneously on 10-slot window. Highest score selected. confidence = 0 if score < 40.

Alert Threshold Check

Score compared to MIN_SEVERITY_TO_PAUSE. Redis key checked for cooldown (key: sentinel:cooldown:{protocol}:{rule}). Duplicate suppressed if within 30s window.

On-chain Pause

pause_withdrawals CPI submitted using watcher keypair. Anchor program validates signer, sets paused = true on SentinelState PDA. Tx confirmed before webhook fires.

Webhooks + Kafka

Elysia dispatcher fans out to Discord, Circle, Wormhole via Promise.allSettled. Kafka event published with full alert payload for audit trail.

Automated Defense

The on-chain pause is the terminal action. Once paused = true is set on the SentinelState PDA, the mock_protocol vault rejects all withdrawal instructions until an authorized keypair resets it.

The watcher keypair must be pre-authorized in the Anchor program via the authorized_watcher field on SentinelState. Deploying without this set causes all pause CPIs to fail silently.

sentinel_state.rsrust
// SentinelState PDA layout
pub struct SentinelState {
    pub paused: bool,
    pub authorized_watcher: Pubkey,
    pub last_alert_slot: u64,
    pub bump: u8,
}

Next Steps

Was this page helpful?