/ ledger / governor / src / restructuring.rs
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  }