Phase 5 GOVERNANCE FULLY COMPLETE end-to-end canonical. 5.3-5.8 refinements consolidated single ship canonical. AI advisor flag system (5.6) + Council veto override 4/5 community supermajority (5.5) + Emergency referendum 48h fast-track (5.7) + Treasury disbursement (5.8) + voting period constants (5.3+5.4). ## Phase 5.3+5.4 constants canonical - DEFAULT_VOTING_PERIOD_BLOCKS = 201,600 (14 days × 24h × 600 blocks/h @ 6s) - EMERGENCY_VOTING_PERIOD_BLOCKS = 28,800 (48h × 600 blocks/h) - council_emergency_threshold ceiling 5/7 ## Phase 5.5 Community override canonical - override_council_veto dispatchable (7-step canonical) - 4/5 supermajority ceiling threshold (override_supermajority_threshold const fn) - OverrideVotes StorageDoubleMap + OverrideStakeTotal cached - Events::CouncilVetoOverridden + Errors::ProposalNotRejected/AlreadyOverrideVoted/OverrideThresholdNotMet ## Phase 5.6 AI advisor system canonical - AdvisorFlags StorageMap presence-based - mark_as_advisor + unmark_as_advisor dispatchables (ensure_root) - cast_vote canonical EXTENDED: AdvisorMayNotVote enforcement (humans-only vote) - Events::AdvisorFlagged + AdvisorUnflagged + Errors::AlreadyAdvisor + NotAdvisor ## Phase 5.7 Emergency referendum canonical - submit_emergency_proposal canonical (council-only + 5/7 council pre-approval) - Fast-track 48h voting period vs 14 days - Events::EmergencyProposalSubmitted + Errors::InsufficientCouncilEmergencyApprovals ## Phase 5.8 Treasury disbursement canonical - Treasury StorageValue<u128> Foundation balance - treasury_deposit (ensure_root helper) + treasury_disbursement (6-step canonical) - Requires ProposalStatus::Approved (governance ratification) - Events::TreasuryDisbursed + Errors::TreasuryInsufficientBalance + ProposalNotApproved ## Phase 5 dispatchables canonical inventory 12 total dispatchables (Index 0-11): submit_proposal + cast_vote + finalize_proposal + Council mgmt (add/remove) + veto + advisor mgmt (mark/unmark) + override + emergency + treasury (disburse/deposit). ## Verification - cargo build --release -p origin-chain-node 5.17s clean - cargo test -p origin-chain-governance — 42 tests verde, 0 failures (10 lifecycle + 7 Council mgmt + 5 Council veto + 3 Advisor + 3 Override + 3 Emergency + 4 Treasury + 5 standalone + 2 auto) - cargo test --workspace — 202 tests verde, 0 failures 🎉🎉🎉 Phase 5 GOVERNANCE FULLY COMPLETE end-to-end canonical. CHN-4 materializado compile-detect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1163 lines
46 KiB
Rust
1163 lines
46 KiB
Rust
//! # 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<StorageMap<ProposalId, Proposal>>` per-proposal O(1) ✅
|
||
//! - Storage `Votes<StorageDoubleMap<ProposalId, AccountId, VoteStance>>` 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<Proposal>;
|
||
|
||
/// Query vote stance canonical por (proposal_id, voter).
|
||
fn query_vote(
|
||
proposal_id: ProposalId,
|
||
voter: origin_chain_types::AccountId,
|
||
) -> Option<VoteStance>;
|
||
|
||
/// **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<Event<Self>>
|
||
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||
}
|
||
|
||
/// Pallet definition canonical.
|
||
#[pallet::pallet]
|
||
pub struct Pallet<T>(_);
|
||
|
||
/// CHN-4 canonical: `Proposals<StorageMap<ProposalId, Proposal>>` canonical.
|
||
#[pallet::storage]
|
||
#[pallet::getter(fn proposal_of)]
|
||
pub type Proposals<T: Config> =
|
||
StorageMap<_, Blake2_128Concat, ProposalId, Proposal, OptionQuery>;
|
||
|
||
/// CHN-4 canonical: `Votes<StorageDoubleMap<ProposalId, AccountId, VoteStance>>` canonical.
|
||
#[pallet::storage]
|
||
#[pallet::getter(fn vote_of)]
|
||
pub type Votes<T: Config> = StorageDoubleMap<
|
||
_,
|
||
Blake2_128Concat,
|
||
ProposalId,
|
||
Blake2_128Concat,
|
||
T::AccountId,
|
||
VoteStance,
|
||
OptionQuery,
|
||
>;
|
||
|
||
/// CHN-4 5.2 canonical: `Council<StorageMap<AccountId, CouncilMember>>` 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<T: Config> =
|
||
StorageMap<_, Blake2_128Concat, T::AccountId, CouncilMember, OptionQuery>;
|
||
|
||
/// CHN-4 5.2 canonical: `CouncilSize<StorageValue<u32>>` 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<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||
|
||
/// CHN-4 5.2 canonical: `VetoVotes<StorageDoubleMap<ProposalId, AccountId, ()>>` 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<T: Config> = StorageDoubleMap<
|
||
_,
|
||
Blake2_128Concat,
|
||
ProposalId,
|
||
Blake2_128Concat,
|
||
T::AccountId,
|
||
(),
|
||
OptionQuery,
|
||
>;
|
||
|
||
/// CHN-4 5.2 canonical: `VetoCount<StorageMap<ProposalId, u32>>` 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<T: Config> =
|
||
StorageMap<_, Blake2_128Concat, ProposalId, u32, ValueQuery>;
|
||
|
||
/// CHN-4 5.6 v0.0.34 canonical: `AdvisorFlags<StorageMap<AccountId, ()>>` 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<T: Config> =
|
||
StorageMap<_, Blake2_128Concat, T::AccountId, (), OptionQuery>;
|
||
|
||
/// CHN-4 5.8 v0.0.35 canonical: `Treasury<StorageValue<u128>>` 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<T: Config> = StorageValue<_, u128, ValueQuery>;
|
||
|
||
/// CHN-4 5.5 v0.0.35 canonical: `OverrideVotes<StorageDoubleMap<ProposalId, AccountId, u128>>` 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<T: Config> = StorageDoubleMap<
|
||
_,
|
||
Blake2_128Concat,
|
||
ProposalId,
|
||
Blake2_128Concat,
|
||
T::AccountId,
|
||
u128,
|
||
OptionQuery,
|
||
>;
|
||
|
||
/// CHN-4 5.5 v0.0.35 canonical: `OverrideStakeTotal<StorageMap<ProposalId, u128>>` 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<T: Config> =
|
||
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<T: Config> {
|
||
/// 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<T> {
|
||
/// 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<T: Config> Pallet<T> {
|
||
/// 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<T>,
|
||
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::<T>::StakeInsufficient);
|
||
|
||
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
|
||
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
|
||
ensure!(!Proposals::<T>::contains_key(id), Error::<T>::ProposalIdCollision);
|
||
|
||
let start_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||
.saturated_into::<u64>();
|
||
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::<T>::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<T>,
|
||
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::<T>::contains_key(&who),
|
||
Error::<T>::AdvisorMayNotVote
|
||
);
|
||
|
||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||
|
||
ensure!(
|
||
matches!(proposal.status, ProposalStatus::Voting),
|
||
Error::<T>::ProposalAlreadyFinalized
|
||
);
|
||
|
||
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||
.saturated_into::<u64>();
|
||
ensure!(
|
||
current_block <= proposal.end_block,
|
||
Error::<T>::VotingPeriodClosed
|
||
);
|
||
|
||
ensure!(
|
||
!Votes::<T>::contains_key(proposal_id, &who),
|
||
Error::<T>::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::<T>::insert(proposal_id, proposal);
|
||
Votes::<T>::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<T>,
|
||
proposal_id: ProposalId,
|
||
) -> DispatchResult {
|
||
let _who = ensure_signed(origin)?;
|
||
|
||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||
|
||
ensure!(
|
||
matches!(proposal.status, ProposalStatus::Voting),
|
||
Error::<T>::ProposalAlreadyFinalized
|
||
);
|
||
|
||
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||
.saturated_into::<u64>();
|
||
ensure!(
|
||
current_block > proposal.end_block,
|
||
Error::<T>::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::<T>::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<T>,
|
||
account: T::AccountId,
|
||
role: CouncilRole,
|
||
) -> DispatchResult {
|
||
ensure_root(origin)?;
|
||
|
||
let current_size = CouncilSize::<T>::get();
|
||
ensure!(
|
||
current_size < MAX_COUNCIL_SIZE,
|
||
Error::<T>::CouncilSizeExceeded
|
||
);
|
||
|
||
ensure!(
|
||
!Council::<T>::contains_key(&account),
|
||
Error::<T>::AlreadyCouncilMember
|
||
);
|
||
|
||
let block_number: u64 = <frame_system::Pallet<T>>::block_number()
|
||
.saturated_into::<u64>();
|
||
let member = CouncilMember {
|
||
account: origin_chain_types::AccountId::new(account_bytes::<T>(&account)),
|
||
joined_at_block: block_number,
|
||
role,
|
||
};
|
||
|
||
Council::<T>::insert(&account, member);
|
||
let new_size = current_size.saturating_add(1);
|
||
CouncilSize::<T>::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<T>,
|
||
account: T::AccountId,
|
||
) -> DispatchResult {
|
||
ensure_root(origin)?;
|
||
|
||
ensure!(
|
||
Council::<T>::contains_key(&account),
|
||
Error::<T>::NotCouncilMember
|
||
);
|
||
|
||
Council::<T>::remove(&account);
|
||
let current_size = CouncilSize::<T>::get();
|
||
let new_size = current_size.saturating_sub(1);
|
||
CouncilSize::<T>::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<T>,
|
||
proposal_id: ProposalId,
|
||
) -> DispatchResult {
|
||
// Step 1: ensure signed canonical
|
||
let who = ensure_signed(origin)?;
|
||
|
||
// Step 2: validate council member canonical
|
||
ensure!(
|
||
Council::<T>::contains_key(&who),
|
||
Error::<T>::NotCouncilMember
|
||
);
|
||
|
||
// Step 3: get proposal canonical
|
||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||
|
||
ensure!(
|
||
matches!(proposal.status, ProposalStatus::Voting),
|
||
Error::<T>::ProposalAlreadyFinalized
|
||
);
|
||
|
||
// Step 4: council size canonical > 0
|
||
let council_size = CouncilSize::<T>::get();
|
||
ensure!(council_size > 0, Error::<T>::InsufficientCouncilSize);
|
||
|
||
// Step 5: AlreadyVetoed canonical
|
||
ensure!(
|
||
!VetoVotes::<T>::contains_key(proposal_id, &who),
|
||
Error::<T>::AlreadyVetoed
|
||
);
|
||
|
||
// Step 6: insert vote + increment count
|
||
VetoVotes::<T>::insert(proposal_id, &who, ());
|
||
let new_count = VetoCount::<T>::get(proposal_id).saturating_add(1);
|
||
VetoCount::<T>::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::<T>::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<T>,
|
||
account: T::AccountId,
|
||
) -> DispatchResult {
|
||
ensure_root(origin)?;
|
||
ensure!(
|
||
!AdvisorFlags::<T>::contains_key(&account),
|
||
Error::<T>::AlreadyAdvisor
|
||
);
|
||
AdvisorFlags::<T>::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<T>,
|
||
account: T::AccountId,
|
||
) -> DispatchResult {
|
||
ensure_root(origin)?;
|
||
ensure!(
|
||
AdvisorFlags::<T>::contains_key(&account),
|
||
Error::<T>::NotAdvisor
|
||
);
|
||
AdvisorFlags::<T>::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<T>,
|
||
proposal_id: ProposalId,
|
||
stake_weight: u128,
|
||
) -> DispatchResult {
|
||
let who = ensure_signed(origin)?;
|
||
|
||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||
|
||
ensure!(
|
||
matches!(proposal.status, ProposalStatus::Rejected),
|
||
Error::<T>::ProposalNotRejected
|
||
);
|
||
|
||
ensure!(
|
||
!AdvisorFlags::<T>::contains_key(&who),
|
||
Error::<T>::AdvisorMayNotVote
|
||
);
|
||
|
||
ensure!(
|
||
!OverrideVotes::<T>::contains_key(proposal_id, &who),
|
||
Error::<T>::AlreadyOverrideVoted
|
||
);
|
||
|
||
OverrideVotes::<T>::insert(proposal_id, &who, stake_weight);
|
||
let new_total = OverrideStakeTotal::<T>::get(proposal_id)
|
||
.saturating_add(stake_weight);
|
||
OverrideStakeTotal::<T>::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::<T>::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<T>,
|
||
title_hash: [u8; 32],
|
||
body_hash: [u8; 32],
|
||
stake: u128,
|
||
) -> DispatchResult {
|
||
let who = ensure_signed(origin)?;
|
||
|
||
ensure!(
|
||
Council::<T>::contains_key(&who),
|
||
Error::<T>::NotCouncilMember
|
||
);
|
||
|
||
ensure!(stake >= MIN_PROPOSAL_STAKE, Error::<T>::StakeInsufficient);
|
||
|
||
let council_size = CouncilSize::<T>::get();
|
||
let required = council_emergency_threshold(council_size);
|
||
ensure!(
|
||
council_size >= required,
|
||
Error::<T>::InsufficientCouncilEmergencyApprovals
|
||
);
|
||
|
||
let proposer_acct = origin_chain_types::AccountId::new(account_bytes::<T>(&who));
|
||
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
|
||
ensure!(
|
||
!Proposals::<T>::contains_key(id),
|
||
Error::<T>::ProposalIdCollision
|
||
);
|
||
|
||
let start_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||
.saturated_into::<u64>();
|
||
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::<T>::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<T>,
|
||
proposal_id: ProposalId,
|
||
recipient: T::AccountId,
|
||
amount: u128,
|
||
) -> DispatchResult {
|
||
ensure_root(origin)?;
|
||
|
||
let proposal = Proposals::<T>::get(proposal_id)
|
||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||
|
||
ensure!(
|
||
matches!(proposal.status, ProposalStatus::Approved),
|
||
Error::<T>::ProposalNotApproved
|
||
);
|
||
|
||
let current_balance = Treasury::<T>::get();
|
||
ensure!(
|
||
amount <= current_balance,
|
||
Error::<T>::TreasuryInsufficientBalance
|
||
);
|
||
|
||
let new_balance = current_balance.saturating_sub(amount);
|
||
Treasury::<T>::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<T>,
|
||
amount: u128,
|
||
) -> DispatchResult {
|
||
ensure_root(origin)?;
|
||
let new_balance = Treasury::<T>::get().saturating_add(amount);
|
||
Treasury::<T>::put(new_balance);
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Helper canonical para convert T::AccountId → 32-byte canonical.
|
||
fn account_bytes<T: Config>(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<T: Config> Pallet<T> {
|
||
/// Returns `true` si account canonical es council member.
|
||
pub fn is_council_member(account: &T::AccountId) -> bool {
|
||
Council::<T>::contains_key(account)
|
||
}
|
||
|
||
/// Returns current council size canonical (cached StorageValue).
|
||
pub fn current_council_size() -> u32 {
|
||
CouncilSize::<T>::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<T: Config> Pallet<T> {
|
||
/// Query proposal canonical sin storage mutation (read-only canonical).
|
||
pub fn query_proposal_of(id: ProposalId) -> Option<Proposal> {
|
||
Proposals::<T>::get(id)
|
||
}
|
||
|
||
/// Query vote stance canonical por (proposal_id, voter) sin storage mutation.
|
||
pub fn query_vote_of(id: ProposalId, voter: &T::AccountId) -> Option<VoteStance> {
|
||
Votes::<T>::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;
|
||
}
|
||
}
|