Skip to main content

tenzro_token/
erc3643.rs

1//! ERC-3643 T-REX Compliance Module
2//!
3//! Implements global dynamic compliance for tokenized assets (RWA/security tokens).
4//! Transfer rules are checked against on-chain identity claims at runtime.
5//! Integrates with Tenzro's TDIP identity system for KYC and accreditation verification.
6//!
7//! See: https://www.erc3643.org/
8
9use 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
17// ---------------------------------------------------------------------------
18// Claim topic constants
19// ---------------------------------------------------------------------------
20
21/// Claim topic: KYC verification.
22pub const CLAIM_TOPIC_KYC: u64 = 1;
23/// Claim topic: Accredited investor status.
24pub const CLAIM_TOPIC_ACCREDITED_INVESTOR: u64 = 2;
25/// Claim topic: Country of residence / tax domicile.
26pub const CLAIM_TOPIC_COUNTRY: u64 = 3;
27/// Claim topic: Qualified purchaser status.
28pub const CLAIM_TOPIC_QUALIFIED_PURCHASER: u64 = 4;
29
30// ---------------------------------------------------------------------------
31// Violation types
32// ---------------------------------------------------------------------------
33
34/// Reason why a transfer was rejected by the compliance module.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum ComplianceViolation {
37    /// Sender or receiver does not meet the minimum KYC tier.
38    InsufficientKyc {
39        address: String,
40        required_tier: u8,
41        actual_tier: Option<u8>,
42    },
43    /// Sender or receiver is not accredited.
44    NotAccredited { address: String },
45    /// The country of the sender or receiver is restricted.
46    CountryRestricted { address: String, country_code: u16 },
47    /// The address is frozen for this token.
48    AddressFrozen { address: String, reason: String },
49    /// The transfer amount exceeds the per-transfer limit.
50    AmountExceedsLimit { amount: u128, limit: u128 },
51    /// The holding period has not elapsed since the receiver acquired tokens.
52    HoldingPeriodNotMet {
53        address: String,
54        required_secs: u64,
55        elapsed_secs: u64,
56    },
57    /// The token has reached its maximum number of holders.
58    MaxHoldersReached { max: u64, current: u64 },
59    /// The address is not on the whitelist.
60    NotWhitelisted { address: String },
61    /// The token is globally paused.
62    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// ---------------------------------------------------------------------------
115// ComplianceCheckResult
116// ---------------------------------------------------------------------------
117
118/// Result of a `can_transfer` compliance check.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ComplianceCheckResult {
121    /// Whether the transfer is compliant.
122    pub compliant: bool,
123    /// List of violations found (empty when compliant).
124    pub violations: Vec<ComplianceViolation>,
125    /// Names of the rules that were evaluated.
126    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// ---------------------------------------------------------------------------
149// Domain types
150// ---------------------------------------------------------------------------
151
152/// Identity claim attached to an address (mirrors ERC-735 claim structure).
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct IdentityClaim {
155    /// Claim topic identifier (see `CLAIM_TOPIC_*` constants).
156    pub topic: u64,
157    /// Signature scheme (1 = ECDSA, 2 = RSA).
158    pub scheme: u64,
159    /// DID of the issuer that signed this claim.
160    pub issuer: String,
161    /// Cryptographic signature over the claim data.
162    pub signature: Vec<u8>,
163    /// Claim payload (topic-specific encoding).
164    pub data: Vec<u8>,
165    /// Optional URI for off-chain claim storage.
166    pub uri: String,
167    /// Unix timestamp from which the claim is valid.
168    pub valid_from: i64,
169    /// Unix timestamp at which the claim expires.
170    pub valid_to: i64,
171}
172
173impl IdentityClaim {
174    /// Returns `true` if the claim is currently within its validity window.
175    pub fn is_valid(&self, now: i64) -> bool {
176        now >= self.valid_from && now < self.valid_to
177    }
178
179    /// Extracts the KYC tier from the claim data (topic must be `CLAIM_TOPIC_KYC`).
180    /// The tier is stored as the first byte of `data`.
181    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    /// Extracts the ISO 3166-1 numeric country code from the claim data.
190    /// Stored as a little-endian u16 in the first two bytes.
191    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/// A trusted claim issuer registered with the compliance module.
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct TrustedIssuer {
203    /// DID of the issuer.
204    pub issuer_did: String,
205    /// Claim topics this issuer is trusted for.
206    pub trusted_topics: Vec<u64>,
207    /// Human-readable name.
208    pub name: String,
209    /// Whether this issuer is currently active.
210    pub enabled: bool,
211}
212
213/// Claim topic metadata.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ClaimTopicInfo {
216    pub topic_id: u64,
217    pub name: String,
218    pub description: String,
219}
220
221/// Whitelist entry for an address.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct WhitelistEntry {
224    /// Unix timestamp when the address was whitelisted.
225    pub added_at: i64,
226    /// Who added the entry (admin DID or address).
227    pub added_by: String,
228}
229
230/// Information about a frozen address.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct FreezeInfo {
233    /// Reason the address was frozen.
234    pub reason: String,
235    /// Unix timestamp when the freeze was applied.
236    pub frozen_at: i64,
237    /// Who froze the address.
238    pub frozen_by: String,
239}
240
241/// Event emitted when tokens are forcibly recovered.
242#[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/// Supply limits for a compliant token.
253#[derive(Debug, Clone, Default, Serialize, Deserialize)]
254pub struct SupplyLimits {
255    /// Maximum total supply that may be minted.
256    pub max_supply: Option<u128>,
257    /// Minimum tokens that must remain in circulation.
258    pub min_supply: Option<u128>,
259}
260
261/// Transfer restriction rules for a compliant token.
262#[derive(Debug, Clone)]
263pub struct TransferRestrictions {
264    /// Maximum amount per individual transfer.
265    pub max_transfer_amount: Option<u128>,
266    /// Minimum holding period (seconds) before tokens can be transferred out.
267    pub min_holding_period_secs: Option<u64>,
268    /// Whether only whitelisted addresses may hold the token.
269    pub require_whitelist: bool,
270    /// Per-token whitelist of addresses.
271    pub whitelist: DashMap<Address, WhitelistEntry>,
272    /// Whether country-of-residence checks are enforced.
273    pub country_check: bool,
274    /// Whether cross-border transfers are allowed.
275    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
291/// Full compliance rule set for a single token.
292pub struct ComplianceRules {
293    /// Token this rule set applies to.
294    pub token_id: TokenId,
295    /// Whether holders must have a valid KYC claim.
296    pub require_kyc: bool,
297    /// Minimum KYC tier required (maps to `tenzro_identity::KycTier`).
298    pub min_kyc_tier: u8,
299    /// Whether holders must be accredited investors.
300    pub require_accreditation: bool,
301    /// Maximum number of unique holders (Reg D / Reg S cap).
302    pub max_holders: Option<u64>,
303    /// Current unique holder count.
304    current_holders: AtomicU64,
305    /// Transfer restriction rules.
306    pub transfer_restrictions: TransferRestrictions,
307    /// Supply limits.
308    pub supply_limits: SupplyLimits,
309    /// Whether all transfers for this token are paused.
310    pub paused: bool,
311}
312
313impl ComplianceRules {
314    /// Creates a new rule set with sensible defaults (nothing enforced).
315    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    /// Builder: require KYC with minimum tier.
330    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    /// Builder: require accredited investor claim.
337    pub fn with_accreditation(mut self) -> Self {
338        self.require_accreditation = true;
339        self
340    }
341
342    /// Builder: cap the number of unique holders.
343    pub fn with_max_holders(mut self, max: u64) -> Self {
344        self.max_holders = Some(max);
345        self
346    }
347
348    /// Builder: set a per-transfer amount limit.
349    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    /// Builder: require whitelist.
355    pub fn with_whitelist(mut self) -> Self {
356        self.transfer_restrictions.require_whitelist = true;
357        self
358    }
359
360    /// Builder: enforce country checks.
361    pub fn with_country_check(mut self) -> Self {
362        self.transfer_restrictions.country_check = true;
363        self
364    }
365
366    /// Builder: set minimum holding period.
367    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    /// Returns the current holder count.
373    pub fn holder_count(&self) -> u64 {
374        self.current_holders.load(Ordering::Relaxed)
375    }
376
377    /// Increments the holder count by one. Returns the new count.
378    pub fn increment_holders(&self) -> u64 {
379        self.current_holders.fetch_add(1, Ordering::Relaxed) + 1
380    }
381
382    /// Decrements the holder count by one (saturating at zero).
383    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
392// ---------------------------------------------------------------------------
393// ComplianceRegistry
394// ---------------------------------------------------------------------------
395
396/// Central compliance registry for all ERC-3643-governed tokens.
397///
398/// This registry stores per-token compliance rules, identity claims for
399/// addresses, trusted issuers, frozen addresses, and country restrictions.
400/// The primary entry point is [`can_transfer`], which evaluates all applicable
401/// rules and returns a detailed [`ComplianceCheckResult`].
402pub struct ComplianceRegistry {
403    /// Per-token compliance rules.
404    token_compliance: DashMap<TokenId, ComplianceRules>,
405    /// Identity claims per address.
406    identity_claims: DashMap<Address, Vec<IdentityClaim>>,
407    /// Trusted claim issuers.
408    trusted_issuers: DashMap<String, TrustedIssuer>,
409    /// Claim topic metadata, governance-managed (`register_claim_topic` /
410    /// `list_claim_topics` / `get_claim_topic`).
411    claim_topics: DashMap<u64, ClaimTopicInfo>,
412    /// Frozen address set: (token_id, address) -> freeze info.
413    frozen_addresses: DashMap<(TokenId, Address), FreezeInfo>,
414    /// Country allowlist per token: (token_id, country_code) -> allowed.
415    country_restrictions: DashMap<(TokenId, u16), bool>,
416    /// Timestamp of first token receipt per (address, token_id) for holding
417    /// period enforcement.
418    first_receipt: DashMap<(Address, TokenId), i64>,
419    /// Token balances tracked by the compliance module (shadow of on-chain
420    /// state) for holder-count and holding-period tracking.
421    balances: DashMap<(Address, TokenId), u128>,
422    /// Recovery event log.
423    recovery_events: DashMap<u64, RecoveryEvent>,
424    /// Monotonic nonce for recovery events.
425    recovery_nonce: AtomicU64,
426}
427
428impl ComplianceRegistry {
429    /// Creates a new empty compliance registry.
430    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    /// Returns the current Unix timestamp in seconds.
446    fn now_secs() -> i64 {
447        chrono::Utc::now().timestamp()
448    }
449
450    // -- Rule management -----------------------------------------------------
451
452    /// Registers (or replaces) the compliance rules for a token.
453    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    /// Returns `true` if a compliance rule set is registered for the token.
462    pub fn has_compliance(&self, token_id: &TokenId) -> bool {
463        self.token_compliance.contains_key(token_id)
464    }
465
466    // -- Identity claims -----------------------------------------------------
467
468    /// Adds an identity claim for a given address.
469    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    /// Returns `true` if the address holds a valid claim for the given topic
481    /// issued by a trusted issuer.
482    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    /// Returns the best (highest) KYC tier that the address holds via a valid,
497    /// trusted claim.
498    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    /// Returns the country code from the address's valid country claim.
513    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    // -- Trusted issuers -----------------------------------------------------
528
529    /// Registers a trusted claim issuer.
530    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    /// Returns `true` if the issuer is trusted for the given claim topic.
540    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    // -- Claim topics --------------------------------------------------------
548
549    /// Registers (or replaces) metadata for a claim topic.
550    ///
551    /// ERC-3643 claim topics are governance-managed identifiers that
552    /// describe what a claim asserts (e.g. KYC tier, accredited investor
553    /// status, country of residence).
554    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    /// Returns metadata for a registered claim topic.
563    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    /// Lists all registered claim topics.
568    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    // -- Freeze / unfreeze ---------------------------------------------------
576
577    /// Freezes an address for a specific token.
578    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    /// Unfreezes an address for a specific token.
602    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    /// Returns `true` if the address is frozen for the given token.
612    pub fn is_frozen(&self, token_id: &TokenId, address: &Address) -> bool {
613        self.frozen_addresses.contains_key(&(*token_id, *address))
614    }
615
616    // -- Country restrictions ------------------------------------------------
617
618    /// Sets whether a country is allowed for a specific token.
619    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    /// Returns `true` if the address's country is allowed (or no country
631    /// restrictions are configured for the token).
632    pub fn check_country(&self, token_id: &TokenId, address: &Address) -> bool {
633        let country = match self.effective_country(address) {
634            Some(c) => c,
635            // No country claim -- the `can_transfer` caller decides via the
636            // ComplianceViolation whether this is acceptable.
637            None => return true,
638        };
639
640        self.country_restrictions
641            .get(&(*token_id, country))
642            .map(|v| *v)
643            .unwrap_or(true) // default: allowed when no explicit entry exists
644    }
645
646    // -- Whitelist -----------------------------------------------------------
647
648    /// Adds an address to the token's whitelist.
649    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    /// Removes an address from the token's whitelist.
674    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    // -- Balance shadow (for holder count / holding period) -------------------
691
692    /// Records a token receipt for holding-period and holder-count tracking.
693    /// Called by the settlement layer after a successful transfer.
694    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        // Update balance
708        self.balances
709            .entry(key)
710            .and_modify(|b| *b = b.saturating_add(amount))
711            .or_insert(amount);
712
713        // Track first receipt timestamp
714        if was_zero {
715            self.first_receipt
716                .entry((address, *token_id))
717                .or_insert_with(Self::now_secs);
718
719            // Increment holder count
720            if let Some(rules) = self.token_compliance.get(token_id) {
721                rules.increment_holders();
722            }
723        }
724    }
725
726    /// Records a token send for holder-count tracking.
727    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    // -- Forced recovery -----------------------------------------------------
752
753    /// Forcibly transfers tokens from one address to another for compliance
754    /// reasons (e.g., lost keys, court order).
755    ///
756    /// This bypasses normal compliance checks but logs a `RecoveryEvent`.
757    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        // Verify the `from` address has sufficient shadow balance.
772        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        // Move shadow balances
783        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    // -- Core compliance check -----------------------------------------------
810
811    /// The primary compliance gate. Evaluates all registered rules for the
812    /// given token and returns a detailed result.
813    ///
814    /// # Arguments
815    ///
816    /// * `token_id` - Token being transferred.
817    /// * `from` - Sender address.
818    /// * `to` - Receiver address.
819    /// * `amount` - Transfer amount (smallest unit).
820    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        // 1. Token paused check
837        result.add_rule("token_paused");
838        if rules.paused {
839            result.add_violation(ComplianceViolation::TokenPaused);
840        }
841
842        // 2. Frozen address check (both sender and receiver)
843        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        // 3. KYC check (both sender and receiver)
858        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 => { /* ok */ }
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        // 4. Accreditation check
878        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        // 5. Country restriction check
890        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        // 6. Transfer amount limit
911        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        // 7. Whitelist check
922        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        // 8. Holding period check (sender only)
934        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            // If no first_receipt, the sender has never received tokens, so
948            // the transfer will fail at the balance check anyway.
949        }
950
951        // 9. Max holders check (receiver only, if they don't already hold)
952        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                // This would be a new holder
958                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// ---------------------------------------------------------------------------
979// Tests
980// ---------------------------------------------------------------------------
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985    use crate::tnzo::ONE_TNZO;
986
987    /// Helper: a token ID for testing.
988    fn test_token() -> TokenId {
989        TokenId::new([0x42; 32])
990    }
991
992    /// Helper: creates a claim with the given topic, issuer, tier, and validity window.
993    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    /// Setup: registry with a token that requires KYC tier 2.
1042    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        // Both addresses have KYC tier 3 (satisfies tier 2 requirement)
1061        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        // `from` has tier 1 (below required 2)
1105        reg.add_identity_claim(from, make_kyc_claim("did:tenzro:human:issuer1", 1, now - 100, now + 100_000));
1106        // `to` has tier 2 (exactly meets requirement)
1107        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        // Unfreeze and retry
1131        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        // US = 840, restricted
1154        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)); // Germany
1156
1157        reg.set_country_restriction(token, 840, false); // US not allowed
1158        reg.set_country_restriction(token, 276, true);  // Germany allowed
1159
1160        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        // Under limit passes
1190        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        // Neither address is whitelisted
1207        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        // Whitelist both
1212        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        // Different topic, no claim
1234        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        // Add a claim from an untrusted issuer
1243        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        // No trusted issuers registered, so claim should not verify
1247        assert!(!reg.verify_claim(&addr, CLAIM_TOPIC_KYC));
1248
1249        // Register the issuer as trusted
1250        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        // Give `from` a shadow balance
1268        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        // Verify shadow balances updated
1279        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        // Record two existing holders
1302        reg.record_receipt(&token, addr1, 100);
1303        reg.record_receipt(&token, addr2, 100);
1304
1305        // Transfer to a third address (new holder) should fail
1306        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        // Transfer to an existing holder should succeed
1314        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        // Require 1-day holding period
1324        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        // Record receipt just now
1330        reg.record_receipt(&token, from, 1_000);
1331
1332        // Try to transfer immediately -- holding period not met
1333        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        // Manually backdate the first receipt to simulate time passing
1341        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        // Only `from` is accredited
1382        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        // `to` is not accredited
1387        assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::NotAccredited { .. })));
1388
1389        // Accredit `to` as well
1390        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}