From d1a26a12bc5851b7744dc5b42942e9262e5e37e3 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Mon, 12 Jan 2026 09:06:23 +0800 Subject: [PATCH 1/9] feat: Merkle Airdrop - removed --- Cargo.lock | 20 - Cargo.toml | 2 - pallets/merkle-airdrop/Cargo.toml | 63 --- pallets/merkle-airdrop/README.md | 14 - pallets/merkle-airdrop/src/benchmarking.rs | 196 ------- pallets/merkle-airdrop/src/lib.rs | 584 -------------------- pallets/merkle-airdrop/src/mock.rs | 129 ----- pallets/merkle-airdrop/src/tests.rs | 591 --------------------- pallets/merkle-airdrop/src/weights.rs | 193 ------- runtime/Cargo.toml | 3 - runtime/src/benchmarks.rs | 1 - runtime/src/configs/mod.rs | 16 - runtime/src/lib.rs | 4 - 13 files changed, 1816 deletions(-) delete mode 100644 pallets/merkle-airdrop/Cargo.toml delete mode 100644 pallets/merkle-airdrop/README.md delete mode 100644 pallets/merkle-airdrop/src/benchmarking.rs delete mode 100644 pallets/merkle-airdrop/src/lib.rs delete mode 100644 pallets/merkle-airdrop/src/mock.rs delete mode 100644 pallets/merkle-airdrop/src/tests.rs delete mode 100644 pallets/merkle-airdrop/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index 35560863..ef334b1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7119,25 +7119,6 @@ dependencies = [ "sp-runtime", ] -[[package]] -name = "pallet-merkle-airdrop" -version = "0.1.0" -dependencies = [ - "binary-merkle-tree", - "frame-benchmarking", - "frame-support", - "frame-system", - "log", - "pallet-balances 40.0.1", - "pallet-vesting", - "parity-scale-codec", - "scale-info", - "sha2 0.10.9", - "sp-core", - "sp-io", - "sp-runtime", -] - [[package]] name = "pallet-message-queue" version = "44.0.0" @@ -9191,7 +9172,6 @@ dependencies = [ "pallet-assets-holder", "pallet-balances 40.0.1", "pallet-conviction-voting", - "pallet-merkle-airdrop", "pallet-mining-rewards", "pallet-preimage", "pallet-qpow", diff --git a/Cargo.toml b/Cargo.toml index b6c3abde..32a7f406 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ members = [ "miner-api", "node", "pallets/balances", - "pallets/merkle-airdrop", "pallets/mining-rewards", "pallets/qpow", "pallets/reversible-transfers", @@ -131,7 +130,6 @@ zeroize = { version = "1.7.0", default-features = false } # Own dependencies pallet-balances = { path = "./pallets/balances", default-features = false } -pallet-merkle-airdrop = { path = "./pallets/merkle-airdrop", default-features = false } pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = false } pallet-qpow = { path = "./pallets/qpow", default-features = false } pallet-reversible-transfers = { path = "./pallets/reversible-transfers", default-features = false } diff --git a/pallets/merkle-airdrop/Cargo.toml b/pallets/merkle-airdrop/Cargo.toml deleted file mode 100644 index 3c88c4bb..00000000 --- a/pallets/merkle-airdrop/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -authors.workspace = true -description = "A pallet for distributing tokens via Merkle proofs" -edition.workspace = true -homepage.workspace = true -license = "MIT-0" -name = "pallet-merkle-airdrop" -publish = false -repository.workspace = true -version = "0.1.0" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -binary-merkle-tree.workspace = true -codec = { workspace = true, default-features = false, features = ["derive"] } -frame-benchmarking = { optional = true, workspace = true } -frame-support.workspace = true -frame-system.workspace = true -log.workspace = true -pallet-vesting = { workspace = true, optional = true } -scale-info = { workspace = true, default-features = false, features = ["derive"] } -sha2.workspace = true -sp-core.workspace = true -sp-io.workspace = true -sp-runtime.workspace = true - -[dev-dependencies] -pallet-balances.features = ["std"] -pallet-balances.workspace = true -pallet-vesting.workspace = true -sp-core.workspace = true -sp-io.workspace = true -sp-runtime.workspace = true - -[features] -default = ["std"] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "pallet-vesting", -] -std = [ - "binary-merkle-tree/std", - "codec/std", - "frame-benchmarking?/std", - "frame-support/std", - "frame-system/std", - "log/std", - "pallet-balances/std", - "pallet-vesting?/std", - "scale-info/std", - "sha2/std", - "sp-core/std", - "sp-io/std", - "sp-runtime/std", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", -] diff --git a/pallets/merkle-airdrop/README.md b/pallets/merkle-airdrop/README.md deleted file mode 100644 index 2e8495fb..00000000 --- a/pallets/merkle-airdrop/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Merkle Airdrop Pallet - -A Substrate pallet for distributing tokens via Merkle proofs with optional vesting of the airdropped tokens. - -## Testing & Usage - -For testing and interacting with this pallet, please refer to the CLI tool and example in the [resonance-api-client](https://github.com/Quantus-Network/resonance-api-client/blob/master/examples/async/examples/merkle-airdrop-README.md) repository: -- `examples/ac-examples-async/examples/merkle_airdrop_cli.rs` -- `examples/ac-examples-async/examples/merkle_airdrop_cli-README.md` for the documentation - -These tool demonstrates how to: -- Generate Merkle trees and proofs -- Create and fund airdrops -- Claim tokens using proofs diff --git a/pallets/merkle-airdrop/src/benchmarking.rs b/pallets/merkle-airdrop/src/benchmarking.rs deleted file mode 100644 index d965bd8d..00000000 --- a/pallets/merkle-airdrop/src/benchmarking.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Benchmarking setup for pallet-merkle-airdrop - -extern crate alloc; - -use super::*; -use crate::Pallet as MerkleAirdrop; -use frame_benchmarking::v2::*; -use frame_support::BoundedVec; -use frame_system::RawOrigin; -use sp_io::hashing::blake2_256; -use sp_runtime::traits::{Get, Saturating}; - -// Helper function to mirror pallet's Merkle proof verification logic -fn calculate_expected_root_for_benchmark( - initial_leaf_hash: MerkleHash, - proof_elements: &[MerkleHash], -) -> MerkleHash { - let mut computed_hash = initial_leaf_hash; - for proof_element in proof_elements.iter() { - // The comparison logic must match how MerkleHash is ordered in your pallet - if computed_hash.as_ref() < proof_element.as_ref() { - // This replicates Self::calculate_parent_hash_blake2(&computed_hash, proof_element) - let mut combined_data = computed_hash.as_ref().to_vec(); - combined_data.extend_from_slice(proof_element.as_ref()); - computed_hash = blake2_256(&combined_data); - } else { - // This replicates Self::calculate_parent_hash_blake2(proof_element, &computed_hash) - let mut combined_data = proof_element.as_ref().to_vec(); - combined_data.extend_from_slice(computed_hash.as_ref()); - computed_hash = blake2_256(&combined_data); - } - } - computed_hash -} - -#[benchmarks( - where - T: Send + Sync, - T: Config + pallet_vesting::Config>, -)] -mod benchmarks { - use super::*; - - #[benchmark] - fn create_airdrop() { - let caller: T::AccountId = whitelisted_caller(); - let merkle_root = [0u8; 32]; - let vesting_period = None; - let vesting_schedule = None; - - #[extrinsic_call] - create_airdrop(RawOrigin::Signed(caller), merkle_root, vesting_period, vesting_schedule); - } - - #[benchmark] - fn fund_airdrop() { - let caller: T::AccountId = whitelisted_caller(); - let merkle_root = [0u8; 32]; - - let airdrop_id = MerkleAirdrop::::next_airdrop_id(); - AirdropInfo::::insert( - airdrop_id, - AirdropMetadata { - merkle_root, - balance: 0u32.into(), - creator: caller.clone(), - vesting_period: None, - vesting_delay: None, - }, - ); - - NextAirdropId::::put(airdrop_id + 1); - - let amount: BalanceOf = ::MinVestedTransfer::get(); - - // Get ED and ensure caller has sufficient balance - let ed = CurrencyOf::::minimum_balance(); - - let caller_balance = ed.saturating_mul(10u32.into()).saturating_add(amount); - CurrencyOf::::make_free_balance_be(&caller, caller_balance); - - CurrencyOf::::make_free_balance_be(&MerkleAirdrop::::account_id(), ed); - - #[extrinsic_call] - fund_airdrop(RawOrigin::Signed(caller), airdrop_id, amount); - } - - #[benchmark] - fn claim(p: Linear<0, { T::MaxProofs::get() }>) { - let caller: T::AccountId = whitelisted_caller(); - let recipient: T::AccountId = account("recipient", 0, 0); - - let amount: BalanceOf = ::MinVestedTransfer::get(); - - // 1. Calculate the initial leaf hash - let leaf_hash = MerkleAirdrop::::calculate_leaf_hash_blake2(&recipient, amount); - - // 2. Generate `p` dummy proof elements that will be passed to the extrinsic - let proof_elements_for_extrinsic: alloc::vec::Vec = (0..p) - .map(|i| { - let mut dummy_data = [0u8; 32]; - dummy_data[0] = i as u8; // Make them slightly different for each proof element - blake2_256(&dummy_data) // Hash it to make it a valid MerkleHash type - }) - .collect(); - - let merkle_root_to_store = - calculate_expected_root_for_benchmark(leaf_hash, &proof_elements_for_extrinsic); - - let airdrop_id = MerkleAirdrop::::next_airdrop_id(); - - AirdropInfo::::insert( - airdrop_id, - AirdropMetadata { - merkle_root: merkle_root_to_store, - balance: amount.saturating_mul(2u32.into()), // Ensure enough balance for the claim - creator: caller.clone(), - vesting_period: None, // Simplest case: no vesting period - vesting_delay: None, // Simplest case: no vesting delay - }, - ); - - let large_balance = - amount.saturating_mul(T::MaxProofs::get().into()).saturating_add(amount); - - // Creator might not be strictly needed for `claim` from `None` origin, but good practice - CurrencyOf::::make_free_balance_be(&caller, large_balance); - // Recipient starts with minimal balance or nothing, will receive the airdrop - CurrencyOf::::make_free_balance_be(&recipient, amount); - // Pallet's account needs funds to make the transfer - CurrencyOf::::make_free_balance_be( - &MerkleAirdrop::::account_id(), - large_balance, // Pallet account needs enough to cover the claim - ); - - AirdropInfo::::mutate(airdrop_id, |maybe_info| { - if let Some(info) = maybe_info { - info.balance = large_balance; - } - }); - - // Prepare the Merkle proof argument for the extrinsic call - let merkle_proof_arg = - BoundedVec::::try_from(proof_elements_for_extrinsic) - .expect("Proof elements vector should fit into BoundedVec"); - - // Ensure recipient hasn't claimed yet (benchmark state should be clean) - assert!(!Claimed::::contains_key(airdrop_id, &recipient)); - - #[extrinsic_call] - claim(RawOrigin::None, airdrop_id, recipient.clone(), amount, merkle_proof_arg); - - // Verify successful claim - assert!(Claimed::::contains_key(airdrop_id, &recipient)); - } - - #[benchmark] - fn delete_airdrop() { - let caller: T::AccountId = whitelisted_caller(); - let merkle_root = [0u8; 32]; - - // Create an airdrop first - let airdrop_id = MerkleAirdrop::::next_airdrop_id(); - - AirdropInfo::::insert( - airdrop_id, - AirdropMetadata { - merkle_root, - balance: 0u32.into(), - creator: caller.clone(), - vesting_period: None, - vesting_delay: None, - }, - ); - - NextAirdropId::::put(airdrop_id + 1); - - let ed = CurrencyOf::::minimum_balance(); - let tiny_amount: BalanceOf = 1u32.into(); - let large_balance = ed.saturating_mul(1_000_000u32.into()); - - CurrencyOf::::make_free_balance_be(&caller, large_balance); - CurrencyOf::::make_free_balance_be(&MerkleAirdrop::::account_id(), large_balance); - - AirdropInfo::::mutate(airdrop_id, |info| { - if let Some(info) = info { - info.balance = tiny_amount; - } - }); - - #[extrinsic_call] - delete_airdrop(RawOrigin::Signed(caller), airdrop_id); - } - - impl_benchmark_test_suite!(MerkleAirdrop, crate::mock::new_test_ext(), crate::mock::Test); -} diff --git a/pallets/merkle-airdrop/src/lib.rs b/pallets/merkle-airdrop/src/lib.rs deleted file mode 100644 index b3b2651d..00000000 --- a/pallets/merkle-airdrop/src/lib.rs +++ /dev/null @@ -1,584 +0,0 @@ -//! # Merkle Airdrop Pallet -//! -//! A pallet for distributing tokens via Merkle proofs, allowing efficient token airdrops -//! where recipients can claim their tokens by providing cryptographic proofs of eligibility. -//! -//! ## Overview -//! -//! This pallet provides functionality for: -//! - Creating airdrops with a Merkle root representing all valid claims, and optional vesting -//! parameters -//! - Funding airdrops with tokens to be distributed -//! - Allowing users to claim tokens by providing Merkle proofs -//! - Allowing creators to delete airdrops and reclaim any unclaimed tokens -//! -//! The use of Merkle trees allows for gas-efficient verification of eligibility without -//! storing the complete list of recipients on-chain. -//! -//! ## Interface -//! -//! ### Dispatchable Functions -//! -//! * `create_airdrop` - Create a new airdrop with a Merkle root and vesting parameters -//! * `fund_airdrop` - Fund an existing airdrop with tokens -//! * `claim` - Claim tokens from an airdrop by providing a Merkle proof -//! * `delete_airdrop` - Delete an airdrop and reclaim any remaining tokens (creator only) - -#![cfg_attr(not(feature = "std"), no_std)] - -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_system::pallet_prelude::BlockNumberFor; -pub use pallet::*; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod tests; - -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; -pub mod weights; -use scale_info::TypeInfo; -use sp_core::RuntimeDebug; -pub use weights::*; - -use frame_support::traits::{Currency, VestedTransfer}; - -/// NOTE: Vesting traits still use deprecated `Currency` trait. -type CurrencyOf = - <::Vesting as VestedTransfer<::AccountId>>::Currency; - -/// NOTE: Vesting traits still use deprecated `Currency` trait. -type BalanceOf = as Currency<::AccountId>>::Balance; - -/// Type alias for airdrop info for this pallet -type AirdropMetadataFor = - AirdropMetadata, BalanceOf, ::AccountId>; - -/// Type for storing a Merkle root hash -pub type MerkleRoot = [u8; 32]; - -/// Type for Merkle hash values -pub type MerkleHash = [u8; 32]; - -/// Airdrop ID type -pub type AirdropId = u32; - -#[derive( - Encode, - Decode, - PartialEq, - Eq, - Clone, - TypeInfo, - RuntimeDebug, - MaxEncodedLen, - DecodeWithMemTracking, -)] -pub struct AirdropMetadata { - /// Merkle root of the airdrop - pub merkle_root: MerkleHash, - /// Creator of the airdrop - pub creator: AccountId, - /// Current airdrop balance - pub balance: Balance, - /// Vesting period for the airdrop. `None` for immediate release. - pub vesting_period: Option, - /// Vesting start delay. `None` for immediate start - pub vesting_delay: Option, -} - -#[frame_support::pallet] -pub mod pallet { - use crate::{ - AirdropId, AirdropMetadata, AirdropMetadataFor, BalanceOf, CurrencyOf, MerkleHash, - MerkleRoot, - }; - - use super::weights::WeightInfo; - use frame_support::{ - pallet_prelude::*, - traits::{Currency, Get, VestedTransfer, VestingSchedule}, - }; - use frame_system::pallet_prelude::{BlockNumberFor, *}; - use sp_io::hashing::blake2_256; - use sp_runtime::{ - traits::{AccountIdConversion, BlockNumberProvider, Convert, Saturating}, - transaction_validity::{ - InvalidTransaction, TransactionLongevity, TransactionSource, TransactionValidity, - ValidTransaction, - }, - }; - extern crate alloc; - use alloc::vec; - - #[pallet::pallet] - pub struct Pallet(_); - - /// Configuration trait for the Merkle airdrop pallet. - #[pallet::config] - pub trait Config: frame_system::Config { - /// The vesting mechanism. - type Vesting: VestedTransfer> - + VestingSchedule>; - - /// Convert the block number into a balance. - type BlockNumberToBalance: Convert, BalanceOf>; - - /// The maximum number of proof elements allowed in a Merkle proof. - #[pallet::constant] - type MaxProofs: Get; - - /// The pallet id, used for deriving its sovereign account ID. - #[pallet::constant] - type PalletId: Get; - - /// Priority for unsigned claim transactions. - #[pallet::constant] - type UnsignedClaimPriority: Get; - - /// Weight information for the extrinsics in this pallet. - type WeightInfo: WeightInfo; - - /// Block number provider. - type BlockNumberProvider: BlockNumberProvider>; - } - - /// Stores general info about an airdrop - #[pallet::storage] - #[pallet::getter(fn airdrop_info)] - pub type AirdropInfo = StorageMap< - _, - Blake2_128Concat, - AirdropId, - AirdropMetadata, BalanceOf, T::AccountId>, - >; - - /// Storage for claimed status - #[pallet::storage] - #[pallet::getter(fn is_claimed)] - #[allow(clippy::unused_unit)] - pub type Claimed = StorageDoubleMap< - _, - Blake2_128Concat, - AirdropId, - Blake2_128Concat, - T::AccountId, - (), - ValueQuery, - >; - - /// Counter for airdrop IDs - #[pallet::storage] - #[pallet::getter(fn next_airdrop_id)] - pub type NextAirdropId = StorageValue<_, AirdropId, ValueQuery>; - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// A new airdrop has been created. - /// - /// Parameters: [airdrop_id, merkle_root] - AirdropCreated { - /// The ID of the created airdrop - airdrop_id: AirdropId, - /// Airdrop metadata - airdrop_metadata: AirdropMetadataFor, - }, - /// An airdrop has been funded with tokens. - /// - /// Parameters: [airdrop_id, amount] - AirdropFunded { - /// The ID of the funded airdrop - airdrop_id: AirdropId, - /// The amount of tokens added to the airdrop - amount: BalanceOf, - }, - /// A user has claimed tokens from an airdrop. - /// - /// Parameters: [airdrop_id, account, amount] - Claimed { - /// The ID of the airdrop claimed from - airdrop_id: AirdropId, - /// The account that claimed the tokens - account: T::AccountId, - /// The amount of tokens claimed - amount: BalanceOf, - }, - /// An airdrop has been deleted. - /// - /// Parameters: [airdrop_id] - AirdropDeleted { - /// The ID of the deleted airdrop - airdrop_id: AirdropId, - }, - } - - #[pallet::error] - #[repr(u8)] - pub enum Error { - /// The specified airdrop does not exist. - AirdropNotFound, - /// The airdrop does not have sufficient balance for this operation. - InsufficientAirdropBalance, - /// The user has already claimed from this airdrop. - AlreadyClaimed, - /// The provided Merkle proof is invalid. - InvalidProof, - /// Only the creator of an airdrop can delete it. - NotAirdropCreator, - } - - impl Error { - /// Convert the error to its underlying code - pub fn to_code(&self) -> u8 { - match self { - Error::::AirdropNotFound => 1, - Error::::InsufficientAirdropBalance => 2, - Error::::AlreadyClaimed => 3, - Error::::InvalidProof => 4, - Error::::NotAirdropCreator => 5, - _ => 0, - } - } - } - - impl Pallet { - /// Returns the account ID of the pallet. - /// - /// This account is used to hold the funds for all airdrops. - pub fn account_id() -> T::AccountId { - T::PalletId::get().into_account_truncating() - } - - /// Verifies a Merkle proof against a Merkle root using Blake2 hash. - /// - /// This function checks if an account is eligible to claim a specific amount from an - /// airdrop by verifying a Merkle proof against the stored Merkle root. - /// - /// # Parameters - /// - /// * `account` - The account ID claiming tokens - /// * `amount` - The amount of tokens being claimed - /// * `merkle_root` - The Merkle root to verify against - /// * `merkle_proof` - The proof path from the leaf to the root - /// - /// # Returns - /// - /// `true` if the proof is valid, `false` otherwise - pub fn verify_merkle_proof( - account: &T::AccountId, - amount: BalanceOf, - merkle_root: &MerkleRoot, - merkle_proof: &[MerkleHash], - ) -> bool { - let leaf = Self::calculate_leaf_hash_blake2(account, amount); - - // Verify the proof by walking up the tree - let mut computed_hash = leaf; - for proof_element in merkle_proof.iter() { - computed_hash = if computed_hash < *proof_element { - Self::calculate_parent_hash_blake2(&computed_hash, proof_element) - } else { - Self::calculate_parent_hash_blake2(proof_element, &computed_hash) - }; - } - computed_hash == *merkle_root - } - - /// Calculates the leaf hash for a Merkle tree using Blake2. - /// - /// This function creates a leaf node hash from an account and amount using the - /// Blake2 hash function, which is optimized for zero-knowledge proofs. - /// - /// # Parameters - /// - /// * `account` - The account ID to include in the leaf - /// * `amount` - The token amount to include in the leaf - /// - /// # Returns - /// - /// A 32-byte array containing the Blake2 hash of the account and amount - pub fn calculate_leaf_hash_blake2( - account: &T::AccountId, - amount: BalanceOf, - ) -> MerkleHash { - let bytes = (account, amount).encode(); - blake2_256(&bytes) - } - - /// Calculates the parent hash in a Merkle tree using Blake2. - /// - /// This function combines two child hashes to create their parent hash in the Merkle tree. - /// The children are ordered lexicographically before hashing to ensure consistency. - /// - /// # Parameters - /// - /// * `left` - The first child hash - /// * `right` - The second child hash - /// - /// # Returns - /// - /// A 32-byte array containing the Blake2 hash of the combined children - pub fn calculate_parent_hash_blake2(left: &MerkleHash, right: &MerkleHash) -> MerkleHash { - // Ensure consistent ordering of inputs (important for verification) - let combined = if left < right { - [left.as_slice(), right.as_slice()].concat() - } else { - [right.as_slice(), left.as_slice()].concat() - }; - - blake2_256(&combined) - } - } - - #[pallet::call] - impl Pallet { - /// Create a new airdrop with a Merkle root. - /// - /// The Merkle root is a cryptographic hash that represents all valid claims - /// for this airdrop. Users will later provide Merkle proofs to verify their - /// eligibility to claim tokens. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call (must be signed) - /// * `merkle_root` - The Merkle root hash representing all valid claims - /// * `vesting_period` - Optional vesting period for the airdrop - /// * `vesting_delay` - Optional delay before vesting starts - #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::create_airdrop())] - pub fn create_airdrop( - origin: OriginFor, - merkle_root: MerkleRoot, - vesting_period: Option>, - vesting_delay: Option>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - let airdrop_id = Self::next_airdrop_id(); - - let airdrop_metadata = AirdropMetadata { - merkle_root, - creator: who.clone(), - balance: Zero::zero(), - vesting_period, - vesting_delay, - }; - - AirdropInfo::::insert(airdrop_id, &airdrop_metadata); - NextAirdropId::::put(airdrop_id.saturating_add(1)); - - Self::deposit_event(Event::AirdropCreated { airdrop_id, airdrop_metadata }); - - Ok(()) - } - - /// Fund an existing airdrop with tokens. - /// - /// This function transfers tokens from the caller to the airdrop's account, - /// making them available for users to claim. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call (must be signed) - /// * `airdrop_id` - The ID of the airdrop to fund - /// * `amount` - The amount of tokens to add to the airdrop - /// - /// # Errors - /// - /// * `AirdropNotFound` - If the specified airdrop does not exist - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::fund_airdrop())] - pub fn fund_airdrop( - origin: OriginFor, - airdrop_id: AirdropId, - amount: BalanceOf, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - ensure!(AirdropInfo::::contains_key(airdrop_id), Error::::AirdropNotFound); - - CurrencyOf::::transfer( - &who, - &Self::account_id(), - amount, - frame_support::traits::ExistenceRequirement::KeepAlive, - )?; - - AirdropInfo::::mutate(airdrop_id, |maybe_metadata| { - if let Some(metadata) = maybe_metadata { - metadata.balance = metadata.balance.saturating_add(amount); - } - }); - - Self::deposit_event(Event::AirdropFunded { airdrop_id, amount }); - - Ok(()) - } - - /// Claim tokens from an airdrop by providing a Merkle proof. - /// - /// Users can claim their tokens by providing a proof of their eligibility. - /// The proof is verified against the airdrop's Merkle root. - /// Anyone can trigger a claim for any eligible recipient. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call - /// * `airdrop_id` - The ID of the airdrop to claim from - /// * `amount` - The amount of tokens to claim - /// * `merkle_proof` - The Merkle proof verifying eligibility - /// - /// # Errors - /// - /// * `AirdropNotFound` - If the specified airdrop does not exist - /// * `AlreadyClaimed` - If the recipient has already claimed from this airdrop - /// * `InvalidProof` - If the provided Merkle proof is invalid - /// * `InsufficientAirdropBalance` - If the airdrop doesn't have enough tokens - #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::claim(merkle_proof.len() as u32))] - pub fn claim( - origin: OriginFor, - airdrop_id: AirdropId, - recipient: T::AccountId, - amount: BalanceOf, - merkle_proof: BoundedVec, - ) -> DispatchResult { - ensure_none(origin)?; - - ensure!( - !Claimed::::contains_key(airdrop_id, &recipient), - Error::::AlreadyClaimed - ); - - let airdrop_metadata = - AirdropInfo::::get(airdrop_id).ok_or(Error::::AirdropNotFound)?; - - ensure!( - Self::verify_merkle_proof( - &recipient, - amount, - &airdrop_metadata.merkle_root, - &merkle_proof - ), - Error::::InvalidProof - ); - - ensure!(airdrop_metadata.balance >= amount, Error::::InsufficientAirdropBalance); - - // Mark as claimed before performing the transfer - Claimed::::insert(airdrop_id, &recipient, ()); - - AirdropInfo::::mutate(airdrop_id, |maybe_metadata| { - if let Some(metadata) = maybe_metadata { - metadata.balance = metadata.balance.saturating_sub(amount); - } - }); - - let per_block = if let Some(vesting_period) = airdrop_metadata.vesting_period { - amount - .checked_div(&T::BlockNumberToBalance::convert(vesting_period)) - .ok_or(Error::::InsufficientAirdropBalance)? - } else { - amount - }; - - let current_block = T::BlockNumberProvider::current_block_number(); - let vesting_start = - current_block.saturating_add(airdrop_metadata.vesting_delay.unwrap_or_default()); - - T::Vesting::vested_transfer( - &Self::account_id(), - &recipient, - amount, - per_block, - vesting_start, - )?; - - Self::deposit_event(Event::Claimed { airdrop_id, account: recipient, amount }); - - Ok(()) - } - - /// Delete an airdrop and reclaim any remaining funds. - /// - /// This function allows the creator of an airdrop to delete it and reclaim - /// any remaining tokens that haven't been claimed. - /// - /// # Parameters - /// - /// * `origin` - The origin of the call (must be the airdrop creator) - /// * `airdrop_id` - The ID of the airdrop to delete - /// - /// # Errors - /// - /// * `AirdropNotFound` - If the specified airdrop does not exist - /// * `NotAirdropCreator` - If the caller is not the creator of the airdrop - #[pallet::call_index(3)] - #[pallet::weight(T::WeightInfo::delete_airdrop())] - pub fn delete_airdrop(origin: OriginFor, airdrop_id: AirdropId) -> DispatchResult { - let who = ensure_signed(origin)?; - - let airdrop_metadata = - AirdropInfo::::take(airdrop_id).ok_or(Error::::AirdropNotFound)?; - - ensure!(airdrop_metadata.creator == who, Error::::NotAirdropCreator); - - CurrencyOf::::transfer( - &Self::account_id(), - &airdrop_metadata.creator, - airdrop_metadata.balance, - frame_support::traits::ExistenceRequirement::KeepAlive, - )?; - - Self::deposit_event(Event::AirdropDeleted { airdrop_id }); - - Ok(()) - } - } - - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; - - fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - if let Call::claim { airdrop_id, recipient, amount, merkle_proof } = call { - // 1. Check if airdrop exists - let airdrop_metadata = AirdropInfo::::get(airdrop_id).ok_or_else(|| { - let error = Error::::AirdropNotFound; - InvalidTransaction::Custom(error.to_code()) - })?; - - // 2. Check if already claimed - if Claimed::::contains_key(airdrop_id, recipient) { - let error = Error::::AlreadyClaimed; - return InvalidTransaction::Custom(error.to_code()).into(); - } - - // 3. Verify Merkle Proof - if !Self::verify_merkle_proof( - recipient, - *amount, - &airdrop_metadata.merkle_root, - merkle_proof, - ) { - let error = Error::::InvalidProof; - return InvalidTransaction::Custom(error.to_code()).into(); - } - - Ok(ValidTransaction { - priority: T::UnsignedClaimPriority::get(), - requires: vec![], - provides: vec![(airdrop_id, recipient, amount).encode()], - longevity: TransactionLongevity::MAX, - propagate: true, - }) - } else { - log::error!(target: "merkle-airdrop", "ValidateUnsigned: Received non-claim transaction or unexpected call structure"); - InvalidTransaction::Call.into() - } - } - } -} diff --git a/pallets/merkle-airdrop/src/mock.rs b/pallets/merkle-airdrop/src/mock.rs deleted file mode 100644 index 0a5c865c..00000000 --- a/pallets/merkle-airdrop/src/mock.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate as pallet_merkle_airdrop; -use frame_support::{ - parameter_types, - traits::{ConstU32, Everything, WithdrawReasons}, - PalletId, -}; -use frame_system::{self as system}; -use sp_core::H256; -use sp_runtime::{ - traits::{BlakeTwo256, ConvertInto, IdentityLookup}, - BuildStorage, -}; - -type Block = frame_system::mocking::MockBlock; - -// Configure a mock runtime to test the pallet. -frame_support::construct_runtime!( - pub enum Test { - System: frame_system, - Vesting: pallet_vesting, - Balances: pallet_balances, - MerkleAirdrop: pallet_merkle_airdrop, - } -); - -parameter_types! { - pub const BlockHashCount: u64 = 250; - pub const SS58Prefix: u8 = 189; -} - -impl system::Config for Test { - type BaseCallFilter = Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Block = Block; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = SS58Prefix; - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; - type RuntimeTask = (); - type ExtensionsWeightInfo = (); - type SingleBlockMigrations = (); - type MultiBlockMigrator = (); - type PreInherents = (); - type PostInherents = (); - type PostTransactions = (); - type RuntimeEvent = RuntimeEvent; -} - -parameter_types! { - pub const ExistentialDeposit: u64 = 1; - pub const MaxLocks: u32 = 50; -} - -impl pallet_balances::Config for Test { - type Balance = u64; - type DustRemoval = (); - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = (); - type MaxLocks = MaxLocks; - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; - type RuntimeHoldReason = (); - type FreezeIdentifier = (); - type MaxFreezes = (); - type RuntimeFreezeReason = (); - type DoneSlashHandler = (); -} - -parameter_types! { - pub const MinVestedTransfer: u64 = 1; - pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = - WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); -} - -impl pallet_vesting::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type WeightInfo = (); - type BlockNumberProvider = System; - type MinVestedTransfer = MinVestedTransfer; - type BlockNumberToBalance = ConvertInto; - type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; - - const MAX_VESTING_SCHEDULES: u32 = 3; -} - -parameter_types! { - pub const MaxProofs: u32 = 100; - pub const MerkleAirdropPalletId: PalletId = PalletId(*b"airdrop!"); - pub const UnsignedClaimPriority: u64 = 100; -} - -impl pallet_merkle_airdrop::Config for Test { - type Vesting = Vesting; - type MaxProofs = MaxProofs; - type PalletId = MerkleAirdropPalletId; - type UnsignedClaimPriority = UnsignedClaimPriority; - type WeightInfo = (); - type BlockNumberProvider = System; - type BlockNumberToBalance = ConvertInto; -} - -// Build genesis storage according to the mock runtime. -pub fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(1, 10_000_000), (MerkleAirdrop::account_id(), 1)], - } - .assimilate_storage(&mut t) - .unwrap(); - - t.into() -} diff --git a/pallets/merkle-airdrop/src/tests.rs b/pallets/merkle-airdrop/src/tests.rs deleted file mode 100644 index a142b6c5..00000000 --- a/pallets/merkle-airdrop/src/tests.rs +++ /dev/null @@ -1,591 +0,0 @@ -#![allow(clippy::unit_cmp)] - -use crate::{mock::*, Error, Event}; -use codec::Encode; -use frame_support::{ - assert_noop, assert_ok, - traits::{InspectLockableCurrency, LockIdentifier}, - BoundedVec, -}; -use sp_core::blake2_256; -use sp_runtime::TokenError; - -fn bounded_proof(proof: Vec<[u8; 32]>) -> BoundedVec<[u8; 32], MaxProofs> { - proof.try_into().expect("Proof exceeds maximum size") -} - -// Helper function to calculate a leaf hash for testing -fn calculate_leaf_hash(account: &u64, amount: u64) -> [u8; 32] { - let account_bytes = account.encode(); - let amount_bytes = amount.encode(); - let leaf_data = [&account_bytes[..], &amount_bytes[..]].concat(); - - blake2_256(&leaf_data) -} - -// Helper function to calculate a parent hash for testing -fn calculate_parent_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { - let combined = if left < right { - [&left[..], &right[..]].concat() - } else { - [&right[..], &left[..]].concat() - }; - - blake2_256(&combined) -} - -const VESTING_ID: LockIdentifier = *b"vesting "; - -#[test] -fn create_airdrop_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - - let airdrop_metadata = crate::AirdropMetadata { - merkle_root, - creator: 1, - balance: 0, - vesting_period: Some(100), - vesting_delay: Some(10), - }; - - System::assert_last_event( - Event::AirdropCreated { airdrop_id: 0, airdrop_metadata: airdrop_metadata.clone() } - .into(), - ); - - assert_eq!(MerkleAirdrop::airdrop_info(0), Some(airdrop_metadata)); - }); -} - -#[test] -fn fund_airdrop_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let amount = 100; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(10), - Some(10) - )); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, 0); - - // fund airdrop with insufficient balance should fail - assert_noop!( - MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(123456), 0, amount * 10000), - TokenError::FundsUnavailable, - ); - - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, amount)); - - System::assert_last_event(Event::AirdropFunded { airdrop_id: 0, amount }.into()); - - // Check that the airdrop balance was updated - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, amount); - - // Check that the balance was transferred - assert_eq!(Balances::free_balance(1), 9999900); // 10000000 - 100 - assert_eq!(Balances::free_balance(MerkleAirdrop::account_id()), 101); - - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, amount)); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, amount * 2); - assert_eq!(Balances::free_balance(1), 9999800); // 9999900 - 100 - assert_eq!(Balances::free_balance(MerkleAirdrop::account_id()), 201); // locked for vesting - }); -} - -#[test] -fn claim_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - let account1: u64 = 2; // Account that will claim - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(2) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - // Create proof for account1d - let merkle_proof = bounded_proof(vec![leaf2]); - - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof.clone())); - - System::assert_last_event(Event::Claimed { airdrop_id: 0, account: 2, amount: 500 }.into()); - - assert_eq!(MerkleAirdrop::is_claimed(0, 2), ()); - assert_eq!(Balances::balance_locked(VESTING_ID, &2), 500); // Unlocked - - assert_eq!(Balances::free_balance(2), 500); - assert_eq!(Balances::free_balance(MerkleAirdrop::account_id()), 501); // 1 (initial) + 1000 - // (funded) - 500 (claimed) - }); -} - -#[test] -fn create_airdrop_requires_signed_origin() { - new_test_ext().execute_with(|| { - let merkle_root = [0u8; 32]; - - assert_noop!( - MerkleAirdrop::create_airdrop(RuntimeOrigin::none(), merkle_root, None, None), - frame_support::error::BadOrigin - ); - }); -} - -#[test] -fn fund_airdrop_fails_for_nonexistent_airdrop() { - new_test_ext().execute_with(|| { - assert_noop!( - MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 999, 1000), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn claim_fails_for_nonexistent_airdrop() { - new_test_ext().execute_with(|| { - let merkle_proof = bounded_proof(vec![[0u8; 32]]); - - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 999, 1, 500, merkle_proof), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn claim_already_claimed() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let account1: u64 = 2; // Account that will claim - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - let merkle_proof = bounded_proof(vec![leaf2]); - - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof.clone())); - - // Try to claim again - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof.clone()), - Error::::AlreadyClaimed - ); - }); -} - -#[test] -fn verify_merkle_proof_works() { - new_test_ext().execute_with(|| { - // Create test accounts and amounts - let account1: u64 = 1; - let amount1: u64 = 500; - let account2: u64 = 2; - let amount2: u64 = 300; - - // Calculate leaf hashes - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - - // Calculate the Merkle root (hash of the two leaves) - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - // Create proofs - let proof_for_account1 = vec![leaf2]; - let proof_for_account2 = vec![leaf1]; - - // Test the verify_merkle_proof function directly - assert!( - MerkleAirdrop::verify_merkle_proof( - &account1, - amount1, - &merkle_root, - &proof_for_account1 - ), - "Proof for account1 should be valid" - ); - - assert!( - MerkleAirdrop::verify_merkle_proof( - &account2, - amount2, - &merkle_root, - &proof_for_account2 - ), - "Proof for account2 should be valid" - ); - - assert!( - !MerkleAirdrop::verify_merkle_proof( - &account1, - 400, // Wrong amount - &merkle_root, - &proof_for_account1 - ), - "Proof with wrong amount should be invalid" - ); - - let wrong_proof = vec![[1u8; 32]]; - assert!( - !MerkleAirdrop::verify_merkle_proof(&account1, amount1, &merkle_root, &wrong_proof), - "Wrong proof should be invalid" - ); - - assert!( - !MerkleAirdrop::verify_merkle_proof( - &3, // Wrong account - amount1, - &merkle_root, - &proof_for_account1 - ), - "Proof with wrong account should be invalid" - ); - }); -} - -#[test] -fn claim_invalid_proof_fails() { - new_test_ext().execute_with(|| { - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - let invalid_proof = bounded_proof(vec![[1u8; 32]]); // Different from the actual leaf2 - - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, invalid_proof), - Error::::InvalidProof - ); - }); -} - -#[test] -fn claim_insufficient_airdrop_balance_fails() { - new_test_ext().execute_with(|| { - // Create a valid merkle tree - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(1000), - Some(100) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 400)); // Fund less than claim amount - - let merkle_proof = bounded_proof(vec![leaf2]); - - // Attempt to claim more than available - assert_noop!( - MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 500, merkle_proof), - Error::::InsufficientAirdropBalance - ); - }); -} - -#[test] -fn claim_nonexistent_airdrop_fails() { - new_test_ext().execute_with(|| { - // Attempt to claim from a nonexistent airdrop - assert_noop!( - MerkleAirdrop::claim( - RuntimeOrigin::none(), - 999, - 2, - 500, - bounded_proof(vec![[0u8; 32]]) - ), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn claim_updates_balances_correctly() { - new_test_ext().execute_with(|| { - // Create a valid merkle tree - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 1000)); - - let initial_account_balance = Balances::free_balance(2); - let initial_pallet_balance = Balances::free_balance(MerkleAirdrop::account_id()); - - assert_ok!(MerkleAirdrop::claim( - RuntimeOrigin::none(), - 0, - 2, - 500, - bounded_proof(vec![leaf2]) - )); - - assert_eq!(Balances::free_balance(2), initial_account_balance + 500); - assert_eq!( - Balances::free_balance(MerkleAirdrop::account_id()), - initial_pallet_balance - 500 - ); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, 500); - assert_eq!(MerkleAirdrop::is_claimed(0, 2), ()); - }); -} - -#[test] -fn multiple_users_can_claim() { - new_test_ext().execute_with(|| { - let account1: u64 = 2; - let amount1: u64 = 5000; - let account2: u64 = 3; - let amount2: u64 = 3000; - let account3: u64 = 4; - let amount3: u64 = 2000; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let leaf3 = calculate_leaf_hash(&account3, amount3); - let parent1 = calculate_parent_hash(&leaf1, &leaf2); - let merkle_root = calculate_parent_hash(&parent1, &leaf3); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(1), - merkle_root, - Some(1000), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(1), 0, 10001)); - - // User 1 claims - let proof1 = bounded_proof(vec![leaf2, leaf3]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 2, 5000, proof1)); - assert_eq!(Balances::free_balance(2), 5000); // free balance but it's locked for vesting - assert_eq!(Balances::balance_locked(VESTING_ID, &2), 5000); - - // User 2 claims - let proof2 = bounded_proof(vec![leaf1, leaf3]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 3, 3000, proof2)); - assert_eq!(Balances::free_balance(3), 3000); - assert_eq!(Balances::balance_locked(VESTING_ID, &3), 3000); - - // User 3 claims - let proof3 = bounded_proof(vec![parent1]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, 4, 2000, proof3)); - assert_eq!(Balances::free_balance(4), 2000); - assert_eq!(Balances::balance_locked(VESTING_ID, &4), 2000); - - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, 1); - - assert_eq!(MerkleAirdrop::is_claimed(0, 2), ()); - assert_eq!(MerkleAirdrop::is_claimed(0, 3), ()); - assert_eq!(MerkleAirdrop::is_claimed(0, 4), ()); - }); -} - -#[test] -fn delete_airdrop_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let creator = 1; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - - let airdrop_info = MerkleAirdrop::airdrop_info(0).unwrap(); - - assert_eq!(airdrop_info.creator, creator); - - // Delete the airdrop (balance is zero) - assert_ok!(MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(creator), 0)); - - System::assert_last_event(Event::AirdropDeleted { airdrop_id: 0 }.into()); - - // Check that the airdrop no longer exists - assert!(MerkleAirdrop::airdrop_info(0).is_none()); - }); -} - -#[test] -fn delete_airdrop_with_balance_refunds_creator() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let creator = 1; - let initial_creator_balance = Balances::free_balance(creator); - let fund_amount = 100; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(creator), 0, fund_amount)); - - // Creator's balance should be reduced by fund_amount - assert_eq!(Balances::free_balance(creator), initial_creator_balance - fund_amount); - - assert_ok!(MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(creator), 0)); - - // Check that the funds were returned to the creator - assert_eq!(Balances::free_balance(creator), initial_creator_balance); - - System::assert_last_event(Event::AirdropDeleted { airdrop_id: 0 }.into()); - }); -} - -#[test] -fn delete_airdrop_non_creator_fails() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let merkle_root = [0u8; 32]; - let creator = 1; - let non_creator = 2; - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - - assert_noop!( - MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(non_creator), 0), - Error::::NotAirdropCreator - ); - }); -} - -#[test] -fn delete_airdrop_nonexistent_fails() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - assert_noop!( - MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(1), 999), - Error::::AirdropNotFound - ); - }); -} - -#[test] -fn delete_airdrop_after_claims_works() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - - let creator: u64 = 1; - let initial_creator_balance = Balances::free_balance(creator); - let account1: u64 = 2; - let amount1: u64 = 500; - let account2: u64 = 3; - let amount2: u64 = 300; - let total_fund = 1000; - - let leaf1 = calculate_leaf_hash(&account1, amount1); - let leaf2 = calculate_leaf_hash(&account2, amount2); - let merkle_root = calculate_parent_hash(&leaf1, &leaf2); - - assert_ok!(MerkleAirdrop::create_airdrop( - RuntimeOrigin::signed(creator), - merkle_root, - Some(100), - Some(10) - )); - assert_ok!(MerkleAirdrop::fund_airdrop(RuntimeOrigin::signed(creator), 0, total_fund)); - - // Let only one account claim (partial claiming) - let proof1 = bounded_proof(vec![leaf2]); - assert_ok!(MerkleAirdrop::claim(RuntimeOrigin::none(), 0, account1, amount1, proof1)); - - // Check that some balance remains - assert_eq!(MerkleAirdrop::airdrop_info(0).unwrap().balance, total_fund - amount1); - - // Now the creator deletes the airdrop with remaining balance - assert_ok!(MerkleAirdrop::delete_airdrop(RuntimeOrigin::signed(creator), 0)); - - // Check creator was refunded the unclaimed amount - assert_eq!( - Balances::free_balance(creator), - initial_creator_balance - total_fund + (total_fund - amount1) - ); - }); -} diff --git a/pallets/merkle-airdrop/src/weights.rs b/pallets/merkle-airdrop/src/weights.rs deleted file mode 100644 index c0213e38..00000000 --- a/pallets/merkle-airdrop/src/weights.rs +++ /dev/null @@ -1,193 +0,0 @@ -// 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_merkle_airdrop` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 47.2.0 -//! DATE: 2025-06-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `MacBook-Pro-4.local`, CPU: `` -//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` - -// Executed Command: -// frame-omni-bencher -// v1 -// benchmark -// pallet -// --runtime -// ./target/release/wbuild/quantus-runtime/quantus_runtime.wasm -// --pallet -// pallet-merkle-airdrop -// --extrinsic -// * -// --template -// ./.maintain/frame-weight-template.hbs -// --output -// ./pallets/merkle-airdrop/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_merkle_airdrop`. -pub trait WeightInfo { - fn create_airdrop() -> Weight; - fn fund_airdrop() -> Weight; - fn claim(p: u32, ) -> Weight; - fn delete_airdrop() -> Weight; -} - -/// Weights for `pallet_merkle_airdrop` using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: `MerkleAirdrop::NextAirdropId` (r:1 w:1) - /// Proof: `MerkleAirdrop::NextAirdropId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:0 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - fn create_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `6` - // Estimated: `1489` - // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 1489) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn fund_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(42_000_000, 3593) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::Claimed` (r:1 w:1) - /// Proof: `MerkleAirdrop::Claimed` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Vesting::Vesting` (r:1 w:1) - /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Balances::Locks` (r:1 w:1) - /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) - /// Storage: `Balances::Freezes` (r:1 w:0) - /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) - /// The range of component `p` is `[0, 100]`. - fn claim(p: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `441` - // Estimated: `6196` - // Minimum execution time: 73_000_000 picoseconds. - Weight::from_parts(74_879_630, 6196) - // Standard Error: 1_851 - .saturating_add(Weight::from_parts(368_666, 0).saturating_mul(p.into())) - .saturating_add(T::DbWeight::get().reads(7_u64)) - .saturating_add(T::DbWeight::get().writes(6_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn delete_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 39_000_000 picoseconds. - Weight::from_parts(39_000_000, 3593) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } -} - -// For backwards compatibility and tests. -impl WeightInfo for () { - /// Storage: `MerkleAirdrop::NextAirdropId` (r:1 w:1) - /// Proof: `MerkleAirdrop::NextAirdropId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:0 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - fn create_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `6` - // Estimated: `1489` - // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 1489) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn fund_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(42_000_000, 3593) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `MerkleAirdrop::Claimed` (r:1 w:1) - /// Proof: `MerkleAirdrop::Claimed` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Vesting::Vesting` (r:1 w:1) - /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Balances::Locks` (r:1 w:1) - /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) - /// Storage: `Balances::Freezes` (r:1 w:0) - /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) - /// The range of component `p` is `[0, 100]`. - fn claim(p: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `441` - // Estimated: `6196` - // Minimum execution time: 73_000_000 picoseconds. - Weight::from_parts(74_879_630, 6196) - // Standard Error: 1_851 - .saturating_add(Weight::from_parts(368_666, 0).saturating_mul(p.into())) - .saturating_add(RocksDbWeight::get().reads(7_u64)) - .saturating_add(RocksDbWeight::get().writes(6_u64)) - } - /// Storage: `MerkleAirdrop::AirdropInfo` (r:1 w:1) - /// Proof: `MerkleAirdrop::AirdropInfo` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn delete_airdrop() -> Weight { - // Proof Size summary in bytes: - // Measured: `262` - // Estimated: `3593` - // Minimum execution time: 39_000_000 picoseconds. - Weight::from_parts(39_000_000, 3593) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } -} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 51c7b67f..4de5e701 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -31,7 +31,6 @@ pallet-assets.workspace = true pallet-assets-holder = { workspace = true, default-features = false } pallet-balances.workspace = true pallet-conviction-voting.workspace = true -pallet-merkle-airdrop.workspace = true pallet-mining-rewards.workspace = true pallet-preimage.workspace = true pallet-qpow.workspace = true @@ -96,7 +95,6 @@ std = [ "pallet-assets/std", "pallet-balances/std", "pallet-conviction-voting/std", - "pallet-merkle-airdrop/std", "pallet-mining-rewards/std", "pallet-preimage/std", "pallet-qpow/std", @@ -145,7 +143,6 @@ runtime-benchmarks = [ "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", - "pallet-merkle-airdrop/runtime-benchmarks", "pallet-mining-rewards/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-qpow/runtime-benchmarks", diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index c670981c..6efb6e0c 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -30,7 +30,6 @@ frame_benchmarking::define_benchmarks!( [pallet_timestamp, Timestamp] [pallet_sudo, Sudo] [pallet_reversible_transfers, ReversibleTransfers] - [pallet_merkle_airdrop, MerkleAirdrop] [pallet_mining_rewards, MiningRewards] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index e76063a3..2677aef2 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -85,8 +85,6 @@ parameter_types! { // To upload, 10Mbs link takes 4.1s and 100Mbs takes 500ms pub RuntimeBlockLength: BlockLength = BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); pub const SS58Prefix: u8 = 189; - pub const MerkleAirdropPalletId: PalletId = PalletId(*b"airdrop!"); - pub const UnsignedClaimPriority: u32 = 100; } /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from @@ -505,20 +503,6 @@ impl pallet_reversible_transfers::Config for Runtime { type TreasuryAccountId = TreasuryAccountId; } -parameter_types! { - pub const MaxProofs: u32 = 4096; -} - -impl pallet_merkle_airdrop::Config for Runtime { - type Vesting = Vesting; - type MaxProofs = MaxProofs; - type PalletId = MerkleAirdropPalletId; - type WeightInfo = pallet_merkle_airdrop::weights::SubstrateWeight; - type UnsignedClaimPriority = UnsignedClaimPriority; - type BlockNumberProvider = System; - type BlockNumberToBalance = ConvertInto; -} - parameter_types! { pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const ProposalBond: Permill = Permill::from_percent(5); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 447ac474..1eaf36f0 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -22,7 +22,6 @@ use sp_version::RuntimeVersion; pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; -pub use pallet_merkle_airdrop; pub use pallet_reversible_transfers as ReversibleTransfersCall; pub use pallet_timestamp::Call as TimestampCall; @@ -243,9 +242,6 @@ mod runtime { #[runtime::pallet_index(16)] pub type TechReferenda = pallet_referenda::Pallet; - #[runtime::pallet_index(17)] - pub type MerkleAirdrop = pallet_merkle_airdrop; - #[runtime::pallet_index(18)] pub type TreasuryPallet = pallet_treasury; From 306bb4fa42c15b0054eba35c0cdf9b12299bd389 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Mon, 12 Jan 2026 11:49:24 +0800 Subject: [PATCH 2/9] feat: Vesting pallet - removed --- Cargo.lock | 16 - Cargo.toml | 1 - runtime/Cargo.toml | 4 - runtime/src/configs/mod.rs | 25 +- runtime/src/lib.rs | 3 - runtime/tests/governance/mod.rs | 1 - runtime/tests/governance/vesting.rs | 619 ---------------------------- 7 files changed, 3 insertions(+), 666 deletions(-) delete mode 100644 runtime/tests/governance/vesting.rs diff --git a/Cargo.lock b/Cargo.lock index ef334b1f..a5147e1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7551,21 +7551,6 @@ dependencies = [ "sp-runtime", ] -[[package]] -name = "pallet-vesting" -version = "41.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305b437f4832bb563b660afa6549c0f0d446b668b4f098edc48d04e803badb9f" -dependencies = [ - "frame-benchmarking", - "frame-support", - "frame-system", - "log", - "parity-scale-codec", - "scale-info", - "sp-runtime", -] - [[package]] name = "pallet-wormhole" version = "0.1.0" @@ -9186,7 +9171,6 @@ dependencies = [ "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", "pallet-utility", - "pallet-vesting", "parity-scale-codec", "primitive-types 0.13.1", "qp-dilithium-crypto", diff --git a/Cargo.toml b/Cargo.toml index 32a7f406..bd8bcfd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,7 +184,6 @@ 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 } -pallet-vesting = { 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 } sc-block-builder = { version = "0.45.0", default-features = true } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 4de5e701..6f039fe1 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -45,7 +45,6 @@ pallet-transaction-payment.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true pallet-treasury.workspace = true pallet-utility.workspace = true -pallet-vesting.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true qp-header = { workspace = true, features = ["serde"] } @@ -109,7 +108,6 @@ std = [ "pallet-transaction-payment/std", "pallet-treasury/std", "pallet-utility/std", - "pallet-vesting/std", "primitive-types/std", "qp-dilithium-crypto/full_crypto", "qp-dilithium-crypto/std", @@ -155,7 +153,6 @@ runtime-benchmarks = [ "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", - "pallet-vesting/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -174,7 +171,6 @@ try-runtime = [ "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", "pallet-treasury/try-runtime", - "pallet-vesting/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 2677aef2..28c996b4 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -39,7 +39,7 @@ use frame_support::{ derive_impl, parameter_types, traits::{ AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU8, EitherOf, Get, NeverEnsureOrigin, - VariantCountOf, WithdrawReasons, + VariantCountOf, }, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -56,7 +56,7 @@ use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier use qp_poseidon::PoseidonHasher; use qp_scheduler::BlockNumberOrTimestamp; use sp_runtime::{ - traits::{AccountIdConversion, ConvertInto, One}, + traits::{AccountIdConversion, One}, FixedU128, Perbill, Permill, }; use sp_version::RuntimeVersion; @@ -65,7 +65,7 @@ use sp_version::RuntimeVersion; use super::{ AccountId, Balance, Balances, Block, BlockNumber, Hash, Nonce, OriginCaller, PalletInfo, Preimage, Referenda, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, Vesting, DAYS, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Scheduler, System, Timestamp, DAYS, EXISTENTIAL_DEPOSIT, MICRO_UNIT, TARGET_BLOCK_TIME_MS, UNIT, VERSION, }; use sp_core::U512; @@ -423,25 +423,6 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } -parameter_types! { - pub const MinVestedTransfer: Balance = UNIT; - /// Unvested funds can be transferred and reserved for any other means (reserves overlap) - pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = - WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); -} - -impl pallet_vesting::Config for Runtime { - type Currency = Balances; - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_vesting::weights::SubstrateWeight; - type MinVestedTransfer = MinVestedTransfer; - type BlockNumberToBalance = ConvertInto; - type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; - type BlockNumberProvider = System; - - const MAX_VESTING_SCHEDULES: u32 = 28; -} - impl pallet_utility::Config for Runtime { type RuntimeCall = RuntimeCall; type RuntimeEvent = RuntimeEvent; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 1eaf36f0..01b17135 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -215,9 +215,6 @@ mod runtime { #[runtime::pallet_index(7)] pub type MiningRewards = pallet_mining_rewards; - #[runtime::pallet_index(8)] - pub type Vesting = pallet_vesting; - #[runtime::pallet_index(9)] pub type Preimage = pallet_preimage; diff --git a/runtime/tests/governance/mod.rs b/runtime/tests/governance/mod.rs index 13fa1876..1261a867 100644 --- a/runtime/tests/governance/mod.rs +++ b/runtime/tests/governance/mod.rs @@ -2,4 +2,3 @@ pub mod engine; pub mod logic; pub mod tech_collective; pub mod treasury; -pub mod vesting; diff --git a/runtime/tests/governance/vesting.rs b/runtime/tests/governance/vesting.rs deleted file mode 100644 index 02f7700f..00000000 --- a/runtime/tests/governance/vesting.rs +++ /dev/null @@ -1,619 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::common::TestCommons; - use codec::Encode; - use frame_support::{ - assert_ok, - traits::{Bounded, Currency, VestingSchedule}, - }; - use pallet_conviction_voting::{AccountVote, Vote}; - use pallet_vesting::VestingInfo; - use quantus_runtime::{ - Balances, ConvictionVoting, Preimage, Referenda, RuntimeCall, RuntimeOrigin, System, - Utility, Vesting, DAYS, UNIT, - }; - use sp_runtime::{ - traits::{BlakeTwo256, Hash}, - MultiAddress, - }; - - /// Test case: Grant application through referendum with vesting payment schedule - /// - /// Scenario: - /// 1. Grant proposal submitted for referendum voting (treasury track) - /// 2. After positive voting, treasury spend is approved and executed - /// 3. Separate vesting implementation follows (two-stage governance pattern) - #[test] - fn test_grant_application_with_vesting_schedule() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - // Setup accounts - let proposer = TestCommons::account_id(1); - let beneficiary = TestCommons::account_id(2); - let voter1 = TestCommons::account_id(3); - let voter2 = TestCommons::account_id(4); - - // Give voters some balance for voting - Balances::make_free_balance_be(&voter1, 1000 * UNIT); - Balances::make_free_balance_be(&voter2, 1000 * UNIT); - Balances::make_free_balance_be(&proposer, 10000 * UNIT); // Proposer needs more funds for vesting transfer - - // Step 1: Create a treasury proposal for referendum - let grant_amount = 1000 * UNIT; - let vesting_period = 30; // Fast test: 30 blocks instead of 30 days - let per_block = grant_amount / vesting_period as u128; - - // Create the vesting info for later implementation - let vesting_info = VestingInfo::new(grant_amount, per_block, 1); - - // Treasury call for referendum approval - let treasury_call = RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount: grant_amount, - beneficiary: Box::new(MultiAddress::Id(beneficiary.clone())), - valid_from: None, - }); - - // Note: Two-stage process - referendum approves principle, implementation follows - let _vesting_call = RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(beneficiary.clone()), - schedule: vesting_info, - }); - - // Two-stage governance flow: referendum approves treasury spend principle - // Implementation details (like vesting schedule) handled in separate execution phase - let referendum_call = treasury_call; - - // Step 2: Submit preimage for the referendum call - let encoded_proposal = referendum_call.encode(); - let preimage_hash = BlakeTwo256::hash(&encoded_proposal); - - assert_ok!(Preimage::note_preimage( - RuntimeOrigin::signed(proposer.clone()), - encoded_proposal.clone() - )); - - // Step 3: Submit referendum for treasury spending (using treasury track) - let bounded_call = - Bounded::Lookup { hash: preimage_hash, len: encoded_proposal.len() as u32 }; - assert_ok!(Referenda::submit( - RuntimeOrigin::signed(proposer.clone()), - Box::new( - quantus_runtime::governance::pallet_custom_origins::Origin::SmallSpender.into() - ), - bounded_call, - frame_support::traits::schedule::DispatchTime::After(1) - )); - - // Step 4: Vote on referendum - let referendum_index = 0; - - // Vote YES with conviction - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter1.clone()), - referendum_index, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked1x, - }, - balance: 500 * UNIT, - } - )); - - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter2.clone()), - referendum_index, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked2x, - }, - balance: 300 * UNIT, - } - )); - - // Step 5: Wait for referendum to pass and execute - // Fast forward blocks for voting period + confirmation period (using fast governance - // timing) - let blocks_to_advance = 2 + 2 + 2 + 2 + 1; // prepare + decision + confirm + enactment + 1 - TestCommons::run_to_block(System::block_number() + blocks_to_advance); - - // The referendum should now be approved and treasury spend executed - - // Step 6: Implementation phase - after referendum approval, implement with vesting - // This demonstrates a realistic two-stage governance pattern: - // 1. Community votes on grant approval (principle) - // 2. Treasury council/governance implements with appropriate safeguards (vesting) - // This separation allows for community input on allocation while maintaining - // implementation flexibility - - println!("Referendum approved treasury spend. Now implementing vesting..."); - - // Implementation of the approved grant with vesting schedule - // This would typically be done by treasury council or automated system - assert_ok!(Vesting::force_vested_transfer( - RuntimeOrigin::root(), - MultiAddress::Id(proposer.clone()), - MultiAddress::Id(beneficiary.clone()), - vesting_info, - )); - - let initial_balance = Balances::free_balance(&beneficiary); - let locked_balance = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - - println!("Beneficiary balance: {:?}", initial_balance); - println!("Locked balance: {:?}", locked_balance); - - assert!(locked_balance > 0, "Vesting should have been created"); - - // Step 7: Test vesting unlock over time - let initial_block = System::block_number(); - let initial_locked_amount = locked_balance; // Save the initial locked amount - - // Check initial state - println!("Initial balance: {:?}", initial_balance); - println!("Initial locked: {:?}", locked_balance); - println!("Initial block: {:?}", initial_block); - - // Fast forward a few blocks and check unlocking - TestCommons::run_to_block(initial_block + 10); - - // Check after some blocks - let mid_balance = Balances::free_balance(&beneficiary); - let mid_locked = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - - println!("Mid balance: {:?}", mid_balance); - println!("Mid locked: {:?}", mid_locked); - - // The test should pass if vesting is working correctly - // mid_locked should be less than the initial locked amount - assert!( - mid_locked < initial_locked_amount, - "Some funds should be unlocked over time: initial_locked={:?}, mid_locked={:?}", - initial_locked_amount, - mid_locked - ); - - // Fast-forward to end of vesting period - TestCommons::run_to_block(initial_block + vesting_period + 1); - - // All funds should be unlocked - let final_balance = Balances::free_balance(&beneficiary); - let final_locked = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - - println!("Final balance: {:?}", final_balance); - println!("Final locked: {:?}", final_locked); - - assert_eq!(final_locked, 0, "All funds should be unlocked"); - // Note: In the vesting pallet, when funds are fully vested, they become available - // but the balance might not increase if the initial transfer was part of the vesting - // The main assertion is that the vesting worked correctly (final_locked == 0) - println!("Vesting test completed successfully - funds are fully unlocked"); - }); - } - - /// Test case: Multi-milestone grant with multiple vesting schedules - /// - /// Scenario: Grant paid out in multiple tranches (milestones) - /// after achieving specific goals - #[test] - fn test_milestone_based_grant_with_multiple_vesting() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let grantee = TestCommons::account_id(1); - let grantor = TestCommons::account_id(2); - - Balances::make_free_balance_be(&grantor, 10000 * UNIT); - - // Atomic milestone funding: all operations succeed or fail together - let milestone1_amount = 300 * UNIT; - let milestone2_amount = 400 * UNIT; - let milestone3_amount = 300 * UNIT; - - let milestone1_vesting = VestingInfo::new(milestone1_amount, milestone1_amount / 30, 1); - let milestone2_vesting = - VestingInfo::new(milestone2_amount, milestone2_amount / 60, 31); - - // Create batch call for all milestone operations - let _milestone_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Milestone 1: Initial funding with short vesting - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone1_vesting, - }), - // Milestone 2: Mid-term funding with longer vesting - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone2_vesting, - }), - // Milestone 3: Immediate payment - RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { - dest: MultiAddress::Id(grantee.clone()), - value: milestone3_amount, - }), - ], - }); - - // Execute all milestones atomically - let calls = vec![ - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone1_vesting, - }), - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: milestone2_vesting, - }), - RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { - dest: MultiAddress::Id(grantee.clone()), - value: milestone3_amount, - }), - ]; - assert_ok!(Utility::batch_all(RuntimeOrigin::signed(grantor.clone()), calls)); - - // Check that multiple vesting schedules are active - let vesting_schedules = Vesting::vesting(grantee.clone()).unwrap(); - assert_eq!(vesting_schedules.len(), 2, "Should have 2 active vesting schedules"); - - // Fast forward and verify unlocking patterns - TestCommons::run_to_block(40); // Past first vesting period - - let balance_after_first = Balances::free_balance(&grantee); - assert!( - balance_after_first >= milestone1_amount + milestone3_amount, - "First milestone and immediate payment should be available" - ); - - // Fast forward past second vesting period - TestCommons::run_to_block(100); - - let final_balance = Balances::free_balance(&grantee); - let expected_total = milestone1_amount + milestone2_amount + milestone3_amount; - assert!(final_balance >= expected_total, "All grant funds should be available"); - }); - } - - /// Test case: Realistic grant process with Tech Collective milestone evaluation - /// - /// Scenario: - /// 1. Initial referendum approves entire grant plan - /// 2. For each milestone: grantee delivers proof → Tech Collective votes via referenda → - /// payment released - /// 3. Tech Collective determines vesting schedule based on milestone quality/risk assessment - #[test] - fn test_progressive_milestone_referenda() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let grantee = TestCommons::account_id(1); - let proposer = TestCommons::account_id(2); - let voter1 = TestCommons::account_id(3); - let voter2 = TestCommons::account_id(4); - - // Tech Collective members - technical experts who evaluate milestones - let tech_member1 = TestCommons::account_id(5); - let tech_member2 = TestCommons::account_id(6); - let tech_member3 = TestCommons::account_id(7); - let treasury_account = TestCommons::account_id(8); - - // Setup balances for governance participation - Balances::make_free_balance_be(&voter1, 2000 * UNIT); - Balances::make_free_balance_be(&voter2, 2000 * UNIT); - Balances::make_free_balance_be(&proposer, 15000 * UNIT); - Balances::make_free_balance_be(&tech_member1, 3000 * UNIT); - Balances::make_free_balance_be(&tech_member2, 3000 * UNIT); - Balances::make_free_balance_be(&tech_member3, 3000 * UNIT); - Balances::make_free_balance_be(&treasury_account, 10000 * UNIT); - - // Add Tech Collective members - assert_ok!(quantus_runtime::TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::Id(tech_member1.clone()) - )); - assert_ok!(quantus_runtime::TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::Id(tech_member2.clone()) - )); - assert_ok!(quantus_runtime::TechCollective::add_member( - RuntimeOrigin::root(), - MultiAddress::Id(tech_member3.clone()) - )); - - let milestone1_amount = 400 * UNIT; - let milestone2_amount = 500 * UNIT; - let milestone3_amount = 600 * UNIT; - let total_grant = milestone1_amount + milestone2_amount + milestone3_amount; - - // === STEP 1: Initial referendum approves entire grant plan === - println!("=== REFERENDUM: Grant Plan Approval ==="); - - let grant_approval_call = RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount: total_grant, - beneficiary: Box::new(MultiAddress::Id(treasury_account.clone())), - valid_from: None, - }); - - let encoded_proposal = grant_approval_call.encode(); - let preimage_hash = BlakeTwo256::hash(&encoded_proposal); - - assert_ok!(Preimage::note_preimage( - RuntimeOrigin::signed(proposer.clone()), - encoded_proposal.clone() - )); - - let bounded_call = - Bounded::Lookup { hash: preimage_hash, len: encoded_proposal.len() as u32 }; - assert_ok!(Referenda::submit( - RuntimeOrigin::signed(proposer.clone()), - Box::new( - quantus_runtime::governance::pallet_custom_origins::Origin::SmallSpender.into() - ), - bounded_call, - frame_support::traits::schedule::DispatchTime::After(1) - )); - - // Community votes on the grant plan - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter1.clone()), - 0, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked1x, - }, - balance: 800 * UNIT, - } - )); - - assert_ok!(ConvictionVoting::vote( - RuntimeOrigin::signed(voter2.clone()), - 0, - AccountVote::Standard { - vote: Vote { - aye: true, - conviction: pallet_conviction_voting::Conviction::Locked2x, - }, - balance: 600 * UNIT, - } - )); - - let blocks_to_advance = 2 + 2 + 2 + 2 + 1; // Fast governance timing: prepare + decision + confirm + enactment + 1 - TestCommons::run_to_block(System::block_number() + blocks_to_advance); - - println!("✅ Grant plan approved by referendum!"); - - // === STEP 2: Tech Collective milestone evaluations via referenda === - - // === MILESTONE 1: Tech Collective Decision === - println!("=== MILESTONE 1: Tech Collective Decision ==="); - - println!("📋 Grantee delivers milestone 1: Basic protocol implementation"); - TestCommons::run_to_block(System::block_number() + 10); - - // Tech Collective evaluates and decides on milestone 1 payment - let milestone1_vesting = VestingInfo::new( - milestone1_amount, - milestone1_amount / 60, // Fast test: 60 blocks instead of 60 days - System::block_number() + 1, - ); - - println!("🔍 Tech Collective evaluates milestone 1..."); - - // Tech Collective implements milestone payment directly (as technical body with - // authority) In practice this could be through their own governance or automated - // after technical review - assert_ok!(Vesting::force_vested_transfer( - RuntimeOrigin::root(), /* Tech Collective has root-level authority for technical - * decisions */ - MultiAddress::Id(treasury_account.clone()), - MultiAddress::Id(grantee.clone()), - milestone1_vesting, - )); - - println!("✅ Tech Collective approved milestone 1 with 60-day vesting"); - - let milestone1_locked = Vesting::vesting_balance(&grantee).unwrap_or(0); - println!("Grantee locked (vesting): {:?}", milestone1_locked); - assert!(milestone1_locked > 0, "Milestone 1 should be vesting"); - - // === MILESTONE 2: Tech Collective Decision === - println!("=== MILESTONE 2: Tech Collective Decision ==="); - - TestCommons::run_to_block(System::block_number() + 20); - println!("📋 Grantee delivers milestone 2: Advanced features + benchmarks"); - - // Reduced vesting due to high quality - let milestone2_vesting = VestingInfo::new( - milestone2_amount, - milestone2_amount / 30, // Fast test: 30 blocks instead of 30 days - System::block_number() + 1, - ); - - println!("🔍 Tech Collective evaluates milestone 2 (high quality work)..."); - - // Tech Collective approves with reduced vesting due to excellent work - assert_ok!(Vesting::force_vested_transfer( - RuntimeOrigin::root(), - MultiAddress::Id(treasury_account.clone()), - MultiAddress::Id(grantee.clone()), - milestone2_vesting, - )); - - println!("✅ Tech Collective approved milestone 2 with reduced 30-day vesting"); - - // === MILESTONE 3: Final Tech Collective Decision === - println!("=== MILESTONE 3: Final Tech Collective Decision ==="); - - TestCommons::run_to_block(System::block_number() + 20); - println!( - "📋 Grantee delivers final milestone: Production deployment + maintenance plan" - ); - - println!("🔍 Tech Collective evaluates final milestone (project completion)..."); - - // Immediate payment for completed project - no vesting needed - assert_ok!(Balances::transfer_allow_death( - RuntimeOrigin::signed(treasury_account.clone()), - MultiAddress::Id(grantee.clone()), - milestone3_amount, - )); - - println!("✅ Tech Collective approved final milestone with immediate payment"); - - // === Verify Tech Collective governance worked === - let final_balance = Balances::free_balance(&grantee); - let remaining_locked = Vesting::vesting_balance(&grantee).unwrap_or(0); - - println!("Final grantee balance: {:?}", final_balance); - println!("Remaining locked: {:?}", remaining_locked); - - let vesting_schedules = Vesting::vesting(grantee.clone()).unwrap_or_default(); - assert!( - !vesting_schedules.is_empty(), - "Should have active vesting schedules from Tech Collective decisions" - ); - - assert!( - final_balance >= milestone3_amount, - "Tech Collective milestone process should have provided controlled funding" - ); - - println!("🎉 Tech Collective governance process completed successfully!"); - println!(" - One community referendum approved the overall grant plan"); - println!(" - Tech Collective evaluated each milestone with technical expertise"); - println!(" - Vesting schedules determined by technical quality assessment:"); - println!(" * Milestone 1: 60-day vesting (conservative, early stage)"); - println!(" * Milestone 2: 30-day vesting (high confidence, quality work)"); - println!(" * Milestone 3: Immediate payment (project completed successfully)"); - }); - } - - /// Test case: Treasury proposal with automatic vesting integration - /// - /// Scenario: Treasury spend and vesting creation executed atomically - /// through batch calls for integrated fund management - #[test] - fn test_treasury_auto_vesting_integration() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let beneficiary = TestCommons::account_id(1); - let amount = 1000 * UNIT; - - // Create atomic treasury spend + vesting creation through batch calls - let vesting_info = VestingInfo::new(amount, amount / (30 * DAYS) as u128, 1); - - let _treasury_vesting_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Treasury spend - RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount, - beneficiary: Box::new(MultiAddress::Id(beneficiary.clone())), - valid_from: None, - }), - // Vesting creation as part of same atomic transaction - RuntimeCall::Vesting(pallet_vesting::Call::force_vested_transfer { - source: MultiAddress::Id(beneficiary.clone()), /* Simplified - in - * practice treasury - * account */ - target: MultiAddress::Id(beneficiary.clone()), - schedule: vesting_info, - }), - ], - }); - - // Execute atomic treasury spend + vesting batch - let calls = vec![ - RuntimeCall::TreasuryPallet(pallet_treasury::Call::spend { - asset_kind: Box::new(()), - amount, - beneficiary: Box::new(MultiAddress::Id(beneficiary.clone())), - valid_from: None, - }), - RuntimeCall::Vesting(pallet_vesting::Call::force_vested_transfer { - source: MultiAddress::Id(beneficiary.clone()), - target: MultiAddress::Id(beneficiary.clone()), - schedule: vesting_info, - }), - ]; - assert_ok!(Utility::batch_all(RuntimeOrigin::root(), calls)); - - // Verify the integration worked - let locked_amount = Vesting::vesting_balance(&beneficiary).unwrap_or(0); - assert!(locked_amount > 0, "Vesting should be active"); - }); - } - - /// Test case: Emergency vesting operations with batch calls - /// - /// Scenario: Emergency handling of vesting schedules through - /// atomic batch operations for intervention scenarios - #[test] - fn test_emergency_vesting_cancellation() { - TestCommons::new_fast_governance_test_ext().execute_with(|| { - let grantee = TestCommons::account_id(1); - let grantor = TestCommons::account_id(2); - - Balances::make_free_balance_be(&grantor, 2000 * UNIT); - - // Create vesting schedule with atomic batch call setup - let total_amount = 1000 * UNIT; - let vesting_info = VestingInfo::new(total_amount, total_amount / 100, 1); - - // Example of comprehensive grant setup through batch operations - let _grant_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Initial grant setup - RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: vesting_info, - }), - // Could include additional setup calls (metadata, tracking, etc.) - ], - }); - - let calls = vec![RuntimeCall::Vesting(pallet_vesting::Call::vested_transfer { - target: MultiAddress::Id(grantee.clone()), - schedule: vesting_info, - })]; - assert_ok!(Utility::batch_all(RuntimeOrigin::signed(grantor.clone()), calls)); - - // Let some time pass and some funds unlock - TestCommons::run_to_block(50); - - let balance_before_cancellation = Balances::free_balance(&grantee); - let locked_before = Vesting::vesting_balance(&grantee).unwrap_or(0); - - assert!(locked_before > 0, "Should still have locked funds"); - - // Emergency intervention through atomic batch operations - let _emergency_batch = RuntimeCall::Utility(pallet_utility::Call::batch_all { - calls: vec![ - // Emergency action: schedule management operations - RuntimeCall::Vesting(pallet_vesting::Call::merge_schedules { - schedule1_index: 0, - schedule2_index: 0, - }), - // Could include additional emergency measures like fund recovery or - // notifications - ], - }); - - // Execute emergency intervention if vesting exists - if !Vesting::vesting(grantee.clone()).unwrap().is_empty() { - let calls = vec![RuntimeCall::Vesting(pallet_vesting::Call::merge_schedules { - schedule1_index: 0, - schedule2_index: 0, - })]; - assert_ok!(Utility::batch_all(RuntimeOrigin::signed(grantee.clone()), calls)); - } - - let balance_after = Balances::free_balance(&grantee); - - // Verify that emergency operations maintained system integrity - // (In practice, this would involve more sophisticated intervention mechanisms) - assert!( - balance_after >= balance_before_cancellation, - "Emergency handling should maintain or improve user's position" - ); - }); - } -} From 697377b54fb471e97b6152178b13867406921528 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Mon, 12 Jan 2026 17:54:54 +0800 Subject: [PATCH 3/9] feat: First not integrated version --- Cargo.lock | 29 +++ Cargo.toml | 3 + pallets/treasury-config/Cargo.toml | 55 +++++ pallets/treasury-config/README.md | 108 ++++++++++ pallets/treasury-config/src/benchmarking.rs | 37 ++++ pallets/treasury-config/src/lib.rs | 215 ++++++++++++++++++++ pallets/treasury-config/src/mock.rs | 43 ++++ pallets/treasury-config/src/tests.rs | 151 ++++++++++++++ pallets/treasury-config/src/weights.rs | 75 +++++++ runtime/Cargo.toml | 7 + runtime/src/benchmarks.rs | 2 + runtime/src/configs/mod.rs | 33 +++ runtime/src/genesis_config_presets.rs | 60 +++++- runtime/src/lib.rs | 6 + 14 files changed, 819 insertions(+), 5 deletions(-) create mode 100644 pallets/treasury-config/Cargo.toml create mode 100644 pallets/treasury-config/README.md create mode 100644 pallets/treasury-config/src/benchmarking.rs create mode 100644 pallets/treasury-config/src/lib.rs create mode 100644 pallets/treasury-config/src/mock.rs create mode 100644 pallets/treasury-config/src/tests.rs create mode 100644 pallets/treasury-config/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index a5147e1b..5979a6d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7170,6 +7170,18 @@ dependencies = [ "sp-mmr-primitives", ] +[[package]] +name = "pallet-multisig" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4356e277b778c6f307c8d39b40f870f39e0a7a0fd1f4eae20c64b99e8839b3a" +dependencies = [ + "log", + "parity-scale-codec", + "polkadot-sdk-frame", + "scale-info", +] + [[package]] name = "pallet-preimage" version = "41.0.0" @@ -7535,6 +7547,21 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-treasury-config" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-weights", +] + [[package]] name = "pallet-utility" version = "41.0.0" @@ -9158,6 +9185,7 @@ dependencies = [ "pallet-balances 40.0.1", "pallet-conviction-voting", "pallet-mining-rewards", + "pallet-multisig", "pallet-preimage", "pallet-qpow", "pallet-ranked-collective", @@ -9170,6 +9198,7 @@ dependencies = [ "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", + "pallet-treasury-config", "pallet-utility", "parity-scale-codec", "primitive-types 0.13.1", diff --git a/Cargo.toml b/Cargo.toml index bd8bcfd9..78d9f5a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "pallets/qpow", "pallets/reversible-transfers", "pallets/scheduler", + "pallets/treasury-config", "pallets/wormhole", "primitives/consensus/pow", "primitives/consensus/qpow", @@ -134,6 +135,7 @@ pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = 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-config = { path = "./pallets/treasury-config", 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 } @@ -173,6 +175,7 @@ frame-try-runtime = { version = "0.47.0", default-features = false } pallet-assets = { version = "43.0.0", default-features = false } pallet-assets-holder = { version = "0.3.0", default-features = false } pallet-conviction-voting = { version = "41.0.0", default-features = false } +pallet-multisig = { version = "41.0.0", default-features = false } pallet-preimage = { version = "41.0.0", default-features = false } pallet-ranked-collective = { version = "41.0.0", default-features = false } pallet-recovery = { version = "41.0.0", default-features = false } diff --git a/pallets/treasury-config/Cargo.toml b/pallets/treasury-config/Cargo.toml new file mode 100644 index 00000000..2ff1bcbf --- /dev/null +++ b/pallets/treasury-config/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "pallet-treasury-config" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +codec = { workspace = true } +scale-info = { workspace = true } + +# Substrate +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true +sp-weights.workspace = true + +[dev-dependencies] +sp-io.workspace = true +sp-runtime.workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-weights/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] + diff --git a/pallets/treasury-config/README.md b/pallets/treasury-config/README.md new file mode 100644 index 00000000..434f539f --- /dev/null +++ b/pallets/treasury-config/README.md @@ -0,0 +1,108 @@ +# Treasury Config Pallet + +A pallet for managing treasury multisig configuration in a Substrate-based blockchain. + +## Overview + +This pallet stores the treasury multisig signatories and threshold in runtime storage, allowing: +- **Different treasury addresses per network** (dev/heisenberg/dirac) from the same runtime build +- **Deterministic multisig address generation** compatible with `pallet-multisig` +- **Governance-driven updates** of signatories and threshold through tech referenda +- **Maximum transparency** via events when treasury configuration changes + +## Features + +### Storage + +- **Signatories**: Bounded vector of AccountIds (max 100) who can sign treasury transactions +- **Threshold**: Number of signatures required (u16) + +### Extrinsics + +#### `set_treasury_signatories(signatories: Vec, threshold: u16)` +- **Origin**: Root (typically via governance/tech referenda) +- **Updates**: All signatories and threshold in a single transaction +- **Emits**: `TreasurySignatoriesUpdated` event with old and new multisig addresses + +### Events + +#### `TreasurySignatoriesUpdated { old_account, new_account }` +Emitted when treasury configuration changes, showing both the old and new multisig addresses for maximum transparency. + +## Usage + +### Genesis Configuration + +```rust +TreasuryConfigConfig { + signatories: vec![alice, bob, charlie, dave, eve], + threshold: 3, // 3-of-5 multisig +} +``` + +### Updating via Governance + +```rust +TreasuryConfig::set_treasury_signatories( + RuntimeOrigin::root(), + vec![account1, account2, account3, account4, account5], + 3 // new threshold +) +``` + +### Getting Treasury Address + +```rust +let treasury_address = TreasuryConfig::get_treasury_account(); +``` + +The address is deterministically derived from signatories and threshold using the same algorithm as `pallet-multisig`. + +## Configuration + +### Network-Specific Setup + +- **Development**: 5 test signatories (Alice, Bob, Charlie, Dave, Eve), threshold 3 +- **Heisenberg (testnet)**: 5 signatories, threshold 3 +- **Dirac (mainnet)**: 5 production signatories, threshold 3 + +Each network can have different signatories while using the same runtime build. + +## Integration + +Add to your runtime: + +```rust +impl pallet_treasury_config::Config for Runtime { + type MaxSignatories = ConstU32<100>; + type WeightInfo = pallet_treasury_config::weights::SubstrateWeight; +} +``` + +## Testing + +Run unit tests: +```bash +cargo test -p pallet-treasury-config +``` + +Run benchmarks: +```bash +cargo run --release --features runtime-benchmarks -- benchmark pallet \ + --pallet pallet_treasury_config \ + --extrinsic "*" \ + --steps 50 \ + --repeat 20 +``` + +## Security Considerations + +- Only Root origin can update signatories (typically requires governance approval) +- Genesis configuration must have valid signatories and threshold +- Address generation is deterministic and verifiable +- All changes emit events for transparency + +## License + +Apache-2.0 + diff --git a/pallets/treasury-config/src/benchmarking.rs b/pallets/treasury-config/src/benchmarking.rs new file mode 100644 index 00000000..c382cc29 --- /dev/null +++ b/pallets/treasury-config/src/benchmarking.rs @@ -0,0 +1,37 @@ +// Benchmarking setup +#![allow(clippy::unwrap_used)] // Benchmarks can panic on setup failures + +use super::*; +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; + +#[benchmarks] +mod benchmarks { + use super::*; + use alloc::vec::Vec; + use frame_support::BoundedVec; + + #[benchmark] + fn set_treasury_signatories(s: Linear<1, 100>, // Number of signatories + ) { + // Setup: Create signatories + let signatories: Vec = + (0..s).map(|i| frame_benchmarking::account("signatory", i, 0)).collect(); + + let threshold = (s / 2 + 1) as u16; // Majority threshold + + // Initialize with some signatories first + let initial_signatories: Vec = + (0..3).map(|i| frame_benchmarking::account("initial", i, 0)).collect(); + Signatories::::put(BoundedVec::try_from(initial_signatories).unwrap()); + Threshold::::put(2); + + #[extrinsic_call] + _(RawOrigin::Root, signatories, threshold); + + // Verify + assert_eq!(Threshold::::get(), threshold); + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/treasury-config/src/lib.rs b/pallets/treasury-config/src/lib.rs new file mode 100644 index 00000000..702a7ac8 --- /dev/null +++ b/pallets/treasury-config/src/lib.rs @@ -0,0 +1,215 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![doc = "# Treasury Config Pallet"] +#![doc = ""] +#![doc = "This pallet manages the treasury multisig configuration."] +#![doc = "It stores the signatories and threshold for the treasury multisig account,"] +#![doc = "and provides deterministic address generation."] + +extern crate alloc; + +pub use pallet::*; +pub use weights::WeightInfo; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; + +// Allow expect_used in pallet macro expansions - these are auto-generated by frame_support +#[allow(clippy::expect_used)] +#[frame_support::pallet] +pub mod pallet { + use crate::WeightInfo; + use alloc::vec::Vec; + use frame_support::{pallet_prelude::*, traits::Get}; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::Hash; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config>> { + /// Maximum number of signatories allowed. + type MaxSignatories: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// The list of signatories for the treasury multisig account. + /// This is set in genesis and can be updated via governance. + #[pallet::storage] + #[pallet::getter(fn signatories)] + pub type Signatories = + StorageValue<_, BoundedVec, ValueQuery>; + + /// The threshold of signatures required for the treasury multisig. + /// This is set in genesis and can be updated via governance. + #[pallet::storage] + #[pallet::getter(fn threshold)] + pub type Threshold = StorageValue<_, u16, ValueQuery>; + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + pub signatories: Vec, + pub threshold: u16, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + // Validate signatories + if self.signatories.is_empty() { + // Genesis build errors are acceptable - this only runs once during chain init + #[cfg(feature = "std")] + eprintln!("ERROR: Treasury signatories cannot be empty"); + return; + } + + // Validate threshold + if self.threshold == 0 || self.threshold as usize > self.signatories.len() { + #[cfg(feature = "std")] + eprintln!( + "ERROR: Invalid threshold {} for {} signatories", + self.threshold, + self.signatories.len() + ); + return; + } + + // Convert to bounded vec + let bounded_signatories = match BoundedVec::try_from(self.signatories.clone()) { + Ok(bounded) => bounded, + Err(_) => { + #[cfg(feature = "std")] + eprintln!( + "ERROR: Treasury signatories ({}) exceed maximum limit ({})", + self.signatories.len(), + T::MaxSignatories::get() + ); + return; + }, + }; + + Signatories::::put(bounded_signatories); + Threshold::::put(self.threshold); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Treasury signatories have been updated. [old_account, new_account] + TreasurySignatoriesUpdated { old_account: T::AccountId, new_account: T::AccountId }, + } + + #[pallet::call] + impl Pallet { + /// Update the treasury signatories and threshold in a single call. + /// This requires Root origin (typically via governance/tech referenda). + /// + /// This updates ALL signatories at once - you provide the complete new list. + /// Example: To change from 5 signatories to different 5 signatories with new threshold, + /// you call this once with the new Vec and new threshold. + /// + /// Emits `TreasurySignatoriesUpdated` event showing the old and new multisig addresses. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::set_treasury_signatories(signatories.len() as u32))] + pub fn set_treasury_signatories( + origin: OriginFor, + signatories: Vec, + threshold: u16, + ) -> DispatchResult { + ensure_root(origin)?; + + ensure!(!signatories.is_empty(), Error::::EmptySignatories); + ensure!( + threshold > 0 && threshold as usize <= signatories.len(), + Error::::InvalidThreshold + ); + + // Store old address for event + let old_account = Self::get_treasury_account(); + + // Update storage + let bounded_signatories = + BoundedVec::try_from(signatories).map_err(|_| Error::::TooManySignatories)?; + + Signatories::::put(bounded_signatories); + Threshold::::put(threshold); + + // Calculate new address + let new_account = Self::get_treasury_account(); + + // Emit event + Self::deposit_event(Event::TreasurySignatoriesUpdated { old_account, new_account }); + + Ok(()) + } + } + + #[pallet::error] + pub enum Error { + /// The signatories list is empty. + EmptySignatories, + /// The threshold is invalid (0 or greater than number of signatories). + InvalidThreshold, + /// Too many signatories provided. + TooManySignatories, + } + + impl Pallet { + /// Calculate the multisig address from current signatories and threshold. + /// This uses the same algorithm as pallet-multisig for deterministic address generation. + pub fn get_treasury_account() -> T::AccountId { + let signatories = Signatories::::get(); + let threshold = Threshold::::get(); + + // If not configured yet, return default account + if signatories.is_empty() || threshold == 0 { + return match T::AccountId::decode( + &mut sp_runtime::traits::TrailingZeroInput::zeroes(), + ) { + Ok(account) => account, + Err(_) => { + // This should never happen with TrailingZeroInput + // Return a deterministic fallback + Self::multi_account_id(&[], 0) + }, + }; + } + + Self::multi_account_id(&signatories.into_inner(), threshold) + } + + /// Generate multisig account ID from signatories and threshold. + /// This matches the algorithm used by pallet-multisig. + fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId { + let entropy = (b"modlpy/utilisuba", who, threshold).using_encoded(T::Hashing::hash); + // Hash output is always valid input for AccountId decode + // This operation cannot fail in practice, but we handle the Result to satisfy clippy + T::AccountId::decode(&mut entropy.as_ref()).unwrap_or_else(|_| { + // Extremely unlikely fallback: decode from trailing zeros + // If even this fails, we recursively call with empty data + T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .unwrap_or_else(|_| Self::multi_account_id(&[], 0)) + }) + } + } + + /// Implement Get so this can be used as parameter_types! replacement + pub struct TreasuryAccount(core::marker::PhantomData); + + impl Get for TreasuryAccount { + fn get() -> T::AccountId { + Pallet::::get_treasury_account() + } + } +} diff --git a/pallets/treasury-config/src/mock.rs b/pallets/treasury-config/src/mock.rs new file mode 100644 index 00000000..82b4528a --- /dev/null +++ b/pallets/treasury-config/src/mock.rs @@ -0,0 +1,43 @@ +use crate as pallet_treasury_config; +use frame_support::{derive_impl, parameter_types}; +use sp_runtime::{traits::IdentityLookup, BuildStorage}; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + TreasuryConfig: pallet_treasury_config, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = u64; + type Lookup = IdentityLookup; +} + +parameter_types! { + pub const MaxSignatories: u32 = 100; +} + +impl pallet_treasury_config::Config for Test { + type MaxSignatories = MaxSignatories; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_treasury_config::GenesisConfig:: { + signatories: vec![1, 2, 3, 4, 5], + threshold: 3, + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() +} diff --git a/pallets/treasury-config/src/tests.rs b/pallets/treasury-config/src/tests.rs new file mode 100644 index 00000000..3117e29b --- /dev/null +++ b/pallets/treasury-config/src/tests.rs @@ -0,0 +1,151 @@ +use crate::{mock::*, Error, Event}; +use frame_support::{assert_noop, assert_ok}; + +#[test] +fn genesis_config_works() { + new_test_ext().execute_with(|| { + // Check that genesis config was applied + let signatories = TreasuryConfig::signatories(); + assert_eq!(signatories.len(), 5); + assert_eq!(signatories.to_vec(), vec![1, 2, 3, 4, 5]); + + let threshold = TreasuryConfig::threshold(); + assert_eq!(threshold, 3); + + // Check that treasury account is computed correctly + let treasury_account = TreasuryConfig::get_treasury_account(); + assert_ne!(treasury_account, 0); // Should be non-zero + }); +} + +#[test] +fn set_treasury_signatories_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); // Events are registered from block 1 + let old_account = TreasuryConfig::get_treasury_account(); + + // Update signatories + let new_signatories = vec![10, 20, 30, 40, 50]; + assert_ok!(TreasuryConfig::set_treasury_signatories( + RuntimeOrigin::root(), + new_signatories.clone(), + 3 + )); + + // Check storage was updated + assert_eq!(TreasuryConfig::signatories().to_vec(), new_signatories); + assert_eq!(TreasuryConfig::threshold(), 3); + + // Check new account is different + let new_account = TreasuryConfig::get_treasury_account(); + assert_ne!(old_account, new_account); + + // Check event was emitted + System::assert_last_event( + Event::TreasurySignatoriesUpdated { old_account, new_account }.into(), + ); + }); +} + +#[test] +fn set_treasury_signatories_requires_root() { + new_test_ext().execute_with(|| { + // Try to update without root - should fail + assert_noop!( + TreasuryConfig::set_treasury_signatories(RuntimeOrigin::signed(1), vec![10, 20, 30], 2), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn set_treasury_signatories_validates_empty() { + new_test_ext().execute_with(|| { + // Empty signatories should fail + assert_noop!( + TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![], 1), + Error::::EmptySignatories + ); + }); +} + +#[test] +fn set_treasury_signatories_validates_threshold() { + new_test_ext().execute_with(|| { + // Threshold = 0 should fail + assert_noop!( + TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3], 0), + Error::::InvalidThreshold + ); + + // Threshold > signatories should fail + assert_noop!( + TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3], 4), + Error::::InvalidThreshold + ); + }); +} + +#[test] +fn set_treasury_signatories_validates_max() { + new_test_ext().execute_with(|| { + // Create more than MaxSignatories (100) + let too_many: Vec = (0..101).collect(); + + assert_noop!( + TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), too_many, 50), + Error::::TooManySignatories + ); + }); +} + +#[test] +fn changing_threshold_changes_address() { + new_test_ext().execute_with(|| { + let signatories = vec![1, 2, 3, 4, 5]; + + // Set threshold to 2 + assert_ok!(TreasuryConfig::set_treasury_signatories( + RuntimeOrigin::root(), + signatories.clone(), + 2 + )); + let account_threshold_2 = TreasuryConfig::get_treasury_account(); + + // Set threshold to 4 (same signatories) + assert_ok!(TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), signatories, 4)); + let account_threshold_4 = TreasuryConfig::get_treasury_account(); + + // Addresses should be different + assert_ne!(account_threshold_2, account_threshold_4); + }); +} + +#[test] +fn deterministic_address_generation() { + new_test_ext().execute_with(|| { + let signatories = vec![1, 2, 3]; + + // Set signatories + assert_ok!(TreasuryConfig::set_treasury_signatories( + RuntimeOrigin::root(), + signatories.clone(), + 2 + )); + let account1 = TreasuryConfig::get_treasury_account(); + + // Change to different signatories + assert_ok!(TreasuryConfig::set_treasury_signatories( + RuntimeOrigin::root(), + vec![10, 20, 30], + 2 + )); + + // Change back to original + assert_ok!(TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), signatories, 2)); + let account2 = TreasuryConfig::get_treasury_account(); + + // Should get the same address + assert_eq!(account1, account2); + }); +} diff --git a/pallets/treasury-config/src/weights.rs b/pallets/treasury-config/src/weights.rs new file mode 100644 index 00000000..e7c369b9 --- /dev/null +++ b/pallets/treasury-config/src/weights.rs @@ -0,0 +1,75 @@ + +//! Autogenerated weights for `pallet_treasury_config` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-01-12, 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 +// --chain=dev +// --pallet=pallet_treasury_config +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --output=pallets/treasury-config/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_treasury_config`. +pub trait WeightInfo { + fn set_treasury_signatories(s: u32, ) -> Weight; +} + +/// Weights for `pallet_treasury_config` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `TreasuryConfig::Signatories` (r:1 w:1) + /// Proof: `TreasuryConfig::Signatories` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `TreasuryConfig::Threshold` (r:1 w:1) + /// Proof: `TreasuryConfig::Threshold` (`max_values`: Some(1), `max_size`: Some(2), added: 497, mode: `MaxEncodedLen`) + /// The range of component `s` is `[1, 100]`. + fn set_treasury_signatories(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `449` + // Estimated: `4687` + // Minimum execution time: 290_000_000 picoseconds. + Weight::from_parts(226_636_905, 0) + .saturating_add(Weight::from_parts(0, 4687)) + // Standard Error: 36_639 + .saturating_add(Weight::from_parts(4_652_600, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `TreasuryConfig::Signatories` (r:1 w:1) + /// Proof: `TreasuryConfig::Signatories` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `TreasuryConfig::Threshold` (r:1 w:1) + /// Proof: `TreasuryConfig::Threshold` (`max_values`: Some(1), `max_size`: Some(2), added: 497, mode: `MaxEncodedLen`) + /// The range of component `s` is `[1, 100]`. + fn set_treasury_signatories(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `449` + // Estimated: `4687` + // Minimum execution time: 290_000_000 picoseconds. + Weight::from_parts(226_636_905, 0) + .saturating_add(Weight::from_parts(0, 4687)) + // Standard Error: 36_639 + .saturating_add(Weight::from_parts(4_652_600, 0).saturating_mul(s.into())) + .saturating_add(frame_support::weights::constants::RocksDbWeight::get().reads(2)) + .saturating_add(frame_support::weights::constants::RocksDbWeight::get().writes(2)) + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 6f039fe1..a9978810 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -32,6 +32,7 @@ pallet-assets-holder = { workspace = true, default-features = false } pallet-balances.workspace = true pallet-conviction-voting.workspace = true pallet-mining-rewards.workspace = true +pallet-multisig.workspace = true pallet-preimage.workspace = true pallet-qpow.workspace = true pallet-ranked-collective.workspace = true @@ -44,6 +45,7 @@ pallet-timestamp.workspace = true pallet-transaction-payment.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true pallet-treasury.workspace = true +pallet-treasury-config.workspace = true pallet-utility.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true @@ -95,6 +97,7 @@ std = [ "pallet-balances/std", "pallet-conviction-voting/std", "pallet-mining-rewards/std", + "pallet-multisig/std", "pallet-preimage/std", "pallet-qpow/std", "pallet-ranked-collective/std", @@ -107,6 +110,7 @@ std = [ "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", "pallet-treasury/std", + "pallet-treasury-config/std", "pallet-utility/std", "primitive-types/std", "qp-dilithium-crypto/full_crypto", @@ -142,6 +146,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", "pallet-mining-rewards/runtime-benchmarks", + "pallet-multisig/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-qpow/runtime-benchmarks", "pallet-ranked-collective/runtime-benchmarks", @@ -153,6 +158,7 @@ runtime-benchmarks = [ "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", + "pallet-treasury-config/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -171,6 +177,7 @@ try-runtime = [ "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", "pallet-treasury/try-runtime", + "pallet-treasury-config/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index 6efb6e0c..1197c618 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -33,4 +33,6 @@ frame_benchmarking::define_benchmarks!( [pallet_mining_rewards, MiningRewards] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] + [pallet_treasury_config, TreasuryConfig] + [pallet_multisig, Multisig] ); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 28c996b4..6a8af30e 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -567,6 +567,39 @@ impl pallet_assets_holder::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; } +// ============================================================================= +// MULTISIG CONFIGURATION +// ============================================================================= +parameter_types! { + pub const DepositBase: Balance = 100 * MILLI_UNIT; + pub const DepositFactor: Balance = 10 * MILLI_UNIT; + pub const MaxSignatories: u32 = 100; +} + +impl pallet_multisig::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type DepositBase = DepositBase; + type DepositFactor = DepositFactor; + type MaxSignatories = MaxSignatories; + type WeightInfo = pallet_multisig::weights::SubstrateWeight; + type BlockNumberProvider = System; +} + +// ============================================================================= +// TREASURY CONFIG (MULTISIG-BASED) +// ============================================================================= +// This pallet stores the treasury multisig configuration in storage. +// It allows different networks (heisenberg/dirac/dev) to have different +// treasury addresses from genesis, while still being the same runtime build. +// ============================================================================= + +impl pallet_treasury_config::Config for Runtime { + type MaxSignatories = MaxSignatories; + type WeightInfo = pallet_treasury_config::weights::SubstrateWeight; +} + impl TryFrom for pallet_balances::Call { type Error = (); fn try_from(call: RuntimeCall) -> Result { diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 57eb5182..0ba50445 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -19,7 +19,8 @@ #![allow(clippy::expect_used)] use crate::{ - configs::TreasuryPalletId, AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, UNIT, + configs::TreasuryPalletId, AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, + TreasuryConfigConfig, UNIT, }; use alloc::{vec, vec::Vec}; use qp_dilithium_crypto::pair::{crystal_alice, crystal_charlie, dilithium_bob}; @@ -45,6 +46,30 @@ fn dirac_faucet_account() -> AccountId { account_from_ss58("qzn2h1xdg8N1QCLbL5BYxAikYvpVnyELtFkYqHEhwrDTx9bhr") } +// Treasury multisig signatories for development/heisenberg (5 signatories) +fn dev_treasury_signatories() -> Vec { + vec![ + crystal_alice().into_account(), + dilithium_bob().into_account(), + crystal_charlie().into_account(), + // Additional signatories for 5-member multisig + account_from_ss58("qznYQKUeV5un22rXh7CCQB7Bsac74jynVDs2qbHk1hpPMjocB"), // Dave placeholder + account_from_ss58("qzn2h1xdg8N1QCLbL5BYxAikYvpVnyELtFkYqHEhwrDTx9bhr"), // Eve placeholder + ] +} + +// Treasury multisig signatories for dirac (mainnet) (5 signatories) +fn dirac_treasury_signatories() -> Vec { + vec![ + // TODO: Replace with actual mainnet signatories + dirac_root_account(), + account_from_ss58("qznYQKUeV5un22rXh7CCQB7Bsac74jynVDs2qbHk1hpPMjocB"), + account_from_ss58("qzn2h1xdg8N1QCLbL5BYxAikYvpVnyELtFkYqHEhwrDTx9bhr"), + dirac_faucet_account(), + account_from_ss58("qznYQKUeV5un22rXh7CCQB7Bsac74jynVDs2qbHk1hpPMjocB"), // TODO: 5th signatory + ] +} + fn dilithium_default_accounts() -> Vec { vec![ crystal_alice().into_account(), @@ -53,17 +78,27 @@ fn dilithium_default_accounts() -> Vec { ] } // Returns the genesis config presets populated with given parameters. -fn genesis_template(endowed_accounts: Vec, root: AccountId) -> Value { +fn genesis_template( + endowed_accounts: Vec, + root: AccountId, + treasury_signatories: Vec, + treasury_threshold: u16, +) -> Value { let mut balances = endowed_accounts.iter().cloned().map(|k| (k, 1u128 << 60)).collect::>(); const ONE_BILLION: u128 = 1_000_000_000; + // Still use old treasury for initial funding let treasury_account = TreasuryPalletId::get().into_account_truncating(); balances.push((treasury_account, ONE_BILLION * UNIT)); let config = RuntimeGenesisConfig { balances: BalancesConfig { balances }, sudo: SudoConfig { key: Some(root.clone()) }, + treasury_config: TreasuryConfigConfig { + signatories: treasury_signatories, + threshold: treasury_threshold, + }, ..Default::default() }; @@ -80,7 +115,12 @@ pub fn development_config_genesis() -> Value { log::info!("🍆 Endowed account raw: {:?}", account); } - genesis_template(endowed_accounts, crystal_alice().into_account()) + genesis_template( + endowed_accounts, + crystal_alice().into_account(), + dev_treasury_signatories(), + 3, // 3-of-5 multisig for dev + ) } pub fn heisenberg_config_genesis() -> Value { @@ -90,7 +130,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(), + dev_treasury_signatories(), + 3, // 3-of-5 multisig for heisenberg testnet + ) } pub fn dirac_config_genesis() -> Value { @@ -100,7 +145,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_treasury_signatories(), + 3, // 3-of-5 multisig for dirac mainnet + ) } /// Provides the JSON representation of predefined genesis config for given `id`. diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 01b17135..7acb8481 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -253,4 +253,10 @@ mod runtime { #[runtime::pallet_index(22)] pub type AssetsHolder = pallet_assets_holder; + + #[runtime::pallet_index(23)] + pub type Multisig = pallet_multisig; + + #[runtime::pallet_index(24)] + pub type TreasuryConfig = pallet_treasury_config; } From bd32f380b37918fb17024d461f7fe0a9c3a065f4 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Tue, 13 Jan 2026 11:52:07 +0800 Subject: [PATCH 4/9] feat: Treasury pallet - with multisig defined in presets --- Cargo.lock | 21 - Cargo.toml | 1 - node/src/command.rs | 66 +- pallets/mining-rewards/src/benchmarking.rs | 7 +- pallets/mining-rewards/src/lib.rs | 8 +- pallets/mining-rewards/src/mock.rs | 4 +- pallets/mining-rewards/src/tests.rs | 5 +- pallets/treasury-config/src/lib.rs | 42 + pallets/treasury-config/src/tests.rs | 24 + primitives/dilithium-crypto/src/lib.rs | 5 +- primitives/dilithium-crypto/src/pair.rs | 8 + primitives/header/src/lib.rs | 2 +- runtime/Cargo.toml | 4 - runtime/src/configs/mod.rs | 45 +- runtime/src/genesis_config_presets.rs | 48 +- runtime/src/governance/definitions.rs | 20 +- runtime/src/lib.rs | 3 - runtime/tests/common.rs | 5 +- runtime/tests/governance/mod.rs | 1 - runtime/tests/governance/tech_collective.rs | 388 +----- runtime/tests/governance/treasury.rs | 1241 ------------------- 21 files changed, 185 insertions(+), 1763 deletions(-) delete mode 100644 runtime/tests/governance/treasury.rs diff --git a/Cargo.lock b/Cargo.lock index 5979a6d4..bf193822 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7527,26 +7527,6 @@ dependencies = [ "sp-weights", ] -[[package]] -name = "pallet-treasury" -version = "40.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77efcc0e81cf6025128d463fa56d024f1abb6ff26e190fc091da3bafd3882d2a" -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-runtime", -] - [[package]] name = "pallet-treasury-config" version = "0.1.0" @@ -9197,7 +9177,6 @@ dependencies = [ "pallet-timestamp", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", - "pallet-treasury", "pallet-treasury-config", "pallet-utility", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index 78d9f5a9..a1fe8343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,7 +185,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 654d8506..4c16457f 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -9,7 +9,10 @@ use qp_dilithium_crypto::{traits::WormholeAddress, DilithiumPair}; use qp_rusty_crystals_hdwallet::{ generate_mnemonic, wormhole::WormholePair, HDLattice, QUANTUS_DILITHIUM_CHAIN_ID, }; -use quantus_runtime::{Block, EXISTENTIAL_DEPOSIT}; +use quantus_runtime::{ + genesis_config_presets::get_treasury_config_for_chain, Block, TreasuryConfig, + EXISTENTIAL_DEPOSIT, +}; use rand::Rng; use sc_cli::SubstrateCli; use sc_network::config::{NetworkBackendType, NodeKeyConfig, Secret}; @@ -19,7 +22,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 { @@ -179,25 +182,39 @@ impl SubstrateCli for Cli { fn load_spec(&self, id: &str) -> Result, String> { Ok(match id { - "dev" => - Box::new(chain_spec::development_chain_spec()?) as Box, - "dirac_live_spec" => - Box::new(chain_spec::dirac_chain_spec()?) as Box, + "dev" => { + Box::new(chain_spec::development_chain_spec()?) as Box + }, + "dirac_live_spec" => { + Box::new(chain_spec::dirac_chain_spec()?) as Box + }, "dirac" => Box::new(chain_spec::ChainSpec::from_json_bytes(include_bytes!( "chain-specs/dirac.json" ))?) as Box, - "heisenberg_live_spec" => - Box::new(chain_spec::heisenberg_chain_spec()?) as Box, + "heisenberg_live_spec" => { + Box::new(chain_spec::heisenberg_chain_spec()?) as Box + }, "" | "heisenberg" => Box::new(chain_spec::ChainSpec::from_json_bytes(include_bytes!( "chain-specs/heisenberg.json" ))?) as Box, - path => + path => { Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?) - as Box, + as Box + }, }) } } +/// Get treasury account from genesis config for a given chain ID. +/// This ensures the rewards address matches what will be in runtime storage after genesis. +fn get_treasury_account_for_chain(chain_id: &str) -> Result { + let (signatories, threshold) = get_treasury_config_for_chain(chain_id).ok_or_else(|| { + sc_cli::Error::Input(format!("Unknown chain ID for treasury config: {}", chain_id)) + })?; + + Ok(TreasuryConfig::calculate_treasury_account(&signatories, threshold)) +} + /// Parse and run command line arguments #[allow(clippy::result_large_err)] pub fn run() -> sc_cli::Result<()> { @@ -409,8 +426,9 @@ pub fn run() -> sc_cli::Result<()> { cmd.run(client, inherent_benchmark_data()?, Vec::new(), &ext_factory) }, - BenchmarkCmd::Machine(cmd) => - cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()), + BenchmarkCmd::Machine(cmd) => { + cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()) + }, } }) }, @@ -453,24 +471,32 @@ pub fn run() -> sc_cli::Result<()> { let account = address.parse::().map_err(|_| { sc_cli::Error::Input("Invalid rewards address format".into()) })?; - log::info!("⛏️ Using address for rewards: {:?}", account); + log::info!("⛏️ Using explicit rewards address: {:?}", account); account }, None => { - // Automatically set rewards_address to Treasury when --dev is used + // Automatically set rewards_address to Treasury for dev environments 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 = get_treasury_account_for_chain(chain_id)?; log::info!( - "⛏️ Using treasury address for rewards: {:?}", + "⛏️ DEV MODE: Auto-configured mining rewards to genesis treasury address" + ); + log::info!( + "⛏️ Chain: '{}', Treasury: {:?}", + chain_id, treasury_account ); + log::info!( + "⛏️ Note: Treasury block rewards (if any) are read dynamically from runtime storage" + ); treasury_account } else { - // Should never happen - return Err(sc_cli::Error::Input("No rewards address provided".into())); + // Production mode requires explicit rewards address + return Err(sc_cli::Error::Input( + "Mining rewards address is required. Use --rewards-address
".into() + )); } }, }; diff --git a/pallets/mining-rewards/src/benchmarking.rs b/pallets/mining-rewards/src/benchmarking.rs index 2bf70338..d95e2f38 100644 --- a/pallets/mining-rewards/src/benchmarking.rs +++ b/pallets/mining-rewards/src/benchmarking.rs @@ -8,10 +8,7 @@ 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 { @@ -37,7 +34,7 @@ mod benchmarks { ); // Pre-fund Treasury account to ensure it exists - let treasury_account = T::TreasuryPalletId::get().into_account_truncating(); + let treasury_account = T::TreasuryAccountId::get(); 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 cd2a8a32..f1a50d1e 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -30,7 +30,7 @@ pub mod pallet { use sp_consensus_pow::POW_ENGINE_ID; use sp_runtime::{ generic::DigestItem, - traits::{AccountIdConversion, Saturating}, + traits::Saturating, }; pub(crate) type BalanceOf = @@ -60,9 +60,9 @@ pub mod pallet { #[pallet::constant] type TreasuryBlockReward: Get>; - /// The treasury pallet ID + /// The treasury account ID #[pallet::constant] - type TreasuryPalletId: Get; + type TreasuryAccountId: Get; /// Account ID used as the "from" account when creating transfer proofs for minted tokens #[pallet::constant] @@ -177,7 +177,7 @@ pub mod pallet { ); }, None => { - let treasury = T::TreasuryPalletId::get().into_account_truncating(); + let treasury = T::TreasuryAccountId::get(); 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 03ce2a4c..97e4254f 100644 --- a/pallets/mining-rewards/src/mock.rs +++ b/pallets/mining-rewards/src/mock.rs @@ -30,7 +30,7 @@ parameter_types! { pub const SS58Prefix: u8 = 189; pub const BlockReward: u128 = 50; pub const ExistentialDeposit: Balance = 1; - pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub const TreasuryAccountId: sp_core::crypto::AccountId32 = sp_core::crypto::AccountId32::new([111u8; 32]); } impl frame_system::Config for Test { @@ -92,7 +92,7 @@ impl pallet_mining_rewards::Config for Test { type WeightInfo = (); type MinerBlockReward = BlockReward; type TreasuryBlockReward = TreasuryBlockReward; - type TreasuryPalletId = TreasuryPalletId; + type TreasuryAccountId = TreasuryAccountId; type MintingAccount = MintingAccount; } diff --git a/pallets/mining-rewards/src/tests.rs b/pallets/mining-rewards/src/tests.rs index 04d9ef13..58f74c56 100644 --- a/pallets/mining-rewards/src/tests.rs +++ b/pallets/mining-rewards/src/tests.rs @@ -259,7 +259,7 @@ 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 = ::TreasuryAccountId::get(); let initial_treasury_balance = Balances::free_balance(&treasury_account); // Fund Treasury @@ -314,8 +314,7 @@ fn test_fees_split_between_treasury_and_miner() { MiningRewards::on_finalize(System::block_number()); // Get Treasury account - let treasury_account: sp_core::crypto::AccountId32 = - TreasuryPalletId::get().into_account_truncating(); + let treasury_account = ::TreasuryAccountId::get(); // Get actual values from the system AFTER on_finalize let treasury_balance_after_finalize = Balances::free_balance(&treasury_account); diff --git a/pallets/treasury-config/src/lib.rs b/pallets/treasury-config/src/lib.rs index 702a7ac8..df24b3eb 100644 --- a/pallets/treasury-config/src/lib.rs +++ b/pallets/treasury-config/src/lib.rs @@ -84,6 +84,17 @@ pub mod pallet { return; } + // Check for duplicate signatories + let mut sorted_signatories = self.signatories.clone(); + sorted_signatories.sort(); + for i in 1..sorted_signatories.len() { + if sorted_signatories[i - 1] == sorted_signatories[i] { + #[cfg(feature = "std")] + eprintln!("ERROR: Duplicate signatories detected in genesis config"); + return; + } + } + // Convert to bounded vec let bounded_signatories = match BoundedVec::try_from(self.signatories.clone()) { Ok(bounded) => bounded, @@ -135,6 +146,16 @@ pub mod pallet { Error::::InvalidThreshold ); + // Check for duplicate signatories + let mut sorted_signatories = signatories.clone(); + sorted_signatories.sort(); + for i in 1..sorted_signatories.len() { + ensure!( + sorted_signatories[i - 1] != sorted_signatories[i], + Error::::DuplicateSignatories + ); + } + // Store old address for event let old_account = Self::get_treasury_account(); @@ -163,6 +184,8 @@ pub mod pallet { InvalidThreshold, /// Too many signatories provided. TooManySignatories, + /// Duplicate signatories detected - each signatory must be unique. + DuplicateSignatories, } impl Pallet { @@ -189,6 +212,25 @@ pub mod pallet { Self::multi_account_id(&signatories.into_inner(), threshold) } + /// Calculate treasury account from given signatories and threshold WITHOUT accessing storage. + /// This can be used outside of runtime context (e.g., during node initialization). + /// NOTE: This must match the algorithm used by pallet-multisig. + pub fn calculate_treasury_account( + signatories: &[T::AccountId], + threshold: u16, + ) -> T::AccountId { + if signatories.is_empty() || threshold == 0 { + return match T::AccountId::decode( + &mut sp_runtime::traits::TrailingZeroInput::zeroes(), + ) { + Ok(account) => account, + Err(_) => Self::multi_account_id(&[], 0), + }; + } + + Self::multi_account_id(signatories, threshold) + } + /// Generate multisig account ID from signatories and threshold. /// This matches the algorithm used by pallet-multisig. fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId { diff --git a/pallets/treasury-config/src/tests.rs b/pallets/treasury-config/src/tests.rs index 3117e29b..c84e5cc9 100644 --- a/pallets/treasury-config/src/tests.rs +++ b/pallets/treasury-config/src/tests.rs @@ -86,6 +86,30 @@ fn set_treasury_signatories_validates_threshold() { }); } +#[test] +fn set_treasury_signatories_rejects_duplicates() { + new_test_ext().execute_with(|| { + // Same signatory 5 times should fail + assert_noop!( + TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 1, 1, 1, 1], 3), + Error::::DuplicateSignatories + ); + + // Partial duplicates should also fail + assert_noop!( + TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3, 2, 4], 3), + Error::::DuplicateSignatories + ); + + // No duplicates should succeed + assert_ok!(TreasuryConfig::set_treasury_signatories( + RuntimeOrigin::root(), + vec![1, 2, 3, 4, 5], + 3 + )); + }); +} + #[test] fn set_treasury_signatories_validates_max() { new_test_ext().execute_with(|| { diff --git a/primitives/dilithium-crypto/src/lib.rs b/primitives/dilithium-crypto/src/lib.rs index 31d4befb..bd9cd811 100644 --- a/primitives/dilithium-crypto/src/lib.rs +++ b/primitives/dilithium-crypto/src/lib.rs @@ -12,7 +12,10 @@ pub const PUB_KEY_BYTES: usize = ml_dsa_87::PUBLICKEYBYTES; pub const SECRET_KEY_BYTES: usize = ml_dsa_87::SECRETKEYBYTES; pub const SIGNATURE_BYTES: usize = ml_dsa_87::SIGNBYTES; -pub use pair::{create_keypair, crystal_alice, crystal_charlie, dilithium_bob, generate}; +pub use pair::{ + create_keypair, crystal_alice, crystal_charlie, crystal_eve, dilithium_bob, dilithium_dave, + generate, +}; pub use traits::verify; pub use types::{ DilithiumPair, DilithiumPublic, DilithiumSignature, DilithiumSignatureScheme, diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 34c3abb5..e3c245ec 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -27,6 +27,14 @@ pub fn crystal_charlie() -> DilithiumPair { let seed = [2u8; 32]; DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") } +pub fn dilithium_dave() -> DilithiumPair { + let seed = [3u8; 32]; + DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") +} +pub fn crystal_eve() -> DilithiumPair { + let seed = [4u8; 32]; + DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") +} impl IdentifyAccount for DilithiumPair { type AccountId = AccountId32; diff --git a/primitives/header/src/lib.rs b/primitives/header/src/lib.rs index bbe897fe..ccf8a7dd 100644 --- a/primitives/header/src/lib.rs +++ b/primitives/header/src/lib.rs @@ -123,7 +123,7 @@ where // We override the default hashing function to use // a felt aligned pre-image for poseidon hashing. fn hash(&self) -> Self::Hash { - Header::hash(&self) + Header::hash(self) } } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a9978810..1f52525c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -44,7 +44,6 @@ 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-config.workspace = true pallet-utility.workspace = true primitive-types.workspace = true @@ -109,7 +108,6 @@ std = [ "pallet-timestamp/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", - "pallet-treasury/std", "pallet-treasury-config/std", "pallet-utility/std", "primitive-types/std", @@ -157,7 +155,6 @@ runtime-benchmarks = [ "pallet-sudo/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", - "pallet-treasury/runtime-benchmarks", "pallet-treasury-config/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -176,7 +173,6 @@ try-runtime = [ "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", - "pallet-treasury/try-runtime", "pallet-treasury-config/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index 6a8af30e..c533cca4 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -28,8 +28,7 @@ use crate::{ governance::{ definitions::{ CommunityTracksInfo, GlobalMaxMembers, MinRankOfClassConverter, PreimageDeposit, - RootOrMemberForCollectiveOrigin, RootOrMemberForTechReferendaOrigin, - RuntimeNativeBalanceConverter, RuntimeNativePaymaster, TechCollectiveTracksInfo, + RootOrMemberForCollectiveOrigin, RootOrMemberForTechReferendaOrigin, TechCollectiveTracksInfo, }, pallet_custom_origins, Spender, }, @@ -56,7 +55,7 @@ use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier use qp_poseidon::PoseidonHasher; use qp_scheduler::BlockNumberOrTimestamp; use sp_runtime::{ - traits::{AccountIdConversion, One}, + traits::One, FixedU128, Perbill, Permill, }; use sp_version::RuntimeVersion; @@ -131,7 +130,7 @@ impl pallet_mining_rewards::Config for Runtime { type WeightInfo = pallet_mining_rewards::weights::SubstrateWeight; type MinerBlockReward = ConstU128<{ 10 * UNIT }>; // 10 tokens type TreasuryBlockReward = ConstU128<0>; // 0 tokens - type TreasuryPalletId = TreasuryPalletId; + type TreasuryAccountId = pallet_treasury_config::TreasuryAccount; type MintingAccount = MintingAccount; } @@ -461,8 +460,6 @@ parameter_types! { pub const MaxInterceptorAccounts: u32 = 32; /// Volume fee for reversed transactions from high-security accounts only, in basis points (10 = 0.1%) pub const HighSecurityVolumeFee: Permill = Permill::from_percent(1); - /// Treasury account ID - pub TreasuryAccountId: AccountId = TreasuryPalletId::get().into_account_truncating(); } impl pallet_reversible_transfers::Config for Runtime { @@ -481,41 +478,7 @@ impl pallet_reversible_transfers::Config for Runtime { type TimeProvider = Timestamp; type MaxInterceptorAccounts = MaxInterceptorAccounts; type VolumeFee = HighSecurityVolumeFee; - type TreasuryAccountId = TreasuryAccountId; -} - -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 + type TreasuryAccountId = pallet_treasury_config::TreasuryAccount; } parameter_types! { diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 0ba50445..759060d6 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -18,16 +18,15 @@ // 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, - TreasuryConfigConfig, UNIT, -}; +use crate::{AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, TreasuryConfigConfig}; use alloc::{vec, vec::Vec}; -use qp_dilithium_crypto::pair::{crystal_alice, crystal_charlie, dilithium_bob}; +use qp_dilithium_crypto::pair::{ + crystal_alice, crystal_charlie, crystal_eve, dilithium_bob, dilithium_dave, +}; 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"; @@ -46,15 +45,17 @@ fn dirac_faucet_account() -> AccountId { account_from_ss58("qzn2h1xdg8N1QCLbL5BYxAikYvpVnyELtFkYqHEhwrDTx9bhr") } +/// Treasury threshold used across all networks (3-of-5 multisig) +pub const TREASURY_THRESHOLD: u16 = 3; + // Treasury multisig signatories for development/heisenberg (5 signatories) fn dev_treasury_signatories() -> Vec { vec![ crystal_alice().into_account(), dilithium_bob().into_account(), crystal_charlie().into_account(), - // Additional signatories for 5-member multisig - account_from_ss58("qznYQKUeV5un22rXh7CCQB7Bsac74jynVDs2qbHk1hpPMjocB"), // Dave placeholder - account_from_ss58("qzn2h1xdg8N1QCLbL5BYxAikYvpVnyELtFkYqHEhwrDTx9bhr"), // Eve placeholder + dilithium_dave().into_account(), + crystal_eve().into_account(), ] } @@ -70,11 +71,25 @@ fn dirac_treasury_signatories() -> Vec { ] } +/// Public API: Get treasury signatories and threshold for a given chain ID. +/// This matches the genesis config for that chain. +/// Returns (signatories, threshold) tuple. +pub fn get_treasury_config_for_chain(chain_id: &str) -> Option<(Vec, u16)> { + match chain_id { + "dev" => Some((dev_treasury_signatories(), TREASURY_THRESHOLD)), + "heisenberg" => Some((dev_treasury_signatories(), TREASURY_THRESHOLD)), + "dirac" => Some((dirac_treasury_signatories(), TREASURY_THRESHOLD)), + _ => None, + } +} + fn dilithium_default_accounts() -> Vec { vec![ crystal_alice().into_account(), dilithium_bob().into_account(), crystal_charlie().into_account(), + dilithium_dave().into_account(), + crystal_eve().into_account(), ] } // Returns the genesis config presets populated with given parameters. @@ -84,13 +99,10 @@ fn genesis_template( treasury_signatories: Vec, treasury_threshold: u16, ) -> Value { - let mut balances = - endowed_accounts.iter().cloned().map(|k| (k, 1u128 << 60)).collect::>(); + let balances = endowed_accounts.iter().cloned().map(|k| (k, 1u128 << 60)).collect::>(); - const ONE_BILLION: u128 = 1_000_000_000; - // Still use old treasury for initial funding - let treasury_account = TreasuryPalletId::get().into_account_truncating(); - balances.push((treasury_account, ONE_BILLION * UNIT)); + // Note: Treasury multisig address will be computed from signatories in genesis + // We don't pre-fund it here - funds can be transferred to it after chain initialization let config = RuntimeGenesisConfig { balances: BalancesConfig { balances }, @@ -119,7 +131,7 @@ pub fn development_config_genesis() -> Value { endowed_accounts, crystal_alice().into_account(), dev_treasury_signatories(), - 3, // 3-of-5 multisig for dev + TREASURY_THRESHOLD, ) } @@ -134,7 +146,7 @@ pub fn heisenberg_config_genesis() -> Value { endowed_accounts, heisenberg_root_account(), dev_treasury_signatories(), - 3, // 3-of-5 multisig for heisenberg testnet + TREASURY_THRESHOLD, ) } @@ -149,7 +161,7 @@ pub fn dirac_config_genesis() -> Value { endowed_accounts, dirac_root_account(), dirac_treasury_signatories(), - 3, // 3-of-5 multisig for dirac mainnet + TREASURY_THRESHOLD, ) } diff --git a/runtime/src/governance/definitions.rs b/runtime/src/governance/definitions.rs index 98db0f49..cd83d30c 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, + governance::pallet_custom_origins, AccountId, Balance, Balances, BlockNumber, Runtime, + RuntimeOrigin, TreasuryConfig, DAYS, HOURS, MICRO_UNIT, UNIT, }; use alloc::borrow::Cow; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; @@ -21,7 +21,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 @@ -482,12 +482,13 @@ where let pallets_origin = o.into_caller(); match pallets_origin { - crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => + crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => { if pallet_ranked_collective::Members::::contains_key(&who) { Ok(0) } else { Err(original_o_for_error) - }, + } + }, _ => Err(original_o_for_error), } } @@ -531,12 +532,13 @@ where let pallets_origin = o.into_caller(); match pallets_origin { - crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => + crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => { if pallet_ranked_collective::Members::::contains_key(&who) { Ok(who) } else { Err(original_o_for_error) - }, + } + }, _ => Err(original_o_for_error), } } @@ -582,7 +584,7 @@ impl Pay for RuntimeNativePaymaster { _asset_kind: Self::AssetKind, amount: Self::Balance, ) -> Result { - let treasury_account = TreasuryPalletId::get().into_account_truncating(); + let treasury_account = TreasuryConfig::get_treasury_account(); >::transfer( &treasury_account, who, @@ -606,7 +608,7 @@ impl Pay for RuntimeNativePaymaster { _asset_kind: Self::AssetKind, amount: Self::Balance, ) { - let treasury_account = TreasuryPalletId::get().into_account_truncating(); + let treasury_account = TreasuryConfig::get_treasury_account(); let current_balance = crate::Balances::free_balance(&treasury_account); if current_balance < amount { let missing = amount - current_balance; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7acb8481..b488925b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -239,9 +239,6 @@ mod runtime { #[runtime::pallet_index(16)] 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; diff --git a/runtime/tests/common.rs b/runtime/tests/common.rs index d4eed8c8..f741f374 100644 --- a/runtime/tests/common.rs +++ b/runtime/tests/common.rs @@ -27,9 +27,8 @@ 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(); + // Set up treasury multisig account for volume fee collection + let treasury_account = quantus_runtime::TreasuryConfig::get_treasury_account(); Balances::make_free_balance_be(&treasury_account, 1000 * UNIT); }); 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..97058a2a 100644 --- a/runtime/tests/governance/tech_collective.rs +++ b/runtime/tests/governance/tech_collective.rs @@ -1286,390 +1286,8 @@ 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"); - } + // Treasury spend tests have been removed + // pallet-treasury has been replaced with pallet-treasury-config + // which only manages multisig configuration, not spend proposals - // 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" - ); - }); - } -} From de09f5fbeb268e23a4e5471544a9e0a3f1522d55 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Tue, 13 Jan 2026 12:28:01 +0800 Subject: [PATCH 5/9] feat: Treasury-Config to Treasury-Multisig pallet rename --- Cargo.lock | 4 +- Cargo.toml | 4 +- node/src/command.rs | 14 ++-- pallets/mining-rewards/src/lib.rs | 5 +- pallets/mining-rewards/src/mock.rs | 1 - pallets/mining-rewards/src/tests.rs | 1 - pallets/treasury-config/src/weights.rs | 75 ------------------- .../Cargo.toml | 21 +++--- .../README.md | 8 +- .../src/benchmarking.rs | 0 .../src/lib.rs | 12 +-- .../src/mock.rs | 15 +++- .../src/tests.rs | 70 ++++++++++------- pallets/treasury-multisig/src/weights.rs | 54 +++++++++++++ runtime/Cargo.toml | 8 +- runtime/src/benchmarks.rs | 2 +- runtime/src/configs/mod.rs | 16 ++-- runtime/src/genesis_config_presets.rs | 6 +- runtime/src/governance/definitions.rs | 16 ++-- runtime/src/lib.rs | 2 +- runtime/tests/common.rs | 9 +-- runtime/tests/governance/tech_collective.rs | 34 ++++----- 22 files changed, 185 insertions(+), 192 deletions(-) delete mode 100644 pallets/treasury-config/src/weights.rs rename pallets/{treasury-config => treasury-multisig}/Cargo.toml (96%) rename pallets/{treasury-config => treasury-multisig}/README.md (94%) rename pallets/{treasury-config => treasury-multisig}/src/benchmarking.rs (100%) rename pallets/{treasury-config => treasury-multisig}/src/lib.rs (96%) rename pallets/{treasury-config => treasury-multisig}/src/mock.rs (69%) rename pallets/{treasury-config => treasury-multisig}/src/tests.rs (62%) create mode 100644 pallets/treasury-multisig/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index bf193822..3d3b628f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7528,7 +7528,7 @@ dependencies = [ ] [[package]] -name = "pallet-treasury-config" +name = "pallet-treasury-multisig" version = "0.1.0" dependencies = [ "frame-benchmarking", @@ -9177,7 +9177,7 @@ dependencies = [ "pallet-timestamp", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", - "pallet-treasury-config", + "pallet-treasury-multisig", "pallet-utility", "parity-scale-codec", "primitive-types 0.13.1", diff --git a/Cargo.toml b/Cargo.toml index a1fe8343..550c7445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ members = [ "pallets/qpow", "pallets/reversible-transfers", "pallets/scheduler", - "pallets/treasury-config", + "pallets/treasury-multisig", "pallets/wormhole", "primitives/consensus/pow", "primitives/consensus/qpow", @@ -135,7 +135,7 @@ pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = 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-config = { path = "./pallets/treasury-config", default-features = false } +pallet-treasury-multisig = { path = "./pallets/treasury-multisig", 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 } diff --git a/node/src/command.rs b/node/src/command.rs index 4c16457f..94814c7f 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -10,7 +10,7 @@ use qp_rusty_crystals_hdwallet::{ generate_mnemonic, wormhole::WormholePair, HDLattice, QUANTUS_DILITHIUM_CHAIN_ID, }; use quantus_runtime::{ - genesis_config_presets::get_treasury_config_for_chain, Block, TreasuryConfig, + genesis_config_presets::get_treasury_config_for_chain, Block, TreasuryMultisig, EXISTENTIAL_DEPOSIT, }; use rand::Rng; @@ -207,12 +207,15 @@ impl SubstrateCli for Cli { /// Get treasury account from genesis config for a given chain ID. /// This ensures the rewards address matches what will be in runtime storage after genesis. -fn get_treasury_account_for_chain(chain_id: &str) -> Result { +fn get_treasury_account_for_chain(chain_id: &str) -> Result> { let (signatories, threshold) = get_treasury_config_for_chain(chain_id).ok_or_else(|| { - sc_cli::Error::Input(format!("Unknown chain ID for treasury config: {}", chain_id)) + Box::new(sc_cli::Error::Input(format!( + "Unknown chain ID for treasury config: {}", + chain_id + ))) })?; - Ok(TreasuryConfig::calculate_treasury_account(&signatories, threshold)) + Ok(TreasuryMultisig::calculate_treasury_account(&signatories, threshold)) } /// Parse and run command line arguments @@ -478,7 +481,8 @@ pub fn run() -> sc_cli::Result<()> { // Automatically set rewards_address to Treasury for dev environments if cli.run.shared_params.is_dev() { let chain_id = config.chain_spec.id(); - let treasury_account = get_treasury_account_for_chain(chain_id)?; + let treasury_account = + get_treasury_account_for_chain(chain_id).map_err(|e| *e)?; log::info!( "⛏️ DEV MODE: Auto-configured mining rewards to genesis treasury address" ); diff --git a/pallets/mining-rewards/src/lib.rs b/pallets/mining-rewards/src/lib.rs index f1a50d1e..d7d145d2 100644 --- a/pallets/mining-rewards/src/lib.rs +++ b/pallets/mining-rewards/src/lib.rs @@ -28,10 +28,7 @@ pub mod pallet { use frame_system::pallet_prelude::*; use qp_wormhole::TransferProofs; use sp_consensus_pow::POW_ENGINE_ID; - use sp_runtime::{ - generic::DigestItem, - traits::Saturating, - }; + use sp_runtime::{generic::DigestItem, traits::Saturating}; pub(crate) type BalanceOf = <::Currency as Inspect<::AccountId>>::Balance; diff --git a/pallets/mining-rewards/src/mock.rs b/pallets/mining-rewards/src/mock.rs index 97e4254f..ff9fc013 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::{ diff --git a/pallets/mining-rewards/src/tests.rs b/pallets/mining-rewards/src/tests.rs index 58f74c56..3ff10c77 100644 --- a/pallets/mining-rewards/src/tests.rs +++ b/pallets/mining-rewards/src/tests.rs @@ -1,6 +1,5 @@ use crate::{mock::*, weights::WeightInfo, Event}; use frame_support::traits::{Currency, Hooks}; -use sp_runtime::traits::AccountIdConversion; const UNIT: u128 = 1_000_000_000_000; diff --git a/pallets/treasury-config/src/weights.rs b/pallets/treasury-config/src/weights.rs deleted file mode 100644 index e7c369b9..00000000 --- a/pallets/treasury-config/src/weights.rs +++ /dev/null @@ -1,75 +0,0 @@ - -//! Autogenerated weights for `pallet_treasury_config` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-01-12, 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 -// --chain=dev -// --pallet=pallet_treasury_config -// --extrinsic=* -// --steps=50 -// --repeat=20 -// --output=pallets/treasury-config/src/weights.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions needed for `pallet_treasury_config`. -pub trait WeightInfo { - fn set_treasury_signatories(s: u32, ) -> Weight; -} - -/// Weights for `pallet_treasury_config` using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: `TreasuryConfig::Signatories` (r:1 w:1) - /// Proof: `TreasuryConfig::Signatories` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) - /// Storage: `TreasuryConfig::Threshold` (r:1 w:1) - /// Proof: `TreasuryConfig::Threshold` (`max_values`: Some(1), `max_size`: Some(2), added: 497, mode: `MaxEncodedLen`) - /// The range of component `s` is `[1, 100]`. - fn set_treasury_signatories(s: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `449` - // Estimated: `4687` - // Minimum execution time: 290_000_000 picoseconds. - Weight::from_parts(226_636_905, 0) - .saturating_add(Weight::from_parts(0, 4687)) - // Standard Error: 36_639 - .saturating_add(Weight::from_parts(4_652_600, 0).saturating_mul(s.into())) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) - } -} - -// For backwards compatibility and tests. -impl WeightInfo for () { - /// Storage: `TreasuryConfig::Signatories` (r:1 w:1) - /// Proof: `TreasuryConfig::Signatories` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) - /// Storage: `TreasuryConfig::Threshold` (r:1 w:1) - /// Proof: `TreasuryConfig::Threshold` (`max_values`: Some(1), `max_size`: Some(2), added: 497, mode: `MaxEncodedLen`) - /// The range of component `s` is `[1, 100]`. - fn set_treasury_signatories(s: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `449` - // Estimated: `4687` - // Minimum execution time: 290_000_000 picoseconds. - Weight::from_parts(226_636_905, 0) - .saturating_add(Weight::from_parts(0, 4687)) - // Standard Error: 36_639 - .saturating_add(Weight::from_parts(4_652_600, 0).saturating_mul(s.into())) - .saturating_add(frame_support::weights::constants::RocksDbWeight::get().reads(2)) - .saturating_add(frame_support::weights::constants::RocksDbWeight::get().writes(2)) - } -} diff --git a/pallets/treasury-config/Cargo.toml b/pallets/treasury-multisig/Cargo.toml similarity index 96% rename from pallets/treasury-config/Cargo.toml rename to pallets/treasury-multisig/Cargo.toml index 2ff1bcbf..f1721556 100644 --- a/pallets/treasury-config/Cargo.toml +++ b/pallets/treasury-multisig/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "pallet-treasury-config" -version = "0.1.0" authors.workspace = true edition.workspace = true -license = "Apache-2.0" homepage.workspace = true -repository.workspace = true +license = "Apache-2.0" +name = "pallet-treasury-multisig" publish = false +repository.workspace = true +version = "0.1.0" [lints] workspace = true @@ -30,6 +30,12 @@ sp-runtime.workspace = true [features] default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] std = [ "codec/std", "frame-benchmarking?/std", @@ -41,15 +47,8 @@ std = [ "sp-runtime/std", "sp-weights/std", ] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", -] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "sp-runtime/try-runtime", ] - diff --git a/pallets/treasury-config/README.md b/pallets/treasury-multisig/README.md similarity index 94% rename from pallets/treasury-config/README.md rename to pallets/treasury-multisig/README.md index 434f539f..c64db6cf 100644 --- a/pallets/treasury-config/README.md +++ b/pallets/treasury-multisig/README.md @@ -1,4 +1,4 @@ -# Treasury Config Pallet +# Treasury Multisig Pallet A pallet for managing treasury multisig configuration in a Substrate-based blockchain. @@ -34,7 +34,7 @@ Emitted when treasury configuration changes, showing both the old and new multis ### Genesis Configuration ```rust -TreasuryConfigConfig { +TreasuryMultisigConfig { signatories: vec![alice, bob, charlie, dave, eve], threshold: 3, // 3-of-5 multisig } @@ -43,7 +43,7 @@ TreasuryConfigConfig { ### Updating via Governance ```rust -TreasuryConfig::set_treasury_signatories( +TreasuryMultisig::set_treasury_signatories( RuntimeOrigin::root(), vec![account1, account2, account3, account4, account5], 3 // new threshold @@ -53,7 +53,7 @@ TreasuryConfig::set_treasury_signatories( ### Getting Treasury Address ```rust -let treasury_address = TreasuryConfig::get_treasury_account(); +let treasury_address = TreasuryMultisig::get_treasury_account(); ``` The address is deterministically derived from signatories and threshold using the same algorithm as `pallet-multisig`. diff --git a/pallets/treasury-config/src/benchmarking.rs b/pallets/treasury-multisig/src/benchmarking.rs similarity index 100% rename from pallets/treasury-config/src/benchmarking.rs rename to pallets/treasury-multisig/src/benchmarking.rs diff --git a/pallets/treasury-config/src/lib.rs b/pallets/treasury-multisig/src/lib.rs similarity index 96% rename from pallets/treasury-config/src/lib.rs rename to pallets/treasury-multisig/src/lib.rs index df24b3eb..79ac97b2 100644 --- a/pallets/treasury-config/src/lib.rs +++ b/pallets/treasury-multisig/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(feature = "std"), no_std)] -#![doc = "# Treasury Config Pallet"] +#![doc = "# Treasury Multisig Pallet"] #![doc = ""] #![doc = "This pallet manages the treasury multisig configuration."] #![doc = "It stores the signatories and threshold for the treasury multisig account,"] @@ -8,7 +8,7 @@ extern crate alloc; pub use pallet::*; -pub use weights::WeightInfo; +pub use weights::*; #[cfg(test)] mod mock; @@ -24,7 +24,7 @@ pub mod weights; #[allow(clippy::expect_used)] #[frame_support::pallet] pub mod pallet { - use crate::WeightInfo; + use crate::weights::WeightInfo; use alloc::vec::Vec; use frame_support::{pallet_prelude::*, traits::Get}; use frame_system::pallet_prelude::*; @@ -212,9 +212,9 @@ pub mod pallet { Self::multi_account_id(&signatories.into_inner(), threshold) } - /// Calculate treasury account from given signatories and threshold WITHOUT accessing storage. - /// This can be used outside of runtime context (e.g., during node initialization). - /// NOTE: This must match the algorithm used by pallet-multisig. + /// Calculate treasury account from given signatories and threshold WITHOUT accessing + /// storage. This can be used outside of runtime context (e.g., during node + /// initialization). NOTE: This must match the algorithm used by pallet-multisig. pub fn calculate_treasury_account( signatories: &[T::AccountId], threshold: u16, diff --git a/pallets/treasury-config/src/mock.rs b/pallets/treasury-multisig/src/mock.rs similarity index 69% rename from pallets/treasury-config/src/mock.rs rename to pallets/treasury-multisig/src/mock.rs index 82b4528a..d06403a0 100644 --- a/pallets/treasury-config/src/mock.rs +++ b/pallets/treasury-multisig/src/mock.rs @@ -1,4 +1,4 @@ -use crate as pallet_treasury_config; +use crate as pallet_treasury_multisig; use frame_support::{derive_impl, parameter_types}; use sp_runtime::{traits::IdentityLookup, BuildStorage}; @@ -8,7 +8,7 @@ frame_support::construct_runtime!( pub enum Test { System: frame_system, - TreasuryConfig: pallet_treasury_config, + TreasuryMultisig: pallet_treasury_multisig, } ); @@ -23,16 +23,23 @@ parameter_types! { pub const MaxSignatories: u32 = 100; } -impl pallet_treasury_config::Config for Test { +impl crate::Config for Test { type MaxSignatories = MaxSignatories; type WeightInfo = (); } +// Mock implementation of WeightInfo for tests +impl crate::weights::WeightInfo for () { + fn set_treasury_signatories(_s: u32) -> frame_support::weights::Weight { + frame_support::weights::Weight::zero() + } +} + // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_treasury_config::GenesisConfig:: { + pallet_treasury_multisig::GenesisConfig:: { signatories: vec![1, 2, 3, 4, 5], threshold: 3, } diff --git a/pallets/treasury-config/src/tests.rs b/pallets/treasury-multisig/src/tests.rs similarity index 62% rename from pallets/treasury-config/src/tests.rs rename to pallets/treasury-multisig/src/tests.rs index c84e5cc9..ad3de4bb 100644 --- a/pallets/treasury-config/src/tests.rs +++ b/pallets/treasury-multisig/src/tests.rs @@ -5,15 +5,15 @@ use frame_support::{assert_noop, assert_ok}; fn genesis_config_works() { new_test_ext().execute_with(|| { // Check that genesis config was applied - let signatories = TreasuryConfig::signatories(); + let signatories = TreasuryMultisig::signatories(); assert_eq!(signatories.len(), 5); assert_eq!(signatories.to_vec(), vec![1, 2, 3, 4, 5]); - let threshold = TreasuryConfig::threshold(); + let threshold = TreasuryMultisig::threshold(); assert_eq!(threshold, 3); // Check that treasury account is computed correctly - let treasury_account = TreasuryConfig::get_treasury_account(); + let treasury_account = TreasuryMultisig::get_treasury_account(); assert_ne!(treasury_account, 0); // Should be non-zero }); } @@ -22,22 +22,22 @@ fn genesis_config_works() { fn set_treasury_signatories_works() { new_test_ext().execute_with(|| { System::set_block_number(1); // Events are registered from block 1 - let old_account = TreasuryConfig::get_treasury_account(); + let old_account = TreasuryMultisig::get_treasury_account(); // Update signatories let new_signatories = vec![10, 20, 30, 40, 50]; - assert_ok!(TreasuryConfig::set_treasury_signatories( + assert_ok!(TreasuryMultisig::set_treasury_signatories( RuntimeOrigin::root(), new_signatories.clone(), 3 )); // Check storage was updated - assert_eq!(TreasuryConfig::signatories().to_vec(), new_signatories); - assert_eq!(TreasuryConfig::threshold(), 3); + assert_eq!(TreasuryMultisig::signatories().to_vec(), new_signatories); + assert_eq!(TreasuryMultisig::threshold(), 3); // Check new account is different - let new_account = TreasuryConfig::get_treasury_account(); + let new_account = TreasuryMultisig::get_treasury_account(); assert_ne!(old_account, new_account); // Check event was emitted @@ -52,7 +52,11 @@ fn set_treasury_signatories_requires_root() { new_test_ext().execute_with(|| { // Try to update without root - should fail assert_noop!( - TreasuryConfig::set_treasury_signatories(RuntimeOrigin::signed(1), vec![10, 20, 30], 2), + TreasuryMultisig::set_treasury_signatories( + RuntimeOrigin::signed(1), + vec![10, 20, 30], + 2 + ), sp_runtime::DispatchError::BadOrigin ); }); @@ -63,7 +67,7 @@ fn set_treasury_signatories_validates_empty() { new_test_ext().execute_with(|| { // Empty signatories should fail assert_noop!( - TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![], 1), + TreasuryMultisig::set_treasury_signatories(RuntimeOrigin::root(), vec![], 1), Error::::EmptySignatories ); }); @@ -74,13 +78,13 @@ fn set_treasury_signatories_validates_threshold() { new_test_ext().execute_with(|| { // Threshold = 0 should fail assert_noop!( - TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3], 0), + TreasuryMultisig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3], 0), Error::::InvalidThreshold ); // Threshold > signatories should fail assert_noop!( - TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3], 4), + TreasuryMultisig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3], 4), Error::::InvalidThreshold ); }); @@ -91,18 +95,26 @@ fn set_treasury_signatories_rejects_duplicates() { new_test_ext().execute_with(|| { // Same signatory 5 times should fail assert_noop!( - TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 1, 1, 1, 1], 3), + TreasuryMultisig::set_treasury_signatories( + RuntimeOrigin::root(), + vec![1, 1, 1, 1, 1], + 3 + ), Error::::DuplicateSignatories ); // Partial duplicates should also fail assert_noop!( - TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), vec![1, 2, 3, 2, 4], 3), + TreasuryMultisig::set_treasury_signatories( + RuntimeOrigin::root(), + vec![1, 2, 3, 2, 4], + 3 + ), Error::::DuplicateSignatories ); // No duplicates should succeed - assert_ok!(TreasuryConfig::set_treasury_signatories( + assert_ok!(TreasuryMultisig::set_treasury_signatories( RuntimeOrigin::root(), vec![1, 2, 3, 4, 5], 3 @@ -117,7 +129,7 @@ fn set_treasury_signatories_validates_max() { let too_many: Vec = (0..101).collect(); assert_noop!( - TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), too_many, 50), + TreasuryMultisig::set_treasury_signatories(RuntimeOrigin::root(), too_many, 50), Error::::TooManySignatories ); }); @@ -129,16 +141,20 @@ fn changing_threshold_changes_address() { let signatories = vec![1, 2, 3, 4, 5]; // Set threshold to 2 - assert_ok!(TreasuryConfig::set_treasury_signatories( + assert_ok!(TreasuryMultisig::set_treasury_signatories( RuntimeOrigin::root(), signatories.clone(), 2 )); - let account_threshold_2 = TreasuryConfig::get_treasury_account(); + let account_threshold_2 = TreasuryMultisig::get_treasury_account(); // Set threshold to 4 (same signatories) - assert_ok!(TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), signatories, 4)); - let account_threshold_4 = TreasuryConfig::get_treasury_account(); + assert_ok!(TreasuryMultisig::set_treasury_signatories( + RuntimeOrigin::root(), + signatories, + 4 + )); + let account_threshold_4 = TreasuryMultisig::get_treasury_account(); // Addresses should be different assert_ne!(account_threshold_2, account_threshold_4); @@ -151,23 +167,27 @@ fn deterministic_address_generation() { let signatories = vec![1, 2, 3]; // Set signatories - assert_ok!(TreasuryConfig::set_treasury_signatories( + assert_ok!(TreasuryMultisig::set_treasury_signatories( RuntimeOrigin::root(), signatories.clone(), 2 )); - let account1 = TreasuryConfig::get_treasury_account(); + let account1 = TreasuryMultisig::get_treasury_account(); // Change to different signatories - assert_ok!(TreasuryConfig::set_treasury_signatories( + assert_ok!(TreasuryMultisig::set_treasury_signatories( RuntimeOrigin::root(), vec![10, 20, 30], 2 )); // Change back to original - assert_ok!(TreasuryConfig::set_treasury_signatories(RuntimeOrigin::root(), signatories, 2)); - let account2 = TreasuryConfig::get_treasury_account(); + assert_ok!(TreasuryMultisig::set_treasury_signatories( + RuntimeOrigin::root(), + signatories, + 2 + )); + let account2 = TreasuryMultisig::get_treasury_account(); // Should get the same address assert_eq!(account1, account2); diff --git a/pallets/treasury-multisig/src/weights.rs b/pallets/treasury-multisig/src/weights.rs new file mode 100644 index 00000000..47531add --- /dev/null +++ b/pallets/treasury-multisig/src/weights.rs @@ -0,0 +1,54 @@ + +//! Autogenerated weights for `pallet_treasury_multisig` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-01-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 +// --chain=dev +// --pallet=pallet_treasury_multisig +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --output=pallets/treasury-multisig/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_treasury_multisig`. +pub trait WeightInfo { + fn set_treasury_signatories(s: u32) -> Weight; +} + +/// Weights for `pallet_treasury_multisig` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `TreasuryMultisig::Signatories` (r:1 w:1) + /// Proof: `TreasuryMultisig::Signatories` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) + /// Storage: `TreasuryMultisig::Threshold` (r:1 w:1) + /// Proof: `TreasuryMultisig::Threshold` (`max_values`: Some(1), `max_size`: Some(2), added: 497, mode: `MaxEncodedLen`) + /// The range of component `s` is `[1, 100]`. + fn set_treasury_signatories(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `481` + // Estimated: `4687` + // Minimum execution time: 296_000_000 picoseconds. + Weight::from_parts(226_102_074, 0) + .saturating_add(Weight::from_parts(0, 4687)) + // Standard Error: 36_316 + .saturating_add(Weight::from_parts(4_678_327, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 1f52525c..840049f4 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-config.workspace = true +pallet-treasury-multisig.workspace = true pallet-utility.workspace = true primitive-types.workspace = true qp-dilithium-crypto.workspace = true @@ -108,7 +108,7 @@ std = [ "pallet-timestamp/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", - "pallet-treasury-config/std", + "pallet-treasury-multisig/std", "pallet-utility/std", "primitive-types/std", "qp-dilithium-crypto/full_crypto", @@ -155,7 +155,7 @@ runtime-benchmarks = [ "pallet-sudo/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", - "pallet-treasury-config/runtime-benchmarks", + "pallet-treasury-multisig/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -173,7 +173,7 @@ try-runtime = [ "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", - "pallet-treasury-config/try-runtime", + "pallet-treasury-multisig/try-runtime", "sp-runtime/try-runtime", ] diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index 1197c618..0530409b 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -33,6 +33,6 @@ frame_benchmarking::define_benchmarks!( [pallet_mining_rewards, MiningRewards] [pallet_scheduler, Scheduler] [pallet_qpow, QPoW] - [pallet_treasury_config, TreasuryConfig] + [pallet_treasury_multisig, TreasuryMultisig] [pallet_multisig, Multisig] ); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index c533cca4..a44f15d9 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -28,7 +28,8 @@ use crate::{ governance::{ definitions::{ CommunityTracksInfo, GlobalMaxMembers, MinRankOfClassConverter, PreimageDeposit, - RootOrMemberForCollectiveOrigin, RootOrMemberForTechReferendaOrigin, TechCollectiveTracksInfo, + RootOrMemberForCollectiveOrigin, RootOrMemberForTechReferendaOrigin, + TechCollectiveTracksInfo, }, pallet_custom_origins, Spender, }, @@ -54,10 +55,7 @@ use pallet_ranked_collective::Linear; use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier}; use qp_poseidon::PoseidonHasher; use qp_scheduler::BlockNumberOrTimestamp; -use sp_runtime::{ - traits::One, - FixedU128, Perbill, Permill, -}; +use sp_runtime::{traits::One, FixedU128, Perbill, Permill}; use sp_version::RuntimeVersion; // Local module imports @@ -130,7 +128,7 @@ impl pallet_mining_rewards::Config for Runtime { type WeightInfo = pallet_mining_rewards::weights::SubstrateWeight; type MinerBlockReward = ConstU128<{ 10 * UNIT }>; // 10 tokens type TreasuryBlockReward = ConstU128<0>; // 0 tokens - type TreasuryAccountId = pallet_treasury_config::TreasuryAccount; + type TreasuryAccountId = pallet_treasury_multisig::TreasuryAccount; type MintingAccount = MintingAccount; } @@ -478,7 +476,7 @@ impl pallet_reversible_transfers::Config for Runtime { type TimeProvider = Timestamp; type MaxInterceptorAccounts = MaxInterceptorAccounts; type VolumeFee = HighSecurityVolumeFee; - type TreasuryAccountId = pallet_treasury_config::TreasuryAccount; + type TreasuryAccountId = pallet_treasury_multisig::TreasuryAccount; } parameter_types! { @@ -558,9 +556,9 @@ impl pallet_multisig::Config for Runtime { // treasury addresses from genesis, while still being the same runtime build. // ============================================================================= -impl pallet_treasury_config::Config for Runtime { +impl pallet_treasury_multisig::Config for Runtime { type MaxSignatories = MaxSignatories; - type WeightInfo = pallet_treasury_config::weights::SubstrateWeight; + type WeightInfo = pallet_treasury_multisig::weights::SubstrateWeight; } impl TryFrom for pallet_balances::Call { diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 759060d6..672e9ad5 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -18,7 +18,7 @@ // this module is used by the client, so it's ok to panic/unwrap here #![allow(clippy::expect_used)] -use crate::{AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, TreasuryConfigConfig}; +use crate::{AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, TreasuryMultisigConfig}; use alloc::{vec, vec::Vec}; use qp_dilithium_crypto::pair::{ crystal_alice, crystal_charlie, crystal_eve, dilithium_bob, dilithium_dave, @@ -67,7 +67,7 @@ fn dirac_treasury_signatories() -> Vec { account_from_ss58("qznYQKUeV5un22rXh7CCQB7Bsac74jynVDs2qbHk1hpPMjocB"), account_from_ss58("qzn2h1xdg8N1QCLbL5BYxAikYvpVnyELtFkYqHEhwrDTx9bhr"), dirac_faucet_account(), - account_from_ss58("qznYQKUeV5un22rXh7CCQB7Bsac74jynVDs2qbHk1hpPMjocB"), // TODO: 5th signatory + account_from_ss58("qznYQKUeV5un22rXh7CCQB7Bsac74jynVDs2qbHk1hpPMjocB"), /* TODO: 5th signatory */ ] } @@ -107,7 +107,7 @@ fn genesis_template( let config = RuntimeGenesisConfig { balances: BalancesConfig { balances }, sudo: SudoConfig { key: Some(root.clone()) }, - treasury_config: TreasuryConfigConfig { + treasury_multisig: TreasuryMultisigConfig { signatories: treasury_signatories, threshold: treasury_threshold, }, diff --git a/runtime/src/governance/definitions.rs b/runtime/src/governance/definitions.rs index cd83d30c..126d549c 100644 --- a/runtime/src/governance/definitions.rs +++ b/runtime/src/governance/definitions.rs @@ -1,6 +1,6 @@ use crate::{ governance::pallet_custom_origins, AccountId, Balance, Balances, BlockNumber, Runtime, - RuntimeOrigin, TreasuryConfig, DAYS, HOURS, MICRO_UNIT, UNIT, + RuntimeOrigin, TreasuryMultisig, DAYS, HOURS, MICRO_UNIT, UNIT, }; use alloc::borrow::Cow; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; @@ -482,13 +482,12 @@ where let pallets_origin = o.into_caller(); match pallets_origin { - crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => { + crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => if pallet_ranked_collective::Members::::contains_key(&who) { Ok(0) } else { Err(original_o_for_error) - } - }, + }, _ => Err(original_o_for_error), } } @@ -532,13 +531,12 @@ where let pallets_origin = o.into_caller(); match pallets_origin { - crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => { + crate::OriginCaller::system(frame_system::RawOrigin::Signed(who)) => if pallet_ranked_collective::Members::::contains_key(&who) { Ok(who) } else { Err(original_o_for_error) - } - }, + }, _ => Err(original_o_for_error), } } @@ -584,7 +582,7 @@ impl Pay for RuntimeNativePaymaster { _asset_kind: Self::AssetKind, amount: Self::Balance, ) -> Result { - let treasury_account = TreasuryConfig::get_treasury_account(); + let treasury_account = TreasuryMultisig::get_treasury_account(); >::transfer( &treasury_account, who, @@ -608,7 +606,7 @@ impl Pay for RuntimeNativePaymaster { _asset_kind: Self::AssetKind, amount: Self::Balance, ) { - let treasury_account = TreasuryConfig::get_treasury_account(); + let treasury_account = TreasuryMultisig::get_treasury_account(); let current_balance = crate::Balances::free_balance(&treasury_account); if current_balance < amount { let missing = amount - current_balance; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b488925b..bf33e77f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -255,5 +255,5 @@ mod runtime { pub type Multisig = pallet_multisig; #[runtime::pallet_index(24)] - pub type TreasuryConfig = pallet_treasury_config; + pub type TreasuryMultisig = pallet_treasury_multisig; } diff --git a/runtime/tests/common.rs b/runtime/tests/common.rs index f741f374..64cbcd99 100644 --- a/runtime/tests/common.rs +++ b/runtime/tests/common.rs @@ -1,10 +1,7 @@ -use frame_support::{ - traits::{Currency, OnFinalize, OnInitialize}, - PalletId, -}; +use frame_support::traits::{Currency, OnFinalize, OnInitialize}; use quantus_runtime::{Balances, Runtime, System, UNIT}; use sp_core::crypto::AccountId32; -use sp_runtime::{traits::AccountIdConversion, BuildStorage}; +use sp_runtime::BuildStorage; pub struct TestCommons; @@ -28,7 +25,7 @@ impl TestCommons { 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 multisig account for volume fee collection - let treasury_account = quantus_runtime::TreasuryConfig::get_treasury_account(); + let treasury_account = quantus_runtime::TreasuryMultisig::get_treasury_account(); Balances::make_free_balance_be(&treasury_account, 1000 * UNIT); }); diff --git a/runtime/tests/governance/tech_collective.rs b/runtime/tests/governance/tech_collective.rs index 97058a2a..8fc3e0f8 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; @@ -799,10 +796,10 @@ mod tests { // Run to the end of voting TestCommons::run_to_block( - track_info.prepare_period + - track_info.decision_period + - track_info.confirm_period + - 5, + track_info.prepare_period + + track_info.decision_period + + track_info.confirm_period + + 5, ); // Check referendum state - if votes are equal, it should be rejected as the default @@ -902,10 +899,10 @@ mod tests { // Wait until the end of the confirm phase for the second referendum // Use relative advancement to avoid overflow let current_block_for_second_confirm = frame_system::Pallet::::block_number(); - let blocks_to_advance_for_second = track_info.decision_period + - track_info.confirm_period + - track_info.min_enactment_period + - 5; + let blocks_to_advance_for_second = track_info.decision_period + + track_info.confirm_period + + track_info.min_enactment_period + + 5; TestCommons::run_to_block( current_block_for_second_confirm + blocks_to_advance_for_second, ); @@ -1138,10 +1135,10 @@ mod tests { // Wait for the confirmation period for the fourth referendum to complete // Use relative advancement to avoid overflow with fast governance timing let current_block = frame_system::Pallet::::block_number(); - let blocks_to_advance = track_info.prepare_period + - track_info.decision_period + - track_info.confirm_period + - 5; + let blocks_to_advance = track_info.prepare_period + + track_info.decision_period + + track_info.confirm_period + + 5; TestCommons::run_to_block(current_block + blocks_to_advance); // Check fourth referendum outcome @@ -1289,5 +1286,4 @@ mod tests { // Treasury spend tests have been removed // pallet-treasury has been replaced with pallet-treasury-config // which only manages multisig configuration, not spend proposals - } From 74811441d660316a085fd9346ab42a7f4d57fb5f Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Tue, 13 Jan 2026 12:32:46 +0800 Subject: [PATCH 6/9] feat: FMT --- node/src/command.rs | 25 +++++++++------------ runtime/tests/governance/tech_collective.rs | 24 ++++++++++---------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/node/src/command.rs b/node/src/command.rs index 94814c7f..6314cf73 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -182,25 +182,21 @@ impl SubstrateCli for Cli { fn load_spec(&self, id: &str) -> Result, String> { Ok(match id { - "dev" => { - Box::new(chain_spec::development_chain_spec()?) as Box - }, - "dirac_live_spec" => { - Box::new(chain_spec::dirac_chain_spec()?) as Box - }, + "dev" => + Box::new(chain_spec::development_chain_spec()?) as Box, + "dirac_live_spec" => + Box::new(chain_spec::dirac_chain_spec()?) as Box, "dirac" => Box::new(chain_spec::ChainSpec::from_json_bytes(include_bytes!( "chain-specs/dirac.json" ))?) as Box, - "heisenberg_live_spec" => { - Box::new(chain_spec::heisenberg_chain_spec()?) as Box - }, + "heisenberg_live_spec" => + Box::new(chain_spec::heisenberg_chain_spec()?) as Box, "" | "heisenberg" => Box::new(chain_spec::ChainSpec::from_json_bytes(include_bytes!( "chain-specs/heisenberg.json" ))?) as Box, - path => { + path => Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?) - as Box - }, + as Box, }) } } @@ -429,9 +425,8 @@ pub fn run() -> sc_cli::Result<()> { cmd.run(client, inherent_benchmark_data()?, Vec::new(), &ext_factory) }, - BenchmarkCmd::Machine(cmd) => { - cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()) - }, + BenchmarkCmd::Machine(cmd) => + cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()), } }) }, diff --git a/runtime/tests/governance/tech_collective.rs b/runtime/tests/governance/tech_collective.rs index 8fc3e0f8..b0c8eb70 100644 --- a/runtime/tests/governance/tech_collective.rs +++ b/runtime/tests/governance/tech_collective.rs @@ -796,10 +796,10 @@ mod tests { // Run to the end of voting TestCommons::run_to_block( - track_info.prepare_period - + track_info.decision_period - + track_info.confirm_period - + 5, + track_info.prepare_period + + track_info.decision_period + + track_info.confirm_period + + 5, ); // Check referendum state - if votes are equal, it should be rejected as the default @@ -899,10 +899,10 @@ mod tests { // Wait until the end of the confirm phase for the second referendum // Use relative advancement to avoid overflow let current_block_for_second_confirm = frame_system::Pallet::::block_number(); - let blocks_to_advance_for_second = track_info.decision_period - + track_info.confirm_period - + track_info.min_enactment_period - + 5; + let blocks_to_advance_for_second = track_info.decision_period + + track_info.confirm_period + + track_info.min_enactment_period + + 5; TestCommons::run_to_block( current_block_for_second_confirm + blocks_to_advance_for_second, ); @@ -1135,10 +1135,10 @@ mod tests { // Wait for the confirmation period for the fourth referendum to complete // Use relative advancement to avoid overflow with fast governance timing let current_block = frame_system::Pallet::::block_number(); - let blocks_to_advance = track_info.prepare_period - + track_info.decision_period - + track_info.confirm_period - + 5; + let blocks_to_advance = track_info.prepare_period + + track_info.decision_period + + track_info.confirm_period + + 5; TestCommons::run_to_block(current_block + blocks_to_advance); // Check fourth referendum outcome From 5432fb20d21c036a6f5ed6b746e3bb10fbb4c224 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Tue, 13 Jan 2026 12:46:55 +0800 Subject: [PATCH 7/9] fix: dilithium_ -> crystal_ dev accounts rename --- primitives/dilithium-crypto/src/lib.rs | 2 +- primitives/dilithium-crypto/src/pair.rs | 2 +- runtime/src/genesis_config_presets.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/primitives/dilithium-crypto/src/lib.rs b/primitives/dilithium-crypto/src/lib.rs index bd9cd811..b3ccbf87 100644 --- a/primitives/dilithium-crypto/src/lib.rs +++ b/primitives/dilithium-crypto/src/lib.rs @@ -13,7 +13,7 @@ pub const SECRET_KEY_BYTES: usize = ml_dsa_87::SECRETKEYBYTES; pub const SIGNATURE_BYTES: usize = ml_dsa_87::SIGNBYTES; pub use pair::{ - create_keypair, crystal_alice, crystal_charlie, crystal_eve, dilithium_bob, dilithium_dave, + create_keypair, crystal_alice, crystal_charlie, crystal_dave, crystal_eve, dilithium_bob, generate, }; pub use traits::verify; diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index e3c245ec..4be9f270 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -27,7 +27,7 @@ pub fn crystal_charlie() -> DilithiumPair { let seed = [2u8; 32]; DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") } -pub fn dilithium_dave() -> DilithiumPair { +pub fn crystal_dave() -> DilithiumPair { let seed = [3u8; 32]; DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") } diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 672e9ad5..43172410 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -21,7 +21,7 @@ use crate::{AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, TreasuryMultisigConfig}; use alloc::{vec, vec::Vec}; use qp_dilithium_crypto::pair::{ - crystal_alice, crystal_charlie, crystal_eve, dilithium_bob, dilithium_dave, + crystal_alice, crystal_charlie, crystal_dave, crystal_eve, dilithium_bob, }; use serde_json::Value; use sp_core::crypto::Ss58Codec; @@ -54,7 +54,7 @@ fn dev_treasury_signatories() -> Vec { crystal_alice().into_account(), dilithium_bob().into_account(), crystal_charlie().into_account(), - dilithium_dave().into_account(), + crystal_dave().into_account(), crystal_eve().into_account(), ] } @@ -88,7 +88,7 @@ fn dilithium_default_accounts() -> Vec { crystal_alice().into_account(), dilithium_bob().into_account(), crystal_charlie().into_account(), - dilithium_dave().into_account(), + crystal_dave().into_account(), crystal_eve().into_account(), ] } From e205f6202bc552698bdeb548f679c523a6147f5b Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Tue, 13 Jan 2026 13:02:57 +0800 Subject: [PATCH 8/9] fix: dilithium_bob renamed --- node/src/benchmarking.rs | 6 +++--- primitives/dilithium-crypto/src/lib.rs | 2 +- primitives/dilithium-crypto/src/pair.rs | 2 +- runtime/src/genesis_config_presets.rs | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 2d3d33eb..51fe5c1f 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -12,7 +12,7 @@ use sp_core::{Encode, Pair}; use sp_inherents::{InherentData, InherentDataProvider}; use sp_runtime::{traits::IdentifyAccount, OpaqueExtrinsic, SaturatedConversion}; -use qp_dilithium_crypto::{self, dilithium_bob}; +use qp_dilithium_crypto::{self, crystal_bob}; use std::{sync::Arc, time::Duration}; @@ -40,7 +40,7 @@ impl frame_benchmarking_cli::ExtrinsicBuilder for RemarkBuilder { } fn build(&self, nonce: u32) -> std::result::Result { - let acc = dilithium_bob(); + let acc = crystal_bob(); let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( self.client.as_ref(), acc, @@ -79,7 +79,7 @@ impl frame_benchmarking_cli::ExtrinsicBuilder for TransferKeepAliveBuilder { } fn build(&self, nonce: u32) -> std::result::Result { - let acc = dilithium_bob(); + let acc = crystal_bob(); let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( self.client.as_ref(), acc, diff --git a/primitives/dilithium-crypto/src/lib.rs b/primitives/dilithium-crypto/src/lib.rs index b3ccbf87..5118b627 100644 --- a/primitives/dilithium-crypto/src/lib.rs +++ b/primitives/dilithium-crypto/src/lib.rs @@ -13,7 +13,7 @@ pub const SECRET_KEY_BYTES: usize = ml_dsa_87::SECRETKEYBYTES; pub const SIGNATURE_BYTES: usize = ml_dsa_87::SIGNBYTES; pub use pair::{ - create_keypair, crystal_alice, crystal_charlie, crystal_dave, crystal_eve, dilithium_bob, + create_keypair, crystal_alice, crystal_bob, crystal_charlie, crystal_dave, crystal_eve, generate, }; pub use traits::verify; diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 4be9f270..76fa48a2 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -19,7 +19,7 @@ pub fn crystal_alice() -> DilithiumPair { let seed = [0u8; 32]; DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") } -pub fn dilithium_bob() -> DilithiumPair { +pub fn crystal_bob() -> DilithiumPair { let seed = [1u8; 32]; DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") } diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 43172410..93fd046e 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -21,7 +21,7 @@ use crate::{AccountId, BalancesConfig, RuntimeGenesisConfig, SudoConfig, TreasuryMultisigConfig}; use alloc::{vec, vec::Vec}; use qp_dilithium_crypto::pair::{ - crystal_alice, crystal_charlie, crystal_dave, crystal_eve, dilithium_bob, + crystal_alice, crystal_bob, crystal_charlie, crystal_dave, crystal_eve, }; use serde_json::Value; use sp_core::crypto::Ss58Codec; @@ -52,7 +52,7 @@ pub const TREASURY_THRESHOLD: u16 = 3; fn dev_treasury_signatories() -> Vec { vec![ crystal_alice().into_account(), - dilithium_bob().into_account(), + crystal_bob().into_account(), crystal_charlie().into_account(), crystal_dave().into_account(), crystal_eve().into_account(), @@ -86,7 +86,7 @@ pub fn get_treasury_config_for_chain(chain_id: &str) -> Option<(Vec, fn dilithium_default_accounts() -> Vec { vec![ crystal_alice().into_account(), - dilithium_bob().into_account(), + crystal_bob().into_account(), crystal_charlie().into_account(), crystal_dave().into_account(), crystal_eve().into_account(), From 2321f9b539c7b1775c2d31e55740a37a9a8224f9 Mon Sep 17 00:00:00 2001 From: Cezary Olborski Date: Thu, 15 Jan 2026 14:37:50 +0800 Subject: [PATCH 9/9] feat: Treasury Multisig - methods in API --- Cargo.lock | 1 + pallets/treasury-multisig/Cargo.toml | 2 ++ pallets/treasury-multisig/src/lib.rs | 13 +++++++++++++ runtime/src/apis.rs | 16 +++++++++++++++- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3d3b628f..a2ef5b56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7536,6 +7536,7 @@ dependencies = [ "frame-system", "parity-scale-codec", "scale-info", + "sp-api", "sp-core", "sp-io", "sp-runtime", diff --git a/pallets/treasury-multisig/Cargo.toml b/pallets/treasury-multisig/Cargo.toml index f1721556..18e7161d 100644 --- a/pallets/treasury-multisig/Cargo.toml +++ b/pallets/treasury-multisig/Cargo.toml @@ -19,6 +19,7 @@ scale-info = { workspace = true } frame-benchmarking = { optional = true, workspace = true } frame-support.workspace = true frame-system.workspace = true +sp-api = { workspace = true } sp-core.workspace = true sp-io.workspace = true sp-runtime.workspace = true @@ -42,6 +43,7 @@ std = [ "frame-support/std", "frame-system/std", "scale-info/std", + "sp-api/std", "sp-core/std", "sp-io/std", "sp-runtime/std", diff --git a/pallets/treasury-multisig/src/lib.rs b/pallets/treasury-multisig/src/lib.rs index 79ac97b2..9d95e980 100644 --- a/pallets/treasury-multisig/src/lib.rs +++ b/pallets/treasury-multisig/src/lib.rs @@ -20,6 +20,19 @@ mod benchmarking; pub mod weights; +// Runtime API definition +sp_api::decl_runtime_apis! { + /// API to query treasury multisig configuration + pub trait TreasuryMultisigApi where AccountId: codec::Codec { + /// Get the computed treasury multisig account address + fn get_treasury_account() -> AccountId; + /// Get the list of signatories + fn get_signatories() -> alloc::vec::Vec; + /// Get the signature threshold + fn get_threshold() -> u16; + } +} + // Allow expect_used in pallet macro expansions - these are auto-generated by frame_support #[allow(clippy::expect_used)] #[frame_support::pallet] diff --git a/runtime/src/apis.rs b/runtime/src/apis.rs index eb9f8046..707ff1db 100644 --- a/runtime/src/apis.rs +++ b/runtime/src/apis.rs @@ -41,7 +41,7 @@ use sp_version::RuntimeVersion; // Local module imports use super::{ AccountId, Balance, Block, Executive, InherentDataExt, Nonce, Runtime, RuntimeCall, - RuntimeGenesisConfig, System, TransactionPayment, VERSION, + RuntimeGenesisConfig, System, TransactionPayment, TreasuryMultisig, VERSION, }; impl_runtime_apis! { @@ -305,4 +305,18 @@ impl_runtime_apis! { crate::genesis_config_presets::preset_names() } } + + impl pallet_treasury_multisig::TreasuryMultisigApi for Runtime { + fn get_treasury_account() -> AccountId { + TreasuryMultisig::get_treasury_account() + } + + fn get_signatories() -> Vec { + TreasuryMultisig::signatories().into_inner() + } + + fn get_threshold() -> u16 { + TreasuryMultisig::threshold() + } + } }