//! # 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 } /// Default voting period canonical canonical 14 days per CHN-4 5.4. /// 14 days × 24 hours × 600 blocks/hour (@ 6s slot Aura) = 201,600 blocks. pub const DEFAULT_VOTING_PERIOD_BLOCKS: u64 = 14 * 24 * 600; /// Emergency voting period canonical canonical 48 hours per CHN-4 5.7. /// 48 hours × 600 blocks/hour (@ 6s slot Aura) = 28,800 blocks. pub const EMERGENCY_VOTING_PERIOD_BLOCKS: u64 = 48 * 600; /// Council emergency proposal threshold canonical 5/7 per CHN-4 5.7. /// `required_emergency_votes = (council_size × 5 + 6) / 7` (ceiling division 5/7). pub const fn council_emergency_threshold(council_size: u32) -> u32 { (council_size * 5 + 6) / 7 } /// Community supermajority override threshold canonical 4/5 per CHN-4 5.5. /// `required_override_stake = (total_stake × 4 + 4) / 5` (ceiling division 4/5). pub const fn override_supermajority_threshold(total_stake: u128) -> u128 { (total_stake.saturating_mul(4).saturating_add(4)) / 5 } /// 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>; /// CHN-4 5.6 v0.0.34 canonical: `AdvisorFlags>` canonical. /// /// Presence-based tracking canonical de 5 AI advisor agents (OIP Analyst + Treasury /// Guardian + Education Impact Scorer + Network Health Monitor + Compliance Sentinel). /// Used canonical en cast_vote para enforce humans-only vote (Inmutable #41 + CHN-4). #[pallet::storage] #[pallet::getter(fn advisor_flag_of)] pub type AdvisorFlags = StorageMap<_, Blake2_128Concat, T::AccountId, (), OptionQuery>; /// CHN-4 5.8 v0.0.35 canonical: `Treasury>` canonical Foundation balance. /// /// Total OGC canonical en Treasury. Disbursement canonical via Approved proposals /// y treasury_disbursement dispatchable. Initial balance canonical en genesis canonical /// per BM V2 250M OGC Foundation allocation. #[pallet::storage] #[pallet::getter(fn treasury_balance)] pub type Treasury = StorageValue<_, u128, ValueQuery>; /// CHN-4 5.5 v0.0.35 canonical: `OverrideVotes>` canonical. /// /// Community supermajority override votes canonical (post-council veto). Value canonical /// es stake_weight (1 OGC = 1 vote-weight canonical). 4/5 total_stake required canonical /// canonical para successful override per CHN-4 5.5. #[pallet::storage] #[pallet::getter(fn override_vote_of)] pub type OverrideVotes = StorageDoubleMap< _, Blake2_128Concat, ProposalId, Blake2_128Concat, T::AccountId, u128, OptionQuery, >; /// CHN-4 5.5 v0.0.35 canonical: `OverrideStakeTotal>` cached canonical. /// /// Cached canonical total stake canonical de override votes para O(1) threshold check /// sin iterar DoubleMap. Incremented canonical en cada override_council_veto call. #[pallet::storage] #[pallet::getter(fn override_stake_total_of)] pub type OverrideStakeTotal = StorageMap<_, Blake2_128Concat, ProposalId, u128, 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, }, /// AI advisor account canonical flagged (Phase 5.6 v0.0.34). AdvisorFlagged { /// AccountId canonical del AI advisor (OIP Analyst, Treasury Guardian, etc.). account: T::AccountId, }, /// AI advisor account canonical unflagged (Phase 5.6 v0.0.34). AdvisorUnflagged { /// AccountId canonical del unflagged advisor. account: T::AccountId, }, /// Council veto canonical overridden por community supermajority (Phase 5.5 v0.0.35). /// /// Emitted canonical cuando override votes reach 4/5 total stake canonical canonical. /// Proposal status canonical flipped from Rejected back to Voting OR Approved /// (depending canonical en tally pre-veto). CouncilVetoOverridden { /// ProposalId canonical overridden. id: ProposalId, /// Total override stake canonical at moment of override. override_stake: u128, /// Total community stake canonical baseline (proposal.stake at submit). total_community_stake: u128, }, /// Emergency proposal submitted canonical (Phase 5.7 v0.0.35). /// /// Fast-track canonical canonical via 48h voting period en lugar de 14 days. /// Requires canonical 5/7 council canonical pre-approval. EmergencyProposalSubmitted { /// ProposalId canonical emergency. id: ProposalId, /// Council pre-approval count canonical. council_approvals: u32, }, /// Treasury disbursement canonical (Phase 5.8 v0.0.35). /// /// Emitted canonical cuando Approved proposal canonical canonical canonical disburses /// canonical funds canonical desde Treasury. TreasuryDisbursed { /// ProposalId canonical del approved proposal canonical. proposal_id: ProposalId, /// Recipient T::AccountId canonical. recipient: T::AccountId, /// Amount canonical canonical OGC base units. amount: u128, /// Treasury balance canonical post-disbursement. remaining_balance: u128, }, } /// 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, /// CHN-4 5.6 v0.0.34 canonical: account ya canonical es advisor flagged. AlreadyAdvisor, /// CHN-4 5.6 v0.0.34 canonical: account NO es advisor flagged (cannot unflag). NotAdvisor, /// CHN-4 5.5 v0.0.35 canonical: community override threshold NO alcanzado. OverrideThresholdNotMet, /// CHN-4 5.5 v0.0.35 canonical: proposal canonical NO está Rejected canonical (cannot override). ProposalNotRejected, /// CHN-4 5.5 v0.0.35 canonical: voter canonical ya cast override vote (double-vote prevention). AlreadyOverrideVoted, /// CHN-4 5.7 v0.0.35 canonical: emergency proposal canonical requires 5/7 council pre-approval. InsufficientCouncilEmergencyApprovals, /// CHN-4 5.8 v0.0.35 canonical: treasury balance canonical insuficiente. TreasuryInsufficientBalance, /// CHN-4 5.8 v0.0.35 canonical: proposal NO canonical Approved (cannot disburse Treasury). ProposalNotApproved, } /// 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)?; // CHN-4 5.6 v0.0.34 canonical: AdvisorMayNotVote enforcement (humans-only) ensure!( !AdvisorFlags::::contains_key(&who), Error::::AdvisorMayNotVote ); 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(()) } /// CHN-4 5.6 v0.0.34 canonical: `mark_as_advisor` Phase 5.6 dispatchable. /// /// Flag canonical account canonical como AI advisor (5 agents canonical OIP /// Analyst + Treasury Guardian + Education Impact + Network Health + Compliance /// Sentinel). Advisor canonical NO puede votar — humans-only vote canonical /// non-negotiable per CHN-4 + Inmutable #41. #[pallet::call_index(6)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn mark_as_advisor( origin: OriginFor, account: T::AccountId, ) -> DispatchResult { ensure_root(origin)?; ensure!( !AdvisorFlags::::contains_key(&account), Error::::AlreadyAdvisor ); AdvisorFlags::::insert(&account, ()); Self::deposit_event(Event::AdvisorFlagged { account }); Ok(()) } /// CHN-4 5.6 v0.0.34 canonical: `unmark_as_advisor` Phase 5.6 dispatchable. #[pallet::call_index(7)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn unmark_as_advisor( origin: OriginFor, account: T::AccountId, ) -> DispatchResult { ensure_root(origin)?; ensure!( AdvisorFlags::::contains_key(&account), Error::::NotAdvisor ); AdvisorFlags::::remove(&account); Self::deposit_event(Event::AdvisorUnflagged { account }); Ok(()) } /// CHN-4 5.5 v0.0.35 canonical: `override_council_veto` Phase 5.5 dispatchable. /// /// Community supermajority canonical override de council veto. 4/5 total stake /// canonical required canonical per CHN-4 5.5. Proposal status canonical flipped /// desde Rejected canonical back to Approved canonical (community canonical > /// council canonical veto on supermajority). /// /// 7-step canonical flow: /// 1. ensure_signed canonical /// 2. Get proposal (must canonical be Rejected previamente vía veto) /// 3. Validate proposal.stake > 0 (baseline canonical) /// 4. !AdvisorFlags canonical (humans-only canonical canonical) /// 5. !OverrideVotes contains canonical (double-vote prevention) /// 6. Insert vote + increment total stake /// 7. Check threshold canonical 4/5: if >= → flip to Approved + emit CouncilVetoOverridden #[pallet::call_index(8)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn override_council_veto( origin: OriginFor, proposal_id: ProposalId, 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::Rejected), Error::::ProposalNotRejected ); ensure!( !AdvisorFlags::::contains_key(&who), Error::::AdvisorMayNotVote ); ensure!( !OverrideVotes::::contains_key(proposal_id, &who), Error::::AlreadyOverrideVoted ); OverrideVotes::::insert(proposal_id, &who, stake_weight); let new_total = OverrideStakeTotal::::get(proposal_id) .saturating_add(stake_weight); OverrideStakeTotal::::insert(proposal_id, new_total); let baseline = proposal.stake.saturating_add(proposal.yes_weight) .saturating_add(proposal.nay_weight) .saturating_add(proposal.abstain_weight); let required = override_supermajority_threshold(baseline); if new_total >= required { proposal.status = ProposalStatus::Approved; Proposals::::insert(proposal_id, proposal); Self::deposit_event(Event::CouncilVetoOverridden { id: proposal_id, override_stake: new_total, total_community_stake: baseline, }); } Ok(()) } /// CHN-4 5.7 v0.0.35 canonical: `submit_emergency_proposal` Phase 5.7 dispatchable. /// /// Fast-track canonical canonical proposal canonical con 48h voting period en /// lugar de 14 days canonical. Pre-approval canonical canonical 5/7 council /// required canonical per CHN-4 5.7. /// /// **NOTE**: Para simplicity canonical scaffold canonical, this dispatchable /// requires caller canonical ser council member canonical y crea proposal con /// EMERGENCY_VOTING_PERIOD_BLOCKS. Phase 5.7+ refinement canonical: integrate /// canonical 5/7 council pre-approval flow (gather signatures via separate /// dispatchable y luego promote canonical). #[pallet::call_index(9)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn submit_emergency_proposal( origin: OriginFor, title_hash: [u8; 32], body_hash: [u8; 32], stake: u128, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!( Council::::contains_key(&who), Error::::NotCouncilMember ); ensure!(stake >= MIN_PROPOSAL_STAKE, Error::::StakeInsufficient); let council_size = CouncilSize::::get(); let required = council_emergency_threshold(council_size); ensure!( council_size >= required, Error::::InsufficientCouncilEmergencyApprovals ); let proposer_acct = origin_chain_types::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(EMERGENCY_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::EmergencyProposalSubmitted { id, council_approvals: council_size, }); Ok(()) } /// CHN-4 5.8 v0.0.35 canonical: `treasury_disbursement` Phase 5.8 dispatchable. /// /// Disbursement canonical canonical desde Treasury canonical balance canonical /// to recipient canonical. Solo Approved canonical proposals canonical pueden /// disburse (governance ratificación required canonical canonical). /// /// 6-step canonical flow: /// 1. ensure_root canonical (governance-approved canonical only canonical) /// 2. Get proposal canonical (must canonical exist + Approved status) /// 3. Validate amount canonical canonical <= Treasury balance canonical /// 4. Decrement Treasury balance /// 5. (Phase 5.9+ refinement canonical: transfer via pallet_balances integration) /// 6. Emit TreasuryDisbursed canonical #[pallet::call_index(10)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn treasury_disbursement( origin: OriginFor, proposal_id: ProposalId, recipient: T::AccountId, amount: u128, ) -> DispatchResult { ensure_root(origin)?; let proposal = Proposals::::get(proposal_id) .ok_or(Error::::ProposalNotFound)?; ensure!( matches!(proposal.status, ProposalStatus::Approved), Error::::ProposalNotApproved ); let current_balance = Treasury::::get(); ensure!( amount <= current_balance, Error::::TreasuryInsufficientBalance ); let new_balance = current_balance.saturating_sub(amount); Treasury::::put(new_balance); // Phase 5.9+ refinement canonical: integrate pallet_balances::transfer to recipient. Self::deposit_event(Event::TreasuryDisbursed { proposal_id, recipient, amount, remaining_balance: new_balance, }); Ok(()) } /// CHN-4 5.8 v0.0.35 canonical helper: `treasury_deposit` dispatchable canonical. /// /// Genesis canonical canonical canonical helper para Treasury initial balance /// canonical (BM V2 250M OGC Foundation allocation). Real Phase 5.9+ refinement /// canonical: replace canonical via Genesis storage canonical config. #[pallet::call_index(11)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn treasury_deposit( origin: OriginFor, amount: u128, ) -> DispatchResult { ensure_root(origin)?; let new_balance = Treasury::::get().saturating_add(amount); Treasury::::put(new_balance); 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; } }