Skip to main content

tenzro_token/
tnzo.rs

1//! TNZO token management
2//!
3//! This module implements the TNZO governance/utility token with 18-decimal precision.
4
5use crate::error::{Result, TokenError};
6use dashmap::DashMap;
7use serde::{Deserialize, Serialize};
8use tenzro_types::primitives::Address;
9use tenzro_types::asset::AssetId;
10use tenzro_storage::kv::{KvStore, CF_ACCOUNTS};
11use std::sync::Arc;
12use tracing::{debug, info, warn};
13
14/// Decimals for TNZO token (18 decimal places)
15pub const TNZO_DECIMALS: u8 = 18;
16
17/// Default circuit breaker: max 10% of total supply per window
18const CIRCUIT_BREAKER_MAX_OUTFLOW_PERCENT: u128 = 10;
19/// Default circuit breaker window: 1 hour (3600 seconds)
20const CIRCUIT_BREAKER_WINDOW_SECS: u64 = 3600;
21/// Default cooldown after trip: 30 minutes (1800 seconds)
22const CIRCUIT_BREAKER_COOLDOWN_SECS: u64 = 1800;
23
24/// One TNZO token in smallest unit (10^18)
25pub const ONE_TNZO: u128 = 1_000_000_000_000_000_000;
26
27/// Maximum supply of TNZO (1 billion tokens)
28pub const MAX_SUPPLY: u128 = 1_000_000_000 * ONE_TNZO;
29
30// ---------------------------------------------------------------------------
31// Circuit Breaker — ERC-7265 pattern for outflow rate limiting
32// ---------------------------------------------------------------------------
33
34/// Circuit breaker for monitoring and limiting token outflow rates.
35///
36/// Implements the ERC-7265 pattern: tracks cumulative outflow within a sliding
37/// time window and automatically trips (pauses transfers) when the outflow
38/// exceeds a configurable threshold. After a cooldown period, the breaker
39/// automatically resets and transfers resume.
40pub struct CircuitBreaker {
41    /// Maximum outflow per window (in smallest TNZO units)
42    max_outflow_per_window: u128,
43    /// Window duration in seconds
44    window_seconds: u64,
45    /// Current window outflow
46    current_outflow: parking_lot::RwLock<u128>,
47    /// Window start timestamp (Unix seconds)
48    window_start: parking_lot::RwLock<u64>,
49    /// Whether circuit breaker is tripped
50    tripped: parking_lot::RwLock<bool>,
51    /// Cooldown period after trip (seconds)
52    cooldown_seconds: u64,
53    /// Trip timestamp (Unix seconds)
54    tripped_at: parking_lot::RwLock<Option<u64>>,
55}
56
57impl CircuitBreaker {
58    /// Creates a new circuit breaker with the given parameters.
59    ///
60    /// # Arguments
61    /// * `max_outflow_per_window` - Maximum cumulative outflow before tripping
62    /// * `window_seconds` - Duration of the monitoring window
63    /// * `cooldown_seconds` - How long the breaker stays tripped before auto-reset
64    pub fn new(max_outflow_per_window: u128, window_seconds: u64, cooldown_seconds: u64) -> Self {
65        let now = std::time::SystemTime::now()
66            .duration_since(std::time::UNIX_EPOCH)
67            .unwrap_or_default()
68            .as_secs();
69
70        Self {
71            max_outflow_per_window,
72            window_seconds,
73            current_outflow: parking_lot::RwLock::new(0),
74            window_start: parking_lot::RwLock::new(now),
75            tripped: parking_lot::RwLock::new(false),
76            cooldown_seconds,
77            tripped_at: parking_lot::RwLock::new(None),
78        }
79    }
80
81    /// Creates a default circuit breaker: 10% of MAX_SUPPLY per 1-hour window,
82    /// 30-minute cooldown.
83    pub fn default_for_tnzo() -> Self {
84        let max_outflow = MAX_SUPPLY / 100 * CIRCUIT_BREAKER_MAX_OUTFLOW_PERCENT;
85        Self::new(max_outflow, CIRCUIT_BREAKER_WINDOW_SECS, CIRCUIT_BREAKER_COOLDOWN_SECS)
86    }
87
88    /// Returns the current Unix timestamp in seconds.
89    fn now_secs() -> u64 {
90        std::time::SystemTime::now()
91            .duration_since(std::time::UNIX_EPOCH)
92            .unwrap_or_default()
93            .as_secs()
94    }
95
96    /// Checks whether the circuit breaker is currently tripped.
97    /// Also handles auto-reset after the cooldown period.
98    pub fn is_tripped(&self) -> bool {
99        let tripped = *self.tripped.read();
100        if !tripped {
101            return false;
102        }
103
104        // Check if cooldown has elapsed
105        if let Some(trip_time) = *self.tripped_at.read() {
106            let now = Self::now_secs();
107            if now >= trip_time + self.cooldown_seconds {
108                // Auto-reset after cooldown
109                self.reset_internal();
110                return false;
111            }
112        }
113
114        true
115    }
116
117    /// Checks whether adding `amount` to the current window outflow would
118    /// exceed the maximum. Returns Ok(()) if the transfer is allowed, or
119    /// an error if the circuit breaker would trip.
120    ///
121    /// This method also handles window rotation: if the current window has
122    /// expired, it starts a new window before checking.
123    pub fn check_outflow(&self, amount: u128) -> Result<()> {
124        if self.is_tripped() {
125            let remaining = self.tripped_at.read()
126                .map(|t| {
127                    let elapsed = Self::now_secs().saturating_sub(t);
128                    self.cooldown_seconds.saturating_sub(elapsed)
129                })
130                .unwrap_or(self.cooldown_seconds);
131
132            return Err(TokenError::Unauthorized {
133                reason: format!(
134                    "Circuit breaker tripped: outflow limit exceeded. \
135                     Auto-reset in {} seconds.",
136                    remaining
137                ),
138            });
139        }
140
141        let now = Self::now_secs();
142        let mut window_start = self.window_start.write();
143        let mut current_outflow = self.current_outflow.write();
144
145        // Rotate window if expired
146        if now >= *window_start + self.window_seconds {
147            *window_start = now;
148            *current_outflow = 0;
149        }
150
151        // Check if the new outflow would exceed the limit
152        let new_outflow = current_outflow
153            .checked_add(amount)
154            .ok_or_else(|| TokenError::ArithmeticOverflow {
155                operation: "circuit breaker outflow check".to_string(),
156            })?;
157
158        if new_outflow > self.max_outflow_per_window {
159            // Trip the breaker
160            drop(window_start);
161            drop(current_outflow);
162            self.trip();
163            return Err(TokenError::Unauthorized {
164                reason: format!(
165                    "Circuit breaker tripped: transfer of {} would exceed \
166                     window limit of {} ({} already used). \
167                     Cooldown: {} seconds.",
168                    amount,
169                    self.max_outflow_per_window,
170                    new_outflow - amount,
171                    self.cooldown_seconds,
172                ),
173            });
174        }
175
176        Ok(())
177    }
178
179    /// Records a successful outflow of `amount` in the current window.
180    pub fn record_outflow(&self, amount: u128) {
181        let mut current_outflow = self.current_outflow.write();
182        *current_outflow = current_outflow.saturating_add(amount);
183    }
184
185    /// Manually resets the circuit breaker (for authorized callers).
186    pub fn reset(&self) {
187        self.reset_internal();
188        info!("Circuit breaker manually reset");
189    }
190
191    /// Internal reset helper.
192    fn reset_internal(&self) {
193        *self.tripped.write() = false;
194        *self.tripped_at.write() = None;
195        *self.current_outflow.write() = 0;
196        *self.window_start.write() = Self::now_secs();
197    }
198
199    /// Trips the circuit breaker.
200    fn trip(&self) {
201        *self.tripped.write() = true;
202        *self.tripped_at.write() = Some(Self::now_secs());
203        warn!(
204            "Circuit breaker TRIPPED: outflow limit of {} exceeded in {}-second window. \
205             Transfers paused for {} seconds.",
206            self.max_outflow_per_window,
207            self.window_seconds,
208            self.cooldown_seconds,
209        );
210    }
211
212    /// Returns the current outflow in the active window.
213    pub fn current_outflow(&self) -> u128 {
214        *self.current_outflow.read()
215    }
216
217    /// Returns the maximum outflow per window.
218    pub fn max_outflow(&self) -> u128 {
219        self.max_outflow_per_window
220    }
221}
222
223impl std::fmt::Debug for CircuitBreaker {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        f.debug_struct("CircuitBreaker")
226            .field("max_outflow_per_window", &self.max_outflow_per_window)
227            .field("window_seconds", &self.window_seconds)
228            .field("cooldown_seconds", &self.cooldown_seconds)
229            .field("tripped", &*self.tripped.read())
230            .field("current_outflow", &*self.current_outflow.read())
231            .finish()
232    }
233}
234
235/// Storage backend trait for TNZO token balances
236pub trait StorageBackend: Send + Sync {
237    /// Gets the balance of an address
238    fn get_balance(&self, address: &Address) -> Result<Option<u128>>;
239
240    /// Sets the balance of an address
241    fn set_balance(&self, address: &Address, balance: u128) -> Result<()>;
242
243    /// Gets the total supply
244    fn get_total_supply(&self) -> Result<u128>;
245
246    /// Sets the total supply
247    fn set_total_supply(&self, supply: u128) -> Result<()>;
248}
249
250/// RocksDB-backed storage backend
251pub struct RocksDbBackend {
252    store: Arc<dyn KvStore>,
253}
254
255impl RocksDbBackend {
256    /// Creates a new RocksDB backend
257    pub fn new(store: Arc<dyn KvStore>) -> Self {
258        Self { store }
259    }
260
261    /// Creates the key for a balance entry
262    fn balance_key(address: &Address) -> Vec<u8> {
263        let mut key = b"balance:".to_vec();
264        key.extend_from_slice(address.as_bytes());
265        key
266    }
267
268    /// Key for total supply
269    fn supply_key() -> Vec<u8> {
270        b"total_supply".to_vec()
271    }
272}
273
274impl StorageBackend for RocksDbBackend {
275    fn get_balance(&self, address: &Address) -> Result<Option<u128>> {
276        let key = Self::balance_key(address);
277        match self.store.get(CF_ACCOUNTS, &key)? {
278            Some(bytes) => {
279                if bytes.len() == 16 {
280                    let array: [u8; 16] = bytes.try_into().unwrap();
281                    Ok(Some(u128::from_le_bytes(array)))
282                } else {
283                    warn!("Invalid balance bytes length for {}: {}", address, bytes.len());
284                    Ok(None)
285                }
286            }
287            None => Ok(None),
288        }
289    }
290
291    fn set_balance(&self, address: &Address, balance: u128) -> Result<()> {
292        let key = Self::balance_key(address);
293        let value = balance.to_le_bytes();
294        self.store.put(CF_ACCOUNTS, &key, &value)?;
295        Ok(())
296    }
297
298    fn get_total_supply(&self) -> Result<u128> {
299        let key = Self::supply_key();
300        match self.store.get(CF_ACCOUNTS, &key)? {
301            Some(bytes) => {
302                if bytes.len() == 16 {
303                    let array: [u8; 16] = bytes.try_into().unwrap();
304                    Ok(u128::from_le_bytes(array))
305                } else {
306                    warn!("Invalid supply bytes length: {}", bytes.len());
307                    Ok(0)
308                }
309            }
310            None => Ok(0),
311        }
312    }
313
314    fn set_total_supply(&self, supply: u128) -> Result<()> {
315        let key = Self::supply_key();
316        let value = supply.to_le_bytes();
317        self.store.put(CF_ACCOUNTS, &key, &value)?;
318        Ok(())
319    }
320}
321
322/// TNZO token manager
323///
324/// Manages TNZO token balances, transfers, minting, and burning.
325/// Uses `u128` for amounts to properly handle 18-decimal precision.
326/// Balances are cached in memory and persisted to storage backend.
327pub struct TnzoToken {
328    /// Asset ID for TNZO token
329    pub asset_id: AssetId,
330    /// Account balances cache (Address -> Balance)
331    balances: DashMap<Address, u128>,
332    /// Total supply in circulation
333    total_supply: parking_lot::RwLock<u128>,
334    /// Total burned amount
335    total_burned: parking_lot::RwLock<u128>,
336    /// Treasury address (only authorized to mint)
337    treasury_address: parking_lot::RwLock<Option<Address>>,
338    /// Optional storage backend for persistence
339    storage: Option<Arc<dyn StorageBackend>>,
340    /// Circuit breaker for outflow monitoring (ERC-7265 pattern)
341    circuit_breaker: CircuitBreaker,
342}
343
344impl std::fmt::Debug for TnzoToken {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        f.debug_struct("TnzoToken")
347            .field("asset_id", &self.asset_id)
348            .field("total_supply", &self.total_supply)
349            .field("total_burned", &self.total_burned)
350            .field("treasury_address", &self.treasury_address)
351            .field("storage", &self.storage.as_ref().map(|_| "Some(...)"))
352            .field("circuit_breaker", &self.circuit_breaker)
353            .finish()
354    }
355}
356
357impl TnzoToken {
358    /// Creates a new TNZO token instance without storage backend (in-memory only)
359    pub fn new() -> Self {
360        Self {
361            asset_id: AssetId::tnzo(),
362            balances: DashMap::new(),
363            total_supply: parking_lot::RwLock::new(0),
364            total_burned: parking_lot::RwLock::new(0),
365            treasury_address: parking_lot::RwLock::new(None),
366            storage: None,
367            circuit_breaker: CircuitBreaker::default_for_tnzo(),
368        }
369    }
370
371    /// Creates a new TNZO token instance with storage backend
372    pub fn with_storage(storage: Arc<dyn StorageBackend>) -> Result<Self> {
373        // Load total supply from storage
374        let total_supply = storage.get_total_supply()?;
375
376        Ok(Self {
377            asset_id: AssetId::tnzo(),
378            balances: DashMap::new(),
379            total_supply: parking_lot::RwLock::new(total_supply),
380            total_burned: parking_lot::RwLock::new(0),
381            treasury_address: parking_lot::RwLock::new(None),
382            storage: Some(storage),
383            circuit_breaker: CircuitBreaker::default_for_tnzo(),
384        })
385    }
386
387    /// Sets the treasury address (only address authorized to mint)
388    pub fn set_treasury_address(&self, address: Address) {
389        *self.treasury_address.write() = Some(address);
390        info!("Treasury address set to: {}", address);
391    }
392
393    /// Returns the treasury address, if set
394    pub fn treasury_address_ref(&self) -> Option<Address> {
395        *self.treasury_address.read()
396    }
397
398    /// Returns the balance of an address.
399    ///
400    /// When a storage backend is configured, reads always go to storage
401    /// (CF_ACCOUNTS). The in-memory `balances` map is only used as a fallback
402    /// for stand-alone token instances that have no storage attached
403    /// (e.g. unit tests). RocksDB has its own block cache underneath, so
404    /// re-reading on every call is cheap.
405    ///
406    /// Why not cache in `self.balances` after a storage read? The VM's
407    /// `StateAdapter::commit` writes balance updates directly to CF_ACCOUNTS
408    /// (using the same `tnzo_balance_key` encoding) and does NOT invalidate
409    /// `TnzoToken::balances`. Caching here would let stale values shadow
410    /// post-VM-execution balances — exactly the bug that made
411    /// `eth_sendRawTransaction` Transfers appear to land successfully but
412    /// not move balances on the read side.
413    pub fn balance_of(&self, address: &Address) -> u128 {
414        // Storage is the canonical source of truth when configured.
415        if let Some(storage) = &self.storage {
416            return match storage.get_balance(address) {
417                Ok(Some(balance)) => balance,
418                Ok(None) => 0,
419                Err(e) => {
420                    warn!("Failed to load balance from storage: {}", e);
421                    0
422                }
423            };
424        }
425
426        // Storage-less mode: rely on the in-memory map (test/dev only).
427        self.balances.get(address).map(|v| *v).unwrap_or(0)
428    }
429
430    /// Persists a balance to storage if backend exists
431    fn persist_balance(&self, address: &Address, balance: u128) -> Result<()> {
432        if let Some(storage) = &self.storage {
433            storage.set_balance(address, balance)?;
434        }
435        Ok(())
436    }
437
438    /// Persists total supply to storage if backend exists
439    fn persist_supply(&self, supply: u128) -> Result<()> {
440        if let Some(storage) = &self.storage {
441            storage.set_total_supply(supply)?;
442        }
443        Ok(())
444    }
445
446    /// Transfers tokens from one address to another.
447    ///
448    /// The transfer is subject to the ERC-7265 circuit breaker: if cumulative
449    /// outflow within the current window would exceed the configured limit,
450    /// the breaker trips and the transfer is rejected.
451    ///
452    /// # Arguments
453    ///
454    /// * `from` - Source address
455    /// * `to` - Destination address
456    /// * `amount` - Amount to transfer (in smallest unit)
457    pub fn transfer(&self, from: &Address, to: &Address, amount: u128) -> Result<()> {
458        if amount == 0 {
459            return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
460        }
461
462        // ERC-7265 circuit breaker check
463        self.circuit_breaker.check_outflow(amount)?;
464
465        let from_balance = self.balance_of(from);
466        if from_balance < amount {
467            return Err(TokenError::InsufficientBalance {
468                required: amount,
469                available: from_balance,
470            });
471        }
472
473        // Use checked_sub for sender
474        let new_from_balance = from_balance.checked_sub(amount)
475            .ok_or_else(|| TokenError::ArithmeticOverflow {
476                operation: "transfer subtraction".to_string(),
477            })?;
478
479        // Use checked_add for recipient
480        let to_balance = self.balance_of(to);
481        let new_to_balance = to_balance.checked_add(amount)
482            .ok_or_else(|| TokenError::ArithmeticOverflow {
483                operation: "transfer addition".to_string(),
484            })?;
485
486        // Update balances in cache
487        self.balances.insert(*from, new_from_balance);
488        self.balances.insert(*to, new_to_balance);
489
490        // Persist to storage
491        self.persist_balance(from, new_from_balance)?;
492        self.persist_balance(to, new_to_balance)?;
493
494        // Record outflow in circuit breaker
495        self.circuit_breaker.record_outflow(amount);
496
497        debug!("Transferred {} TNZO from {} to {}", amount, from, to);
498        Ok(())
499    }
500
501    /// Mints new tokens (only callable by treasury)
502    ///
503    /// # Arguments
504    ///
505    /// * `to` - Address to mint tokens to
506    /// * `amount` - Amount to mint
507    /// * `caller` - Address calling the mint function (must be treasury)
508    pub fn mint(&self, to: &Address, amount: u128, caller: &Address) -> Result<()> {
509        // Check authorization
510        let treasury = self.treasury_address.read();
511        if treasury.as_ref() != Some(caller) {
512            return Err(TokenError::Unauthorized {
513                reason: "Only treasury can mint tokens".to_string(),
514            });
515        }
516
517        if amount == 0 {
518            return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
519        }
520
521        // Check max supply
522        let current_supply = *self.total_supply.read();
523        let new_supply = current_supply.checked_add(amount)
524            .ok_or_else(|| TokenError::ArithmeticOverflow {
525                operation: "mint supply increase".to_string(),
526            })?;
527
528        if new_supply > MAX_SUPPLY {
529            return Err(TokenError::InvalidAmount(
530                format!("Minting would exceed max supply: {} > {}", new_supply, MAX_SUPPLY)
531            ));
532        }
533
534        // Update balance
535        let current_balance = self.balance_of(to);
536        let new_balance = current_balance.checked_add(amount)
537            .ok_or_else(|| TokenError::ArithmeticOverflow {
538                operation: "mint balance increase".to_string(),
539            })?;
540        self.balances.insert(*to, new_balance);
541
542        // Update total supply
543        *self.total_supply.write() = new_supply;
544
545        // Persist to storage
546        self.persist_balance(to, new_balance)?;
547        self.persist_supply(new_supply)?;
548
549        info!("Minted {} TNZO to {}", amount, to);
550        Ok(())
551    }
552
553    /// Burns tokens from an address
554    ///
555    /// # Arguments
556    ///
557    /// * `from` - Address to burn tokens from
558    /// * `amount` - Amount to burn
559    pub fn burn(&self, from: &Address, amount: u128) -> Result<()> {
560        if amount == 0 {
561            return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
562        }
563
564        let balance = self.balance_of(from);
565        if balance < amount {
566            return Err(TokenError::InsufficientBalance {
567                required: amount,
568                available: balance,
569            });
570        }
571
572        // Use checked_sub for balance
573        let new_balance = balance.checked_sub(amount)
574            .ok_or_else(|| TokenError::ArithmeticOverflow {
575                operation: "burn balance decrease".to_string(),
576            })?;
577        self.balances.insert(*from, new_balance);
578
579        // Update total supply with checked_sub
580        let mut supply = self.total_supply.write();
581        *supply = supply.checked_sub(amount)
582            .ok_or_else(|| TokenError::ArithmeticOverflow {
583                operation: "burn supply decrease".to_string(),
584            })?;
585        let new_supply = *supply;
586        drop(supply);
587
588        // Update burned with checked_add
589        let mut burned = self.total_burned.write();
590        *burned = burned.checked_add(amount)
591            .ok_or_else(|| TokenError::ArithmeticOverflow {
592                operation: "burn total increase".to_string(),
593            })?;
594
595        // Persist to storage
596        self.persist_balance(from, new_balance)?;
597        self.persist_supply(new_supply)?;
598
599        info!("Burned {} TNZO from {}", amount, from);
600        Ok(())
601    }
602
603    /// Returns the total supply of TNZO
604    pub fn total_supply(&self) -> u128 {
605        *self.total_supply.read()
606    }
607
608    /// Returns the circulating supply (total supply - burned)
609    pub fn circulating_supply(&self) -> u128 {
610        self.total_supply()
611    }
612
613    /// Returns the total amount burned
614    pub fn total_burned(&self) -> u128 {
615        *self.total_burned.read()
616    }
617
618    /// Returns token statistics
619    pub fn stats(&self) -> TokenStats {
620        TokenStats {
621            total_supply: self.total_supply(),
622            circulating_supply: self.circulating_supply(),
623            total_burned: self.total_burned(),
624            total_accounts: self.balances.len() as u64,
625        }
626    }
627
628    /// Returns a reference to the circuit breaker.
629    pub fn circuit_breaker(&self) -> &CircuitBreaker {
630        &self.circuit_breaker
631    }
632
633    /// Returns all balances (for debugging/testing)
634    pub fn get_all_balances(&self) -> Vec<(Address, u128)> {
635        self.balances
636            .iter()
637            .map(|entry| (*entry.key(), *entry.value()))
638            .collect()
639    }
640}
641
642impl Default for TnzoToken {
643    fn default() -> Self {
644        Self::new()
645    }
646}
647
648/// TNZO token statistics
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct TokenStats {
651    /// Total supply of TNZO
652    pub total_supply: u128,
653    /// Circulating supply
654    pub circulating_supply: u128,
655    /// Total burned
656    pub total_burned: u128,
657    /// Total number of accounts with balance
658    pub total_accounts: u64,
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn test_token_creation() {
667        let token = TnzoToken::new();
668        assert_eq!(token.total_supply(), 0);
669        assert_eq!(token.total_burned(), 0);
670    }
671
672    #[test]
673    fn test_transfer() {
674        let token = TnzoToken::new();
675        let from = Address::new([1u8; 32]);
676        let to = Address::new([2u8; 32]);
677
678        // Setup: Give initial balance to 'from'
679        token.balances.insert(from, 1000 * ONE_TNZO);
680
681        // Transfer
682        token.transfer(&from, &to, 100 * ONE_TNZO).unwrap();
683
684        assert_eq!(token.balance_of(&from), 900 * ONE_TNZO);
685        assert_eq!(token.balance_of(&to), 100 * ONE_TNZO);
686    }
687
688    #[test]
689    fn test_mint() {
690        let token = TnzoToken::new();
691        let treasury = Address::new([1u8; 32]);
692        let recipient = Address::new([2u8; 32]);
693
694        token.set_treasury_address(treasury);
695        token.mint(&recipient, 1000 * ONE_TNZO, &treasury).unwrap();
696
697        assert_eq!(token.balance_of(&recipient), 1000 * ONE_TNZO);
698        assert_eq!(token.total_supply(), 1000 * ONE_TNZO);
699    }
700
701    #[test]
702    fn test_burn() {
703        let token = TnzoToken::new();
704        let address = Address::new([1u8; 32]);
705
706        // Setup: Give initial balance
707        token.balances.insert(address, 1000 * ONE_TNZO);
708        *token.total_supply.write() = 1000 * ONE_TNZO;
709
710        // Burn
711        token.burn(&address, 100 * ONE_TNZO).unwrap();
712
713        assert_eq!(token.balance_of(&address), 900 * ONE_TNZO);
714        assert_eq!(token.total_supply(), 900 * ONE_TNZO);
715        assert_eq!(token.total_burned(), 100 * ONE_TNZO);
716    }
717
718    // -----------------------------------------------------------------------
719    // Circuit breaker tests (ERC-7265)
720    // -----------------------------------------------------------------------
721
722    #[test]
723    fn test_circuit_breaker_allows_normal_transfer() {
724        // Small max to make testing easy: 1000 TNZO window
725        let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
726        assert!(!cb.is_tripped());
727        assert!(cb.check_outflow(500 * ONE_TNZO).is_ok());
728        cb.record_outflow(500 * ONE_TNZO);
729        assert_eq!(cb.current_outflow(), 500 * ONE_TNZO);
730    }
731
732    #[test]
733    fn test_circuit_breaker_trips_on_excess() {
734        let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
735        cb.record_outflow(900 * ONE_TNZO);
736        // This should trip the breaker
737        let result = cb.check_outflow(200 * ONE_TNZO);
738        assert!(result.is_err());
739        assert!(cb.is_tripped());
740    }
741
742    #[test]
743    fn test_circuit_breaker_blocks_after_trip() {
744        let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
745        cb.record_outflow(900 * ONE_TNZO);
746        let _ = cb.check_outflow(200 * ONE_TNZO); // trips
747        assert!(cb.is_tripped());
748
749        // Even small transfers should be blocked
750        let result = cb.check_outflow(1);
751        assert!(result.is_err());
752    }
753
754    #[test]
755    fn test_circuit_breaker_manual_reset() {
756        let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
757        cb.record_outflow(900 * ONE_TNZO);
758        let _ = cb.check_outflow(200 * ONE_TNZO); // trips
759        assert!(cb.is_tripped());
760
761        cb.reset();
762        assert!(!cb.is_tripped());
763        assert_eq!(cb.current_outflow(), 0);
764        assert!(cb.check_outflow(500 * ONE_TNZO).is_ok());
765    }
766
767    #[test]
768    fn test_circuit_breaker_wired_into_transfer() {
769        let token = TnzoToken::new();
770        let from = Address::new([1u8; 32]);
771        let to = Address::new([2u8; 32]);
772
773        // Give a huge balance
774        token.balances.insert(from, MAX_SUPPLY);
775        *token.total_supply.write() = MAX_SUPPLY;
776
777        // The default breaker allows 10% of MAX_SUPPLY per window
778        let limit = MAX_SUPPLY / 100 * CIRCUIT_BREAKER_MAX_OUTFLOW_PERCENT;
779
780        // Transfer just under the limit should succeed
781        let small_amount = limit / 2;
782        assert!(token.transfer(&from, &to, small_amount).is_ok());
783
784        // Another transfer that exceeds the remaining window should fail
785        let over_amount = limit;
786        let result = token.transfer(&from, &to, over_amount);
787        assert!(result.is_err());
788        assert!(token.circuit_breaker().is_tripped());
789
790        // Reset and transfers should work again
791        token.circuit_breaker().reset();
792        assert!(token.transfer(&from, &to, small_amount).is_ok());
793    }
794
795    #[test]
796    fn test_treasury_address_ref() {
797        let token = TnzoToken::new();
798        assert!(token.treasury_address_ref().is_none());
799
800        let treasury = Address::new([0xFF; 32]);
801        token.set_treasury_address(treasury);
802        assert_eq!(token.treasury_address_ref(), Some(treasury));
803    }
804
805    /// Regression test for the bug where `eth_sendRawTransaction` Transfer
806    /// txs landed in CF_ACCOUNTS via `StateAdapter::commit` but `balance_of`
807    /// returned a stale 0 because of an in-memory cache shadowing fresh
808    /// storage reads.
809    ///
810    /// Scenario: storage backend mutated externally (mimicking the VM's
811    /// direct CF_ACCOUNTS write). `balance_of` must reflect the new value
812    /// on the next call — no stale cache.
813    #[test]
814    fn test_balance_of_reflects_external_storage_writes() {
815        use std::sync::Mutex as StdMutex;
816
817        struct MockBackend {
818            balances: StdMutex<std::collections::HashMap<Address, u128>>,
819            supply: StdMutex<u128>,
820        }
821        impl StorageBackend for MockBackend {
822            fn get_balance(&self, address: &Address) -> Result<Option<u128>> {
823                Ok(self.balances.lock().unwrap().get(address).copied())
824            }
825            fn set_balance(&self, address: &Address, balance: u128) -> Result<()> {
826                self.balances.lock().unwrap().insert(*address, balance);
827                Ok(())
828            }
829            fn get_total_supply(&self) -> Result<u128> {
830                Ok(*self.supply.lock().unwrap())
831            }
832            fn set_total_supply(&self, s: u128) -> Result<()> {
833                *self.supply.lock().unwrap() = s;
834                Ok(())
835            }
836        }
837
838        let backend = Arc::new(MockBackend {
839            balances: StdMutex::new(Default::default()),
840            supply: StdMutex::new(0),
841        });
842
843        let token = TnzoToken::with_storage(backend.clone() as Arc<dyn StorageBackend>).unwrap();
844        let recipient = Address::new([0xAB; 32]);
845
846        // First read: balance is zero (cache miss → storage miss).
847        assert_eq!(token.balance_of(&recipient), 0);
848
849        // External writer (the VM) mutates storage directly, bypassing
850        // TnzoToken::transfer. Pre-fix, balance_of would still return 0.
851        backend.balances.lock().unwrap().insert(recipient, 5_000 * ONE_TNZO);
852
853        // Post-fix: read goes back to storage, sees the new value.
854        assert_eq!(token.balance_of(&recipient), 5_000 * ONE_TNZO);
855
856        // Subsequent external mutation must also be visible.
857        backend.balances.lock().unwrap().insert(recipient, 1_000 * ONE_TNZO);
858        assert_eq!(token.balance_of(&recipient), 1_000 * ONE_TNZO);
859    }
860}