restructuring.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 //! National Restructuring Operations (F-A28, F-A29, F-A30) 17 //! 18 //! Handles geopolitical restructuring of Governor Identities: 19 //! 20 //! ## National Split (F-A28) 21 //! 22 //! When a nation splits into multiple successor states: 23 //! 1. Original GID is suspended pending split 24 //! 2. Outstanding Balance is divided among successors 25 //! 3. Each successor nation onboards independently 26 //! 4. Original GID disabled once all successors active 27 //! 28 //! Requires: 100% quorum, 67% approval 29 //! 30 //! ## National Merger (F-A29) 31 //! 32 //! When multiple nations merge into one: 33 //! 1. Merging GIDs are suspended pending merger 34 //! 2. Outstanding Balances are combined 35 //! 3. New unified GID is created 36 //! 4. Original GIDs disabled once merger complete 37 //! 38 //! Requires: 100% quorum from all merging GIDs, 67% approval each 39 //! 40 //! ## National Dissolution (F-A30) 41 //! 42 //! When a nation ceases to exist without successors: 43 //! 1. Dissolution proposed by external GIDs (not the dissolving nation) 44 //! 2. Outstanding Balance redistributed pro rata to remaining GIDs 45 //! 3. Mint limit redistributed pro rata based on current mint limits 46 //! 4. Original GID permanently disabled (status: Dissolved) 47 //! 48 //! Pro rata redistribution formula: 49 //! ```text 50 //! gid_share = (gid_mint_limit / total_remaining_mint_limits) * dissolved_amount 51 //! ``` 52 //! 53 //! Requires: 67% approval from external GIDs (dissolving GID cannot vote) 54 55 use crate::{ 56 GOVERNOR_APPROVAL_THRESHOLD, 57 console::{prelude::*, types::Field}, 58 gid::GidId, 59 }; 60 use std::collections::HashMap; 61 62 // ============================================================================ 63 // Constants 64 // ============================================================================ 65 66 /// Quorum required for split/merger operations (100%) 67 pub const RESTRUCTURING_QUORUM_THRESHOLD: u8 = 100; 68 69 /// Approval threshold for split/merger (67%) 70 pub const RESTRUCTURING_APPROVAL_THRESHOLD: u8 = GOVERNOR_APPROVAL_THRESHOLD; 71 72 /// Maximum time (in epochs) allowed for successor onboarding 73 pub const MAX_SUCCESSOR_ONBOARDING_EPOCHS: u64 = 365; // 1 year 74 75 /// Maximum successor nations in a split 76 pub const MAX_SUCCESSOR_NATIONS: usize = 10; 77 78 /// Maximum merging nations in a merger 79 pub const MAX_MERGING_NATIONS: usize = 5; 80 81 /// Minimum number of remaining GIDs after dissolution (cannot dissolve to zero) 82 pub const MIN_REMAINING_GIDS_AFTER_DISSOLUTION: usize = 2; 83 84 /// Dissolution voting period in epochs (30 days) 85 pub const DISSOLUTION_VOTING_PERIOD_EPOCHS: u64 = 30; 86 87 // ============================================================================ 88 // Split Status 89 // ============================================================================ 90 91 /// Status of a national split operation 92 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 93 pub enum SplitStatus { 94 /// Split proposed, awaiting approval 95 Proposed, 96 /// Approved, original GID suspended 97 Approved, 98 /// Some successors have onboarded 99 PartiallyComplete, 100 /// All successors onboarded, original disabled 101 Complete, 102 /// Split was rejected 103 Rejected, 104 /// Split expired (successors didn't onboard in time) 105 Expired, 106 } 107 108 impl std::fmt::Display for SplitStatus { 109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 110 match self { 111 SplitStatus::Proposed => write!(f, "proposed"), 112 SplitStatus::Approved => write!(f, "approved"), 113 SplitStatus::PartiallyComplete => write!(f, "partially_complete"), 114 SplitStatus::Complete => write!(f, "complete"), 115 SplitStatus::Rejected => write!(f, "rejected"), 116 SplitStatus::Expired => write!(f, "expired"), 117 } 118 } 119 } 120 121 // ============================================================================ 122 // Successor Allocation 123 // ============================================================================ 124 125 /// Allocation for a successor nation in a split 126 #[derive(Clone, Debug)] 127 pub struct SuccessorAllocation<N: Network> { 128 /// Successor nation identifier (e.g., "Scotland", "Catalonia") 129 nation_id: String, 130 /// Allocated portion of Outstanding Balance (in absolute terms) 131 balance_share: u64, 132 /// Percentage of original mint limit allocated (basis points) 133 mint_limit_share_bps: u16, 134 /// New GID once successor onboards (None until onboarded) 135 successor_gid: Option<GidId<N>>, 136 /// Epoch when successor onboarded (None until onboarded) 137 onboarded_epoch: Option<u64>, 138 } 139 140 impl<N: Network> SuccessorAllocation<N> { 141 /// Create a new successor allocation 142 pub fn new(nation_id: String, balance_share: u64, mint_limit_share_bps: u16) -> Self { 143 Self { nation_id, balance_share, mint_limit_share_bps, successor_gid: None, onboarded_epoch: None } 144 } 145 146 /// Get nation ID 147 pub fn nation_id(&self) -> &str { 148 &self.nation_id 149 } 150 151 /// Get balance share 152 pub fn balance_share(&self) -> u64 { 153 self.balance_share 154 } 155 156 /// Get mint limit share in basis points 157 pub fn mint_limit_share_bps(&self) -> u16 { 158 self.mint_limit_share_bps 159 } 160 161 /// Check if successor has onboarded 162 pub fn is_onboarded(&self) -> bool { 163 self.successor_gid.is_some() 164 } 165 166 /// Get successor GID if onboarded 167 pub fn successor_gid(&self) -> Option<&GidId<N>> { 168 self.successor_gid.as_ref() 169 } 170 171 /// Record successor onboarding 172 pub fn record_onboarding(&mut self, gid: GidId<N>, epoch: u64) { 173 self.successor_gid = Some(gid); 174 self.onboarded_epoch = Some(epoch); 175 } 176 } 177 178 // ============================================================================ 179 // National Split 180 // ============================================================================ 181 182 /// National Split operation (F-A28) 183 /// 184 /// Handles division of a nation into multiple successor states. 185 #[derive(Clone, Debug)] 186 pub struct NationalSplit<N: Network> { 187 /// Unique split ID 188 split_id: Field<N>, 189 /// Original GID being split 190 original_gid: GidId<N>, 191 /// Hash of treaty amendment document 192 treaty_amendment_hash: [u8; 32], 193 /// Successor nation allocations 194 successors: Vec<SuccessorAllocation<N>>, 195 /// Current status 196 status: SplitStatus, 197 /// Epoch when split was proposed 198 proposed_epoch: u64, 199 /// Epoch when split was approved (if approved) 200 approved_epoch: Option<u64>, 201 /// Epoch when split expires if not completed 202 expiry_epoch: u64, 203 /// Approval votes received (GID index -> approved) 204 votes: HashMap<u8, bool>, 205 /// Total signers who voted 206 votes_cast: u8, 207 } 208 209 impl<N: Network> NationalSplit<N> { 210 /// Create a new split proposal 211 pub fn propose( 212 original_gid: GidId<N>, 213 treaty_amendment_hash: [u8; 32], 214 successors: Vec<SuccessorAllocation<N>>, 215 current_epoch: u64, 216 ) -> Result<Self> { 217 // Validate successor count 218 ensure!(!successors.is_empty(), "At least one successor nation required"); 219 ensure!( 220 successors.len() <= MAX_SUCCESSOR_NATIONS, 221 "Maximum {} successor nations allowed", 222 MAX_SUCCESSOR_NATIONS 223 ); 224 225 // Validate allocations sum to 100% 226 let total_bps: u32 = successors.iter().map(|s| s.mint_limit_share_bps as u32).sum(); 227 ensure!(total_bps == 10000, "Successor mint limit shares must sum to 100% (10000 bps), got {} bps", total_bps); 228 229 // Validate unique nation IDs 230 let mut seen_ids = std::collections::HashSet::new(); 231 for successor in &successors { 232 ensure!(!successor.nation_id.is_empty(), "Nation ID cannot be empty"); 233 ensure!(seen_ids.insert(successor.nation_id.clone()), "Duplicate nation ID: {}", successor.nation_id); 234 } 235 236 // Generate split ID 237 let split_id = Self::generate_split_id(&original_gid, &treaty_amendment_hash, current_epoch); 238 239 Ok(Self { 240 split_id, 241 original_gid, 242 treaty_amendment_hash, 243 successors, 244 status: SplitStatus::Proposed, 245 proposed_epoch: current_epoch, 246 approved_epoch: None, 247 expiry_epoch: current_epoch + MAX_SUCCESSOR_ONBOARDING_EPOCHS, 248 votes: HashMap::new(), 249 votes_cast: 0, 250 }) 251 } 252 253 /// Generate unique split ID 254 fn generate_split_id(gid: &GidId<N>, treaty_hash: &[u8; 32], epoch: u64) -> Field<N> { 255 let mut preimage = Vec::new(); 256 preimage.extend_from_slice(b"NATIONAL_SPLIT"); 257 preimage.extend_from_slice(&gid.id().to_bytes_le().unwrap_or_default()); 258 preimage.extend_from_slice(treaty_hash); 259 preimage.extend_from_slice(&epoch.to_le_bytes()); 260 261 let hash_value = preimage.iter().fold(0u64, |acc, &b| acc.wrapping_add(b as u64)); 262 Field::from_u64(hash_value) 263 } 264 265 /// Record a vote on the split proposal 266 pub fn vote(&mut self, signer_index: u8, approve: bool, total_signers: u8) -> Result<()> { 267 ensure!(self.status == SplitStatus::Proposed, "Can only vote on proposed splits"); 268 ensure!(!self.votes.contains_key(&signer_index), "Signer {} has already voted", signer_index); 269 270 self.votes.insert(signer_index, approve); 271 self.votes_cast += 1; 272 273 // Check if we have 100% quorum 274 if self.votes_cast >= total_signers { 275 let approvals = self.votes.values().filter(|&&v| v).count(); 276 let approval_percent = (approvals * 100) / total_signers as usize; 277 278 if approval_percent >= RESTRUCTURING_APPROVAL_THRESHOLD as usize { 279 self.status = SplitStatus::Approved; 280 } else { 281 self.status = SplitStatus::Rejected; 282 } 283 } 284 285 Ok(()) 286 } 287 288 /// Record a successor onboarding 289 pub fn record_successor_onboarding(&mut self, nation_id: &str, new_gid: GidId<N>, epoch: u64) -> Result<()> { 290 ensure!( 291 self.status == SplitStatus::Approved || self.status == SplitStatus::PartiallyComplete, 292 "Split must be approved to onboard successors" 293 ); 294 ensure!(epoch < self.expiry_epoch, "Split has expired"); 295 296 // Find and update successor 297 let successor = self 298 .successors 299 .iter_mut() 300 .find(|s| s.nation_id == nation_id) 301 .ok_or_else(|| anyhow::anyhow!("Unknown successor nation: {}", nation_id))?; 302 303 ensure!(!successor.is_onboarded(), "Successor {} already onboarded", nation_id); 304 305 successor.record_onboarding(new_gid, epoch); 306 307 // Update status 308 if self.successors.iter().all(|s| s.is_onboarded()) { 309 self.status = SplitStatus::Complete; 310 } else { 311 self.status = SplitStatus::PartiallyComplete; 312 } 313 314 Ok(()) 315 } 316 317 /// Check if split has expired 318 pub fn check_expiry(&mut self, current_epoch: u64) { 319 if current_epoch >= self.expiry_epoch && self.status != SplitStatus::Complete { 320 self.status = SplitStatus::Expired; 321 } 322 } 323 324 /// Get split ID 325 pub fn split_id(&self) -> &Field<N> { 326 &self.split_id 327 } 328 329 /// Get original GID 330 pub fn original_gid(&self) -> &GidId<N> { 331 &self.original_gid 332 } 333 334 /// Get treaty amendment hash 335 pub fn treaty_amendment_hash(&self) -> &[u8; 32] { 336 &self.treaty_amendment_hash 337 } 338 339 /// Get successors 340 pub fn successors(&self) -> &[SuccessorAllocation<N>] { 341 &self.successors 342 } 343 344 /// Get status 345 pub fn status(&self) -> SplitStatus { 346 self.status 347 } 348 349 /// Check if split is complete 350 pub fn is_complete(&self) -> bool { 351 self.status == SplitStatus::Complete 352 } 353 354 /// Get number of onboarded successors 355 pub fn onboarded_count(&self) -> usize { 356 self.successors.iter().filter(|s| s.is_onboarded()).count() 357 } 358 } 359 360 // ============================================================================ 361 // Merger Status 362 // ============================================================================ 363 364 /// Status of a national merger operation 365 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 366 pub enum MergerStatus { 367 /// Merger proposed, awaiting approval from all parties 368 Proposed, 369 /// All parties approved, awaiting new GID creation 370 Approved, 371 /// Merger complete, original GIDs disabled 372 Complete, 373 /// Merger was rejected by at least one party 374 Rejected, 375 } 376 377 impl std::fmt::Display for MergerStatus { 378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 379 match self { 380 MergerStatus::Proposed => write!(f, "proposed"), 381 MergerStatus::Approved => write!(f, "approved"), 382 MergerStatus::Complete => write!(f, "complete"), 383 MergerStatus::Rejected => write!(f, "rejected"), 384 } 385 } 386 } 387 388 // ============================================================================ 389 // Merging GID 390 // ============================================================================ 391 392 /// A GID participating in a merger 393 #[derive(Clone, Debug)] 394 pub struct MergingGid<N: Network> { 395 /// GID being merged 396 gid_id: GidId<N>, 397 /// Outstanding balance being contributed 398 balance_contribution: u64, 399 /// Mint limit being contributed 400 mint_limit_contribution: u64, 401 /// Whether this GID has approved the merger 402 approved: bool, 403 /// Epoch when approved (if approved) 404 approved_epoch: Option<u64>, 405 } 406 407 impl<N: Network> MergingGid<N> { 408 /// Create a new merging GID entry 409 pub fn new(gid_id: GidId<N>, balance: u64, mint_limit: u64) -> Self { 410 Self { 411 gid_id, 412 balance_contribution: balance, 413 mint_limit_contribution: mint_limit, 414 approved: false, 415 approved_epoch: None, 416 } 417 } 418 419 /// Get GID ID 420 pub fn gid_id(&self) -> &GidId<N> { 421 &self.gid_id 422 } 423 424 /// Get balance contribution 425 pub fn balance_contribution(&self) -> u64 { 426 self.balance_contribution 427 } 428 429 /// Get mint limit contribution 430 pub fn mint_limit_contribution(&self) -> u64 { 431 self.mint_limit_contribution 432 } 433 434 /// Check if approved 435 pub fn is_approved(&self) -> bool { 436 self.approved 437 } 438 439 /// Record approval 440 pub fn record_approval(&mut self, epoch: u64) { 441 self.approved = true; 442 self.approved_epoch = Some(epoch); 443 } 444 } 445 446 // ============================================================================ 447 // National Merger 448 // ============================================================================ 449 450 /// National Merger operation (F-A29) 451 /// 452 /// Handles merger of multiple nations into one unified state. 453 #[derive(Clone, Debug)] 454 pub struct NationalMerger<N: Network> { 455 /// Unique merger ID 456 merger_id: Field<N>, 457 /// GIDs being merged 458 merging_gids: Vec<MergingGid<N>>, 459 /// Hash of treaty amendment document 460 treaty_amendment_hash: [u8; 32], 461 /// Name of the new merged nation 462 merged_nation_name: String, 463 /// Combined outstanding balance 464 combined_balance: u64, 465 /// Combined mint limit 466 combined_mint_limit: u64, 467 /// New GID for merged nation (once created) 468 successor_gid: Option<GidId<N>>, 469 /// Current status 470 status: MergerStatus, 471 /// Epoch when merger was proposed 472 proposed_epoch: u64, 473 } 474 475 impl<N: Network> NationalMerger<N> { 476 /// Create a new merger proposal 477 pub fn propose( 478 merging_gids: Vec<MergingGid<N>>, 479 treaty_amendment_hash: [u8; 32], 480 merged_nation_name: String, 481 current_epoch: u64, 482 ) -> Result<Self> { 483 // Validate merging count 484 ensure!(merging_gids.len() >= 2, "At least two GIDs required for merger"); 485 ensure!(merging_gids.len() <= MAX_MERGING_NATIONS, "Maximum {} GIDs can merge", MAX_MERGING_NATIONS); 486 487 ensure!(!merged_nation_name.is_empty(), "Merged nation name cannot be empty"); 488 489 // Calculate combined totals 490 let combined_balance: u64 = merging_gids.iter().map(|g| g.balance_contribution).sum(); 491 let combined_mint_limit: u64 = merging_gids.iter().map(|g| g.mint_limit_contribution).sum(); 492 493 // Generate merger ID 494 let merger_id = Self::generate_merger_id(&merging_gids, &treaty_amendment_hash, current_epoch); 495 496 Ok(Self { 497 merger_id, 498 merging_gids, 499 treaty_amendment_hash, 500 merged_nation_name, 501 combined_balance, 502 combined_mint_limit, 503 successor_gid: None, 504 status: MergerStatus::Proposed, 505 proposed_epoch: current_epoch, 506 }) 507 } 508 509 /// Generate unique merger ID 510 fn generate_merger_id(gids: &[MergingGid<N>], treaty_hash: &[u8; 32], epoch: u64) -> Field<N> { 511 let mut preimage = Vec::new(); 512 preimage.extend_from_slice(b"NATIONAL_MERGER"); 513 for gid in gids { 514 preimage.extend_from_slice(&gid.gid_id.id().to_bytes_le().unwrap_or_default()); 515 } 516 preimage.extend_from_slice(treaty_hash); 517 preimage.extend_from_slice(&epoch.to_le_bytes()); 518 519 let hash_value = preimage.iter().fold(0u64, |acc, &b| acc.wrapping_add(b as u64)); 520 Field::from_u64(hash_value) 521 } 522 523 /// Record approval from a merging GID 524 pub fn record_approval(&mut self, gid_id: &GidId<N>, epoch: u64) -> Result<()> { 525 ensure!(self.status == MergerStatus::Proposed, "Can only approve proposed mergers"); 526 527 let merging_gid = self 528 .merging_gids 529 .iter_mut() 530 .find(|g| g.gid_id == *gid_id) 531 .ok_or_else(|| anyhow::anyhow!("GID not part of this merger"))?; 532 533 ensure!(!merging_gid.is_approved(), "GID has already approved this merger"); 534 535 merging_gid.record_approval(epoch); 536 537 // Check if all GIDs have approved 538 if self.merging_gids.iter().all(|g| g.is_approved()) { 539 self.status = MergerStatus::Approved; 540 } 541 542 Ok(()) 543 } 544 545 /// Reject the merger 546 pub fn reject(&mut self) { 547 self.status = MergerStatus::Rejected; 548 } 549 550 /// Complete the merger with the new GID 551 pub fn complete(&mut self, new_gid: GidId<N>) -> Result<()> { 552 ensure!(self.status == MergerStatus::Approved, "Merger must be approved before completion"); 553 554 self.successor_gid = Some(new_gid); 555 self.status = MergerStatus::Complete; 556 Ok(()) 557 } 558 559 /// Get merger ID 560 pub fn merger_id(&self) -> &Field<N> { 561 &self.merger_id 562 } 563 564 /// Get merging GIDs 565 pub fn merging_gids(&self) -> &[MergingGid<N>] { 566 &self.merging_gids 567 } 568 569 /// Get treaty amendment hash 570 pub fn treaty_amendment_hash(&self) -> &[u8; 32] { 571 &self.treaty_amendment_hash 572 } 573 574 /// Get merged nation name 575 pub fn merged_nation_name(&self) -> &str { 576 &self.merged_nation_name 577 } 578 579 /// Get combined balance 580 pub fn combined_balance(&self) -> u64 { 581 self.combined_balance 582 } 583 584 /// Get combined mint limit 585 pub fn combined_mint_limit(&self) -> u64 { 586 self.combined_mint_limit 587 } 588 589 /// Get successor GID 590 pub fn successor_gid(&self) -> Option<&GidId<N>> { 591 self.successor_gid.as_ref() 592 } 593 594 /// Get status 595 pub fn status(&self) -> MergerStatus { 596 self.status 597 } 598 599 /// Check if merger is complete 600 pub fn is_complete(&self) -> bool { 601 self.status == MergerStatus::Complete 602 } 603 604 /// Get number of approved GIDs 605 pub fn approval_count(&self) -> usize { 606 self.merging_gids.iter().filter(|g| g.is_approved()).count() 607 } 608 } 609 610 // ============================================================================ 611 // Dissolution Reason 612 // ============================================================================ 613 614 /// Reason for national dissolution 615 #[derive(Clone, Debug, PartialEq, Eq)] 616 pub enum DissolutionReason { 617 /// Nation absorbed by another without formal merger (e.g., annexation) 618 Absorbed { 619 /// GID that absorbed this nation 620 absorbing_gid_name: String, 621 }, 622 /// Failed state with no functioning government 623 FailedState { 624 /// Description of failure 625 description: String, 626 }, 627 /// Voluntary dissolution by the nation itself 628 Voluntary { 629 /// Reason for voluntary dissolution 630 reason: String, 631 }, 632 /// Treaty-mandated dissolution (international agreement) 633 TreatyMandated { 634 /// Treaty reference 635 treaty_reference: String, 636 }, 637 /// Nation ceased to exist due to other circumstances 638 Other { 639 /// Description 640 description: String, 641 }, 642 } 643 644 impl std::fmt::Display for DissolutionReason { 645 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 646 match self { 647 DissolutionReason::Absorbed { absorbing_gid_name } => { 648 write!(f, "absorbed_by:{}", absorbing_gid_name) 649 } 650 DissolutionReason::FailedState { .. } => write!(f, "failed_state"), 651 DissolutionReason::Voluntary { .. } => write!(f, "voluntary"), 652 DissolutionReason::TreatyMandated { .. } => write!(f, "treaty_mandated"), 653 DissolutionReason::Other { .. } => write!(f, "other"), 654 } 655 } 656 } 657 658 // ============================================================================ 659 // Dissolution Status 660 // ============================================================================ 661 662 /// Status of a national dissolution operation 663 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 664 pub enum DissolutionStatus { 665 /// Dissolution proposed, awaiting external GID votes 666 Proposed, 667 /// Approved by external GIDs, pending redistribution 668 Approved, 669 /// Redistribution complete, GID dissolved 670 Complete, 671 /// Dissolution was rejected 672 Rejected, 673 /// Voting period expired without reaching threshold 674 Expired, 675 } 676 677 impl std::fmt::Display for DissolutionStatus { 678 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 679 match self { 680 DissolutionStatus::Proposed => write!(f, "proposed"), 681 DissolutionStatus::Approved => write!(f, "approved"), 682 DissolutionStatus::Complete => write!(f, "complete"), 683 DissolutionStatus::Rejected => write!(f, "rejected"), 684 DissolutionStatus::Expired => write!(f, "expired"), 685 } 686 } 687 } 688 689 // ============================================================================ 690 // Balance Redistribution 691 // ============================================================================ 692 693 /// Pro rata redistribution of dissolved GID's outstanding balance 694 #[derive(Clone, Debug)] 695 pub struct BalanceRedistribution<N: Network> { 696 /// Recipient GID 697 gid_id: GidId<N>, 698 /// Recipient's mint limit (used for pro rata calculation) 699 mint_limit: u64, 700 /// Share of outstanding balance received 701 balance_share: u64, 702 /// Share of mint limit received 703 mint_limit_share: u64, 704 /// Whether redistribution has been applied 705 applied: bool, 706 } 707 708 impl<N: Network> BalanceRedistribution<N> { 709 /// Create new redistribution entry 710 pub fn new(gid_id: GidId<N>, mint_limit: u64) -> Self { 711 Self { gid_id, mint_limit, balance_share: 0, mint_limit_share: 0, applied: false } 712 } 713 714 /// Get GID ID 715 pub fn gid_id(&self) -> &GidId<N> { 716 &self.gid_id 717 } 718 719 /// Get mint limit 720 pub fn mint_limit(&self) -> u64 { 721 self.mint_limit 722 } 723 724 /// Get balance share 725 pub fn balance_share(&self) -> u64 { 726 self.balance_share 727 } 728 729 /// Get mint limit share 730 pub fn mint_limit_share(&self) -> u64 { 731 self.mint_limit_share 732 } 733 734 /// Check if applied 735 pub fn is_applied(&self) -> bool { 736 self.applied 737 } 738 739 /// Mark as applied 740 pub fn mark_applied(&mut self) { 741 self.applied = true; 742 } 743 } 744 745 // ============================================================================ 746 // National Dissolution 747 // ============================================================================ 748 749 /// National Dissolution operation (F-A30) 750 /// 751 /// Handles permanent dissolution of a nation without successors. 752 /// Outstanding balance and mint limit are redistributed pro rata 753 /// to remaining GIDs based on their current mint limits. 754 #[derive(Clone, Debug)] 755 pub struct NationalDissolution<N: Network> { 756 /// Unique dissolution ID 757 dissolution_id: Field<N>, 758 /// GID being dissolved 759 dissolving_gid: GidId<N>, 760 /// Nation name (for record keeping) 761 nation_name: String, 762 /// Reason for dissolution 763 reason: DissolutionReason, 764 /// Hash of supporting documentation 765 documentation_hash: [u8; 32], 766 /// Outstanding balance at time of dissolution 767 outstanding_balance: u64, 768 /// Mint limit at time of dissolution 769 mint_limit: u64, 770 /// Redistributions to remaining GIDs 771 redistributions: Vec<BalanceRedistribution<N>>, 772 /// Current status 773 status: DissolutionStatus, 774 /// Epoch when dissolution was proposed 775 proposed_epoch: u64, 776 /// Epoch when voting expires 777 voting_expiry_epoch: u64, 778 /// Votes from external GIDs (GidId -> approved) 779 votes: HashMap<GidId<N>, bool>, 780 /// Total external GIDs eligible to vote 781 total_eligible_voters: usize, 782 /// Epoch when dissolution was completed (if complete) 783 completed_epoch: Option<u64>, 784 } 785 786 impl<N: Network> NationalDissolution<N> { 787 /// Create a new dissolution proposal 788 /// 789 /// # Arguments 790 /// * `dissolving_gid` - The GID to be dissolved 791 /// * `nation_name` - Name of the nation being dissolved 792 /// * `reason` - Reason for dissolution 793 /// * `documentation_hash` - Hash of supporting documents 794 /// * `outstanding_balance` - Current outstanding balance of the GID 795 /// * `mint_limit` - Current mint limit of the GID 796 /// * `remaining_gids` - List of (GidId, mint_limit) for all other active GIDs 797 /// * `current_epoch` - Current epoch number 798 #[allow(clippy::too_many_arguments)] 799 pub fn propose( 800 dissolving_gid: GidId<N>, 801 nation_name: String, 802 reason: DissolutionReason, 803 documentation_hash: [u8; 32], 804 outstanding_balance: u64, 805 mint_limit: u64, 806 remaining_gids: Vec<(GidId<N>, u64)>, 807 current_epoch: u64, 808 ) -> Result<Self> { 809 // Validate minimum remaining GIDs 810 ensure!( 811 remaining_gids.len() >= MIN_REMAINING_GIDS_AFTER_DISSOLUTION, 812 "At least {} GIDs must remain after dissolution", 813 MIN_REMAINING_GIDS_AFTER_DISSOLUTION 814 ); 815 816 ensure!(!nation_name.is_empty(), "Nation name cannot be empty"); 817 818 // Calculate total mint limit of remaining GIDs 819 let total_remaining_mint_limit: u64 = remaining_gids.iter().map(|(_, ml)| ml).sum(); 820 ensure!(total_remaining_mint_limit > 0, "Remaining GIDs must have positive mint limits"); 821 822 // Calculate pro rata redistributions 823 let mut redistributions = Vec::new(); 824 for (gid_id, gid_mint_limit) in remaining_gids.iter() { 825 let mut redist = BalanceRedistribution::new(gid_id.clone(), *gid_mint_limit); 826 827 // Pro rata share based on mint limit proportion 828 // balance_share = (gid_mint_limit / total_remaining_mint_limit) * outstanding_balance 829 redist.balance_share = 830 ((*gid_mint_limit as u128 * outstanding_balance as u128) / total_remaining_mint_limit as u128) as u64; 831 832 // Pro rata share of mint limit 833 // mint_limit_share = (gid_mint_limit / total_remaining_mint_limit) * dissolved_mint_limit 834 redist.mint_limit_share = 835 ((*gid_mint_limit as u128 * mint_limit as u128) / total_remaining_mint_limit as u128) as u64; 836 837 redistributions.push(redist); 838 } 839 840 // Handle rounding: assign any remainder to the largest GID 841 let distributed_balance: u64 = redistributions.iter().map(|r| r.balance_share).sum(); 842 let distributed_mint_limit: u64 = redistributions.iter().map(|r| r.mint_limit_share).sum(); 843 844 if let Some(largest) = redistributions.iter_mut().max_by_key(|r| r.mint_limit) { 845 largest.balance_share += outstanding_balance.saturating_sub(distributed_balance); 846 largest.mint_limit_share += mint_limit.saturating_sub(distributed_mint_limit); 847 } 848 849 // Generate dissolution ID 850 let dissolution_id = Self::generate_dissolution_id(&dissolving_gid, &documentation_hash, current_epoch); 851 852 Ok(Self { 853 dissolution_id, 854 dissolving_gid, 855 nation_name, 856 reason, 857 documentation_hash, 858 outstanding_balance, 859 mint_limit, 860 redistributions, 861 status: DissolutionStatus::Proposed, 862 proposed_epoch: current_epoch, 863 voting_expiry_epoch: current_epoch + DISSOLUTION_VOTING_PERIOD_EPOCHS, 864 votes: HashMap::new(), 865 total_eligible_voters: remaining_gids.len(), 866 completed_epoch: None, 867 }) 868 } 869 870 /// Generate unique dissolution ID 871 fn generate_dissolution_id(gid: &GidId<N>, doc_hash: &[u8; 32], epoch: u64) -> Field<N> { 872 let mut preimage = Vec::new(); 873 preimage.extend_from_slice(b"NATIONAL_DISSOLUTION"); 874 preimage.extend_from_slice(&gid.id().to_bytes_le().unwrap_or_default()); 875 preimage.extend_from_slice(doc_hash); 876 preimage.extend_from_slice(&epoch.to_le_bytes()); 877 878 let hash_value = preimage.iter().fold(0u64, |acc, &b| acc.wrapping_add(b as u64)); 879 Field::from_u64(hash_value) 880 } 881 882 /// Record a vote from an external GID 883 /// 884 /// Note: The dissolving GID cannot vote on its own dissolution 885 pub fn vote(&mut self, voter_gid: &GidId<N>, approve: bool, current_epoch: u64) -> Result<()> { 886 ensure!(self.status == DissolutionStatus::Proposed, "Can only vote on proposed dissolutions"); 887 ensure!(current_epoch < self.voting_expiry_epoch, "Voting period has expired"); 888 ensure!(*voter_gid != self.dissolving_gid, "Dissolving GID cannot vote on its own dissolution"); 889 890 // Check voter is in the redistribution list (eligible voter) 891 ensure!(self.redistributions.iter().any(|r| r.gid_id == *voter_gid), "Voter is not an eligible external GID"); 892 893 ensure!(!self.votes.contains_key(voter_gid), "GID has already voted"); 894 895 self.votes.insert(voter_gid.clone(), approve); 896 897 // Check if we have enough votes to decide 898 self.check_vote_result(); 899 900 Ok(()) 901 } 902 903 /// Check if voting result can be determined 904 fn check_vote_result(&mut self) { 905 let approvals = self.votes.values().filter(|&&v| v).count(); 906 let rejections = self.votes.values().filter(|&&v| !v).count(); 907 let votes_cast = self.votes.len(); 908 909 // Calculate approval percentage 910 let approval_percent = 911 if self.total_eligible_voters > 0 { (approvals * 100) / self.total_eligible_voters } else { 0 }; 912 913 // Check if approved (67% of total eligible voters) 914 if approval_percent >= RESTRUCTURING_APPROVAL_THRESHOLD as usize { 915 self.status = DissolutionStatus::Approved; 916 return; 917 } 918 919 // Check if rejection is guaranteed (more than 33% rejected) 920 let rejection_percent = 921 if self.total_eligible_voters > 0 { (rejections * 100) / self.total_eligible_voters } else { 0 }; 922 923 if rejection_percent > (100 - RESTRUCTURING_APPROVAL_THRESHOLD as usize) { 924 self.status = DissolutionStatus::Rejected; 925 return; 926 } 927 928 // Check if impossible to reach threshold with remaining votes 929 let remaining_votes = self.total_eligible_voters - votes_cast; 930 let max_possible_approvals = approvals + remaining_votes; 931 let max_approval_percent = if self.total_eligible_voters > 0 { 932 (max_possible_approvals * 100) / self.total_eligible_voters 933 } else { 934 0 935 }; 936 937 if max_approval_percent < RESTRUCTURING_APPROVAL_THRESHOLD as usize { 938 self.status = DissolutionStatus::Rejected; 939 } 940 } 941 942 /// Check and update expiry status 943 pub fn check_expiry(&mut self, current_epoch: u64) { 944 if current_epoch >= self.voting_expiry_epoch && self.status == DissolutionStatus::Proposed { 945 self.status = DissolutionStatus::Expired; 946 } 947 } 948 949 /// Complete the dissolution (apply redistributions) 950 pub fn complete(&mut self, current_epoch: u64) -> Result<()> { 951 ensure!(self.status == DissolutionStatus::Approved, "Dissolution must be approved before completion"); 952 953 // Mark all redistributions as applied 954 for redist in &mut self.redistributions { 955 redist.mark_applied(); 956 } 957 958 self.status = DissolutionStatus::Complete; 959 self.completed_epoch = Some(current_epoch); 960 Ok(()) 961 } 962 963 /// Get dissolution ID 964 pub fn dissolution_id(&self) -> &Field<N> { 965 &self.dissolution_id 966 } 967 968 /// Get dissolving GID 969 pub fn dissolving_gid(&self) -> &GidId<N> { 970 &self.dissolving_gid 971 } 972 973 /// Get nation name 974 pub fn nation_name(&self) -> &str { 975 &self.nation_name 976 } 977 978 /// Get dissolution reason 979 pub fn reason(&self) -> &DissolutionReason { 980 &self.reason 981 } 982 983 /// Get documentation hash 984 pub fn documentation_hash(&self) -> &[u8; 32] { 985 &self.documentation_hash 986 } 987 988 /// Get outstanding balance being redistributed 989 pub fn outstanding_balance(&self) -> u64 { 990 self.outstanding_balance 991 } 992 993 /// Get mint limit being redistributed 994 pub fn mint_limit(&self) -> u64 { 995 self.mint_limit 996 } 997 998 /// Get redistributions 999 pub fn redistributions(&self) -> &[BalanceRedistribution<N>] { 1000 &self.redistributions 1001 } 1002 1003 /// Get status 1004 pub fn status(&self) -> DissolutionStatus { 1005 self.status 1006 } 1007 1008 /// Check if dissolution is complete 1009 pub fn is_complete(&self) -> bool { 1010 self.status == DissolutionStatus::Complete 1011 } 1012 1013 /// Get vote count 1014 pub fn vote_count(&self) -> (usize, usize) { 1015 let approvals = self.votes.values().filter(|&&v| v).count(); 1016 let rejections = self.votes.values().filter(|&&v| !v).count(); 1017 (approvals, rejections) 1018 } 1019 1020 /// Get total eligible voters 1021 pub fn total_eligible_voters(&self) -> usize { 1022 self.total_eligible_voters 1023 } 1024 1025 /// Get approval percentage 1026 pub fn approval_percentage(&self) -> u8 { 1027 if self.total_eligible_voters == 0 { 1028 return 0; 1029 } 1030 let approvals = self.votes.values().filter(|&&v| v).count(); 1031 ((approvals * 100) / self.total_eligible_voters) as u8 1032 } 1033 1034 /// Get completed epoch 1035 pub fn completed_epoch(&self) -> Option<u64> { 1036 self.completed_epoch 1037 } 1038 1039 /// Get redistribution for a specific GID 1040 pub fn get_redistribution(&self, gid_id: &GidId<N>) -> Option<&BalanceRedistribution<N>> { 1041 self.redistributions.iter().find(|r| r.gid_id == *gid_id) 1042 } 1043 1044 /// Check if all redistributions have been applied 1045 pub fn all_redistributions_applied(&self) -> bool { 1046 self.redistributions.iter().all(|r| r.applied) 1047 } 1048 } 1049 1050 // ============================================================================ 1051 // Tests 1052 // ============================================================================ 1053 1054 #[cfg(test)] 1055 mod tests { 1056 use super::*; 1057 use alphavm_console::prelude::TestnetV0; 1058 1059 type TestField = Field<TestnetV0>; 1060 type TestGidId = GidId<TestnetV0>; 1061 1062 fn make_test_gid_id(seed: u8) -> TestGidId { 1063 TestGidId::new(TestField::from_u64(seed as u64)) 1064 } 1065 1066 fn make_test_treaty_hash(seed: u8) -> [u8; 32] { 1067 [seed; 32] 1068 } 1069 1070 // ========== Split Tests ========== 1071 1072 #[test] 1073 fn test_split_creation() { 1074 let gid = make_test_gid_id(1); 1075 let treaty = make_test_treaty_hash(1); 1076 1077 let successors = vec![ 1078 SuccessorAllocation::<TestnetV0>::new("Nation A".to_string(), 60_000, 6000), // 60% 1079 SuccessorAllocation::<TestnetV0>::new("Nation B".to_string(), 40_000, 4000), // 40% 1080 ]; 1081 1082 let split = NationalSplit::propose(gid, treaty, successors, 100).unwrap(); 1083 1084 assert_eq!(split.status(), SplitStatus::Proposed); 1085 assert_eq!(split.successors().len(), 2); 1086 assert_eq!(split.onboarded_count(), 0); 1087 } 1088 1089 #[test] 1090 fn test_split_invalid_allocation() { 1091 let gid = make_test_gid_id(1); 1092 let treaty = make_test_treaty_hash(1); 1093 1094 // Allocations don't sum to 100% 1095 let successors = vec![ 1096 SuccessorAllocation::<TestnetV0>::new("Nation A".to_string(), 60_000, 5000), // 50% 1097 SuccessorAllocation::<TestnetV0>::new("Nation B".to_string(), 40_000, 4000), // 40% 1098 ]; 1099 1100 let result = NationalSplit::propose(gid, treaty, successors, 100); 1101 assert!(result.is_err()); 1102 } 1103 1104 #[test] 1105 fn test_split_empty_successors() { 1106 let gid = make_test_gid_id(1); 1107 let treaty = make_test_treaty_hash(1); 1108 1109 let result = NationalSplit::<TestnetV0>::propose(gid, treaty, vec![], 100); 1110 assert!(result.is_err()); 1111 } 1112 1113 #[test] 1114 fn test_split_voting() { 1115 let gid = make_test_gid_id(1); 1116 let treaty = make_test_treaty_hash(1); 1117 1118 let successors = vec![ 1119 SuccessorAllocation::<TestnetV0>::new("Nation A".to_string(), 50_000, 5000), 1120 SuccessorAllocation::<TestnetV0>::new("Nation B".to_string(), 50_000, 5000), 1121 ]; 1122 1123 let mut split = NationalSplit::propose(gid, treaty, successors, 100).unwrap(); 1124 1125 // 5 signers, need 100% quorum 1126 split.vote(0, true, 5).unwrap(); 1127 assert_eq!(split.status(), SplitStatus::Proposed); 1128 1129 split.vote(1, true, 5).unwrap(); 1130 split.vote(2, true, 5).unwrap(); 1131 split.vote(3, true, 5).unwrap(); 1132 split.vote(4, true, 5).unwrap(); 1133 1134 // All voted yes, should be approved (100% > 67%) 1135 assert_eq!(split.status(), SplitStatus::Approved); 1136 } 1137 1138 #[test] 1139 fn test_split_rejection() { 1140 let gid = make_test_gid_id(1); 1141 let treaty = make_test_treaty_hash(1); 1142 1143 let successors = vec![ 1144 SuccessorAllocation::<TestnetV0>::new("Nation A".to_string(), 50_000, 5000), 1145 SuccessorAllocation::<TestnetV0>::new("Nation B".to_string(), 50_000, 5000), 1146 ]; 1147 1148 let mut split = NationalSplit::propose(gid, treaty, successors, 100).unwrap(); 1149 1150 // 5 signers, only 2 approve (40% < 67%) 1151 split.vote(0, true, 5).unwrap(); 1152 split.vote(1, true, 5).unwrap(); 1153 split.vote(2, false, 5).unwrap(); 1154 split.vote(3, false, 5).unwrap(); 1155 split.vote(4, false, 5).unwrap(); 1156 1157 assert_eq!(split.status(), SplitStatus::Rejected); 1158 } 1159 1160 #[test] 1161 fn test_split_onboarding() { 1162 let gid = make_test_gid_id(1); 1163 let treaty = make_test_treaty_hash(1); 1164 1165 let successors = vec![ 1166 SuccessorAllocation::<TestnetV0>::new("Nation A".to_string(), 50_000, 5000), 1167 SuccessorAllocation::<TestnetV0>::new("Nation B".to_string(), 50_000, 5000), 1168 ]; 1169 1170 let mut split = NationalSplit::propose(gid, treaty, successors, 100).unwrap(); 1171 split.status = SplitStatus::Approved; // Skip voting for test 1172 1173 let new_gid_a = make_test_gid_id(10); 1174 let new_gid_b = make_test_gid_id(11); 1175 1176 split.record_successor_onboarding("Nation A", new_gid_a, 150).unwrap(); 1177 assert_eq!(split.status(), SplitStatus::PartiallyComplete); 1178 assert_eq!(split.onboarded_count(), 1); 1179 1180 split.record_successor_onboarding("Nation B", new_gid_b, 160).unwrap(); 1181 assert_eq!(split.status(), SplitStatus::Complete); 1182 assert_eq!(split.onboarded_count(), 2); 1183 } 1184 1185 // ========== Merger Tests ========== 1186 1187 #[test] 1188 fn test_merger_creation() { 1189 let gids = vec![ 1190 MergingGid::<TestnetV0>::new(make_test_gid_id(1), 100_000, 500_000), 1191 MergingGid::<TestnetV0>::new(make_test_gid_id(2), 150_000, 600_000), 1192 ]; 1193 let treaty = make_test_treaty_hash(1); 1194 1195 let merger = NationalMerger::propose(gids, treaty, "United Nation".to_string(), 100).unwrap(); 1196 1197 assert_eq!(merger.status(), MergerStatus::Proposed); 1198 assert_eq!(merger.combined_balance(), 250_000); 1199 assert_eq!(merger.combined_mint_limit(), 1_100_000); 1200 assert_eq!(merger.approval_count(), 0); 1201 } 1202 1203 #[test] 1204 fn test_merger_single_gid_rejected() { 1205 let gids = vec![MergingGid::<TestnetV0>::new(make_test_gid_id(1), 100_000, 500_000)]; 1206 let treaty = make_test_treaty_hash(1); 1207 1208 let result = NationalMerger::propose(gids, treaty, "Solo".to_string(), 100); 1209 assert!(result.is_err()); 1210 } 1211 1212 #[test] 1213 fn test_merger_approval() { 1214 let gid1 = make_test_gid_id(1); 1215 let gid2 = make_test_gid_id(2); 1216 1217 let gids = vec![ 1218 MergingGid::<TestnetV0>::new(gid1.clone(), 100_000, 500_000), 1219 MergingGid::<TestnetV0>::new(gid2.clone(), 150_000, 600_000), 1220 ]; 1221 let treaty = make_test_treaty_hash(1); 1222 1223 let mut merger = NationalMerger::propose(gids, treaty, "United Nation".to_string(), 100).unwrap(); 1224 1225 merger.record_approval(&gid1, 110).unwrap(); 1226 assert_eq!(merger.status(), MergerStatus::Proposed); 1227 assert_eq!(merger.approval_count(), 1); 1228 1229 merger.record_approval(&gid2, 120).unwrap(); 1230 assert_eq!(merger.status(), MergerStatus::Approved); 1231 assert_eq!(merger.approval_count(), 2); 1232 } 1233 1234 #[test] 1235 fn test_merger_completion() { 1236 let gid1 = make_test_gid_id(1); 1237 let gid2 = make_test_gid_id(2); 1238 1239 let gids = vec![ 1240 MergingGid::<TestnetV0>::new(gid1.clone(), 100_000, 500_000), 1241 MergingGid::<TestnetV0>::new(gid2.clone(), 150_000, 600_000), 1242 ]; 1243 let treaty = make_test_treaty_hash(1); 1244 1245 let mut merger = NationalMerger::propose(gids, treaty, "United Nation".to_string(), 100).unwrap(); 1246 1247 merger.record_approval(&gid1, 110).unwrap(); 1248 merger.record_approval(&gid2, 120).unwrap(); 1249 1250 let new_gid = make_test_gid_id(100); 1251 merger.complete(new_gid.clone()).unwrap(); 1252 1253 assert_eq!(merger.status(), MergerStatus::Complete); 1254 assert!(merger.is_complete()); 1255 assert_eq!(merger.successor_gid(), Some(&new_gid)); 1256 } 1257 1258 #[test] 1259 fn test_successor_allocation() { 1260 let mut alloc = SuccessorAllocation::<TestnetV0>::new("New Nation".to_string(), 100_000, 5000); 1261 1262 assert!(!alloc.is_onboarded()); 1263 assert_eq!(alloc.nation_id(), "New Nation"); 1264 assert_eq!(alloc.balance_share(), 100_000); 1265 assert_eq!(alloc.mint_limit_share_bps(), 5000); 1266 1267 let new_gid = make_test_gid_id(10); 1268 alloc.record_onboarding(new_gid.clone(), 200); 1269 1270 assert!(alloc.is_onboarded()); 1271 assert_eq!(alloc.successor_gid(), Some(&new_gid)); 1272 } 1273 1274 // ========== Dissolution Tests ========== 1275 1276 #[test] 1277 fn test_dissolution_creation() { 1278 let dissolving_gid = make_test_gid_id(1); 1279 let doc_hash = make_test_treaty_hash(1); 1280 1281 // Remaining GIDs with their mint limits 1282 let remaining = vec![ 1283 (make_test_gid_id(2), 100_000), // 25% 1284 (make_test_gid_id(3), 200_000), // 50% 1285 (make_test_gid_id(4), 100_000), // 25% 1286 ]; 1287 1288 let dissolution = NationalDissolution::propose( 1289 dissolving_gid, 1290 "Dissolved Nation".to_string(), 1291 DissolutionReason::FailedState { description: "Government collapsed".to_string() }, 1292 doc_hash, 1293 1_000_000, // outstanding balance 1294 500_000, // mint limit 1295 remaining, 1296 100, 1297 ) 1298 .unwrap(); 1299 1300 assert_eq!(dissolution.status(), DissolutionStatus::Proposed); 1301 assert_eq!(dissolution.outstanding_balance(), 1_000_000); 1302 assert_eq!(dissolution.mint_limit(), 500_000); 1303 assert_eq!(dissolution.redistributions().len(), 3); 1304 assert_eq!(dissolution.total_eligible_voters(), 3); 1305 } 1306 1307 #[test] 1308 fn test_dissolution_pro_rata_distribution() { 1309 let dissolving_gid = make_test_gid_id(1); 1310 let doc_hash = make_test_treaty_hash(1); 1311 1312 // Remaining GIDs: 25%, 50%, 25% of total mint limit 1313 let remaining = vec![ 1314 (make_test_gid_id(2), 100_000), // 25% 1315 (make_test_gid_id(3), 200_000), // 50% 1316 (make_test_gid_id(4), 100_000), // 25% 1317 ]; 1318 1319 let dissolution = NationalDissolution::propose( 1320 dissolving_gid, 1321 "Dissolved Nation".to_string(), 1322 DissolutionReason::Voluntary { reason: "Peaceful dissolution".to_string() }, 1323 doc_hash, 1324 1_000_000, // outstanding balance to distribute 1325 400_000, // mint limit to distribute 1326 remaining, 1327 100, 1328 ) 1329 .unwrap(); 1330 1331 // Check pro rata distribution 1332 let gid2 = make_test_gid_id(2); 1333 let gid3 = make_test_gid_id(3); 1334 let gid4 = make_test_gid_id(4); 1335 1336 let r2 = dissolution.get_redistribution(&gid2).unwrap(); 1337 let r3 = dissolution.get_redistribution(&gid3).unwrap(); 1338 let r4 = dissolution.get_redistribution(&gid4).unwrap(); 1339 1340 // GID 2 (25%): 250,000 balance, 100,000 mint limit 1341 assert_eq!(r2.balance_share(), 250_000); 1342 assert_eq!(r2.mint_limit_share(), 100_000); 1343 1344 // GID 3 (50%): 500,000 balance, 200,000 mint limit 1345 assert_eq!(r3.balance_share(), 500_000); 1346 assert_eq!(r3.mint_limit_share(), 200_000); 1347 1348 // GID 4 (25%): 250,000 balance, 100,000 mint limit 1349 assert_eq!(r4.balance_share(), 250_000); 1350 assert_eq!(r4.mint_limit_share(), 100_000); 1351 1352 // Total should equal original amounts 1353 let total_balance: u64 = dissolution.redistributions().iter().map(|r| r.balance_share()).sum(); 1354 let total_mint: u64 = dissolution.redistributions().iter().map(|r| r.mint_limit_share()).sum(); 1355 assert_eq!(total_balance, 1_000_000); 1356 assert_eq!(total_mint, 400_000); 1357 } 1358 1359 #[test] 1360 fn test_dissolution_voting_approval() { 1361 let dissolving_gid = make_test_gid_id(1); 1362 let doc_hash = make_test_treaty_hash(1); 1363 1364 let gid2 = make_test_gid_id(2); 1365 let gid3 = make_test_gid_id(3); 1366 let gid4 = make_test_gid_id(4); 1367 1368 let remaining = vec![(gid2.clone(), 100_000), (gid3.clone(), 100_000), (gid4.clone(), 100_000)]; 1369 1370 let mut dissolution = NationalDissolution::propose( 1371 dissolving_gid, 1372 "Dissolved Nation".to_string(), 1373 DissolutionReason::TreatyMandated { treaty_reference: "UN Resolution 1234".to_string() }, 1374 doc_hash, 1375 1_000_000, 1376 500_000, 1377 remaining, 1378 100, 1379 ) 1380 .unwrap(); 1381 1382 // 3 voters, need 67% = 2 votes 1383 dissolution.vote(&gid2, true, 105).unwrap(); 1384 assert_eq!(dissolution.status(), DissolutionStatus::Proposed); 1385 1386 dissolution.vote(&gid3, true, 110).unwrap(); 1387 // 2/3 = 66.67%, which rounds down to 66% < 67%, still proposed 1388 assert_eq!(dissolution.status(), DissolutionStatus::Proposed); 1389 1390 dissolution.vote(&gid4, true, 115).unwrap(); 1391 // 3/3 = 100% >= 67%, approved 1392 assert_eq!(dissolution.status(), DissolutionStatus::Approved); 1393 } 1394 1395 #[test] 1396 fn test_dissolution_voting_rejection() { 1397 let dissolving_gid = make_test_gid_id(1); 1398 let doc_hash = make_test_treaty_hash(1); 1399 1400 let gid2 = make_test_gid_id(2); 1401 let gid3 = make_test_gid_id(3); 1402 let gid4 = make_test_gid_id(4); 1403 1404 let remaining = vec![(gid2.clone(), 100_000), (gid3.clone(), 100_000), (gid4.clone(), 100_000)]; 1405 1406 let mut dissolution = NationalDissolution::propose( 1407 dissolving_gid, 1408 "Dissolved Nation".to_string(), 1409 DissolutionReason::Other { description: "Test".to_string() }, 1410 doc_hash, 1411 1_000_000, 1412 500_000, 1413 remaining, 1414 100, 1415 ) 1416 .unwrap(); 1417 1418 // With 3 voters, after 1 rejection max possible approval is 2/3 = 66% < 67% 1419 // So it gets rejected immediately when approval becomes impossible 1420 dissolution.vote(&gid2, false, 105).unwrap(); 1421 1422 // After 1 rejection with 3 voters, max possible = 2/3 = 66% < 67%, so rejected 1423 assert_eq!(dissolution.status(), DissolutionStatus::Rejected); 1424 } 1425 1426 #[test] 1427 fn test_dissolution_self_vote_rejected() { 1428 let dissolving_gid = make_test_gid_id(1); 1429 let doc_hash = make_test_treaty_hash(1); 1430 1431 let gid2 = make_test_gid_id(2); 1432 let gid3 = make_test_gid_id(3); 1433 1434 let remaining = vec![(gid2.clone(), 100_000), (gid3.clone(), 100_000)]; 1435 1436 let mut dissolution = NationalDissolution::propose( 1437 dissolving_gid.clone(), 1438 "Dissolved Nation".to_string(), 1439 DissolutionReason::Absorbed { absorbing_gid_name: "Larger Nation".to_string() }, 1440 doc_hash, 1441 1_000_000, 1442 500_000, 1443 remaining, 1444 100, 1445 ) 1446 .unwrap(); 1447 1448 // Dissolving GID cannot vote on its own dissolution 1449 let result = dissolution.vote(&dissolving_gid, true, 105); 1450 assert!(result.is_err()); 1451 } 1452 1453 #[test] 1454 fn test_dissolution_completion() { 1455 let dissolving_gid = make_test_gid_id(1); 1456 let doc_hash = make_test_treaty_hash(1); 1457 1458 let gid2 = make_test_gid_id(2); 1459 let gid3 = make_test_gid_id(3); 1460 1461 let remaining = vec![(gid2.clone(), 100_000), (gid3.clone(), 100_000)]; 1462 1463 let mut dissolution = NationalDissolution::propose( 1464 dissolving_gid, 1465 "Dissolved Nation".to_string(), 1466 DissolutionReason::Voluntary { reason: "Test".to_string() }, 1467 doc_hash, 1468 1_000_000, 1469 500_000, 1470 remaining, 1471 100, 1472 ) 1473 .unwrap(); 1474 1475 // Approve 1476 dissolution.vote(&gid2, true, 105).unwrap(); 1477 dissolution.vote(&gid3, true, 110).unwrap(); 1478 assert_eq!(dissolution.status(), DissolutionStatus::Approved); 1479 1480 // Complete 1481 dissolution.complete(120).unwrap(); 1482 assert_eq!(dissolution.status(), DissolutionStatus::Complete); 1483 assert!(dissolution.is_complete()); 1484 assert_eq!(dissolution.completed_epoch(), Some(120)); 1485 assert!(dissolution.all_redistributions_applied()); 1486 } 1487 1488 #[test] 1489 fn test_dissolution_insufficient_remaining_gids() { 1490 let dissolving_gid = make_test_gid_id(1); 1491 let doc_hash = make_test_treaty_hash(1); 1492 1493 // Only 1 remaining GID (below minimum of 2) 1494 let remaining = vec![(make_test_gid_id(2), 100_000)]; 1495 1496 let result = NationalDissolution::<TestnetV0>::propose( 1497 dissolving_gid, 1498 "Dissolved Nation".to_string(), 1499 DissolutionReason::FailedState { description: "Test".to_string() }, 1500 doc_hash, 1501 1_000_000, 1502 500_000, 1503 remaining, 1504 100, 1505 ); 1506 1507 assert!(result.is_err()); 1508 } 1509 1510 #[test] 1511 fn test_dissolution_expiry() { 1512 let dissolving_gid = make_test_gid_id(1); 1513 let doc_hash = make_test_treaty_hash(1); 1514 1515 let remaining = vec![(make_test_gid_id(2), 100_000), (make_test_gid_id(3), 100_000)]; 1516 1517 let mut dissolution = NationalDissolution::propose( 1518 dissolving_gid, 1519 "Dissolved Nation".to_string(), 1520 DissolutionReason::Voluntary { reason: "Test".to_string() }, 1521 doc_hash, 1522 1_000_000, 1523 500_000, 1524 remaining, 1525 100, 1526 ) 1527 .unwrap(); 1528 1529 // Check expiry after voting period 1530 dissolution.check_expiry(100 + DISSOLUTION_VOTING_PERIOD_EPOCHS); 1531 assert_eq!(dissolution.status(), DissolutionStatus::Expired); 1532 } 1533 1534 #[test] 1535 fn test_dissolution_reason_display() { 1536 let absorbed = DissolutionReason::Absorbed { absorbing_gid_name: "BigNation".to_string() }; 1537 assert_eq!(absorbed.to_string(), "absorbed_by:BigNation"); 1538 1539 let failed = DissolutionReason::FailedState { description: "test".to_string() }; 1540 assert_eq!(failed.to_string(), "failed_state"); 1541 1542 let voluntary = DissolutionReason::Voluntary { reason: "test".to_string() }; 1543 assert_eq!(voluntary.to_string(), "voluntary"); 1544 1545 let treaty = DissolutionReason::TreatyMandated { treaty_reference: "test".to_string() }; 1546 assert_eq!(treaty.to_string(), "treaty_mandated"); 1547 1548 let other = DissolutionReason::Other { description: "test".to_string() }; 1549 assert_eq!(other.to_string(), "other"); 1550 } 1551 1552 #[test] 1553 fn test_balance_redistribution() { 1554 let gid = make_test_gid_id(1); 1555 let mut redist = BalanceRedistribution::<TestnetV0>::new(gid.clone(), 100_000); 1556 1557 assert_eq!(redist.gid_id(), &gid); 1558 assert_eq!(redist.mint_limit(), 100_000); 1559 assert_eq!(redist.balance_share(), 0); 1560 assert_eq!(redist.mint_limit_share(), 0); 1561 assert!(!redist.is_applied()); 1562 1563 redist.mark_applied(); 1564 assert!(redist.is_applied()); 1565 } 1566 }