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
12 changes: 7 additions & 5 deletions bill_payments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1360,11 +1360,7 @@ impl BillPayments {
.get(&STORAGE_UNPAID_TOTALS)
.unwrap_or_else(|| Map::new(env));
let current = totals.get(owner.clone()).unwrap_or(0);
let next = if delta >= 0 {
current.saturating_add(delta)
} else {
current.saturating_sub(delta.saturating_abs())
};
let next = current.checked_add(delta).expect("overflow");
totals.set(owner.clone(), next);
env.storage()
.instance()
Expand Down Expand Up @@ -2264,6 +2260,7 @@ mod test {
&(now - 1 - i as u64),
&false,
&0,
&String::from_str(&env, "XLM"),
);
}

Expand All @@ -2276,6 +2273,7 @@ mod test {
&(now + 1 + i as u64),
&false,
&0,
&String::from_str(&env, "XLM"),
);
}

Expand Down Expand Up @@ -2309,6 +2307,7 @@ mod test {
&(now + i as u64), // due_date >= now — strict less-than is required to be overdue
&false,
&0,
&String::from_str(&env, "XLM"),
);
}

Expand Down Expand Up @@ -2346,6 +2345,7 @@ mod test {
&base_due,
&true,
&freq_days,
&String::from_str(&env, "XLM"),
);

client.pay_bill(&owner, &bill_id);
Expand All @@ -2359,6 +2359,8 @@ mod test {
);
prop_assert!(!next_bill.paid, "next recurring bill must be unpaid");
}
}

/// Issue #102 – When pay_bill is called on a recurring bill, the contract
/// creates the next occurrence. This test asserts every cloned field
/// individually so that a regression in the clone logic (e.g. paid left
Expand Down
64 changes: 32 additions & 32 deletions bill_payments/tests/stress_test_large_amounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,43 +158,43 @@ fn test_get_total_unpaid_with_two_large_bills() {
assert_eq!(total, amount + amount);
}

// #[test]
// #[should_panic(expected = "overflow")]
// fn test_get_total_unpaid_overflow_panics() {
// let env = Env::default();
// let contract_id = env.register_contract(None, BillPayments);
// let client = BillPaymentsClient::new(&env, &contract_id);
// let owner = <soroban_sdk::Address as AddressTrait>::generate(&env);
#[test]
#[should_panic(expected = "overflow")]
fn test_get_total_unpaid_overflow_panics() {
let env = Env::default();
let contract_id = env.register_contract(None, BillPayments);
let client = BillPaymentsClient::new(&env, &contract_id);
let owner = <soroban_sdk::Address as AddressTrait>::generate(&env);

// env.mock_all_auths();
env.mock_all_auths();

// // Create two bills that will overflow when added
// let amount = i128::MAX / 2 + 1000;
// Create two bills that will overflow when added
let amount = i128::MAX / 2 + 1000;

// client.create_bill(
// &owner,
// &String::from_str(&env, "Bill1"),
// &amount,
// &1000000,
// &false,
// &0,
// &String::from_str(&env, "XLM"),
// );
client.create_bill(
&owner,
&String::from_str(&env, "Bill1"),
&amount,
&1000000,
&false,
&0,
&String::from_str(&env, "XLM"),
);

// env.mock_all_auths();
// client.create_bill(
// &owner,
// &String::from_str(&env, "Bill2"),
// &amount,
// &1000000,
// &false,
// &0,
// &String::from_str(&env, "XLM"),
// );
env.mock_all_auths();
client.create_bill(
&owner,
&String::from_str(&env, "Bill2"),
&amount,
&1000000,
&false,
&0,
&String::from_str(&env, "XLM"),
);

// // This should panic due to overflow
// client.get_total_unpaid(&owner);
// }
// This should panic due to overflow
client.get_total_unpaid(&owner);
}

