diff --git a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs index 7b10e8d..53bc477 100644 --- a/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/compliance/src/lib.rs @@ -32,7 +32,10 @@ pub enum DataKey { pub struct ComplianceContract; fn is_paused(e: &Env) -> bool { - e.storage().instance().get(&DataKey::Paused).unwrap_or(false) + e.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) } fn check_not_paused(e: &Env) -> Result<(), ContractError> { @@ -100,21 +103,16 @@ impl ComplianceContract { ) -> Result<(), ContractError> { check_not_paused(&e)?; admin.require_auth(); - e.storage() - .instance() - .set(&DataKey::Status(addr.clone()), &AddressStatus::AllowedUntil(until)); - e.events().publish( - (Symbol::new(&e, "address_allowed_until"),), - (addr, until), + e.storage().instance().set( + &DataKey::Status(addr.clone()), + &AddressStatus::AllowedUntil(until), ); + e.events() + .publish((Symbol::new(&e, "address_allowed_until"),), (addr, until)); Ok(()) } - pub fn transfer_admin( - e: Env, - admin: Address, - new_admin: Address, - ) -> Result<(), ContractError> { + pub fn transfer_admin(e: Env, admin: Address, new_admin: Address) -> Result<(), ContractError> { check_not_paused(&e)?; admin.require_auth(); e.storage().instance().set(&DataKey::Admin, &new_admin); @@ -133,13 +131,9 @@ impl ComplianceContract { return Err(ContractError::Unauthorized); } e.storage().instance().set(&DataKey::Admin, &new_admin); - e.storage() - .instance() - .remove(&DataKey::PendingAdmin); - e.events().publish( - (Symbol::new(&e, "accept_admin"),), - &new_admin, - ); + e.storage().instance().remove(&DataKey::PendingAdmin); + e.events() + .publish((Symbol::new(&e, "accept_admin"),), &new_admin); Ok(()) } @@ -160,7 +154,7 @@ impl ComplianceContract { .instance() .remove(&DataKey::Status(addr.clone())); e.events() - .publish((Symbol::new(&e, "address_cleared"),), addr); + .publish((Symbol::new(&e, "address_cleared"),), (addr, status)); Ok(()) } diff --git a/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs b/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs index 1a55be7..1ce090e 100644 --- a/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/invoice/src/lib.rs @@ -114,18 +114,14 @@ impl InvoiceContract { if env.storage().persistent().has(&DataKey::Admin) { return Err(ContractError::AlreadyInitialized); } - env.storage() - .persistent() - .set(&DataKey::Admin, &admin); + env.storage().persistent().set(&DataKey::Admin, &admin); env.storage() .persistent() .set(&DataKey::GraceWindow, &86400u64); env.storage() .persistent() .set(&DataKey::InvoiceCount, &0u64); - env.storage() - .persistent() - .set(&DataKey::Paused, &false); + env.storage().persistent().set(&DataKey::Paused, &false); Ok(()) } @@ -169,16 +165,14 @@ impl InvoiceContract { if env.storage().persistent().has(&nonce_key) { return Err(ContractError::DuplicateNonce); } - env.storage() - .persistent() - .set(&nonce_key, &true); + env.storage().persistent().set(&nonce_key, &true); let mut count: u64 = env .storage() .persistent() .get(&DataKey::InvoiceCount) .unwrap_or(0); - count += 1; + count = count.checked_add(1).ok_or(ContractError::Overflow)?; env.storage() .persistent() .set(&DataKey::InvoiceCount, &count); @@ -440,7 +434,11 @@ impl InvoiceContract { .persistent() .get(&DataKey::GraceWindow) .unwrap(); - if env.ledger().timestamp() < invoice.created_at + grace_window { + let release_at = invoice + .created_at + .checked_add(grace_window) + .ok_or(ContractError::Overflow)?; + if env.ledger().timestamp() < release_at { return Err(ContractError::GraceWindowNotExpired); } invoice.status = InvoiceStatus::Released; @@ -509,9 +507,7 @@ impl InvoiceContract { /// /// Returns `None` if `set_treasury` has not been called yet. pub fn get_treasury(env: Env) -> Option
{ - env.storage() - .persistent() - .get(&DataKey::TreasuryContract) + env.storage().persistent().get(&DataKey::TreasuryContract) } /// Raises a dispute on an invoice via a cross-contract call to the treasury. @@ -588,9 +584,7 @@ impl InvoiceContract { /// Emits `contract_paused()` on success. pub fn pause(env: Env, caller: Address) -> Result<(), ContractError> { check_admin(&env, &caller)?; - env.storage() - .persistent() - .set(&DataKey::Paused, &true); + env.storage().persistent().set(&DataKey::Paused, &true); events::contract_paused(&env); Ok(()) } @@ -607,9 +601,7 @@ impl InvoiceContract { /// Emits `contract_unpaused()` on success. pub fn unpause(env: Env, caller: Address) -> Result<(), ContractError> { check_admin(&env, &caller)?; - env.storage() - .persistent() - .set(&DataKey::Paused, &false); + env.storage().persistent().set(&DataKey::Paused, &false); events::contract_unpaused(&env); Ok(()) } @@ -734,6 +726,45 @@ mod tests { assert_eq!(result, Err(Ok(ContractError::ContractPaused))); } + #[test] + fn test_create_invoice_near_u64_max_count_returns_overflow() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register(InvoiceContract, ()); + let client = InvoiceContractClient::new(&env, &contract_id); + client.initialize(&admin); + env.storage() + .persistent() + .set(&DataKey::InvoiceCount, &u64::MAX); + + let merchant = Address::generate(&env); + let customer = Address::generate(&env); + let token = Address::generate(&env); + let result = client.try_create_invoice(&merchant, &customer, &1000i128, &token, &5000, &1); + assert_eq!(result, Err(Ok(ContractError::Overflow))); + } + + #[test] + fn test_release_escrow_overflow_returns_error() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register(InvoiceContract, ()); + let client = InvoiceContractClient::new(&env, &contract_id); + client.initialize(&admin); + env.ledger().with_mut(|li| li.timestamp = u64::MAX - 1); + + let merchant = Address::generate(&env); + let customer = Address::generate(&env); + let token = Address::generate(&env); + let invoice_id = client.create_invoice(&merchant, &customer, &1000i128, &token, &5000, &1); + client.mark_paids(&soroban_sdk::vec![&env, invoice_id]); + client.request_refund(&invoice_id, &customer); + let result = client.try_release_escrow(&invoice_id, &merchant); + assert_eq!(result, Err(Ok(ContractError::Overflow))); + } + #[test] fn test_unpause_restores_create_invoice() { let env = Env::default(); @@ -843,7 +874,13 @@ mod tests { invoice_client.initialize(&admin); invoice_client.set_treasury(&admin, &treasury_cid); env.ledger().with_mut(|li| li.timestamp = ts); - (env, invoice_cid, treasury_cid, admin, Address::generate(&env)) + ( + env, + invoice_cid, + treasury_cid, + admin, + Address::generate(&env), + ) } #[test] @@ -860,7 +897,10 @@ mod tests { invoice_client.raise_dispute(&invoice_id, &1u64, &merchant, &1u32); - assert!(treasury_client.was_held(&1u64), "settlement should be on hold"); + assert!( + treasury_client.was_held(&1u64), + "settlement should be on hold" + ); } #[test] @@ -878,7 +918,10 @@ mod tests { // invoice_created + dispute_raised = at least 2 events let all_events = env.events().all(); - assert!(all_events.len() >= 2, "dispute_raised event should be emitted"); + assert!( + all_events.len() >= 2, + "dispute_raised event should be emitted" + ); } #[test] diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs b/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs index a075081..3ef8388 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/integration_settlement_multisig.rs @@ -43,14 +43,22 @@ fn test_multisig_propose_collect_2_of_3_execute() { client.approve_settlement(&signer_a, &settlement_id); let pending_mid = client.get_pending_settlements(&None, &None); - assert_eq!(pending_mid.len(), 1, "settlement should still be pending after 1-of-2 approvals"); + assert_eq!( + pending_mid.len(), + 1, + "settlement should still be pending after 1-of-2 approvals" + ); client.approve_settlement(&signer_b, &settlement_id); client.execute_settlement(&signer_a, &settlement_id, &token); let pending_after = client.get_pending_settlements(&None, &None); - assert_eq!(pending_after.len(), 0, "settlement should no longer be pending after execution"); + assert_eq!( + pending_after.len(), + 0, + "settlement should no longer be pending after execution" + ); } #[test] @@ -79,7 +87,10 @@ fn test_single_signer_insufficient_for_threshold_2() { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { client.execute_settlement(&signer_a, &settlement_id, &token); })); - assert!(result.is_err(), "execution should fail with insufficient approvals"); + assert!( + result.is_err(), + "execution should fail with insufficient approvals" + ); } #[test] @@ -109,7 +120,11 @@ fn test_weighted_signers_reach_threshold() { client.execute_settlement(&signer_a, &settlement_id, &token); let pending = client.get_pending_settlements(&None, &None); - assert_eq!(pending.len(), 0, "weighted signer with weight=2 should meet threshold=2"); + assert_eq!( + pending.len(), + 0, + "weighted signer with weight=2 should meet threshold=2" + ); } #[test] @@ -123,11 +138,7 @@ fn test_multiple_settlements_independent_approvals() { let token = Address::generate(&env); let merchant = Address::generate(&env); - let signers = vec![ - &env, - (signer_a.clone(), 1u64), - (signer_b.clone(), 1u64), - ]; + let signers = vec![&env, (signer_a.clone(), 1u64), (signer_b.clone(), 1u64)]; client.initialize(&signers, &2u64, &admin); let s1 = client.propose_settlement(&signer_a, &token, &1_000_000u64, &merchant); @@ -144,7 +155,10 @@ fn test_multiple_settlements_independent_approvals() { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { client.execute_settlement(&signer_a, &s2, &token); })); - assert!(result.is_err(), "s2 should not be executable without approvals"); + assert!( + result.is_err(), + "s2 should not be executable without approvals" + ); } #[test] @@ -158,11 +172,7 @@ fn test_execute_settlement_verifies_token_transfer_setup() { let token = Address::generate(&env); let merchant = Address::generate(&env); - let signers = vec![ - &env, - (signer_a.clone(), 1u64), - (signer_b.clone(), 1u64), - ]; + let signers = vec![&env, (signer_a.clone(), 1u64), (signer_b.clone(), 1u64)]; client.initialize(&signers, &2u64, &admin); let settlement_id = client.propose_settlement(&signer_a, &token, &10_000_000u64, &merchant); @@ -173,7 +183,11 @@ fn test_execute_settlement_verifies_token_transfer_setup() { client.execute_settlement(&signer_a, &settlement_id, &token); let pending = client.get_pending_settlements(&None, &None); - assert_eq!(pending.len(), 0, "executed settlement should no longer appear in pending list"); + assert_eq!( + pending.len(), + 0, + "executed settlement should no longer appear in pending list" + ); } use proptest::prelude::*; diff --git a/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs b/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs index 93de47b..8586c1c 100644 --- a/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs +++ b/COMEBACKHERE-contracts/contracts/treasury/src/lib.rs @@ -80,7 +80,10 @@ pub enum DataKey { } fn is_paused(e: &Env) -> bool { - e.storage().instance().get(&DataKey::Paused).unwrap_or(false) + e.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) } fn check_not_paused(e: &Env) -> Result<(), TreasuryError> { @@ -310,13 +313,21 @@ impl TreasuryContract { e: Env, offset: Option, limit: Option, - ) -> Vec { + ) -> Result, TreasuryError> { + const MAX_PAGE_SIZE: u32 = 100; let next_id: u64 = e .storage() .instance() .get(&DataKey::NextSettlementId) .unwrap_or(1u64); - let cap: u32 = limit.unwrap_or(100).min(100); + let cap: u32 = if let Some(limit) = limit { + if limit > MAX_PAGE_SIZE { + return Err(TreasuryError::InvalidPagination); + } + limit + } else { + MAX_PAGE_SIZE + }; let skip: u32 = offset.unwrap_or(0); let mut result: Vec = Vec::new(&e); @@ -341,7 +352,7 @@ impl TreasuryContract { } } } - result + Ok(result) } fn check_admin(e: &Env, admin: &Address) -> Result<(), TreasuryError> { @@ -684,7 +695,7 @@ mod tests { let signer = soroban_sdk::Address::generate(&e); c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); let result = c.get_pending_settlements(&None, &None); - assert_eq!(result.len(), 0); + assert_eq!(result, Ok(Vec::new(&e))); } #[test] @@ -695,13 +706,9 @@ mod tests { let token = soroban_sdk::Address::generate(&e); let merchant = soroban_sdk::Address::generate(&e); let signer = soroban_sdk::Address::generate(&e); - c.initialize( - &soroban_sdk::vec![&e, (signer.clone(), 1u64)], - &1, - &admin, - ); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); let sid = c.propose_settlement(&signer, &token, &1000u64, &merchant); - let result = c.get_pending_settlements(&None, &None); + let result = c.get_pending_settlements(&None, &None).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result.get(0).unwrap(), sid); } @@ -714,16 +721,12 @@ mod tests { let token = soroban_sdk::Address::generate(&e); let merchant = soroban_sdk::Address::generate(&e); let signer = soroban_sdk::Address::generate(&e); - c.initialize( - &soroban_sdk::vec![&e, (signer.clone(), 2u64)], - &1, - &admin, - ); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 2u64)], &1, &admin); let s1 = c.propose_settlement(&signer, &token, &1000u64, &merchant); let s2 = c.propose_settlement(&signer, &token, &2000u64, &merchant); c.approve_settlement(&signer, &s1); c.execute_settlement(&signer, &s1, &token); - let result = c.get_pending_settlements(&None, &None); + let result = c.get_pending_settlements(&None, &None).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result.get(0).unwrap(), s2); } @@ -736,15 +739,11 @@ mod tests { let token = soroban_sdk::Address::generate(&e); let merchant = soroban_sdk::Address::generate(&e); let signer = soroban_sdk::Address::generate(&e); - c.initialize( - &soroban_sdk::vec![&e, (signer.clone(), 1u64)], - &1, - &admin, - ); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); for _ in 0..5 { c.propose_settlement(&signer, &token, &100u64, &merchant); } - let page = c.get_pending_settlements(&Some(2u32), &Some(2u32)); + let page = c.get_pending_settlements(&Some(2u32), &Some(2u32)).unwrap(); assert_eq!(page.len(), 2); assert_eq!(page.get(0).unwrap(), 3u64); assert_eq!(page.get(1).unwrap(), 4u64); @@ -758,16 +757,28 @@ mod tests { let token = soroban_sdk::Address::generate(&e); let merchant = soroban_sdk::Address::generate(&e); let signer = soroban_sdk::Address::generate(&e); - c.initialize( - &soroban_sdk::vec![&e, (signer.clone(), 1u64)], - &1, - &admin, - ); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); for _ in 0..5 { c.propose_settlement(&signer, &token, &100u64, &merchant); } let result = c.get_pending_settlements(&None, &Some(200u32)); - assert_eq!(result.len(), 5); + assert_eq!(result, Err(TreasuryError::InvalidPagination)); + } + + #[test] + fn test_offset_beyond_pending_count_returns_empty_page() { + let (e, id) = setup(); + let c = client(&e, &id); + let admin = soroban_sdk::Address::generate(&e); + let token = soroban_sdk::Address::generate(&e); + let merchant = soroban_sdk::Address::generate(&e); + let signer = soroban_sdk::Address::generate(&e); + c.initialize(&soroban_sdk::vec![&e, (signer.clone(), 1u64)], &1, &admin); + c.propose_settlement(&signer, &token, &100u64, &merchant); + let page = c + .get_pending_settlements(&Some(10u32), &Some(5u32)) + .unwrap(); + assert!(page.is_empty()); } // ── paused guard tests ─────────────────────────────────────────────────── @@ -857,7 +868,12 @@ mod tests { // threshold=3, each signer weight=1 → total weight=3 >= 3 (valid init) // After s1 approves: approval_weight=1 < 3 c.initialize( - &soroban_sdk::vec![&e, (s1.clone(), 1u64), (s2.clone(), 1u64), (s3.clone(), 1u64)], + &soroban_sdk::vec![ + &e, + (s1.clone(), 1u64), + (s2.clone(), 1u64), + (s3.clone(), 1u64) + ], &3, &admin, ); @@ -867,7 +883,7 @@ mod tests { let res = c.try_execute_settlement(&s1, &sid, &token); assert_eq!(res, Err(Ok(TreasuryError::InsufficientApprovals))); // settlement must still be Pending - let pending = c.get_pending_settlements(&None, &None); + let pending = c.get_pending_settlements(&None, &None).unwrap(); assert!(pending.contains(&sid)); } @@ -885,7 +901,7 @@ mod tests { c.approve_settlement(&signer, &sid); c.execute_settlement(&signer, &sid, &token); // settlement no longer pending - let pending = c.get_pending_settlements(&None, &None); + let pending = c.get_pending_settlements(&None, &None).unwrap(); assert!(!pending.contains(&sid)); } @@ -902,7 +918,7 @@ mod tests { let sid = c.propose_settlement(&signer, &token, &500u64, &merchant); c.approve_settlement(&signer, &sid); c.execute_settlement(&signer, &sid, &token); - let pending = c.get_pending_settlements(&None, &None); + let pending = c.get_pending_settlements(&None, &None).unwrap(); assert!(!pending.contains(&sid)); } @@ -1010,7 +1026,7 @@ mod tests { // s2 approves: weight=3 == 3, can execute c.approve_settlement(&s2, &sid); c.execute_settlement(&s1, &sid, &token); - let pending = c.get_pending_settlements(&None, &None); + let pending = c.get_pending_settlements(&None, &None).unwrap(); assert!(!pending.contains(&sid)); } diff --git a/contracts/invoice/src/lib.rs b/contracts/invoice/src/lib.rs index b1256c9..5832b03 100644 --- a/contracts/invoice/src/lib.rs +++ b/contracts/invoice/src/lib.rs @@ -174,7 +174,8 @@ impl InvoiceContract { status: InvoiceStatus::Pending, }; env.storage().instance().set(&DataKey::Invoice(id), &invoice); - env.storage().instance().set(&DataKey::NextId, &(id + 1)); + let next_id = id.checked_add(1).ok_or(InvoiceError::Overflow)?; + env.storage().instance().set(&DataKey::NextId, &next_id); Ok(id) }