diff --git a/Cargo.lock b/Cargo.lock index 2aad4dd..aba15ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6401,6 +6401,7 @@ dependencies = [ "frame-support", "frame-system", "origin-chain-types", + "pallet-balances", "parity-scale-codec", "scale-info", "sp-api 39.0.0", diff --git a/crates/origin-chain-governance/Cargo.toml b/crates/origin-chain-governance/Cargo.toml index ec60f4c..240346d 100644 --- a/crates/origin-chain-governance/Cargo.toml +++ b/crates/origin-chain-governance/Cargo.toml @@ -33,6 +33,8 @@ scale-info = { workspace = true } sp-core = { workspace = true, default-features = true } sp-io = { workspace = true, default-features = true } sp-runtime = { workspace = true, default-features = true } +# FIX-6: pallet_balances en el mock para testear la tesorería real (transfers). +pallet-balances = { workspace = true, default-features = true } [features] default = ["std"] diff --git a/crates/origin-chain-governance/src/lib.rs b/crates/origin-chain-governance/src/lib.rs index 091e516..2d5308a 100644 --- a/crates/origin-chain-governance/src/lib.rs +++ b/crates/origin-chain-governance/src/lib.rs @@ -104,11 +104,14 @@ sp_api::decl_runtime_apis! { #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; + use frame_support::traits::fungible::{Inspect, Mutate}; + use frame_support::traits::tokens::Preservation; + use frame_support::PalletId; 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; + use sp_runtime::traits::{AccountIdConversion, SaturatedConversion}; /// ProposalStatus canonical — proposal lifecycle stages canonical. #[derive( @@ -219,6 +222,18 @@ pub mod pallet { /// Runtime event type canonical wired into `RuntimeEvent` aggregate. type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// FIX-6 (V2-CA2/V3-C3): Currency real (fungible) para mover OGC de la + /// tesorería. La tesorería deja de ser un contador u128 y pasa a ser una + /// cuenta real respaldada por pallet_balances. + type Currency: Inspect + Mutate; + + /// FIX-6: PalletId del que se deriva la cuenta-tesorería (vault). Cuenta + /// determinística sin private key — nadie la controla por wallet, solo la + /// governance vía treasury_disbursement (Root). Honor Inmutable #16: la + /// tesorería no depende de una llave humana custodiable. + #[pallet::constant] + type TreasuryPalletId: Get; } /// Pallet definition canonical. @@ -301,14 +316,11 @@ pub mod pallet { 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>; + // FIX-6 (V2-CA2/V3-C3): la tesorería ya NO es un contador `u128` desconectado. + // El balance real vive en pallet_balances, en la cuenta-tesorería derivada del + // PalletId (`Self::treasury_account()`). Esto cierra el hallazgo del audit de + // FIX-5 (treasury_disbursement era simbólico). Ver helpers treasury_account() + + // treasury_balance() + las dispatchables treasury_disbursement/treasury_deposit. /// CHN-4 5.5 v0.0.35 canonical: `OverrideVotes>` canonical. /// @@ -1026,22 +1038,21 @@ pub mod pallet { 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. + // FIX-6: transfer REAL de OGC desde la cuenta-tesorería al recipient + // (cierra el hallazgo de FIX-5: antes solo decrementaba un contador + // u128 desconectado de pallet_balances). Si la tesorería no tiene + // fondos suficientes (manteniendo ED vía Preservation::Preserve), el + // transfer falla → TreasuryInsufficientBalance. + let treasury = Self::treasury_account(); + T::Currency::transfer(&treasury, &recipient, amount, Preservation::Preserve) + .map_err(|_| Error::::TreasuryInsufficientBalance)?; + let remaining_balance = Self::treasury_balance(); Self::deposit_event(Event::TreasuryDisbursed { proposal_id, recipient, amount, - remaining_balance: new_balance, + remaining_balance, }); Ok(()) @@ -1054,17 +1065,32 @@ pub mod pallet { /// 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); + pub fn treasury_deposit(origin: OriginFor, amount: u128) -> DispatchResult { + // FIX-6: donación REAL a la tesorería — el caller transfiere `amount` + // de su propio balance a la cuenta-tesorería. Cualquiera puede fondear + // la tesorería (no requiere Root: aportar no es extraer). + let who = ensure_signed(origin)?; + T::Currency::transfer(&who, &Self::treasury_account(), amount, Preservation::Preserve) + .map_err(|_| Error::::TreasuryInsufficientBalance)?; Ok(()) } } + /// FIX-6 — helpers de la tesorería real (cuenta respaldada por pallet_balances). + impl Pallet { + /// Cuenta-tesorería (vault) derivada del PalletId — determinística, sin + /// private key. Tiene el balance real de OGC; solo governance (Root) la + /// drena vía treasury_disbursement. + pub fn treasury_account() -> T::AccountId { + T::TreasuryPalletId::get().into_account_truncating() + } + + /// Balance REAL de la tesorería (lee pallet_balances, no un contador). + pub fn treasury_balance() -> u128 { + T::Currency::balance(&Self::treasury_account()) + } + } + /// Helper canonical para convert T::AccountId → 32-byte canonical. fn account_bytes(account: &T::AccountId) -> [u8; 32] { let encoded = account.encode(); diff --git a/crates/origin-chain-governance/src/mock.rs b/crates/origin-chain-governance/src/mock.rs index 96d959d..a5e3f3f 100644 --- a/crates/origin-chain-governance/src/mock.rs +++ b/crates/origin-chain-governance/src/mock.rs @@ -14,6 +14,7 @@ type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( pub enum Test { System: frame_system, + Balances: pallet_balances, Governance: origin_chain_governance, } ); @@ -23,18 +24,44 @@ impl frame_system::Config for Test { type Block = Block; type AccountId = sp_runtime::AccountId32; type Lookup = sp_runtime::traits::IdentityLookup; + type AccountData = pallet_balances::AccountData; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = u128; +} + +frame_support::parameter_types! { + /// FIX-6: PalletId de la cuenta-tesorería del mock. + pub const TestTreasuryPalletId: frame_support::PalletId = + frame_support::PalletId(*b"og/gvtry"); } impl origin_chain_governance::Config for Test { type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type TreasuryPalletId = TestTreasuryPalletId; } /// Test externalities builder canonical (block_number=1). pub fn new_test_ext() -> sp_io::TestExternalities { - let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::::default() + let mut storage = frame_system::GenesisConfig::::default() .build_storage() - .expect("genesis canonical builds") - .into(); + .expect("genesis canonical builds"); + // FIX-6: fondea cuentas para los tests de tesorería real (transfers OGC). + pallet_balances::GenesisConfig:: { + balances: vec![ + (alice_account(), 100_000_000), + (bob_account(), 100_000_000), + (charlie_account(), 100_000_000), + ], + ..Default::default() + } + .assimilate_storage(&mut storage) + .expect("balances genesis builds"); + let mut ext: sp_io::TestExternalities = storage.into(); ext.execute_with(|| { System::set_block_number(1); }); diff --git a/crates/origin-chain-governance/src/tests_integration.rs b/crates/origin-chain-governance/src/tests_integration.rs index aa0dc38..2a797f9 100644 --- a/crates/origin-chain-governance/src/tests_integration.rs +++ b/crates/origin-chain-governance/src/tests_integration.rs @@ -17,7 +17,7 @@ use crate::mock::{ alice_account, alice_origin, bob_account, bob_origin, charlie_account, charlie_origin, - new_test_ext, run_to_block, Governance, RuntimeEvent, System, Test, + new_test_ext, run_to_block, Balances, Governance, RuntimeEvent, System, Test, }; use crate::pallet::{Proposal, ProposalStatus, MIN_PROPOSAL_STAKE}; use crate::{derive_proposal_id, Error, Event, Proposals, Votes}; @@ -758,10 +758,10 @@ fn treasury_scenario_1_deposit_then_query_canonical() { assert_eq!(Governance::treasury_balance(), 0); - assert_ok!(Governance::treasury_deposit(RawOrigin::Root.into(), 1_000_000)); + assert_ok!(Governance::treasury_deposit(alice_origin(),1_000_000)); assert_eq!(Governance::treasury_balance(), 1_000_000); - assert_ok!(Governance::treasury_deposit(RawOrigin::Root.into(), 500_000)); + assert_ok!(Governance::treasury_deposit(alice_origin(),500_000)); assert_eq!(Governance::treasury_balance(), 1_500_000); }); } @@ -772,7 +772,7 @@ fn treasury_scenario_2_disbursement_requires_approved_proposal_canonical() { use frame_support::dispatch::RawOrigin; // Treasury deposit - assert_ok!(Governance::treasury_deposit(RawOrigin::Root.into(), 1_000_000)); + assert_ok!(Governance::treasury_deposit(alice_origin(),1_000_000)); let id = submit_default(alice_origin()); @@ -795,7 +795,7 @@ fn treasury_scenario_3_insufficient_balance_rejected_canonical() { use frame_support::dispatch::RawOrigin; // Treasury 100 OGC only - assert_ok!(Governance::treasury_deposit(RawOrigin::Root.into(), 100)); + assert_ok!(Governance::treasury_deposit(alice_origin(),100)); // Setup approved proposal let id = submit_default(alice_origin()); @@ -822,7 +822,7 @@ fn treasury_scenario_4_successful_disbursement_canonical() { use frame_support::dispatch::RawOrigin; // Treasury 1M OGC - assert_ok!(Governance::treasury_deposit(RawOrigin::Root.into(), 1_000_000)); + assert_ok!(Governance::treasury_deposit(alice_origin(),1_000_000)); // Setup approved proposal let id = submit_default(alice_origin()); @@ -838,9 +838,16 @@ fn treasury_scenario_4_successful_disbursement_canonical() { 300_000, )); - // Balance canonical = 700_000 + // Treasury balance real = 700_000 (1M - 300k) assert_eq!(Governance::treasury_balance(), 700_000); + // FIX-6: el recipient recibió OGC REAL (lo que distingue un transfer real + // de un contador): bob pasó de 100M a 100M + 300k en pallet_balances. + assert_eq!( + >::balance(&bob_account()), + 100_000_000 + 300_000 + ); + System::assert_last_event(RuntimeEvent::Governance(Event::TreasuryDisbursed { proposal_id: id, recipient: bob_account(), diff --git a/crates/origin-chain-runtime/src/configs.rs b/crates/origin-chain-runtime/src/configs.rs index fea0bda..0012a8d 100644 --- a/crates/origin-chain-runtime/src/configs.rs +++ b/crates/origin-chain-runtime/src/configs.rs @@ -401,8 +401,19 @@ impl origin_chain_agent_identity::Config for Runtime { // - Custom origin canonical para Technical Council veto (5.5) // - `IsGovernanceAdvisor: Trait` (5.6 — humans-only vote enforcement) +parameter_types! { + /// FIX-6: PalletId de la cuenta-tesorería de governance (vault, sin private + /// key, determinística). Tiene el balance real OGC; solo la governance (Root) + /// la drena vía treasury_disbursement. PRE-LAUNCH: el genesis mainnet debe + /// rutear la Foundation 250M a esta cuenta (hoy va a FNDT — follow-up). + pub const GovernanceTreasuryPalletId: frame_support::PalletId = + frame_support::PalletId(*b"og/gvtry"); +} + impl origin_chain_governance::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type Currency = crate::Balances; + type TreasuryPalletId = GovernanceTreasuryPalletId; } // ============================================================