#[test]
fn test_multiple_large_bills_different_owners() {
Expand Down
7 changes: 7 additions & 0 deletions gas_results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{"contract":"bill_payments","method":"get_total_unpaid","scenario":"100_bills_50_cancelled","cpu":1077221,"mem":235460},
{"contract":"savings_goals","method":"get_all_goals","scenario":"100_goals_single_owner","cpu":2661552,"mem":480721},
{"contract":"insurance","method":"get_total_monthly_premium","scenario":"100_active_policies","cpu":2373104,"mem":427575},
{"contract":"family_wallet","method":"configure_multisig","scenario":"9_signers_threshold_all","cpu":342677,"mem":69106},
{"contract":"remittance_split","method":"distribute_usdc","scenario":"4_recipients_all_nonzero","cpu":654751,"mem":86208}
]
81 changes: 81 additions & 0 deletions insurance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,87 @@ mod test_events {
assert_eq!(result, Err(Ok(InsuranceError::PolicyInactive)));
}

// -----------------------------------------------------------------------
// Property-based tests: time-dependent behavior
// -----------------------------------------------------------------------

proptest! {
/// After paying a premium at any timestamp `now`,
/// next_payment_date must always equal now + 30 days.
#[test]
fn prop_pay_premium_sets_next_payment_date(
now in 1_000_000u64..100_000_000u64,
) {
let env = make_env();
env.ledger().set_timestamp(now);
env.mock_all_auths();
let cid = env.register_contract(None, Insurance);
let client = InsuranceClient::new(&env, &cid);
let owner = Address::generate(&env);

let policy_id = client.create_policy(
&owner,
&String::from_str(&env, "Policy"),
&String::from_str(&env, "health"),
&100,
&10000,
);

client.pay_premium(&owner, &policy_id);

let policy = client.get_policy(&policy_id).unwrap();
prop_assert_eq!(
policy.next_payment_date,
now + 30 * 86400,
"next_payment_date must equal now + 30 days after premium payment"
);
}
}

proptest! {
/// A premium schedule must not execute before its due date,
/// and must execute at or after its due date.
#[test]
fn prop_execute_due_schedules_only_triggers_past_due(
creation_time in 1_000_000u64..5_000_000u64,
gap in 1000u64..1_000_000u64,
) {
let env = make_env();
env.ledger().set_timestamp(creation_time);
env.mock_all_auths();
let cid = env.register_contract(None, Insurance);
let client = InsuranceClient::new(&env, &cid);
let owner = Address::generate(&env);

let policy_id = client.create_policy(
&owner,
&String::from_str(&env, "Policy"),
&String::from_str(&env, "health"),
&100,
&10000,
);

// Schedule fires at creation_time + gap (strictly in the future)
let next_due = creation_time + gap;
let schedule_id = client.create_premium_schedule(&owner, &policy_id, &next_due, &0);

// One tick before due: schedule must not execute
env.ledger().set_timestamp(next_due - 1);
let executed_before = client.execute_due_premium_schedules();
prop_assert_eq!(
executed_before.len(),
0u32,
"schedule must not fire before its due date"
);

// Exactly at due date: schedule must execute
env.ledger().set_timestamp(next_due);
let executed_at = client.execute_due_premium_schedules();
prop_assert_eq!(executed_at.len(), 1u32);
prop_assert_eq!(executed_at.get(0).unwrap(), schedule_id);
}
}

