Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions contracts/invoice-escrow/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
48 changes: 48 additions & 0 deletions contracts/invoice-escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
28 changes: 28 additions & 0 deletions contracts/invoice-escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
15 changes: 15 additions & 0 deletions contracts/invoice-escrow/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address>,
pub threshold: u32,
}

// Update Invoice escrow state
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvoiceEscrow {
// ... existing fields ...
pub emergency_admin: Option<EmergencyAdmin>,
pub emergency_released: bool,
}
Loading