1use crate::cross_vm::TokenId;
10use crate::error::{Result, TokenError};
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13use std::sync::atomic::{AtomicU64, Ordering};
14use tenzro_types::primitives::Address;
15use tracing::{debug, info, warn};
16
17pub const CLAIM_TOPIC_KYC: u64 = 1;
23pub const CLAIM_TOPIC_ACCREDITED_INVESTOR: u64 = 2;
25pub const CLAIM_TOPIC_COUNTRY: u64 = 3;
27pub const CLAIM_TOPIC_QUALIFIED_PURCHASER: u64 = 4;
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum ComplianceViolation {
37 InsufficientKyc {
39 address: String,
40 required_tier: u8,
41 actual_tier: Option<u8>,
42 },
43 NotAccredited { address: String },
45 CountryRestricted { address: String, country_code: u16 },
47 AddressFrozen { address: String, reason: String },
49 AmountExceedsLimit { amount: u128, limit: u128 },
51 HoldingPeriodNotMet {
53 address: String,
54 required_secs: u64,
55 elapsed_secs: u64,
56 },
57 MaxHoldersReached { max: u64, current: u64 },
59 NotWhitelisted { address: String },
61 TokenPaused,
63}
64
65impl std::fmt::Display for ComplianceViolation {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::InsufficientKyc {
69 address,
70 required_tier,
71 actual_tier,
72 } => write!(
73 f,
74 "Insufficient KYC for {}: required tier {}, actual {:?}",
75 address, required_tier, actual_tier
76 ),
77 Self::NotAccredited { address } => {
78 write!(f, "Address {} is not accredited", address)
79 }
80 Self::CountryRestricted {
81 address,
82 country_code,
83 } => write!(
84 f,
85 "Country {} restricted for address {}",
86 country_code, address
87 ),
88 Self::AddressFrozen { address, reason } => {
89 write!(f, "Address {} is frozen: {}", address, reason)
90 }
91 Self::AmountExceedsLimit { amount, limit } => {
92 write!(f, "Amount {} exceeds limit {}", amount, limit)
93 }
94 Self::HoldingPeriodNotMet {
95 address,
96 required_secs,
97 elapsed_secs,
98 } => write!(
99 f,
100 "Holding period not met for {}: required {}s, elapsed {}s",
101 address, required_secs, elapsed_secs
102 ),
103 Self::MaxHoldersReached { max, current } => {
104 write!(f, "Max holders reached: {} / {}", current, max)
105 }
106 Self::NotWhitelisted { address } => {
107 write!(f, "Address {} is not whitelisted", address)
108 }
109 Self::TokenPaused => write!(f, "Token transfers are paused"),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ComplianceCheckResult {
121 pub compliant: bool,
123 pub violations: Vec<ComplianceViolation>,
125 pub checked_rules: Vec<String>,
127}
128
129impl ComplianceCheckResult {
130 fn new() -> Self {
131 Self {
132 compliant: true,
133 violations: Vec::new(),
134 checked_rules: Vec::new(),
135 }
136 }
137
138 fn add_violation(&mut self, v: ComplianceViolation) {
139 self.compliant = false;
140 self.violations.push(v);
141 }
142
143 fn add_rule(&mut self, name: &str) {
144 self.checked_rules.push(name.to_string());
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct IdentityClaim {
155 pub topic: u64,
157 pub scheme: u64,
159 pub issuer: String,
161 pub signature: Vec<u8>,
163 pub data: Vec<u8>,
165 pub uri: String,
167 pub valid_from: i64,
169 pub valid_to: i64,
171}
172
173impl IdentityClaim {
174 pub fn is_valid(&self, now: i64) -> bool {
176 now >= self.valid_from && now < self.valid_to
177 }
178
179 pub fn kyc_tier(&self) -> Option<u8> {
182 if self.topic == CLAIM_TOPIC_KYC {
183 self.data.first().copied()
184 } else {
185 None
186 }
187 }
188
189 pub fn country_code(&self) -> Option<u16> {
192 if self.topic == CLAIM_TOPIC_COUNTRY && self.data.len() >= 2 {
193 Some(u16::from_le_bytes([self.data[0], self.data[1]]))
194 } else {
195 None
196 }
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct TrustedIssuer {
203 pub issuer_did: String,
205 pub trusted_topics: Vec<u64>,
207 pub name: String,
209 pub enabled: bool,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ClaimTopicInfo {
216 pub topic_id: u64,
217 pub name: String,
218 pub description: String,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct WhitelistEntry {
224 pub added_at: i64,
226 pub added_by: String,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct FreezeInfo {
233 pub reason: String,
235 pub frozen_at: i64,
237 pub frozen_by: String,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct RecoveryEvent {
244 pub token_id: TokenId,
245 pub from: Address,
246 pub to: Address,
247 pub amount: u128,
248 pub reason: String,
249 pub timestamp: i64,
250}
251
252#[derive(Debug, Clone, Default, Serialize, Deserialize)]
254pub struct SupplyLimits {
255 pub max_supply: Option<u128>,
257 pub min_supply: Option<u128>,
259}
260
261#[derive(Debug, Clone)]
263pub struct TransferRestrictions {
264 pub max_transfer_amount: Option<u128>,
266 pub min_holding_period_secs: Option<u64>,
268 pub require_whitelist: bool,
270 pub whitelist: DashMap<Address, WhitelistEntry>,
272 pub country_check: bool,
274 pub allow_cross_border: bool,
276}
277
278impl Default for TransferRestrictions {
279 fn default() -> Self {
280 Self {
281 max_transfer_amount: None,
282 min_holding_period_secs: None,
283 require_whitelist: false,
284 whitelist: DashMap::new(),
285 country_check: false,
286 allow_cross_border: true,
287 }
288 }
289}
290
291pub struct ComplianceRules {
293 pub token_id: TokenId,
295 pub require_kyc: bool,
297 pub min_kyc_tier: u8,
299 pub require_accreditation: bool,
301 pub max_holders: Option<u64>,
303 current_holders: AtomicU64,
305 pub transfer_restrictions: TransferRestrictions,
307 pub supply_limits: SupplyLimits,
309 pub paused: bool,
311}
312
313impl ComplianceRules {
314 pub fn new(token_id: TokenId) -> Self {
316 Self {
317 token_id,
318 require_kyc: false,
319 min_kyc_tier: 0,
320 require_accreditation: false,
321 max_holders: None,
322 current_holders: AtomicU64::new(0),
323 transfer_restrictions: TransferRestrictions::default(),
324 supply_limits: SupplyLimits::default(),
325 paused: false,
326 }
327 }
328
329 pub fn with_kyc(mut self, min_tier: u8) -> Self {
331 self.require_kyc = true;
332 self.min_kyc_tier = min_tier;
333 self
334 }
335
336 pub fn with_accreditation(mut self) -> Self {
338 self.require_accreditation = true;
339 self
340 }
341
342 pub fn with_max_holders(mut self, max: u64) -> Self {
344 self.max_holders = Some(max);
345 self
346 }
347
348 pub fn with_max_transfer_amount(mut self, max: u128) -> Self {
350 self.transfer_restrictions.max_transfer_amount = Some(max);
351 self
352 }
353
354 pub fn with_whitelist(mut self) -> Self {
356 self.transfer_restrictions.require_whitelist = true;
357 self
358 }
359
360 pub fn with_country_check(mut self) -> Self {
362 self.transfer_restrictions.country_check = true;
363 self
364 }
365
366 pub fn with_holding_period(mut self, secs: u64) -> Self {
368 self.transfer_restrictions.min_holding_period_secs = Some(secs);
369 self
370 }
371
372 pub fn holder_count(&self) -> u64 {
374 self.current_holders.load(Ordering::Relaxed)
375 }
376
377 pub fn increment_holders(&self) -> u64 {
379 self.current_holders.fetch_add(1, Ordering::Relaxed) + 1
380 }
381
382 pub fn decrement_holders(&self) {
384 let _ = self
385 .current_holders
386 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
387 Some(v.saturating_sub(1))
388 });
389 }
390}
391
392pub struct ComplianceRegistry {
403 token_compliance: DashMap<TokenId, ComplianceRules>,
405 identity_claims: DashMap<Address, Vec<IdentityClaim>>,
407 trusted_issuers: DashMap<String, TrustedIssuer>,
409 claim_topics: DashMap<u64, ClaimTopicInfo>,
412 frozen_addresses: DashMap<(TokenId, Address), FreezeInfo>,
414 country_restrictions: DashMap<(TokenId, u16), bool>,
416 first_receipt: DashMap<(Address, TokenId), i64>,
419 balances: DashMap<(Address, TokenId), u128>,
422 recovery_events: DashMap<u64, RecoveryEvent>,
424 recovery_nonce: AtomicU64,
426}
427
428impl ComplianceRegistry {
429 pub fn new() -> Self {
431 Self {
432 token_compliance: DashMap::new(),
433 identity_claims: DashMap::new(),
434 trusted_issuers: DashMap::new(),
435 claim_topics: DashMap::new(),
436 frozen_addresses: DashMap::new(),
437 country_restrictions: DashMap::new(),
438 first_receipt: DashMap::new(),
439 balances: DashMap::new(),
440 recovery_events: DashMap::new(),
441 recovery_nonce: AtomicU64::new(1),
442 }
443 }
444
445 fn now_secs() -> i64 {
447 chrono::Utc::now().timestamp()
448 }
449
450 pub fn register_compliance(&self, rules: ComplianceRules) {
454 info!(
455 "Registered compliance rules for token {}",
456 rules.token_id.to_hex()
457 );
458 self.token_compliance.insert(rules.token_id, rules);
459 }
460
461 pub fn has_compliance(&self, token_id: &TokenId) -> bool {
463 self.token_compliance.contains_key(token_id)
464 }
465
466 pub fn add_identity_claim(&self, address: Address, claim: IdentityClaim) {
470 debug!(
471 "Adding claim topic {} for address {} from issuer {}",
472 claim.topic, address, claim.issuer
473 );
474 self.identity_claims
475 .entry(address)
476 .or_default()
477 .push(claim);
478 }
479
480 pub fn verify_claim(&self, address: &Address, topic: u64) -> bool {
483 let now = Self::now_secs();
484 let claims = match self.identity_claims.get(address) {
485 Some(c) => c,
486 None => return false,
487 };
488
489 claims.iter().any(|c| {
490 c.topic == topic
491 && c.is_valid(now)
492 && self.is_trusted_issuer_for_topic(&c.issuer, topic)
493 })
494 }
495
496 fn effective_kyc_tier(&self, address: &Address) -> Option<u8> {
499 let now = Self::now_secs();
500 let claims = self.identity_claims.get(address)?;
501 claims
502 .iter()
503 .filter(|c| {
504 c.topic == CLAIM_TOPIC_KYC
505 && c.is_valid(now)
506 && self.is_trusted_issuer_for_topic(&c.issuer, CLAIM_TOPIC_KYC)
507 })
508 .filter_map(|c| c.kyc_tier())
509 .max()
510 }
511
512 fn effective_country(&self, address: &Address) -> Option<u16> {
514 let now = Self::now_secs();
515 let claims = self.identity_claims.get(address)?;
516 claims
517 .iter()
518 .filter(|c| {
519 c.topic == CLAIM_TOPIC_COUNTRY
520 && c.is_valid(now)
521 && self.is_trusted_issuer_for_topic(&c.issuer, CLAIM_TOPIC_COUNTRY)
522 })
523 .filter_map(|c| c.country_code())
524 .next()
525 }
526
527 pub fn add_trusted_issuer(&self, issuer: TrustedIssuer) {
531 info!(
532 "Added trusted issuer {} ({}) for topics {:?}",
533 issuer.issuer_did, issuer.name, issuer.trusted_topics
534 );
535 self.trusted_issuers
536 .insert(issuer.issuer_did.clone(), issuer);
537 }
538
539 fn is_trusted_issuer_for_topic(&self, issuer_did: &str, topic: u64) -> bool {
541 self.trusted_issuers
542 .get(issuer_did)
543 .map(|i| i.enabled && i.trusted_topics.contains(&topic))
544 .unwrap_or(false)
545 }
546
547 pub fn register_claim_topic(&self, info: ClaimTopicInfo) {
555 info!(
556 "Registered claim topic {}: {}",
557 info.topic_id, info.name
558 );
559 self.claim_topics.insert(info.topic_id, info);
560 }
561
562 pub fn get_claim_topic(&self, topic_id: u64) -> Option<ClaimTopicInfo> {
564 self.claim_topics.get(&topic_id).map(|e| e.clone())
565 }
566
567 pub fn list_claim_topics(&self) -> Vec<ClaimTopicInfo> {
569 self.claim_topics
570 .iter()
571 .map(|e| e.value().clone())
572 .collect()
573 }
574
575 pub fn freeze_address(
579 &self,
580 token_id: TokenId,
581 address: Address,
582 reason: String,
583 frozen_by: String,
584 ) {
585 info!(
586 "Freezing address {} for token {}: {}",
587 address,
588 token_id.to_hex(),
589 reason
590 );
591 self.frozen_addresses.insert(
592 (token_id, address),
593 FreezeInfo {
594 reason,
595 frozen_at: Self::now_secs(),
596 frozen_by,
597 },
598 );
599 }
600
601 pub fn unfreeze_address(&self, token_id: &TokenId, address: &Address) {
603 info!(
604 "Unfreezing address {} for token {}",
605 address,
606 token_id.to_hex()
607 );
608 self.frozen_addresses.remove(&(*token_id, *address));
609 }
610
611 pub fn is_frozen(&self, token_id: &TokenId, address: &Address) -> bool {
613 self.frozen_addresses.contains_key(&(*token_id, *address))
614 }
615
616 pub fn set_country_restriction(&self, token_id: TokenId, country_code: u16, allowed: bool) {
620 debug!(
621 "Setting country {} {} for token {}",
622 country_code,
623 if allowed { "allowed" } else { "restricted" },
624 token_id.to_hex()
625 );
626 self.country_restrictions
627 .insert((token_id, country_code), allowed);
628 }
629
630 pub fn check_country(&self, token_id: &TokenId, address: &Address) -> bool {
633 let country = match self.effective_country(address) {
634 Some(c) => c,
635 None => return true,
638 };
639
640 self.country_restrictions
641 .get(&(*token_id, country))
642 .map(|v| *v)
643 .unwrap_or(true) }
645
646 pub fn add_to_whitelist(
650 &self,
651 token_id: &TokenId,
652 address: Address,
653 added_by: String,
654 ) -> Result<()> {
655 let entry = self
656 .token_compliance
657 .get(token_id)
658 .ok_or_else(|| TokenError::AssetNotFound {
659 asset_id: token_id.to_hex(),
660 })?;
661
662 entry.value().transfer_restrictions.whitelist.insert(
663 address,
664 WhitelistEntry {
665 added_at: Self::now_secs(),
666 added_by,
667 },
668 );
669 debug!("Whitelisted {} for token {}", address, token_id.to_hex());
670 Ok(())
671 }
672
673 pub fn remove_from_whitelist(
675 &self,
676 token_id: &TokenId,
677 address: &Address,
678 ) -> Result<()> {
679 let entry = self
680 .token_compliance
681 .get(token_id)
682 .ok_or_else(|| TokenError::AssetNotFound {
683 asset_id: token_id.to_hex(),
684 })?;
685
686 entry.value().transfer_restrictions.whitelist.remove(address);
687 Ok(())
688 }
689
690 pub fn record_receipt(
695 &self,
696 token_id: &TokenId,
697 address: Address,
698 amount: u128,
699 ) {
700 let key = (address, *token_id);
701 let was_zero = self
702 .balances
703 .get(&key)
704 .map(|v| *v == 0)
705 .unwrap_or(true);
706
707 self.balances
709 .entry(key)
710 .and_modify(|b| *b = b.saturating_add(amount))
711 .or_insert(amount);
712
713 if was_zero {
715 self.first_receipt
716 .entry((address, *token_id))
717 .or_insert_with(Self::now_secs);
718
719 if let Some(rules) = self.token_compliance.get(token_id) {
721 rules.increment_holders();
722 }
723 }
724 }
725
726 pub fn record_send(
728 &self,
729 token_id: &TokenId,
730 address: Address,
731 amount: u128,
732 ) {
733 let key = (address, *token_id);
734 let mut became_zero = false;
735
736 self.balances.entry(key).and_modify(|b| {
737 *b = b.saturating_sub(amount);
738 if *b == 0 {
739 became_zero = true;
740 }
741 });
742
743 if became_zero {
744 self.first_receipt.remove(&(address, *token_id));
745 if let Some(rules) = self.token_compliance.get(token_id) {
746 rules.decrement_holders();
747 }
748 }
749 }
750
751 pub fn recover_tokens(
758 &self,
759 token_id: TokenId,
760 from: Address,
761 to: Address,
762 amount: u128,
763 reason: String,
764 ) -> Result<RecoveryEvent> {
765 if amount == 0 {
766 return Err(TokenError::InvalidAmount(
767 "Recovery amount must be greater than zero".to_string(),
768 ));
769 }
770
771 let from_key = (from, token_id);
773 let from_balance = self.balances.get(&from_key).map(|v| *v).unwrap_or(0);
774
775 if from_balance < amount {
776 return Err(TokenError::InsufficientBalance {
777 required: amount,
778 available: from_balance,
779 });
780 }
781
782 self.record_send(&token_id, from, amount);
784 self.record_receipt(&token_id, to, amount);
785
786 let nonce = self.recovery_nonce.fetch_add(1, Ordering::Relaxed);
787 let event = RecoveryEvent {
788 token_id,
789 from,
790 to,
791 amount,
792 reason: reason.clone(),
793 timestamp: Self::now_secs(),
794 };
795
796 warn!(
797 "RECOVERY: {} tokens of {} transferred from {} to {}: {}",
798 amount,
799 token_id.to_hex(),
800 from,
801 to,
802 reason
803 );
804
805 self.recovery_events.insert(nonce, event.clone());
806 Ok(event)
807 }
808
809 pub fn can_transfer(
821 &self,
822 token_id: &TokenId,
823 from: &Address,
824 to: &Address,
825 amount: u128,
826 ) -> Result<ComplianceCheckResult> {
827 let rules = self
828 .token_compliance
829 .get(token_id)
830 .ok_or_else(|| TokenError::AssetNotFound {
831 asset_id: token_id.to_hex(),
832 })?;
833
834 let mut result = ComplianceCheckResult::new();
835
836 result.add_rule("token_paused");
838 if rules.paused {
839 result.add_violation(ComplianceViolation::TokenPaused);
840 }
841
842 result.add_rule("frozen_address");
844 if let Some(info) = self.frozen_addresses.get(&(*token_id, *from)) {
845 result.add_violation(ComplianceViolation::AddressFrozen {
846 address: from.to_string(),
847 reason: info.reason.clone(),
848 });
849 }
850 if let Some(info) = self.frozen_addresses.get(&(*token_id, *to)) {
851 result.add_violation(ComplianceViolation::AddressFrozen {
852 address: to.to_string(),
853 reason: info.reason.clone(),
854 });
855 }
856
857 if rules.require_kyc {
859 result.add_rule("kyc_tier");
860 let min_tier = rules.min_kyc_tier;
861
862 for addr in [from, to] {
863 let tier = self.effective_kyc_tier(addr);
864 match tier {
865 Some(t) if t >= min_tier => { }
866 _ => {
867 result.add_violation(ComplianceViolation::InsufficientKyc {
868 address: addr.to_string(),
869 required_tier: min_tier,
870 actual_tier: tier,
871 });
872 }
873 }
874 }
875 }
876
877 if rules.require_accreditation {
879 result.add_rule("accreditation");
880 for addr in [from, to] {
881 if !self.verify_claim(addr, CLAIM_TOPIC_ACCREDITED_INVESTOR) {
882 result.add_violation(ComplianceViolation::NotAccredited {
883 address: addr.to_string(),
884 });
885 }
886 }
887 }
888
889 if rules.transfer_restrictions.country_check {
891 result.add_rule("country_restriction");
892 for addr in [from, to] {
893 if let Some(country) = self.effective_country(addr) {
894 let allowed = self
895 .country_restrictions
896 .get(&(*token_id, country))
897 .map(|v| *v)
898 .unwrap_or(true);
899
900 if !allowed {
901 result.add_violation(ComplianceViolation::CountryRestricted {
902 address: addr.to_string(),
903 country_code: country,
904 });
905 }
906 }
907 }
908 }
909
910 if let Some(max) = rules.transfer_restrictions.max_transfer_amount {
912 result.add_rule("max_transfer_amount");
913 if amount > max {
914 result.add_violation(ComplianceViolation::AmountExceedsLimit {
915 amount,
916 limit: max,
917 });
918 }
919 }
920
921 if rules.transfer_restrictions.require_whitelist {
923 result.add_rule("whitelist");
924 for addr in [from, to] {
925 if !rules.transfer_restrictions.whitelist.contains_key(addr) {
926 result.add_violation(ComplianceViolation::NotWhitelisted {
927 address: addr.to_string(),
928 });
929 }
930 }
931 }
932
933 if let Some(min_secs) = rules.transfer_restrictions.min_holding_period_secs {
935 result.add_rule("holding_period");
936 let now = Self::now_secs();
937 if let Some(first) = self.first_receipt.get(&(*from, *token_id)) {
938 let elapsed = (now - *first).max(0) as u64;
939 if elapsed < min_secs {
940 result.add_violation(ComplianceViolation::HoldingPeriodNotMet {
941 address: from.to_string(),
942 required_secs: min_secs,
943 elapsed_secs: elapsed,
944 });
945 }
946 }
947 }
950
951 if let Some(max) = rules.max_holders {
953 result.add_rule("max_holders");
954 let to_key = (*to, *token_id);
955 let to_balance = self.balances.get(&to_key).map(|v| *v).unwrap_or(0);
956 if to_balance == 0 {
957 let current = rules.holder_count();
959 if current >= max {
960 result.add_violation(ComplianceViolation::MaxHoldersReached {
961 max,
962 current,
963 });
964 }
965 }
966 }
967
968 Ok(result)
969 }
970}
971
972impl Default for ComplianceRegistry {
973 fn default() -> Self {
974 Self::new()
975 }
976}
977
978#[cfg(test)]
983mod tests {
984 use super::*;
985 use crate::tnzo::ONE_TNZO;
986
987 fn test_token() -> TokenId {
989 TokenId::new([0x42; 32])
990 }
991
992 fn make_kyc_claim(issuer: &str, tier: u8, valid_from: i64, valid_to: i64) -> IdentityClaim {
994 IdentityClaim {
995 topic: CLAIM_TOPIC_KYC,
996 scheme: 1,
997 issuer: issuer.to_string(),
998 signature: vec![0xAA; 64],
999 data: vec![tier],
1000 uri: String::new(),
1001 valid_from,
1002 valid_to,
1003 }
1004 }
1005
1006 fn make_accreditation_claim(issuer: &str, valid_from: i64, valid_to: i64) -> IdentityClaim {
1007 IdentityClaim {
1008 topic: CLAIM_TOPIC_ACCREDITED_INVESTOR,
1009 scheme: 1,
1010 issuer: issuer.to_string(),
1011 signature: vec![0xBB; 64],
1012 data: vec![1],
1013 uri: String::new(),
1014 valid_from,
1015 valid_to,
1016 }
1017 }
1018
1019 fn make_country_claim(issuer: &str, country: u16, valid_from: i64, valid_to: i64) -> IdentityClaim {
1020 IdentityClaim {
1021 topic: CLAIM_TOPIC_COUNTRY,
1022 scheme: 1,
1023 issuer: issuer.to_string(),
1024 signature: vec![0xCC; 64],
1025 data: country.to_le_bytes().to_vec(),
1026 uri: String::new(),
1027 valid_from,
1028 valid_to,
1029 }
1030 }
1031
1032 fn trusted_issuer(did: &str, topics: Vec<u64>) -> TrustedIssuer {
1033 TrustedIssuer {
1034 issuer_did: did.to_string(),
1035 trusted_topics: topics,
1036 name: "TestIssuer".to_string(),
1037 enabled: true,
1038 }
1039 }
1040
1041 fn setup_kyc_registry() -> (ComplianceRegistry, Address, Address) {
1043 let reg = ComplianceRegistry::new();
1044 let token = test_token();
1045
1046 reg.register_compliance(
1047 ComplianceRules::new(token).with_kyc(2),
1048 );
1049
1050 reg.add_trusted_issuer(trusted_issuer(
1051 "did:tenzro:human:issuer1",
1052 vec![CLAIM_TOPIC_KYC, CLAIM_TOPIC_ACCREDITED_INVESTOR, CLAIM_TOPIC_COUNTRY],
1053 ));
1054
1055 let from = Address::new([0x01; 32]);
1056 let to = Address::new([0x02; 32]);
1057
1058 let now = chrono::Utc::now().timestamp();
1059
1060 reg.add_identity_claim(from, make_kyc_claim("did:tenzro:human:issuer1", 3, now - 1000, now + 100_000));
1062 reg.add_identity_claim(to, make_kyc_claim("did:tenzro:human:issuer1", 3, now - 1000, now + 100_000));
1063
1064 (reg, from, to)
1065 }
1066
1067 #[test]
1068 fn test_register_compliance() {
1069 let reg = ComplianceRegistry::new();
1070 let token = test_token();
1071 assert!(!reg.has_compliance(&token));
1072
1073 reg.register_compliance(ComplianceRules::new(token));
1074 assert!(reg.has_compliance(&token));
1075 }
1076
1077 #[test]
1078 fn test_can_transfer_passes() {
1079 let (reg, from, to) = setup_kyc_registry();
1080 let token = test_token();
1081
1082 let result = reg.can_transfer(&token, &from, &to, 100 * ONE_TNZO).unwrap();
1083 assert!(result.compliant);
1084 assert!(result.violations.is_empty());
1085 assert!(result.checked_rules.contains(&"kyc_tier".to_string()));
1086 }
1087
1088 #[test]
1089 fn test_insufficient_kyc_blocks() {
1090 let reg = ComplianceRegistry::new();
1091 let token = test_token();
1092
1093 reg.register_compliance(ComplianceRules::new(token).with_kyc(2));
1094 reg.add_trusted_issuer(trusted_issuer(
1095 "did:tenzro:human:issuer1",
1096 vec![CLAIM_TOPIC_KYC],
1097 ));
1098
1099 let from = Address::new([0x01; 32]);
1100 let to = Address::new([0x02; 32]);
1101
1102 let now = chrono::Utc::now().timestamp();
1103
1104 reg.add_identity_claim(from, make_kyc_claim("did:tenzro:human:issuer1", 1, now - 100, now + 100_000));
1106 reg.add_identity_claim(to, make_kyc_claim("did:tenzro:human:issuer1", 2, now - 100, now + 100_000));
1108
1109 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1110 assert!(!result.compliant);
1111 assert_eq!(result.violations.len(), 1);
1112 assert!(matches!(
1113 &result.violations[0],
1114 ComplianceViolation::InsufficientKyc { actual_tier: Some(1), .. }
1115 ));
1116 }
1117
1118 #[test]
1119 fn test_frozen_address_blocks() {
1120 let (reg, from, to) = setup_kyc_registry();
1121 let token = test_token();
1122
1123 reg.freeze_address(token, from, "Suspicious activity".to_string(), "admin".to_string());
1124 assert!(reg.is_frozen(&token, &from));
1125
1126 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1127 assert!(!result.compliant);
1128 assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::AddressFrozen { .. })));
1129
1130 reg.unfreeze_address(&token, &from);
1132 assert!(!reg.is_frozen(&token, &from));
1133 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1134 assert!(result.compliant);
1135 }
1136
1137 #[test]
1138 fn test_country_restriction_blocks() {
1139 let reg = ComplianceRegistry::new();
1140 let token = test_token();
1141
1142 reg.register_compliance(ComplianceRules::new(token).with_country_check());
1143 reg.add_trusted_issuer(trusted_issuer(
1144 "did:tenzro:human:issuer1",
1145 vec![CLAIM_TOPIC_COUNTRY],
1146 ));
1147
1148 let from = Address::new([0x01; 32]);
1149 let to = Address::new([0x02; 32]);
1150
1151 let now = chrono::Utc::now().timestamp();
1152
1153 reg.add_identity_claim(from, make_country_claim("did:tenzro:human:issuer1", 840, now - 100, now + 100_000));
1155 reg.add_identity_claim(to, make_country_claim("did:tenzro:human:issuer1", 276, now - 100, now + 100_000)); reg.set_country_restriction(token, 840, false); reg.set_country_restriction(token, 276, true); let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1161 assert!(!result.compliant);
1162 assert!(result.violations.iter().any(|v| matches!(
1163 v,
1164 ComplianceViolation::CountryRestricted { country_code: 840, .. }
1165 )));
1166 }
1167
1168 #[test]
1169 fn test_amount_limit_blocks() {
1170 let reg = ComplianceRegistry::new();
1171 let token = test_token();
1172
1173 reg.register_compliance(
1174 ComplianceRules::new(token).with_max_transfer_amount(1_000 * ONE_TNZO),
1175 );
1176
1177 let from = Address::new([0x01; 32]);
1178 let to = Address::new([0x02; 32]);
1179
1180 let result = reg
1181 .can_transfer(&token, &from, &to, 2_000 * ONE_TNZO)
1182 .unwrap();
1183 assert!(!result.compliant);
1184 assert!(result.violations.iter().any(|v| matches!(
1185 v,
1186 ComplianceViolation::AmountExceedsLimit { .. }
1187 )));
1188
1189 let result = reg
1191 .can_transfer(&token, &from, &to, 500 * ONE_TNZO)
1192 .unwrap();
1193 assert!(result.compliant);
1194 }
1195
1196 #[test]
1197 fn test_whitelist_enforcement() {
1198 let reg = ComplianceRegistry::new();
1199 let token = test_token();
1200
1201 reg.register_compliance(ComplianceRules::new(token).with_whitelist());
1202
1203 let from = Address::new([0x01; 32]);
1204 let to = Address::new([0x02; 32]);
1205
1206 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1208 assert!(!result.compliant);
1209 assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::NotWhitelisted { .. })));
1210
1211 reg.add_to_whitelist(&token, from, "admin".to_string()).unwrap();
1213 reg.add_to_whitelist(&token, to, "admin".to_string()).unwrap();
1214
1215 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1216 assert!(result.compliant);
1217 }
1218
1219 #[test]
1220 fn test_claim_verification() {
1221 let reg = ComplianceRegistry::new();
1222 let addr = Address::new([0x01; 32]);
1223
1224 reg.add_trusted_issuer(trusted_issuer(
1225 "did:tenzro:human:issuer1",
1226 vec![CLAIM_TOPIC_KYC],
1227 ));
1228
1229 let now = chrono::Utc::now().timestamp();
1230 reg.add_identity_claim(addr, make_kyc_claim("did:tenzro:human:issuer1", 2, now - 100, now + 100_000));
1231
1232 assert!(reg.verify_claim(&addr, CLAIM_TOPIC_KYC));
1233 assert!(!reg.verify_claim(&addr, CLAIM_TOPIC_ACCREDITED_INVESTOR));
1235 }
1236
1237 #[test]
1238 fn test_trusted_issuer_check() {
1239 let reg = ComplianceRegistry::new();
1240 let addr = Address::new([0x01; 32]);
1241
1242 let now = chrono::Utc::now().timestamp();
1244 reg.add_identity_claim(addr, make_kyc_claim("did:tenzro:human:untrusted", 3, now - 100, now + 100_000));
1245
1246 assert!(!reg.verify_claim(&addr, CLAIM_TOPIC_KYC));
1248
1249 reg.add_trusted_issuer(trusted_issuer(
1251 "did:tenzro:human:untrusted",
1252 vec![CLAIM_TOPIC_KYC],
1253 ));
1254 assert!(reg.verify_claim(&addr, CLAIM_TOPIC_KYC));
1255 }
1256
1257 #[test]
1258 fn test_forced_recovery() {
1259 let reg = ComplianceRegistry::new();
1260 let token = test_token();
1261
1262 reg.register_compliance(ComplianceRules::new(token));
1263
1264 let from = Address::new([0x01; 32]);
1265 let to = Address::new([0x02; 32]);
1266
1267 reg.record_receipt(&token, from, 1_000);
1269
1270 let event = reg
1271 .recover_tokens(token, from, to, 500, "Court order #123".to_string())
1272 .unwrap();
1273
1274 assert_eq!(event.amount, 500);
1275 assert_eq!(event.from, from);
1276 assert_eq!(event.to, to);
1277
1278 assert_eq!(
1280 reg.balances.get(&(from, token)).map(|v| *v).unwrap_or(0),
1281 500
1282 );
1283 assert_eq!(
1284 reg.balances.get(&(to, token)).map(|v| *v).unwrap_or(0),
1285 500
1286 );
1287 }
1288
1289 #[test]
1290 fn test_max_holders_limit() {
1291 let reg = ComplianceRegistry::new();
1292 let token = test_token();
1293
1294 reg.register_compliance(ComplianceRules::new(token).with_max_holders(2));
1295
1296 let addr1 = Address::new([0x01; 32]);
1297 let addr2 = Address::new([0x02; 32]);
1298 let addr3 = Address::new([0x03; 32]);
1299 let _addr4 = Address::new([0x04; 32]);
1300
1301 reg.record_receipt(&token, addr1, 100);
1303 reg.record_receipt(&token, addr2, 100);
1304
1305 let result = reg.can_transfer(&token, &addr1, &addr3, 50).unwrap();
1307 assert!(!result.compliant);
1308 assert!(result.violations.iter().any(|v| matches!(
1309 v,
1310 ComplianceViolation::MaxHoldersReached { max: 2, current: 2 }
1311 )));
1312
1313 let result = reg.can_transfer(&token, &addr1, &addr2, 50).unwrap();
1315 assert!(result.compliant);
1316 }
1317
1318 #[test]
1319 fn test_holding_period_check() {
1320 let reg = ComplianceRegistry::new();
1321 let token = test_token();
1322
1323 reg.register_compliance(ComplianceRules::new(token).with_holding_period(86_400));
1325
1326 let from = Address::new([0x01; 32]);
1327 let to = Address::new([0x02; 32]);
1328
1329 reg.record_receipt(&token, from, 1_000);
1331
1332 let result = reg.can_transfer(&token, &from, &to, 500).unwrap();
1334 assert!(!result.compliant);
1335 assert!(result.violations.iter().any(|v| matches!(
1336 v,
1337 ComplianceViolation::HoldingPeriodNotMet { .. }
1338 )));
1339
1340 let old_ts = chrono::Utc::now().timestamp() - 100_000;
1342 reg.first_receipt.insert((from, token), old_ts);
1343
1344 let result = reg.can_transfer(&token, &from, &to, 500).unwrap();
1345 assert!(result.compliant);
1346 }
1347
1348 #[test]
1349 fn test_token_paused_blocks() {
1350 let reg = ComplianceRegistry::new();
1351 let token = test_token();
1352
1353 let mut rules = ComplianceRules::new(token);
1354 rules.paused = true;
1355 reg.register_compliance(rules);
1356
1357 let from = Address::new([0x01; 32]);
1358 let to = Address::new([0x02; 32]);
1359
1360 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1361 assert!(!result.compliant);
1362 assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::TokenPaused)));
1363 }
1364
1365 #[test]
1366 fn test_accreditation_check() {
1367 let reg = ComplianceRegistry::new();
1368 let token = test_token();
1369
1370 reg.register_compliance(ComplianceRules::new(token).with_accreditation());
1371 reg.add_trusted_issuer(trusted_issuer(
1372 "did:tenzro:human:issuer1",
1373 vec![CLAIM_TOPIC_ACCREDITED_INVESTOR],
1374 ));
1375
1376 let from = Address::new([0x01; 32]);
1377 let to = Address::new([0x02; 32]);
1378
1379 let now = chrono::Utc::now().timestamp();
1380
1381 reg.add_identity_claim(from, make_accreditation_claim("did:tenzro:human:issuer1", now - 100, now + 100_000));
1383
1384 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1385 assert!(!result.compliant);
1386 assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::NotAccredited { .. })));
1388
1389 reg.add_identity_claim(to, make_accreditation_claim("did:tenzro:human:issuer1", now - 100, now + 100_000));
1391
1392 let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
1393 assert!(result.compliant);
1394 }
1395
1396 #[test]
1397 fn test_recovery_fails_insufficient_balance() {
1398 let reg = ComplianceRegistry::new();
1399 let token = test_token();
1400
1401 let from = Address::new([0x01; 32]);
1402 let to = Address::new([0x02; 32]);
1403
1404 let result = reg.recover_tokens(token, from, to, 100, "test".to_string());
1405 assert!(result.is_err());
1406 let err = result.unwrap_err().to_string();
1407 assert!(err.contains("Insufficient balance"));
1408 }
1409}