Skip to content
Merged
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
97 changes: 97 additions & 0 deletions contract/src/insurance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use soroban_sdk::{token, Address, Env};
use crate::storage::{
get_insurance_policy, has_insurance_provider, save_insurance_policy,
get_trade,
};
use crate::types::{TradeStatus, InsurancePolicy};
use crate::errors::ContractError;
use crate::events;

/// Calculate insurance premium based on trade amount (e.g., 2% flat for this implementation)
/// Supports multiple providers by allowing them to define premiums (simulated here)
pub fn calculate_premium(_env: &Env, amount: u64, _provider: &Address) -> u64 {
// 2% premium (200 bps)
amount * 200 / 10000
}

/// Attach an insurance policy to a trade
pub fn purchase_insurance(
env: &Env,
trade_id: u64,
buyer: Address,
provider: Address,
) -> Result<(), ContractError> {
if !has_insurance_provider(env, &provider) {
return Err(ContractError::InsuranceProviderNotRegistered);
}

let mut trade = get_trade(env, trade_id)?;
if trade.status != TradeStatus::Created {
return Err(ContractError::InvalidStatus);
}
if trade.buyer != buyer {
return Err(ContractError::Unauthorized);
}
buyer.require_auth();

if get_insurance_policy(env, trade_id).is_some() {
return Err(ContractError::InvalidStatus); // Already insured
}

let premium = calculate_premium(env, trade.amount, &provider);
if premium > (trade.amount * crate::types::MAX_INSURANCE_PREMIUM_BPS as u64 / 10000) {
return Err(ContractError::InsurancePremiumTooHigh);
}

let token_client = token::Client::new(env, &trade.currency);
token_client.transfer(&buyer, &env.current_contract_address(), &(premium as i128));

let coverage = trade.amount; // 100% coverage
let policy = InsurancePolicy {
provider: provider.clone(),
premium,
coverage,
claimed: false,
};

save_insurance_policy(env, trade_id, &policy);
events::emit_insurance_purchased(env, trade_id, provider, premium, coverage);

Ok(())
}

/// Process an insurance claim after a dispute resolution
pub fn claim_insurance(
env: &Env,
trade_id: u64,
recipient: Address,
) -> Result<(), ContractError> {
let mut policy = get_insurance_policy(env, trade_id).ok_or(ContractError::TradeNotInsured)?;
if policy.claimed {
return Err(ContractError::InsuranceAlreadyClaimed);
}

let trade = get_trade(env, trade_id)?;
if trade.status != TradeStatus::Disputed {
return Err(ContractError::InvalidStatus);
}

// Only allow claims if the recipient was potentially wronged
// (e.g., seller sends junk, buyer loses money)
// For simplicity, we assume the claim is valid if the trade was disputed
// and the insurance provider authorizes it or it's triggered by specific resolution
recipient.require_auth();

let payout = policy.coverage;
let token_client = token::Client::new(env, &trade.currency);

// Transfer from provider to recipient
token_client.transfer(&policy.provider, &recipient, &(payout as i128));

policy.claimed = true;
save_insurance_policy(env, trade_id, &policy);

events::emit_insurance_claimed(env, trade_id, payout, recipient);

Ok(())
}
81 changes: 69 additions & 12 deletions contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod tiers;
mod types;
mod upgrade;
mod proxy;
mod insurance;

use soroban_sdk::{contract, contractimpl, token::TokenClient, Address, BytesN, Env};

Expand Down Expand Up @@ -65,6 +66,7 @@ use storage::{
save_arbitrator, save_arbitrator_reputation, save_trade, set_accumulated_fees, set_admin,
set_currency_fees, set_fee_bps, set_initialized, set_paused, set_trade_counter,
set_usdc_token, CrossChainInfo, InsurancePolicy,
has_insurance_provider, save_insurance_provider, remove_insurance_provider,
};

fn token_client<'a>(env: &'a Env, token: &Address) -> token::Client<'a> {
Expand Down Expand Up @@ -321,7 +323,8 @@ impl StellarEscrowContract {
let resolution = multisig::resolve_expired_dispute(&env, trade_id, &admin)?;
let trade = get_trade(&env, trade_id)?;
StellarEscrowContract::execute_dispute_resolution(env, trade_id, resolution, trade)
// Arbitrator Reputation
}

// -------------------------------------------------------------------------

/// Rate the arbitrator of a disputed trade (buyer or seller, once each).
Expand Down Expand Up @@ -401,6 +404,60 @@ impl StellarEscrowContract {
Ok(())
}

// -------------------------------------------------------------------------
// Trade Insurance
// -------------------------------------------------------------------------

