From cc1ef0b19a6945a57cf8c0e17545a5a7271ac132 Mon Sep 17 00:00:00 2001 From: CodedBay Date: Mon, 31 Aug 2026 09:58:31 +0000 Subject: [PATCH] fix(#695): reject velocity_limit > 0 when velocity_window == 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When velocity_limit is set to a positive value but velocity_window is zero, dividing by the window to compute per-payer spend rates results in division-by-zero or undefined behaviour. Add an early guard in _create_invoice_inner that calls panic_with_error!(env, ContractError::InvalidAmount) before any storage is written whenever velocity_limit > 0 && velocity_window == 0. velocity_limit == 0 is always allowed regardless of velocity_window (the feature is simply disabled). Tests added: - test_velocity_limit_without_window_rejected: velocity_limit=1000, velocity_window=0 → panic (should_panic) - test_velocity_limit_with_valid_window_accepted: velocity_limit=1000, velocity_window=3600 → invoice created successfully Closes #695 --- contracts/split/src/lib.rs | 6 ++++ contracts/split/src/test.rs | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index b9b686c..b58fe4f 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -5443,6 +5443,12 @@ impl SplitContract { env.panic_with_error(e); } assert!(min_funding_bps <= 10_000, "min_funding_bps must be ≤ 10000"); + // Issue #695: velocity_limit > 0 paired with velocity_window == 0 would cause + // division-by-zero or undefined behaviour when computing per-payer spend rates. + // Reject the configuration before any storage is written. + if velocity_limit > 0 && velocity_window == 0 { + panic_with_error!(env, ContractError::InvalidAmount); + } assert_valid_bps(tax_bps).expect("tax_bps must be ≤ 10000"); assert_valid_bps(insurance_premium_bps).expect("insurance_premium_bps must be ≤ 10000"); // Issue #489 / #696: early-bird discounted platform fee must not exceed the diff --git a/contracts/split/src/test.rs b/contracts/split/src/test.rs index 8dc6bbc..b3e175f 100644 --- a/contracts/split/src/test.rs +++ b/contracts/split/src/test.rs @@ -2907,6 +2907,74 @@ fn test_min_funding_bps_allows_release_above_threshold() { assert_eq!(tk.balance(&recipient), 900); } +// --------------------------------------------------------------------------- +// Issue #695: validate velocity_window is non-zero when velocity_limit is set +// --------------------------------------------------------------------------- + +#[test] +#[should_panic] +fn test_velocity_limit_without_window_rejected() { + // velocity_limit=1000 with velocity_window=0 must be rejected with ContractError::InvalidAmount. + let (env, contract_id, token_id) = setup_initialized(); + let c = client(&env, &contract_id); + + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + + env.ledger().set_timestamp(1_000); + + let mut recipients = Vec::new(&env); + recipients.push_back(recipient.clone()); + let mut amounts = Vec::new(&env); + amounts.push_back(1_000_i128); + + // velocity_limit > 0 but velocity_window == 0 — undefined behaviour; must panic. + c.create_invoice( + &creator, + &recipients, + &amounts, + &token_id, + &9_999_u64, + &InvoiceOptions { + velocity_limit: 1_000, + velocity_window: 0, + ..default_options(&env) + }, + ); +} + +#[test] +fn test_velocity_limit_with_valid_window_accepted() { + // velocity_limit=1000, velocity_window=3600 is a valid configuration and must be accepted. + let (env, contract_id, token_id) = setup_initialized(); + let c = client(&env, &contract_id); + + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + + env.ledger().set_timestamp(1_000); + + let mut recipients = Vec::new(&env); + recipients.push_back(recipient.clone()); + let mut amounts = Vec::new(&env); + amounts.push_back(1_000_i128); + + // Both limit and window are set — valid; invoice creation must succeed. + let id = c.create_invoice( + &creator, + &recipients, + &amounts, + &token_id, + &9_999_u64, + &InvoiceOptions { + velocity_limit: 1_000, + velocity_window: 3_600, + ..default_options(&env) + }, + ); + assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); +} + // --------------------------------------------------------------------------- // Issue #85: generate_payment_proof // ---------------------------------------------------------------------------