// ══════════════════════════════════════════════════════════════════════
// Time & Ledger Drift Resilience Tests (#158)
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,26 @@
}
]
}
},
{
"key": {
"symbol": "PRM_TOT"
},
"val": {
"map": [
{
"key": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
},
"val": {
"i128": {
"hi": 0,
"lo": 1400
}
}
}
]
}
}
]
}
Expand Down
52 changes: 35 additions & 17 deletions remittance_split/tests/stress_test_large_amounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ fn test_calculate_split_with_large_amount() {
// client.calculate_split returns Vec<i128> directly
let amounts = client.calculate_split(&large_amount);

let result = client.try_calculate_split(&large_amount);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();
assert_eq!(amounts.len(), 4);
let total: i128 = amounts.iter().sum();
assert_eq!(total, large_amount);
Expand All @@ -54,6 +58,10 @@ fn test_calculate_split_near_max_safe_value() {
let max_safe = i128::MAX / 100 - 1;
let amounts = client.calculate_split(&max_safe);

let result = client.try_calculate_split(&max_safe);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();
let total: i128 = amounts.iter().sum();
assert!((total - max_safe).abs() < 4); // Allow small rounding difference
}
Expand All @@ -69,8 +77,8 @@ fn test_calculate_split_near_max_safe_value() {

// client.initialize_split(&owner, &0, &50, &30, &15, &5);

// // Value that will overflow when multiplied by percentage
// let overflow_amount = i128::MAX / 50; // Will overflow when multiplied by 50
// Value that will overflow when multiplied by percentage
let overflow_amount = i128::MAX / 50 + 1; // Will overflow when multiplied by 50

// let result = client.try_calculate_split(&overflow_amount);

Expand All @@ -91,9 +99,10 @@ fn test_calculate_split_with_minimal_percentages() {

let large_amount = i128::MAX / 150;

// FIX: Remove .is_ok() and .unwrap()
let amounts = client.calculate_split(&large_amount);
let result = client.try_calculate_split(&large_amount);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();
let total: i128 = amounts.iter().sum();
assert_eq!(total, large_amount);
}
Expand All @@ -111,8 +120,10 @@ fn test_get_split_allocations_with_large_amount() {

let large_amount = i128::MAX / 200;

let allocations = client.get_split_allocations(&large_amount);
let result = client.try_get_split_allocations(&large_amount);
assert!(result.is_ok());

let allocations = result.unwrap().unwrap();
assert_eq!(allocations.len(), 4);
let total: i128 = allocations.iter().map(|a| a.amount).sum();
assert_eq!(total, large_amount);
Expand All @@ -132,9 +143,10 @@ fn test_multiple_splits_with_large_amounts() {
let large_amount = i128::MAX / 300;

for _ in 0..5 {
// FIX: result is now directly the amounts Vec
let amounts = client.calculate_split(&large_amount);
let result = client.try_calculate_split(&large_amount);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();
let total: i128 = amounts.iter().sum();
assert_eq!(total, large_amount);
}
Expand All @@ -153,9 +165,10 @@ fn test_edge_case_i128_max_divided_by_100() {
// Exact edge case: i128::MAX / 100
let edge_amount = i128::MAX / 100;

// FIX: Remove .is_ok() and .unwrap()
let amounts = client.calculate_split(&edge_amount);
let result = client.try_calculate_split(&edge_amount);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();
assert_eq!(amounts.len(), 4);
}

Expand All @@ -173,9 +186,10 @@ fn test_split_with_100_percent_to_one_category() {

let large_amount = i128::MAX / 150;

// FIX: result is now the amounts Vec directly
let amounts = client.calculate_split(&large_amount);
let result = client.try_calculate_split(&large_amount);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();
// First amount should be the full amount
// .get(i) returns Option, so .unwrap() here is correct and necessary
assert_eq!(amounts.get(0).unwrap(), large_amount);
Expand All @@ -199,8 +213,10 @@ fn test_rounding_behavior_with_large_amounts() {

let large_amount = i128::MAX / 200;

let amounts = client.calculate_split(&large_amount);
let result = client.try_calculate_split(&large_amount);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();
let total: i128 = amounts.iter().sum();

// Due to rounding, total should equal input
Expand Down Expand Up @@ -228,9 +244,10 @@ fn test_sequential_large_calculations() {
];

for amount in amounts_to_test {
// FIX: result is directly the soroban_sdk::Vec<i128>
let splits = client.calculate_split(&amount);
let result = client.try_calculate_split(&amount);
assert!(result.is_ok(), "Failed for amount: {}", amount);

let splits = result.unwrap().unwrap();
let total: i128 = splits.iter().sum();
assert_eq!(total, amount, "Failed for amount: {}", amount);
}
Expand Down Expand Up @@ -279,9 +296,10 @@ fn test_insurance_remainder_calculation_with_large_values() {

let large_amount = i128::MAX / 200;

// FIX: Remove .is_ok() and .unwrap()
// result is already soroban_sdk::Vec<i128>
let amounts = client.calculate_split(&large_amount);
let result = client.try_calculate_split(&large_amount);
assert!(result.is_ok());

let amounts = result.unwrap().unwrap();

// Verify insurance (last element) is calculated correctly as remainder
// Note: Soroban Vec::get returns Option, so these unwrap()s are correct for the elements
Expand Down
Loading
Loading