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.
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.
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
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
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
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 Range | Classification | Action |
|---|---|---|
| 40–59 | LOW | Logged to Kafka only |
| 60–74 | MEDIUM | Alert published, no pause |
| 75–89 | HIGH | Alert + webhook |
| 90–99 | CRITICAL | Alert + 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.
// SentinelState PDA layout
pub struct SentinelState {
pub paused: bool,
pub authorized_watcher: Pubkey,
pub last_alert_slot: u64,
pub bump: u8,
}