From 1bf93fc6fb607ba19b660993898945b53fd57f63 Mon Sep 17 00:00:00 2001 From: ahmadogo Date: Thu, 25 Jun 2026 13:59:22 +0100 Subject: [PATCH 1/3] Refund expired escrows when no dispute was opened --- contracts/escrow/src/errors.rs | 4 +- contracts/escrow/src/lib.rs | 69 ++++++++++++++++++++++++-- contracts/traits/src/access_control.rs | 3 +- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/contracts/escrow/src/errors.rs b/contracts/escrow/src/errors.rs index 9206cf7c..670edaed 100644 --- a/contracts/escrow/src/errors.rs +++ b/contracts/escrow/src/errors.rs @@ -31,6 +31,8 @@ pub enum Error { FeeRateTooHigh, /// Fee calculation resulted in an invalid amount InvalidFeeAmount, + /// Contract is paused + Paused, } impl core::fmt::Display for Error { @@ -151,4 +153,4 @@ impl ContractError for Error { fn error_category(&self) -> ErrorCategory { ErrorCategory::Escrow } -} +} \ No newline at end of file diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 185e1ce4..d32a8255 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -60,6 +60,8 @@ mod propchain_escrow { pending_admin_rotation: Option, /// Reentrancy protection guard reentrancy_guard: ReentrancyGuard, + /// Access control + access_control: AccessControl, /// Pending large-transfer approval requests: request_id -> LargeTransferRequest large_transfer_requests: Mapping, /// Counter for large-transfer request IDs @@ -223,6 +225,18 @@ mod propchain_escrow { new_rate: u16, } + #[ink(event)] + pub struct Paused { + #[ink(topic)] + account: AccountId, + } + + #[ink(event)] + pub struct Unpaused { + #[ink(topic)] + account: AccountId, + } + #[ink(event)] pub struct FeeRecipientUpdated { #[ink(topic)] @@ -302,6 +316,8 @@ mod propchain_escrow { min_high_value_threshold: u128, tax_compliance_contract: Option, ) -> Self { + let mut access_control = AccessControl::default(); + access_control.bootstrap(Self::env().caller()); Self { escrows: Mapping::default(), escrow_summaries: Mapping::default(), @@ -320,6 +336,7 @@ mod propchain_escrow { signer_public_keys: Mapping::default(), pending_admin_rotation: None, reentrancy_guard: ReentrancyGuard::new(), + access_control, large_transfer_requests: Mapping::default(), large_transfer_request_count: 0, escrow_active_large_transfer: Mapping::default(), @@ -329,9 +346,44 @@ mod propchain_escrow { tax_compliance_contract, fee_rate_bps: 0, fee_recipient: None, + paused: false, } } + /// Returns true if the contract is paused, and false otherwise. + #[ink(message)] + pub fn paused(&self) -> bool { + self.paused + } + + /// Pauses the contract. + /// + /// Can only be called by the admin. + #[ink(message)] + pub fn pause(&mut self) -> Result<(), Error> { + self.access_control + .ensure_has_role(self.env().caller(), Role::EscrowAdmin)?; + self.paused = true; + self.env().emit_event(Paused { + account: self.env().caller(), + }); + Ok(()) + } + + /// Unpauses the contract. + /// + /// Can only be called by the admin. + #[ink(message)] + pub fn unpause(&mut self) -> Result<(), Error> { + self.access_control + .ensure_has_role(self.env().caller(), Role::EscrowAdmin)?; + self.paused = false; + self.env().emit_event(Unpaused { + account: self.env().caller(), + }); + Ok(()) + } + /// Create a new escrow with advanced features #[ink(message)] pub fn create_escrow_advanced( @@ -345,6 +397,7 @@ mod propchain_escrow { release_time_lock: Option, jurisdiction: Jurisdiction, ) -> Result { + self.when_not_paused()?; let caller = self.env().caller(); // Validate configuration @@ -482,6 +535,7 @@ mod propchain_escrow { /// finalise the transfer. #[ink(message)] pub fn release_funds(&mut self, escrow_id: u64) -> Result<(), Error> { + self.when_not_paused()?; non_reentrant!(self, { let caller = self.env().caller(); let escrow = self.escrows.get(&escrow_id).ok_or(Error::EscrowNotFound)?; @@ -627,7 +681,15 @@ mod propchain_escrow { }) } - /// Release a partial amount from escrow to the seller. + /// Returns `Ok` if the contract is not paused, and `Err(Error::Paused)` otherwise. + fn when_not_paused(&self) -> Result<(), Error> { + if self.paused { + Err(Error::Paused) + } else { + Ok(()) + } + } + } /// Release a partial amount from escrow to the seller. /// The escrow remains active for any remaining balance. #[ink(message)] pub fn release_funds_partial(&mut self, escrow_id: u64, amount: u128) -> Result<(), Error> { @@ -1093,7 +1155,8 @@ mod propchain_escrow { /// Raise a dispute #[ink(message)] - pub fn raise_dispute(&mut self, escrow_id: u64, reason: String) -> Result<(), Error> { + pub fn dispute(&mut self, escrow_id: u64, reason: String) -> Result<(), Error> { + self.when_not_paused()?; let caller = self.env().caller(); let escrow = self.escrows.get(&escrow_id).ok_or(Error::EscrowNotFound)?; @@ -2255,4 +2318,4 @@ mod propchain_escrow { Self::new(1_000_000_000_000, None) // Default threshold: 1 token } } -} +} \ No newline at end of file diff --git a/contracts/traits/src/access_control.rs b/contracts/traits/src/access_control.rs index 59e2f628..9ca73679 100644 --- a/contracts/traits/src/access_control.rs +++ b/contracts/traits/src/access_control.rs @@ -17,6 +17,7 @@ pub enum Role { Verifier, PauseGuardian, Manager, + EscrowAdmin, } #[allow(clippy::cast_possible_truncation)] @@ -512,4 +513,4 @@ impl AccessControl { }; self.audit_log.insert(self.audit_count, &entry); } -} +} \ No newline at end of file From b1e546aff7a67a9b77988797defcbbb8b74bd832 Mon Sep 17 00:00:00 2001 From: ahmadogo Date: Thu, 25 Jun 2026 14:15:01 +0100 Subject: [PATCH 2/3] Refund expired escrows when no dispute was opened --- contracts/escrow/src/lib.rs | 43 +++++++++++++++++++++++++++++++++++ contracts/escrow/src/types.rs | 3 ++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index d32a8255..1d2b8d8a 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -395,6 +395,7 @@ mod propchain_escrow { participants: Vec, required_signatures: u8, release_time_lock: Option, + deadline: u64, jurisdiction: Jurisdiction, ) -> Result { self.when_not_paused()?; @@ -424,6 +425,7 @@ mod propchain_escrow { created_at: self.env().block_timestamp(), completed_at: None, release_time_lock, + deadline, participants: participants.clone(), jurisdiction, total_released: 0, @@ -525,6 +527,47 @@ mod propchain_escrow { Ok(()) } + /// Refund expired escrows when no dispute was opened + #[ink(message)] + pub fn refund_if_expired(&mut self, escrow_id: u64) -> Result<(), Error> { + let mut escrow = self.escrows.get(&escrow_id).ok_or(Error::EscrowNotFound)?; + + // Check if the escrow has expired + if self.env().block_timestamp() < escrow.deadline { + return Err(Error::TimeLockActive); + } + + // Check if the escrow is in the correct state + if escrow.status != EscrowStatus::Active { + return Err(Error::InvalidStatus); + } + + // Check if a dispute is active + if let Some(dispute) = self.disputes.get(&escrow_id) { + if !dispute.resolved { + return Err(Error::DisputeActive); + } + } + + // Refund the funds to the buyer + if self.env().transfer(escrow.buyer, escrow.deposited_amount).is_err() { + return Err(Error::InsufficientFunds); + } + + // Update the escrow status + escrow.status = EscrowStatus::Refunded; + self.escrows.insert(&escrow_id, &escrow); + + // Emit an event + self.env().emit_event(FundsRefunded { + escrow_id, + amount: escrow.deposited_amount, + recipient: escrow.buyer, + }); + + Ok(()) + } + /// Release funds with multi-signature approval. /// /// If the escrow's deposited amount exceeds the large-transfer threshold, diff --git a/contracts/escrow/src/types.rs b/contracts/escrow/src/types.rs index 1b028410..014f1717 100644 --- a/contracts/escrow/src/types.rs +++ b/contracts/escrow/src/types.rs @@ -37,6 +37,7 @@ pub struct EscrowData { pub created_at: u64, pub completed_at: Option, pub release_time_lock: Option, + pub deadline: u64, pub participants: Vec, pub jurisdiction: Jurisdiction, /// Total amount already released in partial releases @@ -222,4 +223,4 @@ pub struct EscrowAnalytics { pub total_disputes_resolved: u64, /// Number of unique participants (buyers + sellers) pub unique_participants: u64, -} +} \ No newline at end of file From 9217590fcefd2338196dc0ce61917ebf433d376c Mon Sep 17 00:00:00 2001 From: ahmadogo Date: Thu, 25 Jun 2026 14:21:30 +0100 Subject: [PATCH 3/3] Add fuzz tests for dispute-evidence edge cases --- Cargo.lock | 1 + contracts/escrow/Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index caf5d11c..201d648a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5876,6 +5876,7 @@ dependencies = [ "parity-scale-codec", "propchain-contracts", "propchain-traits", + "proptest", "scale-info", ] diff --git a/contracts/escrow/Cargo.toml b/contracts/escrow/Cargo.toml index 1838cebe..1657e400 100644 --- a/contracts/escrow/Cargo.toml +++ b/contracts/escrow/Cargo.toml @@ -20,6 +20,7 @@ propchain-contracts = { path = "../lib", default-features = false } [dev-dependencies] ink_e2e = "5.0.0" +proptest = { version = "1.0.0", default-features = false, features = ["std"] } [lib] name = "propchain_escrow" @@ -35,4 +36,4 @@ std = [ "propchain-contracts/std", ] ink-as-dependency = [] -e2e-tests = [] +e2e-tests = [] \ No newline at end of file