diff --git a/Cargo.lock b/Cargo.lock index f7e1efa..a4b7e22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,6 +435,7 @@ version = "0.1.0" dependencies = [ "delivery_contract", "escrow_contract", + "identity_reputation_contract", "shared_types", "soroban-sdk", ] @@ -523,6 +524,7 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" name = "escrow_contract" version = "0.1.0" dependencies = [ + "settlement_contract", "shared_types", "soroban-sdk", ] diff --git a/contracts/dispute_resolution_contract/Cargo.toml b/contracts/dispute_resolution_contract/Cargo.toml index a5fd7c8..908aeea 100644 --- a/contracts/dispute_resolution_contract/Cargo.toml +++ b/contracts/dispute_resolution_contract/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib", "rlib"] soroban-sdk = { workspace = true } delivery_contract = { path = "../delivery_contract" } escrow_contract = { path = "../escrow_contract" } +identity_reputation_contract = { path = "../identity_reputation_contract" } shared_types = { path = "../shared_types" } [dev-dependencies] diff --git a/contracts/dispute_resolution_contract/lib.rs b/contracts/dispute_resolution_contract/lib.rs index fbbf10d..69c8b01 100644 --- a/contracts/dispute_resolution_contract/lib.rs +++ b/contracts/dispute_resolution_contract/lib.rs @@ -6,6 +6,9 @@ use soroban_sdk::{ }; use shared_types::{DeliveryId, SwiftChainError}; use delivery_contract::{DeliveryContractClient, DeliveryStatus}; +use identity_reputation_contract::IdentityReputationContractClient; + +const DISPUTE_REPUTATION_PENALTY: u32 = 10; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -32,6 +35,7 @@ pub enum DataKey { Admin(Address), DeliveryContract, EscrowContract, + IdentityReputationContract, DisputeTimeLimit, Dispute(DeliveryId), } @@ -94,6 +98,23 @@ impl DisputeResolutionContract { .unwrap_or_else(|| panic_with_error!(&env, SwiftChainError::NotInitialized)) } + pub fn set_identity_reputation_contract(env: Env, caller: Address, reputation_contract: Address) { + caller.require_auth(); + if !Self::is_admin(env.clone(), caller.clone()) { + panic_with_error!(&env, SwiftChainError::Unauthorized); + } + env.storage() + .instance() + .set(&DataKey::IdentityReputationContract, &reputation_contract); + } + + pub fn get_identity_reputation_contract(env: Env) -> Address { + env.storage() + .instance() + .get(&DataKey::IdentityReputationContract) + .unwrap_or_else(|| panic_with_error!(&env, SwiftChainError::NotInitialized)) + } + pub fn get_dispute_time_limit(env: Env) -> u64 { env.storage() .instance() @@ -218,6 +239,21 @@ impl DisputeResolutionContract { env.storage().persistent().set(&dispute_key, &dispute); env.storage().persistent().extend_ttl(&dispute_key, 518400, 518400); + let delivery_contract_addr = Self::get_delivery_contract(env.clone()); + let delivery_client = DeliveryContractClient::new(&env, &delivery_contract_addr); + let delivery = delivery_client.get_delivery(&delivery_id); + let driver = delivery + .driver + .unwrap_or_else(|| panic_with_error!(&env, SwiftChainError::ProviderNotFound)); + + let reputation_addr = Self::get_identity_reputation_contract(env.clone()); + let reputation_client = IdentityReputationContractClient::new(&env, &reputation_addr); + reputation_client.decrease_reputation( + &env.current_contract_address(), + &driver, + &DISPUTE_REPUTATION_PENALTY, + ); + let escrow_addr = Self::get_escrow_contract(env.clone()); use soroban_sdk::IntoVal; @@ -234,7 +270,7 @@ impl DisputeResolutionContract { env.events().publish( (Symbol::new(&env, "dispute_resolved_refund"), delivery_id), - (caller, delivery_id), + (caller, delivery_id, driver, DISPUTE_REPUTATION_PENALTY), ); } @@ -340,4 +376,3 @@ impl DisputeResolutionContract { #[cfg(test)] mod test; - diff --git a/contracts/escrow_contract/Cargo.toml b/contracts/escrow_contract/Cargo.toml index 82f7912..599e0c9 100644 --- a/contracts/escrow_contract/Cargo.toml +++ b/contracts/escrow_contract/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = { workspace = true } shared_types = { path = "../shared_types" } +settlement_contract = { path = "../settlement_contract" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/escrow_contract/lib.rs b/contracts/escrow_contract/lib.rs index 40b8bde..501a3ca 100644 --- a/contracts/escrow_contract/lib.rs +++ b/contracts/escrow_contract/lib.rs @@ -8,6 +8,7 @@ use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, panic_with_error, token, Address, Env, Symbol, }; +use settlement_contract::SettlementContractClient; pub mod constants { pub const ESCROW_TTL_THRESHOLD: u32 = 518400; @@ -50,6 +51,35 @@ fn calculate_fee(amount: i128, platform_fee_bps: u32) -> i128 { amount.saturating_mul(platform_fee_bps as i128) / 10_000 } +fn get_settlement_contract(env: &Env) -> Option
{ + env.storage().instance().get(&DataKey::SettlementContract) +} + +fn payout_driver(env: &Env, token: &Address, driver: &Address, amount: i128) { + if amount <= 0 { + return; + } + + if let Some(settlement_addr) = get_settlement_contract(env) { + let settlement_client = SettlementContractClient::new(env, &settlement_addr); + if let Some(preferred_asset) = settlement_client.get_driver_preference(driver) { + if preferred_asset != token.clone() { + settlement_client.execute_settlement_swap( + &env.current_contract_address(), + token, + &preferred_asset, + driver, + &amount, + &0i128, + ); + return; + } + } + } + + token::Client::new(env, token).transfer(&env.current_contract_address(), driver, &amount); +} + fn save_escrow(env: &Env, delivery_id: u64, record: &EscrowRecord) { let key = escrow_key(delivery_id); env.storage().persistent().set(&key, record); @@ -79,6 +109,7 @@ fn load_escrow(env: &Env, delivery_id: u64) -> EscrowRecord { #[derive(Clone)] enum DataKey { PendingAdmin, + SettlementContract, } #[contracterror] @@ -189,6 +220,18 @@ impl EscrowContract { load_protocol_config(&env).protocol_version } + pub fn set_settlement_contract(env: Env, admin: Address, settlement_contract: Address) { + admin.require_auth(); + require_admin(&env, &admin); + env.storage() + .instance() + .set(&DataKey::SettlementContract, &settlement_contract); + } + + pub fn get_settlement_contract(env: Env) -> Option { + get_settlement_contract(&env) + } + pub fn propose_admin(env: Env, current_admin: Address, new_admin: Address) { current_admin.require_auth(); let stored_admin: Address = env @@ -302,13 +345,7 @@ impl EscrowContract { let platform_fee = calculate_fee(record.amount, platform_fee_bps); let driver_amount = record.amount.saturating_sub(platform_fee); - if driver_amount > 0 { - token::Client::new(&env, &record.token).transfer( - &env.current_contract_address(), - &record.driver, - &driver_amount, - ); - } + payout_driver(&env, &record.token, &record.driver, driver_amount); if platform_fee > 0 { let admin: Address = env @@ -398,13 +435,7 @@ impl EscrowContract { let platform_fee = calculate_fee(record.amount, platform_fee_bps); let driver_amount = record.amount.saturating_sub(platform_fee); - if driver_amount > 0 { - token::Client::new(&env, &record.token).transfer( - &env.current_contract_address(), - &record.driver, - &driver_amount, - ); - } + payout_driver(&env, &record.token, &record.driver, driver_amount); if platform_fee > 0 { let admin: Address = env diff --git a/contracts/settlement_contract/Cargo.toml b/contracts/settlement_contract/Cargo.toml index cc2fdf1..887daf4 100644 --- a/contracts/settlement_contract/Cargo.toml +++ b/contracts/settlement_contract/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] path = "lib.rs" [dependencies] diff --git a/contracts/settlement_contract/lib.rs b/contracts/settlement_contract/lib.rs index 93d94d5..b94c213 100644 --- a/contracts/settlement_contract/lib.rs +++ b/contracts/settlement_contract/lib.rs @@ -1,7 +1,8 @@ #![no_std] use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Env, + contract, contracterror, contractimpl, contracttype, panic_with_error, token, Address, Env, + IntoVal, Symbol, Vec, }; #[contracttype] @@ -17,10 +18,13 @@ pub enum SupportedAsset { #[derive(Clone, Debug, Eq, PartialEq)] enum DataKey { Admin, + Router, Xlm, Usdc, Ngnc, Kes, + AssetPair(Address, Address), + DriverPreference(Address), } #[contracterror] @@ -30,6 +34,9 @@ pub enum SettlementError { AlreadyInitialized = 1, NotInitialized = 2, Unauthorized = 3, + UnsupportedAssetPair = 4, + InvalidAmount = 5, + SlippageExceeded = 6, } fn asset_key(asset: SupportedAsset) -> DataKey { @@ -52,6 +59,38 @@ fn require_admin(env: &Env, caller: &Address) { } } +fn require_positive_amount(env: &Env, amount: i128) { + if amount <= 0 { + panic_with_error!(env, SettlementError::InvalidAmount); + } +} + +fn require_supported_pair(env: &Env, source_asset: &Address, destination_asset: &Address) { + if source_asset == destination_asset { + return; + } + + let key = DataKey::AssetPair(source_asset.clone(), destination_asset.clone()); + let supported = env.storage().persistent().get(&key).unwrap_or(false); + if !supported { + panic_with_error!(env, SettlementError::UnsupportedAssetPair); + } +} + +fn get_router(env: &Env) -> Address { + env.storage() + .instance() + .get(&DataKey::Router) + .unwrap_or_else(|| panic_with_error!(env, SettlementError::NotInitialized)) +} + +fn pair_path(env: &Env, source_asset: Address, destination_asset: Address) -> Vec { + let mut path = Vec::new(env); + path.push_back(source_asset); + path.push_back(destination_asset); + path +} + #[contract] pub struct SettlementContract; @@ -75,6 +114,66 @@ impl SettlementContract { env.storage().instance().set(&asset_key(asset), &supported); } + pub fn set_router(env: Env, admin: Address, router: Address) { + admin.require_auth(); + require_admin(&env, &admin); + env.storage().instance().set(&DataKey::Router, &router); + } + + pub fn get_router(env: Env) -> Address { + get_router(&env) + } + + pub fn set_supported_asset_pair( + env: Env, + admin: Address, + source_asset: Address, + destination_asset: Address, + supported: bool, + ) { + admin.require_auth(); + require_admin(&env, &admin); + + if source_asset == destination_asset { + panic_with_error!(&env, SettlementError::UnsupportedAssetPair); + } + + let key = DataKey::AssetPair(source_asset, destination_asset); + if supported { + env.storage().persistent().set(&key, &true); + } else { + env.storage().persistent().remove(&key); + } + } + + pub fn is_supported_asset_pair( + env: Env, + source_asset: Address, + destination_asset: Address, + ) -> bool { + if source_asset == destination_asset { + return true; + } + + env.storage() + .persistent() + .get(&DataKey::AssetPair(source_asset, destination_asset)) + .unwrap_or(false) + } + + pub fn register_driver_preference(env: Env, driver: Address, asset: Address) { + driver.require_auth(); + env.storage() + .persistent() + .set(&DataKey::DriverPreference(driver), &asset); + } + + pub fn get_driver_preference(env: Env, driver: Address) -> Option { + env.storage() + .persistent() + .get(&DataKey::DriverPreference(driver)) + } + pub fn is_supported(env: Env, asset: SupportedAsset) -> bool { env.storage() .instance() @@ -88,6 +187,102 @@ impl SettlementContract { .get(&DataKey::Admin) .unwrap_or_else(|| panic_with_error!(&env, SettlementError::NotInitialized)) } + + pub fn calculate_swap_estimate( + env: Env, + source_asset: Address, + destination_asset: Address, + amount_in: i128, + ) -> i128 { + require_positive_amount(&env, amount_in); + + if source_asset == destination_asset { + return amount_in; + } + + require_supported_pair(&env, &source_asset, &destination_asset); + + let router = get_router(&env); + let path = pair_path(&env, source_asset, destination_asset); + env.invoke_contract( + &router, + &Symbol::new(&env, "quote_exact_input"), + soroban_sdk::vec![&env, path.into_val(&env), amount_in.into_val(&env)], + ) + } + + pub fn execute_settlement_swap( + env: Env, + caller: Address, + source_asset: Address, + destination_asset: Address, + recipient: Address, + amount_in: i128, + min_amount_out: i128, + ) -> i128 { + caller.require_auth(); + require_positive_amount(&env, amount_in); + + if min_amount_out < 0 { + panic_with_error!(&env, SettlementError::InvalidAmount); + } + + if source_asset == destination_asset { + if amount_in < min_amount_out { + panic_with_error!(&env, SettlementError::SlippageExceeded); + } + token::Client::new(&env, &source_asset).transfer(&caller, &recipient, &amount_in); + return amount_in; + } + + require_supported_pair(&env, &source_asset, &destination_asset); + + let estimated_amount_out = Self::calculate_swap_estimate( + env.clone(), + source_asset.clone(), + destination_asset.clone(), + amount_in, + ); + if estimated_amount_out < min_amount_out { + panic_with_error!(&env, SettlementError::SlippageExceeded); + } + + let router = get_router(&env); + let contract_address = env.current_contract_address(); + token::Client::new(&env, &source_asset).transfer(&caller, &contract_address, &amount_in); + token::Client::new(&env, &source_asset).approve( + &contract_address, + &router, + &amount_in, + &(env.ledger().sequence() + 100), + ); + + let path = pair_path(&env, source_asset, destination_asset); + let amount_out: i128 = env.invoke_contract( + &router, + &Symbol::new(&env, "swap_exact_tokens_for_tokens"), + soroban_sdk::vec![ + &env, + contract_address.into_val(&env), + recipient.clone().into_val(&env), + path.into_val(&env), + amount_in.into_val(&env), + min_amount_out.into_val(&env), + ], + ); + + if amount_out < min_amount_out { + panic_with_error!(&env, SettlementError::SlippageExceeded); + } + + env.events().publish( + (Symbol::new(&env, "settlement_swap_executed"),), + (caller, recipient, amount_in, amount_out), + ); + + amount_out + } + } #[cfg(test)] @@ -141,4 +336,3 @@ mod test { assert!(!client.is_supported(&SupportedAsset::Kes)); } } -