Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions contracts/src/fuzz_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
#![cfg(test)]
use soroban_sdk::{testutils::Address as _, Address, Env, IntoVal};

/// Fuzz testing utilities for discovering edge cases and vulnerabilities
/// Run with: cargo test --lib fuzz_

/// Property-based testing configuration
pub struct FuzzConfig {
pub iterations: u32,
pub max_value: i128,
pub seed: u64,
}

impl Default for FuzzConfig {
fn default() -> Self {
Self {
iterations: 100,
max_value: 1_000_000,
seed: 12345,
}
}
}

/// Simple PRNG for fuzz testing
pub struct Fuzzer {
state: u64,
}

impl Fuzzer {
pub fn new(seed: u64) -> Self {
Self { state: seed }
}

/// Generate random i128 in range
pub fn next_i128(&mut self, max: i128) -> i128 {
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
let abs = (self.state as i128).abs() % max.abs().max(1);
if (self.state & 1) != 0 { abs } else { -abs }
}

/// Generate random u32 in range
pub fn next_u32(&mut self, max: u32) -> u32 {
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
(self.state % max as u64) as u32
}

/// Generate random bool
pub fn next_bool(&mut self) -> bool {
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
(self.state & 1) != 0
}

/// Generate random address
pub fn next_address(&mut self, env: &Env) -> Address {
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
Address::from_account_id(&env, &[self.state as u8; 32])
}

/// Generate random bytes
pub fn next_bytes(&mut self, env: &Env, len: u32) -> soroban_sdk::Bytes {
let mut bytes = soroban_sdk::Bytes::new(env);
for _ in 0..len {
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
bytes.push_back(self.state as u8);
}
bytes
}
}

/// Property: Arithmetic operations should not overflow/underflow
pub mod arithmetic {
use super::*;

pub fn test_addition_no_overflow(env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
for _ in 0..100 {
let a = fuzzer.next_i128(1_000_000);
let b = fuzzer.next_i128(1_000_000);

// In Soroban, i128 can handle large values, but boundary testing is important
let result = a + b;

// Property: result should be deterministic
let result2 = a + b;
assert_eq!(result, result2, "Addition should be deterministic");
}
}

pub fn test_subtraction_no_underflow(env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
for _ in 0..100 {
let a = fuzzer.next_i128(1_000_000);
let b = fuzzer.next_i128(1_000_000);

// Test that subtraction works in both orders
let result_a_minus_b = a - b;
let result_b_minus_a = b - a;

// Property: a - b = -(b - a)
assert_eq!(result_a_minus_b, -result_b_minus_a, "Subtraction should be symmetric");
}
}

pub fn test_multiplication_bounds(env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
for _ in 0..100 {
let a = fuzzer.next_i128(10_000);
let b = fuzzer.next_i128(10_000);

let result = a * b;

// Property: multiplication should not lose sign
let expected_sign = (a.signum() * b.signum()).signum();
assert_eq!(result.signum(), expected_sign, "Multiplication should preserve sign");
}
}

pub fn test_division_by_zero(env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
for _ in 0..100 {
let a = fuzzer.next_i128(1_000_000);
let zero = 0i128;

// Division by zero should be handled gracefully
// In practice, contracts should check for zero divisor before division
let _ = if zero != 0 { Some(a / zero) } else { None };
}
}
}

/// Property: Access control should be enforced
pub mod access_control {
use super::*;

pub fn test_unauthorized_access_denied(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Non-admin users should not be able to call admin functions
// This is tested in the main contract tests
// Here we document the expected behavior
}

pub fn test_double_spend_prevention(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Once tokens are spent, they cannot be spent again
// This should be enforced by the state machine
}

pub fn test_signer_authorization(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Only authorized signers can approve multi-sig proposals
}
}

