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();