diff --git a/contracts/split/src/calc.rs b/contracts/split/src/calc.rs index a1f61ea..de19239 100644 --- a/contracts/split/src/calc.rs +++ b/contracts/split/src/calc.rs @@ -307,4 +307,34 @@ mod tests { assert_exact(&env, total, ratios, denom); } } + + // ----------------------------------------------------------------------- + // calc_platform_fee tests + // ----------------------------------------------------------------------- + + #[test] + fn test_calc_platform_fee_normal() { + // 1_000_000 funded at 250 bps (2.5%) → fee = 25_000 + let fee = calc_platform_fee(1_000_000, 250).unwrap(); + assert_eq!(fee, 25_000); + } + + #[test] + fn test_calc_platform_fee_zero_bps() { + // Zero fee rate → always zero fee regardless of funded amount + assert_eq!(calc_platform_fee(999_999_999, 0).unwrap(), 0); + } + + #[test] + fn test_calc_platform_fee_max_bps() { + // 10_000 bps = 100% → fee equals funded + assert_eq!(calc_platform_fee(500, 10_000).unwrap(), 500); + } + + #[test] + fn test_calc_platform_fee_overflow() { + // i128::MAX * any fee_bps > 0 will overflow the intermediate multiplication + let result = calc_platform_fee(i128::MAX, 1); + assert_eq!(result, Err(crate::error::ContractError::ArithmeticOverflow)); + } } diff --git a/contracts/split/src/events.rs b/contracts/split/src/events.rs index 76416aa..37f3049 100644 --- a/contracts/split/src/events.rs +++ b/contracts/split/src/events.rs @@ -278,25 +278,26 @@ pub fn invoice_archived(env: &Env, invoice_id: u64) { /// Emitted when a delegate is assigned to an invoice. /// Topics: (split, delegated, invoice_id) -/// Data: delegate +/// Data: (delegate, event_seq) pub fn delegate_set(env: &Env, invoice_id: u64, delegate: &Address) { + let event_seq = next_seq(env, invoice_id); env.events().publish( ( symbol_short!("split"), symbol_short!("delegated"), invoice_id, ), - delegate.clone(), + (delegate.clone(), event_seq), ); } /// Emitted when a delegate is revoked from an invoice. /// Topics: (split, revoked, invoice_id) -/// Data: () -pub fn delegate_revoked(env: &Env, invoice_id: u64) { +/// Data: (revoker, ledger_sequence) +pub fn delegate_revoked(env: &Env, invoice_id: u64, revoker: &Address) { env.events().publish( (symbol_short!("split"), symbol_short!("revoked"), invoice_id), - (), + (revoker.clone(), env.ledger().sequence()), ); } diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index 8ddc962..0a0202b 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -1947,6 +1947,22 @@ fn load_invoice(env: &Env, id: u64) -> Invoice { invoice } +/// Guard: return `Err(ContractError::InvalidStatus)` unless the invoice is +/// still in the `Pending` state. Calling this at the top of any mutating +/// function replaces the repeated inline pattern: +/// +/// ```rust,ignore +/// if invoice.status != InvoiceStatus::Pending { +/// return Err(ContractError::InvalidStatus); +/// } +/// ``` +fn assert_invoice_pending(invoice: &Invoice) -> Result<(), ContractError> { + if invoice.status != InvoiceStatus::Pending { + return Err(ContractError::InvalidStatus); + } + Ok(()) +} + /// Estimates the serialised size (in bytes) of an invoice's persisted /// representation (issue #425). Sums the XDR-encoded length of the three /// pieces `save_invoice` actually writes to storage (`InvoiceCore`, @@ -3041,9 +3057,7 @@ impl SplitContract { let mut invoice = load_invoice(&env, invoice_id); // Only allow withdrawal while invoice is in Pending (Open) status. - if invoice.status != InvoiceStatus::Pending { - return Err(ContractError::InvalidStatus); - } + assert_invoice_pending(&invoice)?; let contrib_key = contribution_key(invoice_id, &payer); let amount: i128 = env @@ -9152,7 +9166,7 @@ impl SplitContract { let fee = if is_waived { 0 } else { - (proportional as u128 * platform_fee_bps as u128 / 10_000u128) as i128 + calc_platform_fee(proportional, platform_fee_bps).expect("ArithmeticOverflow") }; let tax = (proportional as u128 * invoice.tax_bps as u128 / 10_000u128) as i128; let payout = proportional - fee - tax; @@ -9716,7 +9730,7 @@ impl SplitContract { let fee = if is_waived { 0 } else { - (payout_raw as u128 * platform_fee_bps as u128 / 10_000u128) as i128 + calc_platform_fee(payout_raw, platform_fee_bps).expect("ArithmeticOverflow") }; let tax = (payout_raw as u128 * invoice.tax_bps as u128 / 10_000u128) as i128; let payout = payout_raw - fee - tax; @@ -10446,8 +10460,8 @@ impl SplitContract { (amount as u128 * member_funded as u128 / member_total as u128) as i128 }; - let fee = (proportional as u128 * platform_fee_bps as u128 / 10_000u128) - as i128; + let fee = calc_platform_fee(proportional, platform_fee_bps) + .expect("ArithmeticOverflow"); let tax = (proportional as u128 * member.tax_bps as u128 / 10_000u128) as i128; let payout = proportional - fee - tax; @@ -13100,7 +13114,7 @@ impl SplitContract { env.storage().persistent().remove(&delegate_key(invoice_id)); - events::delegate_revoked(&env, invoice_id); + events::delegate_revoked(&env, invoice_id, &invoice.creator); append_audit_entry(&env, invoice_id, symbol_short!("rvk_del"), &invoice.creator); }