1use crate::cross_vm::TokenId;
10use crate::error::{Result, TokenError};
11use crate::registry::TokenRegistry;
12use crate::tnzo::TnzoToken;
13use dashmap::DashMap;
14use serde::{Deserialize, Serialize};
15use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
16use std::sync::Arc;
17use tenzro_types::primitives::Address;
18use tracing::{debug, info};
19
20struct AtomicU128(parking_lot::Mutex<u128>);
26
27impl AtomicU128 {
28 fn new(v: u128) -> Self {
29 Self(parking_lot::Mutex::new(v))
30 }
31
32 fn load(&self) -> u128 {
33 *self.0.lock()
34 }
35
36 fn store(&self, v: u128) {
37 *self.0.lock() = v;
38 }
39
40 fn fetch_add(&self, v: u128) -> u128 {
41 let mut guard = self.0.lock();
42 let old = *guard;
43 *guard = old.saturating_add(v);
44 old
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct CrosschainMintEvent {
55 pub nonce: u64,
57 pub to: Address,
59 pub amount: u128,
61 pub sender: Vec<u8>,
63 pub bridge: [u8; 20],
65 pub timestamp: i64,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CrosschainBurnEvent {
72 pub nonce: u64,
74 pub from: Address,
76 pub amount: u128,
78 pub sender: Vec<u8>,
80 pub bridge: [u8; 20],
82 pub timestamp: i64,
84}
85
86pub struct BridgeAuthorization {
88 pub bridge_address: [u8; 20],
90 pub name: String,
92 pub authorized_tokens: Vec<TokenId>,
94 pub max_mint_per_tx: Option<u128>,
96 pub max_burn_per_tx: Option<u128>,
98 pub daily_mint_limit: Option<u128>,
100 pub daily_burn_limit: Option<u128>,
102 minted_today: AtomicU128,
104 burned_today: AtomicU128,
106 last_reset: AtomicI64,
108 pub enabled: bool,
110}
111
112impl BridgeAuthorization {
113 fn maybe_reset_daily(&self, now: i64) {
115 let last = self.last_reset.load(Ordering::Relaxed);
116 if now - last >= 86_400 {
117 self.minted_today.store(0);
118 self.burned_today.store(0);
119 self.last_reset.store(now, Ordering::Relaxed);
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct BridgeInfo {
127 pub bridge_address: String,
128 pub name: String,
129 pub authorized_tokens: Vec<String>,
130 pub max_mint_per_tx: Option<u128>,
131 pub max_burn_per_tx: Option<u128>,
132 pub daily_mint_limit: Option<u128>,
133 pub daily_burn_limit: Option<u128>,
134 pub minted_today: u128,
135 pub burned_today: u128,
136 pub enabled: bool,
137}
138
139pub struct CrosschainTokenManager {
148 authorized_bridges: DashMap<[u8; 20], BridgeAuthorization>,
150 tnzo_token: Arc<TnzoToken>,
152 registry: Arc<TokenRegistry>,
157 mint_events: DashMap<u64, CrosschainMintEvent>,
159 burn_events: DashMap<u64, CrosschainBurnEvent>,
161 nonce: AtomicU64,
163}
164
165impl CrosschainTokenManager {
166 pub fn new(tnzo_token: Arc<TnzoToken>, registry: Arc<TokenRegistry>) -> Self {
168 Self {
169 authorized_bridges: DashMap::new(),
170 tnzo_token,
171 registry,
172 mint_events: DashMap::new(),
173 burn_events: DashMap::new(),
174 nonce: AtomicU64::new(1),
175 }
176 }
177
178 pub fn authorize_bridge(
182 &self,
183 bridge_address: [u8; 20],
184 name: String,
185 authorized_tokens: Vec<TokenId>,
186 max_mint_per_tx: Option<u128>,
187 max_burn_per_tx: Option<u128>,
188 daily_mint_limit: Option<u128>,
189 daily_burn_limit: Option<u128>,
190 ) -> Result<()> {
191 let now = chrono::Utc::now().timestamp();
192 let auth = BridgeAuthorization {
193 bridge_address,
194 name: name.clone(),
195 authorized_tokens,
196 max_mint_per_tx,
197 max_burn_per_tx,
198 daily_mint_limit,
199 daily_burn_limit,
200 minted_today: AtomicU128::new(0),
201 burned_today: AtomicU128::new(0),
202 last_reset: AtomicI64::new(now),
203 enabled: true,
204 };
205
206 info!(
207 "Authorized bridge {} (0x{})",
208 name,
209 hex::encode(bridge_address)
210 );
211 self.authorized_bridges.insert(bridge_address, auth);
212 Ok(())
213 }
214
215 pub fn revoke_bridge(&self, bridge_address: &[u8; 20]) -> Result<()> {
219 match self.authorized_bridges.remove(bridge_address) {
220 Some((_, auth)) => {
221 info!(
222 "Revoked bridge {} (0x{})",
223 auth.name,
224 hex::encode(bridge_address)
225 );
226 Ok(())
227 }
228 None => Err(TokenError::Unauthorized {
229 reason: format!(
230 "Bridge 0x{} is not authorized",
231 hex::encode(bridge_address)
232 ),
233 }),
234 }
235 }
236
237 pub fn update_bridge_limits(
239 &self,
240 bridge_address: &[u8; 20],
241 max_mint_per_tx: Option<u128>,
242 max_burn_per_tx: Option<u128>,
243 daily_mint_limit: Option<u128>,
244 daily_burn_limit: Option<u128>,
245 ) -> Result<()> {
246 let mut entry = self
247 .authorized_bridges
248 .get_mut(bridge_address)
249 .ok_or_else(|| TokenError::Unauthorized {
250 reason: format!(
251 "Bridge 0x{} is not authorized",
252 hex::encode(bridge_address)
253 ),
254 })?;
255
256 let auth = entry.value_mut();
257 auth.max_mint_per_tx = max_mint_per_tx;
258 auth.max_burn_per_tx = max_burn_per_tx;
259 auth.daily_mint_limit = daily_mint_limit;
260 auth.daily_burn_limit = daily_burn_limit;
261
262 debug!(
263 "Updated limits for bridge 0x{}",
264 hex::encode(bridge_address)
265 );
266 Ok(())
267 }
268
269 pub fn is_bridge_authorized(&self, bridge_address: &[u8; 20]) -> bool {
271 self.authorized_bridges
272 .get(bridge_address)
273 .map(|e| e.value().enabled)
274 .unwrap_or(false)
275 }
276
277 pub fn get_bridge_info(&self, bridge_address: &[u8; 20]) -> Option<BridgeInfo> {
279 self.authorized_bridges.get(bridge_address).map(|entry| {
280 let auth = entry.value();
281 BridgeInfo {
282 bridge_address: format!("0x{}", hex::encode(auth.bridge_address)),
283 name: auth.name.clone(),
284 authorized_tokens: auth
285 .authorized_tokens
286 .iter()
287 .map(|t| t.to_hex())
288 .collect(),
289 max_mint_per_tx: auth.max_mint_per_tx,
290 max_burn_per_tx: auth.max_burn_per_tx,
291 daily_mint_limit: auth.daily_mint_limit,
292 daily_burn_limit: auth.daily_burn_limit,
293 minted_today: auth.minted_today.load(),
294 burned_today: auth.burned_today.load(),
295 enabled: auth.enabled,
296 }
297 })
298 }
299
300 pub fn resolve_authorized_tokens(
307 &self,
308 bridge_address: &[u8; 20],
309 ) -> Vec<crate::TokenDefinition> {
310 let entry = match self.authorized_bridges.get(bridge_address) {
311 Some(e) => e,
312 None => return Vec::new(),
313 };
314 entry
315 .value()
316 .authorized_tokens
317 .iter()
318 .filter_map(|tid| self.registry.get(tid))
319 .collect()
320 }
321
322 pub fn list_authorized_bridges(&self) -> Vec<BridgeInfo> {
324 self.authorized_bridges
325 .iter()
326 .map(|entry| {
327 let auth = entry.value();
328 BridgeInfo {
329 bridge_address: format!("0x{}", hex::encode(auth.bridge_address)),
330 name: auth.name.clone(),
331 authorized_tokens: auth
332 .authorized_tokens
333 .iter()
334 .map(|t| t.to_hex())
335 .collect(),
336 max_mint_per_tx: auth.max_mint_per_tx,
337 max_burn_per_tx: auth.max_burn_per_tx,
338 daily_mint_limit: auth.daily_mint_limit,
339 daily_burn_limit: auth.daily_burn_limit,
340 minted_today: auth.minted_today.load(),
341 burned_today: auth.burned_today.load(),
342 enabled: auth.enabled,
343 }
344 })
345 .collect()
346 }
347
348 fn validate_bridge(
353 &self,
354 bridge_address: &[u8; 20],
355 amount: u128,
356 is_mint: bool,
357 ) -> Result<()> {
358 let entry =
359 self.authorized_bridges
360 .get(bridge_address)
361 .ok_or_else(|| TokenError::Unauthorized {
362 reason: format!(
363 "Bridge 0x{} is not authorized",
364 hex::encode(bridge_address)
365 ),
366 })?;
367
368 let auth = entry.value();
369
370 if !auth.enabled {
371 return Err(TokenError::Unauthorized {
372 reason: format!(
373 "Bridge 0x{} is disabled",
374 hex::encode(bridge_address)
375 ),
376 });
377 }
378
379 if amount == 0 {
380 return Err(TokenError::InvalidAmount(
381 "Cross-chain amount must be greater than zero".to_string(),
382 ));
383 }
384
385 let now = chrono::Utc::now().timestamp();
386 auth.maybe_reset_daily(now);
387
388 if is_mint {
389 if let Some(max) = auth.max_mint_per_tx
391 && amount > max
392 {
393 return Err(TokenError::InvalidAmount(format!(
394 "Mint amount {} exceeds per-tx limit {}",
395 amount, max
396 )));
397 }
398 if let Some(daily) = auth.daily_mint_limit {
400 let already = auth.minted_today.load();
401 if already.saturating_add(amount) > daily {
402 return Err(TokenError::InvalidAmount(format!(
403 "Mint amount {} would exceed daily limit {} (already minted {})",
404 amount, daily, already
405 )));
406 }
407 }
408 } else {
409 if let Some(max) = auth.max_burn_per_tx
411 && amount > max
412 {
413 return Err(TokenError::InvalidAmount(format!(
414 "Burn amount {} exceeds per-tx limit {}",
415 amount, max
416 )));
417 }
418 if let Some(daily) = auth.daily_burn_limit {
420 let already = auth.burned_today.load();
421 if already.saturating_add(amount) > daily {
422 return Err(TokenError::InvalidAmount(format!(
423 "Burn amount {} would exceed daily limit {} (already burned {})",
424 amount, daily, already
425 )));
426 }
427 }
428 }
429
430 Ok(())
431 }
432
433 fn now_secs() -> i64 {
435 chrono::Utc::now().timestamp()
436 }
437
438 pub fn crosschain_mint(
451 &self,
452 bridge: [u8; 20],
453 to: Address,
454 amount: u128,
455 sender: Vec<u8>,
456 ) -> Result<CrosschainMintEvent> {
457 self.validate_bridge(&bridge, amount, true)?;
459
460 let treasury = self
462 .tnzo_token
463 .treasury_address_ref()
464 .ok_or_else(|| TokenError::Unauthorized {
465 reason: "Treasury address not set; cannot mint cross-chain tokens".to_string(),
466 })?;
467
468 self.tnzo_token.mint(&to, amount, &treasury)?;
469
470 {
472 let entry = self.authorized_bridges.get(&bridge).unwrap();
473 entry.value().minted_today.fetch_add(amount);
474 }
475
476 let nonce = self.nonce.fetch_add(1, Ordering::Relaxed);
478 let event = CrosschainMintEvent {
479 nonce,
480 to,
481 amount,
482 sender,
483 bridge,
484 timestamp: Self::now_secs(),
485 };
486
487 info!(
488 "crosschainMint: bridge=0x{} to={} amount={} nonce={}",
489 hex::encode(bridge),
490 to,
491 amount,
492 nonce
493 );
494
495 self.mint_events.insert(nonce, event.clone());
496 Ok(event)
497 }
498
499 pub fn crosschain_burn(
511 &self,
512 bridge: [u8; 20],
513 from: Address,
514 amount: u128,
515 sender: Vec<u8>,
516 ) -> Result<CrosschainBurnEvent> {
517 self.validate_bridge(&bridge, amount, false)?;
519
520 self.tnzo_token.burn(&from, amount)?;
522
523 {
525 let entry = self.authorized_bridges.get(&bridge).unwrap();
526 entry.value().burned_today.fetch_add(amount);
527 }
528
529 let nonce = self.nonce.fetch_add(1, Ordering::Relaxed);
531 let event = CrosschainBurnEvent {
532 nonce,
533 from,
534 amount,
535 sender,
536 bridge,
537 timestamp: Self::now_secs(),
538 };
539
540 info!(
541 "crosschainBurn: bridge=0x{} from={} amount={} nonce={}",
542 hex::encode(bridge),
543 from,
544 amount,
545 nonce
546 );
547
548 self.burn_events.insert(nonce, event.clone());
549 Ok(event)
550 }
551
552 pub fn get_mint_events(&self) -> Vec<CrosschainMintEvent> {
556 let mut events: Vec<CrosschainMintEvent> = self
557 .mint_events
558 .iter()
559 .map(|e| e.value().clone())
560 .collect();
561 events.sort_by(|a, b| b.nonce.cmp(&a.nonce));
562 events
563 }
564
565 pub fn get_burn_events(&self) -> Vec<CrosschainBurnEvent> {
567 let mut events: Vec<CrosschainBurnEvent> = self
568 .burn_events
569 .iter()
570 .map(|e| e.value().clone())
571 .collect();
572 events.sort_by(|a, b| b.nonce.cmp(&a.nonce));
573 events
574 }
575
576 pub fn get_mint_event(&self, nonce: u64) -> Option<CrosschainMintEvent> {
578 self.mint_events.get(&nonce).map(|e| e.value().clone())
579 }
580
581 pub fn get_burn_event(&self, nonce: u64) -> Option<CrosschainBurnEvent> {
583 self.burn_events.get(&nonce).map(|e| e.value().clone())
584 }
585}
586
587#[cfg(test)]
592mod tests {
593 use super::*;
594 use crate::tnzo::ONE_TNZO;
595
596 fn setup() -> (CrosschainTokenManager, [u8; 20], Address) {
598 let token = Arc::new(TnzoToken::new());
599 let registry = Arc::new(TokenRegistry::new());
600
601 let treasury = Address::new([0xAA; 32]);
603 token.set_treasury_address(treasury);
604
605 token
607 .mint(&Address::new([0xBB; 32]), 1_000_000 * ONE_TNZO, &treasury)
608 .unwrap();
609
610 let mgr = CrosschainTokenManager::new(token, registry);
611
612 let bridge: [u8; 20] = [0x01; 20];
613 mgr.authorize_bridge(
614 bridge,
615 "TestBridge".to_string(),
616 vec![],
617 Some(10_000 * ONE_TNZO),
618 Some(10_000 * ONE_TNZO),
619 Some(100_000 * ONE_TNZO),
620 Some(100_000 * ONE_TNZO),
621 )
622 .unwrap();
623
624 (mgr, bridge, treasury)
625 }
626
627 #[test]
628 fn test_authorize_bridge() {
629 let token = Arc::new(TnzoToken::new());
630 let registry = Arc::new(TokenRegistry::new());
631 let mgr = CrosschainTokenManager::new(token, registry);
632
633 let bridge: [u8; 20] = [0x42; 20];
634 mgr.authorize_bridge(
635 bridge,
636 "MyBridge".to_string(),
637 vec![],
638 None,
639 None,
640 None,
641 None,
642 )
643 .unwrap();
644
645 assert!(mgr.is_bridge_authorized(&bridge));
646 let info = mgr.get_bridge_info(&bridge).unwrap();
647 assert_eq!(info.name, "MyBridge");
648 assert!(info.enabled);
649 }
650
651 #[test]
652 fn test_crosschain_mint_succeeds() {
653 let (mgr, bridge, _treasury) = setup();
654
655 let to = Address::new([0xCC; 32]);
656 let event = mgr
657 .crosschain_mint(bridge, to, 500 * ONE_TNZO, vec![1, 2, 3])
658 .unwrap();
659
660 assert_eq!(event.amount, 500 * ONE_TNZO);
661 assert_eq!(event.to, to);
662 assert_eq!(event.bridge, bridge);
663 assert_eq!(mgr.tnzo_token.balance_of(&to), 500 * ONE_TNZO);
664 }
665
666 #[test]
667 fn test_crosschain_mint_fails_unauthorized() {
668 let (mgr, _bridge, _treasury) = setup();
669
670 let rogue: [u8; 20] = [0xFF; 20];
671 let to = Address::new([0xCC; 32]);
672 let result = mgr.crosschain_mint(rogue, to, 100 * ONE_TNZO, vec![]);
673 assert!(result.is_err());
674 let err = result.unwrap_err().to_string();
675 assert!(err.contains("not authorized"));
676 }
677
678 #[test]
679 fn test_crosschain_mint_per_tx_limit() {
680 let (mgr, bridge, _treasury) = setup();
681
682 let to = Address::new([0xCC; 32]);
684 let result = mgr.crosschain_mint(bridge, to, 20_000 * ONE_TNZO, vec![]);
685 assert!(result.is_err());
686 let err = result.unwrap_err().to_string();
687 assert!(err.contains("exceeds per-tx limit"));
688 }
689
690 #[test]
691 fn test_crosschain_mint_daily_limit() {
692 let (mgr, bridge, _treasury) = setup();
693
694 let to = Address::new([0xCC; 32]);
695
696 for _ in 0..10 {
698 mgr.crosschain_mint(bridge, to, 10_000 * ONE_TNZO, vec![])
699 .unwrap();
700 }
701
702 let result = mgr.crosschain_mint(bridge, to, ONE_TNZO, vec![]);
704 assert!(result.is_err());
705 let err = result.unwrap_err().to_string();
706 assert!(err.contains("daily limit"));
707 }
708
709 #[test]
710 fn test_crosschain_burn_succeeds() {
711 let (mgr, bridge, _treasury) = setup();
712
713 let from = Address::new([0xBB; 32]);
715 let event = mgr
716 .crosschain_burn(bridge, from, 500 * ONE_TNZO, vec![4, 5, 6])
717 .unwrap();
718
719 assert_eq!(event.amount, 500 * ONE_TNZO);
720 assert_eq!(event.from, from);
721 assert_eq!(
722 mgr.tnzo_token.balance_of(&from),
723 999_500 * ONE_TNZO
724 );
725 }
726
727 #[test]
728 fn test_crosschain_burn_insufficient_balance() {
729 let (mgr, bridge, _treasury) = setup();
730
731 let from = Address::new([0xDD; 32]);
733 let result = mgr.crosschain_burn(bridge, from, 100 * ONE_TNZO, vec![]);
734 assert!(result.is_err());
735 let err = result.unwrap_err().to_string();
736 assert!(err.contains("Insufficient balance"));
737 }
738
739 #[test]
740 fn test_crosschain_burn_per_tx_limit() {
741 let (mgr, bridge, _treasury) = setup();
742
743 let from = Address::new([0xBB; 32]);
744 let result = mgr.crosschain_burn(bridge, from, 20_000 * ONE_TNZO, vec![]);
745 assert!(result.is_err());
746 let err = result.unwrap_err().to_string();
747 assert!(err.contains("exceeds per-tx limit"));
748 }
749
750 #[test]
751 fn test_revoke_bridge() {
752 let (mgr, bridge, _treasury) = setup();
753
754 mgr.revoke_bridge(&bridge).unwrap();
755 assert!(!mgr.is_bridge_authorized(&bridge));
756
757 let to = Address::new([0xCC; 32]);
759 let result = mgr.crosschain_mint(bridge, to, ONE_TNZO, vec![]);
760 assert!(result.is_err());
761 }
762
763 #[test]
764 fn test_event_tracking() {
765 let (mgr, bridge, _treasury) = setup();
766
767 let to = Address::new([0xCC; 32]);
768 let from = Address::new([0xBB; 32]);
769
770 let mint_evt = mgr
771 .crosschain_mint(bridge, to, 100 * ONE_TNZO, vec![10])
772 .unwrap();
773 let burn_evt = mgr
774 .crosschain_burn(bridge, from, 50 * ONE_TNZO, vec![20])
775 .unwrap();
776
777 let mints = mgr.get_mint_events();
778 assert_eq!(mints.len(), 1);
779 assert_eq!(mints[0].nonce, mint_evt.nonce);
780
781 let burns = mgr.get_burn_events();
782 assert_eq!(burns.len(), 1);
783 assert_eq!(burns[0].nonce, burn_evt.nonce);
784
785 assert!(mgr.get_mint_event(mint_evt.nonce).is_some());
787 assert!(mgr.get_burn_event(burn_evt.nonce).is_some());
788 assert!(mgr.get_mint_event(999).is_none());
789 }
790
791 #[test]
792 fn test_multiple_bridges() {
793 let (mgr, bridge_a, _treasury) = setup();
794
795 let bridge_b: [u8; 20] = [0x02; 20];
796 mgr.authorize_bridge(
797 bridge_b,
798 "BridgeB".to_string(),
799 vec![],
800 Some(5_000 * ONE_TNZO),
801 Some(5_000 * ONE_TNZO),
802 None,
803 None,
804 )
805 .unwrap();
806
807 let to = Address::new([0xCC; 32]);
808
809 mgr.crosschain_mint(bridge_a, to, 1_000 * ONE_TNZO, vec![])
811 .unwrap();
812 mgr.crosschain_mint(bridge_b, to, 2_000 * ONE_TNZO, vec![])
813 .unwrap();
814
815 assert_eq!(mgr.tnzo_token.balance_of(&to), 3_000 * ONE_TNZO);
816
817 let bridges = mgr.list_authorized_bridges();
818 assert_eq!(bridges.len(), 2);
819 }
820
821 #[test]
822 fn test_crosschain_mint_zero_amount_rejected() {
823 let (mgr, bridge, _treasury) = setup();
824
825 let to = Address::new([0xCC; 32]);
826 let result = mgr.crosschain_mint(bridge, to, 0, vec![]);
827 assert!(result.is_err());
828 let err = result.unwrap_err().to_string();
829 assert!(err.contains("greater than zero"));
830 }
831
832 #[test]
833 fn test_update_bridge_limits() {
834 let (mgr, bridge, _treasury) = setup();
835
836 mgr.update_bridge_limits(&bridge, Some(50_000 * ONE_TNZO), None, None, None)
837 .unwrap();
838
839 let info = mgr.get_bridge_info(&bridge).unwrap();
840 assert_eq!(info.max_mint_per_tx, Some(50_000 * ONE_TNZO));
841 assert_eq!(info.max_burn_per_tx, None);
843 }
844
845 #[test]
846 fn test_revoke_nonexistent_bridge_fails() {
847 let (mgr, _bridge, _treasury) = setup();
848
849 let fake: [u8; 20] = [0xFE; 20];
850 let result = mgr.revoke_bridge(&fake);
851 assert!(result.is_err());
852 }
853
854 #[test]
855 fn test_daily_burn_limit() {
856 let token = Arc::new(TnzoToken::new());
857 let registry = Arc::new(TokenRegistry::new());
858
859 let treasury = Address::new([0xAA; 32]);
860 token.set_treasury_address(treasury);
861
862 let from = Address::new([0xBB; 32]);
864 token.mint(&from, 10_000_000 * ONE_TNZO, &treasury).unwrap();
865
866 let mgr = CrosschainTokenManager::new(token, registry);
867
868 let bridge: [u8; 20] = [0x05; 20];
869 mgr.authorize_bridge(
870 bridge,
871 "LimitedBridge".to_string(),
872 vec![],
873 None,
874 Some(5_000 * ONE_TNZO),
875 None,
876 Some(10_000 * ONE_TNZO), )
878 .unwrap();
879
880 mgr.crosschain_burn(bridge, from, 5_000 * ONE_TNZO, vec![])
882 .unwrap();
883 mgr.crosschain_burn(bridge, from, 5_000 * ONE_TNZO, vec![])
884 .unwrap();
885
886 let result = mgr.crosschain_burn(bridge, from, ONE_TNZO, vec![]);
888 assert!(result.is_err());
889 let err = result.unwrap_err().to_string();
890 assert!(err.contains("daily limit"));
891 }
892}