1use sentri_core::model::{Invariant, ProgramModel, SimulationReport};
4use sentri_core::traits::Simulator;
5use sentri_core::Result;
6use tracing::info;
7
8pub struct SimulationEngine;
14
15impl SimulationEngine {
16 pub fn new(_seed: u64) -> Self {
18 Self
19 }
20}
21
22impl Default for SimulationEngine {
23 fn default() -> Self {
24 Self
25 }
26}
27
28impl Simulator for SimulationEngine {
29 fn simulate(
30 &self,
31 program: &ProgramModel,
32 invariants: &[Invariant],
33 ) -> Result<SimulationReport> {
34 info!(
35 "Starting analysis of {} with {} invariants",
36 program.name,
37 invariants.len()
38 );
39
40 let mut traces = Vec::new();
41 let mut detected_violations = 0;
42
43 for invariant in invariants {
45 if !is_invariant_applicable(invariant, program) {
47 continue;
48 }
49
50 if let Some(violation) = analyze_program_invariant(invariant, program) {
52 detected_violations += 1;
53 traces.push(violation);
54 }
55
56 for (func_name, function) in &program.functions {
58 if let Some(violation) =
59 analyze_function_invariant(invariant, program, function, func_name)
60 {
61 detected_violations += 1;
62 traces.push(violation);
63 }
64 }
65 }
66
67 let total_checks = invariants.len();
69 let coverage = if total_checks > 0 {
70 ((total_checks - detected_violations.min(total_checks)) as f64 / total_checks as f64)
71 * 100.0
72 } else {
73 100.0
74 };
75
76 info!(
77 "Analysis complete: {} violations detected in {} invariants, {:.1}% coverage",
78 detected_violations, total_checks, coverage
79 );
80
81 Ok(SimulationReport {
82 violations: detected_violations,
83 traces,
84 coverage: coverage.max(0.0),
85 seed: 0,
86 })
87 }
88
89 fn chain(&self) -> &str {
90 "analysis"
91 }
92}
93
94fn is_invariant_applicable(invariant: &Invariant, program: &ProgramModel) -> bool {
100 let program_chain = program.chain.to_lowercase();
101 let invariant_category = invariant.category.to_lowercase();
102
103 if invariant_category.contains("access")
105 || invariant_category.contains("overflow")
106 || invariant_category.contains("general")
107 {
108 return true;
109 }
110
111 program_chain.contains(&invariant_category)
113}
114
115fn analyze_program_invariant(invariant: &Invariant, program: &ProgramModel) -> Option<String> {
122 let invariant_name_lower = invariant.name.to_lowercase();
123
124 if invariant_name_lower.contains("reentrancy") {
126 let entry_points: Vec<_> = program
127 .functions
128 .iter()
129 .filter(|(_, f)| f.is_entry_point)
130 .collect();
131
132 let mutating_functions: Vec<_> = program
133 .functions
134 .iter()
135 .filter(|(_, f)| !f.mutates.is_empty())
136 .collect();
137
138 if entry_points.len() > 1 && !mutating_functions.is_empty() {
139 return Some(format!(
140 "Invariant '{}' violated in {}: Reentrancy risk detected - {} entry points with state mutations across {} functions",
141 invariant.name,
142 program.name,
143 entry_points.len(),
144 mutating_functions.len()
145 ));
146 }
147 }
148
149 if invariant_name_lower.contains("access") {
151 let public_entry_points: Vec<_> = program
152 .functions
153 .iter()
154 .filter(|(_, f)| f.is_entry_point && !f.reads.is_empty())
155 .collect();
156
157 if !public_entry_points.is_empty() {
158 return Some(format!(
159 "Invariant '{}' violated in {}: Access control risk - {} entry points access state without guards",
160 invariant.name,
161 program.name,
162 public_entry_points.len()
163 ));
164 }
165 }
166
167 if invariant_name_lower.contains("overflow") || invariant_name_lower.contains("underflow") {
169 let numeric_functions: Vec<_> = program
170 .functions
171 .iter()
172 .filter(|(_, f)| {
173 f.parameters.iter().any(|p| {
174 let p_lower = p.to_lowercase();
175 p_lower.contains("u") || p_lower.contains("i") || p_lower.contains("int")
176 })
177 })
178 .collect();
179
180 if numeric_functions.len() > 2 {
181 return Some(format!(
182 "Invariant '{}' violated in {}: Arithmetic risk - {} functions with numeric parameters may have unchecked operations",
183 invariant.name,
184 program.name,
185 numeric_functions.len()
186 ));
187 }
188 }
189
190 None
191}
192
193fn analyze_function_invariant(
201 invariant: &Invariant,
202 program: &ProgramModel,
203 function: &sentri_core::model::FunctionModel,
204 func_name: &str,
205) -> Option<String> {
206 let invariant_name_lower = invariant.name.to_lowercase();
207 let state_interaction_count = function.reads.len() + function.mutates.len();
208
209 if program.chain.to_lowercase().contains("solana")
211 && function
212 .mutates
213 .iter()
214 .any(|m| m.contains("SOLANA_LAMPORT_UNSAFE"))
215 {
216 return Some(format!(
217 "Function '{}' in {} violates '{}': Unsafe direct lamport manipulation detected",
218 func_name, program.name, "Solana: Safe Lamport Operations"
219 ));
220 }
221
222 if program.chain.to_lowercase().contains("evm")
224 && function
225 .mutates
226 .iter()
227 .any(|m| m.contains("EVM_REENTRANCY"))
228 {
229 return Some(format!(
230 "Function '{}' in {} violates '{}': Reentrancy vulnerability - state mutation after external call",
231 func_name, program.name, "EVM: Reentrancy Guard"
232 ));
233 }
234
235 if program.chain.to_lowercase().contains("evm")
237 && function
238 .mutates
239 .iter()
240 .any(|m| m.contains("EVM_UNCHECKED_CALL"))
241 {
242 return Some(format!(
243 "Function '{}' in {} violates '{}': Unchecked external call return value",
244 func_name, program.name, "EVM: Checked Calls"
245 ));
246 }
247
248 if program.chain.to_lowercase().contains("evm")
250 && function
251 .mutates
252 .iter()
253 .any(|m| m.contains("EVM_UNCHECKED_ARITHMETIC"))
254 {
255 return Some(format!(
256 "Function '{}' in {} violates '{}': Integer overflow/underflow risk",
257 func_name, program.name, "EVM: Safe Arithmetic"
258 ));
259 }
260
261 if program.chain.to_lowercase().contains("evm")
263 && function
264 .mutates
265 .iter()
266 .any(|m| m.contains("EVM_DELEGATECALL_ABUSE"))
267 {
268 return Some(format!(
269 "Function '{}' in {} violates '{}': Unsafe delegatecall to untrusted contract",
270 func_name, program.name, "EVM: Delegatecall Safety"
271 ));
272 }
273
274 if program.chain.to_lowercase().contains("evm")
276 && function
277 .mutates
278 .iter()
279 .any(|m| m.contains("EVM_TIMESTAMP_DEPENDENCY"))
280 {
281 return Some(format!(
282 "Function '{}' in {} violates '{}': Logic depends on block.timestamp (mutable)",
283 func_name, program.name, "EVM: Timestamp Independence"
284 ));
285 }
286
287 if program.chain.to_lowercase().contains("evm")
289 && function
290 .mutates
291 .iter()
292 .any(|m| m.contains("EVM_FRONT_RUNNING"))
293 {
294 return Some(format!(
295 "Function '{}' in {} violates '{}': Vulnerable to front-running attacks",
296 func_name, program.name, "EVM: Front-run Protection"
297 ));
298 }
299
300 if program.chain.to_lowercase().contains("evm")
302 && function
303 .mutates
304 .iter()
305 .any(|m| m.contains("EVM_ACCESS_CONTROL"))
306 {
307 return Some(format!(
308 "Function '{}' in {} violates '{}': Missing access control checks",
309 func_name, program.name, "EVM: Access Control"
310 ));
311 }
312
313 if program.chain.to_lowercase().contains("evm")
315 && function
316 .mutates
317 .iter()
318 .any(|m| m.contains("EVM_INPUT_VALIDATION"))
319 {
320 return Some(format!(
321 "Function '{}' in {} violates '{}': Missing input validation",
322 func_name, program.name, "EVM: Input Validation"
323 ));
324 }
325
326 if program.chain.to_lowercase().contains("move")
328 && function
329 .mutates
330 .iter()
331 .any(|m| m.contains("MOVE_RESOURCE_LEAK"))
332 {
333 return Some(format!(
334 "Function '{}' in {} violates '{}': Potential resource leak - move_from result not bound",
335 func_name, program.name, "Move: Resource Safety"
336 ));
337 }
338
339 if program.chain.to_lowercase().contains("move")
341 && function
342 .mutates
343 .iter()
344 .any(|m| m.contains("MOVE_MISSING_ABILITY"))
345 {
346 return Some(format!(
347 "Function '{}' in {} violates '{}': Resource moves without required abilities",
348 func_name, program.name, "Move: Ability Requirements"
349 ));
350 }
351
352 if program.chain.to_lowercase().contains("move")
354 && function
355 .mutates
356 .iter()
357 .any(|m| m.contains("MOVE_UNCHECKED_ARITHMETIC"))
358 {
359 return Some(format!(
360 "Function '{}' in {} violates '{}': Unchecked arithmetic operation",
361 func_name, program.name, "Move: Safe Arithmetic"
362 ));
363 }
364
365 if program.chain.to_lowercase().contains("move")
367 && function
368 .mutates
369 .iter()
370 .any(|m| m.contains("MOVE_MISSING_SIGNER"))
371 {
372 return Some(format!(
373 "Function '{}' in {} violates '{}': Resource moved without signer verification",
374 func_name, program.name, "Move: Signer Verification"
375 ));
376 }
377
378 if program.chain.to_lowercase().contains("move")
380 && function
381 .mutates
382 .iter()
383 .any(|m| m.contains("MOVE_UNGUARDED_MUTATION"))
384 {
385 return Some(format!(
386 "Function '{}' in {} violates '{}': Unguarded global state mutation",
387 func_name, program.name, "Move: Guarded Mutations"
388 ));
389 }
390
391 if program.chain.to_lowercase().contains("move")
393 && function
394 .mutates
395 .iter()
396 .any(|m| m.contains("MOVE_PRIVILEGE_ESCALATION"))
397 {
398 return Some(format!(
399 "Function '{}' in {} violates '{}': Potential privilege escalation through signer extraction",
400 func_name, program.name, "Move: Privilege Control"
401 ));
402 }
403
404 if program.chain.to_lowercase().contains("move")
406 && function
407 .mutates
408 .iter()
409 .any(|m| m.contains("MOVE_UNSAFE_ABORT"))
410 {
411 return Some(format!(
412 "Function '{}' in {} violates '{}': Abort without error code explanation",
413 func_name, program.name, "Move: Error Handling"
414 ));
415 }
416
417 if program.chain.to_lowercase().contains("solana")
419 && function
420 .mutates
421 .iter()
422 .any(|m| m.contains("SOLANA_MISSING_SIGNER"))
423 {
424 return Some(format!(
425 "Function '{}' in {} violates '{}': Missing Signer requirement - SOL_001 (Missing Signer Check)",
426 func_name,
427 program.name,
428 "Solana: Required Signers"
429 ));
430 }
431
432 if program.chain.to_lowercase().contains("solana")
434 && function
435 .mutates
436 .iter()
437 .any(|m| m.contains("SOLANA_UNCHECKED_ARITHMETIC"))
438 {
439 return Some(format!(
440 "Function '{}' in {} violates '{}': Unchecked arithmetic operation detected - SOL_003 (Integer Overflow)",
441 func_name,
442 program.name,
443 "Solana: Safe Arithmetic Operations"
444 ));
445 }
446
447 if program.chain.to_lowercase().contains("solana")
449 && function
450 .mutates
451 .iter()
452 .any(|m| m.contains("SOLANA__MISSING_VALIDATION"))
453 {
454 return Some(format!(
455 "Function '{}' in {} violates '{}': Missing account validation - SOL_002 (Lack of Account Ownership Validation)",
456 func_name,
457 program.name,
458 "Solana: Account Validation"
459 ));
460 }
461
462 if program.chain.to_lowercase().contains("solana")
464 && function
465 .mutates
466 .iter()
467 .any(|m| m.contains("SOLANA_RENT_EXEMPTION"))
468 {
469 return Some(format!(
470 "Function '{}' in {} violates '{}': Potential rent exemption violation - SOL_004 (Rent Exemption)",
471 func_name,
472 program.name,
473 "Solana: Rent Exemption"
474 ));
475 }
476
477 if program.chain.to_lowercase().contains("solana")
479 && function
480 .mutates
481 .iter()
482 .any(|m| m.contains("SOLANA_PDA_DERIVATION"))
483 {
484 return Some(format!(
485 "Function '{}' in {} violates '{}': Potential PDA derivation issue - SOL_005 (PDA Derivation)",
486 func_name,
487 program.name,
488 "Solana: PDA Derivation"
489 ));
490 }
491
492 if program.chain.to_lowercase().contains("solana")
494 && function
495 .mutates
496 .iter()
497 .any(|m| m.contains("SOLANA_UNSAFE_DESERIALIZATION"))
498 {
499 return Some(format!(
500 "Function '{}' in {} violates '{}': Unsafe deserialization of account data - SOL_007 (Account Deserialization)",
501 func_name,
502 program.name,
503 "Solana: Safe Deserialization"
504 ));
505 }
506
507 if program.chain.to_lowercase().contains("solana")
509 && function
510 .mutates
511 .iter()
512 .any(|m| m.contains("SOLANA_UNCHECKED_TOKEN_TRANSFER"))
513 {
514 return Some(format!(
515 "Function '{}' in {} violates '{}': Unchecked token transfer operation - SOL_008 (Token Transfer Overflow)",
516 func_name,
517 program.name,
518 "Solana: Safe Token Operations"
519 ));
520 }
521
522 if invariant_name_lower.contains("reentrancy")
524 && function.is_entry_point
525 && state_interaction_count > 2
526 {
527 return Some(format!(
528 "Function '{}' in {} violates '{}': Complex state interactions (reads: {}, writes: {}) without reentrancy guards",
529 func_name,
530 program.name,
531 invariant.name,
532 function.reads.len(),
533 function.mutates.len()
534 ));
535 }
536
537 if invariant_name_lower.contains("access")
539 && function.is_entry_point
540 && !function.reads.is_empty()
541 && !function.is_pure
542 {
543 return Some(format!(
544 "Function '{}' in {} violates '{}': Entry point accesses {} state variables without authorization checks",
545 func_name,
546 program.name,
547 invariant.name,
548 function.reads.len()
549 ));
550 }
551
552 if (invariant_name_lower.contains("overflow") || invariant_name_lower.contains("underflow"))
554 && function.parameters.len() > 1
555 {
556 let numeric_params = function
557 .parameters
558 .iter()
559 .filter(|p| {
560 let p_lower = p.to_lowercase();
561 p_lower.contains("u") || p_lower.contains("i") || p_lower.contains("int")
562 })
563 .count();
564
565 if numeric_params > 1 {
566 return Some(format!(
567 "Function '{}' in {} violates '{}': {} numeric parameters without overflow checks",
568 func_name, program.name, invariant.name, numeric_params
569 ));
570 }
571 }
572
573 None
574}