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
8 changes: 8 additions & 0 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# cargo-mutants configuration file
# Ref: https://mutants.rs/

# Additional cargo args to pass to every cargo invocation
additional_cargo_args = ["--features", "std"]

# Only mutate the property-token contract file
examine_globs = ["contracts/property-token/src/lib.rs"]
33 changes: 33 additions & 0 deletions .github/workflows/nightly-mutation-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Nightly Mutation Test

on:
schedule:
# Run every night at 2:00 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:

env:
CARGO_TERM_COLOR: always

jobs:
mutation-test:
name: Mutation Testing (property-token)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- uses: Swatinem/rust-cache@v2

- name: Install cargo-mutants
run: |
mkdir -p ~/.local/bin
curl -L https://github.com/sourcefrog/cargo-mutants/releases/download/v24.5.0/cargo-mutants-v24.5.0-x86_64-unknown-linux-gnu.tar.gz | tar xz -C ~/.local/bin --strip-components=1
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Run cargo mutants
run: |
cargo mutants -p property-token --additional-cargo-arguments="--features std"
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,8 @@ plan.md

# Codex CLI artifacts
.codex

# cargo-mutants artifacts
/mutants.out/
/mutants.out.old/

8 changes: 8 additions & 0 deletions contracts/identity/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@ pub mod propchain_identity {
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum KycTier {
<<<<<<< feat/mutation-testing-issue-483
Tier0Unverified, // No KYC, basic access only
Tier1Basic, // Basic identity verification
Tier2Standard, // Standard KYC with document verification
Tier3Enhanced, // Enhanced due diligence
Tier4Premium, // Premium verification with full background check
=======
Tier0Unverified, // No KYC, basic access only
Tier1Basic, // Basic identity verification
Tier2Standard, // Standard KYC with document verification
Expand All @@ -234,6 +241,7 @@ pub mod propchain_identity {
Tier2_Standard, // Standard KYC with document verification
Tier3_Enhanced, // Enhanced due diligence
Tier4_Premium, // Premium verification with full background check
>>>>>>> main
}

/// KYC Tier privileges
Expand Down
171 changes: 2 additions & 169 deletions contracts/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,8 @@ pub mod propchain_contracts {
SelfTransferNotAllowed,
/// Range is invalid (min > max)
InvalidRange,
/// External dependency circuit breaker is open
ExternalDependencyUnavailable,
/// Reentrancy guard detected a reentrant call
ReentrantCall,
/// External dependency is temporarily unavailable because its circuit breaker is open
ExternalDependencyUnavailable,
}

impl From<crate::ReentrancyError> for Error {
Expand All @@ -124,91 +120,9 @@ pub mod propchain_contracts {
}
}

/// Dependency type for circuit breaker
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
scale::Encode,
scale::Decode,
ink::storage::traits::StorageLayout,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum ExternalDependency {
ComplianceRegistry,
IdentityRegistry,
FeeManager,
Oracle,
}

/// Circuit breaker state for external dependencies
#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
pub enum ExternalDependency {
Oracle,
ComplianceRegistry,
FeeManager,
IdentityRegistry,
PropertyManagement,
Bridge,
Insurance,
Governance,
}

#[derive(
Debug,
Clone,
PartialEq,
Eq,
Default,
scale::Encode,
scale::Decode,
scale::Encode,
scale::Decode,
Default,
ink::storage::traits::StorageLayout,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct CircuitBreakerState {
pub failure_count: u8,
pub total_failures: u32,
pub failure_count: u64,
pub total_failures: u64,
pub last_failure_at: Option<u64>,
pub open_until: Option<u64>,
}

/// Configuration for circuit breakers
#[derive(
Debug,
Clone,
PartialEq,
Eq,
scale::Encode,
scale::Decode,
Default,
ink::storage::traits::StorageLayout,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct CircuitBreakerConfig {
pub failure_threshold: u8,
pub cooldown_period_secs: u64,
}

impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 3,
cooldown_period_secs: 300, // 5 minutes default
}
}
}

pub failure_threshold: u64,
pub cooldown_period_secs: u64,
}

/// Property Registry contract
#[ink(storage)]
Expand Down Expand Up @@ -284,88 +198,11 @@ pub mod propchain_contracts {
/// Shared external call circuit breaker configuration.
external_call_config: CircuitBreakerConfig,

/// Circuit breakers for external calls
external_call_breakers: Mapping<ExternalDependency, CircuitBreakerState>,
/// Circuit breaker configuration
external_call_config: CircuitBreakerConfig,

/// Reentrancy protection guard
reentrancy_guard: ReentrancyGuard,
/// Circuit breaker configuration for external calls
external_call_config: CircuitBreakerConfig,
/// Circuit breaker states per external dependency
external_call_breakers: Mapping<ExternalDependency, CircuitBreakerState>,
}

#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
scale::Encode,
scale::Decode,
ink::storage::traits::StorageLayout,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum ExternalDependency {
FeeManager,
Oracle,
ComplianceRegistry,
IdentityRegistry,
}

