From 6fe30e62c64594dbd5ec03d17870813169c49bcc Mon Sep 17 00:00:00 2001 From: Yusufolosun Date: Wed, 25 Feb 2026 17:19:15 +0100 Subject: [PATCH] test: add fuzz tests for zero-duration, zero-cliff, and zero-amount vaults Cover edge cases where duration=0, cliff=0, or amount=0 to verify no divide-by-zero panics occur and behavior matches expectations. Closes #67, closes #41 --- contracts/vesting_contracts/src/test.rs | 99 +++++++++++++++++++++++++ contracts/vesting_curves/src/test.rs | 93 +++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/contracts/vesting_contracts/src/test.rs b/contracts/vesting_contracts/src/test.rs index e68528c..bcf4fb4 100644 --- a/contracts/vesting_contracts/src/test.rs +++ b/contracts/vesting_contracts/src/test.rs @@ -1036,6 +1036,105 @@ impl MockStakingContract { client.rescue_unallocated_tokens(&token_addr); // must panic } + + // ------------------------------------------------------------------------- + // Zero-duration vault fuzz tests (Issue #41) + // ------------------------------------------------------------------------- + + #[test] + fn test_zero_duration_vault_immediate_unlock() { + let (env, _cid, client, _admin) = setup(); + let beneficiary = Address::generate(&env); + let now = env.ledger().timestamp(); + + let vault_id = client.create_vault_full( + &beneficiary, &5_000i128, &now, &now, + &0i128, &true, &false, &0u64, + ); + + let claimable = client.get_claimable_amount(&vault_id); + assert_eq!(claimable, 5_000i128, "zero-duration vault should unlock 100% immediately"); + } + + #[test] + fn test_zero_duration_vault_claim_full() { + let (env, _cid, client, _admin) = setup(); + let beneficiary = Address::generate(&env); + let now = env.ledger().timestamp(); + + let vault_id = client.create_vault_full( + &beneficiary, &10_000i128, &now, &now, + &0i128, &true, &false, &0u64, + ); + + let claimed = client.claim_tokens(&vault_id, &10_000i128); + assert_eq!(claimed, 10_000i128, "should claim full amount from zero-duration vault"); + + let vault = client.get_vault(&vault_id); + assert_eq!(vault.released_amount, 10_000i128); + } + + #[test] + fn test_zero_duration_vault_before_start() { + let (env, _cid, client, _admin) = setup(); + let beneficiary = Address::generate(&env); + let future = env.ledger().timestamp() + 1_000; + + let vault_id = client.create_vault_full( + &beneficiary, &5_000i128, &future, &future, + &0i128, &true, &false, &0u64, + ); + + let claimable = client.get_claimable_amount(&vault_id); + assert_eq!(claimable, 0, "zero-duration vault should not unlock before start_time"); + } + + #[test] + fn test_zero_cliff_vault_vests_immediately() { + let (env, _cid, client, _admin) = setup(); + let beneficiary = Address::generate(&env); + let now = env.ledger().timestamp(); + + let vault_id = client.create_vault_full( + &beneficiary, &10_000i128, &now, &(now + 1_000), + &0i128, &true, &false, &0u64, + ); + + env.ledger().with_mut(|l| l.timestamp = now + 500); + let claimable = client.get_claimable_amount(&vault_id); + assert!(claimable > 0, "zero-cliff vault should vest from start_time"); + } + + #[test] + fn test_zero_amount_vault_no_claimable() { + let (env, _cid, client, _admin) = setup(); + let beneficiary = Address::generate(&env); + let now = env.ledger().timestamp(); + + let vault_id = client.create_vault_full( + &beneficiary, &0i128, &now, &(now + 1_000), + &0i128, &true, &false, &0u64, + ); + + env.ledger().with_mut(|l| l.timestamp = now + 1_001); + let claimable = client.get_claimable_amount(&vault_id); + assert_eq!(claimable, 0, "zero-amount vault should have nothing claimable"); + } + + #[test] + fn test_zero_duration_zero_amount_vault() { + let (env, _cid, client, _admin) = setup(); + let beneficiary = Address::generate(&env); + let now = env.ledger().timestamp(); + + let vault_id = client.create_vault_full( + &beneficiary, &0i128, &now, &now, + &0i128, &true, &false, &0u64, + ); + + let claimable = client.get_claimable_amount(&vault_id); + assert_eq!(claimable, 0, "zero-duration + zero-amount vault should have nothing claimable"); + } } }); assert!(result.is_err()); diff --git a/contracts/vesting_curves/src/test.rs b/contracts/vesting_curves/src/test.rs index 6c018bc..3daf30f 100644 --- a/contracts/vesting_curves/src/test.rs +++ b/contracts/vesting_curves/src/test.rs @@ -255,4 +255,97 @@ fn i6_double_claim_only_yields_incremental_amount() { // Total received = TOTAL let bal = TokenClient::new(&s.env, &s.token).balance(&s.beneficiary); assert_eq!(bal, TOTAL); +} + +// ── Zero-duration / zero-amount edge cases (Issue #41) ────────────────────── + +#[test] +#[should_panic(expected = "duration must be positive")] +fn z1_zero_duration_panics() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + let token_admin = Address::generate(&env); + + let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()); + let token = token_id.address(); + StellarAssetClient::new(&env, &token).mint(&admin, &TOTAL); + + let vault_id = env.register(crate::VestingVault, ()); + let vault = VestingVaultClient::new(&env, &vault_id); + TokenClient::new(&env, &token).transfer(&admin, &vault_id, &TOTAL); + + env.ledger().with_mut(|l| l.timestamp = START); + + vault.initialize( + &admin, + &beneficiary, + &token, + &TOTAL, + &START, + &0u64, + &VestingCurve::Linear, + ); +} + +#[test] +#[should_panic(expected = "total_amount must be positive")] +fn z2_zero_amount_panics() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + let token_admin = Address::generate(&env); + + let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()); + let token = token_id.address(); + + let vault_id = env.register(crate::VestingVault, ()); + let vault = VestingVaultClient::new(&env, &vault_id); + + env.ledger().with_mut(|l| l.timestamp = START); + + vault.initialize( + &admin, + &beneficiary, + &token, + &0i128, + &START, + &DURATION, + &VestingCurve::Linear, + ); +} + +#[test] +#[should_panic(expected = "duration must be positive")] +fn z3_zero_duration_exponential_panics() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let beneficiary = Address::generate(&env); + let token_admin = Address::generate(&env); + + let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()); + let token = token_id.address(); + StellarAssetClient::new(&env, &token).mint(&admin, &TOTAL); + + let vault_id = env.register(crate::VestingVault, ()); + let vault = VestingVaultClient::new(&env, &vault_id); + TokenClient::new(&env, &token).transfer(&admin, &vault_id, &TOTAL); + + env.ledger().with_mut(|l| l.timestamp = START); + + vault.initialize( + &admin, + &beneficiary, + &token, + &TOTAL, + &START, + &0u64, + &VestingCurve::Exponential, + ); } \ No newline at end of file