Skip to main content

tenzro_token/
erc7802.rs

1//! ERC-7802 Cross-Chain Token Standard
2//!
3//! Implements the crosschainMint/crosschainBurn interface proposed by OP Labs
4//! for standardized bridge interop. Bridges are authorized via an ACL and can
5//! mint/burn tokens atomically on behalf of cross-chain transfers.
6//!
7//! See: https://eips.ethereum.org/EIPS/eip-7802
8
9use 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
20// ---------------------------------------------------------------------------
21// Portable AtomicU128 — std AtomicU128 is not available on all targets, so we
22// wrap a parking_lot::Mutex<u128> behind the same load/store API.
23// ---------------------------------------------------------------------------
24
25struct 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// ---------------------------------------------------------------------------
49// Types
50// ---------------------------------------------------------------------------
51
52/// Event emitted after a successful cross-chain mint.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct CrosschainMintEvent {
55    /// Unique nonce for this event.
56    pub nonce: u64,
57    /// Recipient address on this chain.
58    pub to: Address,
59    /// Amount minted (in smallest TNZO unit, 18 decimals).
60    pub amount: u128,
61    /// Opaque sender identifier on the source chain.
62    pub sender: Vec<u8>,
63    /// Bridge address that executed the mint.
64    pub bridge: [u8; 20],
65    /// Unix timestamp (seconds) when the event was created.
66    pub timestamp: i64,
67}
68
69/// Event emitted after a successful cross-chain burn.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CrosschainBurnEvent {
72    /// Unique nonce for this event.
73    pub nonce: u64,
74    /// Address whose tokens were burned.
75    pub from: Address,
76    /// Amount burned (in smallest TNZO unit, 18 decimals).
77    pub amount: u128,
78    /// Opaque destination identifier on the target chain.
79    pub sender: Vec<u8>,
80    /// Bridge address that executed the burn.
81    pub bridge: [u8; 20],
82    /// Unix timestamp (seconds) when the event was created.
83    pub timestamp: i64,
84}
85
86/// Authorization record for a bridge that may call crosschainMint/crosschainBurn.
87pub struct BridgeAuthorization {
88    /// 20-byte bridge address (EVM-style).
89    pub bridge_address: [u8; 20],
90    /// Human-readable name of the bridge.
91    pub name: String,
92    /// Token IDs this bridge may mint/burn. Empty means all tokens.
93    pub authorized_tokens: Vec<TokenId>,
94    /// Maximum amount that can be minted in a single transaction.
95    pub max_mint_per_tx: Option<u128>,
96    /// Maximum amount that can be burned in a single transaction.
97    pub max_burn_per_tx: Option<u128>,
98    /// Maximum cumulative mint amount per 24-hour window.
99    pub daily_mint_limit: Option<u128>,
100    /// Maximum cumulative burn amount per 24-hour window.
101    pub daily_burn_limit: Option<u128>,
102    /// Cumulative amount minted since last daily reset.
103    minted_today: AtomicU128,
104    /// Cumulative amount burned since last daily reset.
105    burned_today: AtomicU128,
106    /// Unix timestamp of the last daily counter reset.
107    last_reset: AtomicI64,
108    /// Whether this authorization is currently active.
109    pub enabled: bool,
110}
111
112impl BridgeAuthorization {
113    /// Resets the daily counters if 24 hours have elapsed since the last reset.
114    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/// Serializable snapshot of a bridge authorization (for API responses).
125#[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
139// ---------------------------------------------------------------------------
140// CrosschainTokenManager
141// ---------------------------------------------------------------------------
142
143/// Manages ERC-7802 crosschainMint/crosschainBurn operations.
144///
145/// Bridges must be explicitly authorized before they can mint or burn tokens.
146/// Per-transaction and daily limits are enforced to bound risk.
147pub struct CrosschainTokenManager {
148    /// Bridge address -> authorization record.
149    authorized_bridges: DashMap<[u8; 20], BridgeAuthorization>,
150    /// Reference to the native TNZO token (mint/burn target).
151    tnzo_token: Arc<TnzoToken>,
152    /// Reference to the unified token registry. Used by
153    /// [`Self::resolve_authorized_tokens`] to project a bridge's
154    /// `authorized_tokens` set into full `TokenInfo` records for
155    /// observability and registry-aware authorization.
156    registry: Arc<TokenRegistry>,
157    /// Mint event log, keyed by nonce.
158    mint_events: DashMap<u64, CrosschainMintEvent>,
159    /// Burn event log, keyed by nonce.
160    burn_events: DashMap<u64, CrosschainBurnEvent>,
161    /// Monotonically increasing nonce for events.
162    nonce: AtomicU64,
163}
164
165impl CrosschainTokenManager {
166    /// Creates a new manager backed by the given token and registry instances.
167    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    // -- Bridge management ---------------------------------------------------
179
180    /// Authorizes a bridge to call crosschainMint/crosschainBurn.
181    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    /// Revokes a bridge's authorization. In-flight operations that already
216    /// passed the authorization check will still complete, but new calls will
217    /// be rejected.
218    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    /// Updates the per-tx and daily limits for an authorized bridge.
238    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    /// Returns `true` if the bridge is authorized and enabled.
270    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    /// Returns a serializable snapshot of the bridge's authorization record.
278    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    /// Resolves a bridge's `authorized_tokens` set into full
301    /// [`TokenDefinition`] records via the unified token registry.
302    ///
303    /// Tokens that no longer exist in the registry are silently skipped,
304    /// allowing operators to detect orphaned authorizations by length
305    /// comparison against the bridge's raw `authorized_tokens` vector.
306    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    /// Lists all currently authorized bridges.
323    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    // -- Core ERC-7802 operations --------------------------------------------
349
350    /// Validates that a bridge is authorized and within limits for a given
351    /// operation (mint or burn).
352    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            // Per-tx limit
390            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            // Daily limit
399            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            // Per-tx limit
410            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            // Daily limit
419            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    /// Returns the current Unix timestamp in seconds.
434    fn now_secs() -> i64 {
435        chrono::Utc::now().timestamp()
436    }
437
438    /// Mints tokens on this chain as part of a cross-chain transfer.
439    ///
440    /// The bridge must be authorized and within its per-tx and daily limits.
441    /// Tokens are minted via `TnzoToken::mint()` using the treasury address as
442    /// the authorized caller.
443    ///
444    /// # Arguments
445    ///
446    /// * `bridge` - 20-byte address of the calling bridge.
447    /// * `to` - Recipient address on this chain.
448    /// * `amount` - Amount to mint (smallest unit, 18 decimals).
449    /// * `sender` - Opaque sender identifier on the source chain.
450    pub fn crosschain_mint(
451        &self,
452        bridge: [u8; 20],
453        to: Address,
454        amount: u128,
455        sender: Vec<u8>,
456    ) -> Result<CrosschainMintEvent> {
457        // 1. Validate bridge authorization and limits
458        self.validate_bridge(&bridge, amount, true)?;
459
460        // 2. Mint tokens via TnzoToken. The caller must be the treasury.
461        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        // 3. Update daily tracking
471        {
472            let entry = self.authorized_bridges.get(&bridge).unwrap();
473            entry.value().minted_today.fetch_add(amount);
474        }
475
476        // 4. Build and store event
477        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    /// Burns tokens on this chain as part of a cross-chain transfer.
500    ///
501    /// The bridge must be authorized and within its per-tx and daily limits.
502    /// Tokens are burned via `TnzoToken::burn()`.
503    ///
504    /// # Arguments
505    ///
506    /// * `bridge` - 20-byte address of the calling bridge.
507    /// * `from` - Address whose tokens are burned.
508    /// * `amount` - Amount to burn (smallest unit, 18 decimals).
509    /// * `sender` - Opaque destination identifier on the target chain.
510    pub fn crosschain_burn(
511        &self,
512        bridge: [u8; 20],
513        from: Address,
514        amount: u128,
515        sender: Vec<u8>,
516    ) -> Result<CrosschainBurnEvent> {
517        // 1. Validate bridge authorization and limits
518        self.validate_bridge(&bridge, amount, false)?;
519
520        // 2. Burn tokens
521        self.tnzo_token.burn(&from, amount)?;
522
523        // 3. Update daily tracking
524        {
525            let entry = self.authorized_bridges.get(&bridge).unwrap();
526            entry.value().burned_today.fetch_add(amount);
527        }
528
529        // 4. Build and store event
530        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    // -- Event queries -------------------------------------------------------
553
554    /// Returns all recorded mint events, newest first.
555    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    /// Returns all recorded burn events, newest first.
566    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    /// Returns a specific mint event by nonce.
577    pub fn get_mint_event(&self, nonce: u64) -> Option<CrosschainMintEvent> {
578        self.mint_events.get(&nonce).map(|e| e.value().clone())
579    }
580
581    /// Returns a specific burn event by nonce.
582    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// ---------------------------------------------------------------------------
588// Tests
589// ---------------------------------------------------------------------------
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594    use crate::tnzo::ONE_TNZO;
595
596    /// Helper: creates a manager with a funded treasury and a single authorized bridge.
597    fn setup() -> (CrosschainTokenManager, [u8; 20], Address) {
598        let token = Arc::new(TnzoToken::new());
599        let registry = Arc::new(TokenRegistry::new());
600
601        // Set up treasury and pre-mint some tokens for burn tests
602        let treasury = Address::new([0xAA; 32]);
603        token.set_treasury_address(treasury);
604
605        // Pre-mint supply so we can test minting and burning
606        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        // The per-tx mint limit is 10_000 TNZO
683        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        // Mint up to close to the daily limit (100_000 TNZO) in 10_000-chunks
697        for _ in 0..10 {
698            mgr.crosschain_mint(bridge, to, 10_000 * ONE_TNZO, vec![])
699                .unwrap();
700        }
701
702        // 100_000 already minted; next should fail
703        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        // The pre-minted address has 1_000_000 TNZO
714        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        // Address with no balance
732        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        // Minting should fail now
758        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        // Lookup by nonce
786        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        // Both bridges should be able to mint independently
810        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        // Burn limit was Some(10_000) but we passed None so it gets overwritten
842        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        // Give the burn source a large balance
863        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), // daily burn limit
877        )
878        .unwrap();
879
880        // Burn twice at 5_000 each = 10_000 total
881        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        // Third burn should fail
887        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}