fix(governance): FIX-6 Stage 1 — tesoreria real Currency (cierra hallazgo de FIX-5)

Build + tests verde: governance 42 + runtime 27. Verificacion adversarial
ultracode (2 skeptics): 2 confirmed/low, sin blockers.

Cierra el hallazgo HIGH de FIX-5: treasury_disbursement era simbolico (solo
decrementaba un contador u128 desconectado de pallet_balances). Ahora:
- governance Config: type Currency (fungible Inspect+Mutate) + type
  TreasuryPalletId (pallet::constant).
- Eliminado el contador Treasury<StorageValue<u128>> (0 referencias colgantes).
- treasury_account() = PalletId.into_account_truncating() — vault keyless
  deterministico (nadie lo controla por wallet; honor Inmutable #16).
- treasury_balance() lee el balance REAL (Inspect::balance), no un contador.
- treasury_disbursement: T::Currency::transfer real desde el vault al recipient
  (ensure_root + proposal Approved). treasury_deposit: signed, dona del propio
  balance del caller (cualquiera fondea, no requiere Root, imposible robar).
- runtime configs.rs: Currency=Balances + GovernanceTreasuryPalletId(og/gvtry).
- mock con pallet_balances + scenario_4 prueba que el RECIPIENT recibe OGC REAL
  en pallet_balances (bob 100M -> 100M+300k), no solo que el contador baja.

Decision constitucional (FIX-6 V2-CA3): David ratifico sub_mult = ORTOGONALES
(la suscripcion premium NO reduce el subsidio anti-clasista de hardware, #16).
Input para el OnChargeTransaction de Stage 2. Registrado en KG.

FOLLOW-UPS (low, documentados, no bloquean):
- genesis mainnet: rutear los 250M Foundation a la cuenta og/gvtry (hoy van a
  FNDT) — detalle de pre-launch que David ratifica. El vault arranca en 0 hasta
  fondear.
- treasury_disbursement no vincula amount/recipient al proposal Approved, y el
  mismo proposal es reusable (no se marca disbursed). Check ceremonial, no
  autorizacion per-desembolso. Sin escalada nueva (Root ya puede todo via sudo).
  Endurecer antes del sunset sudo->governance.

FIX-6 Stage 1 restante (next): emision que paga Aura authors (cierra V2-CA2) +
remover APR falso del staking contract. Stage 2: NPoS + fee unificado
(OnChargeTransaction con el sub_mult ortogonal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Opus 4.8 committed 2026-06-10 05:36:31 -05:00
1 parent 88e277e1fd
commit 1a9e37021d
6 files changed
+111 -37

No files matched your search

+53 -27
View File
@@ -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<Event<Self>>
+ IsType<<Self as frame_system::Config>::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<Self::AccountId, Balance = u128> + Mutate<Self::AccountId>;
/// 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<PalletId>;
}
/// Pallet definition canonical.
@@ -301,14 +316,11 @@ pub mod pallet {
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>;
// 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<StorageDoubleMap<ProposalId, AccountId, u128>>` canonical.
///
@@ -1026,22 +1038,21 @@ pub mod pallet {
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.
// 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::<T>::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<T>,
amount: u128,
) -> DispatchResult {
ensure_root(origin)?;
let new_balance = Treasury::<T>::get().saturating_add(amount);
Treasury::<T>::put(new_balance);
pub fn treasury_deposit(origin: OriginFor<T>, 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::<T>::TreasuryInsufficientBalance)?;
Ok(())
}
}
/// FIX-6 — helpers de la tesorería real (cuenta respaldada por pallet_balances).
impl<T: Config> Pallet<T> {
/// 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<T: Config>(account: &T::AccountId) -> [u8; 32] {
let encoded = account.encode();
+30 -3
View File
@@ -14,6 +14,7 @@ type Block = frame_system::mocking::MockBlock<Test>;
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<Self::AccountId>;
type AccountData = pallet_balances::AccountData<u128>;
}
#[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::<Test>::default()
let mut storage = frame_system::GenesisConfig::<Test>::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::<Test> {
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);
});
@@ -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!(
<Balances as frame_support::traits::fungible::Inspect<_>>::balance(&bob_account()),
100_000_000 + 300_000
);
System::assert_last_event(RuntimeEvent::Governance(Event::TreasuryDisbursed {
proposal_id: id,
recipient: bob_account(),