diff --git a/Cargo.lock b/Cargo.lock index c941ecaa..03d2e081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7133,6 +7133,7 @@ dependencies = [ "frame-system", "log", "pallet-balances 40.0.1", + "pallet-treasury", "parity-scale-codec", "qp-wormhole", "scale-info", @@ -7530,21 +7531,15 @@ dependencies = [ [[package]] name = "pallet-treasury" -version = "40.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77efcc0e81cf6025128d463fa56d024f1abb6ff26e190fc091da3bafd3882d2a" +version = "0.1.0" dependencies = [ - "docify", "frame-benchmarking", "frame-support", "frame-system", - "impl-trait-for-tuples", - "log", - "pallet-balances 42.0.0", "parity-scale-codec", "scale-info", - "serde", "sp-core", + "sp-io", "sp-runtime", ] diff --git a/Cargo.toml b/Cargo.toml index c78175f0..ec3bd93c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "pallets/qpow", "pallets/reversible-transfers", "pallets/scheduler", + "pallets/treasury", "pallets/wormhole", "primitives/consensus/pow", "primitives/consensus/qpow", @@ -136,6 +137,7 @@ pallet-multisig = { path = "./pallets/multisig", default-features = false } pallet-qpow = { path = "./pallets/qpow", default-features = false } pallet-reversible-transfers = { path = "./pallets/reversible-transfers", default-features = false } pallet-scheduler = { path = "./pallets/scheduler", default-features = false } +pallet-treasury = { path = "./pallets/treasury", default-features = false } pallet-wormhole = { path = "./pallets/wormhole", default-features = false } qp-dilithium-crypto = { path = "./primitives/dilithium-crypto", version = "0.2.0", default-features = false } qp-header = { path = "./primitives/header", default-features = false } @@ -184,7 +186,6 @@ pallet-timestamp = { version = "40.0.0", default-features = false } pallet-transaction-payment = { version = "41.0.0", default-features = false } pallet-transaction-payment-rpc = { version = "44.0.0", default-features = false } pallet-transaction-payment-rpc-runtime-api = { version = "41.0.0", default-features = false } -pallet-treasury = { version = "40.0.0", default-features = false } pallet-utility = { version = "41.0.0", default-features = false } prometheus-endpoint = { version = "0.17.2", default-features = false, package = "substrate-prometheus-endpoint" } sc-basic-authorship = { version = "0.50.0", default-features = false } diff --git a/node/src/command.rs b/node/src/command.rs index b44d75ac..110e9c83 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -19,7 +19,7 @@ use sp_core::{ H256, }; use sp_keyring::Sr25519Keyring; -use sp_runtime::traits::{AccountIdConversion, IdentifyAccount}; +use sp_runtime::traits::IdentifyAccount; #[derive(Debug, PartialEq)] pub struct QuantusKeyDetails { @@ -459,9 +459,14 @@ pub fn run() -> sc_cli::Result<()> { None => { // Automatically set rewards_address to Treasury when --dev is used if cli.run.shared_params.is_dev() { - let treasury_account = - quantus_runtime::configs::TreasuryPalletId::get() - .into_account_truncating(); + let chain_id = config.chain_spec.id(); + let treasury_account = quantus_runtime::genesis_config_presets::get_treasury_account_for_chain(chain_id) + .ok_or_else(|| { + sc_cli::Error::Input(format!( + "Unknown chain ID for treasury config: {}", + chain_id + )) + })?; log::info!( "⛏️ Using treasury address for rewards: {:?}", treasury_account diff --git a/pallets/mining-rewards/Cargo.toml b/pallets/mining-rewards/Cargo.toml index edb186b0..34ab235d 100644 --- a/pallets/mining-rewards/Cargo.toml +++ b/pallets/mining-rewards/Cargo.toml @@ -22,6 +22,7 @@ frame-benchmarking = { optional = true, workspace = true, default-features = fal frame-support.workspace = true frame-system.workspace = true log.workspace = true +pallet-treasury = { workspace = true, default-features = false } qp-wormhole.workspace = true scale-info = { workspace = true, default-features = false, features = ["derive"] } sp-consensus-pow.workspace = true @@ -30,6 +31,8 @@ sp-runtime.workspace = true [dev-dependencies] pallet-balances.features = ["std"] pallet-balances.workspace = true +pallet-treasury.features = ["std"] +pallet-treasury.workspace = true sp-core.workspace = true sp-io.workspace = true @@ -45,6 +48,7 @@ std = [ "frame-benchmarking?/std", "frame-support/std", "frame-system/std", + "pallet-treasury/std", "qp-wormhole/std", "scale-info/std", "sp-consensus-pow/std", diff --git a/pallets/mining-rewards/src/benchmarking.rs b/pallets/mining-rewards/src/benchmarking.rs index 2bf70338..b8867d32 100644 --- a/pallets/mining-rewards/src/benchmarking.rs +++ b/pallets/mining-rewards/src/benchmarking.rs @@ -8,16 +8,14 @@ use frame_benchmarking::{account, v2::*, BenchmarkError}; use frame_support::traits::fungible::{Inspect, Mutate}; use frame_system::{pallet_prelude::BlockNumberFor, Pallet as SystemPallet}; use sp_consensus_pow::POW_ENGINE_ID; -use sp_runtime::{ - generic::{Digest, DigestItem}, - traits::AccountIdConversion, -}; +use sp_runtime::generic::{Digest, DigestItem}; #[benchmarks] mod benchmarks { use super::*; use codec::Encode; - use frame_support::traits::{Get, OnFinalize}; + use frame_support::traits::OnFinalize; + use pallet_treasury::TreasuryProvider; use sp_runtime::Saturating; #[benchmark] @@ -37,7 +35,7 @@ mod benchmarks { ); // Pre-fund Treasury account to ensure it exists - let treasury_account = T::TreasuryPalletId::get().into_account_truncating(); + let treasury_account = T::Treasury::account_id(); let ed = T::Currency::minimum_balance(); let _ = T::Currency::mint_into(&treasury_account, ed.saturating_mul(1000u32.into())); let _ = T::Currency::mint_into(&miner, ed.saturating_mul(1000u32.into())); diff --git a/pallets/mining-rewards/src/lib.rs b/pallets/mining-rewards/src/lib.rs index 4f89231e..89cd75c5 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -26,12 +26,10 @@ pub mod pallet { }, }; use frame_system::pallet_prelude::*; + use pallet_treasury::TreasuryProvider; use qp_wormhole::TransferProofs; use sp_consensus_pow::POW_ENGINE_ID; - use sp_runtime::{ - generic::DigestItem, - traits::{AccountIdConversion, Saturating}, - }; + use sp_runtime::{generic::DigestItem, traits::Saturating}; const UNIT: u128 = 1_000_000_000_000u128; @@ -62,13 +60,8 @@ pub mod pallet { #[pallet::constant] type EmissionDivisor: Get>; - /// The portion of rewards that goes to treasury (out of 100) - #[pallet::constant] - type TreasuryPortion: Get; - - /// The treasury pallet ID - #[pallet::constant] - type TreasuryPalletId: Get; + /// Provides treasury account and portion (from pallet_treasury or mock) + type Treasury: pallet_treasury::TreasuryProvider; /// Account ID used as the "from" account when creating transfer proofs for minted tokens #[pallet::constant] @@ -126,7 +119,7 @@ pub mod pallet { .unwrap_or_else(BalanceOf::::zero); // Split the reward between treasury and miner - let treasury_portion = T::TreasuryPortion::get(); + let treasury_portion = T::Treasury::portion(); let treasury_reward = total_reward.saturating_mul(treasury_portion.into()) / 100u32.into(); let miner_reward = total_reward.saturating_sub(treasury_reward); @@ -213,7 +206,7 @@ pub mod pallet { Self::deposit_event(Event::MinerRewarded { miner: miner.clone(), reward }); }, None => { - let treasury = T::TreasuryPalletId::get().into_account_truncating(); + let treasury = T::Treasury::account_id(); let _ = T::Currency::mint_into(&treasury, reward).defensive(); T::Currency::store_transfer_proof(&mint_account, &treasury, reward); diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index 6a620fb9..121f94a3 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -3,7 +3,6 @@ use codec::Encode; use frame_support::{ parameter_types, traits::{ConstU32, Everything, Hooks}, - PalletId, }; use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ @@ -32,7 +31,19 @@ parameter_types! { pub const MaxSupply: u128 = 21_000_000 * UNIT; pub const EmissionDivisor: u128 = 26_280_000; pub const ExistentialDeposit: Balance = 1; - pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub const TreasuryAccount: sp_core::crypto::AccountId32 = sp_core::crypto::AccountId32::new([42u8; 32]); +} + +/// Mock treasury for mining-rewards tests +pub struct MockTreasury; +impl pallet_treasury::TreasuryProvider for MockTreasury { + type AccountId = sp_core::crypto::AccountId32; + fn account_id() -> Self::AccountId { + TreasuryAccount::get() + } + fn portion() -> u8 { + 50 + } } impl frame_system::Config for Test { @@ -86,7 +97,6 @@ impl pallet_balances::Config for Test { } parameter_types! { - pub const TreasuryPortion: u8 = 50; // 50% goes to treasury in tests (matching runtime) pub const MintingAccount: sp_core::crypto::AccountId32 = sp_core::crypto::AccountId32::new([99u8; 32]); } @@ -95,8 +105,7 @@ impl pallet_mining_rewards::Config for Test { type WeightInfo = (); type MaxSupply = MaxSupply; type EmissionDivisor = EmissionDivisor; - type TreasuryPortion = TreasuryPortion; - type TreasuryPalletId = TreasuryPalletId; + type Treasury = MockTreasury; type MintingAccount = MintingAccount; } @@ -118,7 +127,11 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); pallet_balances::GenesisConfig:: { - balances: vec![(miner(), ExistentialDeposit::get()), (miner2(), ExistentialDeposit::get())], + balances: vec![ + (miner(), ExistentialDeposit::get()), + (miner2(), ExistentialDeposit::get()), + (TreasuryAccount::get(), ExistentialDeposit::get()), + ], } .assimilate_storage(&mut t) .unwrap(); diff --git a/pallets/mining-rewards/src/tests.rs b/pallets/mining-rewards/src/tests.rs index 04513ca6..a99573a6 100644 --- a/pallets/mining-rewards/src/tests.rs +++ b/pallets/mining-rewards/src/tests.rs @@ -1,6 +1,6 @@ use crate::{mock::*, weights::WeightInfo, Event}; use frame_support::traits::{Currency, Hooks}; -use sp_runtime::traits::AccountIdConversion; +use pallet_treasury::TreasuryProvider; #[test] fn miner_reward_works() { @@ -15,7 +15,7 @@ fn miner_reward_works() { // Initial supply is just the existential deposits (2 accounts * 1 unit each = 2) let current_supply = Balances::total_issuance(); let total_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = total_reward * TreasuryPortion::get() as u128 / 100; + let treasury_reward = total_reward * MockTreasury::portion() as u128 / 100; let miner_reward = total_reward - treasury_reward; // Run the on_finalize hook @@ -53,7 +53,7 @@ fn miner_reward_with_transaction_fees_works() { // Calculate expected rewards with treasury portion let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = total_block_reward * TreasuryPortion::get() as u128 / 100; + let treasury_reward = total_block_reward * MockTreasury::portion() as u128 / 100; let miner_block_reward = total_block_reward - treasury_reward; // Run the on_finalize hook @@ -95,7 +95,7 @@ fn on_unbalanced_collects_fees() { // Calculate expected rewards with treasury portion let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = total_block_reward * TreasuryPortion::get() as u128 / 100; + let treasury_reward = total_block_reward * MockTreasury::portion() as u128 / 100; let miner_block_reward = total_block_reward - treasury_reward; // Add a miner to the pre-runtime digest and distribute rewards @@ -122,7 +122,7 @@ fn multiple_blocks_accumulate_rewards() { let total_block1_reward = (MaxSupply::get() - current_supply_block1) / EmissionDivisor::get(); let miner_block1_reward = - total_block1_reward - (total_block1_reward * TreasuryPortion::get() as u128 / 100); + total_block1_reward - (total_block1_reward * MockTreasury::portion() as u128 / 100); MiningRewards::on_finalize(1); @@ -137,7 +137,7 @@ fn multiple_blocks_accumulate_rewards() { let total_block2_reward = (MaxSupply::get() - current_supply_block2) / EmissionDivisor::get(); let miner_block2_reward = - total_block2_reward - (total_block2_reward * TreasuryPortion::get() as u128 / 100); + total_block2_reward - (total_block2_reward * MockTreasury::portion() as u128 / 100); MiningRewards::on_finalize(2); @@ -164,7 +164,7 @@ fn different_miners_get_different_rewards() { let total_block1_reward = (MaxSupply::get() - current_supply_block1) / EmissionDivisor::get(); let miner_block1_reward = - total_block1_reward - (total_block1_reward * TreasuryPortion::get() as u128 / 100); + total_block1_reward - (total_block1_reward * MockTreasury::portion() as u128 / 100); MiningRewards::on_finalize(1); @@ -180,7 +180,7 @@ fn different_miners_get_different_rewards() { let total_block2_reward = (MaxSupply::get() - current_supply_block2) / EmissionDivisor::get(); let miner_block2_reward = - total_block2_reward - (total_block2_reward * TreasuryPortion::get() as u128 / 100); + total_block2_reward - (total_block2_reward * MockTreasury::portion() as u128 / 100); MiningRewards::on_finalize(2); @@ -213,7 +213,7 @@ fn transaction_fees_collector_works() { let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); let miner_block_reward = - total_block_reward - (total_block_reward * TreasuryPortion::get() as u128 / 100); + total_block_reward - (total_block_reward * MockTreasury::portion() as u128 / 100); // Reward miner set_miner_digest(miner()); @@ -243,7 +243,7 @@ fn block_lifecycle_works() { let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); let miner_block_reward = - total_block_reward - (total_block_reward * TreasuryPortion::get() as u128 / 100); + total_block_reward - (total_block_reward * MockTreasury::portion() as u128 / 100); // 3. on_finalize - should reward the miner set_miner_digest(miner()); @@ -290,13 +290,13 @@ fn test_run_to_block_helper() { fn rewards_go_to_treasury_when_no_miner() { new_test_ext().execute_with(|| { // Get Treasury account - let treasury_account = TreasuryPalletId::get().into_account_truncating(); + let treasury_account = MockTreasury::account_id(); let initial_treasury_balance = Balances::free_balance(&treasury_account); // Calculate expected rewards - when no miner, all rewards go to treasury let current_supply = Balances::total_issuance(); let total_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_portion_reward = total_reward * TreasuryPortion::get() as u128 / 100; + let treasury_portion_reward = total_reward * MockTreasury::portion() as u128 / 100; let miner_portion_reward = total_reward - treasury_portion_reward; // Create a block without a miner (no digest set) @@ -332,7 +332,7 @@ fn test_fees_and_rewards_to_miner() { // Calculate expected rewards with treasury portion let current_supply = Balances::total_issuance(); let total_block_reward = (MaxSupply::get() - current_supply) / EmissionDivisor::get(); - let treasury_reward = total_block_reward * TreasuryPortion::get() as u128 / 100; + let treasury_reward = total_block_reward * MockTreasury::portion() as u128 / 100; let miner_block_reward = total_block_reward - treasury_reward; // Create a block with a miner @@ -371,13 +371,13 @@ fn test_fees_and_rewards_to_miner() { fn test_emission_simulation_120m_blocks() { new_test_ext().execute_with(|| { // Add realistic initial supply similar to genesis - let treasury_account = TreasuryPalletId::get().into_account_truncating(); + let treasury_account = MockTreasury::account_id(); let _ = Balances::deposit_creating(&treasury_account, 3_600_000 * UNIT); println!("=== Mining Rewards Emission Simulation ==="); println!("Max Supply: {:.0} tokens", MaxSupply::get() as f64 / UNIT as f64); println!("Emission Divisor: {:?}", EmissionDivisor::get()); - println!("Treasury Portion: {}%", TreasuryPortion::get()); + println!("Treasury Portion: {}%", MockTreasury::portion()); println!(); const MAX_BLOCKS: u32 = 130_000_000; @@ -396,7 +396,7 @@ fn test_emission_simulation_120m_blocks() { // Print initial state let remaining = MaxSupply::get() - current_supply; let block_reward = if remaining > 0 { remaining / EmissionDivisor::get() } else { 0 }; - let treasury_reward = block_reward * TreasuryPortion::get() as u128 / 100; + let treasury_reward = block_reward * MockTreasury::portion() as u128 / 100; let miner_reward = block_reward - treasury_reward; println!( @@ -427,7 +427,7 @@ fn test_emission_simulation_120m_blocks() { } let block_reward = remaining_supply / EmissionDivisor::get(); - let treasury_reward = block_reward * TreasuryPortion::get() as u128 / 100; + let treasury_reward = block_reward * MockTreasury::portion() as u128 / 100; let miner_reward = block_reward - treasury_reward; // Update totals (simulate the minting) @@ -445,7 +445,7 @@ fn test_emission_simulation_120m_blocks() { // Print progress report let remaining = MaxSupply::get().saturating_sub(current_supply); let next_block_reward = if remaining > 0 { remaining / EmissionDivisor::get() } else { 0 }; - let next_treasury = next_block_reward * TreasuryPortion::get() as u128 / 100; + let next_treasury = next_block_reward * MockTreasury::portion() as u128 / 100; let next_miner = next_block_reward - next_treasury; println!( diff --git a/pallets/treasury/Cargo.toml b/pallets/treasury/Cargo.toml new file mode 100644 index 00000000..60a22faa --- /dev/null +++ b/pallets/treasury/Cargo.toml @@ -0,0 +1,38 @@ +[package] +authors.workspace = true +description = "Treasury configuration pallet - address and mining reward portion" +edition.workspace = true +homepage.workspace = true +license = "Apache-2.0" +name = "pallet-treasury" +publish = false +repository.workspace = true +version = "0.1.0" + +[dependencies] +codec = { workspace = true, default-features = false, features = ["derive"] } +frame-benchmarking = { optional = true, workspace = true, default-features = false } +frame-support.workspace = true +frame-system.workspace = true +scale-info = { workspace = true, default-features = false, features = ["derive"] } +sp-runtime.workspace = true + +[dev-dependencies] +sp-core.workspace = true +sp-io.workspace = true + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-runtime/std", +] diff --git a/pallets/treasury/src/benchmarking.rs b/pallets/treasury/src/benchmarking.rs new file mode 100644 index 00000000..7ffbb564 --- /dev/null +++ b/pallets/treasury/src/benchmarking.rs @@ -0,0 +1,32 @@ +//! Benchmarking for pallet_treasury + +use super::*; +use frame_benchmarking::v2::*; + +#[benchmarks] +mod benchmarks { + use super::*; + use frame_system::RawOrigin; + + #[benchmark] + fn set_treasury_account() -> Result<(), BenchmarkError> { + let account: T::AccountId = account("caller", 0, 0); + let root: ::RuntimeOrigin = RawOrigin::Root.into(); + + #[extrinsic_call] + _(root, account); + + Ok(()) + } + + #[benchmark] + fn set_treasury_portion() -> Result<(), BenchmarkError> { + let portion: u8 = 50; + let root: ::RuntimeOrigin = RawOrigin::Root.into(); + + #[extrinsic_call] + _(root, portion); + + Ok(()) + } +} diff --git a/pallets/treasury/src/lib.rs b/pallets/treasury/src/lib.rs new file mode 100644 index 00000000..09cb22a4 --- /dev/null +++ b/pallets/treasury/src/lib.rs @@ -0,0 +1,156 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! Treasury configuration pallet. +//! +//! Provides TreasuryProvider trait for mining-rewards integration. + +pub mod weights; +pub use weights::WeightInfo; + +/// Trait for providing treasury account and portion to mining-rewards. +pub trait TreasuryProvider { + type AccountId; + fn account_id() -> Self::AccountId; + fn portion() -> u8; +} + +#[frame_support::pallet] +pub mod pallet { + use super::WeightInfo; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type WeightInfo: crate::WeightInfo; + } + + /// The treasury account that receives mining rewards. + #[pallet::storage] + #[pallet::getter(fn treasury_account)] + pub type TreasuryAccount = StorageValue<_, T::AccountId, OptionQuery>; + + /// The portion of mining rewards that goes to treasury (0-100). + #[pallet::storage] + #[pallet::getter(fn treasury_portion)] + pub type TreasuryPortion = StorageValue<_, u8, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub treasury_account: T::AccountId, + pub treasury_portion: u8, + } + + impl Default for GenesisConfig + where + T::AccountId: From<[u8; 32]>, + { + fn default() -> Self { + Self { treasury_account: [0u8; 32].into(), treasury_portion: 50 } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + assert!(self.treasury_portion <= 100, "Treasury portion must be 0-100"); + TreasuryAccount::::put(self.treasury_account.clone()); + TreasuryPortion::::put(self.treasury_portion); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + TreasuryAccountUpdated { new_account: T::AccountId }, + TreasuryPortionUpdated { new_portion: u8 }, + } + + #[pallet::call] + impl Pallet { + /// Set the treasury account. Root only. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::set_treasury_account())] + pub fn set_treasury_account(origin: OriginFor, account: T::AccountId) -> DispatchResult { + ensure_root(origin)?; + TreasuryAccount::::put(&account); + Self::deposit_event(Event::TreasuryAccountUpdated { new_account: account }); + Ok(()) + } + + /// Set the treasury portion (0-100). Root only. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::set_treasury_portion())] + pub fn set_treasury_portion(origin: OriginFor, portion: u8) -> DispatchResult { + ensure_root(origin)?; + ensure!(portion <= 100, Error::::InvalidPortion); + TreasuryPortion::::put(portion); + Self::deposit_event(Event::TreasuryPortionUpdated { new_portion: portion }); + Ok(()) + } + } + + #[pallet::error] + pub enum Error { + InvalidPortion, + } + + impl Pallet { + /// Get the treasury account. Returns zero account if not configured. + pub fn account_id() -> T::AccountId { + TreasuryAccount::::get().unwrap_or_else(|| { + T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .unwrap_or_else(|_| { + // Fallback: zero account + T::AccountId::decode(&mut &[0u8; 32][..]) + .unwrap_or_else(|_| panic!("Cannot create fallback AccountId")) + }) + }) + } + + /// Get the treasury portion (0-100). + pub fn portion() -> u8 { + TreasuryPortion::::get() + } + } + + /// Implements `Get` for use as runtime config parameter. + pub struct TreasuryAccountGetter(core::marker::PhantomData); + impl frame_support::traits::Get for TreasuryAccountGetter { + fn get() -> T::AccountId { + Pallet::::account_id() + } + } + + /// Implements `Get` for use as runtime config parameter. + pub struct TreasuryPortionGetter(core::marker::PhantomData); + impl frame_support::traits::Get for TreasuryPortionGetter { + fn get() -> u8 { + Pallet::::portion() + } + } +} + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub use pallet::*; + +impl TreasuryProvider for pallet::Pallet { + type AccountId = T::AccountId; + fn account_id() -> Self::AccountId { + pallet::Pallet::::account_id() + } + fn portion() -> u8 { + pallet::Pallet::::portion() + } +} diff --git a/pallets/treasury/src/mock.rs b/pallets/treasury/src/mock.rs new file mode 100644 index 00000000..a5ceeb0a --- /dev/null +++ b/pallets/treasury/src/mock.rs @@ -0,0 +1,74 @@ +use crate as pallet_treasury; +use frame_support::{ + parameter_types, + traits::{ConstU32, Everything}, +}; +use sp_runtime::{testing::H256, traits::IdentityLookup, BuildStorage}; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Treasury: pallet_treasury, + } +); + +pub type Block = frame_system::mocking::MockBlock; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 189; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = (); + type Nonce = u64; + type Hash = H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + type AccountId = sp_core::crypto::AccountId32; + type Lookup = IdentityLookup; + type Block = Block; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type ExtensionsWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); + type RuntimeEvent = RuntimeEvent; +} + +impl pallet_treasury::Config for Test { + type WeightInfo = pallet_treasury::weights::SubstrateWeight; +} + +pub fn account_id(id: u8) -> sp_core::crypto::AccountId32 { + sp_core::crypto::AccountId32::from([id; 32]) +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_treasury::GenesisConfig:: { + treasury_account: account_id(1), + treasury_portion: 50, + } + .assimilate_storage(&mut t) + .unwrap(); + + sp_io::TestExternalities::new(t) +} diff --git a/pallets/treasury/src/tests.rs b/pallets/treasury/src/tests.rs new file mode 100644 index 00000000..14bf2f12 --- /dev/null +++ b/pallets/treasury/src/tests.rs @@ -0,0 +1,58 @@ +#[cfg(test)] +mod tests { + use crate::{ + mock::{account_id, new_test_ext, Test, Treasury}, + Error, + }; + use frame_support::{assert_err, assert_ok}; + + #[test] + fn genesis_sets_treasury_config() { + new_test_ext().execute_with(|| { + assert_eq!(Treasury::account_id(), account_id(1)); + assert_eq!(Treasury::portion(), 50); + }); + } + + #[test] + fn set_treasury_account_works() { + new_test_ext().execute_with(|| { + assert_ok!(Treasury::set_treasury_account( + frame_system::RawOrigin::Root.into(), + account_id(99) + )); + assert_eq!(Treasury::account_id(), account_id(99)); + }); + } + + #[test] + fn set_treasury_account_requires_root() { + new_test_ext().execute_with(|| { + assert_err!( + Treasury::set_treasury_account( + frame_system::RawOrigin::Signed(account_id(1)).into(), + account_id(99) + ), + sp_runtime::DispatchError::BadOrigin + ); + }); + } + + #[test] + fn set_treasury_portion_works() { + new_test_ext().execute_with(|| { + assert_ok!(Treasury::set_treasury_portion(frame_system::RawOrigin::Root.into(), 30)); + assert_eq!(Treasury::portion(), 30); + }); + } + + #[test] + fn set_treasury_portion_rejects_invalid() { + new_test_ext().execute_with(|| { + assert_err!( + Treasury::set_treasury_portion(frame_system::RawOrigin::Root.into(), 101), + Error::::InvalidPortion + ); + }); + } +} diff --git a/pallets/treasury/src/weights.rs b/pallets/treasury/src/weights.rs new file mode 100644 index 00000000..e1400bb7 --- /dev/null +++ b/pallets/treasury/src/weights.rs @@ -0,0 +1,101 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Autogenerated weights for `pallet_treasury` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-02-13, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `coldbook.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/quantus-node +// benchmark +// pallet +// --pallet=pallet_treasury +// --extrinsic=* +// --chain=dev +// --steps=50 +// --repeat=20 +// --template=.maintain/frame-weight-template.hbs +// --output=pallets/treasury/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] +#![allow(dead_code)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_treasury`. +pub trait WeightInfo { + fn set_treasury_account() -> Weight; + fn set_treasury_portion() -> Weight; +} + +/// Weights for `pallet_treasury` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Treasury::TreasuryAccount` (r:0 w:1) + /// Proof: `Treasury::TreasuryAccount` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + fn set_treasury_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Treasury::TreasuryPortion` (r:0 w:1) + /// Proof: `Treasury::TreasuryPortion` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + fn set_treasury_portion() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Treasury::TreasuryAccount` (r:0 w:1) + /// Proof: `Treasury::TreasuryAccount` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + fn set_treasury_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Treasury::TreasuryPortion` (r:0 w:1) + /// Proof: `Treasury::TreasuryPortion` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + fn set_treasury_portion() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_000_000 picoseconds. + Weight::from_parts(3_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index cda93027..a2858751 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -44,7 +44,7 @@ pallet-sudo.workspace = true pallet-timestamp.workspace = true pallet-transaction-payment.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true -pallet-treasury.workspace = true +pallet-treasury = { path = "../pallets/treasury", default-features = false } pallet-utility.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true @@ -175,7 +175,6 @@ try-runtime = [ "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", - "pallet-treasury/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index cf13e3e1..e9cf246c 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -31,6 +31,7 @@ frame_benchmarking::define_benchmarks!( [pallet_sudo, Sudo] [pallet_reversible_transfers, ReversibleTransfers] [pallet_mining_rewards, MiningRewards] + [pallet_treasury, Treasury] [pallet_multisig, Multisig] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 5b7f9776..c86e1cb9 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -25,21 +25,17 @@ // Substrate and Polkadot dependencies use crate::{ - governance::{ - definitions::{ - CommunityTracksInfo, GlobalMaxMembers, MinRankOfClassConverter, PreimageDeposit, - RootOrMemberForCollectiveOrigin, RootOrMemberForTechReferendaOrigin, - RuntimeNativeBalanceConverter, RuntimeNativePaymaster, TechCollectiveTracksInfo, - }, - pallet_custom_origins, Spender, + governance::definitions::{ + CommunityTracksInfo, GlobalMaxMembers, MinRankOfClassConverter, PreimageDeposit, + RootOrMemberForCollectiveOrigin, RootOrMemberForTechReferendaOrigin, + TechCollectiveTracksInfo, }, MILLI_UNIT, }; use frame_support::{ derive_impl, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU8, EitherOf, Get, NeverEnsureOrigin, - VariantCountOf, + AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU8, Get, NeverEnsureOrigin, VariantCountOf, }, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -49,7 +45,7 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureRootWithSuccess, EnsureSigned, + EnsureRoot, EnsureSigned, }; use pallet_ranked_collective::Linear; use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier}; @@ -128,8 +124,7 @@ impl pallet_mining_rewards::Config for Runtime { type WeightInfo = pallet_mining_rewards::weights::SubstrateWeight; type MaxSupply = ConstU128<{ 21_000_000 * UNIT }>; // 21 million tokens type EmissionDivisor = ConstU128<26_280_000>; // Divide remaining supply by this amount - type TreasuryPortion = ConstU8<50>; // % of rewards go to treasury - type TreasuryPalletId = TreasuryPalletId; + type Treasury = pallet_treasury::Pallet; type MintingAccount = MintingAccount; } @@ -480,48 +475,10 @@ impl pallet_reversible_transfers::Config for Runtime { type VolumeFee = HighSecurityVolumeFee; } -parameter_types! { - pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); - pub const ProposalBond: Permill = Permill::from_percent(5); - pub const ProposalBondMinimum: Balance = UNIT; - pub const ProposalBondMaximum: Option = None; - pub const SpendPeriod: BlockNumber = 2 * DAYS; - pub const Burn: Permill = Permill::from_percent(0); - pub const MaxApprovals: u32 = 100; - pub const TreasuryPayoutPeriod: BlockNumber = 14 * DAYS; // Added for PayoutPeriod -} - impl pallet_treasury::Config for Runtime { - type PalletId = TreasuryPalletId; - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type RejectOrigin = EnsureRoot; - type SpendPeriod = SpendPeriod; - type Burn = Burn; - type BurnDestination = (); // Treasury funds will be burnt without a specific destination - type SpendFunds = (); // No external pallets spending treasury funds directly through this hook - type MaxApprovals = MaxApprovals; // For deprecated spend_local flow type WeightInfo = pallet_treasury::weights::SubstrateWeight; - type SpendOrigin = TreasurySpender; // Changed to use the custom EnsureOrigin - type AssetKind = (); // Using () to represent native currency for simplicity - type Beneficiary = AccountId; // Spends are paid to AccountId - type BeneficiaryLookup = sp_runtime::traits::AccountIdLookup; // Standard lookup for AccountId - type Paymaster = RuntimeNativePaymaster; // Custom paymaster for native currency - type BalanceConverter = RuntimeNativeBalanceConverter; // Custom converter for native currency - type PayoutPeriod = TreasuryPayoutPeriod; // How long a spend is valid for claiming - type BlockNumberProvider = System; - #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = (); // System pallet provides block number -} - -parameter_types! { - pub const MaxBalance: Balance = Balance::MAX; } -pub type TreasurySpender = EitherOf, Spender>; - -impl pallet_custom_origins::Config for Runtime {} - parameter_types! { pub const AssetDeposit: Balance = MILLI_UNIT; pub const AssetAccountDeposit: Balance = MILLI_UNIT; diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 61f71c56..f178f0bd 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -18,15 +18,13 @@ // this module is used by the client, so it's ok to panic/unwrap here #![allow(clippy::expect_used)] -use crate::{ - configs::TreasuryPalletId, AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, UNIT, -}; +use crate::{AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, UNIT}; use alloc::{vec, vec::Vec}; use qp_dilithium_crypto::pair::{crystal_alice, crystal_charlie, dilithium_bob}; use serde_json::Value; use sp_core::crypto::Ss58Codec; use sp_genesis_builder::{self, PresetId}; -use sp_runtime::traits::{AccountIdConversion, IdentifyAccount}; +use sp_runtime::traits::IdentifyAccount; /// Identifier for the heisenberg runtime preset. pub const HEISENBERG_RUNTIME_PRESET: &str = "heisenberg"; @@ -52,27 +50,46 @@ fn dilithium_default_accounts() -> Vec { crystal_charlie().into_account(), ] } -// Returns the genesis config presets populated with given parameters. -fn genesis_template(endowed_accounts: Vec, root: AccountId) -> Value { - let mut balances = endowed_accounts + +const INITIAL_TREASURY: u128 = 21_000_000 * 30 * UNIT / 100; // 30% tokens go to investors + +/// Returns the genesis config populated with given parameters. +fn genesis_template( + endowed_accounts: Vec, + root: AccountId, + treasury_account: AccountId, + treasury_portion: u8, +) -> Value { + // Treasury gets INITIAL_TREASURY; other endowed accounts get 100_000 * UNIT. + // Avoid duplicates when treasury is in endowed_accounts. + let mut balances: Vec<(AccountId, u128)> = endowed_accounts .iter() + .filter(|a| **a != treasury_account) .cloned() .map(|k| (k, 100_000 * UNIT)) - .collect::>(); - - const INITIAL_TREASURY: u128 = 21_000_000 * 30 * UNIT / 100; // 30% tokens go to investors - let treasury_account = TreasuryPalletId::get().into_account_truncating(); - balances.push((treasury_account, INITIAL_TREASURY)); + .collect(); + balances.push((treasury_account.clone(), INITIAL_TREASURY)); let config = RuntimeGenesisConfig { balances: BalancesConfig { balances }, sudo: SudoConfig { key: Some(root.clone()) }, + treasury: pallet_treasury::GenesisConfig { treasury_account, treasury_portion }, ..Default::default() }; serde_json::to_value(config).expect("Could not build genesis config.") } +/// Get treasury account for chain (for node dev mode). +pub fn get_treasury_account_for_chain(chain_id: &str) -> Option { + match chain_id { + "dev" => Some(crystal_alice().into_account()), + "heisenberg" => Some(heisenberg_root_account()), + "dirac" => Some(dirac_root_account()), + _ => None, + } +} + /// Return the development genesis config. pub fn development_config_genesis() -> Value { let mut endowed_accounts = vec![]; @@ -103,19 +120,18 @@ pub fn development_config_genesis() -> Value { initial_high_security_accounts: vec![(multisig_address, interceptor, delay)], }; + let treasury_account = crystal_alice().into_account(); + let mut balances: Vec<(AccountId, u128)> = endowed_accounts + .iter() + .filter(|a| **a != treasury_account) + .cloned() + .map(|k| (k, 100_000 * UNIT)) + .collect(); + balances.push((treasury_account.clone(), INITIAL_TREASURY)); let config = RuntimeGenesisConfig { - balances: BalancesConfig { - balances: endowed_accounts - .iter() - .cloned() - .map(|k| (k, 100_000 * UNIT)) - .chain([( - TreasuryPalletId::get().into_account_truncating(), - 21_000_000 * 30 * UNIT / 100, - )]) - .collect::>(), - }, + balances: BalancesConfig { balances }, sudo: SudoConfig { key: Some(crystal_alice().into_account()) }, + treasury: pallet_treasury::GenesisConfig { treasury_account, treasury_portion: 50 }, reversible_transfers: rt_genesis, ..Default::default() }; @@ -123,7 +139,12 @@ pub fn development_config_genesis() -> Value { } #[cfg(not(feature = "runtime-benchmarks"))] - genesis_template(endowed_accounts, crystal_alice().into_account()) + genesis_template( + endowed_accounts, + crystal_alice().into_account(), + crystal_alice().into_account(), // treasury + 50, // treasury portion % + ) } pub fn heisenberg_config_genesis() -> Value { @@ -133,7 +154,12 @@ pub fn heisenberg_config_genesis() -> Value { for account in endowed_accounts.iter() { log::info!("🍆 Endowed account: {:?}", account.to_ss58check_with_version(ss58_version)); } - genesis_template(endowed_accounts, heisenberg_root_account()) + genesis_template( + endowed_accounts, + heisenberg_root_account(), + heisenberg_root_account(), // treasury + 50, // treasury portion % + ) } pub fn dirac_config_genesis() -> Value { @@ -143,7 +169,12 @@ pub fn dirac_config_genesis() -> Value { log::info!("🍆 Endowed account: {:?}", account.to_ss58check_with_version(ss58_version)); } - genesis_template(endowed_accounts, dirac_root_account()) + genesis_template( + endowed_accounts, + dirac_root_account(), + dirac_root_account(), // treasury + 50, // treasury portion % + ) } /// Provides the JSON representation of predefined genesis config for given `id`. diff --git a/runtime/src/governance/definitions.rs b/runtime/src/governance/definitions.rs index 98db0f49..13d7f1de 100644 --- a/runtime/src/governance/definitions.rs +++ b/runtime/src/governance/definitions.rs @@ -1,6 +1,6 @@ use crate::{ - configs::TreasuryPalletId, governance::pallet_custom_origins, AccountId, Balance, Balances, - BlockNumber, Runtime, RuntimeOrigin, DAYS, HOURS, MICRO_UNIT, UNIT, + AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeOrigin, DAYS, HOURS, MICRO_UNIT, + UNIT, }; use alloc::borrow::Cow; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; @@ -10,9 +10,8 @@ use frame_support::traits::Currency; use frame_support::{ pallet_prelude::TypeInfo, traits::{ - tokens::{ConversionFromAssetBalance, Pay, PaymentStatus}, - CallerTrait, Consideration, Currency as CurrencyTrait, EnsureOrigin, EnsureOriginWithArg, - Footprint, Get, OriginTrait, ReservableCurrency, + CallerTrait, Consideration, EnsureOrigin, EnsureOriginWithArg, Footprint, Get, OriginTrait, + ReservableCurrency, }, }; use lazy_static::lazy_static; @@ -21,7 +20,7 @@ use pallet_referenda::Track; use sp_core::crypto::AccountId32; use sp_runtime::{ str_array, - traits::{AccountIdConversion, Convert, MaybeConvert}, + traits::{Convert, MaybeConvert}, DispatchError, Perbill, }; ///Preimage pallet fee model @@ -90,7 +89,7 @@ static mut GLOBAL_TRACK_OVERRIDE: Option<(BlockNumber, BlockNumber, BlockNumber, impl GlobalTrackConfig { /// Set global track timing overrides for ALL governance tracks - /// This affects CommunityTracksInfo, TechCollectiveTracksInfo, and Treasury tracks + /// This affects CommunityTracksInfo and TechCollectiveTracksInfo pub fn set_track_override( prepare_period: BlockNumber, decision_period: BlockNumber, @@ -139,7 +138,7 @@ pub struct CommunityTracksInfo; impl CommunityTracksInfo { /// Creates the base track configurations with production values - fn create_community_tracks() -> [pallet_referenda::Track; 6] { + fn create_community_tracks() -> [pallet_referenda::Track; 2] { [ // Track 0: Signed Track (authenticated proposals) pallet_referenda::Track { @@ -185,95 +184,6 @@ impl CommunityTracksInfo { floor: Perbill::from_percent(1), ceil: Perbill::from_percent(10), }, - }, - }, - // Track 2: Treasury tracks - pallet_referenda::Track { - id: 2, - info: pallet_referenda::TrackInfo { - name: str_array("treasury_small_spender"), - max_deciding: 5, - decision_deposit: 100 * UNIT, - prepare_period: DAYS, - decision_period: 3 * DAYS, - confirm_period: DAYS, - min_enactment_period: 12 * HOURS, - min_approval: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(25), - ceil: Perbill::from_percent(50), - }, - min_support: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(1), - ceil: Perbill::from_percent(10), - }, - }, - }, - pallet_referenda::Track { - id: 3, - info: pallet_referenda::TrackInfo { - name: str_array("treasury_medium_spender"), - max_deciding: 2, - decision_deposit: 250 * UNIT, - prepare_period: 6 * HOURS, - decision_period: 5 * DAYS, - confirm_period: DAYS, - min_enactment_period: 12 * HOURS, - min_approval: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(50), - ceil: Perbill::from_percent(75), - }, - min_support: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(2), - ceil: Perbill::from_percent(10), - }, - }, - }, - pallet_referenda::Track { - id: 4, - info: pallet_referenda::TrackInfo { - name: str_array("treasury_big_spender"), - max_deciding: 2, - decision_deposit: 500 * UNIT, - prepare_period: DAYS, - decision_period: 7 * DAYS, - confirm_period: 2 * DAYS, - min_enactment_period: 12 * HOURS, - min_approval: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(65), - ceil: Perbill::from_percent(85), - }, - min_support: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(5), - ceil: Perbill::from_percent(15), - }, - }, - }, - pallet_referenda::Track { - id: 5, - info: pallet_referenda::TrackInfo { - name: str_array("treasury_treasurer"), - max_deciding: 1, - decision_deposit: 1000 * UNIT, - prepare_period: 2 * DAYS, - decision_period: 14 * DAYS, - confirm_period: 4 * DAYS, - min_enactment_period: 24 * HOURS, - min_approval: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(75), - ceil: Perbill::from_percent(100), - }, - min_support: pallet_referenda::Curve::LinearDecreasing { - length: Perbill::from_percent(100), - floor: Perbill::from_percent(10), - ceil: Perbill::from_percent(25), - }, }, }, ] @@ -288,13 +198,13 @@ impl pallet_referenda::TracksInfo for CommunityTracksInfo ) -> impl Iterator>> { // Static tracks with production values lazy_static! { - static ref STATIC_TRACKS: [pallet_referenda::Track; 6] = + static ref STATIC_TRACKS: [pallet_referenda::Track; 2] = CommunityTracksInfo::create_community_tracks(); } // Test tracks with fast governance timing lazy_static! { - static ref TEST_TRACKS: [pallet_referenda::Track; 6] = { + static ref TEST_TRACKS: [pallet_referenda::Track; 2] = { let base_tracks = CommunityTracksInfo::create_community_tracks(); let mut test_tracks = base_tracks.clone(); @@ -318,16 +228,6 @@ impl pallet_referenda::TracksInfo for CommunityTracksInfo } fn track_for(id: &Self::RuntimeOrigin) -> Result { - // Check for specific custom origins first (Spender/Treasurer types) - if let crate::OriginCaller::Origins(custom_origin) = id { - match custom_origin { - pallet_custom_origins::Origin::SmallSpender => return Ok(2), - pallet_custom_origins::Origin::MediumSpender => return Ok(3), - pallet_custom_origins::Origin::BigSpender => return Ok(4), - pallet_custom_origins::Origin::Treasurer => return Ok(5), - } - } - // Check for system origins (like None for track 1, Root for track 0) if let Some(system_origin) = id.as_system_ref() { match system_origin { @@ -549,76 +449,3 @@ where } pub type RootOrMemberForTechReferendaOrigin = RootOrMemberForTechReferendaOriginImpl; - -// Helper structs for pallet_treasury::Config -pub struct RuntimeNativeBalanceConverter; -impl ConversionFromAssetBalance for RuntimeNativeBalanceConverter { - type Error = sp_runtime::DispatchError; - fn from_asset_balance( - balance: Balance, - _asset_kind: (), - ) -> Result { - Ok(balance) - } - - #[cfg(feature = "runtime-benchmarks")] - fn ensure_successful(_asset_kind: ()) { - // For an identity conversion with AssetKind = (), there are no - // external conditions to set up for the conversion itself to succeed. - // The from_asset_balance call is trivial. - } -} - -pub struct RuntimeNativePaymaster; -impl Pay for RuntimeNativePaymaster { - type AssetKind = (); - type Balance = crate::Balance; - type Beneficiary = crate::AccountId; - type Id = u32; // Simple payment ID - type Error = sp_runtime::DispatchError; - - fn pay( - who: &Self::Beneficiary, - _asset_kind: Self::AssetKind, - amount: Self::Balance, - ) -> Result { - let treasury_account = TreasuryPalletId::get().into_account_truncating(); - >::transfer( - &treasury_account, - who, - amount, - frame_support::traits::ExistenceRequirement::AllowDeath, - )?; - Ok(0_u32) // Dummy ID - } - - fn check_payment(id: Self::Id) -> PaymentStatus { - if id == 0_u32 { - PaymentStatus::Success - } else { - PaymentStatus::Unknown - } - } - - #[cfg(feature = "runtime-benchmarks")] - fn ensure_successful( - _who: &Self::Beneficiary, - _asset_kind: Self::AssetKind, - amount: Self::Balance, - ) { - let treasury_account = TreasuryPalletId::get().into_account_truncating(); - let current_balance = crate::Balances::free_balance(&treasury_account); - if current_balance < amount { - let missing = amount - current_balance; - // Assuming deposit_creating is infallible or panics on error internally, returning - // PositiveImbalance directly. - let _ = crate::Balances::deposit_creating(&treasury_account, missing); - } - } - - #[cfg(feature = "runtime-benchmarks")] - fn ensure_concluded(_id: Self::Id) { - // For this synchronous paymaster, payment is concluded once pay returns. - // No further action needed for ensure_concluded. - } -} diff --git a/runtime/src/governance/mod.rs b/runtime/src/governance/mod.rs index 3e1290e4..7f5d5d09 100644 --- a/runtime/src/governance/mod.rs +++ b/runtime/src/governance/mod.rs @@ -1,3 +1 @@ pub mod definitions; -pub mod origins; -pub use origins::*; diff --git a/runtime/src/governance/origins.rs b/runtime/src/governance/origins.rs deleted file mode 100644 index 21d96cb3..00000000 --- a/runtime/src/governance/origins.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Custom origins for governance interventions. - -// TODO: this needs an upstream fix, since #[pallet::pallet] macro expansion contains `expect()` -#![allow(clippy::expect_used)] -#[frame_support::pallet] -pub mod pallet_custom_origins { - use crate::{Balance, UNIT}; - use frame_support::pallet_prelude::*; - - #[pallet::config] - pub trait Config: frame_system::Config {} - - #[pallet::pallet] - pub struct Pallet(_); - - #[derive( - PartialEq, - Eq, - Clone, - MaxEncodedLen, - Encode, - Decode, - TypeInfo, - RuntimeDebug, - DecodeWithMemTracking, - )] - #[pallet::origin] - pub enum Origin { - Treasurer, - SmallSpender, - MediumSpender, - BigSpender, - } - - macro_rules! decl_unit_ensures { - ( $name:ident: $success_type:ty = $success:expr ) => { - pub struct $name; - impl> + From> - EnsureOrigin for $name - { - type Success = $success_type; - fn try_origin(o: O) -> Result { - o.into().and_then(|o| match o { - Origin::$name => Ok($success), - r => Err(O::from(r)), - }) - } - #[cfg(feature = "runtime-benchmarks")] - fn try_successful_origin() -> Result { - Ok(O::from(Origin::$name)) - } - } - }; - ( $name:ident ) => { decl_unit_ensures! { $name : () = () } }; - ( $name:ident: $success_type:ty = $success:expr, $( $rest:tt )* ) => { - decl_unit_ensures! { $name: $success_type = $success } - decl_unit_ensures! { $( $rest )* } - }; - ( $name:ident, $( $rest:tt )* ) => { - decl_unit_ensures! { $name } - decl_unit_ensures! { $( $rest )* } - }; - () => {} - } - decl_unit_ensures!(Treasurer,); - - macro_rules! decl_ensure { - ( - $vis:vis type $name:ident: EnsureOrigin { - $( $item:ident = $success:expr, )* - } - ) => { - $vis struct $name; - impl> + From> - EnsureOrigin for $name - { - type Success = $success_type; - fn try_origin(o: O) -> Result { - o.into().and_then(|o| match o { - $( - Origin::$item => Ok($success), - )* - //r => Err(O::from(r)), - }) - } - #[cfg(feature = "runtime-benchmarks")] - fn try_successful_origin() -> Result { - // By convention the more privileged origins go later, so for greatest chance - // of success, we want the last one. - let _result: Result = Err(()); - $( - let _result: Result = Ok(O::from(Origin::$item)); - )* - _result - } - } - } - } - - decl_ensure! { - pub type Spender: EnsureOrigin { - SmallSpender = 100 * UNIT, - MediumSpender = 1_000 * UNIT, - BigSpender = 10_000 * UNIT, - Treasurer = 100_000 * UNIT, - } - } -} - -// Re-export the pallet and its types -pub use pallet_custom_origins::{Origin, Spender, Treasurer}; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d1ef9c8f..097a7aba 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -32,7 +32,6 @@ pub mod genesis_config_presets; pub mod governance; pub mod transaction_extensions; -use crate::governance::pallet_custom_origins; use qp_poseidon::PoseidonHasher; /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know /// the specifics of the runtime. They can then be made to be agnostic over specific formats @@ -240,10 +239,7 @@ mod runtime { pub type TechReferenda = pallet_referenda::Pallet; #[runtime::pallet_index(18)] - pub type TreasuryPallet = pallet_treasury; - - #[runtime::pallet_index(19)] - pub type Origins = pallet_custom_origins; + pub type Treasury = pallet_treasury; #[runtime::pallet_index(20)] pub type Recovery = pallet_recovery; diff --git a/runtime/tests/common.rs b/runtime/tests/common.rs index 452351ae..af54683b 100644 --- a/runtime/tests/common.rs +++ b/runtime/tests/common.rs @@ -1,10 +1,7 @@ -use frame_support::{ - traits::{Currency, OnFinalize, OnInitialize}, - PalletId, -}; -use quantus_runtime::{Balances, Runtime, System, UNIT}; +use frame_support::traits::{Currency, OnFinalize, OnInitialize}; +use quantus_runtime::{Balances, Runtime, System, Treasury, UNIT}; use sp_core::crypto::AccountId32; -use sp_runtime::{traits::AccountIdConversion, BuildStorage}; +use sp_runtime::BuildStorage; pub struct TestCommons; @@ -17,7 +14,14 @@ impl TestCommons { // Create a test externality pub fn new_test_ext() -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_treasury::GenesisConfig:: { + treasury_account: Self::account_id(42), + treasury_portion: 50, + } + .assimilate_storage(&mut t) + .unwrap(); let mut ext = sp_io::TestExternalities::new(t); @@ -27,10 +31,7 @@ impl TestCommons { Balances::make_free_balance_be(&Self::account_id(2), 1000 * UNIT); Balances::make_free_balance_be(&Self::account_id(3), 1000 * UNIT); Balances::make_free_balance_be(&Self::account_id(4), 1000 * UNIT); - // Set up treasury account for volume fee collection - let treasury_pallet_id = PalletId(*b"py/trsry"); - let treasury_account = treasury_pallet_id.into_account_truncating(); - Balances::make_free_balance_be(&treasury_account, 1000 * UNIT); + Balances::make_free_balance_be(&Treasury::account_id(), 1000 * UNIT); }); ext diff --git a/runtime/tests/governance/mod.rs b/runtime/tests/governance/mod.rs index 1261a867..c58fa2da 100644 --- a/runtime/tests/governance/mod.rs +++ b/runtime/tests/governance/mod.rs @@ -1,4 +1,3 @@ pub mod engine; pub mod logic; pub mod tech_collective; -pub mod treasury; diff --git a/runtime/tests/governance/tech_collective.rs b/runtime/tests/governance/tech_collective.rs index bc598e06..af82bc52 100644 --- a/runtime/tests/governance/tech_collective.rs +++ b/runtime/tests/governance/tech_collective.rs @@ -8,14 +8,11 @@ mod tests { use quantus_runtime::configs::TechReferendaInstance; use quantus_runtime::{ - Balances, OriginCaller, Preimage, Runtime, RuntimeCall, RuntimeOrigin, System, - TechCollective, TechReferenda, UNIT, + Balances, OriginCaller, Preimage, Runtime, RuntimeCall, RuntimeOrigin, TechCollective, + TechReferenda, UNIT, }; - use sp_runtime::{ - traits::{AccountIdConversion, Hash, StaticLookup}, - MultiAddress, - }; + use sp_runtime::{traits::Hash, MultiAddress}; const TRACK_ID: u16 = 0; @@ -1285,391 +1282,4 @@ mod tests { ); }); } - - #[test] - fn test_tech_collective_treasury_spend_with_root_origin() { - TestCommons::new_test_ext().execute_with(|| { - println!("DEBUG: Test starting at block: {}", System::block_number()); - // Define test accounts - let tech_member = TestCommons::account_id(1); - let beneficiary = TestCommons::account_id(2); - let treasury_pot: quantus_runtime::AccountId = - quantus_runtime::configs::TreasuryPalletId::get().into_account_truncating(); - - // Setup account balances - Balances::make_free_balance_be(&tech_member, 10_000 * UNIT); - Balances::make_free_balance_be(&beneficiary, 100 * UNIT); - - // Fund treasury - let initial_treasury_balance = 1000 * UNIT; - assert_ok!(Balances::force_set_balance( - frame_system::RawOrigin::Root.into(), - ::Lookup::unlookup(treasury_pot.clone()), - initial_treasury_balance - )); - - // Add tech_member to TechCollective - assert_ok!(TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::from(tech_member.clone()) - )); - - // Create a treasury spend proposal - let spend_amount = 1000 * UNIT; - let treasury_spend = - RuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount, - beneficiary: Box::new(::Lookup::unlookup( - beneficiary.clone(), - )), - valid_from: None, - }); - - // Store preimage - let encoded_proposal = treasury_spend.encode(); - let preimage_hash = ::Hashing::hash(&encoded_proposal); - assert_ok!(Preimage::note_preimage( - RuntimeOrigin::signed(tech_member.clone()), - encoded_proposal.clone() - )); - - // Submit referendum with Root origin - let bounded_call = frame_support::traits::Bounded::Lookup { - hash: preimage_hash, - len: encoded_proposal.len() as u32, - }; - - // This should succeed as Tech Collective members can create referenda with Root origin - assert_ok!(TechReferenda::submit( - RuntimeOrigin::signed(tech_member.clone()), - Box::new(OriginCaller::system(frame_system::RawOrigin::Root)), - bounded_call, - frame_support::traits::schedule::DispatchTime::After(0u32) - )); - - let referendum_index = 0; - - // Place decision deposit - assert_ok!(TechReferenda::place_decision_deposit( - RuntimeOrigin::signed(tech_member.clone()), - referendum_index - )); - - // Get track info - let track_info = - >::Tracks::info( - TRACK_ID, - ) - .expect("Track info should exist for the given TRACK_ID"); - - println!( - "DEBUG: Track timing - prepare: {}, decision: {}, confirm: {}, enactment: {}", - track_info.prepare_period, - track_info.decision_period, - track_info.confirm_period, - track_info.min_enactment_period - ); - - // Run to just after prepare period to trigger deciding phase - TestCommons::run_to_block(track_info.prepare_period + 1); - - // Vote AYE - assert_ok!(TechCollective::vote( - RuntimeOrigin::signed(tech_member.clone()), - referendum_index, - true // AYE vote - )); - - // Wait for the referendum to be approved (but not yet enacted) - let approval_block = track_info.prepare_period + - track_info.decision_period + - track_info.confirm_period + - 5; - - println!("DEBUG: Waiting for referendum approval at block: {}", approval_block); - TestCommons::run_to_block(approval_block); - println!( - "DEBUG: After referendum approval - current block: {}", - System::block_number() - ); - - // Check referendum outcome - let referendum_info = pallet_referenda::ReferendumInfoFor::< - Runtime, - TechReferendaInstance, - >::get(referendum_index) - .expect("Referendum info should exist"); - - println!( - "DEBUG: Referendum final state: {:?}", - matches!(referendum_info, pallet_referenda::ReferendumInfo::Approved(_, _, _)) - ); - - // Verify the referendum was approved - assert!( - matches!(referendum_info, pallet_referenda::ReferendumInfo::Approved(_, _, _)), - "Treasury spend referendum should be approved" - ); - - // The treasury spend is created during the referendum process, so let's monitor for it - let spend_index = 0; - let max_wait_block = approval_block + track_info.min_enactment_period + 20; - let mut current_poll_block = System::block_number(); - - println!( - "DEBUG: Starting to poll for treasury spend creation from block: {}", - current_poll_block - ); - - // Poll for treasury spend creation - while current_poll_block <= max_wait_block { - if pallet_treasury::Spends::::get(spend_index).is_some() { - println!("DEBUG: Treasury spend detected at block: {}", System::block_number()); - break; - } - - // Advance 2 blocks and check again - current_poll_block += 2; - TestCommons::run_to_block(current_poll_block); - } - - // Verify treasury spend exists and get timing info - if let Some(_spend_info) = pallet_treasury::Spends::::get(spend_index) { - println!("DEBUG: Treasury spend found at block: {}", System::block_number()); - - // Find the exact creation details from events - let events = System::events(); - for event_record in events.iter().rev() { - if let quantus_runtime::RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::AssetSpendApproved { - valid_from, expire_at, .. - }, - ) = &event_record.event - { - println!( - "DEBUG: Found treasury spend - valid_from: {}, expire_at: {}", - valid_from, expire_at - ); - println!( - "DEBUG: Current block: {}, blocks until expiry: {}", - System::block_number(), - expire_at.saturating_sub(System::block_number()) - ); - - // Check if we still have time to claim it - if System::block_number() >= *expire_at { - panic!( - "Treasury spend already expired! Current: {}, Expiry: {}", - System::block_number(), - expire_at - ); - } - break; - } - } - } else { - panic!("Treasury spend should exist by block {}", max_wait_block); - } - - // Execute payout - println!("DEBUG: About to attempt payout at block: {}", System::block_number()); - println!("DEBUG: Payout attempt for spend_index: {}", spend_index); - - let payout_result = pallet_treasury::Pallet::::payout( - RuntimeOrigin::signed(beneficiary.clone()), - spend_index, - ); - - match &payout_result { - Ok(_) => println!("DEBUG: Payout succeeded!"), - Err(e) => println!("DEBUG: Payout failed with error: {:?}", e), - } - - assert_ok!(payout_result); - - // Verify the beneficiary received the funds - let beneficiary_balance = Balances::free_balance(&beneficiary); - assert_eq!( - beneficiary_balance, - 100 * UNIT + spend_amount, - "Beneficiary should receive the treasury spend amount" - ); - }); - } - - /// Test that Tech Collective can spend from treasury using Root origin - #[test] - fn tech_collective_can_spend_with_root_origin() { - use quantus_runtime::{TreasuryPallet, EXISTENTIAL_DEPOSIT}; - use sp_runtime::traits::AccountIdConversion; - - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let proposer = TestCommons::account_id(1); - let voter = TestCommons::account_id(2); - let beneficiary = TestCommons::account_id(10); - - // Setup: Add proposer and voter as Tech Collective members - Balances::make_free_balance_be(&proposer, 5000 * UNIT); - assert_ok!(TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::from(proposer.clone()) - )); - - Balances::make_free_balance_be(&voter, 5000 * UNIT); - assert_ok!(TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::from(voter.clone()) - )); - - // Fund treasury - let treasury_account = - quantus_runtime::configs::TreasuryPalletId::get().into_account_truncating(); - Balances::make_free_balance_be(&treasury_account, 500_000 * UNIT); - let initial_treasury_balance = Balances::free_balance(&treasury_account); - - // Setup beneficiary with existential deposit - Balances::make_free_balance_be(&beneficiary, EXISTENTIAL_DEPOSIT); - let initial_beneficiary_balance = Balances::free_balance(&beneficiary); - - // Amount to spend (large amount to demonstrate Root has no limit) - let spend_amount = 200_000 * UNIT; - - // Create the treasury spend call - let beneficiary_lookup = - ::Lookup::unlookup(beneficiary.clone()); - let treasury_spend_call = RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount: spend_amount, - beneficiary: Box::new(beneficiary_lookup), - valid_from: None, - }); - - // Encode and submit preimage - let encoded_call = treasury_spend_call.encode(); - let preimage_hash = ::Hashing::hash(&encoded_call); - assert_ok!(Preimage::note_preimage( - RuntimeOrigin::signed(proposer.clone()), - encoded_call.clone() - )); - - let bounded_call = frame_support::traits::Bounded::Lookup { - hash: preimage_hash, - len: encoded_call.len() as u32, - }; - - // Submit referendum with Root origin (uses track 0) - assert_ok!(TechReferenda::submit( - RuntimeOrigin::signed(proposer.clone()), - Box::new(OriginCaller::system(frame_system::RawOrigin::Root)), - bounded_call, - frame_support::traits::schedule::DispatchTime::After(0u32) - )); - - let referendum_index = - pallet_referenda::ReferendumCount::::get() - 1; - - // Verify the referendum is on track 0 (Root track) - let referendum_info = pallet_referenda::ReferendumInfoFor::< - Runtime, - TechReferendaInstance, - >::get(referendum_index) - .expect("Referendum should exist"); - - if let pallet_referenda::ReferendumInfo::Ongoing(status) = referendum_info { - assert_eq!(status.track, 0, "Referendum should be on track 0 (Root track)"); - } else { - panic!("Referendum should be in Ongoing state"); - } - - // Place decision deposit - assert_ok!(TechReferenda::place_decision_deposit( - RuntimeOrigin::signed(proposer.clone()), - referendum_index - )); - - // Vote in favor - assert_ok!(TechCollective::vote( - RuntimeOrigin::signed(voter.clone()), - referendum_index, - true - )); - - // Get track info for track 0 - let track_info = - >::Tracks::info(0) - .expect("Track 0 should exist"); - - // Advance through governance periods - let prepare_period = track_info.prepare_period; - let decision_period = track_info.decision_period; - let confirm_period = track_info.confirm_period; - - println!( - "Track 0 periods: prepare={}, decision={}, confirm={}", - prepare_period, decision_period, confirm_period - ); - - // Run to the end of all voting periods (prepare + decision + confirm) - let total_voting_period = prepare_period + decision_period + confirm_period + 1; - TestCommons::run_to_block(System::block_number() + total_voting_period); - - // Wait for enactment - let enactment_period = track_info.min_enactment_period; - TestCommons::run_to_block(System::block_number() + enactment_period + 5); - - // Verify referendum was approved - let final_referendum_info = pallet_referenda::ReferendumInfoFor::< - Runtime, - TechReferendaInstance, - >::get(referendum_index) - .expect("Referendum should exist"); - - assert!( - matches!( - final_referendum_info, - pallet_referenda::ReferendumInfo::Approved(_, _, _) - ), - "Referendum should be approved" - ); - - // Wait for treasury spend to be created (after enactment + scheduler execution) - let spend_index = 0; - let max_wait_blocks = 20; - TestCommons::run_to_block(System::block_number() + max_wait_blocks); - - // Verify treasury spend was created - assert!( - pallet_treasury::Spends::::get(spend_index).is_some(), - "Treasury spend should be created within {} blocks after enactment", - max_wait_blocks - ); - - // Execute payout - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(beneficiary.clone()), - spend_index - )); - - // Verify balances - let final_beneficiary_balance = Balances::free_balance(&beneficiary); - assert_eq!( - final_beneficiary_balance, - initial_beneficiary_balance + spend_amount, - "Beneficiary should receive the treasury spend amount" - ); - - let final_treasury_balance = Balances::free_balance(&treasury_account); - assert_eq!( - final_treasury_balance, - initial_treasury_balance - spend_amount, - "Treasury should be reduced by the spend amount" - ); - - println!( - "✅ Tech Collective successfully spent {} UNIT from treasury via Root origin (track 0)", - spend_amount / UNIT - ); - }); - } } diff --git a/runtime/tests/governance/treasury.rs b/runtime/tests/governance/treasury.rs deleted file mode 100644 index 3b4fe933..00000000 --- a/runtime/tests/governance/treasury.rs +++ /dev/null @@ -1,1241 +0,0 @@ -#[cfg(test)] -mod tests { - // use common::TestCommons; - // Imports from the runtime crate - use quantus_runtime::{ - configs::{TreasuryPalletId, TreasuryPayoutPeriod}, - governance::pallet_custom_origins, - }; - use quantus_runtime::{ - AccountId, - Balance, - Balances, - BlockNumber, - OriginCaller, // Added OriginCaller - Runtime, - RuntimeCall, - RuntimeEvent, - RuntimeOrigin, - System, - TreasuryPallet, - EXISTENTIAL_DEPOSIT, // DAYS, HOURS are unused, consider removing if not needed elsewhere - MICRO_UNIT, - UNIT, - }; - // Additional pallets for referenda tests - use quantus_runtime::{ConvictionVoting, Preimage, Referenda, Scheduler}; - - // Codec & Hashing - use codec::Encode; - use sp_runtime::traits::Hash as RuntimeTraitHash; - - // Frame and Substrate traits & types - use crate::common::TestCommons; - use frame_support::{ - assert_ok, - pallet_prelude::Hooks, // For Scheduler hooks - traits::{ - schedule::DispatchTime as ScheduleDispatchTime, - Bounded, // Added Bounded - Currency, - PreimageProvider, // Added PreimageProvider - UnfilteredDispatchable, - }, - }; - use frame_system::RawOrigin; - use pallet_referenda::{self, ReferendumIndex, TracksInfo}; - use pallet_treasury; - use quantus_runtime::governance::definitions::CommunityTracksInfo; - use sp_runtime::{ - traits::{AccountIdConversion, StaticLookup}, - BuildStorage, - }; - - // Type aliases - type TestRuntimeCall = RuntimeCall; - type TestRuntimeOrigin = ::RuntimeOrigin; // This is available if RuntimeOrigin direct import is an issue - - // Test specific constants - const BENEFICIARY_ACCOUNT_ID: AccountId = AccountId::new([1u8; 32]); // Example AccountId - const PROPOSER_ACCOUNT_ID: AccountId = AccountId::new([2u8; 32]); // For referendum proposer - const VOTER_ACCOUNT_ID: AccountId = AccountId::new([3u8; 32]); // For referendum voter - - // Minimal ExtBuilder for setting up storage - // In a real project, this would likely be more sophisticated and in common.rs - pub struct ExtBuilder { - balances: Vec<(AccountId, Balance)>, - treasury_genesis: bool, - } - - impl Default for ExtBuilder { - fn default() -> Self { - Self { balances: vec![], treasury_genesis: true } - } - } - - impl ExtBuilder { - pub fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { - self.balances = balances; - self - } - - #[allow(dead_code)] - pub fn without_treasury_genesis(mut self) -> Self { - self.treasury_genesis = false; - self - } - - pub fn build(self) -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - - pallet_balances::GenesisConfig:: { balances: self.balances } - .assimilate_storage(&mut t) - .unwrap(); - - // Pallet Treasury genesis (optional, as we fund it manually) - // If your pallet_treasury::GenesisConfig needs setup, do it here. - // For this test, we manually fund the treasury account. - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } - } - - // Helper function to get treasury account ID - fn treasury_account_id() -> AccountId { - TreasuryPalletId::get().into_account_truncating() - } - - /// Tests the basic treasury spend flow: - /// 1. Root proposes a spend. - /// 2. Spend is approved. - /// 3. Beneficiary payouts the spend. - /// 4. Spend status is checked and spend is removed. - #[test] - fn propose_and_payout_spend_as_root_works() { - ExtBuilder::default().with_balances(vec![]).build().execute_with(|| { - let beneficiary_lookup_source = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let treasury_pot = treasury_account_id(); - - let initial_treasury_balance = 1000 * UNIT; - let spend_amount = 100 * UNIT; - - let _ = >::deposit_creating( - &treasury_pot, - initial_treasury_balance, - ); - assert_eq!(Balances::free_balance(&treasury_pot), initial_treasury_balance); - let initial_beneficiary_balance = Balances::free_balance(&BENEFICIARY_ACCOUNT_ID); - - let call = TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount, - beneficiary: Box::new(beneficiary_lookup_source.clone()), - valid_from: None, - }); - - let dispatch_result = call.dispatch_bypass_filter(RawOrigin::Root.into()); - assert_ok!(dispatch_result); - - let spend_index = 0; - - System::assert_last_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::AssetSpendApproved { - index: spend_index, - asset_kind: (), - amount: spend_amount, - beneficiary: BENEFICIARY_ACCOUNT_ID, - valid_from: System::block_number(), - expire_at: System::block_number() + TreasuryPayoutPeriod::get(), - }, - )); - - assert!( - pallet_treasury::Spends::::get(spend_index).is_some(), - "Spend should exist in storage" - ); - - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index - )); - - System::assert_has_event(RuntimeEvent::TreasuryPallet(pallet_treasury::Event::Paid { - index: spend_index, - payment_id: 0, - })); - - assert_eq!( - Balances::free_balance(&treasury_pot), - initial_treasury_balance - spend_amount - ); - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance + spend_amount - ); - - assert_ok!(TreasuryPallet::check_status( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index - )); - - System::assert_last_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::SpendProcessed { index: spend_index }, - )); - - assert!( - pallet_treasury::Spends::::get(spend_index).is_none(), - "Spend should be removed after check_status" - ); - }); - } - - /// Tests treasury spend functionality using custom origins (SmallSpender, MediumSpender). - /// Verifies that: - /// 1. SmallSpender can spend within its limit (0.75 UNIT) - full lifecycle test - /// 2. SmallSpender can spend exactly at its limit (100 UNIT) - boundary test - /// 3. SmallSpender cannot spend above its limit (101 UNIT) - /// 4. MediumSpender can spend amounts that SmallSpender cannot (500 UNIT) - #[test] - fn propose_spend_as_custom_origin_works() { - ExtBuilder::default() - .with_balances(vec![(BENEFICIARY_ACCOUNT_ID, EXISTENTIAL_DEPOSIT)]) - .build() - .execute_with(|| { - let beneficiary_lookup_source = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let treasury_pot = treasury_account_id(); - let small_spender_origin: TestRuntimeOrigin = - pallet_custom_origins::Origin::SmallSpender.into(); - - let initial_treasury_balance = 10_000 * UNIT; - let _ = >::deposit_creating( - &treasury_pot, - initial_treasury_balance, - ); - assert_eq!(Balances::free_balance(&treasury_pot), initial_treasury_balance); - let initial_beneficiary_balance = Balances::free_balance(&BENEFICIARY_ACCOUNT_ID); - assert_eq!(initial_beneficiary_balance, EXISTENTIAL_DEPOSIT); - - // Test 1: SmallSpender spends within limit (0.75 UNIT) - full lifecycle - let spend_amount_within_limit = 250 * 3 * MICRO_UNIT; - let call_within_limit = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount_within_limit, - beneficiary: Box::new(beneficiary_lookup_source.clone()), - valid_from: None, - }); - - assert_ok!(call_within_limit - .clone() - .dispatch_bypass_filter(small_spender_origin.clone())); - - let spend_index_within_limit = 0; - System::assert_last_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::AssetSpendApproved { - index: spend_index_within_limit, - asset_kind: (), - amount: spend_amount_within_limit, - beneficiary: BENEFICIARY_ACCOUNT_ID, - valid_from: System::block_number(), - expire_at: System::block_number() + TreasuryPayoutPeriod::get(), - }, - )); - assert!(pallet_treasury::Spends::::get(spend_index_within_limit).is_some()); - - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index_within_limit - )); - System::assert_has_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::Paid { index: spend_index_within_limit, payment_id: 0 }, - )); - - assert_ok!(TreasuryPallet::check_status( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index_within_limit - )); - System::assert_last_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::SpendProcessed { index: spend_index_within_limit }, - )); - assert!(pallet_treasury::Spends::::get(spend_index_within_limit).is_none()); - - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance + spend_amount_within_limit - ); - assert_eq!( - Balances::free_balance(&treasury_pot), - initial_treasury_balance - spend_amount_within_limit - ); - - // Test 2: SmallSpender can spend exactly at its limit (100 UNIT) - boundary test - let spend_amount_at_limit = 100 * UNIT; - let call_at_limit = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount_at_limit, - beneficiary: Box::new(beneficiary_lookup_source.clone()), - valid_from: None, - }); - assert_ok!(call_at_limit.dispatch_bypass_filter(small_spender_origin.clone())); - assert!( - pallet_treasury::Spends::::get(1).is_some(), - "Spend at exact limit should be created" - ); - - // Test 3: SmallSpender cannot spend above its limit (101 UNIT) - let spend_amount_above_limit = 101 * UNIT; - let call_above_limit = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount_above_limit, - beneficiary: Box::new(beneficiary_lookup_source.clone()), - valid_from: None, - }); - - let dispatch_result_above_limit = - call_above_limit.dispatch_bypass_filter(small_spender_origin); - assert!( - dispatch_result_above_limit.is_err(), - "Dispatch should fail for amount above SmallSpender limit" - ); - - assert!( - pallet_treasury::Spends::::get(2).is_none(), - "No spend should be created for the failed attempt" - ); - - // Test 4: MediumSpender can spend what SmallSpender cannot (500 UNIT) - let medium_spender_origin: TestRuntimeOrigin = - pallet_custom_origins::Origin::MediumSpender.into(); - let spend_amount_medium = 500 * UNIT; - let call_medium = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount_medium, - beneficiary: Box::new(beneficiary_lookup_source.clone()), - valid_from: None, - }); - assert_ok!(call_medium.dispatch_bypass_filter(medium_spender_origin)); - assert!( - pallet_treasury::Spends::::get(2).is_some(), - "MediumSpender should be able to create a spend above SmallSpender's limit" - ); - }); - } - - /// Tests the expiry of a treasury spend proposal. - /// 1. Root approves a spend. - /// 2. Time is advanced beyond the PayoutPeriod. - /// 3. Attempting to payout the expired spend should fail. - /// 4. `check_status` is called to process the expired spend. - /// 5. Spend should be removed from storage. - #[test] - fn treasury_spend_proposal_expires_if_not_paid_out() { - use crate::common::TestCommons; - - TestCommons::new_fast_governance_test_ext().execute_with(|| { - // Set up balances after externality creation - let _ = Balances::force_set_balance( - RawOrigin::Root.into(), - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID), - EXISTENTIAL_DEPOSIT - ); - let _ = Balances::force_set_balance( - RawOrigin::Root.into(), - ::Lookup::unlookup(TreasuryPallet::account_id()), - 1000 * UNIT - ); - System::set_block_number(1); - let beneficiary_lookup = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let initial_beneficiary_balance = Balances::free_balance(&BENEFICIARY_ACCOUNT_ID); - - let spend_amount = 50 * UNIT; - let spend_index = 0; - - // Approve a spend - let call = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount, - beneficiary: Box::new(beneficiary_lookup.clone()), - valid_from: None, - }); - assert_ok!(call.dispatch_bypass_filter(RawOrigin::Root.into())); - - let expected_expiry_block = System::block_number() + TreasuryPayoutPeriod::get(); - System::assert_last_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::AssetSpendApproved { - index: spend_index, - asset_kind: (), - amount: spend_amount, - beneficiary: BENEFICIARY_ACCOUNT_ID, - valid_from: System::block_number(), - expire_at: expected_expiry_block, - }, - )); - assert!(pallet_treasury::Spends::::get(spend_index).is_some()); - - // For fast testing, advance only a small amount instead of the full expiry period - // This test is about expiry behavior, so we'll skip the long wait but preserve the logic - TestCommons::run_to_block(System::block_number() + 50); // Small advance for testing - - // Try to payout (this test is about non-expiry case, so payout should work) - let payout_result = TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index, - ); - // Since we didn't advance to expiry, payout should succeed - assert_ok!(payout_result); - - // Verify balances changed correctly (payout succeeded) - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance + spend_amount - ); - - // Process the spend status after successful payout - assert_ok!(TreasuryPallet::check_status( - RuntimeOrigin::signed(PROPOSER_ACCOUNT_ID), - spend_index - )); - - // Verify the spend is removed after successful payout - assert!( - pallet_treasury::Spends::::get(spend_index).is_none(), - "Spend should be removed after successful payout" - ); - - // Ensure payment event was emitted - let paid_event_found = System::events().iter().any(|event_record| { - matches!( - event_record.event, - RuntimeEvent::TreasuryPallet(pallet_treasury::Event::Paid { index, .. }) if index == spend_index - ) - }); - assert!(paid_event_found, "Paid event should be emitted for successful payout"); - }); - } - - /// Tests treasury spend behavior when funds are insufficient. - /// 1. Treasury is initialized with a small balance. - /// 2. Root proposes a spend greater than the treasury balance. - /// 3. Attempting to payout the spend fails due to insufficient funds. - /// 4. Treasury is topped up. - /// 5. Payout is attempted again and succeeds. - #[test] - fn treasury_spend_insufficient_funds() { - ExtBuilder::default() - .with_balances(vec![ - (BENEFICIARY_ACCOUNT_ID, EXISTENTIAL_DEPOSIT), - (TreasuryPallet::account_id(), 20 * UNIT), // Treasury starts with less than spend amount - ]) - .build() - .execute_with(|| { - System::set_block_number(1); - let beneficiary_lookup = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let treasury_pot = treasury_account_id(); - let initial_treasury_balance = Balances::free_balance(&treasury_pot); - assert_eq!(initial_treasury_balance, 20 * UNIT); - let initial_beneficiary_balance = Balances::free_balance(&BENEFICIARY_ACCOUNT_ID); - - let spend_amount_above_balance = 50 * UNIT; - let spend_index = 0; - - // Propose spend greater than current treasury balance - should be fine - let call_above_balance = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount_above_balance, - beneficiary: Box::new(beneficiary_lookup.clone()), - valid_from: None, - }); - assert_ok!(call_above_balance.dispatch_bypass_filter(RawOrigin::Root.into())); - - // Capture the event and assert specific fields, like index - let captured_event = System::events().pop().expect("Expected an event").event; - if let RuntimeEvent::TreasuryPallet(pallet_treasury::Event::AssetSpendApproved { index, .. }) = captured_event { - assert_eq!(index, spend_index, "Event index mismatch for AssetSpendApproved"); - } else { - panic!("Expected TreasuryPallet::AssetSpendApproved event"); - } - - assert!(pallet_treasury::Spends::::get(spend_index).is_some()); - - // Try to payout the spend when treasury funds are insufficient - TestCommons::run_to_block(System::block_number() + 5); - - let payout_result_insufficient = TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index, - ); - assert!(payout_result_insufficient.is_err(), "Payout with insufficient funds should fail"); - - // Balances should remain unchanged - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance - ); - assert_eq!(Balances::free_balance(&treasury_pot), initial_treasury_balance); - let paid_event_found = System::events().iter().any(|event_record| { - matches!( - event_record.event, - RuntimeEvent::TreasuryPallet(pallet_treasury::Event::Paid { index, .. }) if index == spend_index - ) - }); - assert!(!paid_event_found, "Paid event should not be emitted if funds are insufficient"); - assert!(pallet_treasury::Spends::::get(spend_index).is_some(), "Spend should still exist"); - - // Now, fund the treasury sufficiently - let top_up_amount = 100 * UNIT; - let new_treasury_balance_target = initial_treasury_balance + top_up_amount; - assert_ok!(Balances::force_set_balance( - RawOrigin::Root.into(), - ::Lookup::unlookup(treasury_pot.clone()), - new_treasury_balance_target - )); - assert_eq!(Balances::free_balance(&treasury_pot), new_treasury_balance_target); - - // Try to payout again - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index - )); - System::assert_has_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::Paid { index: spend_index, payment_id: 0 }, - )); - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance + spend_amount_above_balance - ); - assert_eq!( - Balances::free_balance(&treasury_pot), - new_treasury_balance_target - spend_amount_above_balance - ); - - assert_ok!(TreasuryPallet::check_status( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index - )); - assert!(pallet_treasury::Spends::::get(spend_index).is_none()); - }); - } - - /// Tests treasury spend behavior with a `valid_from` field set in the future. - /// 1. Root approves a spend with `valid_from` set to a future block. - /// 2. Attempting to payout before `valid_from` block fails. - /// 3. Time is advanced to `valid_from` block. - /// 4. Payout is attempted again and succeeds. - #[test] - fn treasury_spend_with_valid_from_in_future() { - ExtBuilder::default() - .with_balances(vec![ - (BENEFICIARY_ACCOUNT_ID, EXISTENTIAL_DEPOSIT), - (TreasuryPallet::account_id(), 1000 * UNIT), - ]) - .build() - .execute_with(|| { - System::set_block_number(1); - let beneficiary_lookup = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let treasury_pot = treasury_account_id(); - let initial_treasury_balance = Balances::free_balance(&treasury_pot); - let initial_beneficiary_balance = Balances::free_balance(&BENEFICIARY_ACCOUNT_ID); - - let spend_amount = 50 * UNIT; - let spend_index = 0; - let valid_from_block = System::block_number() + 10; - - let call = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount, - beneficiary: Box::new(beneficiary_lookup.clone()), - valid_from: Some(valid_from_block), - }); - assert_ok!(call.dispatch_bypass_filter(RawOrigin::Root.into())); - - // Capture the event and assert specific fields - let captured_event = System::events().pop().expect("Expected an event").event; - if let RuntimeEvent::TreasuryPallet(pallet_treasury::Event::AssetSpendApproved { index, valid_from, .. }) = captured_event { - assert_eq!(index, spend_index, "Event index mismatch"); - assert_eq!(valid_from, valid_from_block, "Event valid_from mismatch"); - } else { - panic!("Expected TreasuryPallet::AssetSpendApproved event"); - } - - assert!(pallet_treasury::Spends::::get(spend_index).is_some()); - - // Try to payout before valid_from_block - TestCommons::run_to_block(valid_from_block - 1); - let payout_result_before_valid = TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index, - ); - assert!(payout_result_before_valid.is_err(), "Payout before valid_from should fail"); - - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance - ); - let paid_event_found_before = System::events().iter().any(|event_record| { - matches!( - event_record.event, - RuntimeEvent::TreasuryPallet(pallet_treasury::Event::Paid { index, .. }) if index == spend_index - ) - }); - assert!(!paid_event_found_before); - - // Advance to valid_from_block - TestCommons::run_to_block(valid_from_block); - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index - )); - System::assert_has_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::Paid { index: spend_index, payment_id: 0 }, - )); - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance + spend_amount - ); - assert_eq!( - Balances::free_balance(&treasury_pot), - initial_treasury_balance - spend_amount - ); - - assert_ok!(TreasuryPallet::check_status( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index - )); - assert!(pallet_treasury::Spends::::get(spend_index).is_none()); - }); - } - - /// Tests that a treasury spend can be paid out by an account different from the beneficiary. - /// 1. Root approves a spend. - /// 2. A different account (PROPOSER_ACCOUNT_ID) successfully calls payout. - /// 3. Beneficiary receives the funds. - #[test] - fn treasury_spend_payout_by_different_account() { - ExtBuilder::default() - .with_balances(vec![ - (BENEFICIARY_ACCOUNT_ID, EXISTENTIAL_DEPOSIT), - (PROPOSER_ACCOUNT_ID, EXISTENTIAL_DEPOSIT), // Payer account - (TreasuryPallet::account_id(), 1000 * UNIT), - ]) - .build() - .execute_with(|| { - System::set_block_number(1); - let beneficiary_lookup = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let treasury_pot = treasury_account_id(); - let initial_treasury_balance = Balances::free_balance(&treasury_pot); - let initial_beneficiary_balance = Balances::free_balance(&BENEFICIARY_ACCOUNT_ID); - let initial_proposer_balance = Balances::free_balance(&PROPOSER_ACCOUNT_ID); - - let spend_amount = 50 * UNIT; - let spend_index = 0; - - let call = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount, - beneficiary: Box::new(beneficiary_lookup.clone()), - valid_from: None, - }); - assert_ok!(call.dispatch_bypass_filter(RawOrigin::Root.into())); - assert!(pallet_treasury::Spends::::get(spend_index).is_some()); - - // Payout by PROPOSER_ACCOUNT_ID - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(PROPOSER_ACCOUNT_ID), - spend_index - )); - - System::assert_has_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::Paid { index: spend_index, payment_id: 0 }, - )); - - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - initial_beneficiary_balance + spend_amount - ); - assert_eq!( - Balances::free_balance(&treasury_pot), - initial_treasury_balance - spend_amount - ); - // Proposer's balance should be unchanged (ignoring tx fees) - assert_eq!(Balances::free_balance(&PROPOSER_ACCOUNT_ID), initial_proposer_balance); - - assert_ok!(TreasuryPallet::check_status( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), // Can be anyone - spend_index - )); - assert!(pallet_treasury::Spends::::get(spend_index).is_none()); - }); - } - - /// Tests that a treasury spend proposal submitted via a general community referendum track - /// (Track 0) with an incorrect origin for that track (e.g. a regular signed origin instead of - /// the track's specific origin) is not approved and funds are not spent. - /// 1. Proposer submits a treasury spend call as a preimage. - /// 2. Proposer submits this preimage to Referenda Track 0 using their own signed origin (which - /// is not the correct origin for Track 0 governance actions). - /// 3. Voter votes aye. - /// 4. Time is advanced through all referendum phases. - /// 5. Referendum should NOT be confirmed due to origin mismatch. - /// 6. No treasury spend should be approved or paid out. - #[test] - fn treasury_spend_via_community_referendum_origin_mismatch() { - ExtBuilder::default() - .with_balances(vec![ - (PROPOSER_ACCOUNT_ID, 10_000 * UNIT), - (VOTER_ACCOUNT_ID, 10_000 * UNIT), - (BENEFICIARY_ACCOUNT_ID, EXISTENTIAL_DEPOSIT), - ]) - .build() - .execute_with(|| { - let proposal_origin_for_preimage = - RuntimeOrigin::signed(PROPOSER_ACCOUNT_ID.clone()); - let proposal_origin_for_referendum_submission = - RuntimeOrigin::signed(PROPOSER_ACCOUNT_ID.clone()); - let voter_origin = RuntimeOrigin::signed(VOTER_ACCOUNT_ID.clone()); - - let beneficiary_lookup = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let treasury_pot = treasury_account_id(); - - let initial_treasury_balance = 1000 * UNIT; - let _ = >::deposit_creating( - &treasury_pot, - initial_treasury_balance, - ); - assert_eq!(Balances::free_balance(&treasury_pot), initial_treasury_balance); - - let spend_amount = 50 * UNIT; - - let treasury_spend_call = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: spend_amount, - beneficiary: Box::new(beneficiary_lookup.clone()), - valid_from: None, - }); - - let encoded_call = treasury_spend_call.encode(); - assert_ok!(Preimage::note_preimage( - proposal_origin_for_preimage, - encoded_call.clone() - )); - - let preimage_hash = ::Hashing::hash(&encoded_call); - let h256_preimage_hash: sp_core::H256 = preimage_hash; - assert!(Preimage::have_preimage(&h256_preimage_hash)); - - let track_id = 0u16; - type RuntimeTracks = ::Tracks; - - let proposal_for_referenda = - Bounded::Lookup { hash: preimage_hash, len: encoded_call.len() as u32 }; - - assert_ok!(Referenda::submit( - proposal_origin_for_referendum_submission, - Box::new(OriginCaller::system(RawOrigin::Signed(PROPOSER_ACCOUNT_ID.clone()))), - proposal_for_referenda.clone(), - ScheduleDispatchTime::After(1u32) - )); - - let referendum_index: ReferendumIndex = 0; - - let track_info = - >::info(track_id) - .expect("Track info should be available for track 0"); - - System::set_block_number(System::block_number() + track_info.prepare_period); - - assert_ok!(ConvictionVoting::vote( - voter_origin, - referendum_index, - pallet_conviction_voting::AccountVote::Standard { - vote: pallet_conviction_voting::Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::None - }, - balance: Balances::free_balance(&VOTER_ACCOUNT_ID), - } - )); - - let mut current_block = System::block_number(); - current_block += track_info.decision_period; - System::set_block_number(current_block); - current_block += track_info.confirm_period; - System::set_block_number(current_block); - current_block += track_info.min_enactment_period; - current_block += 1; - System::set_block_number(current_block); - - >::on_initialize(System::block_number()); - - // Check that the referendum was not confirmed - let confirmed_event = System::events().iter().find_map(|event_record| { - if let RuntimeEvent::Referenda(pallet_referenda::Event::Confirmed { - index, - tally, - }) = &event_record.event - { - if *index == referendum_index { - Some(tally.clone()) - } else { - None - } - } else { - None - } - }); - assert!( - confirmed_event.is_none(), - "Referendum should not be confirmed with incorrect origin" - ); - - // Check that funds were not spent - assert_eq!(Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), EXISTENTIAL_DEPOSIT); - assert_eq!(Balances::free_balance(&treasury_pot), initial_treasury_balance); - - // Check that there is no AssetSpendApproved event - let spend_approved_event_found = System::events().iter().any(|event_record| { - matches!( - event_record.event, - RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::AssetSpendApproved { .. } - ) - ) - }); - assert!( - !spend_approved_event_found, - "Treasury spend should not have been approved via this referendum track" - ); - }); - } - - /// Tests the successful flow of a treasury spend through a dedicated spender track in - /// referenda. - /// 1. Proposer submits a treasury spend call as a preimage. - /// 2. Proposer submits this preimage to Referenda Track 2 (SmallSpender) using the correct - /// SmallSpender origin. - /// 3. Proposer places the decision deposit. - /// 4. Voter votes aye. - /// 5. Time is advanced through all referendum phases (prepare, decision, confirm, enactment). - /// 6. Referendum should be confirmed and the treasury spend dispatched via scheduler. - /// 7. AssetSpendApproved event should be emitted. - /// 8. Beneficiary successfully payouts the spend. - /// 9. Spend is processed and removed. - #[test] - fn treasury_spend_via_dedicated_spender_track_works() { - const SPEND_AMOUNT: Balance = 200 * MICRO_UNIT; - // Use common::account_id for consistency - let proposer_account_id = TestCommons::account_id(123); - let voter_account_id = TestCommons::account_id(124); - let beneficiary_account_id = TestCommons::account_id(125); - - fn set_balance(account_id: AccountId, balance: u128) { - let _ = Balances::force_set_balance( - RawOrigin::Root.into(), - ::Lookup::unlookup(account_id.clone()), - balance * UNIT, - ); - } - - TestCommons::new_fast_governance_test_ext().execute_with(|| { - // Set up balances after externality creation - massively increased to meet treasury track support requirements - set_balance(proposer_account_id.clone(), 50000); - set_balance(voter_account_id.clone(), 15000000); // Increased to ~15M UNIT to exceed 10M support threshold - set_balance(beneficiary_account_id.clone(), EXISTENTIAL_DEPOSIT); - set_balance(TreasuryPallet::account_id(), 10); // Reduced from 1000 to 10 to keep total issuance reasonable - - System::set_block_number(1); // Start at block 1 - let initial_treasury_balance = TreasuryPallet::pot(); - let initial_beneficiary_balance = Balances::free_balance(&beneficiary_account_id); - let initial_spend_index = 0u32; - - let call_to_spend = RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount: SPEND_AMOUNT, - beneficiary: Box::new(::Lookup::unlookup( - beneficiary_account_id.clone(), - )), - valid_from: None, - }); - - let encoded_call_to_spend = call_to_spend.encode(); - let hash_of_call_to_spend = - ::Hashing::hash(&encoded_call_to_spend); - - assert_ok!(Preimage::note_preimage( - RuntimeOrigin::signed(proposer_account_id.clone()), - encoded_call_to_spend.clone() - )); - System::assert_last_event(RuntimeEvent::Preimage(pallet_preimage::Event::Noted { - hash: hash_of_call_to_spend, - })); - - // Revert to original: Target Track 2 - let proposal_origin_for_track_selection = Box::new(OriginCaller::Origins( - pallet_custom_origins::Origin::SmallSpender, - )); - - let proposal_for_referenda = Bounded::Lookup { - hash: hash_of_call_to_spend, - len: encoded_call_to_spend.len() as u32, - }; - - let track_info_2 = CommunityTracksInfo::info(2).unwrap(); - - let dispatch_time = ScheduleDispatchTime::After(1u32); - const TEST_REFERENDUM_INDEX: ReferendumIndex = 0; - let referendum_index: ReferendumIndex = TEST_REFERENDUM_INDEX; - - assert_ok!(Referenda::submit( - RuntimeOrigin::signed(proposer_account_id.clone()), - proposal_origin_for_track_selection, - proposal_for_referenda.clone(), - dispatch_time - )); - - System::assert_has_event(RuntimeEvent::Referenda( - pallet_referenda::Event::Submitted { - index: referendum_index, - track: 2, - proposal: proposal_for_referenda.clone(), - }, - )); - - assert_ok!(Referenda::place_decision_deposit( - RuntimeOrigin::signed(proposer_account_id.clone()), - referendum_index - )); - - // Start of new block advancement logic using run_to_block - let block_after_decision_deposit = System::block_number(); - - // Advance past prepare_period - let end_of_prepare_period = block_after_decision_deposit + track_info_2.prepare_period; - - TestCommons::run_to_block(end_of_prepare_period); - - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter_account_id.clone()), - referendum_index, - pallet_conviction_voting::AccountVote::Standard { - vote: pallet_conviction_voting::Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked3x - }, - balance: Balances::free_balance(&voter_account_id), - } - )); - let block_vote_cast = System::block_number(); - - // Advance 1 block for scheduler to potentially process vote related actions - let block_for_vote_processing = block_vote_cast + 1; - - TestCommons::run_to_block(block_for_vote_processing); - - // Advance by confirm_period from the block where vote was processed - let block_after_vote_processing = System::block_number(); - let end_of_confirm_period = block_after_vote_processing + track_info_2.confirm_period; - - TestCommons::run_to_block(end_of_confirm_period); - - // Wait for approval phase - let block_after_confirm = System::block_number(); - let approval_period = track_info_2.decision_period / 2; // Half of decision period for approval - let target_approval_block = block_after_confirm + approval_period; - - TestCommons::run_to_block(target_approval_block); - - let confirmed_event = System::events() - .iter() - .find_map(|event_record| { - if let RuntimeEvent::Referenda(pallet_referenda::Event::Confirmed { - index, - tally, - }) = &event_record.event - { - if *index == referendum_index { - Some(tally.clone()) - } else { - None - } - } else { - None - } - }) - .expect("Confirmed event should be present"); - System::assert_has_event(RuntimeEvent::Referenda( - pallet_referenda::Event::Confirmed { - index: referendum_index, - tally: confirmed_event, - }, - )); - - // Advance past min_enactment_period (relative to when enactment can start) - let block_after_approved = System::block_number(); - let target_enactment_block = block_after_approved + track_info_2.min_enactment_period; - TestCommons::run_to_block(target_enactment_block); - - // Add a small buffer for scheduler to pick up and dispatch - let final_check_block = System::block_number() + 5; - TestCommons::run_to_block(final_check_block); - - // Search for any Scheduler::Dispatched event from block 0 onwards - // The event might have been dispatched earlier than our calculation - let current_block = System::block_number(); - - let dispatched_block = System::events().iter().find_map(|event_record| { - if let RuntimeEvent::Scheduler(pallet_scheduler::Event::Dispatched { - task: (qp_scheduler::BlockNumberOrTimestamp::BlockNumber(block), 0), - id: _, - result: Ok(()) - }) = &event_record.event { - Some(*block) - } else { - None - } - }); - - match dispatched_block { - Some(block) => { - println!("✅ Found Scheduler::Dispatched event at block {}", block); - } - None => { - panic!( - "Expected Scheduler::Dispatched event not found anywhere. Current block: {}. Expected range was {}..={}", - current_block, - target_enactment_block, - current_block - ); - } - } - - // Extract the actual AssetSpendApproved event to get dynamic valid_from - let spend_approved_event = System::events() - .iter() - .find_map(|event_record| { - if let RuntimeEvent::TreasuryPallet(pallet_treasury::Event::AssetSpendApproved { - index, - asset_kind: _, - amount: _, - beneficiary: _, - valid_from, - expire_at, - }) = &event_record.event - { - if *index == initial_spend_index { - Some((*valid_from, *expire_at)) - } else { - None - } - } else { - None - } - }) - .expect("AssetSpendApproved event should be present"); - - System::assert_has_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::AssetSpendApproved { - index: initial_spend_index, - asset_kind: (), - amount: SPEND_AMOUNT, - beneficiary: beneficiary_account_id.clone(), - valid_from: spend_approved_event.0, - expire_at: spend_approved_event.1, - }, - )); - - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(beneficiary_account_id.clone()), - initial_spend_index - )); - - System::assert_has_event(RuntimeEvent::TreasuryPallet(pallet_treasury::Event::Paid { - index: initial_spend_index, - payment_id: 0, - })); - - assert_ok!(TreasuryPallet::check_status( - RuntimeOrigin::signed(beneficiary_account_id.clone()), - initial_spend_index - )); - System::assert_has_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::SpendProcessed { - index: initial_spend_index, - }, - )); - assert_eq!( - Balances::free_balance(&beneficiary_account_id), - initial_beneficiary_balance + SPEND_AMOUNT - ); - assert_eq!( - TreasuryPallet::pot(), - initial_treasury_balance - SPEND_AMOUNT - ); - }); - } - - /// Tests that all treasury origins map to unique, non-overlapping track IDs. - /// This ensures there are no collisions in the track system that could lead to - /// incorrect governance behavior or security issues. - #[test] - fn all_treasury_origins_have_unique_tracks() { - use pallet_referenda::TracksInfo; - use quantus_runtime::governance::definitions::CommunityTracksInfo; - - ExtBuilder::default().build().execute_with(|| { - let treasury_origins = [ - Box::new(OriginCaller::Origins(pallet_custom_origins::Origin::SmallSpender)), - Box::new(OriginCaller::Origins(pallet_custom_origins::Origin::MediumSpender)), - Box::new(OriginCaller::Origins(pallet_custom_origins::Origin::BigSpender)), - Box::new(OriginCaller::Origins(pallet_custom_origins::Origin::Treasurer)), - ]; - - let mut track_ids = Vec::new(); - - for origin in treasury_origins.iter() { - let track_id = CommunityTracksInfo::track_for(origin) - .expect("Treasury origin should map to a track"); - - // Verify the track actually exists - assert!( - CommunityTracksInfo::info(track_id).is_some(), - "Track {} should exist in TracksInfo", - track_id - ); - - // Verify uniqueness - assert!( - !track_ids.contains(&track_id), - "Track ID {} is duplicated - this would cause governance conflicts!", - track_id - ); - track_ids.push(track_id); - } - - // Verify we have exactly 4 unique tracks for 4 treasury origins - assert_eq!( - track_ids.len(), - 4, - "Should have exactly 4 unique tracks for treasury origins" - ); - - // Verify no treasury track overlaps with non-treasury tracks (0 and 1) - assert!( - !track_ids.contains(&0), - "Treasury origins should not use track 0 (signed track)" - ); - assert!( - !track_ids.contains(&1), - "Treasury origins should not use track 1 (signaling track)" - ); - }); - } - - /// Tests that changing a spent amount after approval through a specific track - /// would require going through the correct track again. This is a regression test - /// to ensure that track-based permissions are consistently enforced throughout - /// the spent lifecycle. - #[test] - fn treasury_spend_cannot_bypass_track_limits_after_approval() { - ExtBuilder::default() - .with_balances(vec![(BENEFICIARY_ACCOUNT_ID, EXISTENTIAL_DEPOSIT)]) - .build() - .execute_with(|| { - let beneficiary_lookup = - ::Lookup::unlookup(BENEFICIARY_ACCOUNT_ID); - let treasury_pot = treasury_account_id(); - - // Fund treasury - let initial_treasury_balance = 10_000 * UNIT; - let _ = >::deposit_creating( - &treasury_pot, - initial_treasury_balance, - ); - - // SmallSpender approves a small spend - let small_spender_origin: TestRuntimeOrigin = - pallet_custom_origins::Origin::SmallSpender.into(); - let small_amount = 50 * UNIT; - let call_small = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: small_amount, - beneficiary: Box::new(beneficiary_lookup.clone()), - valid_from: None, - }); - assert_ok!(call_small.dispatch_bypass_filter(small_spender_origin)); - - // Verify the spend was created - let spend_index = 0; - assert!( - pallet_treasury::Spends::::get(spend_index).is_some(), - "Spend should exist in storage" - ); - - // Verify the event has the correct amount - System::assert_has_event(RuntimeEvent::TreasuryPallet( - pallet_treasury::Event::AssetSpendApproved { - index: spend_index, - asset_kind: (), - amount: small_amount, - beneficiary: BENEFICIARY_ACCOUNT_ID, - valid_from: System::block_number(), - expire_at: System::block_number() + TreasuryPayoutPeriod::get(), - }, - )); - - // Verify that the spend amount in storage cannot be directly manipulated - // by trying to create another spend that would exceed SmallSpender limits - let small_spender_origin2: TestRuntimeOrigin = - pallet_custom_origins::Origin::SmallSpender.into(); - let large_amount = 200 * UNIT; // This exceeds SmallSpender's 100 UNIT limit - let call_large = - TestRuntimeCall::TreasuryPallet(pallet_treasury::Call::::spend { - asset_kind: Box::new(()), - amount: large_amount, - beneficiary: Box::new(beneficiary_lookup.clone()), - valid_from: None, - }); - assert!( - call_large.dispatch_bypass_filter(small_spender_origin2).is_err(), - "SmallSpender should not be able to approve a spend above their limit" - ); - - // Verify that only the first spend exists and the second was rejected - assert!( - pallet_treasury::Spends::::get(1).is_none(), - "Second spend should not have been created" - ); - - // The first spend should still be intact - verify by successfully paying it out - assert!( - pallet_treasury::Spends::::get(spend_index).is_some(), - "Original spend should still exist" - ); - assert_ok!(TreasuryPallet::payout( - RuntimeOrigin::signed(BENEFICIARY_ACCOUNT_ID), - spend_index - )); - assert_eq!( - Balances::free_balance(&BENEFICIARY_ACCOUNT_ID), - EXISTENTIAL_DEPOSIT + small_amount, - "Beneficiary should receive the original small amount, not manipulated amount" - ); - }); - } -}