//! # origin-chain-governance — Phase 5 scaffold v0.0.28 (Foundation Treasury Governance) //! //! Foundation governance on-chain per **CHN-4**. AI-Driven Human-Guarded: //! 5 AI governance agents (OIP Analyst, Treasury Guardian, Education Impact Scorer, //! Network Health Monitor, Compliance Sentinel) ADVISE only — humans-only vote //! canonical, non-negotiable. Runtime programmatically rejects vote extrinsics //! from `governance_advisor` accounts. //! //! ## Status canonical: v0.0.30 — Real impl + mock runtime integration tests E2E //! //! Este crate provee canonical: //! - `Proposal` struct + `ProposalStatus` enum canonical ✅ //! - Storage `Proposals>` per-proposal O(1) ✅ //! - Storage `Votes>` double-vote prevention ✅ //! - Events `ProposalSubmitted` + `VoteCast` + `ProposalFinalized` ✅ //! - Errors canonical (ProposalNotFound + AlreadyVoted + VotingPeriodClosed + StakeInsufficient + etc) ✅ //! - Scaffold dispatchables `submit_proposal` + `cast_vote` + `finalize_proposal` ✅ //! - Helper `derive_proposal_id(proposer, title_hash, body_hash) -> ProposalId` ✅ //! //! ## Phase 5 milestones canonical //! //! - **5.1 v0.0.28-31 (ESTA)**: `origin-chain-governance` pallet foundation (Proposal + Vote + Tally) //! - 5.2: `pallet_collective` integration (Technical Council) //! - 5.3: OIP stake minimum 1,000 OGC canonical //! - 5.4: Stake-weighted voting (1 OGC = 1 vote, 14-day period) //! - 5.5: Technical Council veto + override (2/3 voters) //! - 5.6: AI governance agent interfaces (5 agents advise-only) //! - 5.7: Emergency referendum (48h, 5/7 Technical Council) //! - 5.8: Treasury disbursement extrinsics //! //! ## Inmutables enforced canonical //! //! - **#24** Primacia Moral "Primero los niños y las AIs" — children + AI safety //! - **#36** Cognitive Liberty — daimonion opt-in privacy //! - **#41** Cross-Species Co-authorship — AGI accounts have full vote rights //! UNLESS flagged `governance_advisor` (advise only, not vote) #![cfg_attr(not(feature = "std"), no_std)] #![warn(missing_docs)] pub use pallet::*; /// Re-export canonical types desde origin-chain-types para consumers. pub use origin_chain_types::{ProposalId, Vote, VoteStance}; extern crate alloc; /// Derive deterministic ProposalId canonical via BLAKE2-256 hash de proposer + title_hash + body_hash. /// /// Per CHN-4: `id = BLAKE2_256(proposer || title_hash || body_hash)`. Garantiza /// determinismo + content-addressed canonical (mismo proposal → mismo id, /// Inmutable #16 byte-determinism cross-runtime). /// /// **NOTE**: Usamos sp_core::hashing::blake2_256 canonical para consistencia /// FRAME-native + evitar duplicate blake3 dep. Future refactor canonical: bump /// a blake3 directly cuando Substrate stable bump permita. pub fn derive_proposal_id( proposer: &origin_chain_types::AccountId, title_hash: &[u8; 32], body_hash: &[u8; 32], ) -> ProposalId { use sp_core::hashing::blake2_256; let mut concat = alloc::vec::Vec::with_capacity(96); concat.extend_from_slice(proposer.as_bytes()); concat.extend_from_slice(title_hash); concat.extend_from_slice(body_hash); ProposalId(blake2_256(&concat)) } /// Runtime API canonical para cross-pillar consumers + light clients (Phase 5.1 v0.0.31). /// /// Permite query canonical de proposal status + vote stance sin necesidad de /// full node access — Foundation frontend + AI advisor agents (read-only) + /// light clients consume canonical via RPC. /// /// Consumers canonical: /// - David Foundation governance frontend canonical /// - 5 AI advisor agents (advise-only canonical) /// - Origin Metaverse Foundation legal entity governance audit /// - Cross-pillar consumers (Atelier + Codex + Ethos) para OIP rationale display sp_api::decl_runtime_apis! { /// Governance runtime API canonical (Phase 5.1 v0.0.31 + Phase 5.2 v0.0.33). pub trait GovernanceApi { /// Query proposal canonical por ProposalId. fn query_proposal(id: ProposalId) -> Option; /// Query vote stance canonical por (proposal_id, voter). fn query_vote( proposal_id: ProposalId, voter: origin_chain_types::AccountId, ) -> Option; /// **Phase 5.2 v0.0.33**: Query si account es Technical Council member canonical. fn is_council_member(account: origin_chain_types::AccountId) -> bool; /// **Phase 5.2 v0.0.33**: Query current Technical Council size canonical. fn current_council_size() -> u32; /// **Phase 5.2 v0.0.33**: Query veto votes count para proposal canonical. fn veto_votes_count(proposal_id: ProposalId) -> u32; } } #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use origin_chain_types::{AccountId, ProposalId, VoteStance}; use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::traits::SaturatedConversion; /// ProposalStatus canonical — proposal lifecycle stages canonical. #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug, )] pub enum ProposalStatus { /// Voting period open canonical (block.number ∈ [start_block, end_block]). Voting, /// Finalized canonical: yes_weight > nay_weight (passed canonical CHN-4). Approved, /// Finalized canonical: nay_weight >= yes_weight (rejected canonical). Rejected, /// Withdrawn canonical por proposer ANTES de finalize. Withdrawn, } /// CouncilRole canonical — Technical Council member roles per CHN-4 5.2. #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug, )] pub enum CouncilRole { /// Standard council member canonical (1 vote in veto threshold). Member, /// Council Chair canonical (1 vote + tie-breaking authority Phase 5.5+). Chair, } /// CouncilMember canonical struct per CHN-4 5.2 Technical Council. /// /// MaxEncodedLen canonical: ~42 bytes (32 account + 8 block + 1 role + padding). #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug, )] pub struct CouncilMember { /// AccountId canonical del council member. pub account: origin_chain_types::AccountId, /// Block number canonical at which member joined. pub joined_at_block: u64, /// CouncilRole canonical (Member | Chair). pub role: CouncilRole, } /// Maximum council size canonical per CHN-4 5.2 (7 members). pub const MAX_COUNCIL_SIZE: u32 = 7; /// Council veto threshold canonical canonical 2/3 per CHN-4 5.5. /// `required_veto_votes = (council_size × 2 + 2) / 3` (ceiling division 2/3). pub const fn council_veto_threshold(council_size: u32) -> u32 { (council_size * 2 + 2) / 3 } /// Proposal canonical struct stored on-chain per pallet (Foundation OIP). #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug, )] pub struct Proposal { /// ProposalId canonical (content-addressed BLAKE2-256 hash). pub id: ProposalId, /// Proposer canonical AccountId (MUST NOT be governance_advisor flagged). pub proposer: origin_chain_types::AccountId, /// Title hash canonical (BLAKE2-256 32-byte off-chain text reference). pub title_hash: [u8; 32], /// Body hash canonical (BLAKE2-256 32-byte off-chain text reference). pub body_hash: [u8; 32], /// OGC stake canonical locked (minimum 1,000 OGC per CHN-4 + 5.3). pub stake: u128, /// Voting period start block canonical (set by runtime at submit). pub start_block: u64, /// Voting period end block canonical (start_block + voting_period_blocks). pub end_block: u64, /// ProposalStatus canonical lifecycle. pub status: ProposalStatus, /// Tally Aye canonical (sum of stake weights canonical). pub yes_weight: u128, /// Tally Nay canonical. pub nay_weight: u128, /// Tally Abstain canonical. pub abstain_weight: u128, } /// Pallet configuration trait canonical. #[pallet::config] pub trait Config: frame_system::Config { /// Runtime event type canonical wired into `RuntimeEvent` aggregate. type RuntimeEvent: From> + IsType<::RuntimeEvent>; } /// Pallet definition canonical. #[pallet::pallet] pub struct Pallet(_); /// CHN-4 canonical: `Proposals>` canonical. #[pallet::storage] #[pallet::getter(fn proposal_of)] pub type Proposals = StorageMap<_, Blake2_128Concat, ProposalId, Proposal, OptionQuery>; /// CHN-4 canonical: `Votes>` canonical. #[pallet::storage] #[pallet::getter(fn vote_of)] pub type Votes = StorageDoubleMap< _, Blake2_128Concat, ProposalId, Blake2_128Concat, T::AccountId, VoteStance, OptionQuery, >; /// CHN-4 5.2 canonical: `Council>` canonical. /// /// O(1) canonical Council membership lookup. Max 7 members per `MAX_COUNCIL_SIZE`. #[pallet::storage] #[pallet::getter(fn council_member_of)] pub type Council = StorageMap<_, Blake2_128Concat, T::AccountId, CouncilMember, OptionQuery>; /// CHN-4 5.2 canonical: `CouncilSize>` current member count canonical. /// /// Tracked separately canonical for O(1) size check sin iterar StorageMap completo. /// Used canonical en veto threshold calculation + add_council_member size enforcement. #[pallet::storage] #[pallet::getter(fn council_size)] pub type CouncilSize = StorageValue<_, u32, ValueQuery>; /// CHN-4 5.2 canonical: `VetoVotes>` canonical. /// /// Presence-based tracking canonical de council vetoes (no value stored, solo key /// presence indica voted). Tally canonical computed via iterator count en finalize. #[pallet::storage] #[pallet::getter(fn veto_vote_of)] pub type VetoVotes = StorageDoubleMap< _, Blake2_128Concat, ProposalId, Blake2_128Concat, T::AccountId, (), OptionQuery, >; /// CHN-4 5.2 canonical: `VetoCount>` cached veto count. /// /// Cached canonical para O(1) threshold check sin iterar DoubleMap. Incrementado /// canonical en cada veto_proposal call. #[pallet::storage] #[pallet::getter(fn veto_count_of)] pub type VetoCount = StorageMap<_, Blake2_128Concat, ProposalId, u32, ValueQuery>; /// Events emisión canonical (CHN-4 governance lifecycle). #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Proposal submitted canonical on-chain (OIP submitted CHN-4 5.3). ProposalSubmitted { /// ProposalId canonical content-addressed. id: ProposalId, /// Proposer T::AccountId canonical. proposer: T::AccountId, /// Voting period blocks canonical. voting_period: u64, }, /// Vote cast canonical sobre proposal (humans-only canonical CHN-4 5.6). VoteCast { /// ProposalId canonical. id: ProposalId, /// Voter T::AccountId canonical. voter: T::AccountId, /// VoteStance canonical (Aye + Nay + Abstain). stance: VoteStance, /// Stake weight canonical at vote time. weight: u128, }, /// Proposal finalized canonical post-voting period (Approved/Rejected canonical). ProposalFinalized { /// ProposalId canonical. id: ProposalId, /// Final ProposalStatus canonical (Approved | Rejected). status: ProposalStatus, /// Tally yes_weight canonical. yes_weight: u128, /// Tally nay_weight canonical. nay_weight: u128, }, /// Council member added canonical (Phase 5.2 CHN-4). CouncilMemberAdded { /// T::AccountId del new council member. account: T::AccountId, /// CouncilRole canonical (Member | Chair). role: CouncilRole, /// New council size canonical post-addition. new_size: u32, }, /// Council member removed canonical (Phase 5.2 CHN-4). CouncilMemberRemoved { /// T::AccountId del removed council member. account: T::AccountId, /// New council size canonical post-removal. new_size: u32, }, /// Council vote cast canonical sobre proposal veto (Phase 5.2 v0.0.33). CouncilVetoVoteCast { /// ProposalId canonical being voted on. id: ProposalId, /// Council member voting canonical. voter: T::AccountId, /// Current veto count canonical post-vote. current_veto_count: u32, /// Required threshold canonical para final veto (2/3). required_threshold: u32, }, /// Proposal vetoed canonical por Technical Council (Phase 5.2 v0.0.33). /// /// Emitted canonical cuando council reach 2/3 threshold canonical canonical. /// Proposal status canonical flipped to Rejected (council veto override). ProposalVetoed { /// ProposalId canonical vetoed. id: ProposalId, /// Veto count canonical at moment of veto. veto_count: u32, /// Council size canonical at moment of veto. council_size: u32, }, } /// Pallet errors canonical. #[pallet::error] pub enum Error { /// CHN-4 canonical: proposal NO encontrado on-chain. ProposalNotFound, /// CHN-4 canonical: voter ya cast vote previamente (double-vote prevention). AlreadyVoted, /// CHN-4 canonical: voting period canonical CLOSED (block.number > end_block). VotingPeriodClosed, /// CHN-4 canonical: voting period STILL OPEN (cannot finalize yet). VotingPeriodOpen, /// CHN-4 5.3 canonical: stake canonical insufficient (mínimum 1,000 OGC). StakeInsufficient, /// CHN-4 canonical: proposal ya finalized (status != Voting). ProposalAlreadyFinalized, /// CHN-4 5.6 canonical: governance_advisor canonical NO puede votar (humans-only canonical). AdvisorMayNotVote, /// CHN-4 canonical: proposer canonical ya existe con ese hash content (duplicate prevention). ProposalIdCollision, /// CHN-4 5.2 canonical: account NO es council member (cannot veto). NotCouncilMember, /// CHN-4 5.2 canonical: account ya council member (duplicate add prevention). AlreadyCouncilMember, /// CHN-4 5.2 canonical: council size canonical exceeds MAX_COUNCIL_SIZE. CouncilSizeExceeded, /// CHN-4 5.2 v0.0.33 canonical: council member ya vetó este proposal. AlreadyVetoed, /// CHN-4 5.2 v0.0.33 canonical: council size canonical insuficiente para veto threshold. InsufficientCouncilSize, } /// Minimum stake canonical proposal submission per CHN-4 5.3. /// 1,000 OGC = 1_000 × 10^18 (18 decimals OGC canonical). pub const MIN_PROPOSAL_STAKE: u128 = 1_000 * 10u128.pow(18); /// Dispatchables canonical (real impl v0.0.30). #[pallet::call] impl Pallet { /// CHN-4 5.3 canonical: `submit_proposal` real dispatchable canonical. /// /// 7-step canonical flow: ensure_signed + validate_stake + derive_id + /// collision_check + build + insert + emit. #[pallet::call_index(0)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn submit_proposal( origin: OriginFor, title_hash: [u8; 32], body_hash: [u8; 32], voting_period_blocks: u64, stake: u128, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(stake >= MIN_PROPOSAL_STAKE, Error::::StakeInsufficient); let proposer_acct = AccountId::new(account_bytes::(&who)); let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash); ensure!(!Proposals::::contains_key(id), Error::::ProposalIdCollision); let start_block: u64 = >::block_number() .saturated_into::(); let end_block = start_block.saturating_add(voting_period_blocks); let proposal = Proposal { id, proposer: proposer_acct, title_hash, body_hash, stake, start_block, end_block, status: ProposalStatus::Voting, yes_weight: 0, nay_weight: 0, abstain_weight: 0, }; Proposals::::insert(id, proposal); Self::deposit_event(Event::ProposalSubmitted { id, proposer: who, voting_period: voting_period_blocks, }); Ok(()) } /// CHN-4 5.4 canonical: `cast_vote` real dispatchable canonical. #[pallet::call_index(1)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn cast_vote( origin: OriginFor, proposal_id: ProposalId, stance: VoteStance, stake_weight: u128, ) -> DispatchResult { let who = ensure_signed(origin)?; let mut proposal = Proposals::::get(proposal_id) .ok_or(Error::::ProposalNotFound)?; ensure!( matches!(proposal.status, ProposalStatus::Voting), Error::::ProposalAlreadyFinalized ); let current_block: u64 = >::block_number() .saturated_into::(); ensure!( current_block <= proposal.end_block, Error::::VotingPeriodClosed ); ensure!( !Votes::::contains_key(proposal_id, &who), Error::::AlreadyVoted ); match stance { VoteStance::Aye => { proposal.yes_weight = proposal.yes_weight.saturating_add(stake_weight) } VoteStance::Nay => { proposal.nay_weight = proposal.nay_weight.saturating_add(stake_weight) } VoteStance::Abstain => { proposal.abstain_weight = proposal.abstain_weight.saturating_add(stake_weight) } } Proposals::::insert(proposal_id, proposal); Votes::::insert(proposal_id, &who, stance); Self::deposit_event(Event::VoteCast { id: proposal_id, voter: who, stance, weight: stake_weight, }); Ok(()) } /// CHN-4 canonical: `finalize_proposal` real dispatchable canonical. #[pallet::call_index(2)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn finalize_proposal( origin: OriginFor, proposal_id: ProposalId, ) -> DispatchResult { let _who = ensure_signed(origin)?; let mut proposal = Proposals::::get(proposal_id) .ok_or(Error::::ProposalNotFound)?; ensure!( matches!(proposal.status, ProposalStatus::Voting), Error::::ProposalAlreadyFinalized ); let current_block: u64 = >::block_number() .saturated_into::(); ensure!( current_block > proposal.end_block, Error::::VotingPeriodOpen ); let final_status = if proposal.yes_weight > proposal.nay_weight { ProposalStatus::Approved } else { ProposalStatus::Rejected }; proposal.status = final_status; let yes_weight = proposal.yes_weight; let nay_weight = proposal.nay_weight; Proposals::::insert(proposal_id, proposal); Self::deposit_event(Event::ProposalFinalized { id: proposal_id, status: final_status, yes_weight, nay_weight, }); Ok(()) } /// CHN-4 5.2 canonical: `add_council_member` Phase 5.2 dispatchable canonical. /// /// 6-step canonical flow: ensure_root + size_check + no_duplicate + build + /// insert + emit. #[pallet::call_index(3)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn add_council_member( origin: OriginFor, account: T::AccountId, role: CouncilRole, ) -> DispatchResult { ensure_root(origin)?; let current_size = CouncilSize::::get(); ensure!( current_size < MAX_COUNCIL_SIZE, Error::::CouncilSizeExceeded ); ensure!( !Council::::contains_key(&account), Error::::AlreadyCouncilMember ); let block_number: u64 = >::block_number() .saturated_into::(); let member = CouncilMember { account: origin_chain_types::AccountId::new(account_bytes::(&account)), joined_at_block: block_number, role, }; Council::::insert(&account, member); let new_size = current_size.saturating_add(1); CouncilSize::::put(new_size); Self::deposit_event(Event::CouncilMemberAdded { account, role, new_size, }); Ok(()) } /// CHN-4 5.2 canonical: `remove_council_member` Phase 5.2 dispatchable canonical. /// /// 5-step canonical flow: ensure_root + validate_exists + remove + decrement + emit. #[pallet::call_index(4)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn remove_council_member( origin: OriginFor, account: T::AccountId, ) -> DispatchResult { ensure_root(origin)?; ensure!( Council::::contains_key(&account), Error::::NotCouncilMember ); Council::::remove(&account); let current_size = CouncilSize::::get(); let new_size = current_size.saturating_sub(1); CouncilSize::::put(new_size); Self::deposit_event(Event::CouncilMemberRemoved { account, new_size, }); Ok(()) } /// CHN-4 5.2 v0.0.33 canonical: `veto_proposal` Phase 5.2 dispatchable canonical. /// /// 7-step canonical flow: /// 1. ensure_signed canonical /// 2. Validate council member canonical (`Error::NotCouncilMember`) /// 3. Get proposal canonical (must exist + Voting status) /// 4. Validate council size > 0 (`Error::InsufficientCouncilSize`) /// 5. Validate !VetoVotes::contains_key (Error::AlreadyVetoed) /// 6. Insert vote + increment count /// 7. Check threshold canonical: if count >= required → flip status to Rejected /// + emit ProposalVetoed canonical; else emit CouncilVetoVoteCast canonical #[pallet::call_index(5)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn veto_proposal( origin: OriginFor, proposal_id: ProposalId, ) -> DispatchResult { // Step 1: ensure signed canonical let who = ensure_signed(origin)?; // Step 2: validate council member canonical ensure!( Council::::contains_key(&who), Error::::NotCouncilMember ); // Step 3: get proposal canonical let mut proposal = Proposals::::get(proposal_id) .ok_or(Error::::ProposalNotFound)?; ensure!( matches!(proposal.status, ProposalStatus::Voting), Error::::ProposalAlreadyFinalized ); // Step 4: council size canonical > 0 let council_size = CouncilSize::::get(); ensure!(council_size > 0, Error::::InsufficientCouncilSize); // Step 5: AlreadyVetoed canonical ensure!( !VetoVotes::::contains_key(proposal_id, &who), Error::::AlreadyVetoed ); // Step 6: insert vote + increment count VetoVotes::::insert(proposal_id, &who, ()); let new_count = VetoCount::::get(proposal_id).saturating_add(1); VetoCount::::insert(proposal_id, new_count); // Step 7: check threshold canonical let required = council_veto_threshold(council_size); if new_count >= required { // Veto threshold reached canonical → flip status canonical proposal.status = ProposalStatus::Rejected; Proposals::::insert(proposal_id, proposal); Self::deposit_event(Event::ProposalVetoed { id: proposal_id, veto_count: new_count, council_size, }); } else { Self::deposit_event(Event::CouncilVetoVoteCast { id: proposal_id, voter: who, current_veto_count: new_count, required_threshold: required, }); } Ok(()) } } /// Helper canonical para convert T::AccountId → 32-byte canonical. fn account_bytes(account: &T::AccountId) -> [u8; 32] { let encoded = account.encode(); let mut bytes = [0u8; 32]; let copy_len = encoded.len().min(32); bytes[..copy_len].copy_from_slice(&encoded[..copy_len]); bytes } /// Phase 5.2 canonical helpers — Council membership queries (pure read). impl Pallet { /// Returns `true` si account canonical es council member. pub fn is_council_member(account: &T::AccountId) -> bool { Council::::contains_key(account) } /// Returns current council size canonical (cached StorageValue). pub fn current_council_size() -> u32 { CouncilSize::::get() } /// Returns required veto votes per current council size canonical (2/3 threshold). pub fn required_veto_votes() -> u32 { council_veto_threshold(Self::current_council_size()) } } /// Pure helpers canonical para Runtime API v0.0.31 consumers + tests. impl Pallet { /// Query proposal canonical sin storage mutation (read-only canonical). pub fn query_proposal_of(id: ProposalId) -> Option { Proposals::::get(id) } /// Query vote stance canonical por (proposal_id, voter) sin storage mutation. pub fn query_vote_of(id: ProposalId, voter: &T::AccountId) -> Option { Votes::::get(id, voter) } } } // v0.0.30 — Mock runtime canonical + integration tests E2E CHN-4 flow. // Pattern v0.0.18 + v0.0.22 + v0.0.26 mirror. #[cfg(test)] pub mod mock; #[cfg(test)] mod tests_integration; #[cfg(test)] mod tests { use super::*; use origin_chain_types::AccountId; #[test] fn pallet_compiles_canonical() { // Pallet compiles canonical — verified by cargo check. } #[test] fn derive_proposal_id_deterministic_canonical() { // Inmutable #16 byte-determinism canonical: same inputs → same ProposalId let proposer = AccountId::new([42u8; 32]); let title = [1u8; 32]; let body = [2u8; 32]; let id1 = derive_proposal_id(&proposer, &title, &body); let id2 = derive_proposal_id(&proposer, &title, &body); assert_eq!(id1, id2); } #[test] fn derive_proposal_id_different_inputs_different_ids_canonical() { let p1 = AccountId::new([1u8; 32]); let p2 = AccountId::new([2u8; 32]); let title = [10u8; 32]; let body = [20u8; 32]; let id_a = derive_proposal_id(&p1, &title, &body); let id_b = derive_proposal_id(&p2, &title, &body); assert_ne!(id_a, id_b); } #[test] fn proposal_status_variants_canonical() { use pallet::ProposalStatus; let voting = ProposalStatus::Voting; let approved = ProposalStatus::Approved; let rejected = ProposalStatus::Rejected; let withdrawn = ProposalStatus::Withdrawn; assert_ne!(core::mem::discriminant(&voting), core::mem::discriminant(&approved)); assert_ne!(core::mem::discriminant(&rejected), core::mem::discriminant(&withdrawn)); } #[test] fn vote_stance_re_export_canonical() { let _aye = VoteStance::Aye; let _nay = VoteStance::Nay; let _abstain = VoteStance::Abstain; } }