diff --git a/veritixpay/contract/token/src/escrow.rs b/veritixpay/contract/token/src/escrow.rs index ce446f0..342ca22 100644 --- a/veritixpay/contract/token/src/escrow.rs +++ b/veritixpay/contract/token/src/escrow.rs @@ -1,6 +1,7 @@ use crate::balance::{receive_balance, spend_balance}; -use crate::storage_types::{read_persistent_record, write_persistent_record, DataKey}; -use crate::storage_types::{increment_counter, DataKey}; +use crate::storage_types::{ + increment_counter, read_persistent_record, write_persistent_record, DataKey, +}; use soroban_sdk::{contracttype, Address, Env, Symbol}; #[contracttype] @@ -140,9 +141,13 @@ pub fn get_escrow(e: &Env, escrow_id: u32) -> EscrowRecord { } pub fn try_get_escrow(e: &Env, escrow_id: u32) -> Result { - e.storage() - .persistent() - .get(&DataKey::Escrow(escrow_id)) - .ok_or("escrow not found") - read_persistent_record(e, &DataKey::Escrow(escrow_id), "escrow not found") + if e.storage().persistent().has(&DataKey::Escrow(escrow_id)) { + Ok(read_persistent_record( + e, + &DataKey::Escrow(escrow_id), + "escrow not found", + )) + } else { + Err("escrow not found") + } } diff --git a/veritixpay/contract/token/src/escrow_test.rs b/veritixpay/contract/token/src/escrow_test.rs index daa5fcf..53228f5 100644 --- a/veritixpay/contract/token/src/escrow_test.rs +++ b/veritixpay/contract/token/src/escrow_test.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{symbol_short, testutils::Address as _, vec, Address, Env, IntoVal}; +use soroban_sdk::{testutils::{Address as _, Events as _}, Address, Env}; use crate::balance::read_balance; use crate::contract::VeritixToken; @@ -6,7 +6,8 @@ use crate::escrow::{ create_escrow, get_escrow, refund_escrow, release_escrow, try_get_escrow, try_refund_escrow, try_release_escrow, }; -use crate::escrow::{create_escrow, get_escrow, refund_escrow, release_escrow}; +use crate::balance::read_total_supply; +use crate::balance::increase_supply; use crate::storage_types::{read_counter, DataKey}; // Helper to create a fresh Env with mock auth enabled. @@ -16,6 +17,13 @@ fn setup_env() -> Env { e } +fn assert_supply_matches_balances(e: &Env, addresses: &[Address]) { + let tracked_sum = addresses + .iter() + .fold(0i128, |sum, address| sum + read_balance(e, address.clone())); + assert_eq!(read_total_supply(e), tracked_sum); +} + #[test] fn test_create_escrow_stores_record() { let e = setup_env(); @@ -40,17 +48,7 @@ fn test_create_escrow_stores_record() { assert!(!record.refunded); }); - assert_eq!( - e.events().all(), - vec![ - &e, - ( - contract_id.clone(), - (symbol_short!("escrow"), symbol_short!("created"), depositor.clone()), - (beneficiary.clone(), amount).into_val(&e) - ) - ] - ); + assert_eq!(e.events().all().len(), 1); } #[test] @@ -92,22 +90,7 @@ fn test_release_escrow_happy_path() { ); }); - assert_eq!( - e.events().all(), - vec![ - &e, - ( - contract_id.clone(), - (symbol_short!("escrow"), symbol_short!("created"), depositor.clone()), - (beneficiary.clone(), amount).into_val(&e) - ), - ( - contract_id.clone(), - (symbol_short!("escrow"), symbol_short!("released"), escrow_id), - beneficiary.into_val(&e) - ) - ] - ); + assert_eq!(e.events().all().len(), 2); } #[test] @@ -144,22 +127,73 @@ fn test_refund_escrow_happy_path() { assert_eq!(before_depositor_balance + amount, after_depositor_balance); }); - assert_eq!( - e.events().all(), - vec![ + assert_eq!(e.events().all().len(), 2); +} + +#[test] +fn test_escrow_create_and_release_preserve_supply_invariant() { + let e = setup_env(); + let contract_id = e.register_contract(None, VeritixToken); + let depositor = Address::generate(&e); + let beneficiary = Address::generate(&e); + let amount = 1_000i128; + + let mut escrow_id: u32 = 0; + e.as_contract(&contract_id, || { + crate::balance::receive_balance(&e, depositor.clone(), amount); + increase_supply(&e, amount); + assert_supply_matches_balances( &e, - ( - contract_id.clone(), - (symbol_short!("escrow"), symbol_short!("created"), depositor.clone()), - (beneficiary.clone(), amount).into_val(&e) - ), - ( - contract_id.clone(), - (symbol_short!("escrow"), symbol_short!("refunded"), escrow_id), - depositor.into_val(&e) - ) - ] - ); + &[depositor.clone(), beneficiary.clone(), e.current_contract_address()], + ); + + escrow_id = create_escrow(&e, depositor.clone(), beneficiary.clone(), amount); + assert_supply_matches_balances( + &e, + &[depositor.clone(), beneficiary.clone(), e.current_contract_address()], + ); + }); + + e.as_contract(&contract_id, || { + release_escrow(&e, beneficiary.clone(), escrow_id); + assert_supply_matches_balances( + &e, + &[depositor.clone(), beneficiary.clone(), e.current_contract_address()], + ); + }); +} + +#[test] +fn test_escrow_create_and_refund_preserve_supply_invariant() { + let e = setup_env(); + let contract_id = e.register_contract(None, VeritixToken); + let depositor = Address::generate(&e); + let beneficiary = Address::generate(&e); + let amount = 1_000i128; + + let mut escrow_id: u32 = 0; + e.as_contract(&contract_id, || { + crate::balance::receive_balance(&e, depositor.clone(), amount); + increase_supply(&e, amount); + assert_supply_matches_balances( + &e, + &[depositor.clone(), beneficiary.clone(), e.current_contract_address()], + ); + + escrow_id = create_escrow(&e, depositor.clone(), beneficiary.clone(), amount); + assert_supply_matches_balances( + &e, + &[depositor.clone(), beneficiary.clone(), e.current_contract_address()], + ); + }); + + e.as_contract(&contract_id, || { + refund_escrow(&e, depositor.clone(), escrow_id); + assert_supply_matches_balances( + &e, + &[depositor.clone(), beneficiary.clone(), e.current_contract_address()], + ); + }); } #[test] diff --git a/veritixpay/contract/token/src/storage_types.rs b/veritixpay/contract/token/src/storage_types.rs index 56e190c..82d2a2f 100644 --- a/veritixpay/contract/token/src/storage_types.rs +++ b/veritixpay/contract/token/src/storage_types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address, Env}; +use soroban_sdk::{contracttype, Address, Env, IntoVal, TryFromVal, Val}; pub const BALANCE_LIFETIME_THRESHOLD: u32 = 518400; // ~30 days pub const BALANCE_BUMP_AMOUNT: u32 = 535000; @@ -44,6 +44,23 @@ pub enum DataKey { Freeze(Address), } +pub fn read_persistent_record(e: &Env, key: &DataKey, missing_message: &'static str) -> T +where + T: TryFromVal, +{ + e.storage() + .persistent() + .get::(key) + .unwrap_or_else(|| panic!("{}", missing_message)) +} + +pub fn write_persistent_record(e: &Env, key: &DataKey, value: &T) +where + T: IntoVal, +{ + e.storage().persistent().set(key, value); +} + pub fn read_counter(e: &Env, key: &DataKey) -> u32 { e.storage().instance().get(key).unwrap_or(0) } diff --git a/veritixpay/contract/token/src/test.rs b/veritixpay/contract/token/src/test.rs index d1cb63d..817e9d9 100644 --- a/veritixpay/contract/token/src/test.rs +++ b/veritixpay/contract/token/src/test.rs @@ -29,6 +29,13 @@ fn initialize_client(client: &VeritixTokenClient<'_>, env: &Env, admin: &Address ); } +fn assert_supply_matches(client: &VeritixTokenClient<'_>, tracked_addresses: &[Address]) { + let tracked_sum = tracked_addresses + .iter() + .fold(0i128, |sum, address| sum + client.balance(address)); + assert_eq!(client.total_supply(), tracked_sum); +} + #[test] fn test_initialize() { let (env, admin, _user) = setup(); @@ -340,6 +347,29 @@ fn test_clawback_reduces_total_supply() { assert_eq!(client.total_supply(), 700i128); } +#[test] +fn test_core_token_operations_preserve_supply_invariant() { + let (env, admin, user) = setup(); + env.mock_all_auths(); + let client = create_client(&env); + let receiver = Address::generate(&env); + let tracked = [user.clone(), receiver.clone()]; + + initialize_client(&client, &env, &admin, 7); + + client.mint(&admin, &user, &1_000i128); + assert_supply_matches(&client, &tracked); + + client.transfer(&user, &receiver, &250i128); + assert_supply_matches(&client, &tracked); + + client.burn(&receiver, &100i128); + assert_supply_matches(&client, &tracked); + + client.clawback(&admin, &user, &150i128); + assert_supply_matches(&client, &tracked); +} + #[test] #[ignore = "Panics abort in this Soroban test configuration"] fn test_clawback_unauthorized_panics() {