/// Register an insurance provider (admin only)
pub fn register_insurance_provider(env: Env, provider: Address) -> Result<(), ContractError> {
require_initialized(&env)?;
require_not_paused(&env)?;
let admin = get_admin(&env)?;
admin.require_auth();
save_insurance_provider(&env, &provider);
events::emit_insurance_provider_registered(&env, provider);
Ok(())
}

/// Remove an insurance provider (admin only)
pub fn remove_insurance_provider_fn(env: Env, provider: Address) -> Result<(), ContractError> {
require_initialized(&env)?;
require_not_paused(&env)?;
let admin = get_admin(&env)?;
admin.require_auth();
remove_insurance_provider(&env, &provider);
events::emit_insurance_provider_removed(&env, provider);
Ok(())
}

/// Purchase optional trade insurance for a created trade
pub fn purchase_insurance(
env: Env,
trade_id: u64,
buyer: Address,
provider: Address,
) -> Result<(), ContractError> {
require_initialized(&env)?;
require_not_paused(&env)?;
insurance::purchase_insurance(&env, trade_id, buyer, provider)
}

/// Claim insurance payout for a disputed trade
pub fn claim_insurance(
env: Env,
trade_id: u64,
recipient: Address,
) -> Result<(), ContractError> {
require_initialized(&env)?;
require_not_paused(&env)?;
insurance::claim_insurance(&env, trade_id, recipient)
}

/// Calculate insurance premium for a given trade amount
pub fn get_insurance_premium(env: Env, amount: u64, provider: Address) -> u64 {
insurance::calculate_premium(&env, amount, &provider)
}

pub fn set_user_compliance(
env: Env,
admin: Address,
Expand Down Expand Up @@ -668,6 +725,8 @@ impl StellarEscrowContract {
events::emit_trade_funded(&env, trade_id);
analytics::on_trade_funded(&env);
Ok(())
}

pub fn complete_trade(env: Env, trade_id: u64) -> Result<(), ContractError> {
require_initialized(&env)?;
require_not_paused(&env)?;
Expand Down Expand Up @@ -712,6 +771,8 @@ impl StellarEscrowContract {
events::emit_trade_confirmed(&env, trade_id, payout, trade.fee);
analytics::on_trade_completed(&env, trade.fee);
Ok(())
}

pub fn raise_dispute(env: Env, trade_id: u64, caller: Address) -> Result<(), ContractError> {
if !is_initialized(&env) {
return Err(ContractError::NotInitialized);
Expand Down Expand Up @@ -758,6 +819,8 @@ impl StellarEscrowContract {
events::emit_dispute_raised(&env, trade_id, caller);
analytics::on_trade_disputed(&env);
Ok(())
}

/// Use `DisputeResolution::Partial { buyer_bps }` for a split:
/// `buyer_bps` is the buyer's share of the net payout in basis points (0–10000).
pub fn resolve_dispute(
Expand Down Expand Up @@ -938,7 +1001,10 @@ impl StellarEscrowContract {
save_trade(&env, trade_id, &trade);
events::emit_trade_cancelled(&env, trade_id);
analytics::on_trade_cancelled(&env);
Ok(()): anyone can call this once the expiry has
Ok(())
}

/// anyone can call this once the expiry has
/// passed and the trade is Funded or Completed (not Disputed/Cancelled).
/// Funds are released to the seller minus the platform fee.
pub fn claim_time_release(env: Env, trade_id: u64) -> Result<(), ContractError> {
Expand Down Expand Up @@ -1117,23 +1183,14 @@ impl StellarEscrowContract {
events::emit_unpaused(&env, admin);
Ok(())
}

/// Emergency withdrawal of all contract token balance (admin only).
pub fn emergency_withdraw(env: Env, to: Address) -> Result<(), ContractError> {
if !is_initialized(&env) {
return Err(ContractError::NotInitialized);
}
let admin = get_admin(&env)?;
admin.require_auth();
let token = get_usdc_token(&env)?;
let token_client = token::Client::new(&env, &token);
/// Allowed even while paused so funds can always be recovered.
pub fn emergency_withdraw(env: Env, to: Address) -> Result<(), ContractError> {
require_initialized(&env)?;
let admin = get_admin(&env)?;
admin.require_auth();
let token = get_usdc_token(&env)?;
let token_client = TokenClient::new(&env, &token);
let token_client = token::Client::new(&env, &token);
let balance = token_client.balance(&env.current_contract_address());
if balance > 0 {
token_client.transfer(&env.current_contract_address(), &to, &balance);
Expand Down
Loading