diff --git a/Cargo.lock b/Cargo.lock index 7299a4c6..3f8cb4ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5877,6 +5877,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 diff --git a/contracts/escrow/src/errors.rs b/contracts/escrow/src/errors.rs index cae7c41d..33692e4e 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 { @@ -159,4 +161,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 04c8ec70..a61f9874 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 @@ -227,6 +229,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)] @@ -306,6 +320,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(), @@ -324,6 +340,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(), @@ -351,6 +368,40 @@ mod propchain_escrow { } } + /// 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( @@ -362,8 +413,10 @@ mod propchain_escrow { participants: Vec, required_signatures: u8, release_time_lock: Option, + deadline: u64, jurisdiction: Jurisdiction, ) -> Result { + self.when_not_paused()?; let caller = self.env().caller(); // Validate configuration @@ -390,6 +443,7 @@ mod propchain_escrow { created_at: self.env().block_timestamp(), completed_at: None, release_time_lock, + deadline, participants: participants.clone(), jurisdiction, total_released: 0, @@ -491,6 +545,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, @@ -501,6 +596,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)?; 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 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