Skip to main content

tenzro_token/
treasury.rs

1//! Network treasury management
2//!
3//! This module implements the network treasury that accumulates fees from settlements,
4//! transactions, and bridge operations in multiple assets (stablecoins + crypto).
5
6use crate::error::{Result, TokenError};
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::collections::HashMap;
11use std::sync::Arc;
12use tenzro_types::asset::AssetId;
13use tenzro_types::primitives::Address;
14use tenzro_storage::kv::{KvStore, CF_METADATA};
15use tracing::{debug, info, warn};
16
17/// Fee distribution configuration
18///
19/// Defines how network fees are split between treasury, burn, and staker rewards.
20/// All values are in basis points (bps) and must sum to 10000 (100%).
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FeeDistributionConfig {
23    /// Percentage of fees going to treasury (basis points)
24    pub treasury_share_bps: u32,
25    /// Percentage of fees to burn (basis points)
26    pub burn_share_bps: u32,
27    /// Percentage of fees going to stakers (basis points)
28    pub staker_share_bps: u32,
29}
30
31impl Default for FeeDistributionConfig {
32    fn default() -> Self {
33        Self {
34            treasury_share_bps: 4000, // 40%
35            burn_share_bps: 3000,      // 30%
36            staker_share_bps: 3000,    // 30%
37        }
38    }
39}
40
41impl FeeDistributionConfig {
42    /// Validates that the configuration is correct
43    pub fn validate(&self) -> Result<()> {
44        let total = self.treasury_share_bps
45            .saturating_add(self.burn_share_bps)
46            .saturating_add(self.staker_share_bps);
47
48        if total != 10000 {
49            return Err(TokenError::InvalidConfig(
50                format!("Fee distribution must sum to 10000 bps, got {}", total)
51            ));
52        }
53
54        Ok(())
55    }
56
57    /// Calculates the distribution amounts for a given fee
58    pub fn calculate_distribution(&self, total_fee: u128) -> FeeDistribution {
59        let treasury_amount = total_fee * (self.treasury_share_bps as u128) / 10000;
60        let burn_amount = total_fee * (self.burn_share_bps as u128) / 10000;
61        let staker_amount = total_fee * (self.staker_share_bps as u128) / 10000;
62
63        FeeDistribution {
64            treasury_amount,
65            burn_amount,
66            staker_amount,
67        }
68    }
69}
70
71/// Calculated fee distribution
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct FeeDistribution {
74    /// Amount going to treasury
75    pub treasury_amount: u128,
76    /// Amount to burn
77    pub burn_amount: u128,
78    /// Amount for stakers
79    pub staker_amount: u128,
80}
81
82/// RocksDB-backed storage for treasury state
83pub struct TreasuryStorageBackend {
84    store: Arc<dyn KvStore>,
85}
86
87impl TreasuryStorageBackend {
88    /// Creates a new treasury storage backend
89    pub fn new(store: Arc<dyn KvStore>) -> Self {
90        Self { store }
91    }
92
93    fn balance_key(asset_id: &AssetId) -> Vec<u8> {
94        let mut key = b"treasury:balance:".to_vec();
95        key.extend_from_slice(asset_id.as_str().as_bytes());
96        key
97    }
98
99    fn collected_key(asset_id: &AssetId) -> Vec<u8> {
100        let mut key = b"treasury:collected:".to_vec();
101        key.extend_from_slice(asset_id.as_str().as_bytes());
102        key
103    }
104
105    fn distributed_key(asset_id: &AssetId) -> Vec<u8> {
106        let mut key = b"treasury:distributed:".to_vec();
107        key.extend_from_slice(asset_id.as_str().as_bytes());
108        key
109    }
110
111    fn get_u128(&self, key: &[u8]) -> Result<u128> {
112        match self.store.get(CF_METADATA, key)? {
113            Some(bytes) if bytes.len() == 16 => {
114                let array: [u8; 16] = bytes.try_into().unwrap();
115                Ok(u128::from_le_bytes(array))
116            }
117            Some(bytes) => {
118                warn!("Invalid treasury bytes length: {}", bytes.len());
119                Ok(0)
120            }
121            None => Ok(0),
122        }
123    }
124
125    fn set_u128(&self, key: &[u8], value: u128) -> Result<()> {
126        self.store.put(CF_METADATA, key, &value.to_le_bytes())?;
127        Ok(())
128    }
129
130    /// Persists a treasury balance for an asset
131    pub fn persist_balance(&self, asset_id: &AssetId, balance: u128) -> Result<()> {
132        self.set_u128(&Self::balance_key(asset_id), balance)
133    }
134
135    /// Persists total collected for an asset
136    pub fn persist_collected(&self, asset_id: &AssetId, collected: u128) -> Result<()> {
137        self.set_u128(&Self::collected_key(asset_id), collected)
138    }
139
140    /// Persists total distributed for an asset
141    pub fn persist_distributed(&self, asset_id: &AssetId, distributed: u128) -> Result<()> {
142        self.set_u128(&Self::distributed_key(asset_id), distributed)
143    }
144
145    /// Loads a treasury balance for an asset
146    pub fn load_balance(&self, asset_id: &AssetId) -> Result<u128> {
147        self.get_u128(&Self::balance_key(asset_id))
148    }
149
150    /// Loads total collected for an asset
151    pub fn load_collected(&self, asset_id: &AssetId) -> Result<u128> {
152        self.get_u128(&Self::collected_key(asset_id))
153    }
154
155    /// Loads total distributed for an asset
156    pub fn load_distributed(&self, asset_id: &AssetId) -> Result<u128> {
157        self.get_u128(&Self::distributed_key(asset_id))
158    }
159}
160
161/// Network treasury
162///
163/// Manages multi-asset treasury accumulating network fees and supporting
164/// treasury-based operations.
165pub struct NetworkTreasury {
166    /// Treasury address
167    pub treasury_address: Address,
168    /// Multi-asset balances (AssetId -> Balance)
169    balances: RwLock<HashMap<AssetId, u128>>,
170    /// Total fees collected per asset
171    total_fees_collected: RwLock<HashMap<AssetId, u128>>,
172    /// Total distributed per asset
173    total_distributed: RwLock<HashMap<AssetId, u128>>,
174    /// Fee distribution configuration
175    distribution_config: RwLock<FeeDistributionConfig>,
176    /// Authorized withdrawers
177    authorized_withdrawers: RwLock<Vec<Address>>,
178    /// Minimum number of approvals required for withdrawal (multisig threshold)
179    withdrawal_threshold: RwLock<usize>,
180    /// Pending withdrawal approvals (withdrawal_id -> set of approvers)
181    pending_approvals: RwLock<HashMap<String, Vec<Address>>>,
182    /// Optional storage backend for persistence
183    storage: Option<Arc<TreasuryStorageBackend>>,
184    /// Per-bridge-adapter sponsorship pool balances. Tracks TNZO held
185    /// on behalf of users who sponsored cross-chain bridge fees (per
186    /// the Cosmos ICS-29 / Hyperlane IGP pattern adapted for Tenzro).
187    /// Keyed by the bridge adapter's canonical string id
188    /// (`layerzero`, `ccip`, `wormhole`, `debridge`, `hyperlane`,
189    /// `axelar`, `lifi`, `canton`). Operators tune `refill_threshold_bps`
190    /// via GovernanceEngine; the off-chain rebalancer reads these
191    /// fields to decide when to top up the relayer.
192    bridge_sponsorship_pools: RwLock<HashMap<String, BridgeSponsorshipPoolRecord>>,
193}
194
195/// Per-bridge-adapter sponsorship pool record persisted in the
196/// NetworkTreasury. Mirrors the shape exposed by
197/// `tenzro_bridge::fee_sponsor::SponsorshipPool`.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct BridgeSponsorshipPoolRecord {
200    pub adapter: String,
201    /// 20-byte deterministic vault address — `SHA-256(
202    /// "tenzro/bridge/sponsorship-vault" || adapter)[..20]`.
203    pub vault_address: [u8; 20],
204    pub tnzo_balance_wei: u128,
205    pub refill_threshold_bps: u32,
206    pub native_outstanding_smallest_unit: u128,
207}
208
209impl std::fmt::Debug for NetworkTreasury {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.debug_struct("NetworkTreasury")
212            .field("treasury_address", &self.treasury_address)
213            .field("storage", &self.storage.as_ref().map(|_| "Some(...)"))
214            .finish()
215    }
216}
217
218impl NetworkTreasury {
219    /// Creates a new network treasury (in-memory only)
220    pub fn new(treasury_address: Address) -> Self {
221        Self {
222            treasury_address,
223            balances: RwLock::new(HashMap::new()),
224            total_fees_collected: RwLock::new(HashMap::new()),
225            total_distributed: RwLock::new(HashMap::new()),
226            distribution_config: RwLock::new(FeeDistributionConfig::default()),
227            authorized_withdrawers: RwLock::new(Vec::new()),
228            withdrawal_threshold: RwLock::new(1), // Default: single sig until configured
229            pending_approvals: RwLock::new(HashMap::new()),
230            storage: None,
231            bridge_sponsorship_pools: RwLock::new(HashMap::new()),
232        }
233    }
234
235    /// Creates a new network treasury with persistent storage
236    pub fn with_storage(treasury_address: Address, storage: Arc<TreasuryStorageBackend>) -> Self {
237        Self {
238            treasury_address,
239            balances: RwLock::new(HashMap::new()),
240            total_fees_collected: RwLock::new(HashMap::new()),
241            total_distributed: RwLock::new(HashMap::new()),
242            distribution_config: RwLock::new(FeeDistributionConfig::default()),
243            authorized_withdrawers: RwLock::new(Vec::new()),
244            withdrawal_threshold: RwLock::new(1),
245            pending_approvals: RwLock::new(HashMap::new()),
246            storage: Some(storage),
247            bridge_sponsorship_pools: RwLock::new(HashMap::new()),
248        }
249    }
250
251    /// Get a snapshot of all per-bridge sponsorship pools. Read-only.
252    pub fn bridge_sponsorship_pools_snapshot(&self) -> HashMap<String, BridgeSponsorshipPoolRecord> {
253        self.bridge_sponsorship_pools.read().clone()
254    }
255
256    /// Get the per-adapter sponsorship pool record, creating it lazily
257    /// from the canonical vault-address derivation.
258    pub fn get_or_create_bridge_sponsorship_pool(&self, adapter: &str) -> BridgeSponsorshipPoolRecord {
259        let mut pools = self.bridge_sponsorship_pools.write();
260        if let Some(rec) = pools.get(adapter) {
261            return rec.clone();
262        }
263        // SHA-256("tenzro/bridge/sponsorship-vault" || adapter)[..20]
264        let mut h = Sha256::new();
265        h.update(b"tenzro/bridge/sponsorship-vault");
266        h.update(adapter.as_bytes());
267        let d = h.finalize();
268        let mut vault = [0u8; 20];
269        vault.copy_from_slice(&d[..20]);
270        let rec = BridgeSponsorshipPoolRecord {
271            adapter: adapter.to_string(),
272            vault_address: vault,
273            tnzo_balance_wei: 0,
274            refill_threshold_bps: 0,
275            native_outstanding_smallest_unit: 0,
276        };
277        pools.insert(adapter.to_string(), rec.clone());
278        rec
279    }
280
281    /// Credit a user TNZO sponsorship + record the outstanding
282    /// destination-native commitment. Called by the bridge-fee
283    /// sponsor RPC on a successful debit.
284    pub fn sponsor_bridge_fee(
285        &self,
286        adapter: &str,
287        tnzo_paid_wei: u128,
288        native_committed_smallest_unit: u128,
289    ) {
290        let _ = self.get_or_create_bridge_sponsorship_pool(adapter);
291        let mut pools = self.bridge_sponsorship_pools.write();
292        if let Some(rec) = pools.get_mut(adapter) {
293            rec.tnzo_balance_wei = rec.tnzo_balance_wei.saturating_add(tnzo_paid_wei);
294            rec.native_outstanding_smallest_unit = rec
295                .native_outstanding_smallest_unit
296                .saturating_add(native_committed_smallest_unit);
297        }
298    }
299
300    /// Drain the outstanding-native commitment after the relayer
301    /// submits delivery proof. Does NOT touch the TNZO balance — that
302    /// is paid out to the relayer via a separate explicit drain step
303    /// to keep the audit trail clean.
304    pub fn drain_bridge_sponsorship_outstanding(
305        &self,
306        adapter: &str,
307        native_amount: u128,
308    ) {
309        let mut pools = self.bridge_sponsorship_pools.write();
310        if let Some(rec) = pools.get_mut(adapter) {
311            rec.native_outstanding_smallest_unit = rec
312                .native_outstanding_smallest_unit
313                .saturating_sub(native_amount);
314        }
315    }
316
317    /// Set the per-adapter refill threshold (bps). Governance-tunable
318    /// via the GovernanceEngine; off-chain rebalancer triggers a
319    /// treasury top-up when the relayer's destination-native balance
320    /// drops below `outstanding * refill_threshold_bps / 10000`.
321    pub fn set_bridge_sponsorship_refill_threshold(&self, adapter: &str, bps: u32) {
322        let _ = self.get_or_create_bridge_sponsorship_pool(adapter);
323        let mut pools = self.bridge_sponsorship_pools.write();
324        if let Some(rec) = pools.get_mut(adapter) {
325            rec.refill_threshold_bps = bps;
326        }
327    }
328
329    /// Persists balance, collected, and distributed for an asset to storage
330    fn persist_asset_state(&self, asset_id: &AssetId, balance: u128, collected: u128, distributed: u128) {
331        if let Some(storage) = &self.storage {
332            if let Err(e) = storage.persist_balance(asset_id, balance) {
333                warn!("Failed to persist treasury balance: {}", e);
334            }
335            if let Err(e) = storage.persist_collected(asset_id, collected) {
336                warn!("Failed to persist treasury collected: {}", e);
337            }
338            if let Err(e) = storage.persist_distributed(asset_id, distributed) {
339                warn!("Failed to persist treasury distributed: {}", e);
340            }
341        }
342    }
343
344    /// Sets the multisig withdrawal threshold
345    ///
346    /// Requires that threshold <= number of authorized withdrawers
347    pub fn set_withdrawal_threshold(&self, threshold: usize) -> Result<()> {
348        let withdrawers = self.authorized_withdrawers.read();
349        if threshold == 0 {
350            return Err(TokenError::InvalidConfig(
351                "Withdrawal threshold must be at least 1".to_string(),
352            ));
353        }
354        if threshold > withdrawers.len() && !withdrawers.is_empty() {
355            return Err(TokenError::InvalidConfig(
356                format!(
357                    "Threshold {} exceeds number of authorized withdrawers {}",
358                    threshold, withdrawers.len()
359                ),
360            ));
361        }
362        drop(withdrawers);
363        *self.withdrawal_threshold.write() = threshold;
364        info!("Set withdrawal threshold to {}", threshold);
365        Ok(())
366    }
367
368    /// Approves a pending withdrawal (multisig step)
369    ///
370    /// Returns `true` if the withdrawal has reached threshold and should be executed.
371    pub fn approve_withdrawal(&self, withdrawal_id: &str, approver: &Address) -> Result<bool> {
372        // Verify the approver is authorized
373        let withdrawers = self.authorized_withdrawers.read();
374        if !withdrawers.contains(approver) {
375            return Err(TokenError::Unauthorized {
376                reason: "Not an authorized withdrawer".to_string(),
377            });
378        }
379        drop(withdrawers);
380
381        let threshold = *self.withdrawal_threshold.read();
382        let mut approvals = self.pending_approvals.write();
383        let entry = approvals.entry(withdrawal_id.to_string()).or_default();
384
385        // Prevent duplicate approvals from the same address
386        if entry.contains(approver) {
387            return Err(TokenError::InvalidAmount(
388                format!("Already approved withdrawal {}", withdrawal_id)
389            ));
390        }
391
392        entry.push(*approver);
393        let approval_count = entry.len();
394        info!(
395            "Withdrawal {} approved by {} ({}/{})",
396            withdrawal_id, approver, approval_count, threshold
397        );
398
399        Ok(approval_count >= threshold)
400    }
401
402    /// Clears pending approvals for a withdrawal (after execution or cancellation)
403    pub fn clear_approval(&self, withdrawal_id: &str) {
404        self.pending_approvals.write().remove(withdrawal_id);
405    }
406
407    /// Deposits a fee into the treasury
408    ///
409    /// # Arguments
410    ///
411    /// * `asset_id` - Asset being deposited
412    /// * `amount` - Amount to deposit
413    pub fn deposit_fee(&self, asset_id: &AssetId, amount: u128) -> Result<()> {
414        if amount == 0 {
415            return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
416        }
417
418        // Update balance
419        let mut balances = self.balances.write();
420        let current = balances.get(asset_id).copied().unwrap_or(0);
421        let new_balance = current.checked_add(amount)
422            .ok_or_else(|| TokenError::ArithmeticOverflow {
423                operation: "treasury deposit".to_string(),
424            })?;
425        balances.insert(asset_id.clone(), new_balance);
426        drop(balances);
427
428        // Update total collected
429        let mut total_collected = self.total_fees_collected.write();
430        let current_total = total_collected.get(asset_id).copied().unwrap_or(0);
431        let new_total = current_total.checked_add(amount)
432            .ok_or_else(|| TokenError::ArithmeticOverflow {
433                operation: "treasury total fees collected".to_string(),
434            })?;
435        total_collected.insert(asset_id.clone(), new_total);
436        let distributed = self.total_distributed.read().get(asset_id).copied().unwrap_or(0);
437        drop(total_collected);
438
439        // Persist to storage
440        self.persist_asset_state(asset_id, new_balance, new_total, distributed);
441
442        info!("Deposited {} of {} to treasury", amount, asset_id.as_str());
443        Ok(())
444    }
445
446    /// Withdraws from treasury (requires authorization)
447    ///
448    /// When withdrawal_threshold > 1, the caller must first gather approvals
449    /// via `approve_withdrawal()` before calling this method.
450    ///
451    /// # Arguments
452    ///
453    /// * `asset_id` - Asset to withdraw
454    /// * `amount` - Amount to withdraw
455    /// * `caller` - Address requesting withdrawal (must be authorized)
456    /// * `withdrawal_id` - Unique identifier for multisig tracking (optional for threshold=1)
457    pub fn withdraw(&self, asset_id: &AssetId, amount: u128, caller: &Address) -> Result<()> {
458        // Check authorization
459        let withdrawers = self.authorized_withdrawers.read();
460        if !withdrawers.contains(caller) && caller != &self.treasury_address {
461            return Err(TokenError::Unauthorized {
462                reason: "Not authorized to withdraw from treasury".to_string(),
463            });
464        }
465        drop(withdrawers);
466
467        // Enforce multisig threshold
468        let threshold = *self.withdrawal_threshold.read();
469        if threshold > 1 {
470            return Err(TokenError::Unauthorized {
471                reason: format!(
472                    "Multisig required: use approve_withdrawal() to gather {}/{} approvals first",
473                    threshold, threshold
474                ),
475            });
476        }
477
478        if amount == 0 {
479            return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
480        }
481
482        // Check balance
483        let mut balances = self.balances.write();
484        let current = balances.get(asset_id).copied().unwrap_or(0);
485        if current < amount {
486            return Err(TokenError::InsufficientBalance {
487                required: amount,
488                available: current,
489            });
490        }
491
492        // Deduct balance (safe: checked above that current >= amount)
493        let new_balance = current.checked_sub(amount)
494            .ok_or_else(|| TokenError::ArithmeticOverflow {
495                operation: "treasury withdraw balance".to_string(),
496            })?;
497        balances.insert(asset_id.clone(), new_balance);
498        drop(balances);
499
500        // Update total distributed
501        let mut total_distributed = self.total_distributed.write();
502        let current_distributed = total_distributed.get(asset_id).copied().unwrap_or(0);
503        let new_distributed = current_distributed.checked_add(amount)
504            .ok_or_else(|| TokenError::ArithmeticOverflow {
505                operation: "treasury total distributed".to_string(),
506            })?;
507        total_distributed.insert(asset_id.clone(), new_distributed);
508        let collected = self.total_fees_collected.read().get(asset_id).copied().unwrap_or(0);
509        drop(total_distributed);
510
511        // Persist to storage
512        self.persist_asset_state(asset_id, new_balance, collected, new_distributed);
513
514        info!("Withdrew {} of {} from treasury", amount, asset_id.as_str());
515        Ok(())
516    }
517
518    /// Executes a withdrawal after multisig approval has been reached.
519    ///
520    /// Verifies that the withdrawal_id has sufficient approvals, then executes.
521    pub fn execute_withdrawal(
522        &self,
523        withdrawal_id: &str,
524        asset_id: &AssetId,
525        amount: u128,
526    ) -> Result<()> {
527        let threshold = *self.withdrawal_threshold.read();
528        let approvals = self.pending_approvals.read();
529        let approval_count = approvals.get(withdrawal_id).map(|a| a.len()).unwrap_or(0);
530
531        if approval_count < threshold {
532            return Err(TokenError::Unauthorized {
533                reason: format!(
534                    "Insufficient approvals for withdrawal {}: have {}, need {}",
535                    withdrawal_id, approval_count, threshold
536                ),
537            });
538        }
539        drop(approvals);
540
541        if amount == 0 {
542            return Err(TokenError::InvalidAmount(
543                "Amount must be greater than zero".to_string(),
544            ));
545        }
546
547        // Check balance
548        let mut balances = self.balances.write();
549        let current = balances.get(asset_id).copied().unwrap_or(0);
550        if current < amount {
551            return Err(TokenError::InsufficientBalance {
552                required: amount,
553                available: current,
554            });
555        }
556
557        // Deduct balance (safe: checked above that current >= amount)
558        let new_balance = current.checked_sub(amount)
559            .ok_or_else(|| TokenError::ArithmeticOverflow {
560                operation: "treasury execute_withdrawal balance".to_string(),
561            })?;
562        balances.insert(asset_id.clone(), new_balance);
563        drop(balances);
564
565        // Update total distributed
566        let mut total_distributed = self.total_distributed.write();
567        let current_distributed = total_distributed.get(asset_id).copied().unwrap_or(0);
568        let new_distributed = current_distributed.checked_add(amount)
569            .ok_or_else(|| TokenError::ArithmeticOverflow {
570                operation: "treasury execute_withdrawal distributed".to_string(),
571            })?;
572        total_distributed.insert(asset_id.clone(), new_distributed);
573        let collected = self.total_fees_collected.read().get(asset_id).copied().unwrap_or(0);
574        drop(total_distributed);
575
576        // Persist to storage
577        self.persist_asset_state(asset_id, new_balance, collected, new_distributed);
578
579        // Clear pending approvals
580        self.clear_approval(withdrawal_id);
581
582        info!(
583            "Executed multisig withdrawal {}: {} of {}",
584            withdrawal_id, amount, asset_id.as_str()
585        );
586        Ok(())
587    }
588
589    /// Returns the balance for a specific asset
590    pub fn balance(&self, asset_id: &AssetId) -> u128 {
591        self.balances.read().get(asset_id).copied().unwrap_or(0)
592    }
593
594    /// Returns the total value across all assets (in TNZO equivalent)
595    ///
596    /// Note: This is a simplified version. In production, you'd use an oracle
597    /// or price feed to convert different assets to TNZO equivalent.
598    pub fn total_value(&self) -> u128 {
599        let balances = self.balances.read();
600        balances.values().sum()
601    }
602
603    /// Calculates the backing ratio (treasury_value / tnzo_supply)
604    ///
605    /// # Arguments
606    ///
607    /// * `tnzo_supply` - Current TNZO supply
608    pub fn backing_ratio(&self, tnzo_supply: u128) -> f64 {
609        if tnzo_supply == 0 {
610            return 0.0;
611        }
612
613        let total = self.total_value();
614        total as f64 / tnzo_supply as f64
615    }
616
617    /// Processes a network fee according to the distribution config
618    ///
619    /// # Arguments
620    ///
621    /// * `asset_id` - Asset of the fee
622    /// * `total_fee` - Total fee amount
623    ///
624    /// Returns the calculated distribution
625    pub fn process_network_fee(&self, asset_id: &AssetId, total_fee: u128) -> Result<FeeDistribution> {
626        let config = self.distribution_config.read();
627        config.validate()?;
628
629        let distribution = config.calculate_distribution(total_fee);
630
631        // Deposit treasury share
632        self.deposit_fee(asset_id, distribution.treasury_amount)?;
633
634        debug!(
635            "Processed fee: treasury={}, burn={}, stakers={}",
636            distribution.treasury_amount,
637            distribution.burn_amount,
638            distribution.staker_amount
639        );
640
641        Ok(distribution)
642    }
643
644    /// Updates the fee distribution configuration
645    pub fn update_distribution_config(&self, config: FeeDistributionConfig) -> Result<()> {
646        config.validate()?;
647        *self.distribution_config.write() = config;
648        info!("Updated fee distribution config");
649        Ok(())
650    }
651
652    /// Adds an authorized withdrawer
653    pub fn add_authorized_withdrawer(&self, address: Address) {
654        let mut withdrawers = self.authorized_withdrawers.write();
655        if !withdrawers.contains(&address) {
656            withdrawers.push(address);
657            info!("Added authorized withdrawer: {}", address);
658        }
659    }
660
661    /// Removes an authorized withdrawer
662    pub fn remove_authorized_withdrawer(&self, address: &Address) {
663        let mut withdrawers = self.authorized_withdrawers.write();
664        withdrawers.retain(|a| a != address);
665        info!("Removed authorized withdrawer: {}", address);
666    }
667
668    /// Audits the treasury's supply invariant
669    ///
670    /// Verifies that: total_collected = current_balance + total_distributed
671    ///
672    /// # Arguments
673    ///
674    /// * `asset_id` - Asset to audit
675    ///
676    /// Returns `Ok(())` if the invariant holds, or an error with details if it doesn't.
677    pub fn audit_supply(&self, asset_id: &AssetId) -> Result<()> {
678        let balances = self.balances.read();
679        let total_collected = self.total_fees_collected.read();
680        let total_distributed = self.total_distributed.read();
681
682        let current_balance = balances.get(asset_id).copied().unwrap_or(0);
683        let collected = total_collected.get(asset_id).copied().unwrap_or(0);
684        let distributed = total_distributed.get(asset_id).copied().unwrap_or(0);
685
686        // Invariant: collected = balance + distributed
687        // Or equivalently: balance = collected - distributed
688        let expected_balance = collected.checked_sub(distributed)
689            .ok_or_else(|| TokenError::TreasuryError {
690                message: format!(
691                    "Distributed ({}) exceeds collected ({}) for {}",
692                    distributed, collected, asset_id.as_str()
693                ),
694            })?;
695
696        if current_balance != expected_balance {
697            return Err(TokenError::TreasuryError {
698                message: format!(
699                    "Supply invariant violation for {}: balance {} != expected {} (collected {} - distributed {})",
700                    asset_id.as_str(), current_balance, expected_balance, collected, distributed
701                ),
702            });
703        }
704
705        debug!(
706            "Supply audit passed for {}: balance={}, collected={}, distributed={}",
707            asset_id.as_str(), current_balance, collected, distributed
708        );
709        Ok(())
710    }
711
712    /// Audits all assets in the treasury
713    ///
714    /// Returns the list of assets that failed the audit.
715    pub fn audit_all_supplies(&self) -> Vec<(AssetId, String)> {
716        let balances = self.balances.read();
717        let mut failures = Vec::new();
718
719        for (asset_id, _) in balances.iter() {
720            if let Err(e) = self.audit_supply(asset_id) {
721                failures.push((asset_id.clone(), e.to_string()));
722            }
723        }
724
725        if !failures.is_empty() {
726            warn!("Treasury audit found {} violations", failures.len());
727        } else {
728            info!("Treasury audit passed for all {} assets", balances.len());
729        }
730
731        failures
732    }
733
734    /// Returns treasury statistics
735    pub fn stats(&self) -> TreasuryStats {
736        let balances = self.balances.read().clone();
737        let total_collected = self.total_fees_collected.read().clone();
738        let total_distributed = self.total_distributed.read().clone();
739
740        TreasuryStats {
741            treasury_address: self.treasury_address,
742            balances,
743            total_fees_collected: total_collected,
744            total_distributed,
745            total_value: self.total_value(),
746        }
747    }
748}
749
750/// Treasury statistics
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct TreasuryStats {
753    /// Treasury address
754    pub treasury_address: Address,
755    /// Current balances per asset
756    pub balances: HashMap<AssetId, u128>,
757    /// Total fees collected per asset
758    pub total_fees_collected: HashMap<AssetId, u128>,
759    /// Total distributed per asset
760    pub total_distributed: HashMap<AssetId, u128>,
761    /// Total value (in TNZO equivalent)
762    pub total_value: u128,
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768
769    #[test]
770    fn test_fee_distribution_config() {
771        let config = FeeDistributionConfig::default();
772        assert!(config.validate().is_ok());
773
774        let distribution = config.calculate_distribution(10000);
775        assert_eq!(distribution.treasury_amount, 4000);
776        assert_eq!(distribution.burn_amount, 3000);
777        assert_eq!(distribution.staker_amount, 3000);
778    }
779
780    #[test]
781    fn test_treasury_deposit() {
782        let treasury = NetworkTreasury::new(Address::new([1u8; 32]));
783        let asset_id = AssetId::tnzo();
784
785        treasury.deposit_fee(&asset_id, 1000).unwrap();
786        assert_eq!(treasury.balance(&asset_id), 1000);
787    }
788
789    #[test]
790    fn test_treasury_withdraw() {
791        let treasury_addr = Address::new([1u8; 32]);
792        let treasury = NetworkTreasury::new(treasury_addr);
793        let asset_id = AssetId::tnzo();
794
795        treasury.deposit_fee(&asset_id, 1000).unwrap();
796        treasury.withdraw(&asset_id, 500, &treasury_addr).unwrap();
797        assert_eq!(treasury.balance(&asset_id), 500);
798    }
799
800    #[test]
801    fn test_multisig_withdrawal() {
802        let treasury_addr = Address::new([0u8; 32]);
803        let treasury = NetworkTreasury::new(treasury_addr);
804        let asset_id = AssetId::tnzo();
805
806        let signer1 = Address::new([1u8; 32]);
807        let signer2 = Address::new([2u8; 32]);
808        let signer3 = Address::new([3u8; 32]);
809
810        // Add authorized withdrawers
811        treasury.add_authorized_withdrawer(signer1);
812        treasury.add_authorized_withdrawer(signer2);
813        treasury.add_authorized_withdrawer(signer3);
814
815        // Set 2-of-3 multisig
816        treasury.set_withdrawal_threshold(2).unwrap();
817
818        // Deposit funds
819        treasury.deposit_fee(&asset_id, 1000).unwrap();
820
821        // Single-sig withdraw should be blocked
822        assert!(treasury.withdraw(&asset_id, 500, &signer1).is_err());
823
824        // First approval — not yet reached threshold
825        let reached = treasury.approve_withdrawal("w1", &signer1).unwrap();
826        assert!(!reached);
827
828        // Execute before threshold — should fail
829        assert!(treasury.execute_withdrawal("w1", &asset_id, 500).is_err());
830
831        // Second approval — reached threshold
832        let reached = treasury.approve_withdrawal("w1", &signer2).unwrap();
833        assert!(reached);
834
835        // Execute now — should succeed
836        treasury.execute_withdrawal("w1", &asset_id, 500).unwrap();
837        assert_eq!(treasury.balance(&asset_id), 500);
838    }
839
840    #[test]
841    fn test_no_duplicate_approvals() {
842        let treasury_addr = Address::new([0u8; 32]);
843        let treasury = NetworkTreasury::new(treasury_addr);
844
845        let signer = Address::new([1u8; 32]);
846        treasury.add_authorized_withdrawer(signer);
847        treasury.set_withdrawal_threshold(1).unwrap();
848
849        treasury.approve_withdrawal("w1", &signer).unwrap();
850        // Duplicate approval should fail
851        assert!(treasury.approve_withdrawal("w1", &signer).is_err());
852    }
853
854    #[test]
855    fn test_supply_audit_passes() {
856        let treasury = NetworkTreasury::new(Address::new([1u8; 32]));
857        let asset_id = AssetId::tnzo();
858
859        // Deposit 1000
860        treasury.deposit_fee(&asset_id, 1000).unwrap();
861
862        // Audit should pass
863        assert!(treasury.audit_supply(&asset_id).is_ok());
864
865        // Add authorized withdrawer and set threshold to 1
866        treasury.add_authorized_withdrawer(treasury.treasury_address);
867
868        // Withdraw 400 (single-sig)
869        treasury.withdraw(&asset_id, 400, &treasury.treasury_address).unwrap();
870
871        // Audit should still pass: balance=600, collected=1000, distributed=400
872        assert!(treasury.audit_supply(&asset_id).is_ok());
873    }
874
875    #[test]
876    fn test_supply_audit_detects_violation() {
877        let treasury = NetworkTreasury::new(Address::new([1u8; 32]));
878        let asset_id = AssetId::tnzo();
879
880        // Deposit 1000
881        treasury.deposit_fee(&asset_id, 1000).unwrap();
882
883        // Manually corrupt the distributed counter (simulate a bug)
884        {
885            let mut distributed = treasury.total_distributed.write();
886            distributed.insert(asset_id.clone(), 1500); // Exceeds collected
887        }
888
889        // Audit should fail
890        assert!(treasury.audit_supply(&asset_id).is_err());
891    }
892
893    #[test]
894    fn test_audit_all_supplies() {
895        let treasury = NetworkTreasury::new(Address::new([1u8; 32]));
896        let asset1 = AssetId::tnzo();
897        let asset2 = AssetId::usd_stablecoin();
898
899        // Deposit to both assets
900        treasury.deposit_fee(&asset1, 1000).unwrap();
901        treasury.deposit_fee(&asset2, 2000).unwrap();
902
903        // All audits should pass
904        let failures = treasury.audit_all_supplies();
905        assert_eq!(failures.len(), 0);
906
907        // Corrupt one asset
908        {
909            let mut distributed = treasury.total_distributed.write();
910            distributed.insert(asset1.clone(), 1500);
911        }
912
913        // Should detect exactly one failure
914        let failures = treasury.audit_all_supplies();
915        assert_eq!(failures.len(), 1);
916        assert_eq!(failures[0].0, asset1);
917    }
918}