use crate::FuzzAccount;
#[derive(Debug, Clone)]
pub struct InvariantResult {
pub name: String,
pub passed: bool,
pub message: Option<String>,
pub round: u64,
}
pub type InvariantFn = Box<dyn Fn(&[FuzzAccount], &[FuzzAccount], u64) -> Option<String>>;
pub struct InvariantRegistry {
pub invariants: Vec<(String, InvariantFn)>,
}
impl Default for InvariantRegistry {
fn default() -> Self {
Self::new()
}
}
impl InvariantRegistry {
pub fn new() -> Self {
Self {
invariants: Vec::new(),
}
}
pub fn register(&mut self, name: &str, f: InvariantFn) {
self.invariants.push((name.to_string(), f));
}
pub fn check_all(
&self,
current: &[FuzzAccount],
initial: &[FuzzAccount],
round: u64,
) -> Vec<InvariantResult> {
self.invariants
.iter()
.map(|(name, f)| {
let violation = f(current, initial, round);
InvariantResult {
name: name.clone(),
passed: violation.is_none(),
message: violation,
round,
}
})
.collect()
}
}
pub fn invariant_supply_conservation(
current: &[FuzzAccount],
initial: &[FuzzAccount],
_round: u64,
) -> Option<String> {
let current_sum: u64 = current.iter().map(|a| a.lamports).sum();
let initial_sum: u64 = initial.iter().map(|a| a.lamports).sum();
let rent_tolerance = initial.len() as u64 * initial_sum / 100;
if current_sum < initial_sum.saturating_sub(rent_tolerance) {
Some(format!(
"Supply conservation violated: initial={}, current={}, delta={}",
initial_sum,
current_sum,
initial_sum as i128 - current_sum as i128
))
} else {
None
}
}
pub fn invariant_admin_immutability(
current: &[FuzzAccount],
initial: &[FuzzAccount],
_round: u64,
) -> Option<String> {
for (curr, init) in current.iter().zip(initial.iter()) {
if curr.owner != init.owner && init.is_signer {
return Some(format!(
"Admin account owner changed: {} → {}",
init.owner, curr.owner
));
}
}
None
}
pub fn invariant_state_machine_paused(
current: &[FuzzAccount],
_initial: &[FuzzAccount],
_round: u64,
) -> Option<String> {
for account in current {
if !account.data.is_empty() && (account.data[0] & 0x01) != 0 {
return Some("Program appears to be in paused state".to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_accounts() -> Vec<FuzzAccount> {
vec![
FuzzAccount {
key: "A".into(),
lamports: 100,
..Default::default()
},
FuzzAccount {
key: "B".into(),
lamports: 200,
..Default::default()
},
]
}
#[test]
fn test_supply_conservation_passes() {
let initial = make_test_accounts();
let mut current = initial.clone();
current[0].lamports = 95;
current[1].lamports = 200; let result = invariant_supply_conservation(¤t, &initial, 0);
assert!(result.is_none());
}
#[test]
fn test_supply_conservation_violated() {
let initial = make_test_accounts();
let mut current = initial.clone();
current[0].lamports = 0; let result = invariant_supply_conservation(¤t, &initial, 0);
assert!(result.is_some());
}
#[test]
fn test_registry_checks_all() {
let mut registry = InvariantRegistry::new();
registry.register("supply", Box::new(invariant_supply_conservation));
registry.register("admin", Box::new(invariant_admin_immutability));
let initial = make_test_accounts();
let current = initial.clone();
let results = registry.check_all(¤t, &initial, 0);
assert_eq!(results.len(), 2);
assert!(results[0].passed); }
}