From 4890b74b43b4e52a44e33f20b9e5d91d6d2a7389 Mon Sep 17 00:00:00 2001 From: Marvin Michael Nkut Date: Thu, 28 May 2026 04:47:59 +0000 Subject: [PATCH] feat: add balance verification to _release() (#32) - Assert contract token balance >= invoice.funded before any transfer - Panics with 'insufficient contract balance' on discrepancy - Add test_release_panics_on_low_balance: drains contract mid-payment, verifies panic All 8 tests pass, clippy clean. Closes #32 --- contracts/split/src/lib.rs | 6 ++++++ contracts/split/src/test.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index 9bf6632..ae876a2 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -222,6 +222,12 @@ impl SplitContract { fn _release(env: &Env, invoice_id: u64, invoice: &mut Invoice) { let token_client = token::Client::new(env, &invoice.token); + let contract_balance = token_client.balance(&env.current_contract_address()); + assert!( + contract_balance >= invoice.funded, + "insufficient contract balance" + ); + for (recipient, amount) in invoice.recipients.iter().zip(invoice.amounts.iter()) { token_client.transfer(&env.current_contract_address(), &recipient, &amount); } diff --git a/contracts/split/src/test.rs b/contracts/split/src/test.rs index 7d326a2..e745f6e 100644 --- a/contracts/split/src/test.rs +++ b/contracts/split/src/test.rs @@ -250,3 +250,39 @@ fn test_multi_recipient_release() { assert_eq!(tk.balance(&r2), 200); assert_eq!(tk.balance(&r3), 300); } + +#[test] +#[should_panic(expected = "insufficient contract balance")] +fn test_release_panics_on_low_balance() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + let tk = token_client(&env, &token_id); + + let creator = Address::generate(&env); + let payer = Address::generate(&env); + let recipient = Address::generate(&env); + let drain = Address::generate(&env); + + let stellar_asset = StellarAssetClient::new(&env, &token_id); + stellar_asset.mint(&payer, &200); + + 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(200_i128); + + let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64); + + // Pay partially — invoice stays Pending, contract holds 100. + c.pay(&payer, &id, &100_i128); + + // Drain the contract's balance externally (simulates discrepancy). + // mock_all_auths allows the transfer from the contract address. + tk.transfer(&contract_id, &drain, &100_i128); + + // Pay the remaining 100 — triggers auto-release, which should panic. + stellar_asset.mint(&payer, &100); + c.pay(&payer, &id, &100_i128); +}