/// Property: State transitions should be valid
pub mod state_transitions {
use super::*;

pub fn test_invalid_plan_states(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Plans should only transition through valid states
// Plan states: Active -> Completed/Withdrawn
}

pub fn test_withdraw_before_maturity(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Early withdrawal should either fail or apply penalty
}

pub fn test_completed_plan_immutable(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Once a plan is completed, its balance should be locked
}
}

/// Property: Edge cases in calculations
pub mod calculations {
use super::*;

pub fn test_zero_balance_operations(env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
for _ in 0..50 {
let zero = 0i128;
let amount = fuzzer.next_i128(1_000);

// Property: Operations with zero balance should be safe
let sum = zero + amount;
assert_eq!(sum, amount, "Zero + amount = amount");

let diff = amount - zero;
assert_eq!(diff, amount, "amount - zero = amount");
}
}

pub fn test_interest_calculation_precision(env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
for _ in 0..100 {
let principal = fuzzer.next_i128(100_000);
let rate = fuzzer.next_u32(1000) as i128; // 0-1000 represents 0-100%

// Simple interest: interest = principal * rate / 10000
let interest = principal * rate / 10000;

// Property: Interest should never exceed principal for reasonable rates
if rate <= 10000 {
assert!(interest <= principal * 2, "Interest should be bounded");
}
}
}

pub fn test_time_calculation_overflow(env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
for _ in 0..50 {
let time1 = fuzzer.next_u32(u32::MAX);
let time2 = fuzzer.next_u32(86400); // 1 day in seconds

// Property: Time calculations should handle overflow
let diff = if time1 > time2 { time1 - time2 } else { time2 - time1 };
assert!(diff <= u32::MAX, "Time difference should be valid");
}
}
}

/// Property: Input validation
pub mod input_validation {
use super::*;

pub fn test_negative_amount_rejected(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Negative amounts should be rejected
}

pub fn test_zero_amount_operations(_env: &Env, _fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
// Property: Zero amount operations should be no-ops or fail gracefully
}

pub fn test_max_amount_handling(_env: &Env, fuzzer: &mut Fuzzer, _config: &FuzzConfig) {
let max_i128 = i128::MAX;
let large_amount = max_i128 / 2;

// Property: Large amounts should not cause overflow
let result = large_amount + large_amount;
assert!(result > 0, "Large sum should be valid");
}
}

/// Run all fuzz tests
pub fn run_fuzz_tests(env: &Env) {
let config = FuzzConfig::default();
let mut fuzzer = Fuzzer::new(config.seed);

// Arithmetic tests
arithmetic::test_addition_no_overflow(env, &mut fuzzer, &config);
arithmetic::test_subtraction_no_underflow(env, &mut fuzzer, &config);
arithmetic::test_multiplication_bounds(env, &mut fuzzer, &config);
arithmetic::test_division_by_zero(env, &mut fuzzer, &config);

// Calculation tests
calculations::test_zero_balance_operations(env, &mut fuzzer, &config);
calculations::test_interest_calculation_precision(env, &mut fuzzer, &config);
calculations::test_time_calculation_overflow(env, &mut fuzzer, &config);

// Input validation tests
input_validation::test_max_amount_handling(env, &mut fuzzer, &config);
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_fuzzer_determinism() {
let env = Env::default();
let mut f1 = Fuzzer::new(42);
let mut f2 = Fuzzer::new(42);

for _ in 0..100 {
assert_eq!(f1.next_i128(1000), f2.next_i128(1000));
}
}

#[test]
fn test_arithmetic_properties() {
let env = Env::default();
let config = FuzzConfig::default();
let mut fuzzer = Fuzzer::new(config.seed);

arithmetic::test_addition_no_overflow(&env, &mut fuzzer, &config);
}

#[test]
fn test_calculation_properties() {
let env = Env::default();
let config = FuzzConfig::default();
let mut fuzzer = Fuzzer::new(config.seed);

calculations::test_zero_balance_operations(&env, &mut fuzzer, &config);
}
}
Loading
Loading