Skip to main content

sentri_core/registry/
mod.rs

1//! Exploit registry: Historical DeFi hacks mapped to Sentri invariants.
2//!
3//! This module contains 21 documented exploits from major DeFi protocols,
4//! each mapped to the Sentri invariants they violated.
5//! Data is embedded at compile time via include_bytes! and loaded lazily.
6
7use once_cell::sync::Lazy;
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11/// An historical exploit record.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Exploit {
14    /// Unique identifier (slug format).
15    pub id: String,
16    /// Protocol name.
17    pub protocol: String,
18    /// Date of exploit (YYYY-MM-DD).
19    pub date: String,
20    /// Estimated loss in USD.
21    pub loss_usd: u64,
22    /// Chain affected.
23    pub chain: String,
24    /// Invariants that would have detected this.
25    pub invariant_ids: Vec<String>,
26    /// Summary of the attack.
27    pub attack_summary: String,
28    /// Transaction hash or block number.
29    pub tx_hash: String,
30    /// Link to postmortem or analysis.
31    pub postmortem_url: String,
32}
33
34/// Registry of all known exploits.
35static EXPLOIT_REGISTRY_JSON: &[u8] = include_bytes!("exploits.json");
36
37/// Loaded exploit registry (lazy singleton).
38pub static EXPLOIT_REGISTRY: Lazy<ExploitRegistry> =
39    Lazy::new(|| ExploitRegistry::load().expect("Failed to parse exploit registry"));
40
41/// The exploit registry container.
42#[derive(Debug, Clone)]
43pub struct ExploitRegistry {
44    exploits: BTreeMap<String, Exploit>,
45}
46
47impl ExploitRegistry {
48    /// Load the registry from embedded JSON.
49    fn load() -> Result<Self, Box<dyn std::error::Error>> {
50        let exploits: Vec<Exploit> = serde_json::from_slice(EXPLOIT_REGISTRY_JSON)?;
51        let mut map = BTreeMap::new();
52        for exploit in exploits {
53            map.insert(exploit.id.clone(), exploit);
54        }
55        Ok(Self { exploits: map })
56    }
57
58    /// Get an exploit by ID.
59    pub fn get(&self, id: &str) -> Option<&Exploit> {
60        self.exploits.get(id)
61    }
62
63    /// Get all exploits.
64    pub fn all(&self) -> Vec<&Exploit> {
65        self.exploits.values().collect()
66    }
67
68    /// Get exploits for a specific chain.
69    pub fn by_chain(&self, chain: &str) -> Vec<&Exploit> {
70        self.exploits
71            .values()
72            .filter(|e| e.chain.eq_ignore_ascii_case(chain))
73            .collect()
74    }
75
76    /// Get exploits that violated a specific invariant.
77    pub fn by_invariant(&self, invariant_id: &str) -> Vec<&Exploit> {
78        self.exploits
79            .values()
80            .filter(|e| e.invariant_ids.iter().any(|id| id == invariant_id))
81            .collect()
82    }
83
84    /// Get total loss across all exploits.
85    pub fn total_loss(&self) -> u64 {
86        self.exploits.values().map(|e| e.loss_usd).sum()
87    }
88
89    /// Count total exploits.
90    pub fn count(&self) -> usize {
91        self.exploits.len()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_registry_loads() {
101        let registry = &*EXPLOIT_REGISTRY;
102        assert!(registry.count() > 0, "Registry should have exploits");
103    }
104
105    #[test]
106    fn test_registry_access() {
107        let registry = &*EXPLOIT_REGISTRY;
108        let all = registry.all();
109        assert!(!all.is_empty());
110
111        // Each exploit should have required fields
112        for exploit in all {
113            assert!(!exploit.id.is_empty());
114            assert!(!exploit.protocol.is_empty());
115            assert!(!exploit.chain.is_empty());
116        }
117    }
118}