diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 440a3ff..b672fa7 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -2691,6 +2691,10 @@ impl EscrowContract { } } + // Transfer funds from customer to contract + let token_client = token::Client::new(&env, &token); + token_client.transfer(&customer, &env.current_contract_address(), &amount); + let counter: u64 = env .storage() .instance() diff --git a/contracts/refund/ERRORS.md b/contracts/refund/ERRORS.md index 147fb5b..6fb97f7 100644 --- a/contracts/refund/ERRORS.md +++ b/contracts/refund/ERRORS.md @@ -35,6 +35,7 @@ This document lists the numeric error codes defined in the refund contract, thei | 29 | `CircuitBreakerTripped` | The refund circuit breaker is currently open because the configured threshold was exceeded. | | 30 | `InvalidFeeConfig` | The refund fee configuration is malformed or inconsistent. | | 31 | `InsufficientTreasuryFees` | There are not enough accumulated treasury fees to satisfy the requested withdrawal. | +| 32 | `AutoApproveThresholdExceedsCeiling` | The merchant attempted to set an auto-approval threshold above the platform ceiling. | ## Extension errors (`ExtError`) diff --git a/contracts/refund/src/lib.rs b/contracts/refund/src/lib.rs index 027e4c7..fad5dc1 100644 --- a/contracts/refund/src/lib.rs +++ b/contracts/refund/src/lib.rs @@ -80,6 +80,7 @@ pub enum DataKey { // Payment refund caps PaymentRefundCap(u64), PaymentRefundUsage(u64), + AutoApproveBelowCeiling, // Issue #370: Customer-tier-based refund caps CustomerTier(Address), CustomerTierPolicy(Address, u32), @@ -258,6 +259,7 @@ pub enum CoreError { CircuitBreakerTripped = 29, InvalidFeeConfig = 30, InsufficientTreasuryFees = 31, + AutoApproveThresholdExceedsCeiling = 32, } #[contracterror] @@ -1383,6 +1385,7 @@ impl RefundContract { Self::set_inherit_from_parent_inner(&env, &admin, false); Self::set_requires_admin_approval_inner(&env, &admin, true); Self::set_auto_approve_below_inner(&env, &admin, 0); + Self::set_auto_approve_below_ceiling_inner(&env, 0); env.storage() .instance() .set(&DataKey::AppealWindowSeconds, &604800u64); @@ -4547,6 +4550,19 @@ impl RefundContract { env.storage().instance().set(&composite_key, &value); } + fn get_auto_approve_below_ceiling_inner(env: &Env) -> i128 { + env.storage() + .instance() + .get(&DataKey::AutoApproveBelowCeiling) + .unwrap_or(0) + } + + fn set_auto_approve_below_ceiling_inner(env: &Env, value: i128) { + env.storage() + .instance() + .set(&DataKey::AutoApproveBelowCeiling, &value); + } + fn get_inherit_from_parent_inner(env: &Env, merchant: &Address) -> bool { let key = Symbol::new(env, "inherit_from_parent"); let composite_key: (Symbol, Address) = (key, merchant.clone()); @@ -4602,9 +4618,44 @@ impl RefundContract { /// # Arguments /// * `merchant` - The merchant address to configure (must authenticate). /// * `value` - The threshold amount below which refunds are auto-approved. - pub fn set_auto_approve_below(env: Env, merchant: Address, value: i128) { + pub fn set_auto_approve_below(env: Env, merchant: Address, value: i128) -> Result<(), Error> { merchant.require_auth(); + let ceiling = Self::get_auto_approve_below_ceiling_inner(&env); + if value > ceiling { + return Err(Error::Core(CoreError::AutoApproveThresholdExceedsCeiling)); + } Self::set_auto_approve_below_inner(&env, &merchant, value); + Ok(()) + } + + /// Get the platform-wide ceiling for merchant auto-approval thresholds. + /// + /// Refund thresholds above this value are rejected by `set_auto_approve_below()`. + pub fn get_auto_approve_below_ceiling(env: Env) -> i128 { + Self::get_auto_approve_below_ceiling_inner(&env) + } + + /// Set the platform-wide ceiling for merchant auto-approval thresholds. + /// + /// # Arguments + /// * `admin` - The contract admin configuring the ceiling. + /// * `value` - The maximum auto-approval threshold any merchant may set. + pub fn set_auto_approve_below_ceiling( + env: Env, + admin: Address, + value: i128, + ) -> Result<(), Error> { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::Core(CoreError::Unauthorized))?; + if admin != stored_admin { + return Err(Error::Core(CoreError::Unauthorized)); + } + Self::set_auto_approve_below_ceiling_inner(&env, value); + Ok(()) } /// Check whether a merchant inherits its refund policy from its parent merchant. @@ -4661,6 +4712,49 @@ impl RefundContract { Ok(()) } + /// Set whether a merchant inherits its refund policy from its parent merchant. + /// + /// # Arguments + /// * `merchant` - The merchant address to configure (must authenticate). + /// * `inherit` - `true` to enable inheritance, `false` to disable it. + pub fn set_inherit_from_parent(env: Env, merchant: Address, inherit: bool) { + merchant.require_auth(); + Self::set_inherit_from_parent_inner(&env, &merchant, inherit); + } + + /// Deactivate a merchant's refund policy so it is no longer enforced. + /// + /// # Arguments + /// * `merchant` - The merchant whose policy should be deactivated (must authenticate). + /// + /// # Errors + /// Returns `PolicyNotFound` if no policy exists for the merchant. + /// Returns `PolicyInactive` if the policy is already inactive. + pub fn deactivate_refund_policy(env: Env, merchant: Address) -> Result<(), Error> { + // Require merchant authentication + merchant.require_auth(); + + let mut policy: RefundPolicy = env + .storage() + .instance() + .get(&DataKey::RefundPolicy(merchant.clone())) + .ok_or(Error::Core(CoreError::PolicyNotFound))?; + + if !policy.active { + return Err(Error::Core(CoreError::PolicyInactive)); + } + + policy.active = false; + env.storage() + .instance() + .set(&DataKey::RefundPolicy(merchant.clone()), &policy); + + // Emit RefundPolicyDeactivated event + (RefundPolicyDeactivated { merchant }).publish(&env); + + Ok(()) + } + /// Override a refund decision as an admin and create an immutable audit log entry. /// /// Records the override with a SHA-256 transaction hash for integrity verification. @@ -5447,7 +5541,12 @@ impl RefundContract { }; let requires_approval = Self::get_requires_admin_approval_inner(&env, &effective_merchant); - let auto_below = Self::get_auto_approve_below_inner(&env, &effective_merchant); + let auto_below = { + let merchant_threshold = + Self::get_auto_approve_below_inner(&env, &effective_merchant); + let platform_ceiling = Self::get_auto_approve_below_ceiling_inner(&env); + core::cmp::min(merchant_threshold, platform_ceiling) + }; if !requires_approval && amount <= auto_below { RefundStatus::Approved } else { @@ -6593,8 +6692,23 @@ impl RefundContract { // Helper functions for fraud detection fn get_customer_payment_count(env: &Env, address: &Address) -> u64 { - // Without a payment contract configured, we have no payment data - 0 + let payment_contract: Address = match env + .storage() + .instance() + .get(&DataKey::PaymentContractAddress) + { + Some(addr) => addr, + None => return 0, + }; + + let func = Symbol::new(env, "get_payment_count_by_customer"); + let args = (address.clone(),).into_val(env); + + match env.try_invoke_contract::(&payment_contract, &func, args) + { + Ok(Ok(count)) => count, + _ => 0, + } } fn get_customer_refund_count(env: &Env, address: &Address) -> u64 { @@ -8573,94 +8687,4 @@ impl RefundContract { /// Enable or disable strict tier policy enforcement for a merchant. /// /// When strict mode is enabled, customers without an assigned tier are - /// denied refunds instead of falling back to default behavior. - /// - /// # Arguments - /// * `merchant` - The merchant to configure (must authenticate). - /// * `strict` - `true` to enable strict mode, `false` to disable it. - pub fn set_strict_tier_policy( - env: Env, - merchant: Address, - strict: bool, - ) -> Result<(), Error> { - merchant.require_auth(); - env.storage() - .instance() - .set(&DataKey::StrictTierPolicy(merchant), &strict); - Ok(()) - } - - /// Check whether strict tier policy enforcement is enabled for a merchant. - /// - /// # Arguments - /// * `merchant` - The merchant to query. - /// - /// # Returns - /// `true` if strict mode is enabled, `false` otherwise (the default). - pub fn get_strict_tier_policy(env: Env, merchant: Address) -> bool { - env.storage() - .instance() - .get(&DataKey::StrictTierPolicy(merchant)) - .unwrap_or(false) - } -} - -mod test; -mod test_policy; -mod test_process; -mod test_rate_limit; - -#[cfg(test)] -mod test_payment_refund_cap; - -#[cfg(test)] -mod test_circuit_breaker; - -// #[cfg(test)] -// mod test_versioning; - -#[cfg(test)] -mod test_batch; - -#[cfg(test)] -mod test_cross_contract; - -#[cfg(test)] -mod test_arbitration_fees; - -#[cfg(test)] -mod test_arbitration_stake; - -#[cfg(test)] -mod test_arbitrator_reputation; - -#[cfg(test)] -mod test_auto_refund; - -#[cfg(test)] -mod test_inheritance; - -mod test_customer_history; -#[cfg(test)] -mod test_notification_hooks; - -#[cfg(test)] -mod test_arbitration_timeout; - -#[cfg(test)] -mod test_merchant_eligibility; - -#[cfg(test)] -mod test_customer_tier_policy; - -#[cfg(test)] -mod test_voucher_expiry; - -#[cfg(test)] -mod schema_version_test; - -#[cfg(test)] -mod test_merchant_override_and_error_codes; - -#[cfg(test)] -mod test_admin_rotation; + /// denied refunds inst \ No newline at end of file diff --git a/contracts/refund/src/test_inheritance.rs b/contracts/refund/src/test_inheritance.rs index 29b85d4..68146d9 100644 --- a/contracts/refund/src/test_inheritance.rs +++ b/contracts/refund/src/test_inheritance.rs @@ -20,6 +20,7 @@ fn test_single_level_inheritance() { client.initialize(&admin); env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &1000i128); // Parent sets their own policy let tiers = Vec::from_array( @@ -128,6 +129,7 @@ fn test_child_override_priority() { ); client.set_refund_policy(&child_merchant, &child_tiers); client.set_requires_admin_approval(&child_merchant, &false); + client.set_auto_approve_below_ceiling(&admin, &1000i128); client.set_auto_approve_below(&child_merchant, &100i128); // Child policy should be returned, not parent's @@ -485,6 +487,7 @@ fn test_parent_updates_existing_policy() { client.initialize(&admin); env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &1000i128); // Child sets policy first (no parent yet) let tiers = Vec::from_array( diff --git a/contracts/refund/src/test_policy.rs b/contracts/refund/src/test_policy.rs index 9ba68ea..9f325d6 100644 --- a/contracts/refund/src/test_policy.rs +++ b/contracts/refund/src/test_policy.rs @@ -17,6 +17,7 @@ fn test_set_refund_policy_successfully() { client.initialize(&admin); env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &1000i128); let tiers = Vec::from_array( &env, [RefundTier { @@ -403,6 +404,7 @@ fn test_auto_approve_below_threshold() { client.initialize(&admin); env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &1000i128); // Set policy with auto-approve for amounts <= 500 let tiers = Vec::from_array( &env, @@ -413,6 +415,7 @@ fn test_auto_approve_below_threshold() { ); client.set_refund_policy(&merchant, &tiers); client.set_requires_admin_approval(&merchant, &false); + client.set_auto_approve_below_ceiling(&admin, &1000i128); client.set_auto_approve_below(&merchant, &500i128); // Request refund for 300 (should be auto-approved) @@ -436,6 +439,30 @@ fn test_auto_approve_below_threshold() { assert_eq!(refund.status, RefundStatus::Approved); } +#[test] +fn test_auto_approve_threshold_cannot_exceed_ceiling() { + let env = Env::default(); + let contract_id = env.register(RefundContract, ()); + let client = RefundContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let merchant = Address::generate(&env); + + client.initialize(&admin); + + env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &500i128); + + let result = client.try_set_auto_approve_below(&merchant, &600i128); + + assert_eq!( + result, + Err(Ok(Error::Core( + CoreError::AutoApproveThresholdExceedsCeiling + ))) + ); +} + #[test] fn test_refund_with_inactive_policy_should_fail() { let env = Env::default(); @@ -450,6 +477,7 @@ fn test_refund_with_inactive_policy_should_fail() { client.initialize(&admin); env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &1000i128); // Set a policy let tiers = Vec::from_array( &env, @@ -694,6 +722,7 @@ fn test_request_refund_uses_global_default_when_no_merchant_policy() { }; client.set_default_refund_policy(&admin, &default_policy); client.set_requires_admin_approval(&admin, &false); + client.set_auto_approve_below_ceiling(&admin, &1000i128); client.set_auto_approve_below(&admin, &200i128); // No merchant-specific policy set; amount (100) <= auto_approve_below (200) → auto-approved @@ -726,6 +755,7 @@ fn test_request_refund_returns_policy_not_found_when_no_policy_at_all() { client.initialize(&admin); env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &1000i128); // Remove the default policy that initialize() set, and set NO merchant policy client.remove_default_refund_policy(&admin); @@ -758,6 +788,7 @@ fn test_default_policy_change_does_not_affect_pending_refunds() { client.initialize(&admin); env.mock_all_auths(); + client.set_auto_approve_below_ceiling(&admin, &1000i128); // Submit a refund using the current default (set by initialize()) let refund_id = client.request_refund( @@ -792,6 +823,7 @@ fn test_default_policy_change_does_not_affect_pending_refunds() { }; client.set_default_refund_policy(&admin, &new_default); client.set_requires_admin_approval(&admin, &false); + client.set_auto_approve_below_ceiling(&admin, &1000i128); client.set_auto_approve_below(&admin, &1000i128); // The already-stored refund must NOT be retroactively changed