From c88f6b3c2020534d9ab29e0113b9f3bcbc771704 Mon Sep 17 00:00:00 2001 From: CodedBay Date: Mon, 31 Aug 2026 10:00:20 +0000 Subject: [PATCH] fix(#694): reject min_funding_bps > 10 000 at invoice creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit min_funding_bps sets the minimum percentage of the invoice total that must be funded before release. There was no bounds check; a value above 10 000 would require more than 100% funding, making the invoice permanently unreleasable. Replace the bare assert! with panic_with_error!(env, ContractError::InvalidAmount) so the contract returns a typed, on-chain error code when min_funding_bps > 10_000. The guard fires before any storage is written. Tests added: - test_min_funding_bps_above_10000_rejected: min_funding_bps=10_001 → panic (should_panic) - test_min_funding_bps_exactly_10000_accepted: min_funding_bps=10_000 → invoice created successfully (boundary value) Closes #694 --- contracts/split/src/lib.rs | 7 +++- contracts/split/src/test.rs | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index b9b686c..485e944 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -5442,7 +5442,12 @@ impl SplitContract { if let Err(e) = assert_valid_bps(penalty_bps) { env.panic_with_error(e); } - assert!(min_funding_bps <= 10_000, "min_funding_bps must be ≤ 10000"); + // Issue #694: min_funding_bps must be a valid basis-point fraction (0-10 000). + // Values above 10 000 would require more than 100% funding, making the invoice + // permanently unreleasable. Use panic_with_error! for a typed, on-chain error code. + if min_funding_bps > 10_000 { + 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..f46cfbc 100644 --- a/contracts/split/src/test.rs +++ b/contracts/split/src/test.rs @@ -2907,6 +2907,73 @@ fn test_min_funding_bps_allows_release_above_threshold() { assert_eq!(tk.balance(&recipient), 900); } +// --------------------------------------------------------------------------- +// Issue #694: validate min_funding_bps at invoice creation +// --------------------------------------------------------------------------- + +#[test] +#[should_panic] +fn test_min_funding_bps_above_10000_rejected() { + // min_funding_bps = 10_001 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); + + // 10_001 bps > 10_000 — must be rejected before any storage is written. + c.create_invoice( + &creator, + &recipients, + &amounts, + &token_id, + &9_999_u64, + &InvoiceOptions { + min_funding_bps: Some(10_001), + ..default_options(&env) + }, + ); +} + +#[test] +fn test_min_funding_bps_exactly_10000_accepted() { + // min_funding_bps = 10_000 (100%) is a valid boundary value 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); + + // 10_000 bps == 100% — exactly at the upper boundary, must succeed. + let id = c.create_invoice( + &creator, + &recipients, + &amounts, + &token_id, + &9_999_u64, + &InvoiceOptions { + min_funding_bps: Some(10_000), + ..default_options(&env) + }, + ); + // Invoice was created; a valid status confirms no panic occurred. + assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); +} + // --------------------------------------------------------------------------- // Issue #85: generate_payment_proof // ---------------------------------------------------------------------------