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:
1 parent
88e277e1fd
commit
1a9e37021d
6 files changed
+111
-37
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user