1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FeeDistributionConfig {
23 pub treasury_share_bps: u32,
25 pub burn_share_bps: u32,
27 pub staker_share_bps: u32,
29}
30
31impl Default for FeeDistributionConfig {
32 fn default() -> Self {
33 Self {
34 treasury_share_bps: 4000, burn_share_bps: 3000, staker_share_bps: 3000, }
38 }
39}
40
41impl FeeDistributionConfig {
42 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct FeeDistribution {
74 pub treasury_amount: u128,
76 pub burn_amount: u128,
78 pub staker_amount: u128,
80}
81
82pub struct TreasuryStorageBackend {
84 store: Arc<dyn KvStore>,
85}
86
87impl TreasuryStorageBackend {
88 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 pub fn persist_balance(&self, asset_id: &AssetId, balance: u128) -> Result<()> {
132 self.set_u128(&Self::balance_key(asset_id), balance)
133 }
134
135 pub fn persist_collected(&self, asset_id: &AssetId, collected: u128) -> Result<()> {
137 self.set_u128(&Self::collected_key(asset_id), collected)
138 }
139
140 pub fn persist_distributed(&self, asset_id: &AssetId, distributed: u128) -> Result<()> {
142 self.set_u128(&Self::distributed_key(asset_id), distributed)
143 }
144
145 pub fn load_balance(&self, asset_id: &AssetId) -> Result<u128> {
147 self.get_u128(&Self::balance_key(asset_id))
148 }
149
150 pub fn load_collected(&self, asset_id: &AssetId) -> Result<u128> {
152 self.get_u128(&Self::collected_key(asset_id))
153 }
154
155 pub fn load_distributed(&self, asset_id: &AssetId) -> Result<u128> {
157 self.get_u128(&Self::distributed_key(asset_id))
158 }
159}
160
161pub struct NetworkTreasury {
166 pub treasury_address: Address,
168 balances: RwLock<HashMap<AssetId, u128>>,
170 total_fees_collected: RwLock<HashMap<AssetId, u128>>,
172 total_distributed: RwLock<HashMap<AssetId, u128>>,
174 distribution_config: RwLock<FeeDistributionConfig>,
176 authorized_withdrawers: RwLock<Vec<Address>>,
178 withdrawal_threshold: RwLock<usize>,
180 pending_approvals: RwLock<HashMap<String, Vec<Address>>>,
182 storage: Option<Arc<TreasuryStorageBackend>>,
184 bridge_sponsorship_pools: RwLock<HashMap<String, BridgeSponsorshipPoolRecord>>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct BridgeSponsorshipPoolRecord {
200 pub adapter: String,
201 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 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), pending_approvals: RwLock::new(HashMap::new()),
230 storage: None,
231 bridge_sponsorship_pools: RwLock::new(HashMap::new()),
232 }
233 }
234
235 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 pub fn bridge_sponsorship_pools_snapshot(&self) -> HashMap<String, BridgeSponsorshipPoolRecord> {
253 self.bridge_sponsorship_pools.read().clone()
254 }
255
256 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 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 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 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 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 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 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 pub fn approve_withdrawal(&self, withdrawal_id: &str, approver: &Address) -> Result<bool> {
372 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 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 pub fn clear_approval(&self, withdrawal_id: &str) {
404 self.pending_approvals.write().remove(withdrawal_id);
405 }
406
407 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 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 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 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 pub fn withdraw(&self, asset_id: &AssetId, amount: u128, caller: &Address) -> Result<()> {
458 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 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 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 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 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 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 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 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 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 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 self.persist_asset_state(asset_id, new_balance, collected, new_distributed);
578
579 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 pub fn balance(&self, asset_id: &AssetId) -> u128 {
591 self.balances.read().get(asset_id).copied().unwrap_or(0)
592 }
593
594 pub fn total_value(&self) -> u128 {
599 let balances = self.balances.read();
600 balances.values().sum()
601 }
602
603 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct TreasuryStats {
753 pub treasury_address: Address,
755 pub balances: HashMap<AssetId, u128>,
757 pub total_fees_collected: HashMap<AssetId, u128>,
759 pub total_distributed: HashMap<AssetId, u128>,
761 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 treasury.add_authorized_withdrawer(signer1);
812 treasury.add_authorized_withdrawer(signer2);
813 treasury.add_authorized_withdrawer(signer3);
814
815 treasury.set_withdrawal_threshold(2).unwrap();
817
818 treasury.deposit_fee(&asset_id, 1000).unwrap();
820
821 assert!(treasury.withdraw(&asset_id, 500, &signer1).is_err());
823
824 let reached = treasury.approve_withdrawal("w1", &signer1).unwrap();
826 assert!(!reached);
827
828 assert!(treasury.execute_withdrawal("w1", &asset_id, 500).is_err());
830
831 let reached = treasury.approve_withdrawal("w1", &signer2).unwrap();
833 assert!(reached);
834
835 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 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 treasury.deposit_fee(&asset_id, 1000).unwrap();
861
862 assert!(treasury.audit_supply(&asset_id).is_ok());
864
865 treasury.add_authorized_withdrawer(treasury.treasury_address);
867
868 treasury.withdraw(&asset_id, 400, &treasury.treasury_address).unwrap();
870
871 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 treasury.deposit_fee(&asset_id, 1000).unwrap();
882
883 {
885 let mut distributed = treasury.total_distributed.write();
886 distributed.insert(asset_id.clone(), 1500); }
888
889 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 treasury.deposit_fee(&asset1, 1000).unwrap();
901 treasury.deposit_fee(&asset2, 2000).unwrap();
902
903 let failures = treasury.audit_all_supplies();
905 assert_eq!(failures.len(), 0);
906
907 {
909 let mut distributed = treasury.total_distributed.write();
910 distributed.insert(asset1.clone(), 1500);
911 }
912
913 let failures = treasury.audit_all_supplies();
915 assert_eq!(failures.len(), 1);
916 assert_eq!(failures[0].0, asset1);
917 }
918}