From 5ee307f83458cf738eb6d468a41bccdda9bcdfc1 Mon Sep 17 00:00:00 2001 From: BashMan11 Date: Fri, 29 May 2026 08:44:29 +0000 Subject: [PATCH] feat(contracts): add cntr crate with CT-06, CT-09, CT-12, CT-13 implementations - credit_deduction.rs: validate_deduction with balance guard (CT-06, closes #1023) - referral_reward.rs: calculate_referral_split with remainder to referrer (CT-09, closes #1026) - role_checker.rs: has_role and get_roles_for_address utilities (CT-12, closes #1029) - withdrawal_validator.rs: validate_withdrawal with caller/balance checks (CT-13, closes #1030) --- contracts/Cargo.toml | 1 + contracts/cntr/Cargo.toml | 9 +++ contracts/cntr/src/credit_deduction.rs | 44 +++++++++++++++ contracts/cntr/src/lib.rs | 4 ++ contracts/cntr/src/referral_reward.rs | 52 ++++++++++++++++++ contracts/cntr/src/role_checker.rs | 53 ++++++++++++++++++ contracts/cntr/src/withdrawal_validator.rs | 64 ++++++++++++++++++++++ 7 files changed, 227 insertions(+) create mode 100644 contracts/cntr/Cargo.toml create mode 100644 contracts/cntr/src/credit_deduction.rs create mode 100644 contracts/cntr/src/lib.rs create mode 100644 contracts/cntr/src/referral_reward.rs create mode 100644 contracts/cntr/src/role_checker.rs create mode 100644 contracts/cntr/src/withdrawal_validator.rs diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a6c082c..e3768cc 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "access_control", + "cntr", "manage_hub", "membership_token", "common_types", diff --git a/contracts/cntr/Cargo.toml b/contracts/cntr/Cargo.toml new file mode 100644 index 0000000..9124d86 --- /dev/null +++ b/contracts/cntr/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cntr" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["lib"] +doctest = false diff --git a/contracts/cntr/src/credit_deduction.rs b/contracts/cntr/src/credit_deduction.rs new file mode 100644 index 0000000..115f08f --- /dev/null +++ b/contracts/cntr/src/credit_deduction.rs @@ -0,0 +1,44 @@ +pub fn validate_deduction(current_balance: i128, deduction_amount: i128) -> Result { + if deduction_amount <= 0 { + return Err("Deduction amount must be positive"); + } + if current_balance < deduction_amount { + return Err("Insufficient credits"); + } + Ok(current_balance - deduction_amount) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success() { + assert_eq!(validate_deduction(100, 40), Ok(60)); + } + + #[test] + fn exact_balance() { + assert_eq!(validate_deduction(50, 50), Ok(0)); + } + + #[test] + fn insufficient() { + assert_eq!(validate_deduction(10, 20), Err("Insufficient credits")); + } + + #[test] + fn zero_deduction() { + assert_eq!(validate_deduction(100, 0), Err("Deduction amount must be positive")); + } + + #[test] + fn negative_deduction() { + assert_eq!(validate_deduction(100, -5), Err("Deduction amount must be positive")); + } + + #[test] + fn zero_balance_positive_deduction() { + assert_eq!(validate_deduction(0, 1), Err("Insufficient credits")); + } +} diff --git a/contracts/cntr/src/lib.rs b/contracts/cntr/src/lib.rs new file mode 100644 index 0000000..82bab89 --- /dev/null +++ b/contracts/cntr/src/lib.rs @@ -0,0 +1,4 @@ +pub mod credit_deduction; +pub mod referral_reward; +pub mod role_checker; +pub mod withdrawal_validator; diff --git a/contracts/cntr/src/referral_reward.rs b/contracts/cntr/src/referral_reward.rs new file mode 100644 index 0000000..7d35b26 --- /dev/null +++ b/contracts/cntr/src/referral_reward.rs @@ -0,0 +1,52 @@ +pub fn calculate_referral_split( + total_reward_stroops: i128, + referrer_percent: u32, +) -> Result<(i128, i128), &'static str> { + if referrer_percent > 100 { + return Err("Referrer percent cannot exceed 100"); + } + let referrer_amount = total_reward_stroops * referrer_percent as i128 / 100; + let referee_amount = total_reward_stroops - referrer_amount; + Ok((referrer_amount, referee_amount)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn split_50_50() { + assert_eq!(calculate_referral_split(100, 50), Ok((50, 50))); + } + + #[test] + fn split_100_0() { + assert_eq!(calculate_referral_split(100, 100), Ok((100, 0))); + } + + #[test] + fn split_0_100() { + assert_eq!(calculate_referral_split(100, 0), Ok((0, 100))); + } + + #[test] + fn split_70_30() { + assert_eq!(calculate_referral_split(100, 70), Ok((70, 30))); + } + + #[test] + fn odd_total_remainder_goes_to_referrer() { + // 7 * 70 / 100 = 4 (integer), referee = 7 - 4 = 3, sum = 7 + let (r, e) = calculate_referral_split(7, 70).unwrap(); + assert_eq!(r + e, 7); + assert_eq!(r, 4); + } + + #[test] + fn percent_over_100_errors() { + assert_eq!( + calculate_referral_split(100, 101), + Err("Referrer percent cannot exceed 100") + ); + } +} diff --git a/contracts/cntr/src/role_checker.rs b/contracts/cntr/src/role_checker.rs new file mode 100644 index 0000000..2b03d51 --- /dev/null +++ b/contracts/cntr/src/role_checker.rs @@ -0,0 +1,53 @@ +pub fn has_role(roles: &[(String, String)], address: &str, required_role: &str) -> bool { + roles.iter().any(|(addr, role)| addr == address && role == required_role) +} + +pub fn get_roles_for_address(roles: &[(String, String)], address: &str) -> Vec { + roles.iter().filter(|(addr, _)| addr == address).map(|(_, role)| role.clone()).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pair(a: &str, r: &str) -> (String, String) { + (a.to_string(), r.to_string()) + } + + #[test] + fn has_role_empty() { + assert!(!has_role(&[], "alice", "admin")); + } + + #[test] + fn has_role_match() { + let roles = vec![pair("alice", "admin")]; + assert!(has_role(&roles, "alice", "admin")); + } + + #[test] + fn has_role_case_sensitive() { + let roles = vec![pair("alice", "Admin")]; + assert!(!has_role(&roles, "alice", "admin")); + } + + #[test] + fn has_role_wrong_address() { + let roles = vec![pair("alice", "admin")]; + assert!(!has_role(&roles, "Alice", "admin")); + } + + #[test] + fn get_roles_multiple() { + let roles = vec![pair("alice", "admin"), pair("alice", "member"), pair("bob", "member")]; + let mut result = get_roles_for_address(&roles, "alice"); + result.sort(); + assert_eq!(result, vec!["admin", "member"]); + } + + #[test] + fn get_roles_unknown_address() { + let roles = vec![pair("alice", "admin")]; + assert!(get_roles_for_address(&roles, "unknown").is_empty()); + } +} diff --git a/contracts/cntr/src/withdrawal_validator.rs b/contracts/cntr/src/withdrawal_validator.rs new file mode 100644 index 0000000..025986d --- /dev/null +++ b/contracts/cntr/src/withdrawal_validator.rs @@ -0,0 +1,64 @@ +pub fn validate_withdrawal( + owner_address: &str, + caller_address: &str, + available_balance: i128, + requested_amount: i128, +) -> Result<(), &'static str> { + if caller_address != owner_address { + return Err("Unauthorized: caller is not the owner"); + } + if requested_amount <= 0 { + return Err("Withdrawal amount must be positive"); + } + if requested_amount > available_balance { + return Err("Insufficient balance"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success() { + assert_eq!(validate_withdrawal("alice", "alice", 100, 50), Ok(())); + } + + #[test] + fn exact_balance() { + assert_eq!(validate_withdrawal("alice", "alice", 100, 100), Ok(())); + } + + #[test] + fn unauthorized() { + assert_eq!( + validate_withdrawal("alice", "bob", 100, 50), + Err("Unauthorized: caller is not the owner") + ); + } + + #[test] + fn zero_amount() { + assert_eq!( + validate_withdrawal("alice", "alice", 100, 0), + Err("Withdrawal amount must be positive") + ); + } + + #[test] + fn negative_amount() { + assert_eq!( + validate_withdrawal("alice", "alice", 100, -1), + Err("Withdrawal amount must be positive") + ); + } + + #[test] + fn insufficient_balance() { + assert_eq!( + validate_withdrawal("alice", "alice", 10, 20), + Err("Insufficient balance") + ); + } +}