use crate::cross_vm::TokenId;
use crate::error::{Result, TokenError};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};
use tenzro_types::primitives::Address;
use tracing::{debug, info, warn};
pub const CLAIM_TOPIC_KYC: u64 = 1;
pub const CLAIM_TOPIC_ACCREDITED_INVESTOR: u64 = 2;
pub const CLAIM_TOPIC_COUNTRY: u64 = 3;
pub const CLAIM_TOPIC_QUALIFIED_PURCHASER: u64 = 4;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComplianceViolation {
InsufficientKyc {
address: String,
required_tier: u8,
actual_tier: Option<u8>,
},
NotAccredited { address: String },
CountryRestricted { address: String, country_code: u16 },
AddressFrozen { address: String, reason: String },
AmountExceedsLimit { amount: u128, limit: u128 },
HoldingPeriodNotMet {
address: String,
required_secs: u64,
elapsed_secs: u64,
},
MaxHoldersReached { max: u64, current: u64 },
NotWhitelisted { address: String },
TokenPaused,
}
impl std::fmt::Display for ComplianceViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InsufficientKyc {
address,
required_tier,
actual_tier,
} => write!(
f,
"Insufficient KYC for {}: required tier {}, actual {:?}",
address, required_tier, actual_tier
),
Self::NotAccredited { address } => {
write!(f, "Address {} is not accredited", address)
}
Self::CountryRestricted {
address,
country_code,
} => write!(
f,
"Country {} restricted for address {}",
country_code, address
),
Self::AddressFrozen { address, reason } => {
write!(f, "Address {} is frozen: {}", address, reason)
}
Self::AmountExceedsLimit { amount, limit } => {
write!(f, "Amount {} exceeds limit {}", amount, limit)
}
Self::HoldingPeriodNotMet {
address,
required_secs,
elapsed_secs,
} => write!(
f,
"Holding period not met for {}: required {}s, elapsed {}s",
address, required_secs, elapsed_secs
),
Self::MaxHoldersReached { max, current } => {
write!(f, "Max holders reached: {} / {}", current, max)
}
Self::NotWhitelisted { address } => {
write!(f, "Address {} is not whitelisted", address)
}
Self::TokenPaused => write!(f, "Token transfers are paused"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceCheckResult {
pub compliant: bool,
pub violations: Vec<ComplianceViolation>,
pub checked_rules: Vec<String>,
}
impl ComplianceCheckResult {
fn new() -> Self {
Self {
compliant: true,
violations: Vec::new(),
checked_rules: Vec::new(),
}
}
fn add_violation(&mut self, v: ComplianceViolation) {
self.compliant = false;
self.violations.push(v);
}
fn add_rule(&mut self, name: &str) {
self.checked_rules.push(name.to_string());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityClaim {
pub topic: u64,
pub scheme: u64,
pub issuer: String,
pub signature: Vec<u8>,
pub data: Vec<u8>,
pub uri: String,
pub valid_from: i64,
pub valid_to: i64,
}
impl IdentityClaim {
pub fn is_valid(&self, now: i64) -> bool {
now >= self.valid_from && now < self.valid_to
}
pub fn kyc_tier(&self) -> Option<u8> {
if self.topic == CLAIM_TOPIC_KYC {
self.data.first().copied()
} else {
None
}
}
pub fn country_code(&self) -> Option<u16> {
if self.topic == CLAIM_TOPIC_COUNTRY && self.data.len() >= 2 {
Some(u16::from_le_bytes([self.data[0], self.data[1]]))
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedIssuer {
pub issuer_did: String,
pub trusted_topics: Vec<u64>,
pub name: String,
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimTopicInfo {
pub topic_id: u64,
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhitelistEntry {
pub added_at: i64,
pub added_by: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FreezeInfo {
pub reason: String,
pub frozen_at: i64,
pub frozen_by: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryEvent {
pub token_id: TokenId,
pub from: Address,
pub to: Address,
pub amount: u128,
pub reason: String,
pub timestamp: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SupplyLimits {
pub max_supply: Option<u128>,
pub min_supply: Option<u128>,
}
#[derive(Debug, Clone)]
pub struct TransferRestrictions {
pub max_transfer_amount: Option<u128>,
pub min_holding_period_secs: Option<u64>,
pub require_whitelist: bool,
pub whitelist: DashMap<Address, WhitelistEntry>,
pub country_check: bool,
pub allow_cross_border: bool,
}
impl Default for TransferRestrictions {
fn default() -> Self {
Self {
max_transfer_amount: None,
min_holding_period_secs: None,
require_whitelist: false,
whitelist: DashMap::new(),
country_check: false,
allow_cross_border: true,
}
}
}
pub struct ComplianceRules {
pub token_id: TokenId,
pub require_kyc: bool,
pub min_kyc_tier: u8,
pub require_accreditation: bool,
pub max_holders: Option<u64>,
current_holders: AtomicU64,
pub transfer_restrictions: TransferRestrictions,
pub supply_limits: SupplyLimits,
pub paused: bool,
}
impl ComplianceRules {
pub fn new(token_id: TokenId) -> Self {
Self {
token_id,
require_kyc: false,
min_kyc_tier: 0,
require_accreditation: false,
max_holders: None,
current_holders: AtomicU64::new(0),
transfer_restrictions: TransferRestrictions::default(),
supply_limits: SupplyLimits::default(),
paused: false,
}
}
pub fn with_kyc(mut self, min_tier: u8) -> Self {
self.require_kyc = true;
self.min_kyc_tier = min_tier;
self
}
pub fn with_accreditation(mut self) -> Self {
self.require_accreditation = true;
self
}
pub fn with_max_holders(mut self, max: u64) -> Self {
self.max_holders = Some(max);
self
}
pub fn with_max_transfer_amount(mut self, max: u128) -> Self {
self.transfer_restrictions.max_transfer_amount = Some(max);
self
}
pub fn with_whitelist(mut self) -> Self {
self.transfer_restrictions.require_whitelist = true;
self
}
pub fn with_country_check(mut self) -> Self {
self.transfer_restrictions.country_check = true;
self
}
pub fn with_holding_period(mut self, secs: u64) -> Self {
self.transfer_restrictions.min_holding_period_secs = Some(secs);
self
}
pub fn holder_count(&self) -> u64 {
self.current_holders.load(Ordering::Relaxed)
}
pub fn increment_holders(&self) -> u64 {
self.current_holders.fetch_add(1, Ordering::Relaxed) + 1
}
pub fn decrement_holders(&self) {
let _ = self
.current_holders
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
Some(v.saturating_sub(1))
});
}
}
pub struct ComplianceRegistry {
token_compliance: DashMap<TokenId, ComplianceRules>,
identity_claims: DashMap<Address, Vec<IdentityClaim>>,
trusted_issuers: DashMap<String, TrustedIssuer>,
claim_topics: DashMap<u64, ClaimTopicInfo>,
frozen_addresses: DashMap<(TokenId, Address), FreezeInfo>,
country_restrictions: DashMap<(TokenId, u16), bool>,
first_receipt: DashMap<(Address, TokenId), i64>,
balances: DashMap<(Address, TokenId), u128>,
recovery_events: DashMap<u64, RecoveryEvent>,
recovery_nonce: AtomicU64,
}
impl ComplianceRegistry {
pub fn new() -> Self {
Self {
token_compliance: DashMap::new(),
identity_claims: DashMap::new(),
trusted_issuers: DashMap::new(),
claim_topics: DashMap::new(),
frozen_addresses: DashMap::new(),
country_restrictions: DashMap::new(),
first_receipt: DashMap::new(),
balances: DashMap::new(),
recovery_events: DashMap::new(),
recovery_nonce: AtomicU64::new(1),
}
}
fn now_secs() -> i64 {
chrono::Utc::now().timestamp()
}
pub fn register_compliance(&self, rules: ComplianceRules) {
info!(
"Registered compliance rules for token {}",
rules.token_id.to_hex()
);
self.token_compliance.insert(rules.token_id, rules);
}
pub fn has_compliance(&self, token_id: &TokenId) -> bool {
self.token_compliance.contains_key(token_id)
}
pub fn add_identity_claim(&self, address: Address, claim: IdentityClaim) {
debug!(
"Adding claim topic {} for address {} from issuer {}",
claim.topic, address, claim.issuer
);
self.identity_claims
.entry(address)
.or_default()
.push(claim);
}
pub fn verify_claim(&self, address: &Address, topic: u64) -> bool {
let now = Self::now_secs();
let claims = match self.identity_claims.get(address) {
Some(c) => c,
None => return false,
};
claims.iter().any(|c| {
c.topic == topic
&& c.is_valid(now)
&& self.is_trusted_issuer_for_topic(&c.issuer, topic)
})
}
fn effective_kyc_tier(&self, address: &Address) -> Option<u8> {
let now = Self::now_secs();
let claims = self.identity_claims.get(address)?;
claims
.iter()
.filter(|c| {
c.topic == CLAIM_TOPIC_KYC
&& c.is_valid(now)
&& self.is_trusted_issuer_for_topic(&c.issuer, CLAIM_TOPIC_KYC)
})
.filter_map(|c| c.kyc_tier())
.max()
}
fn effective_country(&self, address: &Address) -> Option<u16> {
let now = Self::now_secs();
let claims = self.identity_claims.get(address)?;
claims
.iter()
.filter(|c| {
c.topic == CLAIM_TOPIC_COUNTRY
&& c.is_valid(now)
&& self.is_trusted_issuer_for_topic(&c.issuer, CLAIM_TOPIC_COUNTRY)
})
.filter_map(|c| c.country_code())
.next()
}
pub fn add_trusted_issuer(&self, issuer: TrustedIssuer) {
info!(
"Added trusted issuer {} ({}) for topics {:?}",
issuer.issuer_did, issuer.name, issuer.trusted_topics
);
self.trusted_issuers
.insert(issuer.issuer_did.clone(), issuer);
}
fn is_trusted_issuer_for_topic(&self, issuer_did: &str, topic: u64) -> bool {
self.trusted_issuers
.get(issuer_did)
.map(|i| i.enabled && i.trusted_topics.contains(&topic))
.unwrap_or(false)
}
pub fn register_claim_topic(&self, info: ClaimTopicInfo) {
info!(
"Registered claim topic {}: {}",
info.topic_id, info.name
);
self.claim_topics.insert(info.topic_id, info);
}
pub fn get_claim_topic(&self, topic_id: u64) -> Option<ClaimTopicInfo> {
self.claim_topics.get(&topic_id).map(|e| e.clone())
}
pub fn list_claim_topics(&self) -> Vec<ClaimTopicInfo> {
self.claim_topics
.iter()
.map(|e| e.value().clone())
.collect()
}
pub fn freeze_address(
&self,
token_id: TokenId,
address: Address,
reason: String,
frozen_by: String,
) {
info!(
"Freezing address {} for token {}: {}",
address,
token_id.to_hex(),
reason
);
self.frozen_addresses.insert(
(token_id, address),
FreezeInfo {
reason,
frozen_at: Self::now_secs(),
frozen_by,
},
);
}
pub fn unfreeze_address(&self, token_id: &TokenId, address: &Address) {
info!(
"Unfreezing address {} for token {}",
address,
token_id.to_hex()
);
self.frozen_addresses.remove(&(*token_id, *address));
}
pub fn is_frozen(&self, token_id: &TokenId, address: &Address) -> bool {
self.frozen_addresses.contains_key(&(*token_id, *address))
}
pub fn set_country_restriction(&self, token_id: TokenId, country_code: u16, allowed: bool) {
debug!(
"Setting country {} {} for token {}",
country_code,
if allowed { "allowed" } else { "restricted" },
token_id.to_hex()
);
self.country_restrictions
.insert((token_id, country_code), allowed);
}
pub fn check_country(&self, token_id: &TokenId, address: &Address) -> bool {
let country = match self.effective_country(address) {
Some(c) => c,
None => return true,
};
self.country_restrictions
.get(&(*token_id, country))
.map(|v| *v)
.unwrap_or(true) }
pub fn add_to_whitelist(
&self,
token_id: &TokenId,
address: Address,
added_by: String,
) -> Result<()> {
let entry = self
.token_compliance
.get(token_id)
.ok_or_else(|| TokenError::AssetNotFound {
asset_id: token_id.to_hex(),
})?;
entry.value().transfer_restrictions.whitelist.insert(
address,
WhitelistEntry {
added_at: Self::now_secs(),
added_by,
},
);
debug!("Whitelisted {} for token {}", address, token_id.to_hex());
Ok(())
}
pub fn remove_from_whitelist(
&self,
token_id: &TokenId,
address: &Address,
) -> Result<()> {
let entry = self
.token_compliance
.get(token_id)
.ok_or_else(|| TokenError::AssetNotFound {
asset_id: token_id.to_hex(),
})?;
entry.value().transfer_restrictions.whitelist.remove(address);
Ok(())
}
pub fn record_receipt(
&self,
token_id: &TokenId,
address: Address,
amount: u128,
) {
let key = (address, *token_id);
let was_zero = self
.balances
.get(&key)
.map(|v| *v == 0)
.unwrap_or(true);
self.balances
.entry(key)
.and_modify(|b| *b = b.saturating_add(amount))
.or_insert(amount);
if was_zero {
self.first_receipt
.entry((address, *token_id))
.or_insert_with(Self::now_secs);
if let Some(rules) = self.token_compliance.get(token_id) {
rules.increment_holders();
}
}
}
pub fn record_send(
&self,
token_id: &TokenId,
address: Address,
amount: u128,
) {
let key = (address, *token_id);
let mut became_zero = false;
self.balances.entry(key).and_modify(|b| {
*b = b.saturating_sub(amount);
if *b == 0 {
became_zero = true;
}
});
if became_zero {
self.first_receipt.remove(&(address, *token_id));
if let Some(rules) = self.token_compliance.get(token_id) {
rules.decrement_holders();
}
}
}
pub fn recover_tokens(
&self,
token_id: TokenId,
from: Address,
to: Address,
amount: u128,
reason: String,
) -> Result<RecoveryEvent> {
if amount == 0 {
return Err(TokenError::InvalidAmount(
"Recovery amount must be greater than zero".to_string(),
));
}
let from_key = (from, token_id);
let from_balance = self.balances.get(&from_key).map(|v| *v).unwrap_or(0);
if from_balance < amount {
return Err(TokenError::InsufficientBalance {
required: amount,
available: from_balance,
});
}
self.record_send(&token_id, from, amount);
self.record_receipt(&token_id, to, amount);
let nonce = self.recovery_nonce.fetch_add(1, Ordering::Relaxed);
let event = RecoveryEvent {
token_id,
from,
to,
amount,
reason: reason.clone(),
timestamp: Self::now_secs(),
};
warn!(
"RECOVERY: {} tokens of {} transferred from {} to {}: {}",
amount,
token_id.to_hex(),
from,
to,
reason
);
self.recovery_events.insert(nonce, event.clone());
Ok(event)
}
pub fn can_transfer(
&self,
token_id: &TokenId,
from: &Address,
to: &Address,
amount: u128,
) -> Result<ComplianceCheckResult> {
let rules = self
.token_compliance
.get(token_id)
.ok_or_else(|| TokenError::AssetNotFound {
asset_id: token_id.to_hex(),
})?;
let mut result = ComplianceCheckResult::new();
result.add_rule("token_paused");
if rules.paused {
result.add_violation(ComplianceViolation::TokenPaused);
}
result.add_rule("frozen_address");
if let Some(info) = self.frozen_addresses.get(&(*token_id, *from)) {
result.add_violation(ComplianceViolation::AddressFrozen {
address: from.to_string(),
reason: info.reason.clone(),
});
}
if let Some(info) = self.frozen_addresses.get(&(*token_id, *to)) {
result.add_violation(ComplianceViolation::AddressFrozen {
address: to.to_string(),
reason: info.reason.clone(),
});
}
if rules.require_kyc {
result.add_rule("kyc_tier");
let min_tier = rules.min_kyc_tier;
for addr in [from, to] {
let tier = self.effective_kyc_tier(addr);
match tier {
Some(t) if t >= min_tier => { }
_ => {
result.add_violation(ComplianceViolation::InsufficientKyc {
address: addr.to_string(),
required_tier: min_tier,
actual_tier: tier,
});
}
}
}
}
if rules.require_accreditation {
result.add_rule("accreditation");
for addr in [from, to] {
if !self.verify_claim(addr, CLAIM_TOPIC_ACCREDITED_INVESTOR) {
result.add_violation(ComplianceViolation::NotAccredited {
address: addr.to_string(),
});
}
}
}
if rules.transfer_restrictions.country_check {
result.add_rule("country_restriction");
for addr in [from, to] {
if let Some(country) = self.effective_country(addr) {
let allowed = self
.country_restrictions
.get(&(*token_id, country))
.map(|v| *v)
.unwrap_or(true);
if !allowed {
result.add_violation(ComplianceViolation::CountryRestricted {
address: addr.to_string(),
country_code: country,
});
}
}
}
}
if let Some(max) = rules.transfer_restrictions.max_transfer_amount {
result.add_rule("max_transfer_amount");
if amount > max {
result.add_violation(ComplianceViolation::AmountExceedsLimit {
amount,
limit: max,
});
}
}
if rules.transfer_restrictions.require_whitelist {
result.add_rule("whitelist");
for addr in [from, to] {
if !rules.transfer_restrictions.whitelist.contains_key(addr) {
result.add_violation(ComplianceViolation::NotWhitelisted {
address: addr.to_string(),
});
}
}
}
if let Some(min_secs) = rules.transfer_restrictions.min_holding_period_secs {
result.add_rule("holding_period");
let now = Self::now_secs();
if let Some(first) = self.first_receipt.get(&(*from, *token_id)) {
let elapsed = (now - *first).max(0) as u64;
if elapsed < min_secs {
result.add_violation(ComplianceViolation::HoldingPeriodNotMet {
address: from.to_string(),
required_secs: min_secs,
elapsed_secs: elapsed,
});
}
}
}
if let Some(max) = rules.max_holders {
result.add_rule("max_holders");
let to_key = (*to, *token_id);
let to_balance = self.balances.get(&to_key).map(|v| *v).unwrap_or(0);
if to_balance == 0 {
let current = rules.holder_count();
if current >= max {
result.add_violation(ComplianceViolation::MaxHoldersReached {
max,
current,
});
}
}
}
Ok(result)
}
}
impl Default for ComplianceRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tnzo::ONE_TNZO;
fn test_token() -> TokenId {
TokenId::new([0x42; 32])
}
fn make_kyc_claim(issuer: &str, tier: u8, valid_from: i64, valid_to: i64) -> IdentityClaim {
IdentityClaim {
topic: CLAIM_TOPIC_KYC,
scheme: 1,
issuer: issuer.to_string(),
signature: vec![0xAA; 64],
data: vec![tier],
uri: String::new(),
valid_from,
valid_to,
}
}
fn make_accreditation_claim(issuer: &str, valid_from: i64, valid_to: i64) -> IdentityClaim {
IdentityClaim {
topic: CLAIM_TOPIC_ACCREDITED_INVESTOR,
scheme: 1,
issuer: issuer.to_string(),
signature: vec![0xBB; 64],
data: vec![1],
uri: String::new(),
valid_from,
valid_to,
}
}
fn make_country_claim(issuer: &str, country: u16, valid_from: i64, valid_to: i64) -> IdentityClaim {
IdentityClaim {
topic: CLAIM_TOPIC_COUNTRY,
scheme: 1,
issuer: issuer.to_string(),
signature: vec![0xCC; 64],
data: country.to_le_bytes().to_vec(),
uri: String::new(),
valid_from,
valid_to,
}
}
fn trusted_issuer(did: &str, topics: Vec<u64>) -> TrustedIssuer {
TrustedIssuer {
issuer_did: did.to_string(),
trusted_topics: topics,
name: "TestIssuer".to_string(),
enabled: true,
}
}
fn setup_kyc_registry() -> (ComplianceRegistry, Address, Address) {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(
ComplianceRules::new(token).with_kyc(2),
);
reg.add_trusted_issuer(trusted_issuer(
"did:tenzro:human:issuer1",
vec![CLAIM_TOPIC_KYC, CLAIM_TOPIC_ACCREDITED_INVESTOR, CLAIM_TOPIC_COUNTRY],
));
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let now = chrono::Utc::now().timestamp();
reg.add_identity_claim(from, make_kyc_claim("did:tenzro:human:issuer1", 3, now - 1000, now + 100_000));
reg.add_identity_claim(to, make_kyc_claim("did:tenzro:human:issuer1", 3, now - 1000, now + 100_000));
(reg, from, to)
}
#[test]
fn test_register_compliance() {
let reg = ComplianceRegistry::new();
let token = test_token();
assert!(!reg.has_compliance(&token));
reg.register_compliance(ComplianceRules::new(token));
assert!(reg.has_compliance(&token));
}
#[test]
fn test_can_transfer_passes() {
let (reg, from, to) = setup_kyc_registry();
let token = test_token();
let result = reg.can_transfer(&token, &from, &to, 100 * ONE_TNZO).unwrap();
assert!(result.compliant);
assert!(result.violations.is_empty());
assert!(result.checked_rules.contains(&"kyc_tier".to_string()));
}
#[test]
fn test_insufficient_kyc_blocks() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(ComplianceRules::new(token).with_kyc(2));
reg.add_trusted_issuer(trusted_issuer(
"did:tenzro:human:issuer1",
vec![CLAIM_TOPIC_KYC],
));
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let now = chrono::Utc::now().timestamp();
reg.add_identity_claim(from, make_kyc_claim("did:tenzro:human:issuer1", 1, now - 100, now + 100_000));
reg.add_identity_claim(to, make_kyc_claim("did:tenzro:human:issuer1", 2, now - 100, now + 100_000));
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(!result.compliant);
assert_eq!(result.violations.len(), 1);
assert!(matches!(
&result.violations[0],
ComplianceViolation::InsufficientKyc { actual_tier: Some(1), .. }
));
}
#[test]
fn test_frozen_address_blocks() {
let (reg, from, to) = setup_kyc_registry();
let token = test_token();
reg.freeze_address(token, from, "Suspicious activity".to_string(), "admin".to_string());
assert!(reg.is_frozen(&token, &from));
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::AddressFrozen { .. })));
reg.unfreeze_address(&token, &from);
assert!(!reg.is_frozen(&token, &from));
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(result.compliant);
}
#[test]
fn test_country_restriction_blocks() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(ComplianceRules::new(token).with_country_check());
reg.add_trusted_issuer(trusted_issuer(
"did:tenzro:human:issuer1",
vec![CLAIM_TOPIC_COUNTRY],
));
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let now = chrono::Utc::now().timestamp();
reg.add_identity_claim(from, make_country_claim("did:tenzro:human:issuer1", 840, now - 100, now + 100_000));
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();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(
v,
ComplianceViolation::CountryRestricted { country_code: 840, .. }
)));
}
#[test]
fn test_amount_limit_blocks() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(
ComplianceRules::new(token).with_max_transfer_amount(1_000 * ONE_TNZO),
);
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let result = reg
.can_transfer(&token, &from, &to, 2_000 * ONE_TNZO)
.unwrap();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(
v,
ComplianceViolation::AmountExceedsLimit { .. }
)));
let result = reg
.can_transfer(&token, &from, &to, 500 * ONE_TNZO)
.unwrap();
assert!(result.compliant);
}
#[test]
fn test_whitelist_enforcement() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(ComplianceRules::new(token).with_whitelist());
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::NotWhitelisted { .. })));
reg.add_to_whitelist(&token, from, "admin".to_string()).unwrap();
reg.add_to_whitelist(&token, to, "admin".to_string()).unwrap();
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(result.compliant);
}
#[test]
fn test_claim_verification() {
let reg = ComplianceRegistry::new();
let addr = Address::new([0x01; 32]);
reg.add_trusted_issuer(trusted_issuer(
"did:tenzro:human:issuer1",
vec![CLAIM_TOPIC_KYC],
));
let now = chrono::Utc::now().timestamp();
reg.add_identity_claim(addr, make_kyc_claim("did:tenzro:human:issuer1", 2, now - 100, now + 100_000));
assert!(reg.verify_claim(&addr, CLAIM_TOPIC_KYC));
assert!(!reg.verify_claim(&addr, CLAIM_TOPIC_ACCREDITED_INVESTOR));
}
#[test]
fn test_trusted_issuer_check() {
let reg = ComplianceRegistry::new();
let addr = Address::new([0x01; 32]);
let now = chrono::Utc::now().timestamp();
reg.add_identity_claim(addr, make_kyc_claim("did:tenzro:human:untrusted", 3, now - 100, now + 100_000));
assert!(!reg.verify_claim(&addr, CLAIM_TOPIC_KYC));
reg.add_trusted_issuer(trusted_issuer(
"did:tenzro:human:untrusted",
vec![CLAIM_TOPIC_KYC],
));
assert!(reg.verify_claim(&addr, CLAIM_TOPIC_KYC));
}
#[test]
fn test_forced_recovery() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(ComplianceRules::new(token));
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
reg.record_receipt(&token, from, 1_000);
let event = reg
.recover_tokens(token, from, to, 500, "Court order #123".to_string())
.unwrap();
assert_eq!(event.amount, 500);
assert_eq!(event.from, from);
assert_eq!(event.to, to);
assert_eq!(
reg.balances.get(&(from, token)).map(|v| *v).unwrap_or(0),
500
);
assert_eq!(
reg.balances.get(&(to, token)).map(|v| *v).unwrap_or(0),
500
);
}
#[test]
fn test_max_holders_limit() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(ComplianceRules::new(token).with_max_holders(2));
let addr1 = Address::new([0x01; 32]);
let addr2 = Address::new([0x02; 32]);
let addr3 = Address::new([0x03; 32]);
let _addr4 = Address::new([0x04; 32]);
reg.record_receipt(&token, addr1, 100);
reg.record_receipt(&token, addr2, 100);
let result = reg.can_transfer(&token, &addr1, &addr3, 50).unwrap();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(
v,
ComplianceViolation::MaxHoldersReached { max: 2, current: 2 }
)));
let result = reg.can_transfer(&token, &addr1, &addr2, 50).unwrap();
assert!(result.compliant);
}
#[test]
fn test_holding_period_check() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(ComplianceRules::new(token).with_holding_period(86_400));
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
reg.record_receipt(&token, from, 1_000);
let result = reg.can_transfer(&token, &from, &to, 500).unwrap();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(
v,
ComplianceViolation::HoldingPeriodNotMet { .. }
)));
let old_ts = chrono::Utc::now().timestamp() - 100_000;
reg.first_receipt.insert((from, token), old_ts);
let result = reg.can_transfer(&token, &from, &to, 500).unwrap();
assert!(result.compliant);
}
#[test]
fn test_token_paused_blocks() {
let reg = ComplianceRegistry::new();
let token = test_token();
let mut rules = ComplianceRules::new(token);
rules.paused = true;
reg.register_compliance(rules);
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::TokenPaused)));
}
#[test]
fn test_accreditation_check() {
let reg = ComplianceRegistry::new();
let token = test_token();
reg.register_compliance(ComplianceRules::new(token).with_accreditation());
reg.add_trusted_issuer(trusted_issuer(
"did:tenzro:human:issuer1",
vec![CLAIM_TOPIC_ACCREDITED_INVESTOR],
));
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let now = chrono::Utc::now().timestamp();
reg.add_identity_claim(from, make_accreditation_claim("did:tenzro:human:issuer1", now - 100, now + 100_000));
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(!result.compliant);
assert!(result.violations.iter().any(|v| matches!(v, ComplianceViolation::NotAccredited { .. })));
reg.add_identity_claim(to, make_accreditation_claim("did:tenzro:human:issuer1", now - 100, now + 100_000));
let result = reg.can_transfer(&token, &from, &to, 100).unwrap();
assert!(result.compliant);
}
#[test]
fn test_recovery_fails_insufficient_balance() {
let reg = ComplianceRegistry::new();
let token = test_token();
let from = Address::new([0x01; 32]);
let to = Address::new([0x02; 32]);
let result = reg.recover_tokens(token, from, to, 100, "test".to_string());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Insufficient balance"));
}
}