/ ledger / governor / src / mint_burn.rs
mint_burn.rs
  1  // Copyright (c) 2025 ADnet Contributors
  2  // This file is part of the AlphaVM library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  //! # Mint/Burn Operations
 17  //!
 18  //! Implements ALPHA token minting and burning as specified in F-A10 to F-A13.
 19  //!
 20  //! ## Overview
 21  //!
 22  //! - **Mint**: Creates new ALPHA tokens within cumulative limits
 23  //! - **Burn**: Destroys ALPHA tokens, freeing up mint capacity
 24  //! - **Outstanding Balance**: Tracks net (minted - burned) per GID
 25  //!
 26  //! ## Cumulative Limit Model
 27  //!
 28  //! Mint limits are CUMULATIVE, not temporal:
 29  //! - `mint_limit` is the maximum `outstanding_balance` allowed
 30  //! - To mint more when at the limit, the governor must burn ALPHA first
 31  //! - Limits are set by 67% collective GID approval via governance proposals
 32  //!
 33  //! ## Specifications
 34  //!
 35  //! - F-A10: Governor Mint - Creates ALPHA tokens within approved limits
 36  //! - F-A11: Governor Burn - Destroys ALPHA tokens
 37  //! - F-A12: Outstanding Balance Query - Returns net minted per GID
 38  //! - F-A13: Emergency Freeze - Halts all GID operations
 39  
 40  use crate::{
 41      bls::{AggregateSignature, VerifyResult},
 42      console::{
 43          prelude::*,
 44          types::{Address, Field},
 45      },
 46      gid::{GidId, GovernorIdentity},
 47  };
 48  
 49  /// Type of mint/burn operation
 50  #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 51  pub enum OperationType {
 52      /// Mint new tokens
 53      Mint,
 54      /// Burn existing tokens
 55      Burn,
 56  }
 57  
 58  /// A mint operation request
 59  #[derive(Clone, Debug)]
 60  pub struct MintRequest<N: Network> {
 61      /// The GID performing the mint
 62      gid_id: GidId<N>,
 63      /// Recipient address
 64      recipient: Address<N>,
 65      /// Amount to mint (in microcredits)
 66      amount: u64,
 67      /// Nonce for replay protection
 68      nonce: u64,
 69      /// Aggregate signature from GID signers
 70      signature: AggregateSignature<N>,
 71  }
 72  
 73  impl<N: Network> MintRequest<N> {
 74      /// Create a new mint request
 75      pub fn new(
 76          gid_id: GidId<N>,
 77          recipient: Address<N>,
 78          amount: u64,
 79          nonce: u64,
 80          signature: AggregateSignature<N>,
 81      ) -> Self {
 82          Self { gid_id, recipient, amount, nonce, signature }
 83      }
 84  
 85      /// Get the GID identifier
 86      pub fn gid_id(&self) -> &GidId<N> {
 87          &self.gid_id
 88      }
 89  
 90      /// Get the recipient address
 91      pub fn recipient(&self) -> &Address<N> {
 92          &self.recipient
 93      }
 94  
 95      /// Get the mint amount
 96      pub fn amount(&self) -> u64 {
 97          self.amount
 98      }
 99  
100      /// Get the nonce
101      pub fn nonce(&self) -> u64 {
102          self.nonce
103      }
104  
105      /// Get the signature
106      pub fn signature(&self) -> &AggregateSignature<N> {
107          &self.signature
108      }
109  
110      /// Create the message that must be signed
111      pub fn signing_message(&self) -> Vec<u8> {
112          let mut msg = Vec::new();
113          msg.extend_from_slice(b"ALPHA_MINT_V1:");
114          msg.extend_from_slice(&self.gid_id.id().to_bytes_le().unwrap_or_default());
115          msg.extend_from_slice(&self.recipient.to_bytes_le().unwrap_or_default());
116          msg.extend_from_slice(&self.amount.to_le_bytes());
117          msg.extend_from_slice(&self.nonce.to_le_bytes());
118          msg
119      }
120  }
121  
122  /// A burn operation request
123  #[derive(Clone, Debug)]
124  pub struct BurnRequest<N: Network> {
125      /// The GID authorizing the burn
126      gid_id: GidId<N>,
127      /// Amount to burn (in microcredits)
128      amount: u64,
129      /// Commitment to the records being burned (for ZK proof)
130      record_commitments: Vec<Field<N>>,
131      /// Nonce for replay protection
132      nonce: u64,
133      /// Aggregate signature from GID signers
134      signature: AggregateSignature<N>,
135  }
136  
137  impl<N: Network> BurnRequest<N> {
138      /// Create a new burn request
139      pub fn new(
140          gid_id: GidId<N>,
141          amount: u64,
142          record_commitments: Vec<Field<N>>,
143          nonce: u64,
144          signature: AggregateSignature<N>,
145      ) -> Self {
146          Self { gid_id, amount, record_commitments, nonce, signature }
147      }
148  
149      /// Get the GID identifier
150      pub fn gid_id(&self) -> &GidId<N> {
151          &self.gid_id
152      }
153  
154      /// Get the burn amount
155      pub fn amount(&self) -> u64 {
156          self.amount
157      }
158  
159      /// Get the record commitments
160      pub fn record_commitments(&self) -> &[Field<N>] {
161          &self.record_commitments
162      }
163  
164      /// Get the nonce
165      pub fn nonce(&self) -> u64 {
166          self.nonce
167      }
168  
169      /// Get the signature
170      pub fn signature(&self) -> &AggregateSignature<N> {
171          &self.signature
172      }
173  
174      /// Create the message that must be signed
175      pub fn signing_message(&self) -> Vec<u8> {
176          let mut msg = Vec::new();
177          msg.extend_from_slice(b"ALPHA_BURN_V1:");
178          msg.extend_from_slice(&self.gid_id.id().to_bytes_le().unwrap_or_default());
179          msg.extend_from_slice(&self.amount.to_le_bytes());
180          for commitment in &self.record_commitments {
181              msg.extend_from_slice(&commitment.to_bytes_le().unwrap_or_default());
182          }
183          msg.extend_from_slice(&self.nonce.to_le_bytes());
184          msg
185      }
186  }
187  
188  /// Result of a mint operation
189  #[derive(Clone, Debug, PartialEq, Eq)]
190  pub enum MintResult<N: Network> {
191      /// Mint succeeded
192      Success {
193          /// Commitment to the minted record
194          record_commitment: Field<N>,
195          /// New outstanding balance
196          new_outstanding_balance: u64,
197      },
198      /// GID not found
199      GidNotFound,
200      /// GID is not active (frozen or disabled)
201      GidNotActive,
202      /// Invalid signature
203      InvalidSignature(VerifyResult),
204      /// Would exceed cumulative mint limit
205      MintLimitExceeded {
206          /// Maximum allowed outstanding balance
207          limit: u64,
208          /// Current outstanding balance
209          outstanding: u64,
210          /// Amount requested to mint
211          requested: u64,
212      },
213      /// Invalid nonce
214      InvalidNonce { expected: u64, got: u64 },
215  }
216  
217  /// Result of a burn operation
218  #[derive(Clone, Debug, PartialEq, Eq)]
219  pub enum BurnResult {
220      /// Burn succeeded
221      Success {
222          /// New outstanding balance
223          new_outstanding_balance: u64,
224      },
225      /// GID not found
226      GidNotFound,
227      /// GID is not active (frozen or disabled)
228      GidNotActive,
229      /// Invalid signature
230      InvalidSignature(VerifyResult),
231      /// Burn amount exceeds outstanding balance
232      ExceedsOutstandingBalance { outstanding: u64, requested: u64 },
233      /// Invalid nonce
234      InvalidNonce { expected: u64, got: u64 },
235      /// Invalid record commitments
236      InvalidRecordCommitments,
237  }
238  
239  /// Outstanding balance record for a GID
240  #[derive(Clone, Debug)]
241  pub struct OutstandingBalance<N: Network> {
242      /// The GID identifier
243      gid_id: GidId<N>,
244      /// Total minted
245      total_minted: u64,
246      /// Total burned
247      total_burned: u64,
248      /// Last update timestamp (epoch)
249      last_update_epoch: u64,
250  }
251  
252  impl<N: Network> OutstandingBalance<N> {
253      /// Create a new outstanding balance record
254      pub fn new(gid_id: GidId<N>) -> Self {
255          Self { gid_id, total_minted: 0, total_burned: 0, last_update_epoch: 0 }
256      }
257  
258      /// Get the GID identifier
259      pub fn gid_id(&self) -> &GidId<N> {
260          &self.gid_id
261      }
262  
263      /// Get the net outstanding balance (minted - burned)
264      pub fn net_balance(&self) -> u64 {
265          self.total_minted.saturating_sub(self.total_burned)
266      }
267  
268      /// Get total minted
269      pub fn total_minted(&self) -> u64 {
270          self.total_minted
271      }
272  
273      /// Get total burned
274      pub fn total_burned(&self) -> u64 {
275          self.total_burned
276      }
277  
278      /// Get last update epoch
279      pub fn last_update_epoch(&self) -> u64 {
280          self.last_update_epoch
281      }
282  
283      /// Record a mint operation
284      pub fn record_mint(&mut self, amount: u64, epoch: u64) {
285          self.total_minted = self.total_minted.saturating_add(amount);
286          self.last_update_epoch = epoch;
287      }
288  
289      /// Record a burn operation
290      pub fn record_burn(&mut self, amount: u64, epoch: u64) -> Result<()> {
291          ensure!(
292              amount <= self.net_balance(),
293              "Burn amount {} exceeds outstanding balance {}",
294              amount,
295              self.net_balance()
296          );
297          self.total_burned = self.total_burned.saturating_add(amount);
298          self.last_update_epoch = epoch;
299          Ok(())
300      }
301  }
302  
303  /// Mint/Burn processor for validating and executing operations
304  pub struct MintBurnProcessor<N: Network> {
305      /// Phantom data for network type
306      _phantom: std::marker::PhantomData<N>,
307  }
308  
309  impl<N: Network> Default for MintBurnProcessor<N> {
310      fn default() -> Self {
311          Self::new()
312      }
313  }
314  
315  impl<N: Network> MintBurnProcessor<N> {
316      /// Create a new processor
317      pub fn new() -> Self {
318          Self { _phantom: std::marker::PhantomData }
319      }
320  
321      /// Process a mint request
322      ///
323      /// This validates the request and returns the result.
324      /// The caller is responsible for actually creating the record.
325      ///
326      /// Minting is subject to cumulative limits:
327      /// - The new outstanding_balance cannot exceed mint_limit
328      /// - To mint more when at the limit, the governor must burn ALPHA first
329      pub fn process_mint(&self, request: &MintRequest<N>, gid: &mut GovernorIdentity<N>) -> MintResult<N> {
330          // Check GID is active
331          if !gid.is_active() {
332              return MintResult::GidNotActive;
333          }
334  
335          // Check nonce
336          let expected_nonce = gid.nonce() + 1;
337          if request.nonce() != expected_nonce {
338              return MintResult::InvalidNonce { expected: expected_nonce, got: request.nonce() };
339          }
340  
341          // Check cumulative limit
342          if !gid.can_mint(request.amount()) {
343              return MintResult::MintLimitExceeded {
344                  limit: gid.mint_limit(),
345                  outstanding: gid.outstanding_balance(),
346                  requested: request.amount(),
347              };
348          }
349  
350          // Verify signature
351          let message = request.signing_message();
352          match gid.verify_signature(request.signature(), &message) {
353              VerifyResult::Valid => {}
354              result => return MintResult::InvalidSignature(result),
355          }
356  
357          // Execute mint
358          if gid.record_mint(request.amount()).is_err() {
359              return MintResult::MintLimitExceeded {
360                  limit: gid.mint_limit(),
361                  outstanding: gid.outstanding_balance(),
362                  requested: request.amount(),
363              };
364          }
365  
366          gid.increment_nonce();
367  
368          // Generate record commitment (placeholder - real implementation uses Poseidon)
369          let commitment_preimage = [
370              request.recipient().to_bytes_le().unwrap_or_default(),
371              request.amount().to_le_bytes().to_vec(),
372              request.nonce().to_le_bytes().to_vec(),
373          ]
374          .concat();
375          let commitment_value = commitment_preimage.iter().fold(0u64, |acc, &b| acc.wrapping_add(b as u64));
376  
377          MintResult::Success {
378              record_commitment: Field::from_u64(commitment_value),
379              new_outstanding_balance: gid.outstanding_balance(),
380          }
381      }
382  
383      /// Process a burn request
384      ///
385      /// This validates the request and returns the result.
386      /// The caller is responsible for nullifying the records.
387      pub fn process_burn(&self, request: &BurnRequest<N>, gid: &mut GovernorIdentity<N>) -> BurnResult {
388          // Check GID is active
389          if !gid.is_active() {
390              return BurnResult::GidNotActive;
391          }
392  
393          // Check nonce
394          let expected_nonce = gid.nonce() + 1;
395          if request.nonce() != expected_nonce {
396              return BurnResult::InvalidNonce { expected: expected_nonce, got: request.nonce() };
397          }
398  
399          // Check outstanding balance
400          if request.amount() > gid.outstanding_balance() {
401              return BurnResult::ExceedsOutstandingBalance {
402                  outstanding: gid.outstanding_balance(),
403                  requested: request.amount(),
404              };
405          }
406  
407          // Verify signature
408          let message = request.signing_message();
409          match gid.verify_signature(request.signature(), &message) {
410              VerifyResult::Valid => {}
411              result => return BurnResult::InvalidSignature(result),
412          }
413  
414          // Validate record commitments are present
415          if request.record_commitments().is_empty() {
416              return BurnResult::InvalidRecordCommitments;
417          }
418  
419          // Execute burn
420          if gid.record_burn(request.amount()).is_err() {
421              return BurnResult::ExceedsOutstandingBalance {
422                  outstanding: gid.outstanding_balance(),
423                  requested: request.amount(),
424              };
425          }
426  
427          gid.increment_nonce();
428  
429          BurnResult::Success { new_outstanding_balance: gid.outstanding_balance() }
430      }
431  
432      /// Validate mint request without executing
433      pub fn validate_mint(&self, request: &MintRequest<N>, gid: &GovernorIdentity<N>) -> Result<()> {
434          ensure!(gid.is_active(), "GID is not active");
435          ensure!(
436              gid.can_mint(request.amount()),
437              "Would exceed mint limit (remaining capacity: {})",
438              gid.remaining_mint_capacity()
439          );
440          ensure!(request.nonce() == gid.nonce() + 1, "Invalid nonce");
441  
442          let message = request.signing_message();
443          match gid.verify_signature(request.signature(), &message) {
444              VerifyResult::Valid => Ok(()),
445              result => bail!("Invalid signature: {:?}", result),
446          }
447      }
448  
449      /// Validate burn request without executing
450      pub fn validate_burn(&self, request: &BurnRequest<N>, gid: &GovernorIdentity<N>) -> Result<()> {
451          ensure!(gid.is_active(), "GID is not active");
452          ensure!(request.amount() <= gid.outstanding_balance(), "Burn amount exceeds outstanding balance");
453          ensure!(request.nonce() == gid.nonce() + 1, "Invalid nonce");
454          ensure!(!request.record_commitments().is_empty(), "No record commitments provided");
455  
456          let message = request.signing_message();
457          match gid.verify_signature(request.signature(), &message) {
458              VerifyResult::Valid => Ok(()),
459              result => bail!("Invalid signature: {:?}", result),
460          }
461      }
462  }
463  
464  /// Event emitted when tokens are minted
465  #[derive(Clone, Debug)]
466  pub struct MintEvent<N: Network> {
467      /// The GID that minted
468      gid_id: GidId<N>,
469      /// Recipient address
470      recipient: Address<N>,
471      /// Amount minted
472      amount: u64,
473      /// New outstanding balance
474      outstanding_balance: u64,
475      /// Record commitment
476      record_commitment: Field<N>,
477      /// Block height when minted
478      block_height: u32,
479  }
480  
481  impl<N: Network> MintEvent<N> {
482      /// Create a new mint event
483      pub fn new(
484          gid_id: GidId<N>,
485          recipient: Address<N>,
486          amount: u64,
487          outstanding_balance: u64,
488          record_commitment: Field<N>,
489          block_height: u32,
490      ) -> Self {
491          Self { gid_id, recipient, amount, outstanding_balance, record_commitment, block_height }
492      }
493  
494      /// Get the GID ID
495      pub fn gid_id(&self) -> &GidId<N> {
496          &self.gid_id
497      }
498  
499      /// Get the recipient
500      pub fn recipient(&self) -> &Address<N> {
501          &self.recipient
502      }
503  
504      /// Get the amount
505      pub fn amount(&self) -> u64 {
506          self.amount
507      }
508  }
509  
510  /// Event emitted when tokens are burned
511  #[derive(Clone, Debug)]
512  pub struct BurnEvent<N: Network> {
513      /// The GID that authorized the burn
514      gid_id: GidId<N>,
515      /// Amount burned
516      amount: u64,
517      /// New outstanding balance
518      outstanding_balance: u64,
519      /// Record commitments that were nullified
520      nullified_commitments: Vec<Field<N>>,
521      /// Block height when burned
522      block_height: u32,
523  }
524  
525  impl<N: Network> BurnEvent<N> {
526      /// Create a new burn event
527      pub fn new(
528          gid_id: GidId<N>,
529          amount: u64,
530          outstanding_balance: u64,
531          nullified_commitments: Vec<Field<N>>,
532          block_height: u32,
533      ) -> Self {
534          Self { gid_id, amount, outstanding_balance, nullified_commitments, block_height }
535      }
536  
537      /// Get the GID ID
538      pub fn gid_id(&self) -> &GidId<N> {
539          &self.gid_id
540      }
541  
542      /// Get the amount
543      pub fn amount(&self) -> u64 {
544          self.amount
545      }
546  }
547  
548  #[cfg(test)]
549  mod tests {
550      use super::*;
551      use crate::{
552          bls::{BlsPublicKey, BlsSignature, SignatureAggregator},
553          console::{network::MainnetV0, types::Group},
554      };
555      use core::ops::Add;
556  
557      type CurrentNetwork = MainnetV0;
558  
559      fn create_unique_pubkey(id: u64) -> BlsPublicKey<CurrentNetwork> {
560          let generator = Group::<CurrentNetwork>::generator();
561          let mut point = generator.clone();
562          for _ in 0..id {
563              point = point.add(&generator);
564          }
565          BlsPublicKey::new(point)
566      }
567  
568      fn create_test_gid() -> GovernorIdentity<CurrentNetwork> {
569          let registrar: Address<CurrentNetwork> =
570              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
571  
572          let generator = Group::<CurrentNetwork>::generator();
573          let mut point = generator.clone();
574          let pubkeys: Vec<BlsPublicKey<CurrentNetwork>> = (0..7)
575              .map(|_| {
576                  let key = BlsPublicKey::new(point.clone());
577                  point = point.add(&generator);
578                  key
579              })
580              .collect();
581  
582          let mut gid = GovernorIdentity::new(registrar, 5, pubkeys, 1_000_000, 100, 0).unwrap();
583          // Set a linked validator so minting tests work (required per F-A20)
584          let validator_key = create_unique_pubkey(1000);
585          gid.set_linked_validator(validator_key).unwrap();
586          gid
587      }
588  
589      fn create_aggregate_signature(count: usize) -> AggregateSignature<CurrentNetwork> {
590          let mut aggregator = SignatureAggregator::new(10);
591          for i in 0..count {
592              let sig = BlsSignature::new(Field::from_u64(i as u64), Field::from_u64(i as u64 + 1));
593              aggregator.add_signature(i, sig).unwrap();
594          }
595          aggregator.aggregate().unwrap()
596      }
597  
598      #[test]
599      fn test_mint_request_creation() {
600          let gid_id: GidId<CurrentNetwork> = GidId::new(Field::from_u64(1));
601          let recipient: Address<CurrentNetwork> =
602              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
603          let signature = create_aggregate_signature(5);
604  
605          let request = MintRequest::new(gid_id, recipient, 1000, 1, signature);
606          assert_eq!(request.amount(), 1000);
607          assert_eq!(request.nonce(), 1);
608      }
609  
610      #[test]
611      fn test_burn_request_creation() {
612          let gid_id: GidId<CurrentNetwork> = GidId::new(Field::from_u64(1));
613          let commitments = vec![Field::from_u64(100), Field::from_u64(200)];
614          let signature = create_aggregate_signature(5);
615  
616          let request = BurnRequest::new(gid_id, 500, commitments.clone(), 1, signature);
617          assert_eq!(request.amount(), 500);
618          assert_eq!(request.record_commitments().len(), 2);
619      }
620  
621      #[test]
622      fn test_outstanding_balance() {
623          let gid_id: GidId<CurrentNetwork> = GidId::new(Field::from_u64(1));
624          let mut balance = OutstandingBalance::new(gid_id);
625  
626          assert_eq!(balance.net_balance(), 0);
627  
628          balance.record_mint(1000, 1);
629          assert_eq!(balance.net_balance(), 1000);
630          assert_eq!(balance.total_minted(), 1000);
631  
632          balance.record_burn(400, 2).unwrap();
633          assert_eq!(balance.net_balance(), 600);
634          assert_eq!(balance.total_burned(), 400);
635  
636          // Cannot burn more than balance
637          assert!(balance.record_burn(700, 3).is_err());
638      }
639  
640      #[test]
641      fn test_process_mint_success() {
642          let mut gid = create_test_gid();
643          let processor = MintBurnProcessor::<CurrentNetwork>::new();
644  
645          let recipient: Address<CurrentNetwork> =
646              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
647          let signature = create_aggregate_signature(5);
648  
649          let request = MintRequest::new(
650              gid.id().clone(),
651              recipient,
652              500_000,
653              1, // nonce = current (0) + 1
654              signature,
655          );
656  
657          match processor.process_mint(&request, &mut gid) {
658              MintResult::Success { new_outstanding_balance, .. } => {
659                  assert_eq!(new_outstanding_balance, 500_000);
660              }
661              other => panic!("Expected success, got {:?}", other),
662          }
663      }
664  
665      #[test]
666      fn test_process_mint_cumulative_limit() {
667          let mut gid = create_test_gid();
668          let processor = MintBurnProcessor::<CurrentNetwork>::new();
669  
670          let recipient: Address<CurrentNetwork> =
671              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
672  
673          // First mint should succeed (up to mint_limit)
674          let sig1 = create_aggregate_signature(5);
675          let request1 = MintRequest::new(
676              gid.id().clone(),
677              recipient.clone(),
678              1_000_000, // Full mint limit
679              1,
680              sig1,
681          );
682  
683          assert!(matches!(processor.process_mint(&request1, &mut gid), MintResult::Success { .. }));
684  
685          // Second mint should fail (exceeds cumulative limit)
686          let sig2 = create_aggregate_signature(5);
687          let request2 = MintRequest::new(
688              gid.id().clone(),
689              recipient.clone(),
690              1, // Even 1 more should fail
691              2,
692              sig2,
693          );
694  
695          assert!(matches!(processor.process_mint(&request2, &mut gid), MintResult::MintLimitExceeded { .. }));
696  
697          // Burning creates capacity for more minting
698          gid.record_burn(500_000).unwrap();
699          assert_eq!(gid.remaining_mint_capacity(), 500_000);
700  
701          // Now minting should work again
702          let sig3 = create_aggregate_signature(5);
703          let request3 = MintRequest::new(gid.id().clone(), recipient, 500_000, 2, sig3);
704  
705          assert!(matches!(processor.process_mint(&request3, &mut gid), MintResult::Success { .. }));
706      }
707  
708      #[test]
709      fn test_process_burn_success() {
710          let mut gid = create_test_gid();
711          let processor = MintBurnProcessor::<CurrentNetwork>::new();
712  
713          // First mint some tokens
714          let recipient: Address<CurrentNetwork> =
715              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
716          let mint_sig = create_aggregate_signature(5);
717          let mint_request = MintRequest::new(gid.id().clone(), recipient, 1_000_000, 1, mint_sig);
718          processor.process_mint(&mint_request, &mut gid);
719  
720          // Now burn some
721          let burn_sig = create_aggregate_signature(5);
722          let burn_request = BurnRequest::new(
723              gid.id().clone(),
724              400_000,
725              vec![Field::from_u64(100)], // Dummy commitment
726              2,                          // Next nonce
727              burn_sig,
728          );
729  
730          match processor.process_burn(&burn_request, &mut gid) {
731              BurnResult::Success { new_outstanding_balance } => {
732                  assert_eq!(new_outstanding_balance, 600_000);
733              }
734              other => panic!("Expected success, got {:?}", other),
735          }
736      }
737  
738      #[test]
739      fn test_process_burn_exceeds_balance() {
740          let mut gid = create_test_gid();
741          let processor = MintBurnProcessor::<CurrentNetwork>::new();
742  
743          // Try to burn without minting first
744          let burn_sig = create_aggregate_signature(5);
745          let burn_request = BurnRequest::new(gid.id().clone(), 100, vec![Field::from_u64(100)], 1, burn_sig);
746  
747          assert!(matches!(
748              processor.process_burn(&burn_request, &mut gid),
749              BurnResult::ExceedsOutstandingBalance { .. }
750          ));
751      }
752  
753      #[test]
754      fn test_invalid_nonce() {
755          let mut gid = create_test_gid();
756          let processor = MintBurnProcessor::<CurrentNetwork>::new();
757  
758          let recipient: Address<CurrentNetwork> =
759              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
760          let signature = create_aggregate_signature(5);
761  
762          // Wrong nonce (should be 1, not 5)
763          let request = MintRequest::new(
764              gid.id().clone(),
765              recipient,
766              500_000,
767              5, // Wrong nonce
768              signature,
769          );
770  
771          assert!(matches!(processor.process_mint(&request, &mut gid), MintResult::InvalidNonce { expected: 1, got: 5 }));
772      }
773  
774      #[test]
775      fn test_frozen_gid_rejected() {
776          let mut gid = create_test_gid();
777          gid.freeze().unwrap();
778  
779          let processor = MintBurnProcessor::<CurrentNetwork>::new();
780  
781          let recipient: Address<CurrentNetwork> =
782              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
783          let signature = create_aggregate_signature(5);
784  
785          let request = MintRequest::new(gid.id().clone(), recipient, 500_000, 1, signature);
786  
787          assert!(matches!(processor.process_mint(&request, &mut gid), MintResult::GidNotActive));
788      }
789  
790      #[test]
791      fn test_signing_messages_different() {
792          let gid_id: GidId<CurrentNetwork> = GidId::new(Field::from_u64(1));
793          let recipient: Address<CurrentNetwork> =
794              Address::from_str("ax1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3qtczd").unwrap();
795          let signature = create_aggregate_signature(5);
796  
797          let mint = MintRequest::new(gid_id.clone(), recipient, 1000, 1, signature.clone());
798          let burn = BurnRequest::new(gid_id, 1000, vec![Field::from_u64(1)], 1, signature);
799  
800          // Signing messages should be different
801          assert_ne!(mint.signing_message(), burn.signing_message());
802          assert!(mint.signing_message().starts_with(b"ALPHA_MINT_V1:"));
803          assert!(burn.signing_message().starts_with(b"ALPHA_BURN_V1:"));
804      }
805  }