diff --git a/contracts/invoice-escrow/src/errors.rs b/contracts/invoice-escrow/src/errors.rs index 231e722..aa57cba 100644 --- a/contracts/invoice-escrow/src/errors.rs +++ b/contracts/invoice-escrow/src/errors.rs @@ -52,4 +52,12 @@ pub enum Error { SignatureExpired = 22, /// Funding amount does not meet the required milestone threshold. InvalidMilestoneAmount = 23, + /// Caller is not authorized as an emergency admin for this escrow + NotEmergencyAdmin, + // Emergency release is not configured or not available for this invoice + EmergencyReleaseNotAllowed, + // Invoice funds have already been released (either normally or via emergency) + InvoiceAlreadyReleased, + // Invoice is not in a locked escrow state and cannot be emergency released + InvoiceNotInEscrow, } diff --git a/contracts/invoice-escrow/src/lib.rs b/contracts/invoice-escrow/src/lib.rs index e76a381..33dcb13 100644 --- a/contracts/invoice-escrow/src/lib.rs +++ b/contracts/invoice-escrow/src/lib.rs @@ -662,6 +662,54 @@ impl InvoiceEscrow { events::escrow_cleaned_up(&env, invoice_id); Ok(()) } + + pub fn emergency_release(env: Env, invoice_id: u64, caller: Address,) -> Result<(), Error> { + // 1. Validate caller is an emergency admin + let escrow = Self::get_escrow(&env, invoice_id)?; + let admin = escrow.emergency_admin + .ok_or(Error::EmergencyReleaseNotAllowed)?; + + // 2. Verify multi-sig threshold + let signature_count = Self::count_approvals(&env, invoice_id, &admin); + if signature_count < admin.threshold { + return Err(Error::NotEmergencyAdmin); + } + + // 3. Check if already released or invalid state + if escrow.emergency_released { + return Err(Error::InvoiceAlreadyReleased); + } + if escrow.status != EscrowStatus::Locked { + return Err(Error::InvoiceNotInEscrow); + } + + // 4. Release funds to appropriate parties + let recipient = escrow.recipient; + let amount = escrow.amount; + + // Transfer funds + token::transfer(&env, &escrow.token, &recipient, &amount)?; + + // 5. Update escrow state + let mut updated_escrow = escrow; + updated_escrow.status = EscrowStatus::EmergencyReleased; + updated_escrow.emergency_released = true; + Self::save_escrow(&env, invoice_id, &updated_escrow); + + // 6. Emit event for transparency + Self::emit_emergency_release_event(&env, invoice_id, caller, recipient, amount); + + Ok(()) +} + +// Helper functions +fn count_approvals(env: &Env, invoice_id: u64, admin: &EmergencyAdmin) -> u32 { + // Count how many admins have approved this emergency release + // Implementation depends on how approvals are stored + admin.addresses.iter() + .filter(|addr| Self::has_approved(env, invoice_id, addr)) + .count() as u32 +} } #[cfg(test)] diff --git a/contracts/invoice-escrow/src/test.rs b/contracts/invoice-escrow/src/test.rs index abc4930..7929083 100644 --- a/contracts/invoice-escrow/src/test.rs +++ b/contracts/invoice-escrow/src/test.rs @@ -3186,3 +3186,31 @@ fn test_fund_escrow_allows_remainder_below_milestone() { let escrow = escrow_client.get_escrow(&invoice_id); assert_eq!(escrow.status, EscrowStatus::Funded); } + +#[test] +fn test_emergency_release_success() { + // Setup admin addresses + let admin1 = Address::generate(&Env::default()); + let admin2 = Address::generate(&Env::default()); + let admin3 = Address::generate(&Env::default()); + + let emergency_admin = EmergencyAdmin { + addresses: vec![admin1.clone(), admin2.clone(), admin3.clone()], + threshold: 2, + }; + + // Create escrow with emergency admin + // Simulate approvals from admin1 and admin2 + // Call emergency_release + // Verify funds transferred and state updated +} + +#[test] +fn test_emergency_release_not_admin() { + // Test that non-admin callers are rejected +} + +#[test] +fn test_emergency_release_insufficient_signatures() { + // Test that below threshold fails +} \ No newline at end of file diff --git a/contracts/invoice-escrow/src/types.rs b/contracts/invoice-escrow/src/types.rs index bfbebc7..588413a 100644 --- a/contracts/invoice-escrow/src/types.rs +++ b/contracts/invoice-escrow/src/types.rs @@ -88,3 +88,18 @@ pub struct EscrowData { /// Set at creation, cannot be modified. SHA-256 hash (32 bytes). pub commitment: soroban_sdk::BytesN<32>, } + +// Add admin multi-sig structure +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EmergencyAdmin { + pub addresses: Vec
, + pub threshold: u32, +} + +// Update Invoice escrow state +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InvoiceEscrow { + // ... existing fields ... + pub emergency_admin: Option