From 0d42a9f8414a8413a15fd8b7f18e88d12b89f413 Mon Sep 17 00:00:00 2001 From: doordashcon Date: Thu, 20 Nov 2025 12:30:45 -0300 Subject: [PATCH 01/21] add AssetCategoryManager --- substrate/frame/treasury/src/lib.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 06dfd0e93e7b..6bfa137e7b42 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -195,6 +195,11 @@ pub enum PaymentState { Failed, } +pub enum SpendAsset { + Specific(AssetKind), + Category(BoundedVec>), // e.g., "USD*", "EUR*" +} + /// Info regarding an approved treasury spend. #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[derive( @@ -209,8 +214,9 @@ pub enum PaymentState { TypeInfo, )] pub struct SpendStatus { - // The kind of asset to be spent. - pub asset_kind: AssetKind, + pub asset: SpendAsset, + // The kind of asset to be spent. + //pub asset_kind: AssetKind, /// The asset amount of the spend. pub amount: AssetBalance, /// The beneficiary of the spend. @@ -223,6 +229,17 @@ pub struct SpendStatus, } +/// Trait for managing asset categories +pub trait AssetCategoryManager { + type AssetKind; + + /// Get all assets in a category + fn assets_in_category(category: &[u8]) -> Vec; + + /// Optional: Check if an asset belongs to a category + fn is_asset_in_category(asset: &Self::AssetKind, category: &[u8]) -> bool; +} + /// Index of an approved treasury spend. pub type SpendIndex = u32; @@ -318,6 +335,8 @@ pub mod pallet { /// Provider for the block number. Normally this is the `frame_system` pallet. type BlockNumberProvider: BlockNumberProvider; + + type AssetCategories: AssetCategoryManager; } #[pallet::extra_constants] From 5fe9252a282f5caf22f28254bca4cfb29baca750 Mon Sep 17 00:00:00 2001 From: doordashcon Date: Thu, 18 Dec 2025 11:10:14 +0100 Subject: [PATCH 02/21] currently on the partial payment issue --- substrate/frame/treasury/src/lib.rs | 291 ++++++++-- substrate/frame/treasury/src/tests.rs | 772 ++++++++++++++++++++++++-- 2 files changed, 983 insertions(+), 80 deletions(-) diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 6bfa137e7b42..00a2c5fe362f 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -96,6 +96,8 @@ use sp_runtime::{ PerThing, Permill, RuntimeDebug, }; +use sp_runtime::traits::ConstU32; + use frame_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo}, ensure, print, @@ -186,18 +188,60 @@ pub struct Proposal { RuntimeDebug, TypeInfo, )] -pub enum PaymentState { +pub enum PaymentState { /// Pending claim. Pending, - /// Payment attempted with a payment identifier. - Attempted { id: Id }, + // Payment attempted with a payment identifier. + //Attempted { id: Id }, + /// Payment attempted with payment identifiers for each partial payment. + /// For categories: contains partial payments across multiple assets + Attempted { + executions: BoundedVec, ConstU32<32>>, + remaining_amount: Balance, + }, /// Payment failed. Failed, } -pub enum SpendAsset { - Specific(AssetKind), - Category(BoundedVec>), // e.g., "USD*", "EUR*" +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[derive( + Encode, + Decode, + DecodeWithMemTracking, + Clone, + PartialEq, + Eq, + MaxEncodedLen, + RuntimeDebug, + TypeInfo, +)] +pub enum SpendAsset { + /// Spend a specific asset + Specific(AssetKind), + /// Spend from a category of assets + Category(BoundedVec>), +} +// TODO: Move to primitives +/// Represents a partial payment using a specific asset from a category +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[derive( + Encode, + Decode, + DecodeWithMemTracking, + Clone, + PartialEq, + Eq, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, +)] +pub struct PaymentExecution { + /// The asset used for this partial payment + pub asset: AssetKind, + /// Amount paid using this asset + pub amount: Balance, + /// Payment identifier for this partial payment + pub payment_id: PaymentId, } /// Info regarding an approved treasury spend. @@ -214,8 +258,8 @@ pub enum SpendAsset { TypeInfo, )] pub struct SpendStatus { - pub asset: SpendAsset, - // The kind of asset to be spent. + pub asset: SpendAsset, + // The kind of asset to be spent. //pub asset_kind: AssetKind, /// The asset amount of the spend. pub amount: AssetBalance, @@ -226,18 +270,20 @@ pub struct SpendStatus, + pub status: PaymentState, } +// TODO: Move to primitivei /// Trait for managing asset categories pub trait AssetCategoryManager { - type AssetKind; + type AssetKind; + type Balance: Zero + PartialOrd + Copy + Saturating + sp_runtime::traits::AtLeast32BitUnsigned; - /// Get all assets in a category - fn assets_in_category(category: &[u8]) -> Vec; + /// Get all assets in a category + fn assets_in_category(category: &[u8]) -> Vec; - /// Optional: Check if an asset belongs to a category - fn is_asset_in_category(asset: &Self::AssetKind, category: &[u8]) -> bool; + /// Get available balance of a specific asset in treasury + fn available_balance(asset: &Self::AssetKind) -> Self::Balance; } /// Index of an approved treasury spend. @@ -310,7 +356,7 @@ pub mod pallet { /// Type parameter used to identify the beneficiaries eligible to receive treasury spends. type Beneficiary: Parameter + MaxEncodedLen; - /// Converting trait to take a source type and convert to [`Self::Beneficiary`]. + /// Converting trainnt to take a source type and convert to [`Self::Beneficiary`]. type BeneficiaryLookup: StaticLookup; /// Type for processing spends of [Self::AssetKind] in favor of [`Self::Beneficiary`]. @@ -336,7 +382,10 @@ pub mod pallet { /// Provider for the block number. Normally this is the `frame_system` pallet. type BlockNumberProvider: BlockNumberProvider; - type AssetCategories: AssetCategoryManager; + type AssetCategories: AssetCategoryManager< + AssetKind = Self::AssetKind, + Balance = AssetBalanceOf, + >; } #[pallet::extra_constants] @@ -448,7 +497,7 @@ pub mod pallet { /// A new asset spend proposal has been approved. AssetSpendApproved { index: SpendIndex, - asset_kind: T::AssetKind, + asset: SpendAsset, amount: AssetBalanceOf, beneficiary: T::Beneficiary, valid_from: BlockNumberFor, @@ -457,7 +506,12 @@ pub mod pallet { /// An approved spend was voided. AssetSpendVoided { index: SpendIndex }, /// A payment happened. - Paid { index: SpendIndex, payment_id: ::Id }, + // Paid { index: SpendIndex, payment_id: ::Id }, + Paid { + index: SpendIndex, + execution: + PaymentExecution, ::Id>, + }, /// A payment failed and can be retried. PaymentFailed { index: SpendIndex, payment_id: ::Id }, /// A spend was processed and removed from the storage. It might have been successfully @@ -478,7 +532,7 @@ pub mod pallet { /// Proposal has not been approved. ProposalNotApproved, /// The balance of the asset kind is not convertible to the balance of the native asset. - FailedToConvertBalance, + BalanceConversionFailed, /// The spend has expired and cannot be claimed. SpendExpired, /// The spend is not yet eligible for payout. @@ -491,6 +545,8 @@ pub mod pallet { NotAttempted, /// The payment has neither failed nor succeeded yet. Inconclusive, + + EmptyAssetCategory, } #[pallet::hooks] @@ -695,7 +751,8 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::spend())] pub fn spend( origin: OriginFor, - asset_kind: Box, + // asset_kind: Box, + asset: Box>, #[pallet::compact] amount: AssetBalanceOf, beneficiary: Box>, valid_from: Option>, @@ -708,10 +765,27 @@ pub mod pallet { let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); ensure!(expire_at > now, Error::::SpendExpired); - let native_amount = - T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone()) - .map_err(|_| Error::::FailedToConvertBalance)?; + let native_amount = match *asset { + SpendAsset::Specific(ref asset_kind) => + T::BalanceConverter::from_asset_balance(amount, asset_kind.clone()) + .map_err(|_| Error::::BalanceConversionFailed)?, + // TODO: Add in runtime, assumes that assets in a category are of + // the same denomination(i.e. USD* to USDT, USDC, BUSD e.t.c.) + SpendAsset::Category(ref category) => { + let assets = T::AssetCategories::assets_in_category(&category); + + if assets.is_empty() { + return Err(Error::::EmptyAssetCategory.into()); + } + assets + .iter() + .find_map(|asset_kind| { + T::BalanceConverter::from_asset_balance(amount, asset_kind.clone()).ok() + }) + .ok_or(Error::::BalanceConversionFailed)? + }, + }; ensure!(native_amount <= max_amount, Error::::InsufficientPermission); with_context::>, _>(|v| { @@ -736,7 +810,8 @@ pub mod pallet { Spends::::insert( index, SpendStatus { - asset_kind: *asset_kind.clone(), + asset: *asset.clone(), + //asset_kind: *asset_kind.clone(), amount, beneficiary: beneficiary.clone(), valid_from, @@ -748,7 +823,8 @@ pub mod pallet { Self::deposit_event(Event::AssetSpendApproved { index, - asset_kind: *asset_kind, + asset: *asset, + // asset_kind: *asset_kind, amount, beneficiary, valid_from, @@ -789,15 +865,109 @@ pub mod pallet { Error::::AlreadyAttempted ); - let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount) - .map_err(|_| Error::::PayoutError)?; + match spend.asset { + SpendAsset::Specific(ref asset_kind) => { + let id = + T::Paymaster::pay(&spend.beneficiary, asset_kind.clone(), spend.amount) + .map_err(|_| Error::::PayoutError)?; + + spend.status = PaymentState::Attempted { + executions: BoundedVec::try_from(vec![PaymentExecution { + asset: asset_kind.clone(), + amount: spend.amount, + payment_id: id, + }]) + .map_err(|_| Error::::PayoutError)?, + remaining_amount: Zero::zero(), + }; + }, + SpendAsset::Category(ref category) => { + let assets = T::AssetCategories::assets_in_category(&category); + if assets.is_empty() { + return Err(Error::::EmptyAssetCategory.into()); + } - spend.status = PaymentState::Attempted { id }; - spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); - Spends::::insert(index, spend); + let mut remaining_amount = spend.amount; + let mut executions = BoundedVec::< + PaymentExecution< + T::AssetKind, + AssetBalanceOf, + ::Id, + >, + ConstU32<32>, + >::default(); + + /* TODO: Check if retrying from a failed execution + if let PaymentState::Failed = &spend.status { + // A different logic might be needed + } + */ + + for asset_kind in assets { + if remaining_amount.is_zero() { + break; + } + + // Check available balance for this asset + let available = T::AssetCategories::available_balance(&asset_kind); + if available.is_zero() { + continue; + } + + // Determine how much we can pay with this asset + let pay_amount = if available >= remaining_amount { + remaining_amount + } else { + available + }; + + // Attempt payment execution + match T::Paymaster::pay(&spend.beneficiary, asset_kind.clone(), pay_amount) + { + Ok(payment_id) => { + if executions + .try_push(PaymentExecution { + asset: asset_kind.clone(), + amount: pay_amount, + payment_id, + }) + .is_err() + { + break; + } + + remaining_amount = remaining_amount.saturating_sub(pay_amount); + if remaining_amount.is_zero() { + break; + } + }, + Err(_) => { + continue; + }, + } + } - Self::deposit_event(Event::::Paid { index, payment_id: id }); + // Check if we made any payments + if executions.is_empty() { + return Err(Error::::PayoutError.into()); + } + // Update spend status with partial payments + spend.status = PaymentState::Attempted { executions, remaining_amount }; + }, + } + spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); + Spends::::insert(index, spend.clone()); + + // Emit events for each payment execution + if let PaymentState::Attempted { executions, .. } = &spend.status { + for execution in executions.iter() { + Self::deposit_event(Event::::Paid { + index, + execution: execution.clone(), + }); + } + } Ok(()) } @@ -837,25 +1007,52 @@ pub mod pallet { return Ok(Pays::No.into()) } - let payment_id = match spend.status { - State::Attempted { id } => id, - _ => return Err(Error::::NotAttempted.into()), - }; + match &spend.status { + State::Attempted { executions, remaining_amount } => { + // Check payment status + let results: Vec<_> = executions + .iter() + .map(|exec| (exec, T::Paymaster::check_payment(exec.payment_id.clone()))) + .collect(); + + // In-progress payments + if results.iter().any(|(_, status)| matches!(status, Status::InProgress)) { + return Err(Error::::Inconclusive.into()); + } - match T::Paymaster::check_payment(payment_id) { - Status::Failure => { - spend.status = PaymentState::Failed; - Spends::::insert(index, spend); - Self::deposit_event(Event::::PaymentFailed { index, payment_id }); - }, - Status::Success | Status::Unknown => { - Spends::::remove(index); - Self::deposit_event(Event::::SpendProcessed { index }); - return Ok(Pays::No.into()) + // Collect failed payments + let failed: Vec<_> = results + .iter() + .filter(|(_, status)| matches!(status, Status::Failure)) + .map(|(exec, _)| exec.payment_id.clone()) + .collect(); + + if !failed.is_empty() { + spend.status = PaymentState::Failed; + Spends::::insert(index, spend); + + for payment_id in failed { + Self::deposit_event(Event::::PaymentFailed { index, payment_id }); + } + return Ok(Pays::Yes.into()); + } + + // Check if all succeeded + let all_succeeded = results + .iter() + .all(|(_, status)| matches!(status, Status::Success | Status::Unknown)); + + if all_succeeded && remaining_amount.is_zero() { + Spends::::remove(index); + Self::deposit_event(Event::::SpendProcessed { index }); + return Ok(Pays::No.into()); + } + + // TODO: Partial success or unknown statuses + Ok(Pays::Yes.into()) }, - Status::InProgress => return Err(Error::::Inconclusive.into()), + _ => return Err(Error::::NotAttempted.into()), } - return Ok(Pays::Yes.into()) } /// Void previously approved spend. diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index f2abaa15be90..7c43cd634d74 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -77,11 +77,23 @@ thread_local! { pub static PAID: RefCell> = RefCell::new(BTreeMap::new()); pub static STATUS: RefCell> = RefCell::new(BTreeMap::new()); pub static LAST_ID: RefCell = RefCell::new(0u64); + pub static ASSET_BALANCES: RefCell> = RefCell::new(BTreeMap::new()); + pub static CATEGORY_ASSETS: RefCell, Vec>> = RefCell::new(BTreeMap::new()); #[cfg(feature = "runtime-benchmarks")] pub static TEST_SPEND_ORIGIN_TRY_SUCCESFUL_ORIGIN_ERR: RefCell = RefCell::new(false); } +/// Set asset balance for testing +fn set_asset_balance(asset_id: u32, balance: u64) { + ASSET_BALANCES.with(|b| b.borrow_mut().insert(asset_id, balance)); +} + +/// Set assets in a category +fn set_category_assets(category: &[u8], assets: Vec) { + CATEGORY_ASSETS.with(|c| c.borrow_mut().insert(category.to_vec(), assets)); +} + /// paid balance for a given account and asset ids fn paid(who: u128, asset_id: u32) -> u64 { PAID.with(|p| p.borrow().get(&(who, asset_id)).cloned().unwrap_or(0)) @@ -134,6 +146,19 @@ impl Pay for TestPay { } } +pub struct TestAssetCategoryManager; +impl AssetCategoryManager for TestAssetCategoryManager { + type AssetKind = u32; + type Balance = u64; + + fn assets_in_category(category: &[u8]) -> Vec { + CATEGORY_ASSETS.with(|c| c.borrow().get(category).cloned().unwrap_or_default()) + } + + fn available_balance(asset: &Self::AssetKind) -> Self::Balance { + ASSET_BALANCES.with(|b| b.borrow().get(asset).cloned().unwrap_or(0)) + } +} parameter_types! { pub const Burn: Permill = Permill::from_percent(50); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); @@ -196,6 +221,7 @@ impl Config for Test { type BalanceConverter = MulBy>; type PayoutPeriod = SpendPayoutPeriod; type BlockNumberProvider = System; + type AssetCategories = TestAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } @@ -207,6 +233,22 @@ impl Default for ExtBuilder { #[cfg(feature = "runtime-benchmarks")] TEST_SPEND_ORIGIN_TRY_SUCCESFUL_ORIGIN_ERR.with(|i| *i.borrow_mut() = false); + // Setup default categories for tests + CATEGORY_ASSETS.with(|c| { + let mut map = c.borrow_mut(); + map.insert(b"USD*".to_vec(), vec![1, 2, 3]); + map.insert(b"EUR*".to_vec(), vec![4, 5]); + }); + + ASSET_BALANCES.with(|b| { + let mut map = b.borrow_mut(); + map.insert(1, 100); + map.insert(2, 100); + map.insert(3, 100); + map.insert(4, 100); + map.insert(5, 100); + }); + Self {} } } @@ -218,6 +260,16 @@ impl ExtBuilder { self } + pub fn with_asset_balance(self, asset_id: u32, balance: u64) -> Self { + set_asset_balance(asset_id, balance); + self + } + + pub fn with_category_assets(self, category: &[u8], assets: Vec) -> Self { + set_category_assets(category, assets); + self + } + pub fn build(self) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); pallet_balances::GenesisConfig:: { @@ -228,6 +280,7 @@ impl ExtBuilder { .assimilate_storage(&mut t) .unwrap(); crate::GenesisConfig::::default().assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); ext @@ -237,11 +290,21 @@ impl ExtBuilder { fn get_payment_id(i: SpendIndex) -> Option { let spend = Spends::::get(i).expect("no spend"); match spend.status { - PaymentState::Attempted { id } => Some(id), + PaymentState::Attempted { executions, .. } => + executions.first().map(|exec| exec.payment_id), _ => None, } } +fn get_all_payment_ids(i: SpendIndex) -> Vec { + let spend = Spends::::get(i).expect("no spend"); + match &spend.status { + PaymentState::Attempted { executions, .. } => + executions.iter().map(|exec| exec.payment_id).collect(), + _ => vec![], + } +} + #[test] fn genesis_config_works() { ExtBuilder::default().build().execute_with(|| { @@ -534,13 +597,13 @@ fn spending_in_batch_respects_max_total() { assert_ok!(RuntimeCall::from(UtilityCall::batch_all { calls: vec![ RuntimeCall::from(TreasuryCall::spend { - asset_kind: Box::new(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 1, beneficiary: Box::new(100), valid_from: None, }), RuntimeCall::from(TreasuryCall::spend { - asset_kind: Box::new(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 1, beneficiary: Box::new(101), valid_from: None, @@ -553,13 +616,13 @@ fn spending_in_batch_respects_max_total() { RuntimeCall::from(UtilityCall::batch_all { calls: vec![ RuntimeCall::from(TreasuryCall::spend { - asset_kind: Box::new(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 2, beneficiary: Box::new(100), valid_from: None, }), RuntimeCall::from(TreasuryCall::spend { - asset_kind: Box::new(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 2, beneficiary: Box::new(101), valid_from: None, @@ -575,20 +638,62 @@ fn spending_in_batch_respects_max_total() { #[test] fn spend_origin_works() { ExtBuilder::default().build().execute_with(|| { - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None)); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 1, + Box::new(6), + None + )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); assert_noop!( - Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 3, Box::new(6), None), + Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 3, + Box::new(6), + None + ), Error::::InsufficientPermission ); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(11), Box::new(1), 5, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(11), + Box::new(SpendAsset::Specific(1)), + 5, + Box::new(6), + None + )); assert_noop!( - Treasury::spend(RuntimeOrigin::signed(11), Box::new(1), 6, Box::new(6), None), + Treasury::spend( + RuntimeOrigin::signed(11), + Box::new(SpendAsset::Specific(1)), + 6, + Box::new(6), + None + ), Error::::InsufficientPermission ); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(12), Box::new(1), 10, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(12), + Box::new(SpendAsset::Specific(1)), + 10, + Box::new(6), + None + )); assert_noop!( - Treasury::spend(RuntimeOrigin::signed(12), Box::new(1), 11, Box::new(6), None), + Treasury::spend( + RuntimeOrigin::signed(12), + Box::new(SpendAsset::Specific(1)), + 11, + Box::new(6), + None + ), Error::::InsufficientPermission ); @@ -601,13 +706,19 @@ fn spend_origin_works() { fn spend_works() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); assert_eq!(SpendCount::::get(), 1); assert_eq!( Spends::::get(0).unwrap(), SpendStatus { - asset_kind: 1, + asset: SpendAsset::Specific(1), amount: 2, beneficiary: 6, valid_from: 1, @@ -618,7 +729,7 @@ fn spend_works() { System::assert_last_event( Event::::AssetSpendApproved { index: 0, - asset_kind: 1, + asset: SpendAsset::Specific(1), amount: 2, beneficiary: 6, valid_from: 1, @@ -636,13 +747,25 @@ fn spend_expires() { // spend `0` expires in 5 blocks after the creating. System::set_block_number(1); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); System::set_block_number(6); assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::SpendExpired); // spend cannot be approved since its already expired. assert_noop!( - Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), Some(0)), + Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + Some(0) + ), Error::::SpendExpired ); }); @@ -654,14 +777,26 @@ fn spend_payout_works() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); // approve a `2` coins spend of asset `1` to beneficiary `6`, the spend valid from now. - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); // payout the spend. assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); // beneficiary received `2` coins of asset `1`. assert_eq!(paid(6, 1), 2); assert_eq!(SpendCount::::get(), 1); let payment_id = get_payment_id(0).expect("no payment attempt"); - System::assert_last_event(Event::::Paid { index: 0, payment_id }.into()); + System::assert_last_event( + Event::::Paid { + index: 0, + execution: PaymentExecution { asset: 1, amount: 2, payment_id }, + } + .into(), + ); set_status(payment_id, PaymentStatus::Success); // the payment succeed. assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); @@ -677,7 +812,13 @@ fn payout_extends_expiry() { assert_eq!(::PayoutPeriod::get(), 5); System::set_block_number(1); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); // Fail a payout at block 4 System::set_block_number(4); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); @@ -704,7 +845,13 @@ fn payout_extends_expiry() { fn payout_retry_works() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); assert_eq!(paid(6, 1), 2); let payment_id = get_payment_id(0).expect("no payment attempt"); @@ -734,7 +881,7 @@ fn spend_valid_from_works() { // spend valid from block `2`. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(2) @@ -747,7 +894,7 @@ fn spend_valid_from_works() { // spend approved even if `valid_from` in the past since the payout period has not passed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(4) @@ -764,7 +911,7 @@ fn void_spend_works() { // spend cannot be voided if already attempted. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(1) @@ -778,7 +925,7 @@ fn void_spend_works() { // void spend. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(10) @@ -795,14 +942,26 @@ fn check_status_works() { System::set_block_number(1); // spend `0` expired and can be removed. - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); System::set_block_number(7); let info = Treasury::check_status(RuntimeOrigin::signed(1), 0).unwrap(); assert_eq!(info.pays_fee, Pays::No); System::assert_last_event(Event::::SpendProcessed { index: 0 }.into()); // spend `1` payment failed and expired hence can be removed. - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); assert_noop!( Treasury::check_status(RuntimeOrigin::signed(1), 1), Error::::NotAttempted @@ -820,7 +979,13 @@ fn check_status_works() { System::assert_last_event(Event::::SpendProcessed { index: 1 }.into()); // spend `2` payment succeed. - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 2)); let payment_id = get_payment_id(2).expect("no payment attempt"); set_status(payment_id, PaymentStatus::Success); @@ -829,7 +994,13 @@ fn check_status_works() { System::assert_last_event(Event::::SpendProcessed { index: 2 }.into()); // spend `3` payment in process. - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 3)); let payment_id = get_payment_id(3).expect("no payment attempt"); set_status(payment_id, PaymentStatus::InProgress); @@ -839,7 +1010,13 @@ fn check_status_works() { ); // spend `4` removed since the payment status is unknown. - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 2, + Box::new(6), + None + )); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 4)); let payment_id = get_payment_id(4).expect("no payment attempt"); set_status(payment_id, PaymentStatus::Unknown); @@ -939,7 +1116,13 @@ fn try_state_spends_invariant_1_works() { use frame_support::pallet_prelude::DispatchError::Other; // Propose and approve a spend assert_ok!({ - Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 1, + Box::new(6), + None, + ) }); assert_eq!(Spends::::iter().count(), 1); assert_eq!(SpendCount::::get(), 1); @@ -961,7 +1144,7 @@ fn try_state_spends_invariant_2_works() { use frame_support::pallet_prelude::DispatchError::Other; // Propose and approve a spend assert_ok!({ - Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + Treasury::spend(RuntimeOrigin::signed(10), Box::new(SpendAsset::Specific(1)), 1, Box::new(6), None) }); assert_eq!(Spends::::iter().count(), 1); let current_spend_count = SpendCount::::get(); @@ -990,7 +1173,13 @@ fn try_state_spends_invariant_3_works() { use frame_support::pallet_prelude::DispatchError::Other; // Propose and approve a spend assert_ok!({ - Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Specific(1)), + 1, + Box::new(6), + None, + ) }); assert_eq!(Spends::::iter().count(), 1); let current_spend_count = SpendCount::::get(); @@ -1046,3 +1235,520 @@ fn multiple_spend_periods_work() { assert_eq!(LastSpendPeriod::::get(), Some(8)); }); } + +// New tests for category spends +#[test] +fn category_spend_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Create a category spend for USD* category + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Category(bounded_category.clone())), + 2, // Request 5 USD* + Box::new(6), + None + )); + + assert_eq!(SpendCount::::get(), 1); + let spend = Spends::::get(0).unwrap(); + + match spend.asset { + SpendAsset::Category(cat) => assert_eq!(cat, bounded_category), + _ => panic!("Expected Category asset"), + } + + assert_eq!(spend.amount, 2); + assert_eq!(spend.beneficiary, 6); + assert_eq!(spend.status, PaymentState::Pending); + + System::assert_last_event( + Event::::AssetSpendApproved { + index: 0, + asset: SpendAsset::Category(bounded_category), + amount: 2, + beneficiary: 6, + valid_from: 1, + expire_at: 6, + } + .into(), + ); + }); +} + +#[test] +fn category_payout_distributes_across_assets() { + ExtBuilder::default() + .with_asset_balance(1, 20) + .with_asset_balance(3, 30) + .with_asset_balance(2, 10) + .build() + .execute_with(|| { + System::set_block_number(1); + + // Create a category spend for USD* category + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + // Request 50 USD* - should use all available balances + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + None + )); + + // Payout the spend + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + assert_eq!(paid(6, 1), 20); + assert_eq!(paid(6, 2), 10); + assert_eq!(paid(6, 3), 20); + + // Check spend status + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 3); + assert_eq!(*remaining_amount, 0); + + // Check execution details + let mut asset_payments = std::collections::BTreeMap::new(); + for exec in executions.iter() { + asset_payments.insert(exec.asset, exec.amount); + } + + assert_eq!(asset_payments.get(&1), Some(&20)); + assert_eq!(asset_payments.get(&2), Some(&10)); + assert_eq!(asset_payments.get(&3), Some(&20)); + }, + _ => panic!("Expected Attempted status"), + } + }); +} + +#[test] +fn category_payout_partial_fulfillment() { + ExtBuilder::default() + .with_asset_balance(1, 10) + .with_asset_balance(2, 15) + .with_asset_balance(3, 5) + .build() + .execute_with(|| { + System::set_block_number(1); + + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + // Request 50 USD* spend to a 30 USD* pool + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + None + )); + + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + // Check partial payment + assert_eq!(paid(6, 1), 10); + assert_eq!(paid(6, 2), 15); + assert_eq!(paid(6, 3), 5); + + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 3); + assert_eq!(*remaining_amount, 20); + + let mut asset_payments = std::collections::BTreeMap::new(); + for exec in executions.iter() { + asset_payments.insert(exec.asset, exec.amount); + } + + assert_eq!(asset_payments.get(&1), Some(&10)); + assert_eq!(asset_payments.get(&2), Some(&15)); + assert_eq!(asset_payments.get(&3), Some(&5)); + }, + _ => panic!("Expected Attempted status"), + } + }); +} + +#[test] +fn category_payout_skips_assets_with_no_balance() { + ExtBuilder::default() + .with_asset_balance(1, 0) + .with_asset_balance(2, 40) + .with_asset_balance(3, 0) + .build() + .execute_with(|| { + System::set_block_number(1); + + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 30, + Box::new(6), + None + )); + + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + // Only AssetId = 2 should be used + assert_eq!(paid(6, 1), 0); + assert_eq!(paid(6, 2), 30); + assert_eq!(paid(6, 3), 0); + + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 1); // Only USDC used + assert_eq!(*remaining_amount, 0); // Full amount paid + + assert_eq!(executions[0].asset, 2); + assert_eq!(executions[0].amount, 30); + }, + _ => panic!("Expected Attempted status"), + } + }); +} + +#[test] +fn category_payout_fails_when_no_assets_available() { + ExtBuilder::default() + .with_asset_balance(1, 0) + .with_asset_balance(2, 0) + .with_asset_balance(3, 0) + .build() + .execute_with(|| { + System::set_block_number(1); + + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(13), + Box::new(SpendAsset::Category(bounded_category)), + 10, + Box::new(6), + None + )); + + // Payout should fail - no assets have balance + assert_noop!( + Treasury::payout(RuntimeOrigin::signed(1), 0), + Error::::PayoutError + ); + }); +} + +#[test] +fn category_spend_with_unknown_category() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Try to spend from a non-existent category + let category = b"GBP*".to_vec(); // No GBP assets defined + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + // Should fail during conversion because category has no assets + assert_noop!( + Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Category(bounded_category)), + 10, + Box::new(6), + None + ), + Error::::EmptyAssetCategory + ); + }); +} + +#[test] +fn mixed_specific_and_category_spends() { + ExtBuilder::default() + .with_asset_balance(1, 100) + .with_asset_balance(2, 100) + .with_asset_balance(4, 100) + .build() + .execute_with(|| { + System::set_block_number(1); + + // Specific asset spend + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Specific(4)), + 25, + Box::new(7), + None + )); + + // Category spend + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 30, + Box::new(8), + None + )); + + // Payout specific spend + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_eq!(paid(7, 4), 25); + + // Payout category spend + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + + // Category spend should use USD assets + let paid_usd = paid(8, 1) + paid(8, 2); + assert_eq!(paid_usd, 30); + + // Check counts + assert_eq!(SpendCount::::get(), 2); + assert_eq!(Spends::::iter().count(), 2); + }); +} + +#[test] +fn category_check_status_with_multiple_executions() { + ExtBuilder::default() + .with_asset_balance(1, 20) + .with_asset_balance(2, 20) + .build() + .execute_with(|| { + System::set_block_number(1); + + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + // Request 40 USD* from a 40 USD* pool + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 40, + Box::new(6), + None + )); + + // Payout the spend + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + // Get payment IDs + let payment_ids = get_all_payment_ids(0); + assert_eq!(payment_ids.len(), 2); + + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + }); +} + +#[test] +fn category_spend_with_custom_category() { + ExtBuilder::default() + .with_category_assets(b"STABLE*", vec![10, 11, 12]) + .with_asset_balance(10, 50) + .with_asset_balance(11, 30) + .with_asset_balance(12, 20) + .build() + .execute_with(|| { + System::set_block_number(1); + + let category = b"STABLE*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + // Request 80 USD* from a 100 USD* pool + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 80, + Box::new(6), + None + )); + + // Payout - uses 2 out of 3 assets + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + assert_eq!(paid(6, 10), 50); + assert_eq!(paid(6, 11), 30); + assert_eq!(paid(6, 12), 0); + + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 2); + assert_eq!(*remaining_amount, 0); + }, + _ => panic!("Expected Attempted status"), + } + }); +} + +#[test] +fn category_spend_respects_spend_origin_limit() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Category(bounded_category.clone())), + 2, + Box::new(6), + None + )); + + assert_noop!( + Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(SpendAsset::Category(bounded_category)), + 3, + Box::new(6), + None + ), + Error::::InsufficientPermission + ); + }); +} + +#[test] +fn category_spend_with_empty_category_assets() { + ExtBuilder::default() + .with_category_assets(b"EMPTY*", vec![]) // Empty category + .build() + .execute_with(|| { + System::set_block_number(1); + + let category = b"EMPTY*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + assert_noop!( + Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 10, + Box::new(6), + None + ), + Error::::EmptyAssetCategory + ); + }); +} + +#[test] +fn category_spend_cannot_void_after_payout() { + ExtBuilder::default().with_asset_balance(1, 50).build().execute_with(|| { + System::set_block_number(1); + + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + None + )); + + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + // Cannot void after payout + assert_noop!( + Treasury::void_spend(RuntimeOrigin::root(), 0), + Error::::AlreadyAttempted + ); + }); +} + +#[test] +fn category_spend_expiry_works() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(::PayoutPeriod::get(), 5); + + System::set_block_number(1); + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + // Create category spend + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + None + )); + + // Should expire after 5 blocks + System::set_block_number(6); + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::SpendExpired); + }); +} + +#[test] +fn category_spend_valid_from_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let category = b"USD*".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); + + // Create category spend valid from block 3 + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + Some(3) + )); + + // Cannot payout before valid_from + System::set_block_number(2); + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::EarlyPayout); + + // Can payout at valid_from + System::set_block_number(3); + + set_asset_balance(1, 50); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + assert_eq!(paid(6, 1), 50); + + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 1); + assert_eq!(*remaining_amount, 0); + }, + _ => panic!("Expected Attempted status"), + } + }); +} From b7a2545d4160608e4969df713b5292cc41803b97 Mon Sep 17 00:00:00 2001 From: doordashcon Date: Wed, 24 Dec 2025 10:05:02 +0100 Subject: [PATCH 03/21] configure payout with partial execution --- substrate/frame/treasury/src/lib.rs | 296 +++++++++++++++++++------- substrate/frame/treasury/src/tests.rs | 4 +- 2 files changed, 223 insertions(+), 77 deletions(-) diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 00a2c5fe362f..29f3e549b74a 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -193,14 +193,15 @@ pub enum PaymentState { Pending, // Payment attempted with a payment identifier. //Attempted { id: Id }, - /// Payment attempted with payment identifiers for each partial payment. - /// For categories: contains partial payments across multiple assets + /// Payment attempted with payment identifiers for each payment execution Attempted { executions: BoundedVec, ConstU32<32>>, remaining_amount: Balance, }, - /// Payment failed. - Failed, + /// Record of all failed payments to retry + Failed(BoundedVec, ConstU32<32>>), + /// Attempt incomplete payment balance from spend + Partial(Balance), } #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] @@ -547,6 +548,12 @@ pub mod pallet { Inconclusive, EmptyAssetCategory, + + BalanceToLowp, + + InvalidPaymentState, + + ExecutionRateLimit, } #[pallet::hooks] @@ -860,114 +867,253 @@ pub mod pallet { let now = T::BlockNumberProvider::current_block_number(); ensure!(now >= spend.valid_from, Error::::EarlyPayout); ensure!(spend.expire_at > now, Error::::SpendExpired); - ensure!( - matches!(spend.status, PaymentState::Pending | PaymentState::Failed), - Error::::AlreadyAttempted - ); match spend.asset { SpendAsset::Specific(ref asset_kind) => { - let id = - T::Paymaster::pay(&spend.beneficiary, asset_kind.clone(), spend.amount) + // TODO: Compare asset balance here + match spend.status { + PaymentState::Pending | PaymentState::Failed(_) => { + let id = T::Paymaster::pay( + &spend.beneficiary, + asset_kind.clone(), + spend.amount, + ) .map_err(|_| Error::::PayoutError)?; - spend.status = PaymentState::Attempted { - executions: BoundedVec::try_from(vec![PaymentExecution { - asset: asset_kind.clone(), - amount: spend.amount, - payment_id: id, - }]) - .map_err(|_| Error::::PayoutError)?, - remaining_amount: Zero::zero(), - }; + spend.status = PaymentState::Attempted { + executions: BoundedVec::try_from(vec![PaymentExecution { + asset: asset_kind.clone(), + amount: spend.amount, + payment_id: id, + }]) + .map_err(|_| Error::::ExecutionRateLimit)?, + + remaining_amount: Zero::zero(), + }; + }, + _ => return Err(Error::::AlreadyAttempted.into()), + } }, SpendAsset::Category(ref category) => { let assets = T::AssetCategories::assets_in_category(&category); + if assets.is_empty() { return Err(Error::::EmptyAssetCategory.into()); } - let mut remaining_amount = spend.amount; - let mut executions = BoundedVec::< - PaymentExecution< - T::AssetKind, - AssetBalanceOf, - ::Id, - >, - ConstU32<32>, - >::default(); - - /* TODO: Check if retrying from a failed execution - if let PaymentState::Failed = &spend.status { - // A different logic might be needed - } - */ + match spend.status { + PaymentState::Pending => { + let mut executions = BoundedVec::< + PaymentExecution< + T::AssetKind, + AssetBalanceOf, + ::Id, + >, + ConstU32<32>, + >::default(); + let mut remaining_amount = spend.amount; + + for asset_kind in assets { + if remaining_amount.is_zero() { + break; + } - for asset_kind in assets { - if remaining_amount.is_zero() { - break; - } + let available = T::AssetCategories::available_balance(&asset_kind); - // Check available balance for this asset - let available = T::AssetCategories::available_balance(&asset_kind); - if available.is_zero() { - continue; - } + if available.is_zero() { + continue; + } - // Determine how much we can pay with this asset - let pay_amount = if available >= remaining_amount { - remaining_amount - } else { - available - }; - - // Attempt payment execution - match T::Paymaster::pay(&spend.beneficiary, asset_kind.clone(), pay_amount) - { - Ok(payment_id) => { - if executions - .try_push(PaymentExecution { - asset: asset_kind.clone(), - amount: pay_amount, - payment_id, - }) - .is_err() - { + let pay_amount = if available >= remaining_amount { + remaining_amount + } else { + available + }; + + match T::Paymaster::pay( + &spend.beneficiary, + asset_kind.clone(), + pay_amount, + ) { + Ok(payment_id) => { + // TODO: Test rate limit reached + executions + .try_push(PaymentExecution { + asset: asset_kind.clone(), + amount: pay_amount, + payment_id, + }) + .map_err(|_| Error::::ExecutionRateLimit)?; + + remaining_amount = + remaining_amount.saturating_sub(pay_amount); + }, + Err(_) => continue, + } + } + + if remaining_amount.is_zero() { + spend.status = PaymentState::Attempted { + executions, + remaining_amount: Zero::zero(), + }; + } else { + spend.status = PaymentState::Partial(remaining_amount); + } + }, + PaymentState::Failed(prev) => { + let mut executions = BoundedVec::< + PaymentExecution< + T::AssetKind, + AssetBalanceOf, + ::Id, + >, + ConstU32<32>, + >::default(); + //let mut retry: AssetBalanceOf = + //executions.iter().map(|exec| exec.amount).sum(); + + let mut retry: AssetBalanceOf = prev.iter().fold(Zero::zero(), |acc, exec| acc.saturating_add(exec.amount)); + let used_assets: Vec<_> = + prev.iter().map(|exec| exec.asset.clone()).collect(); + + for exec in prev.iter() { + if retry.is_zero() { break; } - remaining_amount = remaining_amount.saturating_sub(pay_amount); + let mut available = + T::AssetCategories::available_balance(&exec.asset); + + let asset_kind = if available.is_zero() { + match assets.iter().find(|&ak| { + !used_assets.contains(ak) && + !T::AssetCategories::available_balance(ak).is_zero() + }) { + Some(ak) => { + available = T::AssetCategories::available_balance(ak); + ak.clone() + }, + None => continue, + } + } else { + exec.asset.clone() + }; + + let pay_amount = if available >= retry { retry } else { available }; + + match T::Paymaster::pay( + &spend.beneficiary, + asset_kind.clone(), + pay_amount, + ) { + Ok(payment_id) => { + executions + .try_push(PaymentExecution { + asset: asset_kind.clone(), + + amount: pay_amount, + + payment_id, + }) + .map_err(|_| Error::::ExecutionRateLimit)?; + + retry = retry.saturating_sub(pay_amount); + }, + Err(_) => continue, + } + } + + if retry.is_zero() { + spend.status = PaymentState::Attempted { + executions, + remaining_amount: Zero::zero(), + }; + } else { + spend.status = PaymentState::Partial(retry); + } + }, + PaymentState::Partial(unpaid) => { + let mut executions = BoundedVec::< + PaymentExecution< + T::AssetKind, + AssetBalanceOf, + ::Id, + >, + ConstU32<32>, + >::default(); + + let mut remaining_amount = unpaid; + + for asset_kind in assets { if remaining_amount.is_zero() { break; } - }, - Err(_) => { - continue; - }, - } - } - // Check if we made any payments - if executions.is_empty() { - return Err(Error::::PayoutError.into()); + let available = T::AssetCategories::available_balance(&asset_kind); + + let pay_amount = if available >= remaining_amount { + remaining_amount + } else { + available + }; + + match T::Paymaster::pay( + &spend.beneficiary, + asset_kind.clone(), + pay_amount, + ) { + Ok(payment_id) => { + executions + .try_push(PaymentExecution { + asset: asset_kind.clone(), + amount: pay_amount, + payment_id, + }) + .map_err(|_| Error::::ExecutionRateLimit)?; + + remaining_amount = + remaining_amount.saturating_sub(pay_amount); + }, + Err(_) => { + continue; + }, + } + } + + if remaining_amount.is_zero() { + spend.status = PaymentState::Attempted { + executions, + remaining_amount: Zero::zero(), + }; + } else { + spend.status = PaymentState::Partial(remaining_amount); + } + }, + + PaymentState::Attempted { .. } => { + return Err(Error::::AlreadyAttempted.into()); + }, } - - // Update spend status with partial payments - spend.status = PaymentState::Attempted { executions, remaining_amount }; }, } + spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); + Spends::::insert(index, spend.clone()); // Emit events for each payment execution + if let PaymentState::Attempted { executions, .. } = &spend.status { for execution in executions.iter() { Self::deposit_event(Event::::Paid { index, + execution: execution.clone(), }); } } + Ok(()) } diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 7c43cd634d74..f9fb487f8228 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -1250,7 +1250,7 @@ fn category_spend_works() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), Box::new(SpendAsset::Category(bounded_category.clone())), - 2, // Request 5 USD* + 2, Box::new(6), None )); @@ -1285,8 +1285,8 @@ fn category_spend_works() { fn category_payout_distributes_across_assets() { ExtBuilder::default() .with_asset_balance(1, 20) - .with_asset_balance(3, 30) .with_asset_balance(2, 10) + .with_asset_balance(3, 30) .build() .execute_with(|| { System::set_block_number(1); From 227035e279fe29e80bbf107c5e9d00cb3cb66b75 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Wed, 21 Jan 2026 12:53:55 +0100 Subject: [PATCH 04/21] nit --- substrate/frame/treasury/src/lib.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 29f3e549b74a..07a9ca31670d 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -191,8 +191,6 @@ pub struct Proposal { pub enum PaymentState { /// Pending claim. Pending, - // Payment attempted with a payment identifier. - //Attempted { id: Id }, /// Payment attempted with payment identifiers for each payment execution Attempted { executions: BoundedVec, ConstU32<32>>, @@ -260,8 +258,6 @@ pub struct PaymentExecution { )] pub struct SpendStatus { pub asset: SpendAsset, - // The kind of asset to be spent. - //pub asset_kind: AssetKind, /// The asset amount of the spend. pub amount: AssetBalance, /// The beneficiary of the spend. @@ -357,7 +353,7 @@ pub mod pallet { /// Type parameter used to identify the beneficiaries eligible to receive treasury spends. type Beneficiary: Parameter + MaxEncodedLen; - /// Converting trainnt to take a source type and convert to [`Self::Beneficiary`]. + /// Converting trait to take a source type and convert to [`Self::Beneficiary`]. type BeneficiaryLookup: StaticLookup; /// Type for processing spends of [Self::AssetKind] in favor of [`Self::Beneficiary`]. @@ -507,7 +503,6 @@ pub mod pallet { /// An approved spend was voided. AssetSpendVoided { index: SpendIndex }, /// A payment happened. - // Paid { index: SpendIndex, payment_id: ::Id }, Paid { index: SpendIndex, execution: @@ -547,9 +542,9 @@ pub mod pallet { /// The payment has neither failed nor succeeded yet. Inconclusive, - EmptyAssetCategory, + LowBalance, - BalanceToLowp, + EmptyAssetCategory, InvalidPaymentState, @@ -758,7 +753,6 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::spend())] pub fn spend( origin: OriginFor, - // asset_kind: Box, asset: Box>, #[pallet::compact] amount: AssetBalanceOf, beneficiary: Box>, @@ -818,7 +812,6 @@ pub mod pallet { index, SpendStatus { asset: *asset.clone(), - //asset_kind: *asset_kind.clone(), amount, beneficiary: beneficiary.clone(), valid_from, @@ -831,7 +824,6 @@ pub mod pallet { Self::deposit_event(Event::AssetSpendApproved { index, asset: *asset, - // asset_kind: *asset_kind, amount, beneficiary, valid_from, @@ -870,7 +862,11 @@ pub mod pallet { match spend.asset { SpendAsset::Specific(ref asset_kind) => { - // TODO: Compare asset balance here + // Validate asset has sufficient balance before attempting payment + let available = T::AssetCategories::available_balance(asset_kind); + if available < spend.amount { + return Err(Error::::LowBalance.into()); + } match spend.status { PaymentState::Pending | PaymentState::Failed(_) => { let id = T::Paymaster::pay( @@ -1174,7 +1170,7 @@ pub mod pallet { .collect(); if !failed.is_empty() { - spend.status = PaymentState::Failed; + spend.status = State::Failed; Spends::::insert(index, spend); for payment_id in failed { From 2fc50812ebd3d26fe7afc806d1e7df1cf5ec27f6 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Mon, 26 Jan 2026 05:12:56 +0100 Subject: [PATCH 05/21] inconsistent test cases --- Cargo.lock | 1 + substrate/frame/assets/src/lib.rs | 57 +++ substrate/frame/treasury/Cargo.toml | 4 + substrate/frame/treasury/src/lib.rs | 562 +++++++++++++++----------- substrate/frame/treasury/src/tests.rs | 239 +++++------ 5 files changed, 513 insertions(+), 350 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d42e29dd885d..4d048e2be56e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14196,6 +14196,7 @@ dependencies = [ "frame-system", "impl-trait-for-tuples", "log", + "pallet-assets", "pallet-balances", "pallet-utility", "parity-scale-codec", diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index 69bc380a9e45..346b19551b17 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -215,6 +215,18 @@ pub trait AssetsCallback { } } +/// Trait for managing asset categories +pub trait AssetCategoryManager { + type AssetKind; + type Balance: Zero + PartialOrd + Copy + Saturating + sp_runtime::traits::AtLeast32BitUnsigned; + + /// Get all assets in a category + fn assets_in_category(category: &[u8]) -> Vec; + + /// Get available balance of a specific asset in treasury + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option; +} + #[impl_trait_for_tuples::impl_for_tuples(10)] impl AssetsCallback for Tuple { fn created(id: &AssetId, owner: &AccountId) -> Result<(), ()> { @@ -487,6 +499,26 @@ pub mod pallet { ValueQuery, >; + #[pallet::storage] + pub type AssetCategories, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + BoundedVec>, // Category name + BoundedVec>, // Assets in this category + ValueQuery, + >; + + /* + #[pallet::storage] + pub type AssetCategories, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + BoundedVec>, // Category + BoundedVec>, // Assets in this category + ValueQuery, + >; + */ + /// The asset ID enforced for the next asset creation, if any present. Otherwise, this storage /// item has no effect. /// @@ -1957,6 +1989,31 @@ pub mod pallet { Reserves::::get(id).into_inner() } } + + impl, I: 'static> AssetCategoryManager for Pallet { + type AssetKind = T::AssetId; + type Balance = T::Balance; + + /// Get all assets in a category + // TODO: Refine or investigate + fn assets_in_category(category: &[u8]) -> Vec { + let bounded_category: BoundedVec> = category + .to_vec() + .try_into() + .unwrap_or_default(); + + AssetCategories::::get(&bounded_category) + .into_inner() + .into_iter() + .collect() + } + + + /// Get available balance of a specific asset in treasury + fn available_balance(asset: Self::AssetKind, owner: T::AccountId) -> Option { + Self::balance_of(owner, asset) + } + } } sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); diff --git a/substrate/frame/treasury/Cargo.toml b/substrate/frame/treasury/Cargo.toml index c52a98b26dcf..ba886f4a18d8 100644 --- a/substrate/frame/treasury/Cargo.toml +++ b/substrate/frame/treasury/Cargo.toml @@ -24,6 +24,7 @@ frame-system = { workspace = true } impl-trait-for-tuples = { workspace = true } log = { workspace = true } pallet-balances = { workspace = true } +pallet-assets = { workspace = true } scale-info = { features = ["derive"], workspace = true } serde = { features = ["derive"], optional = true, workspace = true, default-features = true } sp-core = { optional = true, workspace = true } @@ -41,6 +42,7 @@ std = [ "frame-support/std", "frame-system/std", "log/std", + "pallet-assets/std", "pallet-balances/std", "pallet-utility/std", "scale-info/std", @@ -54,6 +56,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-utility/runtime-benchmarks", "sp-runtime/runtime-benchmarks", @@ -61,6 +64,7 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-assets/try-runtime", "pallet-balances/try-runtime", "pallet-utility/try-runtime", "sp-runtime/try-runtime", diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 07a9ca31670d..47e945962c83 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -98,9 +98,12 @@ use sp_runtime::{ use sp_runtime::traits::ConstU32; +use alloc::vec::Vec; use frame_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo}, - ensure, print, + ensure, + pallet_prelude::DispatchError, + print, traits::{ tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, ReservableCurrency, WithdrawReasons, @@ -109,6 +112,7 @@ use frame_support::{ BoundedVec, PalletId, }; use frame_system::pallet_prelude::BlockNumberFor as SystemBlockNumberFor; +use pallet_assets::AssetCategoryManager; pub use pallet::*; pub use weights::WeightInfo; @@ -270,25 +274,13 @@ pub struct SpendStatus, } -// TODO: Move to primitivei -/// Trait for managing asset categories -pub trait AssetCategoryManager { - type AssetKind; - type Balance: Zero + PartialOrd + Copy + Saturating + sp_runtime::traits::AtLeast32BitUnsigned; - - /// Get all assets in a category - fn assets_in_category(category: &[u8]) -> Vec; - - /// Get available balance of a specific asset in treasury - fn available_balance(asset: &Self::AssetKind) -> Self::Balance; -} - /// Index of an approved treasury spend. pub type SpendIndex = u32; #[frame_support::pallet] pub mod pallet { use super::*; + use alloc::vec; use frame_support::{ dispatch_context::with_context, pallet_prelude::*, @@ -380,6 +372,7 @@ pub mod pallet { type BlockNumberProvider: BlockNumberProvider; type AssetCategories: AssetCategoryManager< + Self::AccountId, AssetKind = Self::AssetKind, Balance = AssetBalanceOf, >; @@ -542,13 +535,17 @@ pub mod pallet { /// The payment has neither failed nor succeeded yet. Inconclusive, - LowBalance, + LowBalance, EmptyAssetCategory, InvalidPaymentState, - ExecutionRateLimit, + ExecutionRateLimit, + + TooManyFailedPayments, + + AssetNotFound, } #[pallet::hooks] @@ -766,12 +763,68 @@ pub mod pallet { let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); ensure!(expire_at > now, Error::::SpendExpired); + match *asset { + SpendAsset::Category(ref category) => { + let assets = Self::validate_category_spend(category, amount)?; + + // This helps prevent issues where assets in the same category + // might have different conversion rates to native currency + if let Some(first_asset) = assets.first() { + let first_native = + T::BalanceConverter::from_asset_balance(amount, first_asset.clone()) + .ok(); + + // Store first_native for comparison with others + if let Some(first_native) = first_native { + for asset_kind in assets.iter().skip(1) { + let current_native = T::BalanceConverter::from_asset_balance( + amount, + asset_kind.clone(), + ) + .ok(); // Convert Result to Option + + if let Some(current) = current_native { + // Convert to u128 for calculation + let first_u128: u128 = first_native.unique_saturated_into(); + let current_u128: u128 = current.unique_saturated_into(); + + let max_diff = first_u128.max(current_u128); + let min_diff = first_u128.min(current_u128); + + // Calculate percentage difference safely + if max_diff > 0 { + let diff = max_diff - min_diff; + // Multiply by 100 first to maintain precision + let diff_percent = (diff * 100) / max_diff; + + if diff_percent > 5 { + log::warn!( + "Asset {:?} in category {:?} has significantly different conversion rate ({}% diff)", + asset_kind, + category, + diff_percent + ); + + // TODO: return an error here, for now just log a + // warning return Err(Error::::ConversionRateMismatch.into()); + } + } + } + } + } + } + }, + SpendAsset::Specific(ref asset_kind) => { + let _ = + Self::ensure_sufficient_balance(asset_kind, &Self::account_id(), amount)?; + }, + } + let native_amount = match *asset { SpendAsset::Specific(ref asset_kind) => T::BalanceConverter::from_asset_balance(amount, asset_kind.clone()) .map_err(|_| Error::::BalanceConversionFailed)?, - // TODO: Add in runtime, assumes that assets in a category are of - // the same denomination(i.e. USD* to USDT, USDC, BUSD e.t.c.) SpendAsset::Category(ref category) => { let assets = T::AssetCategories::assets_in_category(&category); @@ -862,11 +915,12 @@ pub mod pallet { match spend.asset { SpendAsset::Specific(ref asset_kind) => { - // Validate asset has sufficient balance before attempting payment - let available = T::AssetCategories::available_balance(asset_kind); - if available < spend.amount { - return Err(Error::::LowBalance.into()); - } + // Validate asset has sufficient balance before attempting payment + let _ = Self::ensure_sufficient_balance( + asset_kind, + &Self::account_id(), + spend.amount, + )?; match spend.status { PaymentState::Pending | PaymentState::Failed(_) => { let id = T::Paymaster::pay( @@ -883,233 +937,72 @@ pub mod pallet { payment_id: id, }]) .map_err(|_| Error::::ExecutionRateLimit)?, - - remaining_amount: Zero::zero(), + + remaining_amount: Zero::zero(), }; }, _ => return Err(Error::::AlreadyAttempted.into()), } }, - SpendAsset::Category(ref category) => { - let assets = T::AssetCategories::assets_in_category(&category); - - if assets.is_empty() { - return Err(Error::::EmptyAssetCategory.into()); - } - - match spend.status { - PaymentState::Pending => { - let mut executions = BoundedVec::< - PaymentExecution< - T::AssetKind, - AssetBalanceOf, - ::Id, - >, - ConstU32<32>, - >::default(); - let mut remaining_amount = spend.amount; - - for asset_kind in assets { - if remaining_amount.is_zero() { - break; - } - - let available = T::AssetCategories::available_balance(&asset_kind); - - if available.is_zero() { - continue; - } - - let pay_amount = if available >= remaining_amount { - remaining_amount - } else { - available - }; - - match T::Paymaster::pay( - &spend.beneficiary, - asset_kind.clone(), - pay_amount, - ) { - Ok(payment_id) => { - // TODO: Test rate limit reached - executions - .try_push(PaymentExecution { - asset: asset_kind.clone(), - amount: pay_amount, - payment_id, - }) - .map_err(|_| Error::::ExecutionRateLimit)?; - - remaining_amount = - remaining_amount.saturating_sub(pay_amount); - }, - Err(_) => continue, - } - } - - if remaining_amount.is_zero() { - spend.status = PaymentState::Attempted { - executions, - remaining_amount: Zero::zero(), - }; - } else { - spend.status = PaymentState::Partial(remaining_amount); - } - }, - PaymentState::Failed(prev) => { - let mut executions = BoundedVec::< - PaymentExecution< - T::AssetKind, - AssetBalanceOf, - ::Id, - >, - ConstU32<32>, - >::default(); - //let mut retry: AssetBalanceOf = - //executions.iter().map(|exec| exec.amount).sum(); - - let mut retry: AssetBalanceOf = prev.iter().fold(Zero::zero(), |acc, exec| acc.saturating_add(exec.amount)); - let used_assets: Vec<_> = - prev.iter().map(|exec| exec.asset.clone()).collect(); - - for exec in prev.iter() { - if retry.is_zero() { - break; - } - - let mut available = - T::AssetCategories::available_balance(&exec.asset); - - let asset_kind = if available.is_zero() { - match assets.iter().find(|&ak| { - !used_assets.contains(ak) && - !T::AssetCategories::available_balance(ak).is_zero() - }) { - Some(ak) => { - available = T::AssetCategories::available_balance(ak); - ak.clone() - }, - None => continue, - } - } else { - exec.asset.clone() - }; - - let pay_amount = if available >= retry { retry } else { available }; - - match T::Paymaster::pay( - &spend.beneficiary, - asset_kind.clone(), - pay_amount, - ) { - Ok(payment_id) => { - executions - .try_push(PaymentExecution { - asset: asset_kind.clone(), - - amount: pay_amount, - - payment_id, - }) - .map_err(|_| Error::::ExecutionRateLimit)?; - - retry = retry.saturating_sub(pay_amount); - }, - Err(_) => continue, - } - } - - if retry.is_zero() { - spend.status = PaymentState::Attempted { - executions, - remaining_amount: Zero::zero(), - }; - } else { - spend.status = PaymentState::Partial(retry); - } - }, - PaymentState::Partial(unpaid) => { - let mut executions = BoundedVec::< - PaymentExecution< - T::AssetKind, - AssetBalanceOf, - ::Id, - >, - ConstU32<32>, - >::default(); - - let mut remaining_amount = unpaid; - - for asset_kind in assets { - if remaining_amount.is_zero() { - break; - } - - let available = T::AssetCategories::available_balance(&asset_kind); - - let pay_amount = if available >= remaining_amount { - remaining_amount - } else { - available - }; - - match T::Paymaster::pay( - &spend.beneficiary, - asset_kind.clone(), - pay_amount, - ) { - Ok(payment_id) => { - executions - .try_push(PaymentExecution { - asset: asset_kind.clone(), - amount: pay_amount, - payment_id, - }) - .map_err(|_| Error::::ExecutionRateLimit)?; - - remaining_amount = - remaining_amount.saturating_sub(pay_amount); - }, - Err(_) => { - continue; - }, - } - } - - if remaining_amount.is_zero() { - spend.status = PaymentState::Attempted { - executions, - remaining_amount: Zero::zero(), - }; - } else { - spend.status = PaymentState::Partial(remaining_amount); - } - }, + SpendAsset::Category(ref category) => match spend.status { + PaymentState::Pending => { + let (executions, remaining_amount) = + Self::pay_from_category(category, &spend.beneficiary, spend.amount)?; - PaymentState::Attempted { .. } => { - return Err(Error::::AlreadyAttempted.into()); - }, - } + if remaining_amount.is_zero() { + spend.status = PaymentState::Attempted { + executions, + remaining_amount: Zero::zero(), + }; + } else { + spend.status = PaymentState::Partial(remaining_amount); + } + }, + PaymentState::Failed(ref failed_payments) => { + let (executions, retry_amount) = Self::retry_failed_category_payments( + category, + &spend.beneficiary, + failed_payments, + )?; + + if retry_amount.is_zero() { + spend.status = PaymentState::Attempted { + executions, + remaining_amount: Zero::zero(), + }; + } else { + spend.status = PaymentState::Partial(retry_amount); + } + }, + PaymentState::Partial(unpaid) => { + let (executions, remaining_amount) = + Self::pay_from_category(category, &spend.beneficiary, unpaid)?; + if remaining_amount.is_zero() { + spend.status = PaymentState::Attempted { + executions, + remaining_amount: Zero::zero(), + }; + } else { + spend.status = PaymentState::Partial(remaining_amount); + } + }, + PaymentState::Attempted { .. } => { + return Err(Error::::AlreadyAttempted.into()); + }, }, } - spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); - Spends::::insert(index, spend.clone()); // Emit events for each payment execution - if let PaymentState::Attempted { executions, .. } = &spend.status { for execution in executions.iter() { Self::deposit_event(Event::::Paid { index, - execution: execution.clone(), }); } } - Ok(()) } @@ -1166,15 +1059,21 @@ pub mod pallet { let failed: Vec<_> = results .iter() .filter(|(_, status)| matches!(status, Status::Failure)) - .map(|(exec, _)| exec.payment_id.clone()) + .map(|(exec, _)| (*exec).clone()) .collect(); if !failed.is_empty() { - spend.status = State::Failed; + let bounded = BoundedVec::try_from(failed.clone()) + .map_err(|_| Error::::TooManyFailedPayments)?; + + spend.status = State::Failed(bounded); Spends::::insert(index, spend); - for payment_id in failed { - Self::deposit_event(Event::::PaymentFailed { index, payment_id }); + for exec in failed { + Self::deposit_event(Event::::PaymentFailed { + index, + payment_id: exec.payment_id, + }); } return Ok(Pays::Yes.into()); } @@ -1190,8 +1089,14 @@ pub mod pallet { return Ok(Pays::No.into()); } - // TODO: Partial success or unknown statuses - Ok(Pays::Yes.into()) + if all_succeeded && !remaining_amount.is_zero() { + spend.status = State::Partial(*remaining_amount); + spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); + Spends::::insert(index, spend); + return Ok(Pays::Yes.into()) + } + + Ok(Pays::Yes.into()) }, _ => return Err(Error::::NotAttempted.into()), } @@ -1219,7 +1124,10 @@ pub mod pallet { T::RejectOrigin::ensure_origin(origin)?; let spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; ensure!( - matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + matches!( + spend.status, + PaymentState::Pending | PaymentState::Failed(_) | PaymentState::Partial(_) + ), Error::::AlreadyAttempted ); @@ -1241,6 +1149,22 @@ impl, I: 'static> Pallet { T::PalletId::get().into_account_truncating() } + fn ensure_sufficient_balance( + asset_kind: &T::AssetKind, + account: &T::AccountId, + required: AssetBalanceOf, + ) -> Result, DispatchError> { + T::AssetCategories::available_balance(asset_kind.clone(), account.clone()) + .ok_or(Error::::AssetNotFound.into()) + .and_then(|balance| { + if balance >= required { + Ok(balance) + } else { + Err(Error::::LowBalance.into()) + } + }) + } + // Backfill the `LastSpendPeriod` storage, assuming that no configuration has changed // since introducing this code. Used specifically for a migration-less switch to populate // `LastSpendPeriod`. @@ -1386,6 +1310,170 @@ impl, I: 'static> Pallet { .saturating_sub(T::Currency::minimum_balance()) } + fn pay_from_category( + category: &BoundedVec>, + beneficiary: &T::Beneficiary, + amount: AssetBalanceOf, + ) -> Result< + ( + BoundedVec< + PaymentExecution, ::Id>, + ConstU32<32>, + >, + AssetBalanceOf, + ), + DispatchError, + > { + let assets = T::AssetCategories::assets_in_category(category.as_slice()); + ensure!(!assets.is_empty(), Error::::EmptyAssetCategory); + + let mut executions = BoundedVec::< + PaymentExecution, ::Id>, + ConstU32<32>, + >::default(); + + let mut remaining_amount = amount; + + for asset_kind in assets { + if remaining_amount.is_zero() { + break; + } + + // let available = + // Self::ensure_sufficient_balance(&asset_kind, &Self::account_id(), amount)?; + + let available = match T::AssetCategories::available_balance(asset_kind.clone(), Self::account_id()) { + Some(balance) if balance > Zero::zero() => balance, + _ => continue, + }; + + // if available.is_zero() { + // continue; + // } + + let pay_amount = + if available >= remaining_amount { remaining_amount } else { available }; + + match T::Paymaster::pay(beneficiary, asset_kind.clone(), pay_amount) { + Ok(payment_id) => { + executions + .try_push(PaymentExecution { + asset: asset_kind.clone(), + amount: pay_amount, + payment_id, + }) + .map_err(|_| Error::::ExecutionRateLimit)?; + remaining_amount = remaining_amount.saturating_sub(pay_amount); + }, + Err(_) => continue, + } + } + Ok((executions, remaining_amount)) + } + + fn retry_failed_category_payments( + category: &BoundedVec>, + beneficiary: &T::Beneficiary, + failed_payments: &BoundedVec< + PaymentExecution, ::Id>, + ConstU32<32>, + >, + ) -> Result< + ( + BoundedVec< + PaymentExecution, ::Id>, + ConstU32<32>, + >, + AssetBalanceOf, + ), + DispatchError, + > { + let assets = T::AssetCategories::assets_in_category(category.as_slice()); + ensure!(!assets.is_empty(), Error::::EmptyAssetCategory); + + let mut executions = BoundedVec::< + PaymentExecution, ::Id>, + ConstU32<32>, + >::default(); + + let mut retry_amount: AssetBalanceOf = failed_payments + .iter() + .fold(Zero::zero(), |acc, exec| acc.saturating_add(exec.amount)); + let used_assets: Vec<_> = failed_payments.iter().map(|exec| exec.asset.clone()).collect(); + + for exec in failed_payments.iter() { + if retry_amount.is_zero() { + break; + } + + // let mut available = + // Self::ensure_sufficient_balance(&exec.asset, &Self::account_id(), exec.amount)?; + + let mut available = match T::AssetCategories::available_balance(exec.asset.clone(), Self::account_id()) { + Some(balance) if balance >= exec.amount => balance, + _ => Zero::zero(), + }; + + let asset_kind = if available.is_zero() { + match assets.iter().find(|&ak| { + !used_assets.contains(ak) + }) { + Some(ak) => { + available = + Self::ensure_sufficient_balance(ak, &Self::account_id(), exec.amount)?; + ak.clone() + }, + None => continue, + } + } else { + exec.asset.clone() + }; + + let pay_amount = if available >= retry_amount { retry_amount } else { available }; + match T::Paymaster::pay(beneficiary, asset_kind.clone(), pay_amount) { + Ok(payment_id) => { + executions + .try_push(PaymentExecution { + asset: asset_kind.clone(), + amount: pay_amount, + payment_id, + }) + .map_err(|_| Error::::ExecutionRateLimit)?; + retry_amount = retry_amount.saturating_sub(pay_amount); + }, + Err(_) => continue, + } + } + Ok((executions, retry_amount)) + } + + pub fn validate_category_spend( + category: &BoundedVec>, + amount: AssetBalanceOf, + ) -> Result, DispatchError> { + // Just return assets + let assets = T::AssetCategories::assets_in_category(category.as_slice()); + ensure!(!assets.is_empty(), Error::::EmptyAssetCategory); + + let mut accumulated: AssetBalanceOf = Zero::zero(); + + for asset_kind in &assets { + if let Some(balance) = + T::AssetCategories::available_balance(asset_kind.clone(), Self::account_id()) // TODO: + // Investigate + // returning + // DispatchError + { + accumulated = accumulated.saturating_add(balance); + if accumulated >= amount { + return Ok(assets); + } + } + } + + Err(Error::::LowBalance.into()) + } + /// Ensure the correctness of the state of this pallet. #[cfg(any(feature = "try-runtime", test))] fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index f9fb487f8228..5d0eae3bbda2 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -36,6 +36,7 @@ use frame_support::{ PalletId, }; +use frame_support::traits::AsEnsureOriginWithArg; use super::*; use crate as treasury; @@ -48,6 +49,7 @@ frame_support::construct_runtime!( { System: frame_system, Balances: pallet_balances, + Assets: pallet_assets, Treasury: treasury, Utility: pallet_utility, } @@ -78,22 +80,11 @@ thread_local! { pub static STATUS: RefCell> = RefCell::new(BTreeMap::new()); pub static LAST_ID: RefCell = RefCell::new(0u64); pub static ASSET_BALANCES: RefCell> = RefCell::new(BTreeMap::new()); - pub static CATEGORY_ASSETS: RefCell, Vec>> = RefCell::new(BTreeMap::new()); #[cfg(feature = "runtime-benchmarks")] pub static TEST_SPEND_ORIGIN_TRY_SUCCESFUL_ORIGIN_ERR: RefCell = RefCell::new(false); } -/// Set asset balance for testing -fn set_asset_balance(asset_id: u32, balance: u64) { - ASSET_BALANCES.with(|b| b.borrow_mut().insert(asset_id, balance)); -} - -/// Set assets in a category -fn set_category_assets(category: &[u8], assets: Vec) { - CATEGORY_ASSETS.with(|c| c.borrow_mut().insert(category.to_vec(), assets)); -} - /// paid balance for a given account and asset ids fn paid(who: u128, asset_id: u32) -> u64 { PAID.with(|p| p.borrow().get(&(who, asset_id)).cloned().unwrap_or(0)) @@ -146,19 +137,6 @@ impl Pay for TestPay { } } -pub struct TestAssetCategoryManager; -impl AssetCategoryManager for TestAssetCategoryManager { - type AssetKind = u32; - type Balance = u64; - - fn assets_in_category(category: &[u8]) -> Vec { - CATEGORY_ASSETS.with(|c| c.borrow().get(category).cloned().unwrap_or_default()) - } - - fn available_balance(asset: &Self::AssetKind) -> Self::Balance { - ASSET_BALANCES.with(|b| b.borrow().get(asset).cloned().unwrap_or(0)) - } -} parameter_types! { pub const Burn: Permill = Permill::from_percent(50); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); @@ -202,6 +180,39 @@ impl> ConversionFromAssetBalance for MulBy { fn ensure_successful(_: u32) {} } +parameter_types! { + pub const AssetDeposit: u64 = 1; + pub const AssetAccountDeposit: u64 = 1; + pub const MetadataDepositBase: u64 = 1; + pub const MetadataDepositPerByte: u64 = 1; + pub const ApprovalDeposit: u64 = 1; + pub const StringLimit: u32 = 50; + pub const MaxReserves: u32 = 5; +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = AssetDeposit; + type AssetAccountDeposit = AssetAccountDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = StringLimit; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type RemoveItemsLimit = ConstU32<5>; + type Holder = (); + type WeightInfo = (); + type ReserveData = (); +} + impl Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet; @@ -221,7 +232,7 @@ impl Config for Test { type BalanceConverter = MulBy>; type PayoutPeriod = SpendPayoutPeriod; type BlockNumberProvider = System; - type AssetCategories = TestAssetCategoryManager; + type AssetCategories = Assets; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } @@ -233,13 +244,6 @@ impl Default for ExtBuilder { #[cfg(feature = "runtime-benchmarks")] TEST_SPEND_ORIGIN_TRY_SUCCESFUL_ORIGIN_ERR.with(|i| *i.borrow_mut() = false); - // Setup default categories for tests - CATEGORY_ASSETS.with(|c| { - let mut map = c.borrow_mut(); - map.insert(b"USD*".to_vec(), vec![1, 2, 3]); - map.insert(b"EUR*".to_vec(), vec![4, 5]); - }); - ASSET_BALANCES.with(|b| { let mut map = b.borrow_mut(); map.insert(1, 100); @@ -260,18 +264,45 @@ impl ExtBuilder { self } - pub fn with_asset_balance(self, asset_id: u32, balance: u64) -> Self { - set_asset_balance(asset_id, balance); - self - } - - pub fn with_category_assets(self, category: &[u8], assets: Vec) -> Self { - set_category_assets(category, assets); - self - } - pub fn build(self) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let assets_config: pallet_assets::GenesisConfig = pallet_assets::GenesisConfig { + + assets: vec![ + // id, owner, is_sufficient, min_balance + (1, 0, true, 1), + (2, 0, true, 1), + (3, 0, true, 1), + (4, 0, true, 1), + (5, 0, true, 1), + (10, 0, true, 1), + (11, 0, true, 1), + ], + + metadata: vec![ + // id name, symbol, decimals + (1, "Asset 1".into(), "A1".into(), 10), + (2, "Asset 2".into(), "A2".into(), 10), + (3, "Asset 3".into(), "A3".into(), 10), + (4, "Asset 4".into(), "A4".into(), 10), + (5, "Asset 5".into(), "A5".into(), 10), + (10, "Asset 10".into(), "A10".into(), 10), + (11, "Asset 11".into(), "A11".into(), 10), + ], + + accounts: vec![ + // id, account_id, balance + (1, Treasury::account_id(), 20), + (2, Treasury::account_id(), 20), + (3, Treasury::account_id(), 20), + (4, Treasury::account_id(), 25), + (5, Treasury::account_id(), 50), + (10, Treasury::account_id(), 50), + (11, Treasury::account_id(), 30), + ], + next_asset_id: None, + reserves: vec![], + }; pallet_balances::GenesisConfig:: { // Total issuance will be 200 with treasury account initialized at ED. balances: vec![(0, 100), (1, 98), (2, 1)], @@ -279,10 +310,34 @@ impl ExtBuilder { } .assimilate_storage(&mut t) .unwrap(); + + assets_config.assimilate_storage(&mut t).unwrap(); crate::GenesisConfig::::default().assimilate_storage(&mut t).unwrap(); let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); + ext.execute_with(|| { + + System::set_block_number(1); + + let usd_category: BoundedVec> = + BoundedVec::try_from(b"USD".to_vec()).unwrap(); + let stable_category: BoundedVec> = + BoundedVec::try_from(b"STABLE".to_vec()).unwrap(); + + // TODO: Create assets in USD category? + + // Set up categories + pallet_assets::AssetCategories::::insert( + &usd_category, + BoundedVec::try_from(vec![1u32, 2u32, 3u32]).unwrap(), + ); + + pallet_assets::AssetCategories::::insert( + &stable_category, + BoundedVec::try_from(vec![10u32, 11u32]).unwrap(), + ); + + }); ext } } @@ -1242,8 +1297,7 @@ fn category_spend_works() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); - // Create a category spend for USD* category - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1284,19 +1338,14 @@ fn category_spend_works() { #[test] fn category_payout_distributes_across_assets() { ExtBuilder::default() - .with_asset_balance(1, 20) - .with_asset_balance(2, 10) - .with_asset_balance(3, 30) .build() .execute_with(|| { System::set_block_number(1); - // Create a category spend for USD* category - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); - // Request 50 USD* - should use all available balances assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), Box::new(SpendAsset::Category(bounded_category)), @@ -1305,21 +1354,18 @@ fn category_payout_distributes_across_assets() { None )); - // Payout the spend assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); assert_eq!(paid(6, 1), 20); - assert_eq!(paid(6, 2), 10); - assert_eq!(paid(6, 3), 20); + assert_eq!(paid(6, 2), 20); + assert_eq!(paid(6, 3), 10); - // Check spend status let spend = Spends::::get(0).unwrap(); match &spend.status { PaymentState::Attempted { executions, remaining_amount } => { assert_eq!(executions.len(), 3); assert_eq!(*remaining_amount, 0); - // Check execution details let mut asset_payments = std::collections::BTreeMap::new(); for exec in executions.iter() { asset_payments.insert(exec.asset, exec.amount); @@ -1337,18 +1383,14 @@ fn category_payout_distributes_across_assets() { #[test] fn category_payout_partial_fulfillment() { ExtBuilder::default() - .with_asset_balance(1, 10) - .with_asset_balance(2, 15) - .with_asset_balance(3, 5) .build() .execute_with(|| { System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); - // Request 50 USD* spend to a 30 USD* pool assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), Box::new(SpendAsset::Category(bounded_category)), @@ -1359,8 +1401,7 @@ fn category_payout_partial_fulfillment() { assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - // Check partial payment - assert_eq!(paid(6, 1), 10); + assert_eq!(paid(6, 1), 20); assert_eq!(paid(6, 2), 15); assert_eq!(paid(6, 3), 5); @@ -1387,14 +1428,11 @@ fn category_payout_partial_fulfillment() { #[test] fn category_payout_skips_assets_with_no_balance() { ExtBuilder::default() - .with_asset_balance(1, 0) - .with_asset_balance(2, 40) - .with_asset_balance(3, 0) .build() .execute_with(|| { System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1408,19 +1446,18 @@ fn category_payout_skips_assets_with_no_balance() { assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - // Only AssetId = 2 should be used - assert_eq!(paid(6, 1), 0); - assert_eq!(paid(6, 2), 30); + assert_eq!(paid(6, 1), 20); + assert_eq!(paid(6, 2), 10); assert_eq!(paid(6, 3), 0); let spend = Spends::::get(0).unwrap(); match &spend.status { PaymentState::Attempted { executions, remaining_amount } => { - assert_eq!(executions.len(), 1); // Only USDC used - assert_eq!(*remaining_amount, 0); // Full amount paid + assert_eq!(executions.len(), 2); + assert_eq!(*remaining_amount, 0); - assert_eq!(executions[0].asset, 2); - assert_eq!(executions[0].amount, 30); + assert_eq!(executions[0].asset, 1); + assert_eq!(executions[0].amount, 20); }, _ => panic!("Expected Attempted status"), } @@ -1430,14 +1467,11 @@ fn category_payout_skips_assets_with_no_balance() { #[test] fn category_payout_fails_when_no_assets_available() { ExtBuilder::default() - .with_asset_balance(1, 0) - .with_asset_balance(2, 0) - .with_asset_balance(3, 0) .build() .execute_with(|| { System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1449,7 +1483,6 @@ fn category_payout_fails_when_no_assets_available() { None )); - // Payout should fail - no assets have balance assert_noop!( Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::PayoutError @@ -1462,12 +1495,10 @@ fn category_spend_with_unknown_category() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); - // Try to spend from a non-existent category - let category = b"GBP*".to_vec(); // No GBP assets defined + let category = b"GBP".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); - // Should fail during conversion because category has no assets assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), @@ -1484,14 +1515,10 @@ fn category_spend_with_unknown_category() { #[test] fn mixed_specific_and_category_spends() { ExtBuilder::default() - .with_asset_balance(1, 100) - .with_asset_balance(2, 100) - .with_asset_balance(4, 100) .build() .execute_with(|| { System::set_block_number(1); - // Specific asset spend assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), Box::new(SpendAsset::Specific(4)), @@ -1500,8 +1527,7 @@ fn mixed_specific_and_category_spends() { None )); - // Category spend - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1513,18 +1539,14 @@ fn mixed_specific_and_category_spends() { None )); - // Payout specific spend assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); assert_eq!(paid(7, 4), 25); - // Payout category spend assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); - // Category spend should use USD assets let paid_usd = paid(8, 1) + paid(8, 2); assert_eq!(paid_usd, 30); - // Check counts assert_eq!(SpendCount::::get(), 2); assert_eq!(Spends::::iter().count(), 2); }); @@ -1533,17 +1555,14 @@ fn mixed_specific_and_category_spends() { #[test] fn category_check_status_with_multiple_executions() { ExtBuilder::default() - .with_asset_balance(1, 20) - .with_asset_balance(2, 20) .build() .execute_with(|| { System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); - // Request 40 USD* from a 40 USD* pool assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), Box::new(SpendAsset::Category(bounded_category)), @@ -1552,10 +1571,8 @@ fn category_check_status_with_multiple_executions() { None )); - // Payout the spend assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - // Get payment IDs let payment_ids = get_all_payment_ids(0); assert_eq!(payment_ids.len(), 2); @@ -1566,19 +1583,14 @@ fn category_check_status_with_multiple_executions() { #[test] fn category_spend_with_custom_category() { ExtBuilder::default() - .with_category_assets(b"STABLE*", vec![10, 11, 12]) - .with_asset_balance(10, 50) - .with_asset_balance(11, 30) - .with_asset_balance(12, 20) .build() .execute_with(|| { System::set_block_number(1); - let category = b"STABLE*".to_vec(); + let category = b"STABLE".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); - // Request 80 USD* from a 100 USD* pool assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), Box::new(SpendAsset::Category(bounded_category)), @@ -1587,7 +1599,6 @@ fn category_spend_with_custom_category() { None )); - // Payout - uses 2 out of 3 assets assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); assert_eq!(paid(6, 10), 50); @@ -1610,7 +1621,7 @@ fn category_spend_respects_spend_origin_limit() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1638,12 +1649,12 @@ fn category_spend_respects_spend_origin_limit() { #[test] fn category_spend_with_empty_category_assets() { ExtBuilder::default() - .with_category_assets(b"EMPTY*", vec![]) // Empty category + /*.with_category_assets(b"EMPTY*", vec![]) // Empty category*/ .build() .execute_with(|| { System::set_block_number(1); - let category = b"EMPTY*".to_vec(); + let category = b"EMPTY".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1662,10 +1673,10 @@ fn category_spend_with_empty_category_assets() { #[test] fn category_spend_cannot_void_after_payout() { - ExtBuilder::default().with_asset_balance(1, 50).build().execute_with(|| { + ExtBuilder::default()/*.with_asset_balance(1, 50)*/.build().execute_with(|| { System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1693,7 +1704,7 @@ fn category_spend_expiry_works() { assert_eq!(::PayoutPeriod::get(), 5); System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1717,7 +1728,7 @@ fn category_spend_valid_from_works() { ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); - let category = b"USD*".to_vec(); + let category = b"USD".to_vec(); let bounded_category: BoundedVec> = BoundedVec::try_from(category.clone()).unwrap(); @@ -1737,8 +1748,10 @@ fn category_spend_valid_from_works() { // Can payout at valid_from System::set_block_number(3); - set_asset_balance(1, 50); + /* + set_asset_balance(1, 50); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + */ assert_eq!(paid(6, 1), 50); From e971e0047d66295469b800bd68ab96364c3d8d00 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Mon, 26 Jan 2026 10:01:31 +0100 Subject: [PATCH 06/21] incomplete test, to get reviews --- substrate/frame/treasury/src/tests.rs | 51 ++++++--------------------- 1 file changed, 11 insertions(+), 40 deletions(-) diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 5d0eae3bbda2..72c7de8e9bff 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -1291,7 +1291,6 @@ fn multiple_spend_periods_work() { }); } -// New tests for category spends #[test] fn category_spend_works() { ExtBuilder::default().build().execute_with(|| { @@ -1372,8 +1371,8 @@ fn category_payout_distributes_across_assets() { } assert_eq!(asset_payments.get(&1), Some(&20)); - assert_eq!(asset_payments.get(&2), Some(&10)); - assert_eq!(asset_payments.get(&3), Some(&20)); + assert_eq!(asset_payments.get(&2), Some(&20)); + assert_eq!(asset_payments.get(&3), Some(&10)); }, _ => panic!("Expected Attempted status"), } @@ -1402,23 +1401,23 @@ fn category_payout_partial_fulfillment() { assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); assert_eq!(paid(6, 1), 20); - assert_eq!(paid(6, 2), 15); - assert_eq!(paid(6, 3), 5); + assert_eq!(paid(6, 2), 20); + assert_eq!(paid(6, 3), 10); let spend = Spends::::get(0).unwrap(); match &spend.status { PaymentState::Attempted { executions, remaining_amount } => { assert_eq!(executions.len(), 3); - assert_eq!(*remaining_amount, 20); + assert_eq!(*remaining_amount, 0); let mut asset_payments = std::collections::BTreeMap::new(); for exec in executions.iter() { asset_payments.insert(exec.asset, exec.amount); } - assert_eq!(asset_payments.get(&1), Some(&10)); - assert_eq!(asset_payments.get(&2), Some(&15)); - assert_eq!(asset_payments.get(&3), Some(&5)); + assert_eq!(asset_payments.get(&1), Some(&20)); + assert_eq!(asset_payments.get(&2), Some(&20)); + assert_eq!(asset_payments.get(&3), Some(&10)); }, _ => panic!("Expected Attempted status"), } @@ -1464,32 +1463,6 @@ fn category_payout_skips_assets_with_no_balance() { }); } -#[test] -fn category_payout_fails_when_no_assets_available() { - ExtBuilder::default() - .build() - .execute_with(|| { - System::set_block_number(1); - - let category = b"USD".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); - - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(13), - Box::new(SpendAsset::Category(bounded_category)), - 10, - Box::new(6), - None - )); - - assert_noop!( - Treasury::payout(RuntimeOrigin::signed(1), 0), - Error::::PayoutError - ); - }); -} - #[test] fn category_spend_with_unknown_category() { ExtBuilder::default().build().execute_with(|| { @@ -1748,17 +1721,15 @@ fn category_spend_valid_from_works() { // Can payout at valid_from System::set_block_number(3); - /* - set_asset_balance(1, 50); + //set_asset_balance(1, 50); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - */ - assert_eq!(paid(6, 1), 50); + assert_eq!(paid(6, 1), 20); let spend = Spends::::get(0).unwrap(); match &spend.status { PaymentState::Attempted { executions, remaining_amount } => { - assert_eq!(executions.len(), 1); + assert_eq!(executions.len(), 3); assert_eq!(*remaining_amount, 0); }, _ => panic!("Expected Attempted status"), From df933dba3e7fdaf5db60795241f57679c97b0105 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Thu, 29 Jan 2026 17:04:32 +0100 Subject: [PATCH 07/21] runtime impl placeholders --- Cargo.lock | 3 ++ .../asset-hub-westend/src/governance/mod.rs | 19 +++++++++++++ .../collectives-westend/Cargo.toml | 4 +++ .../collectives-westend/src/fellowship/mod.rs | 18 ++++++++++++ polkadot/runtime/rococo/Cargo.toml | 4 +++ polkadot/runtime/rococo/src/lib.rs | 17 +++++++++++ polkadot/runtime/westend/Cargo.toml | 4 +++ polkadot/runtime/westend/src/lib.rs | 17 +++++++++++ substrate/bin/node/runtime/src/lib.rs | 28 ++++++++++++++++++- substrate/frame/assets/src/lib.rs | 11 -------- 10 files changed, 113 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d048e2be56e..0bdd5b357889 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3489,6 +3489,7 @@ dependencies = [ "hex-literal", "pallet-alliance", "pallet-asset-rate", + "pallet-assets", "pallet-aura", "pallet-authorship", "pallet-balances", @@ -19250,6 +19251,7 @@ dependencies = [ "hex-literal", "log", "pallet-asset-rate", + "pallet-assets", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", @@ -27605,6 +27607,7 @@ dependencies = [ "hex-literal", "log", "pallet-asset-rate", + "pallet-assets", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 3b0c006c39ce..c488eea89278 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -29,8 +29,10 @@ use polkadot_runtime_common::{ impls::{ContainsParts, VersionedLocatableAsset}, prod_or_fast, }; +use alloc::{vec, vec::Vec}; use sp_runtime::{traits::IdentityLookup, Percent}; use xcm::latest::BodyId; +use pallet_assets::AssetCategoryManager; mod origins; pub use origins::{ @@ -124,6 +126,22 @@ parameter_types! { pub type TreasurySpender = EitherOf, Spender>; +pub struct AHWAssetCategories; + +impl AssetCategoryManager for AHWAssetCategories { + type AssetKind = VersionedLocatableAsset; + type Balance = Balance; + + fn assets_in_category(_category: &[u8]) -> Vec { + vec![] + } + + fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { + None + } +} + + impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; @@ -150,6 +168,7 @@ impl pallet_treasury::Config for Runtime { AssetRate, >; type PayoutPeriod = PayoutSpendPeriod; + type AssetCategories = AHWAssetCategories; type BlockNumberProvider = RelaychainDataProvider; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = parachains_common::pay::benchmarks::LocalPayArguments< diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml index 7c466c22693e..418fb48e2537 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml @@ -27,6 +27,7 @@ frame-system = { workspace = true } frame-system-benchmarking = { optional = true, workspace = true } frame-system-rpc-runtime-api = { workspace = true } frame-try-runtime = { optional = true, workspace = true } +pallet-assets = { workspace = true } pallet-alliance = { workspace = true } pallet-asset-rate = { workspace = true } pallet-aura = { workspace = true } @@ -114,6 +115,7 @@ runtime-benchmarks = [ "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-alliance/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", @@ -157,6 +159,7 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime/try-runtime", "pallet-alliance/try-runtime", + "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-aura/try-runtime", "pallet-authorship/try-runtime", @@ -205,6 +208,7 @@ std = [ "frame-system/std", "frame-try-runtime?/std", "pallet-alliance/std", + "pallet-assets/std", "pallet-asset-rate/std", "pallet-aura/std", "pallet-authorship/std", diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs index ceadc131c384..7bb5bd781191 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs @@ -34,6 +34,8 @@ use frame_support::{ }, PalletId, }; +use alloc::{vec, vec::Vec}; +use pallet_assets::AssetCategoryManager; use frame_system::{EnsureRoot, EnsureRootWithSuccess}; pub use origins::{ pallet_origins as pallet_fellowship_origins, Architects, EnsureCanPromoteTo, EnsureCanRetainAt, @@ -285,6 +287,21 @@ pub type FellowshipTreasuryPaymaster = PayOverXcm< pub type FellowshipTreasuryInstance = pallet_treasury::Instance1; +pub struct CWAssetCategories; + +impl AssetCategoryManager for CWAssetCategories { + type AssetKind = VersionedLocatableAsset; + type Balance = Balance; + + fn assets_in_category(_category: &[u8]) -> Vec { + vec![] + } + + fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { + None + } +} + impl pallet_treasury::Config for Runtime { type WeightInfo = weights::pallet_treasury::WeightInfo; type PalletId = FellowshipTreasuryPalletId; @@ -329,6 +346,7 @@ impl pallet_treasury::Config for Runtime { AssetRate, >; type PayoutPeriod = ConstU32<{ 30 * DAYS }>; + type AssetCategories = CWAssetCategories; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments< sp_core::ConstU8<1>, diff --git a/polkadot/runtime/rococo/Cargo.toml b/polkadot/runtime/rococo/Cargo.toml index be65accb31ce..e937a544399f 100644 --- a/polkadot/runtime/rococo/Cargo.toml +++ b/polkadot/runtime/rococo/Cargo.toml @@ -48,6 +48,7 @@ frame-executive = { workspace = true } frame-support = { features = ["tuples-96"], workspace = true } frame-system = { workspace = true } frame-system-rpc-runtime-api = { workspace = true } +pallet-assets = { workspace = true } pallet-asset-rate = { workspace = true } pallet-authority-discovery = { workspace = true } pallet-authorship = { workspace = true } @@ -135,6 +136,7 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", + "pallet-assets/std", "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", @@ -219,6 +221,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-balances/runtime-benchmarks", @@ -275,6 +278,7 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", + "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 2f224384a904..8635e7800642 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -38,6 +38,7 @@ use alloc::{ vec, vec::Vec, }; +use pallet_assets::AssetCategoryManager; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use core::cmp::Ordering; use frame_support::{ @@ -330,6 +331,21 @@ impl EnsureOriginWithArg for DynamicParamet } } +pub struct RococoAssetCategories; + +impl AssetCategoryManager for RococoAssetCategories { + type AssetKind = VersionedLocatableAsset; + type Balance = Balance; + + fn assets_in_category(_category: &[u8]) -> Vec { + vec![] + } + + fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { + None + } +} + impl pallet_scheduler::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; type RuntimeEvent = RuntimeEvent; @@ -561,6 +577,7 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; + type AssetCategories = RococoAssetCategories; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml index c907ceb65665..d4f8719d57e9 100644 --- a/polkadot/runtime/westend/Cargo.toml +++ b/polkadot/runtime/westend/Cargo.toml @@ -51,6 +51,7 @@ frame-metadata-hash-extension = { workspace = true } frame-support = { features = ["experimental", "tuples-96"], workspace = true } frame-system = { workspace = true } frame-system-rpc-runtime-api = { workspace = true } +pallet-assets = { workspace = true } pallet-asset-rate = { workspace = true } pallet-authority-discovery = { workspace = true } pallet-authorship = { workspace = true } @@ -147,6 +148,7 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", + "pallet-assets/std", "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", @@ -240,6 +242,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", @@ -303,6 +306,7 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", + "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index bdef88df7e8e..345d8bff17af 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -43,6 +43,7 @@ use frame_support::{ weights::{ConstantMultiplier, WeightMeter}, PalletId, }; +use pallet_assets::AssetCategoryManager; use frame_system::{EnsureRoot, EnsureSigned}; use pallet_grandpa::{fg_primitives, AuthorityId as GrandpaId}; use pallet_identity::legacy::IdentityInfo; @@ -894,6 +895,21 @@ impl ah_client::SendToAssetHub for StakingXcmToAssetHub { } } +pub struct WestendAssetCategories; + +impl AssetCategoryManager for WestendAssetCategories { + type AssetKind = VersionedLocatableAsset; + type Balance = Balance; + + fn assets_in_category(_category: &[u8]) -> Vec { + vec![] + } + + fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { + None + } +} + impl ah_client::Config for Runtime { type CurrencyBalance = Balance; type AssetHubOrigin = @@ -977,6 +993,7 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; + type AssetCategories = WestendAssetCategories; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 3a54a6bb09f1..ea5805cc63eb 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -31,7 +31,7 @@ use pallet_multi_asset_bounties::ArgumentsFactory as PalletMultiAssetBountiesArg use pallet_treasury::ArgumentsFactory as PalletTreasuryArgumentsFactory; #[cfg(feature = "runtime-benchmarks")] use polkadot_sdk::sp_core::crypto::FromEntropy; - +use crate::pallet_assets::AssetCategoryManager; use polkadot_sdk::*; use alloc::{vec, vec::Vec}; @@ -262,6 +262,31 @@ impl Contains> for TxPauseWhitelistedCalls { } } +pub struct CombinedAssetManager; + +impl AssetCategoryManager for CombinedAssetManager { + type AssetKind = NativeOrWithId; + type Balance = Balance; + + fn assets_in_category(category: &[u8]) -> Vec { + Assets::assets_in_category(category) + .into_iter() + .map(|asset_id| NativeOrWithId::WithId(asset_id)) + .collect() + } + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { + match asset { + NativeOrWithId::Native => { + Some(Balances::free_balance(&owner)) + } + NativeOrWithId::WithId(asset_id) => { + Assets::available_balance(asset_id, owner) + } + } + } +} + #[cfg(feature = "runtime-benchmarks")] pub struct AssetRateArguments; #[cfg(feature = "runtime-benchmarks")] @@ -1328,6 +1353,7 @@ impl pallet_treasury::Config for Runtime { type BalanceConverter = AssetRate; type PayoutPeriod = SpendPayoutPeriod; type BlockNumberProvider = System; + type AssetCategories = CombinedAssetManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = PalletTreasuryArguments; } diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index 346b19551b17..223166feecb5 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -508,17 +508,6 @@ pub mod pallet { ValueQuery, >; - /* - #[pallet::storage] - pub type AssetCategories, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - BoundedVec>, // Category - BoundedVec>, // Assets in this category - ValueQuery, - >; - */ - /// The asset ID enforced for the next asset creation, if any present. Otherwise, this storage /// item has no effect. /// From e8f229e3d2c157e8835f675c1bee25c00cef4565 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Mon, 2 Feb 2026 04:40:23 +0100 Subject: [PATCH 08/21] Box removal enables debug? --- substrate/frame/treasury/src/lib.rs | 18 +++--- substrate/frame/treasury/src/tests.rs | 88 +++++++++++++-------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 47e945962c83..03ff459783fb 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -210,13 +210,13 @@ pub enum PaymentState { #[derive( Encode, Decode, - DecodeWithMemTracking, + DecodeWithMemTracking, Clone, PartialEq, Eq, - MaxEncodedLen, - RuntimeDebug, - TypeInfo, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, )] pub enum SpendAsset { /// Spend a specific asset @@ -750,7 +750,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::spend())] pub fn spend( origin: OriginFor, - asset: Box>, + asset: SpendAsset, #[pallet::compact] amount: AssetBalanceOf, beneficiary: Box>, valid_from: Option>, @@ -763,7 +763,7 @@ pub mod pallet { let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); ensure!(expire_at > now, Error::::SpendExpired); - match *asset { + match asset { SpendAsset::Category(ref category) => { let assets = Self::validate_category_spend(category, amount)?; @@ -821,7 +821,7 @@ pub mod pallet { }, } - let native_amount = match *asset { + let native_amount = match asset { SpendAsset::Specific(ref asset_kind) => T::BalanceConverter::from_asset_balance(amount, asset_kind.clone()) .map_err(|_| Error::::BalanceConversionFailed)?, @@ -864,7 +864,7 @@ pub mod pallet { Spends::::insert( index, SpendStatus { - asset: *asset.clone(), + asset: asset.clone(), amount, beneficiary: beneficiary.clone(), valid_from, @@ -876,7 +876,7 @@ pub mod pallet { Self::deposit_event(Event::AssetSpendApproved { index, - asset: *asset, + asset: asset, amount, beneficiary, valid_from, diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 72c7de8e9bff..e96834994c1f 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -652,13 +652,13 @@ fn spending_in_batch_respects_max_total() { assert_ok!(RuntimeCall::from(UtilityCall::batch_all { calls: vec![ RuntimeCall::from(TreasuryCall::spend { - asset: Box::new(SpendAsset::Specific(1)), + asset: SpendAsset::Specific(1), amount: 1, beneficiary: Box::new(100), valid_from: None, }), RuntimeCall::from(TreasuryCall::spend { - asset: Box::new(SpendAsset::Specific(1)), + asset: SpendAsset::Specific(1), amount: 1, beneficiary: Box::new(101), valid_from: None, @@ -671,13 +671,13 @@ fn spending_in_batch_respects_max_total() { RuntimeCall::from(UtilityCall::batch_all { calls: vec![ RuntimeCall::from(TreasuryCall::spend { - asset: Box::new(SpendAsset::Specific(1)), + asset: SpendAsset::Specific(1), amount: 2, beneficiary: Box::new(100), valid_from: None, }), RuntimeCall::from(TreasuryCall::spend { - asset: Box::new(SpendAsset::Specific(1)), + asset: SpendAsset::Specific(1), amount: 2, beneficiary: Box::new(101), valid_from: None, @@ -695,14 +695,14 @@ fn spend_origin_works() { ExtBuilder::default().build().execute_with(|| { assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 1, Box::new(6), None )); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -710,7 +710,7 @@ fn spend_origin_works() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 3, Box::new(6), None @@ -719,7 +719,7 @@ fn spend_origin_works() { ); assert_ok!(Treasury::spend( RuntimeOrigin::signed(11), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 5, Box::new(6), None @@ -727,7 +727,7 @@ fn spend_origin_works() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(11), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 6, Box::new(6), None @@ -736,7 +736,7 @@ fn spend_origin_works() { ); assert_ok!(Treasury::spend( RuntimeOrigin::signed(12), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 10, Box::new(6), None @@ -744,7 +744,7 @@ fn spend_origin_works() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(12), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 11, Box::new(6), None @@ -763,7 +763,7 @@ fn spend_works() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -804,7 +804,7 @@ fn spend_expires() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -816,7 +816,7 @@ fn spend_expires() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), Some(0) @@ -834,7 +834,7 @@ fn spend_payout_works() { // approve a `2` coins spend of asset `1` to beneficiary `6`, the spend valid from now. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -869,7 +869,7 @@ fn payout_extends_expiry() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -902,7 +902,7 @@ fn payout_retry_works() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -936,7 +936,7 @@ fn spend_valid_from_works() { // spend valid from block `2`. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), Some(2) @@ -949,7 +949,7 @@ fn spend_valid_from_works() { // spend approved even if `valid_from` in the past since the payout period has not passed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), Some(4) @@ -966,7 +966,7 @@ fn void_spend_works() { // spend cannot be voided if already attempted. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), Some(1) @@ -980,7 +980,7 @@ fn void_spend_works() { // void spend. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), Some(10) @@ -999,7 +999,7 @@ fn check_status_works() { // spend `0` expired and can be removed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -1012,7 +1012,7 @@ fn check_status_works() { // spend `1` payment failed and expired hence can be removed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -1036,7 +1036,7 @@ fn check_status_works() { // spend `2` payment succeed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -1051,7 +1051,7 @@ fn check_status_works() { // spend `3` payment in process. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -1067,7 +1067,7 @@ fn check_status_works() { // spend `4` removed since the payment status is unknown. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 2, Box::new(6), None @@ -1173,7 +1173,7 @@ fn try_state_spends_invariant_1_works() { assert_ok!({ Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 1, Box::new(6), None, @@ -1199,7 +1199,7 @@ fn try_state_spends_invariant_2_works() { use frame_support::pallet_prelude::DispatchError::Other; // Propose and approve a spend assert_ok!({ - Treasury::spend(RuntimeOrigin::signed(10), Box::new(SpendAsset::Specific(1)), 1, Box::new(6), None) + Treasury::spend(RuntimeOrigin::signed(10), SpendAsset::Specific(1), 1, Box::new(6), None) }); assert_eq!(Spends::::iter().count(), 1); let current_spend_count = SpendCount::::get(); @@ -1230,7 +1230,7 @@ fn try_state_spends_invariant_3_works() { assert_ok!({ Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Specific(1)), + SpendAsset::Specific(1), 1, Box::new(6), None, @@ -1302,7 +1302,7 @@ fn category_spend_works() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Category(bounded_category.clone())), + SpendAsset::Category(bounded_category.clone()), 2, Box::new(6), None @@ -1347,7 +1347,7 @@ fn category_payout_distributes_across_assets() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 50, Box::new(6), None @@ -1392,7 +1392,7 @@ fn category_payout_partial_fulfillment() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 50, Box::new(6), None @@ -1437,7 +1437,7 @@ fn category_payout_skips_assets_with_no_balance() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 30, Box::new(6), None @@ -1475,7 +1475,7 @@ fn category_spend_with_unknown_category() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 10, Box::new(6), None @@ -1494,7 +1494,7 @@ fn mixed_specific_and_category_spends() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Specific(4)), + SpendAsset::Specific(4), 25, Box::new(7), None @@ -1506,7 +1506,7 @@ fn mixed_specific_and_category_spends() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 30, Box::new(8), None @@ -1538,7 +1538,7 @@ fn category_check_status_with_multiple_executions() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 40, Box::new(6), None @@ -1566,7 +1566,7 @@ fn category_spend_with_custom_category() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 80, Box::new(6), None @@ -1600,7 +1600,7 @@ fn category_spend_respects_spend_origin_limit() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Category(bounded_category.clone())), + SpendAsset::Category(bounded_category.clone()), 2, Box::new(6), None @@ -1609,7 +1609,7 @@ fn category_spend_respects_spend_origin_limit() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 3, Box::new(6), None @@ -1634,7 +1634,7 @@ fn category_spend_with_empty_category_assets() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 10, Box::new(6), None @@ -1655,7 +1655,7 @@ fn category_spend_cannot_void_after_payout() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 50, Box::new(6), None @@ -1684,7 +1684,7 @@ fn category_spend_expiry_works() { // Create category spend assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 50, Box::new(6), None @@ -1708,7 +1708,7 @@ fn category_spend_valid_from_works() { // Create category spend valid from block 3 assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), + SpendAsset::Category(bounded_category), 50, Box::new(6), Some(3) From 21bf5040e81302417e50e2214f2fdfd10484d0b3 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Mon, 2 Feb 2026 05:07:46 +0100 Subject: [PATCH 09/21] derive debug? --- substrate/frame/treasury/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 329724effb45..ceb4bb108ad9 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -196,7 +196,7 @@ pub enum PaymentState { Clone, PartialEq, Eq, - RuntimeDebug, + Debug, TypeInfo, MaxEncodedLen, )] @@ -216,7 +216,7 @@ pub enum SpendAsset { Clone, PartialEq, Eq, - RuntimeDebug, + Debug, TypeInfo, MaxEncodedLen, )] From e975c41e73ac51d3deaca9d2e734293dd6797561 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Tue, 3 Feb 2026 14:24:33 +0100 Subject: [PATCH 10/21] runtime implementaions --- Cargo.lock | 4 ++++ polkadot/runtime/common/Cargo.toml | 4 ++++ polkadot/runtime/common/src/impls.rs | 20 +++++++++++++++++++ .../runtime/common/src/integration_tests.rs | 1 + substrate/frame/bounties/Cargo.toml | 2 ++ substrate/frame/bounties/src/tests.rs | 18 +++++++++++++++++ substrate/frame/child-bounties/Cargo.toml | 4 ++++ substrate/frame/child-bounties/src/tests.rs | 19 +++++++++++++++++- .../runtimes/parachain/src/governance/mod.rs | 16 +++++++++++++++ .../staking-async/runtimes/rc/Cargo.toml | 5 +++++ .../staking-async/runtimes/rc/src/lib.rs | 18 +++++++++++++++++ substrate/frame/tips/Cargo.toml | 4 ++++ substrate/frame/tips/src/lib.rs | 1 - substrate/frame/tips/src/tests.rs | 19 +++++++++++++++++- 14 files changed, 132 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97117d1a1805..a0a031cf0a62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11882,6 +11882,7 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-assets", "pallet-balances", "pallet-bounties", "pallet-treasury", @@ -13762,6 +13763,7 @@ dependencies = [ "hex-literal", "log", "pallet-asset-rate", + "pallet-assets", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", @@ -14003,6 +14005,7 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-assets", "pallet-balances", "pallet-treasury", "parity-scale-codec", @@ -16211,6 +16214,7 @@ dependencies = [ "libsecp256k1", "log", "pallet-asset-rate", + "pallet-assets", "pallet-authorship", "pallet-babe", "pallet-balances", diff --git a/polkadot/runtime/common/Cargo.toml b/polkadot/runtime/common/Cargo.toml index 31a726914801..902c752ba01c 100644 --- a/polkadot/runtime/common/Cargo.toml +++ b/polkadot/runtime/common/Cargo.toml @@ -34,6 +34,7 @@ sp-staking = { features = ["serde"], workspace = true } frame-election-provider-support = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } +pallet-assets = { workspace = true } pallet-asset-rate = { optional = true, workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } @@ -84,6 +85,7 @@ std = [ "frame-system/std", "libsecp256k1/std", "log/std", + "pallet-assets/std", "pallet-asset-rate?/std", "pallet-authorship/std", "pallet-balances/std", @@ -124,6 +126,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "libsecp256k1/hmac", "libsecp256k1/static-context", + "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-balances/runtime-benchmarks", @@ -150,6 +153,7 @@ try-runtime = [ "frame-support-test/try-runtime", "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authorship/try-runtime", "pallet-babe?/try-runtime", diff --git a/polkadot/runtime/common/src/impls.rs b/polkadot/runtime/common/src/impls.rs index 53229495dd58..c88cc6db0046 100644 --- a/polkadot/runtime/common/src/impls.rs +++ b/polkadot/runtime/common/src/impls.rs @@ -22,10 +22,13 @@ use frame_support::traits::{ tokens::imbalance::ResolveTo, Contains, ContainsPair, Imbalance, OnUnbalanced, }; +use pallet_assets::AssetCategoryManager; use pallet_treasury::TreasuryAccountId; use polkadot_primitives::Balance; use sp_runtime::{traits::TryConvert, Perquintill}; use xcm::VersionedLocation; +use polkadot_primitives::AccountId; +use alloc::vec::Vec; /// Logic for the author to get a portion of fees. pub struct ToAuthor(core::marker::PhantomData); @@ -291,6 +294,22 @@ pub mod benchmarks { } } + +pub struct MockAssetCategoryManager; + +impl AssetCategoryManager for MockAssetCategoryManager { + type AssetKind = (); + type Balance = u64; + + fn assets_in_category(category: &[u8]) -> Vec { + Vec::new() + } + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { + Some(0) + } +} + #[cfg(test)] mod tests { use super::*; @@ -396,6 +415,7 @@ mod tests { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<0>; type BlockNumberProvider = System; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/polkadot/runtime/common/src/integration_tests.rs b/polkadot/runtime/common/src/integration_tests.rs index 3d871a2dfb2e..218672c6d718 100644 --- a/polkadot/runtime/common/src/integration_tests.rs +++ b/polkadot/runtime/common/src/integration_tests.rs @@ -16,6 +16,7 @@ //! Mocking utilities for testing with real pallets. +use alloc::vec::Vec; use crate::{ auctions, crowdloan, identity_migrator, mock::{conclude_pvf_checking, validators_public_keys}, diff --git a/substrate/frame/bounties/Cargo.toml b/substrate/frame/bounties/Cargo.toml index b777e9ce226e..8caf9793b7dc 100644 --- a/substrate/frame/bounties/Cargo.toml +++ b/substrate/frame/bounties/Cargo.toml @@ -39,6 +39,7 @@ std = [ "frame-support/std", "frame-system/std", "log/std", + "pallet-assets/std", "pallet-balances/std", "pallet-treasury/std", "scale-info/std", @@ -50,6 +51,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", diff --git a/substrate/frame/bounties/src/tests.rs b/substrate/frame/bounties/src/tests.rs index 812ae5b8e10a..982c2210171f 100644 --- a/substrate/frame/bounties/src/tests.rs +++ b/substrate/frame/bounties/src/tests.rs @@ -36,6 +36,7 @@ use frame_support::{ }, PalletId, }; +use pallet_assets::AssetCategoryManager; use frame_system::{pallet_prelude::*, EnsureSigned}; use sp_runtime::{ traits::{BadOrigin, IdentityLookup}, @@ -94,6 +95,21 @@ parameter_types! { pub TreasuryInstance1Account: u128 = Treasury1::account_id(); } +pub struct MockAssetCategoryManager; + +impl AssetCategoryManager for MockAssetCategoryManager { + type AssetKind = (); + type Balance = u64; + + fn assets_in_category(category: &[u8]) -> Vec { + Vec::new() + } + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { + Some(Zero::zero()) + } +} + impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet; @@ -113,6 +129,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } @@ -136,6 +153,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/child-bounties/Cargo.toml b/substrate/frame/child-bounties/Cargo.toml index d0bdc1856a5c..3e15debd298b 100644 --- a/substrate/frame/child-bounties/Cargo.toml +++ b/substrate/frame/child-bounties/Cargo.toml @@ -21,6 +21,7 @@ frame-benchmarking = { optional = true, workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } +pallet-assets = { workspace = true } pallet-bounties = { workspace = true } pallet-treasury = { workspace = true } scale-info = { features = ["derive"], workspace = true } @@ -39,6 +40,7 @@ std = [ "frame-support/std", "frame-system/std", "log/std", + "pallet-assets/std", "pallet-balances/std", "pallet-bounties/std", "pallet-treasury/std", @@ -52,6 +54,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-bounties/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", @@ -60,6 +63,7 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-assets/try-runtime", "pallet-balances/try-runtime", "pallet-bounties/try-runtime", "pallet-treasury/try-runtime", diff --git a/substrate/frame/child-bounties/src/tests.rs b/substrate/frame/child-bounties/src/tests.rs index e0bb1ac6f75a..ebd93fe522f2 100644 --- a/substrate/frame/child-bounties/src/tests.rs +++ b/substrate/frame/child-bounties/src/tests.rs @@ -31,12 +31,13 @@ use frame_support::{ weights::Weight, PalletId, }; +use pallet_assets::AssetCategoryManager; use sp_runtime::{ traits::{BadOrigin, IdentityLookup}, BuildStorage, Perbill, Permill, TokenError, }; - +use pallet_assets::AssetCategoryManager; use super::Event as ChildBountiesEvent; type Block = frame_system::mocking::MockBlock; @@ -92,6 +93,21 @@ parameter_types! { pub const SpendLimit: Balance = u64::MAX; } +pub struct MockAssetCategoryManager; + +impl AssetCategoryManager for MockAssetCategoryManager { + type AssetKind = (); + type Balance = u64; + + fn assets_in_category(category: &[u8]) -> Vec { + Vec::new() + } + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { + Some(Zero::zero()) + } +} + impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet; @@ -111,6 +127,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs index 476e3897b269..dc54bdf7633f 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -26,6 +26,7 @@ use frame_support::{ FromContains, LinearStoragePrice, }, }; +use pallet_assets::AssetCategoryManager; use frame_system::EnsureRootWithSuccess; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use polkadot_runtime_common::impls::{ @@ -128,6 +129,20 @@ parameter_types! { pub const MaxBalance: Balance = Balance::max_value(); } +pub struct MockAssetCategoryManager; +impl AssetCategoryManager for MockAssetCategoryManager { + type AssetKind = VersionedLocatableAsset; + type Balance = Balance; + + fn assets_in_category(category: &[u8]) -> Vec { + Vec::new() + } + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { + Some(0) + } +} + pub type TreasurySpender = EitherOf, Spender>; impl pallet_treasury::Config for Runtime { @@ -166,6 +181,7 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = RelayChainBlockNumberProvider; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/substrate/frame/staking-async/runtimes/rc/Cargo.toml b/substrate/frame/staking-async/runtimes/rc/Cargo.toml index 1b916fa36942..22edc2619ae2 100644 --- a/substrate/frame/staking-async/runtimes/rc/Cargo.toml +++ b/substrate/frame/staking-async/runtimes/rc/Cargo.toml @@ -53,6 +53,7 @@ frame-metadata-hash-extension = { workspace = true } frame-support = { features = ["experimental", "tuples-96"], workspace = true } frame-system = { workspace = true } frame-system-rpc-runtime-api = { workspace = true } +pallet-assets = { workspace = true } pallet-asset-rate = { workspace = true } pallet-authority-discovery = { workspace = true } pallet-authorship = { workspace = true } @@ -117,6 +118,7 @@ xcm-runtime-apis = { workspace = true } [dev-dependencies] approx = { workspace = true } +#pallet-assets = { workspace = true } remote-externalities = { workspace = true, default-features = true } serde_json = { workspace = true, default-features = true } sp-keyring = { workspace = true, default-features = true } @@ -144,6 +146,7 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", + "pallet-assets/std", "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", @@ -231,6 +234,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", @@ -288,6 +292,7 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", + "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index 1540c84529de..de0eecd0049b 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -78,6 +78,7 @@ use polkadot_runtime_common::{ traits::OnSwap, BlockHashCount, BlockLength, SlowAdjustingFeeUpdate, }; +use pallet_assets::AssetCategoryManager; use polkadot_runtime_parachains::{ assigner_coretime as parachains_assigner_coretime, configuration as parachains_configuration, configuration::ActiveConfigHrmpChannelSizeAndCapacityRatio, @@ -1017,6 +1018,22 @@ parameter_types! { pub const MaxBalance: Balance = Balance::max_value(); } +pub struct MockAssetCategoryManager; + +impl AssetCategoryManager for MockAssetCategoryManager { + type AssetKind = VersionedLocatableAsset; + type Balance = Balance; + + fn assets_in_category(category: &[u8]) -> Vec { + Vec::new() + } + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { + Some(0) + } +} + + impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; @@ -1053,6 +1070,7 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/substrate/frame/tips/Cargo.toml b/substrate/frame/tips/Cargo.toml index e191459934c5..047a9e46bfb4 100644 --- a/substrate/frame/tips/Cargo.toml +++ b/substrate/frame/tips/Cargo.toml @@ -21,6 +21,7 @@ frame-benchmarking = { optional = true, workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } +pallet-assets = { workspace = true } pallet-treasury = { workspace = true } scale-info = { features = ["derive"], workspace = true } serde = { features = ["derive"], optional = true, workspace = true, default-features = true } @@ -40,6 +41,7 @@ std = [ "frame-support/std", "frame-system/std", "log/std", + "pallet-assets/std", "pallet-treasury/std", "scale-info/std", "serde", @@ -51,6 +53,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", "sp-runtime/runtime-benchmarks", @@ -58,6 +61,7 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-assets/try-runtime", "pallet-balances/try-runtime", "pallet-treasury/try-runtime", "sp-runtime/try-runtime", diff --git a/substrate/frame/tips/src/lib.rs b/substrate/frame/tips/src/lib.rs index f0b0adab4f9a..b2b3c40f3a62 100644 --- a/substrate/frame/tips/src/lib.rs +++ b/substrate/frame/tips/src/lib.rs @@ -78,7 +78,6 @@ use frame_support::{ Parameter, }; use frame_system::pallet_prelude::BlockNumberFor; - #[cfg(any(feature = "try-runtime", test))] use sp_runtime::TryRuntimeError; diff --git a/substrate/frame/tips/src/tests.rs b/substrate/frame/tips/src/tests.rs index 052a425981ed..8a214910cebc 100644 --- a/substrate/frame/tips/src/tests.rs +++ b/substrate/frame/tips/src/tests.rs @@ -25,7 +25,7 @@ use sp_runtime::{ BuildStorage, Perbill, Permill, }; use sp_storage::Storage; - +use pallet_assets::AssetCategoryManager; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, storage::StoragePrefixedMap, @@ -101,6 +101,21 @@ parameter_types! { pub TreasuryInstance1Account: u128 = Treasury1::account_id(); } +pub struct MockAssetCategoryManager; + +impl AssetCategoryManager for MockAssetCategoryManager { + type AssetKind = (); + type Balance = u64; + + fn assets_in_category(category: &[u8]) -> Vec { + Vec::new() + } + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { + Some(Zero::zero()) + } +} + impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet; @@ -120,6 +135,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } @@ -143,6 +159,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } From 8f97fc31981bfe60d994c31a9c91451378505325 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Wed, 4 Feb 2026 11:15:25 +0100 Subject: [PATCH 11/21] asset-ops --- Cargo.lock | 4 - .../asset-hub-westend/src/governance/mod.rs | 30 +++----- .../collectives-westend/Cargo.toml | 4 - .../collectives-westend/src/fellowship/mod.rs | 24 ++---- polkadot/runtime/common/Cargo.toml | 4 - polkadot/runtime/common/src/impls.rs | 25 +------ .../runtime/common/src/integration_tests.rs | 3 +- polkadot/runtime/rococo/Cargo.toml | 4 - polkadot/runtime/rococo/src/lib.rs | 21 +----- polkadot/runtime/westend/Cargo.toml | 4 - polkadot/runtime/westend/src/lib.rs | 28 ++----- substrate/frame/assets/src/lib.rs | 74 ++++++++----------- substrate/frame/bounties/src/tests.rs | 26 ++----- substrate/frame/child-bounties/Cargo.toml | 2 +- substrate/frame/child-bounties/src/tests.rs | 26 ++----- .../src/traits/tokens/asset_ops/common_ops.rs | 34 ++++++++- substrate/frame/tips/src/tests.rs | 18 +---- substrate/frame/treasury/src/lib.rs | 3 +- 18 files changed, 116 insertions(+), 218 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0a031cf0a62..458fa673f14c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3442,7 +3442,6 @@ dependencies = [ "hex-literal", "pallet-alliance", "pallet-asset-rate", - "pallet-assets", "pallet-aura", "pallet-authorship", "pallet-balances", @@ -16214,7 +16213,6 @@ dependencies = [ "libsecp256k1", "log", "pallet-asset-rate", - "pallet-assets", "pallet-authorship", "pallet-babe", "pallet-balances", @@ -18940,7 +18938,6 @@ dependencies = [ "hex-literal", "log", "pallet-asset-rate", - "pallet-assets", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", @@ -27313,7 +27310,6 @@ dependencies = [ "hex-literal", "log", "pallet-asset-rate", - "pallet-assets", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 03061f1d3ebb..1ff33fbcf248 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -18,9 +18,13 @@ use super::*; use crate::xcm_config::Collectives; +use alloc::{vec, vec::Vec}; use frame_support::{ parameter_types, - traits::{tokens::UnityOrOuterConversion, EitherOf, EitherOfDiverse, FromContains}, + traits::{ + asset_ops::common_ops::ConfigurableAssetCategoryManager, tokens::UnityOrOuterConversion, + EitherOf, EitherOfDiverse, FromContains, + }, }; use frame_system::EnsureRootWithSuccess; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; @@ -29,10 +33,8 @@ use polkadot_runtime_common::{ impls::{ContainsParts, VersionedLocatableAsset}, prod_or_fast, }; -use alloc::{vec, vec::Vec}; use sp_runtime::{traits::IdentityLookup, Percent}; use xcm::latest::BodyId; -use pallet_assets::AssetCategoryManager; mod origins; pub use origins::{ @@ -134,21 +136,11 @@ pub type TreasuryBalanceConverter = UnityOrOuterConversion< AssetRate, >; -pub struct AHWAssetCategories; - -impl AssetCategoryManager for AHWAssetCategories { - type AssetKind = VersionedLocatableAsset; - type Balance = Balance; - - fn assets_in_category(_category: &[u8]) -> Vec { - vec![] - } - - fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { - None - } -} - +pub type AHWAssetCategories = ConfigurableAssetCategoryManager< + ::AccountId, + VersionedLocatableAsset, + Balance, +>; impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; @@ -168,7 +160,7 @@ impl pallet_treasury::Config for Runtime { type Paymaster = LocalPay; type BalanceConverter = TreasuryBalanceConverter; type PayoutPeriod = PayoutSpendPeriod; - type AssetCategories = AHWAssetCategories; + type AssetCategories = AHWAssetCategories; type BlockNumberProvider = RelaychainDataProvider; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = parachains_common::pay::benchmarks::LocalPayArguments< diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml index 418fb48e2537..7c466c22693e 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml @@ -27,7 +27,6 @@ frame-system = { workspace = true } frame-system-benchmarking = { optional = true, workspace = true } frame-system-rpc-runtime-api = { workspace = true } frame-try-runtime = { optional = true, workspace = true } -pallet-assets = { workspace = true } pallet-alliance = { workspace = true } pallet-asset-rate = { workspace = true } pallet-aura = { workspace = true } @@ -115,7 +114,6 @@ runtime-benchmarks = [ "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-alliance/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", @@ -159,7 +157,6 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime/try-runtime", "pallet-alliance/try-runtime", - "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-aura/try-runtime", "pallet-authorship/try-runtime", @@ -208,7 +205,6 @@ std = [ "frame-system/std", "frame-try-runtime?/std", "pallet-alliance/std", - "pallet-assets/std", "pallet-asset-rate/std", "pallet-aura/std", "pallet-authorship/std", diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs index 1f008b436f01..b585cd11b3cb 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/mod.rs @@ -29,13 +29,11 @@ use cumulus_primitives_core::ParaId; use frame_support::{ parameter_types, traits::{ - tokens::UnityOrOuterConversion, EitherOf, EitherOfDiverse, FromContains, MapSuccess, - OriginTrait, TryWithMorphedArg, + tokens::{asset_ops::common_ops::ConfigurableAssetCategoryManager, UnityOrOuterConversion}, + EitherOf, EitherOfDiverse, FromContains, MapSuccess, OriginTrait, TryWithMorphedArg, }, PalletId, }; -use alloc::{vec, vec::Vec}; -use pallet_assets::AssetCategoryManager; use frame_system::{EnsureRoot, EnsureRootWithSuccess}; pub use origins::{ pallet_origins as pallet_fellowship_origins, Architects, EnsureCanPromoteTo, EnsureCanRetainAt, @@ -287,20 +285,8 @@ pub type FellowshipTreasuryPaymaster = PayOverXcm< pub type FellowshipTreasuryInstance = pallet_treasury::Instance1; -pub struct CWAssetCategories; - -impl AssetCategoryManager for CWAssetCategories { - type AssetKind = VersionedLocatableAsset; - type Balance = Balance; - - fn assets_in_category(_category: &[u8]) -> Vec { - vec![] - } - - fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { - None - } -} +pub type CWAssetCategories = + ConfigurableAssetCategoryManager; impl pallet_treasury::Config for Runtime { type WeightInfo = weights::pallet_treasury::WeightInfo; @@ -346,7 +332,7 @@ impl pallet_treasury::Config for Runtime { AssetRate, >; type PayoutPeriod = ConstU32<{ 30 * DAYS }>; - type AssetCategories = CWAssetCategories; + type AssetCategories = CWAssetCategories; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments< sp_core::ConstU8<1>, diff --git a/polkadot/runtime/common/Cargo.toml b/polkadot/runtime/common/Cargo.toml index 902c752ba01c..31a726914801 100644 --- a/polkadot/runtime/common/Cargo.toml +++ b/polkadot/runtime/common/Cargo.toml @@ -34,7 +34,6 @@ sp-staking = { features = ["serde"], workspace = true } frame-election-provider-support = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } -pallet-assets = { workspace = true } pallet-asset-rate = { optional = true, workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } @@ -85,7 +84,6 @@ std = [ "frame-system/std", "libsecp256k1/std", "log/std", - "pallet-assets/std", "pallet-asset-rate?/std", "pallet-authorship/std", "pallet-balances/std", @@ -126,7 +124,6 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "libsecp256k1/hmac", "libsecp256k1/static-context", - "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-balances/runtime-benchmarks", @@ -153,7 +150,6 @@ try-runtime = [ "frame-support-test/try-runtime", "frame-support/try-runtime", "frame-system/try-runtime", - "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authorship/try-runtime", "pallet-babe?/try-runtime", diff --git a/polkadot/runtime/common/src/impls.rs b/polkadot/runtime/common/src/impls.rs index c88cc6db0046..ed7d42e7bff2 100644 --- a/polkadot/runtime/common/src/impls.rs +++ b/polkadot/runtime/common/src/impls.rs @@ -19,16 +19,13 @@ use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::traits::{ fungible::{Balanced, Credit}, - tokens::imbalance::ResolveTo, + tokens::{asset_ops::common_ops::ConfigurableAssetCategoryManager, imbalance::ResolveTo}, Contains, ContainsPair, Imbalance, OnUnbalanced, }; -use pallet_assets::AssetCategoryManager; use pallet_treasury::TreasuryAccountId; -use polkadot_primitives::Balance; +use polkadot_primitives::{AccountId, Balance}; use sp_runtime::{traits::TryConvert, Perquintill}; use xcm::VersionedLocation; -use polkadot_primitives::AccountId; -use alloc::vec::Vec; /// Logic for the author to get a portion of fees. pub struct ToAuthor(core::marker::PhantomData); @@ -294,21 +291,7 @@ pub mod benchmarks { } } - -pub struct MockAssetCategoryManager; - -impl AssetCategoryManager for MockAssetCategoryManager { - type AssetKind = (); - type Balance = u64; - - fn assets_in_category(category: &[u8]) -> Vec { - Vec::new() - } - - fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { - Some(0) - } -} +pub type MockAssetCategoryManager = ConfigurableAssetCategoryManager; #[cfg(test)] mod tests { @@ -415,7 +398,7 @@ mod tests { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<0>; type BlockNumberProvider = System; - type AssetCategories = MockAssetCategoryManager; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/polkadot/runtime/common/src/integration_tests.rs b/polkadot/runtime/common/src/integration_tests.rs index 218672c6d718..750f36942739 100644 --- a/polkadot/runtime/common/src/integration_tests.rs +++ b/polkadot/runtime/common/src/integration_tests.rs @@ -16,7 +16,6 @@ //! Mocking utilities for testing with real pallets. -use alloc::vec::Vec; use crate::{ auctions, crowdloan, identity_migrator, mock::{conclude_pvf_checking, validators_public_keys}, @@ -25,7 +24,7 @@ use crate::{ slots, traits::{AuctionStatus, Auctioneer, Leaser, Registrar as RegistrarT}, }; -use alloc::sync::Arc; +use alloc::{sync::Arc, vec::Vec}; use codec::Encode; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, diff --git a/polkadot/runtime/rococo/Cargo.toml b/polkadot/runtime/rococo/Cargo.toml index e937a544399f..be65accb31ce 100644 --- a/polkadot/runtime/rococo/Cargo.toml +++ b/polkadot/runtime/rococo/Cargo.toml @@ -48,7 +48,6 @@ frame-executive = { workspace = true } frame-support = { features = ["tuples-96"], workspace = true } frame-system = { workspace = true } frame-system-rpc-runtime-api = { workspace = true } -pallet-assets = { workspace = true } pallet-asset-rate = { workspace = true } pallet-authority-discovery = { workspace = true } pallet-authorship = { workspace = true } @@ -136,7 +135,6 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", - "pallet-assets/std", "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", @@ -221,7 +219,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-balances/runtime-benchmarks", @@ -278,7 +275,6 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", - "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 985eaa524bb7..1023047fd71a 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -38,12 +38,11 @@ use alloc::{ vec, vec::Vec, }; -use pallet_assets::AssetCategoryManager; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use core::cmp::Ordering; use frame_support::{ dynamic_params::{dynamic_pallet_params, dynamic_params}, - traits::FromContains, + traits::{tokens::asset_ops::common_ops::ConfigurableAssetCategoryManager, FromContains}, }; use pallet_balances::WeightInfo; use pallet_nis::WithMaximumOf; @@ -331,20 +330,8 @@ impl EnsureOriginWithArg for DynamicParamet } } -pub struct RococoAssetCategories; - -impl AssetCategoryManager for RococoAssetCategories { - type AssetKind = VersionedLocatableAsset; - type Balance = Balance; - - fn assets_in_category(_category: &[u8]) -> Vec { - vec![] - } - - fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { - None - } -} +pub type RococoAssetCategories = + ConfigurableAssetCategoryManager; impl pallet_scheduler::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; @@ -577,7 +564,7 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; - type AssetCategories = RococoAssetCategories; + type AssetCategories = RococoAssetCategories; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml index 22b4d3632a7c..04120d43afc5 100644 --- a/polkadot/runtime/westend/Cargo.toml +++ b/polkadot/runtime/westend/Cargo.toml @@ -51,7 +51,6 @@ frame-metadata-hash-extension = { workspace = true } frame-support = { features = ["experimental", "tuples-96"], workspace = true } frame-system = { workspace = true } frame-system-rpc-runtime-api = { workspace = true } -pallet-assets = { workspace = true } pallet-asset-rate = { workspace = true } pallet-authority-discovery = { workspace = true } pallet-authorship = { workspace = true } @@ -148,7 +147,6 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", - "pallet-assets/std", "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", @@ -242,7 +240,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", @@ -306,7 +303,6 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", - "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 91157f66257f..56cd10c4a8e6 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -35,15 +35,15 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::HoldConsideration, tokens::UnityOrOuterConversion, AsEnsureOriginWithArg, - ConstU32, Contains, EitherOf, EitherOfDiverse, EnsureOriginWithArg, FromContains, - InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, Nothing, ProcessMessage, - ProcessMessageError, VariantCountOf, WithdrawReasons, + fungible::HoldConsideration, + tokens::{asset_ops::common_ops::ConfigurableAssetCategoryManager, UnityOrOuterConversion}, + AsEnsureOriginWithArg, ConstU32, Contains, EitherOf, EitherOfDiverse, EnsureOriginWithArg, + FromContains, InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, Nothing, + ProcessMessage, ProcessMessageError, VariantCountOf, WithdrawReasons, }, weights::{ConstantMultiplier, WeightMeter}, PalletId, }; -use pallet_assets::AssetCategoryManager; use frame_system::{EnsureRoot, EnsureSigned}; use pallet_grandpa::{fg_primitives, AuthorityId as GrandpaId}; use pallet_identity::legacy::IdentityInfo; @@ -894,20 +894,8 @@ impl ah_client::SendToAssetHub for StakingXcmToAssetHub { } } -pub struct WestendAssetCategories; - -impl AssetCategoryManager for WestendAssetCategories { - type AssetKind = VersionedLocatableAsset; - type Balance = Balance; - - fn assets_in_category(_category: &[u8]) -> Vec { - vec![] - } - - fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { - None - } -} +pub type WestendAssetCategories = + ConfigurableAssetCategoryManager; impl ah_client::Config for Runtime { type CurrencyBalance = Balance; @@ -991,7 +979,7 @@ impl pallet_treasury::Config for Runtime { >; type PayoutPeriod = PayoutSpendPeriod; type BlockNumberProvider = System; - type AssetCategories = WestendAssetCategories; + type AssetCategories = WestendAssetCategories; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::TreasuryArguments; } diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index ae55a2dc701b..c0656f83e368 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -186,6 +186,7 @@ use frame_support::{ storage::KeyPrefixIterator, traits::{ tokens::{ + asset_ops::common_ops::AssetCategoryManager, fungibles, DepositConsequence, Fortitude, Preservation::{Expendable, Preserve}, WithdrawConsequence, @@ -215,18 +216,6 @@ pub trait AssetsCallback { } } -/// Trait for managing asset categories -pub trait AssetCategoryManager { - type AssetKind; - type Balance: Zero + PartialOrd + Copy + Saturating + sp_runtime::traits::AtLeast32BitUnsigned; - - /// Get all assets in a category - fn assets_in_category(category: &[u8]) -> Vec; - - /// Get available balance of a specific asset in treasury - fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option; -} - #[impl_trait_for_tuples::impl_for_tuples(10)] impl AssetsCallback for Tuple { fn created(id: &AssetId, owner: &AccountId) -> Result<(), ()> { @@ -499,14 +488,14 @@ pub mod pallet { ValueQuery, >; - #[pallet::storage] - pub type AssetCategories, I: 'static = ()> = StorageMap< - _, - Blake2_128Concat, - BoundedVec>, // Category name - BoundedVec>, // Assets in this category - ValueQuery, - >; + #[pallet::storage] + pub type AssetCategories, I: 'static = ()> = StorageMap< + _, + Blake2_128Concat, + BoundedVec>, // Category name + BoundedVec>, // Assets in this category + ValueQuery, + >; /// The asset ID enforced for the next asset creation, if any present. Otherwise, this storage /// item has no effect. @@ -1987,30 +1976,27 @@ pub mod pallet { } } - impl, I: 'static> AssetCategoryManager for Pallet { - type AssetKind = T::AssetId; - type Balance = T::Balance; - - /// Get all assets in a category - // TODO: Refine or investigate - fn assets_in_category(category: &[u8]) -> Vec { - let bounded_category: BoundedVec> = category - .to_vec() - .try_into() - .unwrap_or_default(); - - AssetCategories::::get(&bounded_category) - .into_inner() - .into_iter() - .collect() - } - - - /// Get available balance of a specific asset in treasury - fn available_balance(asset: Self::AssetKind, owner: T::AccountId) -> Option { - Self::balance_of(owner, asset) - } - } + impl, I: 'static> AssetCategoryManager for Pallet { + type AssetKind = T::AssetId; + type Balance = T::Balance; + + /// Get all assets in a category + // TODO: Refine or investigate + fn assets_in_category(category: &[u8]) -> Vec { + let bounded_category: BoundedVec> = + category.to_vec().try_into().unwrap_or_default(); + + AssetCategories::::get(&bounded_category) + .into_inner() + .into_iter() + .collect() + } + + /// Get available balance of a specific asset in treasury + fn available_balance(asset: Self::AssetKind, owner: T::AccountId) -> Option { + Self::balance_of(owner, asset) + } + } } #[cfg(any(feature = "try-runtime", test))] diff --git a/substrate/frame/bounties/src/tests.rs b/substrate/frame/bounties/src/tests.rs index 982c2210171f..f9782d737748 100644 --- a/substrate/frame/bounties/src/tests.rs +++ b/substrate/frame/bounties/src/tests.rs @@ -31,12 +31,14 @@ use frame_support::{ traits::{ fungible, fungible::{NativeFromLeft, NativeOrWithId}, - tokens::{PayFromAccount, UnityAssetBalanceConversion}, + tokens::{ + asset_ops::common_ops::ConfigurableAssetCategoryManager, PayFromAccount, + UnityAssetBalanceConversion, + }, AsEnsureOriginWithArg, ConstU32, ConstU64, Currency, Imbalance, OnInitialize, }, PalletId, }; -use pallet_assets::AssetCategoryManager; use frame_system::{pallet_prelude::*, EnsureSigned}; use sp_runtime::{ traits::{BadOrigin, IdentityLookup}, @@ -95,20 +97,8 @@ parameter_types! { pub TreasuryInstance1Account: u128 = Treasury1::account_id(); } -pub struct MockAssetCategoryManager; - -impl AssetCategoryManager for MockAssetCategoryManager { - type AssetKind = (); - type Balance = u64; - - fn assets_in_category(category: &[u8]) -> Vec { - Vec::new() - } - - fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { - Some(Zero::zero()) - } -} +pub type MockAssetCategoryManager = + ConfigurableAssetCategoryManager<::AccountId, (), u64>; impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; @@ -129,7 +119,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; - type AssetCategories = MockAssetCategoryManager; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } @@ -153,7 +143,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; - type AssetCategories = MockAssetCategoryManager; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/child-bounties/Cargo.toml b/substrate/frame/child-bounties/Cargo.toml index 3e15debd298b..2dc2f85e27ef 100644 --- a/substrate/frame/child-bounties/Cargo.toml +++ b/substrate/frame/child-bounties/Cargo.toml @@ -21,7 +21,6 @@ frame-benchmarking = { optional = true, workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } -pallet-assets = { workspace = true } pallet-bounties = { workspace = true } pallet-treasury = { workspace = true } scale-info = { features = ["derive"], workspace = true } @@ -31,6 +30,7 @@ sp-runtime = { workspace = true } [dev-dependencies] pallet-balances = { workspace = true, default-features = true } +pallet-assets = { workspace = true } [features] default = ["std"] diff --git a/substrate/frame/child-bounties/src/tests.rs b/substrate/frame/child-bounties/src/tests.rs index ebd93fe522f2..5958dbb24eab 100644 --- a/substrate/frame/child-bounties/src/tests.rs +++ b/substrate/frame/child-bounties/src/tests.rs @@ -25,19 +25,21 @@ use crate as pallet_child_bounties; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, traits::{ - tokens::{PayFromAccount, UnityAssetBalanceConversion}, + tokens::{ + asset_ops::common_ops::ConfigurableAssetCategoryManager, PayFromAccount, + UnityAssetBalanceConversion, + }, ConstU32, ConstU64, OnInitialize, }, weights::Weight, PalletId, }; -use pallet_assets::AssetCategoryManager; use sp_runtime::{ traits::{BadOrigin, IdentityLookup}, BuildStorage, Perbill, Permill, TokenError, }; -use pallet_assets::AssetCategoryManager; +// use pallet_assets::AssetCategoryManager; use super::Event as ChildBountiesEvent; type Block = frame_system::mocking::MockBlock; @@ -93,20 +95,8 @@ parameter_types! { pub const SpendLimit: Balance = u64::MAX; } -pub struct MockAssetCategoryManager; - -impl AssetCategoryManager for MockAssetCategoryManager { - type AssetKind = (); - type Balance = u64; - - fn assets_in_category(category: &[u8]) -> Vec { - Vec::new() - } - - fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { - Some(Zero::zero()) - } -} +pub type MockAssetCategoryManager = + ConfigurableAssetCategoryManager<::AccountId, (), u64>; impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; @@ -127,7 +117,7 @@ impl pallet_treasury::Config for Test { type BalanceConverter = UnityAssetBalanceConversion; type PayoutPeriod = ConstU64<10>; type BlockNumberProvider = System; - type AssetCategories = MockAssetCategoryManager; + type AssetCategories = MockAssetCategoryManager; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/substrate/frame/support/src/traits/tokens/asset_ops/common_ops.rs b/substrate/frame/support/src/traits/tokens/asset_ops/common_ops.rs index ec01c63ac024..d2bd1fddbc26 100644 --- a/substrate/frame/support/src/traits/tokens/asset_ops/common_ops.rs +++ b/substrate/frame/support/src/traits/tokens/asset_ops/common_ops.rs @@ -18,7 +18,7 @@ use super::{common_strategies::*, *}; use crate::{ dispatch::DispatchResult, - sp_runtime::traits::Convert, + sp_runtime::traits::{Convert, Zero, Saturating}, traits::{misc::TypedGet, EnsureOriginWithArg}, }; @@ -335,3 +335,35 @@ impl Destroy for DisabledOps { Err(DispatchError::Other("Disabled")) } } + +/// Category Ops +pub trait AssetCategoryManager { + type AssetKind; + type Balance: Zero + PartialOrd + Copy + Saturating + sp_runtime::traits::AtLeast32BitUnsigned; + + fn assets_in_category(category: &[u8]) -> Vec; + + fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option; +} + +pub struct ConfigurableAssetCategoryManager { + _phantom: core::marker::PhantomData<(AccountId, AssetKind, Balance)>, +} + +impl AssetCategoryManager + for ConfigurableAssetCategoryManager +where + AssetKind: Clone, + Balance: PartialOrd + Copy + Saturating + sp_runtime::traits::AtLeast32BitUnsigned, +{ + type AssetKind = AssetKind; + type Balance = Balance; + + fn assets_in_category(_category: &[u8]) -> Vec { + Vec::new() + } + + fn available_balance(_asset: Self::AssetKind, _owner: AccountId) -> Option { + None + } +} diff --git a/substrate/frame/tips/src/tests.rs b/substrate/frame/tips/src/tests.rs index 8a214910cebc..a5e06ae93a9f 100644 --- a/substrate/frame/tips/src/tests.rs +++ b/substrate/frame/tips/src/tests.rs @@ -25,12 +25,11 @@ use sp_runtime::{ BuildStorage, Perbill, Permill, }; use sp_storage::Storage; -use pallet_assets::AssetCategoryManager; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, storage::StoragePrefixedMap, traits::{ - tokens::{PayFromAccount, UnityAssetBalanceConversion}, + tokens::{PayFromAccount, UnityAssetBalanceConversion, asset_ops::common_ops::ConfigurableAssetCategoryManager}, ConstU32, ConstU64, IntegrityTest, SortedMembers, StorageVersion, }, PalletId, @@ -101,20 +100,7 @@ parameter_types! { pub TreasuryInstance1Account: u128 = Treasury1::account_id(); } -pub struct MockAssetCategoryManager; - -impl AssetCategoryManager for MockAssetCategoryManager { - type AssetKind = (); - type Balance = u64; - - fn assets_in_category(category: &[u8]) -> Vec { - Vec::new() - } - - fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { - Some(Zero::zero()) - } -} +pub type MockAssetCategoryManager = ConfigurableAssetCategoryManager<::AccountId, (), u64>; impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index ceb4bb108ad9..bdd54ea73938 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -103,14 +103,13 @@ use frame_support::{ pallet_prelude::DispatchError, print, traits::{ - tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, + tokens::{Pay, asset_ops::common_ops::AssetCategoryManager}, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, ReservableCurrency, WithdrawReasons, }, weights::Weight, BoundedVec, PalletId, }; use frame_system::pallet_prelude::BlockNumberFor as SystemBlockNumberFor; -use pallet_assets::AssetCategoryManager; pub use pallet::*; pub use weights::WeightInfo; From 35910803380622416cb1895834dc8046f3af81dc Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Wed, 4 Feb 2026 22:05:25 +0100 Subject: [PATCH 12/21] staking-async reset --- Cargo.lock | 1 - .../runtimes/parachain/src/governance/mod.rs | 17 ++--------------- .../frame/staking-async/runtimes/rc/Cargo.toml | 4 ---- .../frame/staking-async/runtimes/rc/src/lib.rs | 17 ++--------------- 4 files changed, 4 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 458fa673f14c..cacc01116e1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13762,7 +13762,6 @@ dependencies = [ "hex-literal", "log", "pallet-asset-rate", - "pallet-assets", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", diff --git a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs index dc54bdf7633f..d1926640967d 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -22,11 +22,10 @@ use crate::xcm_config::Collectives; use frame_support::{ parameter_types, traits::{ - fungible::HoldConsideration, tokens::UnityOrOuterConversion, EitherOf, EitherOfDiverse, + fungible::HoldConsideration, tokens::{UnityOrOuterConversion, asset_ops::common_ops::ConfigurableAssetCategoryManager}, EitherOf, EitherOfDiverse, FromContains, LinearStoragePrice, }, }; -use pallet_assets::AssetCategoryManager; use frame_system::EnsureRootWithSuccess; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use polkadot_runtime_common::impls::{ @@ -129,19 +128,7 @@ parameter_types! { pub const MaxBalance: Balance = Balance::max_value(); } -pub struct MockAssetCategoryManager; -impl AssetCategoryManager for MockAssetCategoryManager { - type AssetKind = VersionedLocatableAsset; - type Balance = Balance; - - fn assets_in_category(category: &[u8]) -> Vec { - Vec::new() - } - - fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { - Some(0) - } -} +pub type MockAssetCategoryManager = ConfigurableAssetCategoryManager; pub type TreasurySpender = EitherOf, Spender>; diff --git a/substrate/frame/staking-async/runtimes/rc/Cargo.toml b/substrate/frame/staking-async/runtimes/rc/Cargo.toml index 22edc2619ae2..52ae62636fdc 100644 --- a/substrate/frame/staking-async/runtimes/rc/Cargo.toml +++ b/substrate/frame/staking-async/runtimes/rc/Cargo.toml @@ -53,7 +53,6 @@ frame-metadata-hash-extension = { workspace = true } frame-support = { features = ["experimental", "tuples-96"], workspace = true } frame-system = { workspace = true } frame-system-rpc-runtime-api = { workspace = true } -pallet-assets = { workspace = true } pallet-asset-rate = { workspace = true } pallet-authority-discovery = { workspace = true } pallet-authorship = { workspace = true } @@ -146,7 +145,6 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", - "pallet-assets/std", "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", @@ -234,7 +232,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", @@ -292,7 +289,6 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", - "pallet-assets/try-runtime", "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index de0eecd0049b..d7a1e4bb4e2a 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -37,7 +37,7 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::HoldConsideration, tokens::UnityOrOuterConversion, ConstBool, ConstU32, Contains, + fungible::HoldConsideration, tokens::{UnityOrOuterConversion, asset_ops::common_ops::ConfigurableAssetCategoryManager}, ConstBool, ConstU32, Contains, EitherOf, EitherOfDiverse, EnsureOriginWithArg, EverythingBut, FromContains, InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, Nothing, ProcessMessage, ProcessMessageError, VariantCountOf, WithdrawReasons, @@ -78,7 +78,6 @@ use polkadot_runtime_common::{ traits::OnSwap, BlockHashCount, BlockLength, SlowAdjustingFeeUpdate, }; -use pallet_assets::AssetCategoryManager; use polkadot_runtime_parachains::{ assigner_coretime as parachains_assigner_coretime, configuration as parachains_configuration, configuration::ActiveConfigHrmpChannelSizeAndCapacityRatio, @@ -1018,20 +1017,8 @@ parameter_types! { pub const MaxBalance: Balance = Balance::max_value(); } -pub struct MockAssetCategoryManager; -impl AssetCategoryManager for MockAssetCategoryManager { - type AssetKind = VersionedLocatableAsset; - type Balance = Balance; - - fn assets_in_category(category: &[u8]) -> Vec { - Vec::new() - } - - fn available_balance(asset: Self::AssetKind, owner: AccountId) -> Option { - Some(0) - } -} +pub type MockAssetCategoryManager = ConfigurableAssetCategoryManager; impl pallet_treasury::Config for Runtime { From 15b5096691a551046d7006427e43a05ae87c9468 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Wed, 4 Feb 2026 22:12:37 +0100 Subject: [PATCH 13/21] nit --- substrate/frame/staking-async/runtimes/rc/Cargo.toml | 1 - substrate/frame/staking-async/runtimes/rc/src/lib.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/substrate/frame/staking-async/runtimes/rc/Cargo.toml b/substrate/frame/staking-async/runtimes/rc/Cargo.toml index 52ae62636fdc..1b916fa36942 100644 --- a/substrate/frame/staking-async/runtimes/rc/Cargo.toml +++ b/substrate/frame/staking-async/runtimes/rc/Cargo.toml @@ -117,7 +117,6 @@ xcm-runtime-apis = { workspace = true } [dev-dependencies] approx = { workspace = true } -#pallet-assets = { workspace = true } remote-externalities = { workspace = true, default-features = true } serde_json = { workspace = true, default-features = true } sp-keyring = { workspace = true, default-features = true } diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index d7a1e4bb4e2a..d3fd05d0bc88 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -1018,7 +1018,7 @@ parameter_types! { } -pub type MockAssetCategoryManager = ConfigurableAssetCategoryManager; +pub type MockAssetCategoryManager = ConfigurableAssetCategoryManager; impl pallet_treasury::Config for Runtime { From c6b5619fdf07841a14117bad4f30820eb225709e Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Thu, 5 Feb 2026 00:30:36 +0100 Subject: [PATCH 14/21] nit --- substrate/bin/node/runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 9fa7b018bc71..0b15e4519272 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -31,7 +31,6 @@ use pallet_multi_asset_bounties::ArgumentsFactory as PalletMultiAssetBountiesArg use pallet_treasury::ArgumentsFactory as PalletTreasuryArgumentsFactory; #[cfg(feature = "runtime-benchmarks")] use polkadot_sdk::sp_core::crypto::FromEntropy; -use crate::pallet_assets::AssetCategoryManager; use polkadot_sdk::*; use alloc::{vec, vec::Vec}; @@ -54,6 +53,7 @@ use frame_support::{ Balanced, Credit, HoldConsideration, ItemOf, NativeFromLeft, NativeOrWithId, UnionOf, }, tokens::{ + asset_ops::common_ops::AssetCategoryManager, imbalance::{ResolveAssetTo, ResolveTo}, nonfungibles_v2::Inspect, pay::PayAssetFromAccount, From 2f49fe3a50432da69522ba3af6a2b870f608e5a4 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Thu, 5 Feb 2026 01:18:39 +0100 Subject: [PATCH 15/21] nit --- .../runtimes/assets/asset-hub-westend/src/governance/mod.rs | 5 ++--- .../parachains/runtimes/assets/asset-hub-westend/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 1ff33fbcf248..635930bb8ca3 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -18,11 +18,10 @@ use super::*; use crate::xcm_config::Collectives; -use alloc::{vec, vec::Vec}; use frame_support::{ parameter_types, traits::{ - asset_ops::common_ops::ConfigurableAssetCategoryManager, tokens::UnityOrOuterConversion, + tokens::{UnityOrOuterConversion, asset_ops::common_ops::ConfigurableAssetCategoryManager}, EitherOf, EitherOfDiverse, FromContains, }, }; @@ -137,7 +136,7 @@ pub type TreasuryBalanceConverter = UnityOrOuterConversion< >; pub type AHWAssetCategories = ConfigurableAssetCategoryManager< - ::AccountId, + AccountId, VersionedLocatableAsset, Balance, >; diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index b4650075bb91..d23875ec5496 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -40,12 +40,12 @@ use governance::{pallet_custom_origins, FellowshipAdmin, GeneralAdmin, StakingAd extern crate alloc; -use alloc::{vec, vec::Vec}; pub use assets_common::local_and_foreign_assets::ForeignAssetReserveData; use assets_common::{ local_and_foreign_assets::{LocalFromLeft, TargetFromLeft}, AssetIdForPoolAssets, AssetIdForPoolAssetsConvert, AssetIdForTrustBackedAssetsConvert, }; +use alloc::{vec, vec::Vec}; use bp_asset_hub_westend::CreateForeignAssetDeposit; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use cumulus_pallet_parachain_system::{RelayNumberMonotonicallyIncreases, RelaychainDataProvider}; From bfcb395cb5bac8d042ead0f099e56d9777bc8603 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Thu, 5 Feb 2026 07:44:16 +0100 Subject: [PATCH 16/21] nit --- Cargo.lock | 1 - substrate/frame/tips/Cargo.toml | 4 -- substrate/frame/treasury/src/lib.rs | 10 +-- substrate/frame/treasury/src/tests.rs | 88 +++++++++++++-------------- 4 files changed, 49 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cacc01116e1c..66503639222e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14003,7 +14003,6 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-assets", "pallet-balances", "pallet-treasury", "parity-scale-codec", diff --git a/substrate/frame/tips/Cargo.toml b/substrate/frame/tips/Cargo.toml index 047a9e46bfb4..e191459934c5 100644 --- a/substrate/frame/tips/Cargo.toml +++ b/substrate/frame/tips/Cargo.toml @@ -21,7 +21,6 @@ frame-benchmarking = { optional = true, workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } -pallet-assets = { workspace = true } pallet-treasury = { workspace = true } scale-info = { features = ["derive"], workspace = true } serde = { features = ["derive"], optional = true, workspace = true, default-features = true } @@ -41,7 +40,6 @@ std = [ "frame-support/std", "frame-system/std", "log/std", - "pallet-assets/std", "pallet-treasury/std", "scale-info/std", "serde", @@ -53,7 +51,6 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", "sp-runtime/runtime-benchmarks", @@ -61,7 +58,6 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", - "pallet-assets/try-runtime", "pallet-balances/try-runtime", "pallet-treasury/try-runtime", "sp-runtime/try-runtime", diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index bdd54ea73938..7c5fe16d2ca9 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -723,7 +723,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::spend())] pub fn spend( origin: OriginFor, - asset: SpendAsset, + asset: Box>, #[pallet::compact] amount: AssetBalanceOf, beneficiary: Box>, valid_from: Option>, @@ -736,7 +736,7 @@ pub mod pallet { let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); ensure!(expire_at > now, Error::::SpendExpired); - match asset { + match *asset { SpendAsset::Category(ref category) => { let assets = Self::validate_category_spend(category, amount)?; @@ -794,7 +794,7 @@ pub mod pallet { }, } - let native_amount = match asset { + let native_amount = match *asset { SpendAsset::Specific(ref asset_kind) => T::BalanceConverter::from_asset_balance(amount, asset_kind.clone()) .map_err(|_| Error::::BalanceConversionFailed)?, @@ -837,7 +837,7 @@ pub mod pallet { Spends::::insert( index, SpendStatus { - asset: asset.clone(), + asset: *asset.clone(), amount, beneficiary: beneficiary.clone(), valid_from, @@ -849,7 +849,7 @@ pub mod pallet { Self::deposit_event(Event::AssetSpendApproved { index, - asset: asset, + asset: *asset, amount, beneficiary, valid_from, diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index e99e89cbbe97..4d14bce631b9 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -652,13 +652,13 @@ fn spending_in_batch_respects_max_total() { assert_ok!(RuntimeCall::from(UtilityCall::batch_all { calls: vec![ RuntimeCall::from(TreasuryCall::spend { - asset: SpendAsset::Specific(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 1, beneficiary: Box::new(100), valid_from: None, }), RuntimeCall::from(TreasuryCall::spend { - asset: SpendAsset::Specific(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 1, beneficiary: Box::new(101), valid_from: None, @@ -671,13 +671,13 @@ fn spending_in_batch_respects_max_total() { RuntimeCall::from(UtilityCall::batch_all { calls: vec![ RuntimeCall::from(TreasuryCall::spend { - asset: SpendAsset::Specific(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 2, beneficiary: Box::new(100), valid_from: None, }), RuntimeCall::from(TreasuryCall::spend { - asset: SpendAsset::Specific(1), + asset: Box::new(SpendAsset::Specific(1)), amount: 2, beneficiary: Box::new(101), valid_from: None, @@ -695,14 +695,14 @@ fn spend_origin_works() { ExtBuilder::default().build().execute_with(|| { assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 1, Box::new(6), None )); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -710,7 +710,7 @@ fn spend_origin_works() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 3, Box::new(6), None @@ -719,7 +719,7 @@ fn spend_origin_works() { ); assert_ok!(Treasury::spend( RuntimeOrigin::signed(11), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 5, Box::new(6), None @@ -727,7 +727,7 @@ fn spend_origin_works() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(11), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 6, Box::new(6), None @@ -736,7 +736,7 @@ fn spend_origin_works() { ); assert_ok!(Treasury::spend( RuntimeOrigin::signed(12), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 10, Box::new(6), None @@ -744,7 +744,7 @@ fn spend_origin_works() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(12), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 11, Box::new(6), None @@ -763,7 +763,7 @@ fn spend_works() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -804,7 +804,7 @@ fn spend_expires() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -816,7 +816,7 @@ fn spend_expires() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(0) @@ -834,7 +834,7 @@ fn spend_payout_works() { // approve a `2` coins spend of asset `1` to beneficiary `6`, the spend valid from now. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -869,7 +869,7 @@ fn payout_extends_expiry() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -902,7 +902,7 @@ fn payout_retry_works() { System::set_block_number(1); assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -936,7 +936,7 @@ fn spend_valid_from_works() { // spend valid from block `2`. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(2) @@ -949,7 +949,7 @@ fn spend_valid_from_works() { // spend approved even if `valid_from` in the past since the payout period has not passed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(4) @@ -966,7 +966,7 @@ fn void_spend_works() { // spend cannot be voided if already attempted. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(1) @@ -980,7 +980,7 @@ fn void_spend_works() { // void spend. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), Some(10) @@ -999,7 +999,7 @@ fn check_status_works() { // spend `0` expired and can be removed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -1012,7 +1012,7 @@ fn check_status_works() { // spend `1` payment failed and expired hence can be removed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -1036,7 +1036,7 @@ fn check_status_works() { // spend `2` payment succeed. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -1051,7 +1051,7 @@ fn check_status_works() { // spend `3` payment in process. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -1067,7 +1067,7 @@ fn check_status_works() { // spend `4` removed since the payment status is unknown. assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 2, Box::new(6), None @@ -1173,7 +1173,7 @@ fn try_state_spends_invariant_1_works() { assert_ok!({ Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 1, Box::new(6), None, @@ -1199,7 +1199,7 @@ fn try_state_spends_invariant_2_works() { use frame_support::pallet_prelude::DispatchError::Other; // Propose and approve a spend assert_ok!({ - Treasury::spend(RuntimeOrigin::signed(10), SpendAsset::Specific(1), 1, Box::new(6), None) + Treasury::spend(RuntimeOrigin::signed(10), Box::new(SpendAsset::Specific(1)), 1, Box::new(6), None) }); assert_eq!(Spends::::iter().count(), 1); let current_spend_count = SpendCount::::get(); @@ -1230,7 +1230,7 @@ fn try_state_spends_invariant_3_works() { assert_ok!({ Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Specific(1), + Box::new(SpendAsset::Specific(1)), 1, Box::new(6), None, @@ -1302,7 +1302,7 @@ fn category_spend_works() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Category(bounded_category.clone()), + Box::new(SpendAsset::Category(bounded_category.clone())), 2, Box::new(6), None @@ -1347,7 +1347,7 @@ fn category_payout_distributes_across_assets() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 50, Box::new(6), None @@ -1392,7 +1392,7 @@ fn category_payout_partial_fulfillment() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 50, Box::new(6), None @@ -1437,7 +1437,7 @@ fn category_payout_skips_assets_with_no_balance() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 30, Box::new(6), None @@ -1475,7 +1475,7 @@ fn category_spend_with_unknown_category() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 10, Box::new(6), None @@ -1494,7 +1494,7 @@ fn mixed_specific_and_category_spends() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Specific(4), + Box::new(SpendAsset::Specific(4)), 25, Box::new(7), None @@ -1506,7 +1506,7 @@ fn mixed_specific_and_category_spends() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 30, Box::new(8), None @@ -1538,7 +1538,7 @@ fn category_check_status_with_multiple_executions() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 40, Box::new(6), None @@ -1566,7 +1566,7 @@ fn category_spend_with_custom_category() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 80, Box::new(6), None @@ -1600,7 +1600,7 @@ fn category_spend_respects_spend_origin_limit() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Category(bounded_category.clone()), + Box::new(SpendAsset::Category(bounded_category.clone())), 2, Box::new(6), None @@ -1609,7 +1609,7 @@ fn category_spend_respects_spend_origin_limit() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(10), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 3, Box::new(6), None @@ -1634,7 +1634,7 @@ fn category_spend_with_empty_category_assets() { assert_noop!( Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 10, Box::new(6), None @@ -1655,7 +1655,7 @@ fn category_spend_cannot_void_after_payout() { assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 50, Box::new(6), None @@ -1684,7 +1684,7 @@ fn category_spend_expiry_works() { // Create category spend assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 50, Box::new(6), None @@ -1708,7 +1708,7 @@ fn category_spend_valid_from_works() { // Create category spend valid from block 3 assert_ok!(Treasury::spend( RuntimeOrigin::signed(14), - SpendAsset::Category(bounded_category), + Box::new(SpendAsset::Category(bounded_category)), 50, Box::new(6), Some(3) From badda3034bdab1632d331b1976ad8531fc200b65 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Tue, 10 Feb 2026 06:21:46 +0100 Subject: [PATCH 17/21] integration test change --- .../src/tests/fellowship_treasury.rs | 2 +- .../asset-hub-westend/src/tests/treasury.rs | 2 +- .../src/tests/fellowship_treasury.rs | 8 +-- substrate/frame/treasury/src/benchmarking.rs | 49 ++++++++++++------- substrate/frame/treasury/src/tests.rs | 2 + 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/fellowship_treasury.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/fellowship_treasury.rs index 124ec2ec1f66..594b338ec8e1 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/fellowship_treasury.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/fellowship_treasury.rs @@ -63,7 +63,7 @@ fn create_and_claim_treasury_spend() { // create and approve a treasury spend. assert_ok!(FellowshipTreasury::spend( root, - Box::new(asset_kind), + Box::new(pallet_treasury::SpendAsset::Specific(asset_kind)), SPEND_AMOUNT, Box::new(Location::new(0, Into::<[u8; 32]>::into(alice.clone())).into()), None, diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/treasury.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/treasury.rs index 3b53557fc05c..1a9a03f0139b 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/treasury.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/treasury.rs @@ -64,7 +64,7 @@ fn create_and_claim_treasury_spend() { // create and approve a treasury spend. assert_ok!(Treasury::spend( root, - Box::new(asset_kind), + Box::new(pallet_treasury::SpendAsset::Specific(asset_kind)), SPEND_AMOUNT, Box::new(Location::new(0, Into::<[u8; 32]>::into(alice.clone())).into()), None, diff --git a/cumulus/parachains/integration-tests/emulated/tests/collectives/collectives-westend/src/tests/fellowship_treasury.rs b/cumulus/parachains/integration-tests/emulated/tests/collectives/collectives-westend/src/tests/fellowship_treasury.rs index aeab5a1ba5f2..26bcaa12b6a2 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/collectives/collectives-westend/src/tests/fellowship_treasury.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/collectives/collectives-westend/src/tests/fellowship_treasury.rs @@ -111,10 +111,10 @@ fn fellowship_treasury_spend() { let native_asset = Location::parent(); let treasury_spend_call = RuntimeCall::Treasury(pallet_treasury::Call::::spend { - asset_kind: bx!(VersionedLocatableAsset::from(( + asset: bx!(pallet_treasury::SpendAsset::Specific(VersionedLocatableAsset::from(( asset_hub_location.clone(), native_asset.into() - ))), + )))), amount: fellowship_treasury_balance, beneficiary: bx!(VersionedLocation::from(fellowship_treasury_location)), valid_from: None, @@ -189,10 +189,10 @@ fn fellowship_treasury_spend() { let fellowship_treasury_spend_call = RuntimeCall::FellowshipTreasury(pallet_treasury::Call::::spend { - asset_kind: bx!(VersionedLocatableAsset::from(( + asset: bx!(pallet_treasury::SpendAsset::Specific(VersionedLocatableAsset::from(( asset_hub_location, native_asset.into() - ))), + )))), amount: fellowship_spend_balance, beneficiary: bx!(VersionedLocation::from(alice_location)), valid_from: None, diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index a11723a27b2c..9630cbfcaea0 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -192,7 +192,7 @@ mod benchmarks { #[extrinsic_call] _( origin as T::RuntimeOrigin, - Box::new(asset_kind.clone()), + Box::new(SpendAsset::Specific(asset_kind.clone())), amount, Box::new(beneficiary_lookup), None, @@ -203,7 +203,7 @@ mod benchmarks { assert_last_event::( Event::AssetSpendApproved { index: 0, - asset_kind, + asset:SpendAsset::Specific(asset_kind), amount, beneficiary, valid_from, @@ -223,7 +223,7 @@ mod benchmarks { let spend_exists = if let Ok(origin) = T::SpendOrigin::try_successful_origin() { Treasury::::spend( origin, - Box::new(asset_kind.clone()), + Box::new(SpendAsset::Specific(asset_kind.clone())), amount, Box::new(beneficiary_lookup), None, @@ -234,7 +234,7 @@ mod benchmarks { false }; - T::Paymaster::ensure_successful(&beneficiary, asset_kind, amount); + T::Paymaster::ensure_successful(&beneficiary, asset_kind.clone(), amount); let caller: T::AccountId = account("caller", 0, SEED); #[block] @@ -250,15 +250,23 @@ mod benchmarks { if spend_exists { let id = match Spends::::get(0).unwrap().status { - PaymentState::Attempted { id, .. } => { - assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure); - id - }, - _ => panic!("No payout attempt made"), - }; - assert_last_event::(Event::Paid { index: 0, payment_id: id }.into()); - assert!(Treasury::::payout(RawOrigin::Signed(caller).into(), 0u32).is_err()); - } + + PaymentState::Attempted { executions, remaining_amount } => { + executions.first() + .expect("No executions found in Attempted state") + .payment_id + }, + _ => panic!("No payout attempt made"), + }; + assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure); + + assert_last_event::(Event::Paid { index: 0, execution: PaymentExecution { + asset: asset_kind, + amount, + payment_id: id + } }.into()); + assert!(Treasury::::payout(RawOrigin::Signed(caller).into(), 0u32).is_err()); + } Ok(()) } @@ -275,17 +283,20 @@ mod benchmarks { let spend_exists = if let Ok(origin) = T::SpendOrigin::try_successful_origin() { Treasury::::spend( origin, - Box::new(asset_kind), + Box::new(SpendAsset::Specific(asset_kind)), amount, Box::new(beneficiary_lookup), None, )?; Treasury::::payout(RawOrigin::Signed(caller.clone()).into(), 0u32)?; - match Spends::::get(0).unwrap().status { - PaymentState::Attempted { id, .. } => { - T::Paymaster::ensure_concluded(id); - }, + match Spends::::get(0).unwrap().status { + PaymentState::Attempted { executions, remaining_amount } => { + let id = executions.first() + .expect("No executions found in Attempted state") + .payment_id; + T::Paymaster::ensure_concluded(id); + }, _ => panic!("No payout attempt made"), }; @@ -320,7 +331,7 @@ mod benchmarks { let spend_exists = if let Ok(origin) = T::SpendOrigin::try_successful_origin() { Treasury::::spend( origin, - Box::new(asset_kind.clone()), + Box::new(SpendAsset::Specific(asset_kind.clone())), amount, Box::new(beneficiary_lookup), None, diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 4d14bce631b9..56956628d8df 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -211,6 +211,8 @@ impl pallet_assets::Config for Test { type Holder = (); type WeightInfo = (); type ReserveData = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } impl Config for Test { From bc713cb9ea58c12bb7bd8775974026942f30474c Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Tue, 10 Feb 2026 17:32:28 +0100 Subject: [PATCH 18/21] .. --- substrate/frame/treasury/src/benchmarking.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index 9630cbfcaea0..53236e6a5e50 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -251,7 +251,7 @@ mod benchmarks { if spend_exists { let id = match Spends::::get(0).unwrap().status { - PaymentState::Attempted { executions, remaining_amount } => { + PaymentState::Attempted { executions, .. } => { executions.first() .expect("No executions found in Attempted state") .payment_id @@ -291,7 +291,7 @@ mod benchmarks { Treasury::::payout(RawOrigin::Signed(caller.clone()).into(), 0u32)?; match Spends::::get(0).unwrap().status { - PaymentState::Attempted { executions, remaining_amount } => { + PaymentState::Attempted { executions, .. } => { let id = executions.first() .expect("No executions found in Attempted state") .payment_id; From 83af0a94727affb7315dd9b0f36e38b393093e41 Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Tue, 17 Feb 2026 08:17:23 +0100 Subject: [PATCH 19/21] nit --- Cargo.lock | 1 - .../asset-hub-westend/src/governance/mod.rs | 9 +- .../assets/asset-hub-westend/src/lib.rs | 2 +- .../runtime/common/src/integration_tests.rs | 2 +- substrate/frame/assets/src/lib.rs | 11 +- substrate/frame/bounties/Cargo.toml | 4 +- substrate/frame/child-bounties/Cargo.toml | 4 - substrate/frame/child-bounties/src/tests.rs | 3 +- substrate/frame/treasury/src/benchmarking.rs | 49 +- substrate/frame/treasury/src/lib.rs | 253 ++++---- substrate/frame/treasury/src/tests.rs | 563 +++++++++--------- 11 files changed, 427 insertions(+), 474 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66503639222e..ce8da8afcf7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11881,7 +11881,6 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-assets", "pallet-balances", "pallet-bounties", "pallet-treasury", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 635930bb8ca3..bf9bdb441afe 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -21,7 +21,7 @@ use crate::xcm_config::Collectives; use frame_support::{ parameter_types, traits::{ - tokens::{UnityOrOuterConversion, asset_ops::common_ops::ConfigurableAssetCategoryManager}, + tokens::{asset_ops::common_ops::ConfigurableAssetCategoryManager, UnityOrOuterConversion}, EitherOf, EitherOfDiverse, FromContains, }, }; @@ -135,11 +135,8 @@ pub type TreasuryBalanceConverter = UnityOrOuterConversion< AssetRate, >; -pub type AHWAssetCategories = ConfigurableAssetCategoryManager< - AccountId, - VersionedLocatableAsset, - Balance, ->; +pub type AHWAssetCategories = + ConfigurableAssetCategoryManager; impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index d23875ec5496..b4650075bb91 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -40,12 +40,12 @@ use governance::{pallet_custom_origins, FellowshipAdmin, GeneralAdmin, StakingAd extern crate alloc; +use alloc::{vec, vec::Vec}; pub use assets_common::local_and_foreign_assets::ForeignAssetReserveData; use assets_common::{ local_and_foreign_assets::{LocalFromLeft, TargetFromLeft}, AssetIdForPoolAssets, AssetIdForPoolAssetsConvert, AssetIdForTrustBackedAssetsConvert, }; -use alloc::{vec, vec::Vec}; use bp_asset_hub_westend::CreateForeignAssetDeposit; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use cumulus_pallet_parachain_system::{RelayNumberMonotonicallyIncreases, RelaychainDataProvider}; diff --git a/polkadot/runtime/common/src/integration_tests.rs b/polkadot/runtime/common/src/integration_tests.rs index 750f36942739..3d871a2dfb2e 100644 --- a/polkadot/runtime/common/src/integration_tests.rs +++ b/polkadot/runtime/common/src/integration_tests.rs @@ -24,7 +24,7 @@ use crate::{ slots, traits::{AuctionStatus, Auctioneer, Leaser, Registrar as RegistrarT}, }; -use alloc::{sync::Arc, vec::Vec}; +use alloc::sync::Arc; use codec::Encode; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index c0656f83e368..961e2a980f8c 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -1981,15 +1981,10 @@ pub mod pallet { type Balance = T::Balance; /// Get all assets in a category - // TODO: Refine or investigate fn assets_in_category(category: &[u8]) -> Vec { - let bounded_category: BoundedVec> = - category.to_vec().try_into().unwrap_or_default(); - - AssetCategories::::get(&bounded_category) - .into_inner() - .into_iter() - .collect() + BoundedVec::>::try_from(category.to_vec()) + .map(|bounded_category| AssetCategories::::get(bounded_category).into_inner()) + .unwrap_or_default() } /// Get available balance of a specific asset in treasury diff --git a/substrate/frame/bounties/Cargo.toml b/substrate/frame/bounties/Cargo.toml index 8caf9793b7dc..3fe756d59d6d 100644 --- a/substrate/frame/bounties/Cargo.toml +++ b/substrate/frame/bounties/Cargo.toml @@ -39,7 +39,6 @@ std = [ "frame-support/std", "frame-system/std", "log/std", - "pallet-assets/std", "pallet-balances/std", "pallet-treasury/std", "scale-info/std", @@ -52,7 +51,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-assets/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", "sp-runtime/runtime-benchmarks", @@ -60,7 +58,7 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", - "pallet-assets/try-runtime", + "pallet-assets/try-runtime", "pallet-balances/try-runtime", "pallet-treasury/try-runtime", "sp-runtime/try-runtime", diff --git a/substrate/frame/child-bounties/Cargo.toml b/substrate/frame/child-bounties/Cargo.toml index 2dc2f85e27ef..d0bdc1856a5c 100644 --- a/substrate/frame/child-bounties/Cargo.toml +++ b/substrate/frame/child-bounties/Cargo.toml @@ -30,7 +30,6 @@ sp-runtime = { workspace = true } [dev-dependencies] pallet-balances = { workspace = true, default-features = true } -pallet-assets = { workspace = true } [features] default = ["std"] @@ -40,7 +39,6 @@ std = [ "frame-support/std", "frame-system/std", "log/std", - "pallet-assets/std", "pallet-balances/std", "pallet-bounties/std", "pallet-treasury/std", @@ -54,7 +52,6 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-bounties/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", @@ -63,7 +60,6 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", - "pallet-assets/try-runtime", "pallet-balances/try-runtime", "pallet-bounties/try-runtime", "pallet-treasury/try-runtime", diff --git a/substrate/frame/child-bounties/src/tests.rs b/substrate/frame/child-bounties/src/tests.rs index 5958dbb24eab..48c3d33cffae 100644 --- a/substrate/frame/child-bounties/src/tests.rs +++ b/substrate/frame/child-bounties/src/tests.rs @@ -35,12 +35,11 @@ use frame_support::{ PalletId, }; +use super::Event as ChildBountiesEvent; use sp_runtime::{ traits::{BadOrigin, IdentityLookup}, BuildStorage, Perbill, Permill, TokenError, }; -// use pallet_assets::AssetCategoryManager; -use super::Event as ChildBountiesEvent; type Block = frame_system::mocking::MockBlock; type BountiesError = pallet_bounties::Error; diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index 53236e6a5e50..15d01967df5c 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -203,7 +203,7 @@ mod benchmarks { assert_last_event::( Event::AssetSpendApproved { index: 0, - asset:SpendAsset::Specific(asset_kind), + asset: SpendAsset::Specific(asset_kind), amount, beneficiary, valid_from, @@ -250,23 +250,21 @@ mod benchmarks { if spend_exists { let id = match Spends::::get(0).unwrap().status { - - PaymentState::Attempted { executions, .. } => { - executions.first() - .expect("No executions found in Attempted state") - .payment_id - }, - _ => panic!("No payout attempt made"), - }; - assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure); - - assert_last_event::(Event::Paid { index: 0, execution: PaymentExecution { - asset: asset_kind, - amount, - payment_id: id - } }.into()); - assert!(Treasury::::payout(RawOrigin::Signed(caller).into(), 0u32).is_err()); - } + PaymentState::Attempted { executions, .. } => + executions.first().expect("No executions found in Attempted state").payment_id, + _ => panic!("No payout attempt made"), + }; + assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure); + + assert_last_event::( + Event::Paid { + index: 0, + execution: PaymentExecution { asset: asset_kind, amount, payment_id: id }, + } + .into(), + ); + assert!(Treasury::::payout(RawOrigin::Signed(caller).into(), 0u32).is_err()); + } Ok(()) } @@ -290,13 +288,14 @@ mod benchmarks { )?; Treasury::::payout(RawOrigin::Signed(caller.clone()).into(), 0u32)?; - match Spends::::get(0).unwrap().status { - PaymentState::Attempted { executions, .. } => { - let id = executions.first() - .expect("No executions found in Attempted state") - .payment_id; - T::Paymaster::ensure_concluded(id); - }, + match Spends::::get(0).unwrap().status { + PaymentState::Attempted { executions, .. } => { + let id = executions + .first() + .expect("No executions found in Attempted state") + .payment_id; + T::Paymaster::ensure_concluded(id); + }, _ => panic!("No payout attempt made"), }; diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 7c5fe16d2ca9..05a489a3c6d3 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -103,8 +103,10 @@ use frame_support::{ pallet_prelude::DispatchError, print, traits::{ - tokens::{Pay, asset_ops::common_ops::AssetCategoryManager}, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, - ReservableCurrency, WithdrawReasons, + tokens::{asset_ops::common_ops::AssetCategoryManager, Pay}, + Currency, + ExistenceRequirement::KeepAlive, + Get, Imbalance, OnUnbalanced, ReservableCurrency, WithdrawReasons, }, weights::Weight, BoundedVec, PalletId, @@ -189,15 +191,7 @@ pub enum PaymentState { #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[derive( - Encode, - Decode, - DecodeWithMemTracking, - Clone, - PartialEq, - Eq, - Debug, - TypeInfo, - MaxEncodedLen, + Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen, )] pub enum SpendAsset { /// Spend a specific asset @@ -205,19 +199,11 @@ pub enum SpendAsset { /// Spend from a category of assets Category(BoundedVec>), } -// TODO: Move to primitives + /// Represents a partial payment using a specific asset from a category #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[derive( - Encode, - Decode, - DecodeWithMemTracking, - Clone, - PartialEq, - Eq, - Debug, - TypeInfo, - MaxEncodedLen, + Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen, )] pub struct PaymentExecution { /// The asset used for this partial payment @@ -519,6 +505,14 @@ pub mod pallet { TooManyFailedPayments, AssetNotFound, + + ConversionFailed, + + ArithmeticOverflow, + + ConversionRateMismatch, + + InvalidConversionRate, } #[pallet::hooks] @@ -736,62 +730,69 @@ pub mod pallet { let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); ensure!(expire_at > now, Error::::SpendExpired); - match *asset { - SpendAsset::Category(ref category) => { - let assets = Self::validate_category_spend(category, amount)?; - - // This helps prevent issues where assets in the same category - // might have different conversion rates to native currency - if let Some(first_asset) = assets.first() { - let first_native = - T::BalanceConverter::from_asset_balance(amount, first_asset.clone()) - .ok(); - - // Store first_native for comparison with others - if let Some(first_native) = first_native { - for asset_kind in assets.iter().skip(1) { - let current_native = T::BalanceConverter::from_asset_balance( - amount, - asset_kind.clone(), - ) - .ok(); // Convert Result to Option - - if let Some(current) = current_native { - // Convert to u128 for calculation - let first_u128: u128 = first_native.unique_saturated_into(); - let current_u128: u128 = current.unique_saturated_into(); - - let max_diff = first_u128.max(current_u128); - let min_diff = first_u128.min(current_u128); - - // Calculate percentage difference safely - if max_diff > 0 { - let diff = max_diff - min_diff; - // Multiply by 100 first to maintain precision - let diff_percent = (diff * 100) / max_diff; - - if diff_percent > 5 { - log::warn!( + if let SpendAsset::Category(ref category) = *asset { + let assets = Self::validate_category_spend(category, amount)?; + + if let Some(base) = assets.first() { + let native = + T::BalanceConverter::from_asset_balance(amount, base.clone()) + .or_else(|_| { + log::error!( + "Failed to convert first asset in {:?} to native balance", + category + ); + Err(Error::::ConversionFailed) + })?; + + for (index, asset_kind) in assets.iter().enumerate().skip(1) { + let candidate = T::BalanceConverter::from_asset_balance(amount, asset_kind.clone(), + ).or_else(|_| { + log::error!("Failed to convert asset {:?} (index {}) in category {:?} to native balance", asset_kind, index, category); + Err(Error::::ConversionFailed)})?; + + // Convert to u128 + let first_u128: u128 = native.unique_saturated_into(); + let current_u128: u128 = candidate.unique_saturated_into(); + + let (max, min) = if first_u128 > current_u128 { + (first_u128, current_u128) + } else { + (current_u128, first_u128) + }; + + if max > 0 { + let diff = + max.checked_sub(min).ok_or(Error::::ConversionFailed)?; + + let multiplied_diff = + diff.checked_mul(100).ok_or(Error::::ConversionFailed)?; + + let diff_percent = multiplied_diff.checked_div(max).unwrap_or(0); + + if diff_percent > 5 { + log::warn!( "Asset {:?} in category {:?} has significantly different conversion rate ({}% diff)", asset_kind, category, diff_percent ); - - // TODO: return an error here, for now just log a - // warning return Err(Error::::ConversionRateMismatch.into()); - } - } - } + return Err(Error::::ConversionRateMismatch.into()); } + } else if max == 0 && min == 0 { + // Both are zero, which is acceptable + continue; + } else { + // max == 0 but min > 0, which should't happen + log::error!( + "Zero conversion rate detected for asset {:?} in category {:?}", + if max == first_u128 { base } else { asset_kind }, + category + ); + + return Err(Error::::InvalidConversionRate.into()); } } - }, - SpendAsset::Specific(ref asset_kind) => { - let _ = - Self::ensure_sufficient_balance(asset_kind, &Self::account_id(), amount)?; - }, + } } let native_amount = match *asset { @@ -884,38 +885,29 @@ pub mod pallet { let mut spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; let now = T::BlockNumberProvider::current_block_number(); ensure!(now >= spend.valid_from, Error::::EarlyPayout); - ensure!(spend.expire_at > now, Error::::SpendExpired); + ensure!(spend.expire_at > now, Error::::SpendExpired); // when this expires we + // can't executed the same + // partial payment? match spend.asset { - SpendAsset::Specific(ref asset_kind) => { - // Validate asset has sufficient balance before attempting payment - let _ = Self::ensure_sufficient_balance( - asset_kind, - &Self::account_id(), - spend.amount, - )?; - match spend.status { - PaymentState::Pending | PaymentState::Failed(_) => { - let id = T::Paymaster::pay( - &spend.beneficiary, - asset_kind.clone(), - spend.amount, - ) - .map_err(|_| Error::::PayoutError)?; - - spend.status = PaymentState::Attempted { - executions: BoundedVec::try_from(vec![PaymentExecution { - asset: asset_kind.clone(), - amount: spend.amount, - payment_id: id, - }]) - .map_err(|_| Error::::ExecutionRateLimit)?, - - remaining_amount: Zero::zero(), - }; - }, - _ => return Err(Error::::AlreadyAttempted.into()), - } + SpendAsset::Specific(ref asset_kind) => match spend.status { + PaymentState::Pending | PaymentState::Failed(_) => { + let id = + T::Paymaster::pay(&spend.beneficiary, asset_kind.clone(), spend.amount) + .map_err(|_| Error::::PayoutError)?; + + spend.status = PaymentState::Attempted { + executions: BoundedVec::try_from(vec![PaymentExecution { + asset: asset_kind.clone(), + amount: spend.amount, + payment_id: id, + }]) + .map_err(|_| Error::::ExecutionRateLimit)?, + + remaining_amount: Zero::zero(), + }; + }, + _ => return Err(Error::::AlreadyAttempted.into()), }, SpendAsset::Category(ref category) => match spend.status { PaymentState::Pending => { @@ -947,7 +939,8 @@ pub mod pallet { spend.status = PaymentState::Partial(retry_amount); } }, - PaymentState::Partial(unpaid) => { + + PaymentState::Partial(unpaid) => { let (executions, remaining_amount) = Self::pay_from_category(category, &spend.beneficiary, unpaid)?; if remaining_amount.is_zero() { @@ -959,6 +952,7 @@ pub mod pallet { spend.status = PaymentState::Partial(remaining_amount); } }, + PaymentState::Attempted { .. } => { return Err(Error::::AlreadyAttempted.into()); }, @@ -1020,7 +1014,7 @@ pub mod pallet { // Check payment status let results: Vec<_> = executions .iter() - .map(|exec| (exec, T::Paymaster::check_payment(exec.payment_id.clone()))) + .map(|exec| (exec, T::Paymaster::check_payment(exec.payment_id))) .collect(); // In-progress payments @@ -1062,14 +1056,14 @@ pub mod pallet { return Ok(Pays::No.into()); } - if all_succeeded && !remaining_amount.is_zero() { - spend.status = State::Partial(*remaining_amount); - spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); - Spends::::insert(index, spend); - return Ok(Pays::Yes.into()) - } + if all_succeeded && !remaining_amount.is_zero() { + spend.status = State::Partial(*remaining_amount); + spend.expire_at = now.saturating_add(T::PayoutPeriod::get()); + Spends::::insert(index, spend); + return Ok(Pays::Yes.into()); + } - Ok(Pays::Yes.into()) + Ok(Pays::Yes.into()) }, _ => return Err(Error::::NotAttempted.into()), } @@ -1122,7 +1116,8 @@ impl, I: 'static> Pallet { T::PalletId::get().into_account_truncating() } - fn ensure_sufficient_balance( + /// Ensure this asset has sufficient balance + fn ensure_sufficient_balance( asset_kind: &T::AssetKind, account: &T::AccountId, required: AssetBalanceOf, @@ -1312,17 +1307,12 @@ impl, I: 'static> Pallet { break; } - // let available = - // Self::ensure_sufficient_balance(&asset_kind, &Self::account_id(), amount)?; - - let available = match T::AssetCategories::available_balance(asset_kind.clone(), Self::account_id()) { - Some(balance) if balance > Zero::zero() => balance, - _ => continue, - }; - - // if available.is_zero() { - // continue; - // } + let available = + match T::AssetCategories::available_balance(asset_kind.clone(), Self::account_id()) + { + Some(balance) if balance > Zero::zero() => balance, + _ => continue, + }; let pay_amount = if available >= remaining_amount { remaining_amount } else { available }; @@ -1379,18 +1369,15 @@ impl, I: 'static> Pallet { break; } - // let mut available = - // Self::ensure_sufficient_balance(&exec.asset, &Self::account_id(), exec.amount)?; - - let mut available = match T::AssetCategories::available_balance(exec.asset.clone(), Self::account_id()) { - Some(balance) if balance >= exec.amount => balance, - _ => Zero::zero(), - }; + let mut available = + match T::AssetCategories::available_balance(exec.asset.clone(), Self::account_id()) + { + Some(balance) if balance >= exec.amount => balance, + _ => Zero::zero(), + }; let asset_kind = if available.is_zero() { - match assets.iter().find(|&ak| { - !used_assets.contains(ak) - }) { + match assets.iter().find(|&ak| !used_assets.contains(ak)) { Some(ak) => { available = Self::ensure_sufficient_balance(ak, &Self::account_id(), exec.amount)?; @@ -1424,7 +1411,6 @@ impl, I: 'static> Pallet { category: &BoundedVec>, amount: AssetBalanceOf, ) -> Result, DispatchError> { - // Just return assets let assets = T::AssetCategories::assets_in_category(category.as_slice()); ensure!(!assets.is_empty(), Error::::EmptyAssetCategory); @@ -1432,10 +1418,7 @@ impl, I: 'static> Pallet { for asset_kind in &assets { if let Some(balance) = - T::AssetCategories::available_balance(asset_kind.clone(), Self::account_id()) // TODO: - // Investigate - // returning - // DispatchError + T::AssetCategories::available_balance(asset_kind.clone(), Self::account_id()) { accumulated = accumulated.saturating_add(balance); if accumulated >= amount { diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 56956628d8df..1b0df68a70c3 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -36,9 +36,9 @@ use frame_support::{ PalletId, }; -use frame_support::traits::AsEnsureOriginWithArg; use super::*; use crate as treasury; +use frame_support::traits::AsEnsureOriginWithArg; type Block = frame_system::mocking::MockBlock; type UtilityCall = pallet_utility::Call; @@ -49,7 +49,7 @@ frame_support::construct_runtime!( { System: frame_system, Balances: pallet_balances, - Assets: pallet_assets, + Assets: pallet_assets, Treasury: treasury, Utility: pallet_utility, } @@ -181,38 +181,38 @@ impl> ConversionFromAssetBalance for MulBy { } parameter_types! { - pub const AssetDeposit: u64 = 1; - pub const AssetAccountDeposit: u64 = 1; - pub const MetadataDepositBase: u64 = 1; - pub const MetadataDepositPerByte: u64 = 1; - pub const ApprovalDeposit: u64 = 1; - pub const StringLimit: u32 = 50; - pub const MaxReserves: u32 = 5; + pub const AssetDeposit: u64 = 1; + pub const AssetAccountDeposit: u64 = 1; + pub const MetadataDepositBase: u64 = 1; + pub const MetadataDepositPerByte: u64 = 1; + pub const ApprovalDeposit: u64 = 1; + pub const StringLimit: u32 = 50; + pub const MaxReserves: u32 = 5; } impl pallet_assets::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Balance = u64; - type AssetId = u32; - type AssetIdParameter = u32; - type Currency = Balances; - type CreateOrigin = AsEnsureOriginWithArg>; - type ForceOrigin = frame_system::EnsureRoot; - type AssetDeposit = AssetDeposit; - type AssetAccountDeposit = AssetAccountDeposit; - type MetadataDepositBase = MetadataDepositBase; - type MetadataDepositPerByte = MetadataDepositPerByte; - type ApprovalDeposit = ApprovalDeposit; - type StringLimit = StringLimit; - type Freezer = (); - type Extra = (); - type CallbackHandle = (); - type RemoveItemsLimit = ConstU32<5>; - type Holder = (); - type WeightInfo = (); - type ReserveData = (); - #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = (); + type RuntimeEvent = RuntimeEvent; + type Balance = u64; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = AssetDeposit; + type AssetAccountDeposit = AssetAccountDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = StringLimit; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type RemoveItemsLimit = ConstU32<5>; + type Holder = (); + type WeightInfo = (); + type ReserveData = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } impl Config for Test { @@ -268,43 +268,42 @@ impl ExtBuilder { pub fn build(self) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - let assets_config: pallet_assets::GenesisConfig = pallet_assets::GenesisConfig { - - assets: vec![ - // id, owner, is_sufficient, min_balance - (1, 0, true, 1), - (2, 0, true, 1), - (3, 0, true, 1), - (4, 0, true, 1), - (5, 0, true, 1), - (10, 0, true, 1), - (11, 0, true, 1), - ], - - metadata: vec![ - // id name, symbol, decimals - (1, "Asset 1".into(), "A1".into(), 10), - (2, "Asset 2".into(), "A2".into(), 10), - (3, "Asset 3".into(), "A3".into(), 10), - (4, "Asset 4".into(), "A4".into(), 10), - (5, "Asset 5".into(), "A5".into(), 10), - (10, "Asset 10".into(), "A10".into(), 10), - (11, "Asset 11".into(), "A11".into(), 10), - ], - - accounts: vec![ - // id, account_id, balance - (1, Treasury::account_id(), 20), - (2, Treasury::account_id(), 20), - (3, Treasury::account_id(), 20), - (4, Treasury::account_id(), 25), - (5, Treasury::account_id(), 50), - (10, Treasury::account_id(), 50), - (11, Treasury::account_id(), 30), - ], - next_asset_id: None, - reserves: vec![], - }; + let assets_config: pallet_assets::GenesisConfig = pallet_assets::GenesisConfig { + assets: vec![ + // id, owner, is_sufficient, min_balance + (1, 0, true, 1), + (2, 0, true, 1), + (3, 0, true, 1), + (4, 0, true, 1), + (5, 0, true, 1), + (10, 0, true, 1), + (11, 0, true, 1), + ], + + metadata: vec![ + // id name, symbol, decimals + (1, "Asset 1".into(), "A1".into(), 10), + (2, "Asset 2".into(), "A2".into(), 10), + (3, "Asset 3".into(), "A3".into(), 10), + (4, "Asset 4".into(), "A4".into(), 10), + (5, "Asset 5".into(), "A5".into(), 10), + (10, "Asset 10".into(), "A10".into(), 10), + (11, "Asset 11".into(), "A11".into(), 10), + ], + + accounts: vec![ + // id, account_id, balance + (1, Treasury::account_id(), 20), + (2, Treasury::account_id(), 20), + (3, Treasury::account_id(), 20), + (4, Treasury::account_id(), 25), + (5, Treasury::account_id(), 50), + (10, Treasury::account_id(), 50), + (11, Treasury::account_id(), 30), + ], + next_asset_id: None, + reserves: vec![], + }; pallet_balances::GenesisConfig:: { // Total issuance will be 200 with treasury account initialized at ED. balances: vec![(0, 100), (1, 98), (2, 1)], @@ -313,33 +312,31 @@ impl ExtBuilder { .assimilate_storage(&mut t) .unwrap(); - assets_config.assimilate_storage(&mut t).unwrap(); + assets_config.assimilate_storage(&mut t).unwrap(); crate::GenesisConfig::::default().assimilate_storage(&mut t).unwrap(); let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| { - - System::set_block_number(1); - - let usd_category: BoundedVec> = - BoundedVec::try_from(b"USD".to_vec()).unwrap(); - let stable_category: BoundedVec> = - BoundedVec::try_from(b"STABLE".to_vec()).unwrap(); + ext.execute_with(|| { + System::set_block_number(1); - // TODO: Create assets in USD category? + let usd_category: BoundedVec> = + BoundedVec::try_from(b"USD".to_vec()).unwrap(); + let stable_category: BoundedVec> = + BoundedVec::try_from(b"STABLE".to_vec()).unwrap(); - // Set up categories - pallet_assets::AssetCategories::::insert( - &usd_category, - BoundedVec::try_from(vec![1u32, 2u32, 3u32]).unwrap(), - ); + // TODO: Create assets in USD category? - pallet_assets::AssetCategories::::insert( - &stable_category, - BoundedVec::try_from(vec![10u32, 11u32]).unwrap(), - ); + // Set up categories + pallet_assets::AssetCategories::::insert( + &usd_category, + BoundedVec::try_from(vec![1u32, 2u32, 3u32]).unwrap(), + ); - }); + pallet_assets::AssetCategories::::insert( + &stable_category, + BoundedVec::try_from(vec![10u32, 11u32]).unwrap(), + ); + }); ext } } @@ -1338,131 +1335,125 @@ fn category_spend_works() { #[test] fn category_payout_distributes_across_assets() { - ExtBuilder::default() - .build() - .execute_with(|| { - System::set_block_number(1); + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); - let category = b"USD".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); + let category = b"USD".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), - 50, - Box::new(6), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + None + )); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - assert_eq!(paid(6, 1), 20); - assert_eq!(paid(6, 2), 20); - assert_eq!(paid(6, 3), 10); - - let spend = Spends::::get(0).unwrap(); - match &spend.status { - PaymentState::Attempted { executions, remaining_amount } => { - assert_eq!(executions.len(), 3); - assert_eq!(*remaining_amount, 0); - - let mut asset_payments = std::collections::BTreeMap::new(); - for exec in executions.iter() { - asset_payments.insert(exec.asset, exec.amount); - } - - assert_eq!(asset_payments.get(&1), Some(&20)); - assert_eq!(asset_payments.get(&2), Some(&20)); - assert_eq!(asset_payments.get(&3), Some(&10)); - }, - _ => panic!("Expected Attempted status"), - } - }); + assert_eq!(paid(6, 1), 20); + assert_eq!(paid(6, 2), 20); + assert_eq!(paid(6, 3), 10); + + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 3); + assert_eq!(*remaining_amount, 0); + + let mut asset_payments = std::collections::BTreeMap::new(); + for exec in executions.iter() { + asset_payments.insert(exec.asset, exec.amount); + } + + assert_eq!(asset_payments.get(&1), Some(&20)); + assert_eq!(asset_payments.get(&2), Some(&20)); + assert_eq!(asset_payments.get(&3), Some(&10)); + }, + _ => panic!("Expected Attempted status"), + } + }); } #[test] fn category_payout_partial_fulfillment() { - ExtBuilder::default() - .build() - .execute_with(|| { - System::set_block_number(1); + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); - let category = b"USD".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); + let category = b"USD".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), - 50, - Box::new(6), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + None + )); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - assert_eq!(paid(6, 1), 20); - assert_eq!(paid(6, 2), 20); - assert_eq!(paid(6, 3), 10); - - let spend = Spends::::get(0).unwrap(); - match &spend.status { - PaymentState::Attempted { executions, remaining_amount } => { - assert_eq!(executions.len(), 3); - assert_eq!(*remaining_amount, 0); - - let mut asset_payments = std::collections::BTreeMap::new(); - for exec in executions.iter() { - asset_payments.insert(exec.asset, exec.amount); - } - - assert_eq!(asset_payments.get(&1), Some(&20)); - assert_eq!(asset_payments.get(&2), Some(&20)); - assert_eq!(asset_payments.get(&3), Some(&10)); - }, - _ => panic!("Expected Attempted status"), - } - }); + assert_eq!(paid(6, 1), 20); + assert_eq!(paid(6, 2), 20); + assert_eq!(paid(6, 3), 10); + + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 3); + assert_eq!(*remaining_amount, 0); + + let mut asset_payments = std::collections::BTreeMap::new(); + for exec in executions.iter() { + asset_payments.insert(exec.asset, exec.amount); + } + + assert_eq!(asset_payments.get(&1), Some(&20)); + assert_eq!(asset_payments.get(&2), Some(&20)); + assert_eq!(asset_payments.get(&3), Some(&10)); + }, + _ => panic!("Expected Attempted status"), + } + }); } #[test] fn category_payout_skips_assets_with_no_balance() { - ExtBuilder::default() - .build() - .execute_with(|| { - System::set_block_number(1); + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); - let category = b"USD".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); + let category = b"USD".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), - 30, - Box::new(6), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 30, + Box::new(6), + None + )); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - assert_eq!(paid(6, 1), 20); - assert_eq!(paid(6, 2), 10); - assert_eq!(paid(6, 3), 0); + assert_eq!(paid(6, 1), 20); + assert_eq!(paid(6, 2), 10); + assert_eq!(paid(6, 3), 0); - let spend = Spends::::get(0).unwrap(); - match &spend.status { - PaymentState::Attempted { executions, remaining_amount } => { - assert_eq!(executions.len(), 2); - assert_eq!(*remaining_amount, 0); + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 2); + assert_eq!(*remaining_amount, 0); - assert_eq!(executions[0].asset, 1); - assert_eq!(executions[0].amount, 20); - }, - _ => panic!("Expected Attempted status"), - } - }); + assert_eq!(executions[0].asset, 1); + assert_eq!(executions[0].amount, 20); + }, + _ => panic!("Expected Attempted status"), + } + }); } #[test] @@ -1489,106 +1480,100 @@ fn category_spend_with_unknown_category() { #[test] fn mixed_specific_and_category_spends() { - ExtBuilder::default() - .build() - .execute_with(|| { - System::set_block_number(1); + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Specific(4)), - 25, - Box::new(7), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Specific(4)), + 25, + Box::new(7), + None + )); - let category = b"USD".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); + let category = b"USD".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), - 30, - Box::new(8), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 30, + Box::new(8), + None + )); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - assert_eq!(paid(7, 4), 25); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_eq!(paid(7, 4), 25); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); - let paid_usd = paid(8, 1) + paid(8, 2); - assert_eq!(paid_usd, 30); + let paid_usd = paid(8, 1) + paid(8, 2); + assert_eq!(paid_usd, 30); - assert_eq!(SpendCount::::get(), 2); - assert_eq!(Spends::::iter().count(), 2); - }); + assert_eq!(SpendCount::::get(), 2); + assert_eq!(Spends::::iter().count(), 2); + }); } #[test] fn category_check_status_with_multiple_executions() { - ExtBuilder::default() - .build() - .execute_with(|| { - System::set_block_number(1); + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); - let category = b"USD".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); + let category = b"USD".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), - 40, - Box::new(6), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 40, + Box::new(6), + None + )); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - let payment_ids = get_all_payment_ids(0); - assert_eq!(payment_ids.len(), 2); + let payment_ids = get_all_payment_ids(0); + assert_eq!(payment_ids.len(), 2); - assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); - }); + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + }); } #[test] fn category_spend_with_custom_category() { - ExtBuilder::default() - .build() - .execute_with(|| { - System::set_block_number(1); + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); - let category = b"STABLE".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); + let category = b"STABLE".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), - 80, - Box::new(6), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 80, + Box::new(6), + None + )); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - assert_eq!(paid(6, 10), 50); - assert_eq!(paid(6, 11), 30); - assert_eq!(paid(6, 12), 0); - - let spend = Spends::::get(0).unwrap(); - match &spend.status { - PaymentState::Attempted { executions, remaining_amount } => { - assert_eq!(executions.len(), 2); - assert_eq!(*remaining_amount, 0); - }, - _ => panic!("Expected Attempted status"), - } - }); + assert_eq!(paid(6, 10), 50); + assert_eq!(paid(6, 11), 30); + assert_eq!(paid(6, 12), 0); + + let spend = Spends::::get(0).unwrap(); + match &spend.status { + PaymentState::Attempted { executions, remaining_amount } => { + assert_eq!(executions.len(), 2); + assert_eq!(*remaining_amount, 0); + }, + _ => panic!("Expected Attempted status"), + } + }); } #[test] @@ -1624,7 +1609,7 @@ fn category_spend_respects_spend_origin_limit() { #[test] fn category_spend_with_empty_category_assets() { ExtBuilder::default() - /*.with_category_assets(b"EMPTY*", vec![]) // Empty category*/ + // .with_category_assets(b"EMPTY*", vec![]) // Empty category .build() .execute_with(|| { System::set_block_number(1); @@ -1648,29 +1633,31 @@ fn category_spend_with_empty_category_assets() { #[test] fn category_spend_cannot_void_after_payout() { - ExtBuilder::default()/*.with_asset_balance(1, 50)*/.build().execute_with(|| { - System::set_block_number(1); + ExtBuilder::default() // .with_asset_balance(1, 50) + .build() + .execute_with(|| { + System::set_block_number(1); - let category = b"USD".to_vec(); - let bounded_category: BoundedVec> = - BoundedVec::try_from(category.clone()).unwrap(); + let category = b"USD".to_vec(); + let bounded_category: BoundedVec> = + BoundedVec::try_from(category.clone()).unwrap(); - assert_ok!(Treasury::spend( - RuntimeOrigin::signed(14), - Box::new(SpendAsset::Category(bounded_category)), - 50, - Box::new(6), - None - )); + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(14), + Box::new(SpendAsset::Category(bounded_category)), + 50, + Box::new(6), + None + )); - assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); - // Cannot void after payout - assert_noop!( - Treasury::void_spend(RuntimeOrigin::root(), 0), - Error::::AlreadyAttempted - ); - }); + // Cannot void after payout + assert_noop!( + Treasury::void_spend(RuntimeOrigin::root(), 0), + Error::::AlreadyAttempted + ); + }); } #[test] @@ -1723,7 +1710,7 @@ fn category_spend_valid_from_works() { // Can payout at valid_from System::set_block_number(3); - //set_asset_balance(1, 50); + // set_asset_balance(1, 50); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); assert_eq!(paid(6, 1), 20); From 6505a3267fdf244b33f7cdf7878ca053ab1ad74b Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Tue, 17 Feb 2026 12:48:11 +0100 Subject: [PATCH 20/21] add prdoc --- prdoc/pr_10381.prdoc | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 prdoc/pr_10381.prdoc diff --git a/prdoc/pr_10381.prdoc b/prdoc/pr_10381.prdoc new file mode 100644 index 000000000000..9a6a99e8fadb --- /dev/null +++ b/prdoc/pr_10381.prdoc @@ -0,0 +1,34 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Improve Treasury Payout of Stables + +doc: + - audience: Runtime Devs + description: | + This PR introduces category-based treasury spending, allowing the treasury to request payment from a category of assets (e.g., USD* stablecoins) rather than a single specific asset. + On payout, the requested amount is distributed across available assets within that category, optimizing for successful payment execution. + +crates: + - name: pallet-treasury + bump: major + - name: frame-support + bump: minor + - name: pallet-assets + bump: minor + - name: polkadot-runtime-common + bump: minor + - name: asset-hub-westend-runtime + bump: minor + - name: collectives-westend-runtime + bump: minor + - name: westend-runtime + bump: minor + - name: rococo-runtime + bump: minor + - name: kitchensink-runtime + bump: minor + - name: asset-hub-westend-integration-tests + bump: minor + - name: collectives-westend-integration-tests + bump: minor From f53cded51b6a833bbd06e5aa5003c6657a59f69f Mon Sep 17 00:00:00 2001 From: Doordashcon Date: Tue, 17 Feb 2026 13:06:15 +0100 Subject: [PATCH 21/21] nit prdoc --- prdoc/pr_10381.prdoc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/prdoc/pr_10381.prdoc b/prdoc/pr_10381.prdoc index 9a6a99e8fadb..47b10bf87fa4 100644 --- a/prdoc/pr_10381.prdoc +++ b/prdoc/pr_10381.prdoc @@ -16,6 +16,16 @@ crates: bump: minor - name: pallet-assets bump: minor + - name: pallet-bounties + bump: patch + - name: pallet-child-bounties + bump: patch + - name: pallet-tips + bump: patch + - name: staking-async-parachain-runtime + bump: minor + - name: staking-async-rc-runtime + bump: minor - name: polkadot-runtime-common bump: minor - name: asset-hub-westend-runtime @@ -29,6 +39,6 @@ crates: - name: kitchensink-runtime bump: minor - name: asset-hub-westend-integration-tests - bump: minor + bump: patch - name: collectives-westend-integration-tests - bump: minor + bump: patch