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 }