#[derive(
Debug,
Clone,
PartialEq,
Eq,
scale::Encode,
scale::Decode,
ink::storage::traits::StorageLayout,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct CircuitBreakerState {
pub failure_count: u8,
pub total_failures: u64,
pub last_failure_at: Option<u64>,
pub open_until: Option<u64>,
}

impl Default for CircuitBreakerState {
fn default() -> Self {
Self {
failure_count: 0,
total_failures: 0,
last_failure_at: None,
open_until: None,
}
}
}

#[derive(
Debug,
Clone,
PartialEq,
Eq,
scale::Encode,
scale::Decode,
ink::storage::traits::StorageLayout,
)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct CircuitBreakerConfig {
pub failure_threshold: u8,
pub cooldown_period_secs: u64,
}

impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 3,
cooldown_period_secs: 300,
}
}
}

/// Escrow information
#[derive(
Expand Down Expand Up @@ -1515,8 +1352,6 @@ pub mod propchain_contracts {
cached_analytics: CachedAnalytics::default(),
load_metrics: LoadMetrics::default(),
reentrancy_guard: ReentrancyGuard::new(),
external_call_config: CircuitBreakerConfig::default(),
external_call_breakers: Mapping::default(),
};

// Emit contract initialization event
Expand Down Expand Up @@ -1816,11 +1651,11 @@ pub mod propchain_contracts {
if !self.ensure_admin_rbac() {
return Err(Error::Unauthorized);
}
if failure_threshold == 0 || cooldown_period_secs == 0 {
if failure_threshold == 0 || failure_threshold > 255 || cooldown_period_secs == 0 {
return Err(Error::ValueOutOfBounds);
}
self.external_call_config = CircuitBreakerConfig {
failure_threshold,
failure_threshold: failure_threshold as u8,
cooldown_period_secs,
};
Ok(())
Expand Down Expand Up @@ -1902,7 +1737,6 @@ pub mod propchain_contracts {
self.record_dependency_success(ExternalDependency::Oracle);
val
}
Ok(valuation) => valuation,
Err(_) => {
self.record_dependency_failure(ExternalDependency::Oracle);
return Err(Error::OracleError);
Expand All @@ -1915,7 +1749,6 @@ pub mod propchain_contracts {
self.properties.insert(&property_id, &property);
} else {
return Err(Error::PropertyNotFound);
Ok(())
}

self.record_dependency_success(ExternalDependency::Oracle);
Expand Down
52 changes: 11 additions & 41 deletions contracts/property-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ pub mod property_token {
/// Custom URI overrides for tokens
token_uris: Mapping<TokenId, String>,

// Staking state (Issue #197)
share_stakes: Mapping<(AccountId, TokenId), ShareStakeInfo>,
share_total_staked: Mapping<TokenId, u128>,
share_reward_pool: Mapping<TokenId, u128>,
share_reward_rate_bps: Mapping<TokenId, u128>,
share_acc_reward_per_share: Mapping<TokenId, u128>,
share_last_reward_block: Mapping<TokenId, u64>,

/// Reentrancy protection guard
reentrancy_guard: ReentrancyGuard,
/// Snapshot functionality for governance voting (Issue #194)
Expand Down Expand Up @@ -1470,46 +1478,6 @@ pub mod property_token {
.insert((to, token_id), &(to_balance.saturating_add(amount)));
Ok(())
})
if amount == 0 {
return Err(Error::InvalidAmount);
}
let caller = self.env().caller();
if caller != from && !self.is_approved_for_all(from, caller) {
return Err(Error::Unauthorized);
}
if !self.pass_compliance(from)? || !self.pass_compliance(to)? {
return Err(Error::ComplianceFailed);
}

// Check KYC-based transfer restrictions for share transfers
self.verify_kyc_transfer(&from, &to, token_id, amount)?;

let from_balance = self.balances.get((from, token_id)).unwrap_or(0);
if from_balance < amount {
return Err(Error::InsufficientBalance);
}

// Update user transfer quota tracking
let mut quota =
self.user_transfer_quotas
.get((token_id, from))
.unwrap_or(UserTransferQuota {
amount_transferred: 0,
period_start_block: self.env().block_number(),
acquisition_block: self.env().block_number(),
});

quota.amount_transferred = quota.amount_transferred.saturating_add(amount);
self.user_transfer_quotas.insert((token_id, from), &quota);

self.update_dividend_credit_on_change(from, token_id)?;
self.update_dividend_credit_on_change(to, token_id)?;
self.balances
.insert((from, token_id), &(from_balance.saturating_sub(amount)));
let to_balance = self.balances.get((to, token_id)).unwrap_or(0);
self.balances
.insert((to, token_id), &(to_balance.saturating_add(amount)));
Ok(())
}

/// Deposits dividends for distribution to all share holders of a token.
Expand Down Expand Up @@ -2317,7 +2285,7 @@ pub mod property_token {
};

// Check if period has expired and reset if needed
if current_block.saturating_sub(from_quota.period_start_block as u64)
if (current_block as u64).saturating_sub(from_quota.period_start_block as u64)
>= config.quota_period as u64
{
from_quota.amount_transferred = 0;
Expand Down Expand Up @@ -3851,4 +3819,6 @@ pub mod property_token {
}
}
}

include!("tests.rs");
}
Loading
Loading