Skip to main content

sentri_simulator/
engine.rs

1//! Simulation engine for analyzing program invariants.
2
3use sentri_core::model::{Invariant, ProgramModel, SimulationReport};
4use sentri_core::traits::Simulator;
5use sentri_core::Result;
6use tracing::info;
7
8/// Deterministic simulation engine for invariant testing.
9///
10/// This engine performs static analysis of program models against invariants.
11/// It analyzes function control flow, state access patterns, and structural properties
12/// to detect potential violations.
13pub struct SimulationEngine;
14
15impl SimulationEngine {
16    /// Create a new simulation engine.
17    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        // Analyze each invariant against the program
44        for invariant in invariants {
45            // Check if invariant is applicable to this program type
46            if !is_invariant_applicable(invariant, program) {
47                continue;
48            }
49
50            // Analyze program-level patterns
51            if let Some(violation) = analyze_program_invariant(invariant, program) {
52                detected_violations += 1;
53                traces.push(violation);
54            }
55
56            // Analyze each function against the invariant
57            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        // Calculate coverage based on violations found
68        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
94/// Check if an invariant is applicable to this program type.
95///
96/// An invariant applies if:
97/// - It's a cross-platform invariant (access control, overflow), OR
98/// - Its category matches the program's blockchain platform
99fn 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    // Cross-platform invariants apply to all programs
104    if invariant_category.contains("access")
105        || invariant_category.contains("overflow")
106        || invariant_category.contains("general")
107    {
108        return true;
109    }
110
111    // Platform-specific invariants must match the program's chain
112    program_chain.contains(&invariant_category)
113}
114
115/// Analyze program-level invariants to detect violations.
116///
117/// Performs static analysis of:
118/// - Reentrancy risks (multiple entry points with state mutations)
119/// - Access control patterns (entry point functions without state checks)
120/// - Arithmetic overflow risks (functions with numeric parameters)
121fn analyze_program_invariant(invariant: &Invariant, program: &ProgramModel) -> Option<String> {
122    let invariant_name_lower = invariant.name.to_lowercase();
123
124    // Reentrancy risk: multiple entry points with state mutations
125    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    // Access control risk: entry points with public state access
150    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    // Overflow/underflow risk: functions with numeric operations
168    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
193/// Analyze function-level invariants to detect violations.
194///
195/// Performs static analysis of:
196/// - Complex state interactions (reads + mutates)
197/// - Entry point authorization patterns
198/// - Pure function expectations
199/// - Solana-specific vulnerabilities (lamport manipulation, unsafe arithmetic)
200fn 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    // Solana-specific: Unsafe lamport manipulation
210    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    // EVM-specific: Reentrancy vulnerability
223    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    // EVM-specific: Unchecked external calls
236    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    // EVM-specific: Integer overflow/underflow
249    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    // EVM-specific: Delegatecall abuse
262    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    // EVM-specific: Timestamp dependency
275    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    // EVM-specific: Front-running vulnerability
288    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    // EVM-specific: Missing access control
301    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    // EVM-specific: Input validation
314    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    // Move-specific: Resource leak
327    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    // Move-specific: Missing ability checks
340    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    // Move-specific: Unchecked arithmetic
353    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    // Move-specific: Missing signer verification
366    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    // Move-specific: Unguarded state mutation
379    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    // Move-specific: Privilege escalation
392    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    // Move-specific: Unsafe abort
405    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    // Solana-specific: Missing signer check
418    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    // Solana-specific: Unchecked arithmetic with overflow risk
433    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    // Solana-specific: Missing account validation
448    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    // Solana-specific: Rent exemption violations
463    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    // Solana-specific: PDA derivation issues
478    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    // Solana-specific: Unsafe deserialization
493    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    // Solana-specific: Unchecked token transfers
508    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    // Reentrancy: entry points with complex state interactions
523    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    // Access control: public entry points without authorization
538    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    // Arithmetic: functions with multiple numeric parameters
553    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}