1use crate::Finding;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct ExploitTestCase {
11 pub h_code: String,
13 pub project_name: String,
15 pub loss_millions: f64,
17 pub year: u32,
19 pub vulnerable_code: String,
21 pub expected_detections: Vec<String>,
23 pub min_findings: usize,
25}
26
27pub struct IntegrationTestSuite {
29 test_cases: Vec<ExploitTestCase>,
30 results: HashMap<String, IntegrationTestResult>,
31}
32
33#[derive(Debug, Clone)]
35pub struct IntegrationTestResult {
36 pub h_code: String,
38 pub passed: bool,
40 pub findings_count: usize,
42 pub matching_detectors: Vec<String>,
44 pub false_negatives: Vec<String>,
46 pub execution_time_ms: u64,
48}
49
50impl IntegrationTestSuite {
51 #[allow(clippy::new_without_default)]
53 pub fn new() -> Self {
54 let mut suite = Self {
55 test_cases: Vec::new(),
56 results: HashMap::new(),
57 };
58
59 suite.initialize_real_exploits();
60 suite
61 }
62
63 fn initialize_real_exploits(&mut self) {
65 self.test_cases.push(ExploitTestCase {
67 h_code: "H19".to_string(),
68 project_name: "Euler".to_string(),
69 loss_millions: 197.0,
70 year: 2023,
71 vulnerable_code: r#"
72function depositCollateral(address asset, uint256 amount) public {
73 collateral[msg.sender][asset] += amount;
74 // Missing: require(isHealthy(msg.sender));
75 emit DepositCollateral(asset, amount);
76}
77 "#
78 .to_string(),
79 expected_detections: vec!["health_check".to_string()],
80 min_findings: 1,
81 });
82
83 self.test_cases.push(ExploitTestCase {
84 h_code: "H16".to_string(),
85 project_name: "Nomad".to_string(),
86 loss_millions: 190.0,
87 year: 2022,
88 vulnerable_code: r#"
89bytes32 public merkleRoot;
90
91function initialize() external {
92 // Zero root initialization
93 merkleRoot = bytes32(0);
94}
95
96function verifyMessage(bytes32[] calldata proof) public {
97 if (verifyProof(proof, merkleRoot)) {
98 executeMessage();
99 }
100}
101 "#
102 .to_string(),
103 expected_detections: vec!["merkle_root".to_string()],
104 min_findings: 1,
105 });
106
107 self.test_cases.push(ExploitTestCase {
108 h_code: "H47".to_string(),
109 project_name: "KelpDAO".to_string(),
110 loss_millions: 292.0,
111 year: 2024,
112 vulnerable_code: r#"
113address[] public dvns;
114
115function setDVNs(address[] calldata _dvns) external {
116 // No minimum count validation
117 dvns = _dvns;
118}
119
120function sendMessage(bytes calldata message) external {
121 require(dvns.length > 0);
122 (bool success,) = dvns[0].call(message);
123 require(success);
124}
125 "#
126 .to_string(),
127 expected_detections: vec!["dvn_single_point".to_string()],
128 min_findings: 1,
129 });
130
131 self.test_cases.push(ExploitTestCase {
132 h_code: "H56".to_string(),
133 project_name: "Echo".to_string(),
134 loss_millions: 73.0,
135 year: 2023,
136 vulnerable_code: r#"
137function mint(uint256 amount) public {
138 // No collateral verification
139 totalMinted += amount;
140 balances[msg.sender] += amount;
141}
142 "#
143 .to_string(),
144 expected_detections: vec!["synthetic_mint".to_string()],
145 min_findings: 1,
146 });
147
148 self.test_cases.push(ExploitTestCase {
150 h_code: "H17".to_string(),
151 project_name: "Mango".to_string(),
152 loss_millions: 117.0,
153 year: 2022,
154 vulnerable_code: r#"
155function swapWithOracle() public {
156 uint256 price = oracle.getPrice(token);
157 // Manipulate spot price while using it
158 tokenPrice[msg.sender] = price;
159 executeSwap(price);
160}
161 "#
162 .to_string(),
163 expected_detections: vec!["oracle_self_trade".to_string()],
164 min_findings: 1,
165 });
166 }
167
168 pub fn add_test_case(&mut self, test_case: ExploitTestCase) {
170 self.test_cases.push(test_case);
171 }
172
173 pub fn run_tests<F>(&mut self, detector_fn: F) -> IntegrationTestResults
175 where
176 F: Fn(&str) -> Vec<Finding>,
177 {
178 let mut results = Vec::new();
179 let mut passed = 0;
180 let mut failed = 0;
181
182 for test_case in &self.test_cases {
183 let start = std::time::Instant::now();
184 let findings = detector_fn(&test_case.vulnerable_code);
185 let elapsed = start.elapsed().as_millis() as u64;
186
187 let findings_count = findings.len();
188 let test_passed = findings_count >= test_case.min_findings;
189
190 if test_passed {
191 passed += 1;
192 } else {
193 failed += 1;
194 }
195
196 let matching_detectors = findings.iter().map(|f| f.invariant_id.clone()).collect();
197
198 let result = IntegrationTestResult {
199 h_code: test_case.h_code.clone(),
200 passed: test_passed,
201 findings_count,
202 matching_detectors,
203 false_negatives: test_case
204 .expected_detections
205 .iter()
206 .filter(|d| !test_case.expected_detections.iter().any(|m| m == *d))
207 .cloned()
208 .collect(),
209 execution_time_ms: elapsed,
210 };
211
212 results.push(result.clone());
213 self.results.insert(test_case.h_code.clone(), result);
214 }
215
216 IntegrationTestResults {
217 total: self.test_cases.len(),
218 passed,
219 failed,
220 results,
221 }
222 }
223
224 pub fn get_result(&self, h_code: &str) -> Option<&IntegrationTestResult> {
226 self.results.get(h_code)
227 }
228}
229
230#[derive(Debug, Clone)]
232pub struct IntegrationTestResults {
233 pub total: usize,
235 pub passed: usize,
237 pub failed: usize,
239 pub results: Vec<IntegrationTestResult>,
241}
242
243impl IntegrationTestResults {
244 pub fn success_rate(&self) -> f64 {
246 if self.total == 0 {
247 0.0
248 } else {
249 (self.passed as f64 / self.total as f64) * 100.0
250 }
251 }
252
253 pub fn report(&self) -> String {
255 let mut report = format!(
256 "# Integration Test Results\n\n## Summary\n- **Total Tests:** {}\n",
257 self.total
258 );
259 report.push_str(&format!("- **Passed:** {}\n", self.passed));
260 report.push_str(&format!("- **Failed:** {}\n", self.failed));
261 report.push_str(&format!(
262 "- **Success Rate:** {:.1}%\n\n",
263 self.success_rate()
264 ));
265
266 report.push_str("## Details\n\n");
267 for result in &self.results {
268 let status = if result.passed {
269 "✓ PASS"
270 } else {
271 "✗ FAIL"
272 };
273 report.push_str(&format!(
274 "### {} {}\n- **Findings:** {}\n- **Time:** {} ms\n\n",
275 result.h_code, status, result.findings_count, result.execution_time_ms
276 ));
277 }
278
279 report
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn integration_suite_initializes() {
289 let suite = IntegrationTestSuite::new();
290 assert!(!suite.test_cases.is_empty());
291 }
292
293 #[test]
294 fn exploit_test_case_structure() {
295 let test = ExploitTestCase {
296 h_code: "H99".to_string(),
297 project_name: "Test".to_string(),
298 loss_millions: 10.0,
299 year: 2024,
300 vulnerable_code: "test code".to_string(),
301 expected_detections: vec!["detector1".to_string()],
302 min_findings: 1,
303 };
304
305 assert_eq!(test.h_code, "H99");
306 assert_eq!(test.min_findings, 1);
307 }
308
309 #[test]
310 fn test_results_success_rate() {
311 let results = IntegrationTestResults {
312 total: 10,
313 passed: 8,
314 failed: 2,
315 results: vec![],
316 };
317
318 assert_eq!(results.success_rate(), 80.0);
319 }
320}