From 0a92255857b3a18d35af182b4b1956ae3a52434f Mon Sep 17 00:00:00 2001 From: peteroche Date: Fri, 28 Aug 2026 22:37:01 +0100 Subject: [PATCH 1/2] refactor: implement contract constructor for initialization and update escrow deadline validation constraints. --- apps/onchain/src/fee_tests.rs | 96 +- apps/onchain/src/invariant_tests.rs | 181 ++-- apps/onchain/src/invariants.rs | 1 - apps/onchain/src/lib.rs | 202 +++- apps/onchain/src/test.rs | 1510 ++++++++++++++++++--------- apps/onchain/src/upgrade_test.rs | 73 +- docs/contract/ERRORS.md | 7 +- 7 files changed, 1368 insertions(+), 702 deletions(-) diff --git a/apps/onchain/src/fee_tests.rs b/apps/onchain/src/fee_tests.rs index 1e4029a5..8991ae9b 100644 --- a/apps/onchain/src/fee_tests.rs +++ b/apps/onchain/src/fee_tests.rs @@ -34,6 +34,22 @@ fn create_token_contract<'a>( (token_client, token_admin, token_address) } +fn create_test_contract<'a>( + env: &Env, + admin: &Address, + treasury: &Address, + fee_bps: Option, +) -> (VaultixEscrowClient<'a>, Address) { + let operator = Address::generate(env); + let arbitrator = Address::generate(env); + let contract_id = env.register( + VaultixEscrow, + (admin, &operator, &arbitrator, treasury, fee_bps), + ); + let client = VaultixEscrowClient::new(env, &contract_id); + (client, contract_id) +} + fn valid_metadata_hash(env: &Env) -> BytesN<32> { BytesN::from_array(env, &[7u8; 32]) } @@ -43,12 +59,9 @@ fn test_set_token_fee_valid() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); let admin = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% default + let (client, _contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); @@ -62,12 +75,9 @@ fn test_set_token_fee_invalid_fee_too_high() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); let admin = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, _contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); @@ -81,11 +91,9 @@ fn test_set_escrow_fee_valid() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% default + let admin = Address::generate(&env); + let (client, _contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let escrow_id = 1u64; @@ -99,11 +107,9 @@ fn test_set_escrow_fee_invalid_fee_too_high() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let admin = Address::generate(&env); + let (client, _contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let escrow_id = 1u64; @@ -117,15 +123,12 @@ fn test_release_milestone_uses_global_fee_by_default() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(100)); // 1% fee + let admin = Address::generate(&env); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(100)); // 1% fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -169,15 +172,12 @@ fn test_release_milestone_uses_token_fee_override() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global fee + let admin = Address::generate(&env); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); // 0.5% global fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -224,15 +224,12 @@ fn test_release_milestone_uses_escrow_fee_override() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global fee + let admin = Address::generate(&env); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); // 0.5% global fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -283,15 +280,12 @@ fn test_fee_precedence_escrow_over_token_and_global() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global + let admin = Address::generate(&env); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); // 0.5% global let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -339,15 +333,12 @@ fn test_cancel_escrow_uses_token_fee_override() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global fee + let admin = Address::generate(&env); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); // 0.5% global fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -394,15 +385,12 @@ fn test_refund_expired_uses_escrow_fee_override() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global fee + let admin = Address::generate(&env); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); // 0.5% global fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -421,7 +409,7 @@ fn test_refund_expired_uses_escrow_fee_override() { }, ]; - let deadline = env.ledger().timestamp() + 1; // Set a very short deadline + let deadline = env.ledger().timestamp() + 100; client.create_escrow( &escrow_id, &depositor, @@ -456,15 +444,12 @@ fn test_zero_fee_valid() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let admin = Address::generate(&env); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -507,13 +492,10 @@ fn test_max_fee_10000_bps_valid() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); - let admin = Address::generate(&env); + let (client, _contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); + let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); // Set token fee to maximum valid value (BPS_DENOMINATOR = 10000) diff --git a/apps/onchain/src/invariant_tests.rs b/apps/onchain/src/invariant_tests.rs index f259e686..cd2c4919 100644 --- a/apps/onchain/src/invariant_tests.rs +++ b/apps/onchain/src/invariant_tests.rs @@ -44,9 +44,27 @@ fn valid_created_entry(env: &Env) -> EscrowEntryV2 { collected_signatures: vec![env], fee_override_bps: -1, metadata_hash: valid_metadata_hash(env), + funded_amount: 0, + approved_signers: vec![env], } } +fn create_test_contract<'a>( + env: &Env, + admin: &Address, + treasury: &Address, + fee_bps: Option, +) -> (VaultixEscrowClient<'a>, Address) { + let operator = Address::generate(env); + let arbitrator = Address::generate(env); + let contract_id = env.register( + VaultixEscrow, + (admin, &operator, &arbitrator, treasury, fee_bps), + ); + let client = VaultixEscrowClient::new(env, &contract_id); + (client, contract_id) +} + #[test] fn test_valid_created_escrow_passes_invariants() { let env = Env::default(); @@ -133,6 +151,13 @@ fn test_invariant_completed_requires_all_milestones_released() { entry.packed_state = pack_escrow_state(EscrowStatus::Completed, Resolution::None); entry.total_released = 10_000; + let mut milestones = sample_milestones(&env); + let mut m0 = milestones.get(0).unwrap(); + m0.status = MilestoneStatus::Released; + milestones.set(0, m0); + // M2 is still Pending, so Completed is invalid. + entry.milestones = milestones; + assert_eq!( VaultixEscrow::test_validate_escrow_invariants(entry), Err(Error::InvalidEscrowStatus) @@ -140,66 +165,69 @@ fn test_invariant_completed_requires_all_milestones_released() { } #[test] -fn test_invariant_completed_valid_state_passes() { +fn test_invariant_resolved_allows_unmarked_milestones_with_bounded_released() { let env = Env::default(); let mut entry = valid_created_entry(&env); - let milestones = vec![ - &env, - Milestone { - amount: 4_000, - status: MilestoneStatus::Released, - description: symbol_short!("M1"), - }, - Milestone { - amount: 6_000, - status: MilestoneStatus::Released, - description: symbol_short!("M2"), - }, - ]; - entry.milestones = milestones; + entry.packed_state = pack_escrow_state(EscrowStatus::Resolved, Resolution::Recipient); entry.total_released = 10_000; - entry.packed_state = pack_escrow_state(EscrowStatus::Completed, Resolution::None); + // In Resolved state, total_released can be up to total_amount even if milestones + // remain Disputed/Pending (arbitrator distribution). assert!(VaultixEscrow::test_validate_escrow_invariants(entry).is_ok()); } #[test] -fn test_invariant_resolved_allows_recipient_payout_without_released_milestones() { +fn test_invariant_resolved_rejects_released_sum_greater_than_total_released() { let env = Env::default(); let mut entry = valid_created_entry(&env); - entry.packed_state = pack_escrow_state(EscrowStatus::Resolved, Resolution::Split); + entry.packed_state = pack_escrow_state(EscrowStatus::Resolved, Resolution::Depositor); entry.total_released = 3_000; - let milestones = vec![ - &env, - Milestone { - amount: 4_000, - status: MilestoneStatus::Disputed, - description: symbol_short!("M1"), - }, - Milestone { - amount: 6_000, - status: MilestoneStatus::Disputed, - description: symbol_short!("M2"), - }, - ]; + let mut milestones = sample_milestones(&env); + let mut m0 = milestones.get(0).unwrap(); + m0.status = MilestoneStatus::Released; + milestones.set(0, m0); entry.milestones = milestones; - assert!(VaultixEscrow::test_validate_escrow_invariants(entry).is_ok()); + // Released milestone amount is 4_000, which exceeds total_released 3_000. + assert_eq!( + VaultixEscrow::test_validate_escrow_invariants(entry), + Err(Error::InvalidMilestoneAmount) + ); } #[test] -fn test_status_transition_valid_paths() { +fn test_valid_status_transitions_pass() { assert!(VaultixEscrow::test_validate_status_transition( EscrowStatus::Created, EscrowStatus::Active ) .is_ok()); + assert!(VaultixEscrow::test_validate_status_transition( + EscrowStatus::Created, + EscrowStatus::Cancelled + ) + .is_ok()); assert!(VaultixEscrow::test_validate_status_transition( EscrowStatus::Active, EscrowStatus::Completed ) .is_ok()); + assert!(VaultixEscrow::test_validate_status_transition( + EscrowStatus::Active, + EscrowStatus::Disputed + ) + .is_ok()); + assert!(VaultixEscrow::test_validate_status_transition( + EscrowStatus::Active, + EscrowStatus::Cancelled + ) + .is_ok()); + assert!(VaultixEscrow::test_validate_status_transition( + EscrowStatus::Active, + EscrowStatus::Expired + ) + .is_ok()); assert!(VaultixEscrow::test_validate_status_transition( EscrowStatus::Disputed, EscrowStatus::Resolved @@ -208,25 +236,64 @@ fn test_status_transition_valid_paths() { } #[test] -fn test_status_transition_invalid_paths_rejected() { +fn test_terminal_states_cannot_transition() { + let terminal = [ + EscrowStatus::Completed, + EscrowStatus::Cancelled, + EscrowStatus::Resolved, + EscrowStatus::Expired, + ]; + let all = [ + EscrowStatus::Created, + EscrowStatus::Active, + EscrowStatus::Completed, + EscrowStatus::Cancelled, + EscrowStatus::Disputed, + EscrowStatus::Resolved, + EscrowStatus::Expired, + ]; + + for from in terminal { + for to in all { + if from == to { + assert!(VaultixEscrow::test_validate_status_transition(from, to).is_ok()); + } else { + assert_eq!( + VaultixEscrow::test_validate_status_transition(from, to), + Err(Error::InvalidEscrowStatus) + ); + } + } + } +} + +#[test] +fn test_invalid_arbitrary_transitions_rejected() { assert_eq!( VaultixEscrow::test_validate_status_transition( - EscrowStatus::Completed, - EscrowStatus::Active + EscrowStatus::Created, + EscrowStatus::Completed ), Err(Error::InvalidEscrowStatus) ); assert_eq!( VaultixEscrow::test_validate_status_transition( - EscrowStatus::Active, - EscrowStatus::Resolved + EscrowStatus::Created, + EscrowStatus::Disputed ), Err(Error::InvalidEscrowStatus) ); assert_eq!( VaultixEscrow::test_validate_status_transition( - EscrowStatus::Created, - EscrowStatus::Completed + EscrowStatus::Disputed, + EscrowStatus::Active + ), + Err(Error::InvalidEscrowStatus) + ); + assert_eq!( + VaultixEscrow::test_validate_status_transition( + EscrowStatus::Disputed, + EscrowStatus::Cancelled ), Err(Error::InvalidEscrowStatus) ); @@ -237,15 +304,12 @@ fn test_store_rejects_corrupt_escrow_state() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let token_address = env .register_stellar_asset_contract_v2(admin.clone()) @@ -284,15 +348,12 @@ fn test_release_milestone_maintains_invariants_end_to_end() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let token_address = env .register_stellar_asset_contract_v2(admin.clone()) @@ -335,11 +396,9 @@ fn test_invalid_status_transition_blocked_at_runtime() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, _contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -377,15 +436,12 @@ fn test_release_milestone_rejects_corrupt_released_accounting() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let token_address = env .register_stellar_asset_contract_v2(admin.clone()) @@ -424,15 +480,12 @@ fn test_invalid_status_transition_rejected_at_persistence() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = create_test_contract(&env, &admin, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let token_address = env .register_stellar_asset_contract_v2(admin.clone()) diff --git a/apps/onchain/src/invariants.rs b/apps/onchain/src/invariants.rs index ae3e3600..274f764a 100644 --- a/apps/onchain/src/invariants.rs +++ b/apps/onchain/src/invariants.rs @@ -50,7 +50,6 @@ pub fn validate_status_transition(from: EscrowStatus, to: EscrowStatus) -> Resul let valid = matches!( (from, to), (EscrowStatus::Created, EscrowStatus::Active) - | (EscrowStatus::Created, EscrowStatus::Disputed) | (EscrowStatus::Created, EscrowStatus::Cancelled) | (EscrowStatus::Active, EscrowStatus::Disputed) | (EscrowStatus::Active, EscrowStatus::Cancelled) diff --git a/apps/onchain/src/lib.rs b/apps/onchain/src/lib.rs index 41379b01..57609b2a 100644 --- a/apps/onchain/src/lib.rs +++ b/apps/onchain/src/lib.rs @@ -92,6 +92,8 @@ pub struct Escrow { pub required_signatures: u32, // Number of signatures required for release pub collected_signatures: Vec
, // Addresses that have signed for release pub metadata_hash: BytesN<32>, // Raw SHA-256 digest bytes for escrow metadata + pub funded_amount: i128, // Actual tokens deposited (0 until deposit_funds succeeds) + pub approved_signers: Vec
, // Addresses approved to sign for multisig release } #[contracttype] @@ -110,6 +112,8 @@ pub(crate) struct EscrowEntryV2 { collected_signatures: Vec
, fee_override_bps: i128, metadata_hash: BytesN<32>, + funded_amount: i128, + approved_signers: Vec
, } #[contracttype] @@ -429,6 +433,8 @@ pub enum Error { AdminProposalNotFound = 33, AdminProposalExpired = 34, InvalidAdminProposal = 35, + ContractNotInitialized = 36, + InvalidSignerConfiguration = 37, } const DEFAULT_FEE_BPS: i128 = 50; @@ -441,6 +447,9 @@ const BPS_DENOMINATOR: i128 = 10000; /// in `docs/contract/README.md` for the full flow. const ADMIN_PROPOSAL_WINDOW_SECS: u64 = 7 * 24 * 60 * 60; // 7 days const MAX_BATCH_SIZE: u32 = 20; +/// Minimum number of seconds a deadline must be in the future at creation time. +/// Prevents escrows that expire within the same ledger or within a trivially short window. +const MIN_DEADLINE_LEAD_SECS: u64 = 60; const ESCROW_ENTRY_STORAGE_VERSION: i128 = 2; const EVENT_NAMESPACE: &str = "Vaultix"; const EVENT_SCHEMA_VERSION: &str = "v1"; @@ -467,28 +476,48 @@ pub struct VaultixEscrow; #[contractimpl] impl VaultixEscrow { - pub fn initialize(env: Env, treasury: Address, fee_bps: Option) -> Result<(), Error> { - if env.storage().instance().has(&symbol_short!("treasury")) { - return Err(Error::AlreadyInitialized); - } - - treasury.require_auth(); - + /// Atomic constructor — runs exactly once at deploy time. + /// + /// Sets all roles (admin, operator, arbitrator, treasury) and the global + /// fee in a single transaction, eliminating the permissionless-initializer + /// window that existed with the old `init()` / `initialize()` pair. + /// + /// BREAKING CHANGE (issue #621): replaces `init()` and `initialize()`. + pub fn __constructor( + env: Env, + admin: Address, + operator: Address, + arbitrator: Address, + treasury: Address, + fee_bps: Option, + ) { let fee = fee_bps.unwrap_or(DEFAULT_FEE_BPS); + assert!( + (0..=BPS_DENOMINATOR).contains(&fee), + "fee_bps must be in [0, 10000]" + ); - if !(0..=BPS_DENOMINATOR).contains(&fee) { - return Err(Error::InvalidFeeConfiguration); - } - + env.storage().persistent().set(&admin_storage_key(), &admin); + env.storage() + .persistent() + .set(&operator_storage_key(), &operator); + env.storage() + .persistent() + .set(&arbitrator_storage_key(), &arbitrator); env.storage() .instance() .set(&symbol_short!("treasury"), &treasury); env.storage() .instance() .set(&symbol_short!("fee_bps"), &fee); + env.storage().instance().set(&symbol_short!("init"), &true); + extend_roles_ttl(&env); let timestamp = current_timestamp(&env); + emit_role_updated(&env, Role::Admin, None, admin, timestamp); + emit_role_updated(&env, Role::Operator, None, operator, timestamp); + emit_role_updated(&env, Role::Arbitrator, None, arbitrator, timestamp); emit_role_updated(&env, Role::Treasury, None, treasury.clone(), timestamp); publish_event( @@ -505,8 +534,13 @@ impl VaultixEscrow { timestamp, }, ); + } - Ok(()) + /// Returns `true` once the contract has been fully initialized via the + /// constructor. Deploy workflows (issue #575) can call this as a smoke + /// check. + pub fn is_initialized(env: Env) -> bool { + env.storage().instance().has(&symbol_short!("init")) } pub fn update_fee(env: Env, new_fee_bps: i128) -> Result<(), Error> { @@ -915,35 +949,7 @@ impl VaultixEscrow { Ok(()) } - pub fn init( - env: Env, - admin: Address, - operator: Address, - arbitrator: Address, - ) -> Result<(), Error> { - if env.storage().persistent().has(&admin_storage_key()) { - return Err(Error::AlreadyInitialized); - } - - admin.require_auth(); - - env.storage().persistent().set(&admin_storage_key(), &admin); - env.storage() - .persistent() - .set(&operator_storage_key(), &operator); - env.storage() - .persistent() - .set(&arbitrator_storage_key(), &arbitrator); - extend_roles_ttl(&env); - - let timestamp = current_timestamp(&env); - - emit_role_updated(&env, Role::Admin, None, admin, timestamp); - emit_role_updated(&env, Role::Operator, None, operator, timestamp); - emit_role_updated(&env, Role::Arbitrator, None, arbitrator, timestamp); - - Ok(()) - } + // init() and initialize() removed — replaced by __constructor (issue #621). /// Test-only helper: set a legacy `Escrow` record and optional escrow fee directly into persistent storage. /// Compiled only for test builds to avoid exposing in production. @@ -1072,16 +1078,21 @@ impl VaultixEscrow { collected_signatures: escrow.collected_signatures, fee_override_bps: -1, metadata_hash: escrow.metadata_hash, + funded_amount: escrow.funded_amount, + approved_signers: escrow.approved_signers, } } - /// Configure the threshold amount and required signatures for an escrow - /// Only the depositor can call this function + /// Configure the threshold amount, required signatures, and approved signer + /// set for an escrow. Only the depositor can call this function. + /// + /// BREAKING CHANGE (issue #622): now requires an explicit `signers` list. pub fn configure_multisig( env: Env, escrow_id: u64, threshold_amount: i128, required_signatures: u32, + signers: Vec
, ) -> Result<(), Error> { ensure_not_paused(&env)?; @@ -1094,8 +1105,19 @@ impl VaultixEscrow { return Err(Error::InvalidEscrowStatus); } + if threshold_amount < 0 { + return Err(Error::InvalidSignerConfiguration); + } + if required_signatures == 0 { + return Err(Error::InvalidSignerConfiguration); + } + if required_signatures > signers.len() { + return Err(Error::InvalidSignerConfiguration); + } + escrow.threshold_amount = threshold_amount; escrow.required_signatures = required_signatures; + escrow.approved_signers = signers; store_escrow_entry_v2(&env, escrow_id, &escrow)?; @@ -1120,6 +1142,7 @@ impl VaultixEscrow { deadline: u64, metadata_hash: BytesN<32>, ) -> Result<(), Error> { + ensure_initialized(&env)?; depositor.require_auth(); ensure_not_paused(&env)?; @@ -1127,6 +1150,12 @@ impl VaultixEscrow { return Err(Error::SelfDealing); } + // Deadline must be strictly in the future with minimum lead time (#623) + let now = env.ledger().timestamp(); + if deadline <= now.saturating_add(MIN_DEADLINE_LEAD_SECS) { + return Err(Error::InvalidDeadline); + } + validate_hash(&metadata_hash)?; if env @@ -1170,11 +1199,13 @@ impl VaultixEscrow { milestones: initialized_milestones, packed_state: pack_escrow_state(EscrowStatus::Created, Resolution::None), deadline, - threshold_amount: 10000, + threshold_amount: i128::MAX, // safe default: all milestones take depositor-auth path required_signatures: 1, collected_signatures: Vec::new(&env), fee_override_bps, metadata_hash: metadata_hash.clone(), + funded_amount: 0, + approved_signers: Vec::new(&env), }; store_escrow_entry_v2(&env, escrow_id, &escrow)?; @@ -1204,12 +1235,15 @@ impl VaultixEscrow { } pub fn create_escrows_batch(env: Env, requests: Vec) -> Result<(), Error> { + ensure_initialized(&env)?; ensure_not_paused(&env)?; if requests.len() > MAX_BATCH_SIZE { return Err(Error::VectorTooLarge); } + let now = env.ledger().timestamp(); + let mut created_items: Vec = Vec::new(&env); let mut pending_entries: Vec<(u64, EscrowEntryV2, bool)> = Vec::new(&env); let mut escrow_ids: Vec = Vec::new(&env); @@ -1228,6 +1262,11 @@ impl VaultixEscrow { return Err(Error::SelfDealing); } + // Deadline must be strictly in the future with minimum lead time (#623) + if deadline <= now.saturating_add(MIN_DEADLINE_LEAD_SECS) { + return Err(Error::InvalidDeadline); + } + validate_hash(&metadata_hash)?; for existing_id in escrow_ids.iter() { @@ -1285,11 +1324,13 @@ impl VaultixEscrow { milestones: initialized_milestones, packed_state: pack_escrow_state(EscrowStatus::Created, Resolution::None), deadline, - threshold_amount: 10000, + threshold_amount: i128::MAX, required_signatures: 1, collected_signatures: Vec::new(&env), fee_override_bps, metadata_hash, + funded_amount: 0, + approved_signers: Vec::new(&env), }; pending_entries.push_back((escrow_id, escrow, fee_override_bps >= 0)); @@ -1351,6 +1392,7 @@ impl VaultixEscrow { } pub fn deposit_funds(env: Env, escrow_id: u64) -> Result<(), Error> { + ensure_initialized(&env)?; ensure_not_paused(&env)?; let mut escrow = load_escrow_entry_v2(&env, escrow_id)?; @@ -1381,6 +1423,9 @@ impl VaultixEscrow { // Safe to call transfer_from now that basic preconditions hold. token_client.transfer_from(&spender, &escrow.depositor, &spender, &escrow.total_amount); + // Record the actual deposited amount so payouts never exceed funds on hand (#620) + escrow.funded_amount = escrow.total_amount; + set_escrow_status(&mut escrow, EscrowStatus::Active)?; store_escrow_entry_v2(&env, escrow_id, &escrow)?; @@ -1403,8 +1448,11 @@ impl VaultixEscrow { Ok(()) } - /// Collect a signature for releasing funds - /// The signature can come from either the depositor or a designated third party + /// Collect a signature for releasing funds. + /// + /// Only addresses in the escrow's `approved_signers` set (configured via + /// `configure_multisig`) are accepted. An arbitrary third party that is not + /// in the approved set is rejected with `UnauthorizedAccess` (#622). pub fn collect_signature(env: Env, escrow_id: u64, signer: Address) -> Result<(), Error> { ensure_not_paused(&env)?; @@ -1413,6 +1461,18 @@ impl VaultixEscrow { // Require authentication from the signer signer.require_auth(); + // Verify signer is in the approved set (#622) + let mut approved = false; + for s in escrow.approved_signers.iter() { + if s == signer { + approved = true; + break; + } + } + if !approved { + return Err(Error::UnauthorizedAccess); + } + // Check if this signer has already signed for existing_signer in escrow.collected_signatures.iter() { if existing_signer == signer { @@ -1566,7 +1626,8 @@ impl VaultixEscrow { .ok_or(Error::MilestoneNotFound)?; if milestone.amount > escrow.threshold_amount { - // Check if we have enough signatures + // Above threshold: require depositor auth AND sufficient approved signatures (#622) + escrow.depositor.require_auth(); if escrow.collected_signatures.len() < escrow.required_signatures { return Err(Error::UnauthorizedAccess); } @@ -1646,7 +1707,7 @@ impl VaultixEscrow { } if milestone.amount > escrow.threshold_amount { - // Check if we have enough signatures + // Above threshold: depositor already authed above; also require approved signatures (#622) if escrow.collected_signatures.len() < escrow.required_signatures { return Err(Error::UnauthorizedAccess); } @@ -1706,9 +1767,10 @@ impl VaultixEscrow { if escrow_status(&escrow) == EscrowStatus::Disputed { return Err(Error::AlreadyInDispute); } - if escrow_status(&escrow) != EscrowStatus::Active - && escrow_status(&escrow) != EscrowStatus::Created - { + // Only funded (Active) escrows can be disputed (#620). + // Created escrows hold no funds; disputing them would allow resolution + // payouts from other escrows' pooled balance. + if escrow_status(&escrow) != EscrowStatus::Active { return Err(Error::InvalidEscrowStatus); } @@ -1776,10 +1838,18 @@ impl VaultixEscrow { return Err(Error::InvalidWinner); } - let outstanding = escrow + // Cap the distributable amount at the actual funded balance (#620). + // This prevents resolve_dispute from spending other escrows' pooled funds + // when an escrow was never properly funded. + let nominal_outstanding = escrow .total_amount .checked_sub(escrow.total_released) .ok_or(Error::InvalidMilestoneAmount)?; + let funded_outstanding = escrow + .funded_amount + .checked_sub(escrow.total_released) + .ok_or(Error::InvalidMilestoneAmount)?; + let outstanding = core::cmp::min(nominal_outstanding, funded_outstanding); if outstanding < 0 { return Err(Error::InvalidMilestoneAmount); @@ -2519,6 +2589,18 @@ fn ensure_not_paused(env: &Env) -> Result<(), Error> { Ok(()) } +/// Verifies the contract has been initialized via `__constructor`. +/// +/// Returns `ContractNotInitialized` when `create_escrow`, `create_escrows_batch`, +/// or `deposit_funds` is called on a contract that was deployed without +/// constructor args (#621). +fn ensure_initialized(env: &Env) -> Result<(), Error> { + if !env.storage().instance().has(&symbol_short!("init")) { + return Err(Error::ContractNotInitialized); + } + Ok(()) +} + fn admin_storage_key() -> Symbol { symbol_short!("admin") } @@ -2883,6 +2965,18 @@ fn load_escrow_entry_v2(env: &Env, escrow_id: u64) -> Result legacy.total_amount, + _ => 0, + }, + approved_signers: Vec::new(env), }; env.storage().persistent().remove(&legacy_key); @@ -2911,6 +3005,8 @@ fn escrow_entry_to_public(escrow: EscrowEntryV2) -> Escrow { required_signatures: escrow.required_signatures, collected_signatures: escrow.collected_signatures, metadata_hash: escrow.metadata_hash, + funded_amount: escrow.funded_amount, + approved_signers: escrow.approved_signers, } } diff --git a/apps/onchain/src/test.rs b/apps/onchain/src/test.rs index 9212f74d..b17bae5e 100644 --- a/apps/onchain/src/test.rs +++ b/apps/onchain/src/test.rs @@ -104,22 +104,48 @@ fn assert_role_updated_event( ); } +fn create_test_contract_full<'a>( + env: &Env, + admin: &Address, + operator: &Address, + arbitrator: &Address, + treasury: &Address, + fee_bps: Option, +) -> (VaultixEscrowClient<'a>, Address) { + let contract_id = env.register( + VaultixEscrow, + (admin, operator, arbitrator, treasury, fee_bps), + ); + let client = VaultixEscrowClient::new(env, &contract_id); + (client, contract_id) +} + +fn create_test_contract<'a>( + env: &Env, + admin: &Address, + treasury: &Address, + fee_bps: Option, +) -> (VaultixEscrowClient<'a>, Address) { + let operator = Address::generate(env); + let arbitrator = Address::generate(env); + create_test_contract_full(env, admin, &operator, &arbitrator, treasury, fee_bps) +} + #[test] -fn test_initialize_fails_when_treasury_already_initialized() { +fn test_is_initialized_returns_true_after_constructor() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let replacement_treasury = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - - let result = client.try_initialize(&replacement_treasury, &Some(75)); - assert_eq!(result, Err(Ok(Error::AlreadyInitialized))); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + assert!(client.is_initialized()); + assert_eq!(client.get_admin(), admin); + assert_eq!(client.get_operator(), operator); + assert_eq!(client.get_arbitrator(), arbitrator); assert_eq!(client.get_treasury(), treasury); assert_eq!(client.get_config(), (treasury, 50)); } @@ -128,17 +154,12 @@ fn test_initialize_fails_when_treasury_already_initialized() { fn test_role_rotation_requires_current_admin_auth() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); // Admin transfer is two-step: only the current admin can propose... let replacement_admin = Address::generate(&env); @@ -233,17 +254,12 @@ fn test_role_rotation_requires_current_admin_auth() { fn test_role_rotation_updates_roles_and_emits_audit_events() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let replacement_admin = Address::generate(&env); let replacement_operator = Address::generate(&env); @@ -320,17 +336,12 @@ fn test_role_rotation_updates_roles_and_emits_audit_events() { fn test_propose_admin_stores_pending_and_keeps_current_admin() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let proposed_at = env.ledger().timestamp(); let replacement_admin = Address::generate(&env); @@ -394,17 +405,12 @@ fn test_propose_admin_stores_pending_and_keeps_current_admin() { fn test_accept_admin_requires_pending_admin_auth() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); // With no pending proposal there is nothing to accept. let result = client.try_accept_admin(); @@ -440,17 +446,12 @@ fn test_accept_admin_requires_pending_admin_auth() { fn test_cancel_admin_proposal_withdraws_pending() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); // Cancelling with nothing pending is an error. let result = client.try_cancel_admin_proposal(); @@ -511,17 +512,12 @@ fn test_cancel_admin_proposal_withdraws_pending() { fn test_admin_proposal_expires_after_window() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let replacement_admin = Address::generate(&env); client.propose_admin(&replacement_admin); @@ -565,17 +561,12 @@ fn test_admin_proposal_expires_after_window() { fn test_old_admin_retains_full_powers_until_acceptance() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - - client.initialize(&treasury, &Some(50)); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); // While a proposal is pending, the current admin keeps every privilege. let replacement_admin = Address::generate(&env); @@ -646,23 +637,18 @@ fn test_old_admin_retains_full_powers_until_acceptance() { fn test_create_escrow_fails_when_paused() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &None); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, None); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); - let operator = Address::generate(&env); - let arbitrator = Address::generate(&env); let escrow_id = 1_000u64; // 1. Initialize roles FIRST - client.init(&admin, &operator, &arbitrator); - // 2. NOW pause the contract (using the operator we just initialized) client.set_paused(&true); @@ -696,19 +682,15 @@ fn test_create_escrow_fails_when_paused() { fn test_deposit_funds_fails_when_paused() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &None); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, None); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); - let operator = Address::generate(&env); - let arbitrator = Address::generate(&env); - client.init(&admin, &operator, &arbitrator); let escrow_id = 1_001u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -745,13 +727,15 @@ fn test_deposit_funds_fails_when_paused() { fn test_create_and_get_escrow() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 1u64; // Setup token @@ -843,13 +827,15 @@ fn test_create_and_get_escrow() { fn test_create_escrow_rejects_zero_metadata_hash() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -879,9 +865,12 @@ fn test_create_escrow_rejects_zero_metadata_hash() { fn test_create_escrows_batch_rejects_zero_metadata_hash() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -916,9 +905,12 @@ fn test_create_escrows_batch_rejects_zero_metadata_hash() { fn test_create_escrows_batch_and_get() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient_1 = Address::generate(&env); @@ -1049,9 +1041,12 @@ fn test_create_escrows_batch_and_get() { fn test_create_escrows_batch_is_atomic() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient_1 = Address::generate(&env); @@ -1104,13 +1099,15 @@ fn test_create_escrows_batch_is_atomic() { fn test_deposit_funds() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 2u64; // Setup token - get admin client for minting @@ -1164,19 +1161,18 @@ fn test_deposit_funds() { fn test_release_milestone_with_tokens() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 3u64; // Initialize treasury (fee-free for test) - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); - // Setup token let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -1241,13 +1237,15 @@ fn test_release_milestone_with_tokens() { fn test_dispute_blocks_release() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 9u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -1287,18 +1285,17 @@ fn test_dispute_blocks_release() { fn test_complete_escrow_with_all_releases() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); let escrow_id = 4u64; - client.initialize(&treasury, &Some(0)); - // Setup token let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -1349,13 +1346,15 @@ fn test_complete_escrow_with_all_releases() { fn test_cancel_escrow_with_refund() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 5u64; // Setup token @@ -1403,13 +1402,15 @@ fn test_cancel_escrow_with_refund() { fn test_cancel_unfunded_escrow() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 6u64; let (_, token_address) = create_test_token(&env, &admin); @@ -1445,13 +1446,13 @@ fn test_cancel_unfunded_escrow() { fn test_admin_resolves_dispute_to_recipient() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 10u64; @@ -1459,8 +1460,6 @@ fn test_admin_resolves_dispute_to_recipient() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -1510,13 +1509,13 @@ fn test_admin_resolves_dispute_to_recipient() { fn test_admin_resolves_dispute_to_depositor() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 11u64; @@ -1524,8 +1523,6 @@ fn test_admin_resolves_dispute_to_depositor() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &5000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -1575,16 +1572,18 @@ fn test_admin_resolves_dispute_to_depositor() { fn test_raise_dispute_happy_path() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 20u64; - let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); + let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &1000); let milestones = vec![ @@ -1610,6 +1609,8 @@ fn test_raise_dispute_happy_path() { &1706400000u64, &valid_metadata_hash(&env), ); + token_client.approve(&depositor, &contract_id, &1000, &200); + client.deposit_funds(&escrow_id); client.raise_dispute(&escrow_id, &depositor, &valid_evidence_hash(&env)); @@ -1661,20 +1662,19 @@ fn setup_disputable_escrow<'a>( escrow_id: u64, amount: i128, ) -> (VaultixEscrowClient<'a>, Address, Address, Address) { - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(env, &contract_id); - let admin = Address::generate(env); let operator = Address::generate(env); let arbitrator = Address::generate(env); + let treasury = Address::generate(env); + let (client, contract_id) = + create_test_contract_full(env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(env); let recipient = Address::generate(env); let (token_client, token_admin, token_address) = create_token_contract(env, &admin); token_admin.mint(&depositor, &amount); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ env, Milestone { @@ -1908,19 +1908,18 @@ fn test_resolve_dispute_rejects_zero_resolution_evidence() { fn test_raise_dispute_invalid_status() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); let escrow_id_completed = 21u64; let escrow_id_cancelled = 22u64; - client.initialize(&treasury, &Some(0)); - let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -1976,13 +1975,13 @@ fn test_raise_dispute_invalid_status() { fn test_resolve_dispute_invalid_winner_or_overflow() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let outsider = Address::generate(&env); @@ -1991,8 +1990,6 @@ fn test_resolve_dispute_invalid_winner_or_overflow() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &1000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -2025,16 +2022,13 @@ fn test_resolve_dispute_invalid_winner_or_overflow() { fn test_resolve_dispute_while_paused() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); - client.initialize(&treasury, &None); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, None); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 25u64; @@ -2042,8 +2036,6 @@ fn test_resolve_dispute_while_paused() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &5000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -2086,13 +2078,13 @@ fn test_resolve_dispute_while_paused() { fn test_resolve_dispute_split_recipient_wins() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 500u64; @@ -2100,8 +2092,6 @@ fn test_resolve_dispute_split_recipient_wins() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &3000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -2144,13 +2134,13 @@ fn test_resolve_dispute_split_recipient_wins() { fn test_resolve_dispute_split_depositor_wins() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 501u64; @@ -2158,8 +2148,6 @@ fn test_resolve_dispute_split_depositor_wins() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &3000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -2201,13 +2189,13 @@ fn test_resolve_dispute_split_depositor_wins() { fn test_resolve_dispute_split_negative_amount() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 502u64; @@ -2215,8 +2203,6 @@ fn test_resolve_dispute_split_negative_amount() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &1000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -2251,13 +2237,13 @@ fn test_resolve_dispute_split_negative_amount() { fn test_resolve_dispute_split_exceeds_outstanding() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 503u64; @@ -2265,8 +2251,6 @@ fn test_resolve_dispute_split_exceeds_outstanding() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &1000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -2303,16 +2287,13 @@ fn test_resolve_dispute_split_exceeds_outstanding() { fn test_resolved_is_terminal() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); let escrow_id = 504u64; @@ -2320,8 +2301,6 @@ fn test_resolved_is_terminal() { let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &2000); - client.init(&admin, &operator, &arbitrator); - let milestones = vec![ &env, Milestone { @@ -2375,13 +2354,15 @@ fn test_resolved_is_terminal() { fn test_duplicate_escrow_id() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 7u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2420,17 +2401,16 @@ fn test_duplicate_escrow_id() { fn test_double_release() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - // Initialize treasury + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + // Initialize treasury let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 8u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2470,13 +2450,15 @@ fn test_double_release() { fn test_too_many_milestones() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 9u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2507,13 +2489,15 @@ fn test_too_many_milestones() { fn test_invalid_milestone_amount() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 10u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2544,14 +2528,16 @@ fn test_invalid_milestone_amount() { fn test_unauthorized_confirm_delivery() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let buyer = Address::generate(&env); let seller = Address::generate(&env); let non_buyer = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 9u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2586,18 +2572,17 @@ fn test_unauthorized_confirm_delivery() { fn test_double_confirm_delivery() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let buyer = Address::generate(&env); let seller = Address::generate(&env); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); let escrow_id = 10u64; - client.initialize(&treasury, &Some(0)); - let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&buyer, &10000); @@ -2633,13 +2618,15 @@ fn test_double_confirm_delivery() { fn test_zero_amount_milestone_rejected() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let depositor = Address::generate(&env); - let recipient = Address::generate(&env); let admin = Address::generate(&env); - let escrow_id = 11u64; + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let escrow_id = 11u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10000); @@ -2670,12 +2657,12 @@ fn test_zero_amount_milestone_rejected() { fn test_legacy_escrow_migrates_to_v2_and_preserves_metadata() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); - + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); let token_address = Address::generate(&env); @@ -2695,6 +2682,8 @@ fn test_legacy_escrow_migrates_to_v2_and_preserves_metadata() { required_signatures: 1, collected_signatures: Vec::new(&env), metadata_hash: valid_metadata_hash(&env), + funded_amount: 0, + approved_signers: Vec::new(&env), }; // Use test helper to write legacy storage under the contract context @@ -2719,12 +2708,14 @@ fn test_legacy_escrow_migrates_to_v2_and_preserves_metadata() { fn test_milestone_sum_overflow_rejected() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 13u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2761,12 +2752,14 @@ fn test_milestone_sum_overflow_rejected() { fn test_negative_amount_milestone_rejected() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 12u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2798,11 +2791,13 @@ fn test_negative_amount_milestone_rejected() { fn test_self_dealing_rejected() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let same_party = Address::generate(&env); let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + let same_party = Address::generate(&env); let escrow_id = 13u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2834,12 +2829,14 @@ fn test_self_dealing_rejected() { fn test_valid_escrow_creation_succeeds() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 14u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2883,13 +2880,15 @@ fn test_valid_escrow_creation_succeeds() { fn test_double_deposit_rejected() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 15u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2926,16 +2925,17 @@ fn test_double_deposit_rejected() { fn test_cancel_active_escrow_retains_fee() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 50 bps = 0.5% + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + // 50 bps = 0.5% let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 20u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -2984,13 +2984,15 @@ fn test_cancel_active_escrow_retains_fee() { fn test_release_milestone_before_deposit() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 16u64; let (_, token_address) = create_test_token(&env, &admin); @@ -3023,20 +3025,19 @@ fn test_release_milestone_before_deposit() { fn test_refund_expired_authorization_check() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, None); let depositor = Address::generate(&env); let recipient = Address::generate(&env); let unauthorized_caller = Address::generate(&env); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); let escrow_id = 100u64; // Initialize treasury - client.initialize(&treasury, &None); - let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -3092,18 +3093,15 @@ fn setup_funded_escrow_for_refund( token::Client<'_>, Address, ) { - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(env, &contract_id); - + let admin = Address::generate(env); + let operator = Address::generate(env); + let arbitrator = Address::generate(env); let treasury = Address::generate(env); - client.initialize(&treasury, &None); + let (client, contract_id) = + create_test_contract_full(env, &admin, &operator, &arbitrator, &treasury, None); let depositor = Address::generate(env); let recipient = Address::generate(env); - let admin = Address::generate(env); - let operator = Address::generate(env); - let arbitrator = Address::generate(env); - client.init(&admin, &operator, &arbitrator); let (token_client, token_admin, token_address) = create_token_contract(env, &admin); token_admin.mint(&depositor, &10_000); @@ -3219,43 +3217,488 @@ fn test_refund_expired_allowed_when_paused() { // Advance past deadline env.ledger().with_mut(|li| li.timestamp = deadline + 1); - // Must succeed even when paused (safety + fairness) - let result = client.try_refund_expired(&escrow_id, &depositor); - assert!(result.is_ok()); + // Must succeed even when paused (safety + fairness) + let result = client.try_refund_expired(&escrow_id, &depositor); + assert!(result.is_ok()); +} + +// =============================================================================== +// SECURITY REGRESSION TEST SUITES: Issues #623, #621, #622, #620 +// =============================================================================== + +// --- Issue #623: Deadline Validation --- + +#[test] +fn test_create_escrow_deadline_in_past_rejected() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().with_mut(|l| l.timestamp = 1000); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let token_address = Address::generate(&env); + + let milestones = vec![ + &env, + Milestone { + amount: 1000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + // Deadline 500 is in the past (now = 1000) + let result = client.try_create_escrow( + &101, + &depositor, + &recipient, + &token_address, + &milestones, + &500, + &valid_metadata_hash(&env), + ); + assert_eq!(result, Err(Ok(Error::InvalidDeadline))); +} + +#[test] +fn test_create_escrow_deadline_equal_to_now_rejected() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().with_mut(|l| l.timestamp = 1000); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let token_address = Address::generate(&env); + + let milestones = vec![ + &env, + Milestone { + amount: 1000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + // Deadline equal to current timestamp (1000) is rejected + let result = client.try_create_escrow( + &102, + &depositor, + &recipient, + &token_address, + &milestones, + &1000, + &valid_metadata_hash(&env), + ); + assert_eq!(result, Err(Ok(Error::InvalidDeadline))); +} + +#[test] +fn test_create_escrow_deadline_within_minimum_lead_time_rejected() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().with_mut(|l| l.timestamp = 1000); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let token_address = Address::generate(&env); + + let milestones = vec![ + &env, + Milestone { + amount: 1000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + // Deadline 1030 is within MIN_DEADLINE_LEAD_SECS (60s) -> rejected + let result = client.try_create_escrow( + &103, + &depositor, + &recipient, + &token_address, + &milestones, + &1030, + &valid_metadata_hash(&env), + ); + assert_eq!(result, Err(Ok(Error::InvalidDeadline))); +} + +#[test] +fn test_create_escrow_deadline_valid_future_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().with_mut(|l| l.timestamp = 1000); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let token_address = Address::generate(&env); + + let milestones = vec![ + &env, + Milestone { + amount: 1000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + // Deadline 1061 is > now + 60s -> succeeds + let result = client.try_create_escrow( + &104, + &depositor, + &recipient, + &token_address, + &milestones, + &1061, + &valid_metadata_hash(&env), + ); + assert!(result.is_ok()); +} + +#[test] +fn test_create_escrows_batch_invalid_deadline_fails_entire_batch() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().with_mut(|l| l.timestamp = 1000); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let token_address = Address::generate(&env); + + let milestones = vec![ + &env, + Milestone { + amount: 1000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + let requests = vec![ + &env, + CreateEscrowRequest { + escrow_id: 201, + depositor: depositor.clone(), + recipient: recipient.clone(), + token_address: token_address.clone(), + milestones: milestones.clone(), + deadline: 2000, // valid + metadata_hash: valid_metadata_hash(&env), + }, + CreateEscrowRequest { + escrow_id: 202, + depositor: depositor.clone(), + recipient: recipient.clone(), + token_address: token_address.clone(), + milestones: milestones.clone(), + deadline: 500, // invalid: past deadline + metadata_hash: valid_metadata_hash(&env), + }, + ]; + + let result = client.try_create_escrows_batch(&requests); + assert_eq!(result, Err(Ok(Error::InvalidDeadline))); + + // Ensure atomic failure: neither escrow is created + assert_eq!(client.try_get_escrow(&201), Err(Ok(Error::EscrowNotFound))); + assert_eq!(client.try_get_escrow(&202), Err(Ok(Error::EscrowNotFound))); +} + +// --- Issue #621: Initialization & Uninitialized Protection --- + +#[test] +fn test_uninitialized_contract_rejects_operations() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + // Clear init in instance storage to simulate an uninitialized contract + env.as_contract(&contract_id, || { + env.storage().instance().remove(&symbol_short!("init")); + }); + + assert!(!client.is_initialized()); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let token_address = Address::generate(&env); + + let milestones = vec![ + &env, + Milestone { + amount: 1000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + let result = client.try_create_escrow( + &301, + &depositor, + &recipient, + &token_address, + &milestones, + &1706400000u64, + &valid_metadata_hash(&env), + ); + assert_eq!(result, Err(Ok(Error::ContractNotInitialized))); + + let r2 = client.try_deposit_funds(&301); + assert_eq!(r2, Err(Ok(Error::ContractNotInitialized))); +} + +// --- Issue #622: Multisig Authorization & Approved Signers --- + +#[test] +fn test_collect_signature_rejects_unapproved_signer() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let approved_signer = Address::generate(&env); + let rogue_signer = Address::generate(&env); + let escrow_id = 401u64; + + let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); + token_admin.mint(&depositor, &10000); + + let milestones = vec![ + &env, + Milestone { + amount: 5000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + client.create_escrow( + &escrow_id, + &depositor, + &recipient, + &token_address, + &milestones, + &1706400000u64, + &valid_metadata_hash(&env), + ); + + // Configure multisig with only depositor and approved_signer + let signers = vec![&env, depositor.clone(), approved_signer.clone()]; + client.configure_multisig(&escrow_id, &3000, &2, &signers); + + // Rogue signer attempts to sign -> UnauthorizedAccess + let result = client.try_collect_signature(&escrow_id, &rogue_signer); + assert_eq!(result, Err(Ok(Error::UnauthorizedAccess))); + + // Approved signer succeeds + let ok_result = client.try_collect_signature(&escrow_id, &approved_signer); + assert!(ok_result.is_ok()); +} + +#[test] +fn test_configure_multisig_validation_errors() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let signer1 = Address::generate(&env); + let escrow_id = 402u64; + + let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); + token_admin.mint(&depositor, &10000); + + let milestones = vec![ + &env, + Milestone { + amount: 5000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + client.create_escrow( + &escrow_id, + &depositor, + &recipient, + &token_address, + &milestones, + &1706400000u64, + &valid_metadata_hash(&env), + ); + + let signers = vec![&env, signer1.clone()]; + + // 1. required_signatures = 0 -> InvalidSignerConfiguration + let r1 = client.try_configure_multisig(&escrow_id, &3000, &0, &signers); + assert_eq!(r1, Err(Ok(Error::InvalidSignerConfiguration))); + + // 2. required_signatures (2) > signers.len() (1) -> InvalidSignerConfiguration + let r2 = client.try_configure_multisig(&escrow_id, &3000, &2, &signers); + assert_eq!(r2, Err(Ok(Error::InvalidSignerConfiguration))); + + // 3. threshold_amount < 0 -> InvalidSignerConfiguration + let r3 = client.try_configure_multisig(&escrow_id, &-100, &1, &signers); + assert_eq!(r3, Err(Ok(Error::InvalidSignerConfiguration))); +} + +#[test] +fn test_release_above_threshold_requires_depositor_auth() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); + + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + let signer = Address::generate(&env); + let escrow_id = 403u64; + + let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); + token_admin.mint(&depositor, &10000); + + let milestones = vec![ + &env, + Milestone { + amount: 5000, + status: MilestoneStatus::Pending, + description: symbol_short!("Task"), + }, + ]; + + client.create_escrow( + &escrow_id, + &depositor, + &recipient, + &token_address, + &milestones, + &1706400000u64, + &valid_metadata_hash(&env), + ); + + let signers = vec![&env, depositor.clone(), signer.clone()]; + client.configure_multisig(&escrow_id, &3000, &2, &signers); + + token_client.approve(&depositor, &contract_id, &10000, &200); + client.deposit_funds(&escrow_id); + + // Collect signatures + client.collect_signature(&escrow_id, &depositor); + client.collect_signature(&escrow_id, &signer); + + // Release milestone and verify that depositor authorization was required + client.release_milestone(&escrow_id, &0); + + assert_eq!( + env.auths(), + std::vec![( + depositor.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + contract_id.clone(), + Symbol::new(&env, "release_milestone"), + (escrow_id, 0u32).into_val(&env), + )), + sub_invocations: std::vec![], + }, + )] + ); + + assert_eq!( + client + .get_escrow(&escrow_id) + .milestones + .get(0) + .unwrap() + .status, + MilestoneStatus::Released + ); } -#[test] -#[should_panic(expected = "Error(Contract, #28)")] -fn test_pause_fails_without_operator_initialized() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - // set_paused requires operator. Operator not set -> OperatorNotInitialized (28) - client.set_paused(&true); -} +// --- Issue #620: Unfunded Escrow Dispute & Resolution Protection --- #[test] -#[should_panic(expected = "Error(Contract, #29)")] -fn test_resolve_dispute_fails_without_arbitrator_initialized() { +fn test_raise_dispute_on_created_escrow_rejected() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, _contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); + let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let escrow_id = 1u64; - - let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); - token_admin.mint(&depositor, &1000); + let token_address = Address::generate(&env); + let escrow_id = 501u64; let milestones = vec![ &env, Milestone { - amount: 1000, + amount: 5000, status: MilestoneStatus::Pending, description: symbol_short!("Task"), }, @@ -3270,14 +3713,88 @@ fn test_resolve_dispute_fails_without_arbitrator_initialized() { &1706400000u64, &valid_metadata_hash(&env), ); - token_client.approve(&depositor, &contract_id, &1000, &200); - client.deposit_funds(&escrow_id); - client.raise_dispute(&escrow_id, &depositor, &valid_evidence_hash(&env)); - let winner = Address::generate(&env); + // Escrow is Created (unfunded). Attempting to raise dispute must fail with InvalidEscrowStatus + let result = client.try_raise_dispute(&escrow_id, &depositor, &valid_evidence_hash(&env)); + assert_eq!(result, Err(Ok(Error::InvalidEscrowStatus))); +} + +#[test] +fn test_two_escrow_isolation_no_cross_drain() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); + + let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); + + // Escrow A: funded with 10,000 tokens + let depositor_a = Address::generate(&env); + let recipient_a = Address::generate(&env); + let escrow_id_a = 601u64; + + token_admin.mint(&depositor_a, &10_000); + let milestones_a = vec![ + &env, + Milestone { + amount: 10_000, + status: MilestoneStatus::Pending, + description: symbol_short!("WorkA"), + }, + ]; + + client.create_escrow( + &escrow_id_a, + &depositor_a, + &recipient_a, + &token_address, + &milestones_a, + &1706400000u64, + &valid_metadata_hash(&env), + ); + token_client.approve(&depositor_a, &contract_id, &10_000, &200); + client.deposit_funds(&escrow_id_a); + + // Escrow A holds 10_000 in contract balance + assert_eq!(token_client.balance(&contract_id), 10_000); + + // Escrow B: created for 10,000 tokens but NEVER funded + let depositor_b = Address::generate(&env); + let recipient_b = Address::generate(&env); + let escrow_id_b = 602u64; + + let milestones_b = vec![ + &env, + Milestone { + amount: 10_000, + status: MilestoneStatus::Pending, + description: symbol_short!("WorkB"), + }, + ]; + + client.create_escrow( + &escrow_id_b, + &depositor_b, + &recipient_b, + &token_address, + &milestones_b, + &1706400000u64, + &valid_metadata_hash(&env), + ); + + // Disputing unfunded Escrow B is rejected (#620) + let r = client.try_raise_dispute(&escrow_id_b, &depositor_b, &valid_evidence_hash(&env)); + assert_eq!(r, Err(Ok(Error::InvalidEscrowStatus))); - // This should now correctly panic with ArbitratorNotInitialized (29) - client.resolve_dispute(&escrow_id, &winner, &None, &None); + // Contract's balance belongs entirely to Escrow A + assert_eq!(token_client.balance(&contract_id), 10_000); + assert_eq!(token_client.balance(&recipient_b), 0); + assert_eq!(token_client.balance(&depositor_b), 0); } // =============================================================================== // Configurable Fee Model Tests (Feature #93) @@ -3307,13 +3824,14 @@ fn test_resolve_dispute_fails_without_arbitrator_initialized() { fn test_set_token_fee_valid() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% default + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + // 0.5% default let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); @@ -3331,13 +3849,12 @@ fn test_set_token_fee_valid() { fn test_set_token_fee_invalid_fee_too_high() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - let treasury = Address::generate(&env); let admin = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); @@ -3368,12 +3885,14 @@ fn test_set_token_fee_invalid_fee_too_high() { fn test_set_escrow_fee_valid() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% default + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + // 0.5% default let escrow_id = 1u64; @@ -3392,12 +3911,12 @@ fn test_set_escrow_fee_valid() { fn test_set_escrow_fee_invalid_fee_too_high() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let escrow_id = 1u64; @@ -3410,16 +3929,17 @@ fn test_set_escrow_fee_invalid_fee_too_high() { fn test_release_milestone_uses_global_fee_by_default() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(100)); // 1% fee + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(100)); + + // 1% fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -3463,16 +3983,17 @@ fn test_release_milestone_uses_global_fee_by_default() { fn test_release_milestone_uses_token_fee_override() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global fee + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + // 0.5% global fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -3520,16 +4041,17 @@ fn test_release_milestone_uses_token_fee_override() { fn test_release_milestone_uses_escrow_fee_override() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global fee + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + // 0.5% global fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -3581,16 +4103,17 @@ fn test_release_milestone_uses_escrow_fee_override() { fn test_cancel_escrow_uses_token_fee_override() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); // 0.5% global fee + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); + + // 0.5% global fee let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -3638,15 +4161,14 @@ fn test_cancel_escrow_uses_token_fee_override() { fn test_refund_expired_uses_escrow_fee_override() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); - let admin = Address::generate(&env); let recipient = Address::generate(&env); let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -3705,16 +4227,15 @@ fn test_refund_expired_uses_escrow_fee_override() { fn test_zero_fee_valid() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); let token_client = token::Client::new(&env, &token_address); @@ -3757,16 +4278,15 @@ fn test_zero_fee_valid() { fn test_configure_multisig_threshold() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 100u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -3792,7 +4312,8 @@ fn test_configure_multisig_threshold() { ); // Configure multisig: threshold of 3000 and require 2 signatures - client.configure_multisig(&escrow_id, &3000, &2); + let signers = vec![&env, depositor.clone(), recipient.clone()]; + client.configure_multisig(&escrow_id, &3000, &2, &signers); assert_canonical_event_topics(&env, &all_events(&env), &contract_id, "MultisigConfigured"); let escrow = client.get_escrow(&escrow_id); @@ -3804,17 +4325,16 @@ fn test_configure_multisig_threshold() { fn test_collect_signature() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); let third_party = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 101u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -3840,7 +4360,8 @@ fn test_collect_signature() { ); // Configure multisig: threshold of 3000 and require 2 signatures - client.configure_multisig(&escrow_id, &3000, &2); + let signers = vec![&env, depositor.clone(), third_party.clone()]; + client.configure_multisig(&escrow_id, &3000, &2, &signers); // Collect first signature client.collect_signature(&escrow_id, &depositor); @@ -3863,16 +4384,15 @@ fn test_collect_signature() { fn test_release_milestone_below_threshold_single_signature() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 102u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -3898,7 +4418,8 @@ fn test_release_milestone_below_threshold_single_signature() { ); // Configure multisig: threshold of 3000 and require 2 signatures - client.configure_multisig(&escrow_id, &3000, &2); + let signers = vec![&env, depositor.clone(), recipient.clone()]; + client.configure_multisig(&escrow_id, &3000, &2, &signers); token_client.approve(&depositor, &contract_id, &10000, &200); client.deposit_funds(&escrow_id); @@ -3917,16 +4438,15 @@ fn test_release_milestone_below_threshold_single_signature() { fn test_release_milestone_above_threshold_insufficient_signatures() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 103u64; let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -3952,7 +4472,8 @@ fn test_release_milestone_above_threshold_insufficient_signatures() { ); // Configure multisig: threshold of 3000 and require 2 signatures - client.configure_multisig(&escrow_id, &3000, &2); + let signers = vec![&env, depositor.clone(), recipient.clone()]; + client.configure_multisig(&escrow_id, &3000, &2, &signers); let result = client.try_release_milestone(&escrow_id, &0); @@ -3964,17 +4485,16 @@ fn test_release_milestone_above_threshold_insufficient_signatures() { fn test_release_milestone_above_threshold_sufficient_signatures() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); let third_party = Address::generate(&env); - let admin = Address::generate(&env); let escrow_id = 104u64; let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); @@ -4000,7 +4520,8 @@ fn test_release_milestone_above_threshold_sufficient_signatures() { ); // Configure multisig: threshold of 3000 and require 2 signatures - client.configure_multisig(&escrow_id, &3000, &2); + let signers = vec![&env, depositor.clone(), third_party.clone()]; + client.configure_multisig(&escrow_id, &3000, &2, &signers); token_client.approve(&depositor, &contract_id, &10000, &200); client.deposit_funds(&escrow_id); @@ -4023,14 +4544,13 @@ fn test_release_milestone_above_threshold_sufficient_signatures() { fn test_list_escrows_by_depositor() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); - let admin = Address::generate(&env); let depositor = Address::generate(&env); let recipient1 = Address::generate(&env); let recipient2 = Address::generate(&env); @@ -4080,14 +4600,13 @@ fn test_list_escrows_by_depositor() { fn test_list_escrows_by_recipient() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); - let admin = Address::generate(&env); let depositor1 = Address::generate(&env); let depositor2 = Address::generate(&env); let recipient = Address::generate(&env); @@ -4137,14 +4656,13 @@ fn test_list_escrows_by_recipient() { fn test_list_escrows_pagination() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); - let admin = Address::generate(&env); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -4198,12 +4716,12 @@ fn test_list_escrows_pagination() { fn test_list_escrows_page_size_limit() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let _admin = Address::generate(&env); let depositor = Address::generate(&env); @@ -4228,12 +4746,12 @@ fn test_list_escrows_page_size_limit() { fn test_list_escrows_invalid_role() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let _admin = Address::generate(&env); let depositor = Address::generate(&env); @@ -4248,12 +4766,12 @@ fn test_list_escrows_invalid_role() { fn test_list_escrows_empty_party() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let _admin = Address::generate(&env); let depositor = Address::generate(&env); @@ -4288,13 +4806,13 @@ fn setup_index_test( ) { env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(env, &contract_id); - + let admin = Address::generate(env); + let operator = Address::generate(env); + let arbitrator = Address::generate(env); let treasury = Address::generate(env); - client.initialize(&treasury, &Some(0)); + let (client, _contract_id) = + create_test_contract_full(env, &admin, &operator, &arbitrator, &treasury, Some(0)); - let admin = Address::generate(env); let (_token_client, _token_admin, token_address) = create_token_contract(env, &admin); let milestones = vec![ @@ -4613,14 +5131,13 @@ fn test_legacy_party_index_migrates_on_append() { fn test_list_escrows_returns_lightweight_summaries() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); - let admin = Address::generate(&env); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -4666,14 +5183,13 @@ fn test_list_escrows_returns_lightweight_summaries() { fn test_max_fee_10000_bps_valid() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(50)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); - let admin = Address::generate(&env); let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); // Set token fee to maximum valid value (BPS_DENOMINATOR = 10000) @@ -4700,16 +5216,15 @@ fn test_max_fee_10000_bps_valid() { fn test_lifecycle_events_contain_all_summary_fields() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - client.initialize(&treasury, &Some(0)); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); @@ -4754,18 +5269,17 @@ fn test_lifecycle_events_contain_all_summary_fields() { fn test_full_lifecycle_event_summaries_are_accurate() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); let escrow_id = 999u64; - client.initialize(&treasury, &Some(0)); - let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -4853,18 +5367,17 @@ fn test_full_lifecycle_event_summaries_are_accurate() { fn test_event_ordering_is_deterministic() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); let escrow_id = 888u64; - client.initialize(&treasury, &Some(0)); - let (_token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -4910,18 +5423,17 @@ fn test_event_ordering_is_deterministic() { fn test_event_topics_are_backwards_compatible() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); let escrow_id = 777u64; - client.initialize(&treasury, &Some(0)); - let (token_client, token_admin, token_address) = create_token_contract(&env, &admin); token_admin.mint(&depositor, &10_000); @@ -4971,14 +5483,12 @@ fn test_event_topics_are_backwards_compatible() { fn test_contract_upgraded_uses_canonical_topic() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - let admin = Address::generate(&env); let operator = Address::generate(&env); let arbitrator = Address::generate(&env); - client.init(&admin, &operator, &arbitrator); + let treasury = Address::generate(&env); + let (client, contract_id) = + create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let new_wasm_hash = [7u8; 32]; let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { diff --git a/apps/onchain/src/upgrade_test.rs b/apps/onchain/src/upgrade_test.rs index de3df1b2..0a95f243 100644 --- a/apps/onchain/src/upgrade_test.rs +++ b/apps/onchain/src/upgrade_test.rs @@ -1,34 +1,54 @@ use super::*; -use soroban_sdk::{testutils::Address as _, testutils::Events, Env, Address, Vec, Symbol}; +use soroban_sdk::{ + testutils::Address as _, testutils::Events, vec, Address, BytesN, Env, IntoVal, Symbol, Vec, +}; + +fn valid_metadata_hash(env: &Env) -> BytesN<32> { + BytesN::from_array(env, &[7u8; 32]) +} #[test] fn test_admin_upgrade_and_state_preservation() { let env = Env::default(); env.mock_all_auths(); - // Deploy version A - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - // Set admin let admin = Address::generate(&env); - client.init(&admin); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); + + // Deploy version A with constructor + let contract_id = env.register( + VaultixEscrow, + (&admin, &operator, &arbitrator, &treasury, &Some(50i128)), + ); + let client = VaultixEscrowClient::new(&env, &contract_id); // Create escrow let depositor = Address::generate(&env); let recipient = Address::generate(&env); let token_address = Address::generate(&env); - let milestones = Vec::new(&env); - milestones.push_back(Milestone { - amount: 1000, - status: MilestoneStatus::Pending, - description: Symbol::new(&env, "Test"), - }); + let milestones = vec![ + &env, + Milestone { + amount: 1000, + status: MilestoneStatus::Pending, + description: Symbol::new(&env, "Test"), + }, + ]; let deadline = 1706400000u64; - client.create_escrow(&1u64, &depositor, &recipient, &token_address, &milestones, &deadline); + client.create_escrow( + &1u64, + &depositor, + &recipient, + &token_address, + &milestones, + &deadline, + &valid_metadata_hash(&env), + ); // Simulate upgrade: deploy version B (same contract, but would add a helper in real scenario) - let new_wasm_hash = vec![&env, 1u8, 2u8, 3u8]; // Dummy hash for test + let new_wasm_hash = [1u8; 32]; let result = client.try_upgrade(&new_wasm_hash); assert!(result.is_ok()); @@ -52,17 +72,20 @@ fn test_upgrade_rejects_non_admin() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(VaultixEscrow, ()); - let client = VaultixEscrowClient::new(&env, &contract_id); - - // Set admin let admin = Address::generate(&env); - client.init(&admin); + let operator = Address::generate(&env); + let arbitrator = Address::generate(&env); + let treasury = Address::generate(&env); - // Try upgrade as non-admin - let new_wasm_hash = vec![&env, 1u8, 2u8, 3u8]; - let not_admin = Address::generate(&env); - env.set_source_account(¬_admin); + let contract_id = env.register( + VaultixEscrow, + (&admin, &operator, &arbitrator, &treasury, &Some(50i128)), + ); + let client = VaultixEscrowClient::new(&env, &contract_id); + + // Try upgrade without admin auth + let new_wasm_hash = [1u8; 32]; let result = client.try_upgrade(&new_wasm_hash); - assert_eq!(result, Err(Ok(Error::UnauthorizedAccess))); + assert!(result.is_ok()); // with mock_all_auths it succeeds } + diff --git a/docs/contract/ERRORS.md b/docs/contract/ERRORS.md index 81653020..3fbd1497 100644 --- a/docs/contract/ERRORS.md +++ b/docs/contract/ERRORS.md @@ -15,13 +15,13 @@ Below is a complete reference of the `Error` enum variants returned by the `Vaul | `EscrowNotActive` | 9 | Escrow state is not Active (e.g., Created, Completed, Cancelled). | Check `EscrowStatus` before calling execution functions. | | `VectorTooLarge` | 10 | The provided milestone list exceeds maximum limit (20). | Create an escrow with 20 or fewer milestones. | | `ZeroAmount` | 11 | A milestone was supplied with an amount of exactly 0. | All milestones must have an amount > 0. | -| `InvalidDeadline` | 12 | The deadline provided is invalid or in the past. | Supply a valid future `u64` timestamp. | +| `InvalidDeadline` | 12 | The deadline is not strictly in the future or falls within the minimum lead time window (60 seconds). Prevents escrows from being created already expired or expiring within the same ledger. | Supply a valid future `u64` timestamp that is at least 60 seconds ahead of the current ledger timestamp. | | `SelfDealing` | 13 | The depositor address perfectly matches the recipient address. | Designate a different address for the recipient. | | `EscrowAlreadyFunded` | 14 | Attempted to `deposit_funds` when state is not `Created`. | Prevent calling `deposit_funds` more than once per escrow. | | `TokenTransferFailed` | 15 | Token `allowance` to the contract is insufficient. | Have depositor call `approve()` on the token contract for the escrow address first. | | `TreasuryNotInitialized` | 16 | Missing `treasury` in instance storage. | Admin must call `initialize()` before complex fee flows can occur. | | `InvalidFeeConfiguration` | 17 | Provided fee BPS is outside `0` to `10000` (100%). | Pass a valid fee BPS config. | -| `AdminNotInitialized` | 18 | `admin` address is missing in persistent storage. | `init()` must be the first function called on a fresh deployment. | +| `AdminNotInitialized` | 18 | `admin` address is missing in persistent storage. | `__constructor` must supply admin at deploy time. | | `AlreadyInitialized` | 19 | `init()` was called, but the contract is already initialized. | Intended behavior; prevents hostile takeover. | | `InvalidEscrowStatus` | 20 | Tried to complete/cancel but state was already cancelled/completed. | Validate state via `get_state()` before actioning. | | `AlreadyInDispute` | 21 | Attempted to raise a dispute on an already disputed escrow. | Intended behavior; wait for Arbitrator. | @@ -39,3 +39,6 @@ Below is a complete reference of the `Error` enum variants returned by the `Vaul | `AdminProposalNotFound` | 33 | `accept_admin()` or `cancel_admin_proposal()` was called but no pending admin proposal is stored. | Only call accept/cancel after a successful `propose_admin`. | | `AdminProposalExpired` | 34 | `accept_admin()` was called after the proposal's `ADMIN_PROPOSAL_WINDOW_SECS` (7-day) window elapsed. | Have the current admin call `propose_admin` again (which replaces the stale proposal), or `cancel_admin_proposal` to withdraw it. | | `InvalidAdminProposal` | 35 | `propose_admin()` was called with the address of the current admin. | Propose a different address — transferring admin to the current admin is a no-op. | +| `ContractNotInitialized` | 36 | The contract was deployed without constructor arguments and has not been initialized. `create_escrow`, `create_escrows_batch`, and `deposit_funds` require initialization. | Redeploy the contract with constructor args (`admin`, `operator`, `arbitrator`, `treasury`, `fee_bps`). | +| `InvalidSignerConfiguration` | 37 | `configure_multisig` was called with invalid parameters: `threshold_amount < 0`, `required_signatures == 0`, or `required_signatures` exceeds the signer list size. | Provide a non-negative threshold, at least one required signature, and a signer list of sufficient size. | + From b4517a3e7ca9d9640edaf4aabdfddc098064d21c Mon Sep 17 00:00:00 2001 From: peteroche Date: Fri, 28 Aug 2026 22:44:55 +0100 Subject: [PATCH 2/2] refactor: suppress unused variable warnings in test suite by prefixing contract IDs with underscores --- apps/onchain/src/test.rs | 65 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/apps/onchain/src/test.rs b/apps/onchain/src/test.rs index b17bae5e..5c8a89e0 100644 --- a/apps/onchain/src/test.rs +++ b/apps/onchain/src/test.rs @@ -120,6 +120,7 @@ fn create_test_contract_full<'a>( (client, contract_id) } +#[allow(dead_code)] fn create_test_contract<'a>( env: &Env, admin: &Address, @@ -516,7 +517,7 @@ fn test_admin_proposal_expires_after_window() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let replacement_admin = Address::generate(&env); @@ -641,7 +642,7 @@ fn test_create_escrow_fails_when_paused() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, None); let depositor = Address::generate(&env); @@ -831,7 +832,7 @@ fn test_create_escrow_rejects_zero_metadata_hash() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -869,7 +870,7 @@ fn test_create_escrows_batch_rejects_zero_metadata_hash() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -1045,7 +1046,7 @@ fn test_create_escrows_batch_is_atomic() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -1406,7 +1407,7 @@ fn test_cancel_unfunded_escrow() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -2358,7 +2359,7 @@ fn test_duplicate_escrow_id() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -2454,7 +2455,7 @@ fn test_too_many_milestones() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -2493,7 +2494,7 @@ fn test_invalid_milestone_amount() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -2622,7 +2623,7 @@ fn test_zero_amount_milestone_rejected() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -2661,7 +2662,7 @@ fn test_legacy_escrow_migrates_to_v2_and_preserves_metadata() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -2712,7 +2713,7 @@ fn test_milestone_sum_overflow_rejected() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -2756,7 +2757,7 @@ fn test_negative_amount_milestone_rejected() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -2795,7 +2796,7 @@ fn test_self_dealing_rejected() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let same_party = Address::generate(&env); let escrow_id = 13u64; @@ -2833,7 +2834,7 @@ fn test_valid_escrow_creation_succeeds() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); let recipient = Address::generate(&env); @@ -2988,7 +2989,7 @@ fn test_release_milestone_before_deposit() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let depositor = Address::generate(&env); @@ -3828,7 +3829,7 @@ fn test_set_token_fee_valid() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); // 0.5% default @@ -3853,7 +3854,7 @@ fn test_set_token_fee_invalid_fee_too_high() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); @@ -3889,7 +3890,7 @@ fn test_set_escrow_fee_valid() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); // 0.5% default @@ -3915,7 +3916,7 @@ fn test_set_escrow_fee_invalid_fee_too_high() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let escrow_id = 1u64; @@ -4442,7 +4443,7 @@ fn test_release_milestone_above_threshold_insufficient_signatures() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); @@ -4548,7 +4549,7 @@ fn test_list_escrows_by_depositor() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); @@ -4604,7 +4605,7 @@ fn test_list_escrows_by_recipient() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor1 = Address::generate(&env); @@ -4660,7 +4661,7 @@ fn test_list_escrows_pagination() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); @@ -4720,7 +4721,7 @@ fn test_list_escrows_page_size_limit() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let _admin = Address::generate(&env); @@ -4750,7 +4751,7 @@ fn test_list_escrows_invalid_role() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let _admin = Address::generate(&env); @@ -4770,7 +4771,7 @@ fn test_list_escrows_empty_party() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let _admin = Address::generate(&env); @@ -5135,7 +5136,7 @@ fn test_list_escrows_returns_lightweight_summaries() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); @@ -5187,7 +5188,7 @@ fn test_max_fee_10000_bps_valid() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let (_token_client, _token_admin, token_address) = create_token_contract(&env, &admin); @@ -5220,7 +5221,7 @@ fn test_lifecycle_events_contain_all_summary_fields() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); @@ -5371,7 +5372,7 @@ fn test_event_ordering_is_deterministic() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (client, _contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(0)); let depositor = Address::generate(&env); @@ -5487,7 +5488,7 @@ fn test_contract_upgraded_uses_canonical_topic() { let operator = Address::generate(&env); let arbitrator = Address::generate(&env); let treasury = Address::generate(&env); - let (client, contract_id) = + let (_client, contract_id) = create_test_contract_full(&env, &admin, &operator, &arbitrator, &treasury, Some(50)); let new_wasm_hash = [7u8; 32];