From b95fb06a440440bc775e0d7dd2cdef96eba53899 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 25 Nov 2025 14:42:30 +0100 Subject: [PATCH 01/14] pallet-dap: treasury burns / slashing into DAP Introduce an initial version of the Dynamic Allocation Pool (DAP). It makes possible to collect funds that would otherwise be burned, into the DAP buffer on AssetHub. On Westend AssetHub: - Treasury unspent and staking slashes are now redirected to the DAP buffer instead of being burned. - All user-initiated burns also go to the DAP buffer instead of reducing total issuance. Events: - FundsReturned is emitted only for explicit user actions (e.g., burn extrinsic via FundingSink). Automatic system operations like fee handling and slashes via OnUnbalanced do NOT emit events to avoid bloating blocks with tons of events per block. --- Cargo.lock | 20 + Cargo.toml | 2 + .../assets/asset-hub-westend/Cargo.toml | 4 + .../asset-hub-westend/src/governance/mod.rs | 2 +- .../assets/asset-hub-westend/src/lib.rs | 4 + .../assets/asset-hub-westend/src/staking.rs | 6 +- prdoc/pr_10481.prdoc | 43 ++ substrate/frame/balances/src/lib.rs | 58 ++- substrate/frame/balances/src/tests/mod.rs | 1 + substrate/frame/dap/Cargo.toml | 57 +++ substrate/frame/dap/src/lib.rs | 483 ++++++++++++++++++ substrate/frame/staking-async/Cargo.toml | 3 + .../runtimes/parachain/Cargo.toml | 4 + .../runtimes/parachain/src/governance/mod.rs | 2 +- .../runtimes/parachain/src/lib.rs | 4 + .../runtimes/parachain/src/staking.rs | 6 +- substrate/frame/staking-async/src/mock.rs | 7 +- substrate/frame/support/src/traits/tokens.rs | 2 + .../support/src/traits/tokens/funding.rs | 88 ++++ umbrella/Cargo.toml | 9 + umbrella/src/lib.rs | 4 + 21 files changed, 793 insertions(+), 16 deletions(-) create mode 100644 prdoc/pr_10481.prdoc create mode 100644 substrate/frame/dap/Cargo.toml create mode 100644 substrate/frame/dap/src/lib.rs create mode 100644 substrate/frame/support/src/traits/tokens/funding.rs diff --git a/Cargo.lock b/Cargo.lock index d129f4eea452c..9692ebac7dd80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1403,6 +1403,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-conviction-voting", + "pallet-dap", "pallet-delegated-staking", "pallet-election-provider-multi-block", "pallet-fast-unstake", @@ -12193,6 +12194,22 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-dap" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-default-config-example" version = "10.0.0" @@ -13631,6 +13648,7 @@ dependencies = [ "log", "pallet-bags-list", "pallet-balances", + "pallet-dap", "pallet-staking-async-rc-client", "parity-scale-codec", "rand 0.8.5", @@ -13711,6 +13729,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-conviction-voting", + "pallet-dap", "pallet-delegated-staking", "pallet-election-provider-multi-block", "pallet-fast-unstake", @@ -16570,6 +16589,7 @@ dependencies = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", diff --git a/Cargo.toml b/Cargo.toml index afae7745fd78d..83c71a9e1c586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -355,6 +355,7 @@ members = [ "substrate/frame/contracts/uapi", "substrate/frame/conviction-voting", "substrate/frame/core-fellowship", + "substrate/frame/dap", "substrate/frame/delegated-staking", "substrate/frame/democracy", "substrate/frame/derivatives", @@ -984,6 +985,7 @@ pallet-contracts-proc-macro = { path = "substrate/frame/contracts/proc-macro", d pallet-contracts-uapi = { path = "substrate/frame/contracts/uapi", default-features = false } pallet-conviction-voting = { path = "substrate/frame/conviction-voting", default-features = false } pallet-core-fellowship = { path = "substrate/frame/core-fellowship", default-features = false } +pallet-dap = { path = "substrate/frame/dap", default-features = false } pallet-default-config-example = { path = "substrate/frame/examples/default-config", default-features = false } pallet-delegated-staking = { path = "substrate/frame/delegated-staking", default-features = false } pallet-democracy = { path = "substrate/frame/democracy", default-features = false } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml index 852e7e1196e33..6d19f732c3a51 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -120,6 +120,7 @@ cumulus-primitives-aura = { workspace = true } cumulus-primitives-core = { workspace = true } cumulus-primitives-utility = { workspace = true } pallet-collator-selection = { workspace = true } +pallet-dap = { workspace = true } pallet-message-queue = { workspace = true } parachain-info = { workspace = true } parachains-common = { workspace = true } @@ -173,6 +174,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", "pallet-delegated-staking/runtime-benchmarks", "pallet-election-provider-multi-block/runtime-benchmarks", "pallet-fast-unstake/runtime-benchmarks", @@ -246,6 +248,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-conviction-voting/try-runtime", + "pallet-dap/try-runtime", "pallet-delegated-staking/try-runtime", "pallet-election-provider-multi-block/try-runtime", "pallet-fast-unstake/try-runtime", @@ -326,6 +329,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-conviction-voting/std", + "pallet-dap/std", "pallet-delegated-staking/std", "pallet-election-provider-multi-block/std", "pallet-fast-unstake/std", 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 3b0c006c39ce8..f7522b6c0485b 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 @@ -131,7 +131,7 @@ impl pallet_treasury::Config for Runtime { type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; type Burn = Burn; - type BurnDestination = (); + type BurnDestination = pallet_dap::BurnToDap; type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = (); 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 75c38fc9d1f36..944ac08e0d14d 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -245,6 +245,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_dap::ReturnToDap; } parameter_types! { @@ -1398,6 +1399,9 @@ construct_runtime!( Treasury: pallet_treasury = 94, AssetRate: pallet_asset_rate = 95, + // Dynamic Allocation Pool / Issuance Buffer + Dap: pallet_dap = 100, + // TODO: the pallet instance should be removed once all pools have migrated // to the new account IDs. AssetConversionMigration: pallet_asset_conversion_ops = 200, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs index 9571e38a71a3f..f3fa627b94b7c 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs @@ -278,7 +278,7 @@ impl pallet_staking_async::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type CurrencyToVote = sp_staking::currency_to_vote::SaturatingCurrencyToVote; type RewardRemainder = (); - type Slash = (); + type Slash = pallet_dap::SlashToDap; type Reward = (); type SessionsPerEra = SessionsPerEra; type BondingDuration = BondingDuration; @@ -311,6 +311,10 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +impl pallet_dap::Config for Runtime { + type Currency = Balances; +} + #[derive(Encode, Decode)] // Call indices taken from westend-next runtime. pub enum RelayChainRuntimePallets { diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc new file mode 100644 index 0000000000000..65af2f786c543 --- /dev/null +++ b/prdoc/pr_10481.prdoc @@ -0,0 +1,43 @@ +title: 'Introduce FundingSink trait and pallet-dap for AssetHub' +doc: +- audience: Runtime Dev + description: |- + This PR introduces the foundation for the Dynamic Allocation Pool (DAP) system: + + 1. **FundingSink trait** (frame-support): A new trait for returning funds to an issuance system. + Implementations can either burn directly (`DirectBurn`) or return to a buffer for reuse. + + 2. **pallet-dap**: A new pallet that implements `FundingSink` by collecting funds into a buffer + account instead of burning them. The buffer account is created via `inc_providers` at genesis + or on runtime upgrade, ensuring it can receive any amount including those below ED. + + 3. **AssetHub Westend integration**: The runtime now uses pallet-dap to redirect: + - Treasury unspent funds to the DAP buffer (via `BurnDestination`) + - Staking slashes to the DAP buffer (via `SlashToDap`) + + 4. **staking-async parachain runtime**: Updated to use DAP for slash handling. + + This is the first deliverable of the DAP system. Future PRs will add: + - pallet-dap-satellite for system chains + - Integration with other Westend system chains + - XCM-based fund transfers from satellites to the DAP buffer + - FundingSource trait for pulling funds from DAP + + The `BurnDestination` type in pallet-balances now uses the `FundingSink` trait. + Runtimes that don't integrate with DAP should use `DirectBurn` for the same behavior + as before. +crates: +- name: frame-support + bump: minor +- name: pallet-balances + bump: major +- name: pallet-dap + bump: patch +- name: polkadot-sdk + bump: minor +- name: asset-hub-westend-runtime + bump: major +- name: pallet-staking-async + bump: patch +- name: pallet-staking-async-parachain-runtime + bump: major diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index ebdb70f83dace..fba2412517eee 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -206,12 +206,22 @@ pub mod pallet { use codec::HasCompact; use frame_support::{ pallet_prelude::*, - traits::{fungible::Credit, tokens::Precision, VariantCount, VariantCountOf}, + traits::{ + fungible::Credit, + tokens::{FundingSink, Precision, Preservation}, + VariantCount, VariantCountOf, + }, }; use frame_system::pallet_prelude::*; pub type CreditOf = Credit<::AccountId, Pallet>; + /// Default implementation of `FundingSink` that burns tokens directly. + /// + /// This reduces total issuance when users call the `burn` extrinsic. + /// Used as the default for `BurnDestination` in production runtimes. + pub struct DirectBurn(PhantomData<(T, I)>); + /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. pub mod config_preludes { use super::*; @@ -245,6 +255,7 @@ pub mod pallet { type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = (); } } @@ -333,6 +344,14 @@ pub mod pallet { Self::AccountId, Self::Balance, >; + + /// Handler for user-initiated burns via the `burn` extrinsic. + /// + /// Runtimes can configure this to redirect burned funds to a buffer account + /// (e.g., DAP buffer on Asset Hub). If not specified (or set to `()`), burns + /// reduce total issuance directly. + #[pallet::no_default_bounds] + type BurnDestination: FundingSink; } /// The in-code storage version. @@ -852,8 +871,10 @@ pub mod pallet { /// If the origin's account ends up below the existential deposit as a result /// of the burn and `keep_alive` is false, the account will be reaped. /// - /// Unlike sending funds to a _burn_ address, which merely makes the funds inaccessible, - /// this `burn` operation will reduce total issuance by the amount _burned_. + /// The burned funds are handled by the runtime's configured `BurnDestination`: + /// - `DirectBurn`: burns directly, reducing total issuance. + /// - `AccumulateInSatellite`: transfers to DAP satellite account. + /// - `ReturnToDap`: transfers to DAP buffer. #[pallet::call_index(10)] #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] pub fn burn( @@ -862,14 +883,9 @@ pub mod pallet { keep_alive: bool, ) -> DispatchResult { let source = ensure_signed(origin)?; - let preservation = if keep_alive { Preserve } else { Expendable }; - >::burn_from( - &source, - value, - preservation, - Precision::Exact, - Polite, - )?; + let preservation = + if keep_alive { Preservation::Preserve } else { Preservation::Expendable }; + T::BurnDestination::return_funds(&source, value, preservation)?; Ok(()) } } @@ -1433,3 +1449,23 @@ pub mod pallet { } } } + +impl, I: 'static> frame_support::traits::tokens::FundingSink + for pallet::DirectBurn +{ + fn return_funds( + from: &T::AccountId, + amount: T::Balance, + preservation: frame_support::traits::tokens::Preservation, + ) -> DispatchResult { + use frame_support::traits::tokens::{Fortitude, Precision}; + as fungible::Mutate>::burn_from( + from, + amount, + preservation, + Precision::Exact, + Fortitude::Polite, + )?; + Ok(()) + } +} diff --git a/substrate/frame/balances/src/tests/mod.rs b/substrate/frame/balances/src/tests/mod.rs index 155f78884d122..712b5fb8d27bf 100644 --- a/substrate/frame/balances/src/tests/mod.rs +++ b/substrate/frame/balances/src/tests/mod.rs @@ -130,6 +130,7 @@ impl Config for Test { type RuntimeFreezeReason = TestId; type FreezeIdentifier = TestId; type MaxFreezes = VariantCountOf; + type BurnDestination = pallet_balances::DirectBurn; } #[derive(Clone)] diff --git a/substrate/frame/dap/Cargo.toml b/substrate/frame/dap/Cargo.toml new file mode 100644 index 0000000000000..c7493f08f504d --- /dev/null +++ b/substrate/frame/dap/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pallet-dap" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +description = "FRAME pallet for Dynamic Allocation Pool (DAP)" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { features = ["derive", "max-encoded-len"], workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +log = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +sp-runtime = { workspace = true } + +[dev-dependencies] +pallet-balances = { workspace = true, default-features = true } +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs new file mode 100644 index 0000000000000..ad62620816acd --- /dev/null +++ b/substrate/frame/dap/src/lib.rs @@ -0,0 +1,483 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Dynamic Allocation Pool (DAP) Pallet +//! +//! This pallet implements `FundingSink` to collect funds into a buffer account instead of burning +//! them. The buffer account is created via `inc_providers` at genesis or on runtime upgrade, +//! ensuring it can receive any amount including those below ED. +//! +//! Future phases will add: +//! - `FundingSource` (request_funds) for pulling funds +//! - Issuance curve and minting logic +//! - Distribution rules and scheduling + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Balanced, Credit, Inspect, Mutate}, + tokens::{Fortitude, FundingSink, Precision, Preservation}, + Currency, Imbalance, OnUnbalanced, + }, + PalletId, +}; + +pub use pallet::*; + +const LOG_TARGET: &str = "runtime::dap"; + +/// The DAP pallet ID, used to derive the buffer account. +pub const DAP_PALLET_ID: PalletId = PalletId(*b"dap/buff"); + +/// Type alias for balance. +pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::sp_runtime::traits::AccountIdConversion; + + /// The in-code storage version. + const STORAGE_VERSION: frame_support::traits::StorageVersion = + frame_support::traits::StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency type. + type Currency: Inspect + + Mutate + + Balanced; + } + + impl Pallet { + /// Get the DAP buffer account derived from the pallet ID. + pub fn buffer_account() -> T::AccountId { + DAP_PALLET_ID.into_account_truncating() + } + + /// Ensure the buffer account exists by incrementing its provider count. + /// + /// This is called at genesis and on runtime upgrade. + /// It's idempotent - calling it multiple times is safe. + pub fn ensure_buffer_account_exists() { + let buffer = Self::buffer_account(); + if !frame_system::Pallet::::account_exists(&buffer) { + frame_system::Pallet::::inc_providers(&buffer); + log::info!( + target: LOG_TARGET, + "Created DAP buffer account: {buffer:?}" + ); + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> Weight { + // Create the buffer account if it doesn't exist (for chains upgrading to DAP). + Self::ensure_buffer_account_exists(); + // Weight: 1 read (account_exists) + potentially 1 write (inc_providers) + T::DbWeight::get().reads_writes(1, 1) + } + } + + /// Genesis config for the DAP pallet. + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + #[serde(skip)] + _phantom: core::marker::PhantomData, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + // Create the buffer account at genesis so it can receive funds of any amount. + Pallet::::ensure_buffer_account_exists(); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Funds returned to DAP buffer. + FundsReturned { from: T::AccountId, amount: BalanceOf }, + } +} + +/// Implementation of FundingSink that returns funds to DAP buffer. +/// When using this, returned funds are transferred to the buffer account instead of being burned. +pub struct ReturnToDap(core::marker::PhantomData); + +impl FundingSink> for ReturnToDap { + fn return_funds( + source: &T::AccountId, + amount: BalanceOf, + preservation: Preservation, + ) -> Result<(), DispatchError> { + let buffer = Pallet::::buffer_account(); + + let credit = T::Currency::withdraw( + source, + amount, + Precision::Exact, + preservation, + Fortitude::Polite, + )?; + + // The buffer account is created at genesis or on_runtime_upgrade, so resolve should + // always succeed. If it somehow fails, log the error and let the credit drop (burn). + let _ = T::Currency::resolve(&buffer, credit).map_err(|c| { + log::error!( + target: LOG_TARGET, + "💸 Failed to resolve {:?} to DAP buffer - funds will be burned instead", + c.peek() + ); + drop(c); + }); + + Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Returned {amount:?} from {source:?} to DAP buffer" + ); + + Ok(()) + } +} + +/// Type alias for credit (negative imbalance - funds that were slashed/removed). +/// This is for the `fungible::Balanced` trait as used by staking-async. +pub type CreditOf = Credit<::AccountId, ::Currency>; + +/// Implementation of OnUnbalanced for the fungible::Balanced trait. +/// Use this as `type Slash = SlashToDap` in staking-async config. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via fee splitting). +pub struct SlashToDap(core::marker::PhantomData); + +impl OnUnbalanced> for SlashToDap { + fn on_nonzero_unbalanced(amount: CreditOf) { + let buffer = Pallet::::buffer_account(); + let numeric_amount = amount.peek(); + + // The buffer account is created at genesis or on_runtime_upgrade, so resolve should + // always succeed. If it somehow fails, log the error. + if let Err(remaining) = T::Currency::resolve(&buffer, amount) { + let remaining_amount = remaining.peek(); + if !remaining_amount.is_zero() { + log::error!( + target: LOG_TARGET, + "💸 Failed to deposit slash to DAP buffer - {remaining_amount:?} will be burned!" + ); + } + } + + log::debug!( + target: LOG_TARGET, + "Deposited slash of {numeric_amount:?} to DAP buffer" + ); + } +} + +/// Implementation of OnUnbalanced for the old Currency trait (still used by treasury). +/// Use this as `type BurnDestination = BurnToDap` e.g. in treasury config. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via fee splitting). +pub struct BurnToDap(core::marker::PhantomData<(T, C)>); + +impl OnUnbalanced for BurnToDap +where + T: Config, + C: Currency, +{ + fn on_nonzero_unbalanced(amount: C::NegativeImbalance) { + let buffer = Pallet::::buffer_account(); + let numeric_amount = amount.peek(); + + // Resolve the imbalance by depositing into the buffer account + C::resolve_creating(&buffer, amount); + + log::debug!( + target: LOG_TARGET, + "Deposited burn of {numeric_amount:?} to DAP buffer" + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + assert_noop, assert_ok, derive_impl, + sp_runtime::traits::AccountIdConversion, + traits::{ + fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, + OnUnbalanced, WithdrawReasons, + }, + }; + use sp_runtime::BuildStorage; + + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + Dap: crate, + } + ); + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; + } + + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] + impl pallet_balances::Config for Test { + type AccountStore = System; + } + + impl Config for Test { + type Currency = Balances; + } + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 200), (3, 300)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + crate::pallet::GenesisConfig::::default() + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } + + #[test] + fn buffer_account_is_derived_from_pallet_id() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + let expected: u64 = DAP_PALLET_ID.into_account_truncating(); + assert_eq!(buffer, expected); + }); + } + + #[test] + fn genesis_creates_buffer_account() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + // Buffer account should exist after genesis (created via inc_providers) + assert!(System::account_exists(&buffer)); + }); + } + + // ===== return_funds tests ===== + + #[test] + fn return_funds_accumulates_from_multiple_sources() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let buffer = Dap::buffer_account(); + + // Given: accounts have balances, buffer has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(2), 200); + assert_eq!(Balances::free_balance(3), 300); + assert_eq!(Balances::free_balance(buffer), 0); + + // When: return funds from multiple accounts + assert_ok!(ReturnToDap::::return_funds(&1, 20, Preservation::Preserve)); + assert_ok!(ReturnToDap::::return_funds(&2, 50, Preservation::Preserve)); + assert_ok!(ReturnToDap::::return_funds(&3, 100, Preservation::Preserve)); + + // Then: buffer has accumulated all returns (20 + 50 + 100 = 170) + assert_eq!(Balances::free_balance(buffer), 170); + assert_eq!(Balances::free_balance(1), 80); + assert_eq!(Balances::free_balance(2), 150); + assert_eq!(Balances::free_balance(3), 200); + + // ...and all three events are emitted + System::assert_has_event(Event::::FundsReturned { from: 1, amount: 20 }.into()); + System::assert_has_event(Event::::FundsReturned { from: 2, amount: 50 }.into()); + System::assert_has_event(Event::::FundsReturned { from: 3, amount: 100 }.into()); + }); + } + + #[test] + fn return_funds_fails_with_insufficient_balance() { + new_test_ext().execute_with(|| { + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: try to return 150 (more than balance) + // Then: fails + assert_noop!( + ReturnToDap::::return_funds(&1, 150, Preservation::Preserve), + sp_runtime::TokenError::FundsUnavailable + ); + }); + } + + #[test] + fn return_funds_with_zero_amount_succeeds() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: account 1 has 100, buffer has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); + + // When: return 0 from account 1 + assert_ok!(ReturnToDap::::return_funds(&1, 0, Preservation::Preserve)); + + // Then: balances unchanged (no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); + }); + } + + #[test] + fn return_funds_with_expendable_allows_full_drain() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let buffer = Dap::buffer_account(); + + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: return full balance with Expendable (allows going to 0) + assert_ok!(ReturnToDap::::return_funds(&1, 100, Preservation::Expendable)); + + // Then: account 1 is empty, buffer has 100 + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(buffer), 100); + }); + } + + #[test] + fn return_funds_with_preserve_respects_existential_deposit() { + new_test_ext().execute_with(|| { + // Given: account 1 has 100, ED is 1 (from TestDefaultConfig) + assert_eq!(Balances::free_balance(1), 100); + + // When: try to return 100 with Preserve (would go below ED) + // Then: fails because it would kill the account + assert_noop!( + ReturnToDap::::return_funds(&1, 100, Preservation::Preserve), + sp_runtime::TokenError::FundsUnavailable + ); + + // But returning 99 works (leaves 1 for ED) + assert_ok!(ReturnToDap::::return_funds(&1, 99, Preservation::Preserve)); + assert_eq!(Balances::free_balance(1), 1); + }); + } + + // ===== SlashToDap tests ===== + + #[test] + fn slash_to_dap_accumulates_multiple_slashes_to_buffer() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: multiple slashes occur via OnUnbalanced (simulating a staking slash) + let credit1 = >::issue(30); + SlashToDap::::on_unbalanced(credit1); + + let credit2 = >::issue(20); + SlashToDap::::on_unbalanced(credit2); + + let credit3 = >::issue(50); + SlashToDap::::on_unbalanced(credit3); + + // Then: buffer has accumulated all slashes (30 + 20 + 50 = 100) + assert_eq!(Balances::free_balance(buffer), 100); + }); + } + + #[test] + fn slash_to_dap_handles_zero_amount() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: slash with zero amount + let credit = >::issue(0); + SlashToDap::::on_unbalanced(credit); + + // Then: buffer still has 0 (no-op) + assert_eq!(Balances::free_balance(buffer), 0); + }); + } + + // ===== BurnToDap tests ===== + + #[test] + fn burn_to_dap_accumulates_multiple_burns_to_buffer() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: accounts have balances, buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: create multiple negative imbalances (simulating treasury burns) and send to DAP + let imbalance1 = >::withdraw( + &1, + 30, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + BurnToDap::::on_unbalanced(imbalance1); + + let imbalance2 = >::withdraw( + &2, + 50, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + BurnToDap::::on_unbalanced(imbalance2); + + // Then: buffer has accumulated all burns (30 + 50 = 80) + assert_eq!(Balances::free_balance(buffer), 80); + assert_eq!(Balances::free_balance(1), 70); + assert_eq!(Balances::free_balance(2), 150); + }); + } +} diff --git a/substrate/frame/staking-async/Cargo.toml b/substrate/frame/staking-async/Cargo.toml index a852d8e87571b..d3a22dfc6a902 100644 --- a/substrate/frame/staking-async/Cargo.toml +++ b/substrate/frame/staking-async/Cargo.toml @@ -42,6 +42,7 @@ frame-benchmarking = { workspace = true, default-features = true } frame-support = { features = ["experimental"], workspace = true, default-features = true } pallet-bags-list = { workspace = true, default-features = true } pallet-balances = { workspace = true, default-features = true } +pallet-dap = { workspace = true, default-features = true } rand_chacha = { workspace = true, default-features = true } sp-tracing = { workspace = true, default-features = true } substrate-test-utils = { workspace = true } @@ -79,6 +80,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", "pallet-balances/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", "pallet-staking-async-rc-client/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "sp-staking/runtime-benchmarks", @@ -89,6 +91,7 @@ try-runtime = [ "frame-system/try-runtime", "pallet-bags-list/try-runtime", "pallet-balances/try-runtime", + "pallet-dap/try-runtime", "pallet-staking-async-rc-client/try-runtime", "sp-runtime/try-runtime", ] diff --git a/substrate/frame/staking-async/runtimes/parachain/Cargo.toml b/substrate/frame/staking-async/runtimes/parachain/Cargo.toml index 990079b22a658..c4ead65e064fe 100644 --- a/substrate/frame/staking-async/runtimes/parachain/Cargo.toml +++ b/substrate/frame/staking-async/runtimes/parachain/Cargo.toml @@ -118,6 +118,7 @@ cumulus-primitives-aura = { workspace = true } cumulus-primitives-core = { workspace = true } cumulus-primitives-utility = { workspace = true } pallet-collator-selection = { workspace = true } +pallet-dap = { workspace = true } pallet-message-queue = { workspace = true } parachain-info = { workspace = true } parachains-common = { workspace = true } @@ -169,6 +170,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", "pallet-delegated-staking/runtime-benchmarks", "pallet-election-provider-multi-block/runtime-benchmarks", "pallet-fast-unstake/runtime-benchmarks", @@ -234,6 +236,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-conviction-voting/try-runtime", + "pallet-dap/try-runtime", "pallet-delegated-staking/try-runtime", "pallet-election-provider-multi-block/try-runtime", "pallet-fast-unstake/try-runtime", @@ -305,6 +308,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-conviction-voting/std", + "pallet-dap/std", "pallet-delegated-staking/std", "pallet-election-provider-multi-block/std", "pallet-fast-unstake/std", 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 6ad74378e50b6..d0239fe680e75 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -138,7 +138,7 @@ impl pallet_treasury::Config for Runtime { type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; type Burn = Burn; - type BurnDestination = (); + type BurnDestination = pallet_dap::BurnToDap; type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = (); diff --git a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs index ad42ee31662e0..1d00b8171c807 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs @@ -229,6 +229,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { @@ -1196,6 +1197,9 @@ construct_runtime!( Treasury: pallet_treasury = 96, AssetRate: pallet_asset_rate = 97, + // Dynamic Allocation Pool / Issuance buffer + Dap: pallet_dap = 98, + // Balances. Vesting: pallet_vesting = 100, diff --git a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs index b344a8e047c4d..3ab1853c827ac 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs @@ -436,7 +436,7 @@ impl pallet_staking_async::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type CurrencyToVote = sp_staking::currency_to_vote::SaturatingCurrencyToVote; type RewardRemainder = (); - type Slash = (); + type Slash = pallet_dap::SlashToDap; type Reward = (); type SessionsPerEra = SessionsPerEra; type BondingDuration = BondingDuration; @@ -470,6 +470,10 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +impl pallet_dap::Config for Runtime { + type Currency = Balances; +} + parameter_types! { pub StakingXcmDestination: Location = Location::parent(); } diff --git a/substrate/frame/staking-async/src/mock.rs b/substrate/frame/staking-async/src/mock.rs index c14e5c026accb..6bc42e03ba123 100644 --- a/substrate/frame/staking-async/src/mock.rs +++ b/substrate/frame/staking-async/src/mock.rs @@ -52,6 +52,7 @@ frame_support::construct_runtime!( Balances: pallet_balances, Staking: pallet_staking_async, VoterBagsList: pallet_bags_list::, + Dap: pallet_dap, } ); @@ -110,6 +111,10 @@ impl pallet_balances::Config for Test { type AccountStore = System; } +impl pallet_dap::Config for Test { + type Currency = Balances; +} + parameter_types! { pub static RewardRemainderUnbalanced: u128 = 0; } @@ -454,7 +459,7 @@ impl crate::pallet::pallet::Config for Test { type RcClientInterface = session_mock::Session; type CurrencyBalance = Balance; type CurrencyToVote = SaturatingCurrencyToVote; - type Slash = (); + type Slash = pallet_dap::SlashToDap; type WeightInfo = (); } diff --git a/substrate/frame/support/src/traits/tokens.rs b/substrate/frame/support/src/traits/tokens.rs index be982cd31e33a..55460aca99b97 100644 --- a/substrate/frame/support/src/traits/tokens.rs +++ b/substrate/frame/support/src/traits/tokens.rs @@ -19,6 +19,7 @@ pub mod asset_ops; pub mod currency; +pub mod funding; pub mod fungible; pub mod fungibles; pub mod imbalance; @@ -27,6 +28,7 @@ pub mod nonfungible; pub mod nonfungible_v2; pub mod nonfungibles; pub mod nonfungibles_v2; +pub use funding::{DirectBurn, FundingSink}; pub use imbalance::Imbalance; pub mod pay; pub mod transfer; diff --git a/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs new file mode 100644 index 0000000000000..d4a4d8fe5e74a --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -0,0 +1,88 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for returning funds to an issuance system. +//! +//! This module provides abstractions for returning funds (burns, slashing) in a way that can be +//! configured differently per runtime. +//! +//! Two main patterns: +//! - **Direct burn**: Traditional approach where funds are destroyed on demand +//! - **Buffer-based**: Funds are returned to a buffer for reuse + +use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; +use core::marker::PhantomData; +use sp_runtime::DispatchResult; + +/// Trait for returning funds to an issuance system. +/// +/// Implementations can either burn directly or return to a buffer for reuse. +pub trait FundingSink { + /// Return funds from the given account back to the issuance system. + /// + /// This could mean burning the funds or transferring them to a buffer account. + /// + /// # Parameters + /// - `from`: The account to take funds from + /// - `amount`: The amount to return + /// - `preservation`: Whether to preserve the source account (Preserve = keep alive, Expendable + /// = allow death) + fn return_funds( + from: &AccountId, + amount: Balance, + preservation: Preservation, + ) -> DispatchResult; +} + +/// Direct burning implementation of `FundingSink`. +/// +/// This implementation burns tokens directly when funds are returned. +/// Used for traditional burn-on-return systems (e.g., Kusama). +/// +/// # Type Parameters +/// +/// * `Currency` - The currency type that implements `Mutate` +/// * `AccountId` - The account identifier type +pub struct DirectBurn(PhantomData<(Currency, AccountId)>); + +impl FundingSink + for DirectBurn +where + Currency: fungible::Mutate, + AccountId: Eq, +{ + fn return_funds( + from: &AccountId, + amount: Currency::Balance, + preservation: Preservation, + ) -> DispatchResult { + Currency::burn_from(from, amount, preservation, Precision::Exact, Fortitude::Polite)?; + Ok(()) + } +} + +/// No-op implementation of `FundingSink` for unit type. +/// Used for testing or when no sink behavior is needed. +impl FundingSink for () { + fn return_funds( + _from: &AccountId, + _amount: Balance, + _preservation: Preservation, + ) -> DispatchResult { + Ok(()) + } +} diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 5814cd70c40b4..925236a4d4df6 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -85,6 +85,7 @@ std = [ "pallet-contracts?/std", "pallet-conviction-voting?/std", "pallet-core-fellowship?/std", + "pallet-dap?/std", "pallet-delegated-staking?/std", "pallet-democracy?/std", "pallet-derivatives?/std", @@ -281,6 +282,7 @@ runtime-benchmarks = [ "pallet-contracts?/runtime-benchmarks", "pallet-conviction-voting?/runtime-benchmarks", "pallet-core-fellowship?/runtime-benchmarks", + "pallet-dap?/runtime-benchmarks", "pallet-delegated-staking?/runtime-benchmarks", "pallet-democracy?/runtime-benchmarks", "pallet-derivatives?/runtime-benchmarks", @@ -422,6 +424,7 @@ try-runtime = [ "pallet-contracts?/try-runtime", "pallet-conviction-voting?/try-runtime", "pallet-core-fellowship?/try-runtime", + "pallet-dap?/try-runtime", "pallet-delegated-staking?/try-runtime", "pallet-democracy?/try-runtime", "pallet-derivatives?/try-runtime", @@ -635,6 +638,7 @@ runtime-full = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", @@ -1415,6 +1419,11 @@ default-features = false optional = true path = "../substrate/frame/core-fellowship" +[dependencies.pallet-dap] +default-features = false +optional = true +path = "../substrate/frame/dap" + [dependencies.pallet-delegated-staking] default-features = false optional = true diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index 125939c2edf76..bb383efc5bcd0 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -443,6 +443,10 @@ pub use pallet_conviction_voting; #[cfg(feature = "pallet-core-fellowship")] pub use pallet_core_fellowship; +/// FRAME pallet for Dynamic Allocation Pool (DAP). +#[cfg(feature = "pallet-dap")] +pub use pallet_dap; + /// FRAME delegated staking pallet. #[cfg(feature = "pallet-delegated-staking")] pub use pallet_delegated_staking; From e7ee10a7dd89cb9b62b63bfe9e1709a122bcd30d Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 23:30:05 +0100 Subject: [PATCH 02/14] prdoc --- prdoc/{pr_10481.prdoc => pr_10576.prdoc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename prdoc/{pr_10481.prdoc => pr_10576.prdoc} (100%) diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10576.prdoc similarity index 100% rename from prdoc/pr_10481.prdoc rename to prdoc/pr_10576.prdoc From 565c6b8a9fed7895a3bf0949eefafce921f1d30b Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 23:43:38 +0100 Subject: [PATCH 03/14] clippy --- substrate/frame/asset-rewards/src/mock.rs | 1 + substrate/frame/assets-freezer/src/mock.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs index 320d6c3ec4257..73ddaa9d028e8 100644 --- a/substrate/frame/asset-rewards/src/mock.rs +++ b/substrate/frame/asset-rewards/src/mock.rs @@ -72,6 +72,7 @@ impl pallet_balances::Config for MockRuntime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = (); } impl pallet_assets::Config for MockRuntime { diff --git a/substrate/frame/assets-freezer/src/mock.rs b/substrate/frame/assets-freezer/src/mock.rs index bacca601317d7..3059c2b3b290c 100644 --- a/substrate/frame/assets-freezer/src/mock.rs +++ b/substrate/frame/assets-freezer/src/mock.rs @@ -86,6 +86,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); + type BurnDestination = (); } impl pallet_assets::Config for Test { From b948817bad5b57cb96579b7a91e5840a0f689328 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 10:00:25 +0100 Subject: [PATCH 04/14] pallet-dap: make user-initiated burns burn token This is done in order to reduce the scope of the initial changes. We comment out the BurnDestination from pallet-balances config so that we avoid to introduce a breaking change for all runtimes. This is initially acceptable since burn extrinsic is rarely used. --- .../assets/asset-hub-westend/src/lib.rs | 1 - prdoc/pr_10576.prdoc | 13 ++-- substrate/frame/balances/src/lib.rs | 67 ++++++++----------- substrate/frame/balances/src/tests/mod.rs | 1 - .../runtimes/parachain/src/lib.rs | 1 - 5 files changed, 36 insertions(+), 47 deletions(-) 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 944ac08e0d14d..eda924b33f3bb 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -245,7 +245,6 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); - type BurnDestination = pallet_dap::ReturnToDap; } parameter_types! { diff --git a/prdoc/pr_10576.prdoc b/prdoc/pr_10576.prdoc index 65af2f786c543..50adac72e7d64 100644 --- a/prdoc/pr_10576.prdoc +++ b/prdoc/pr_10576.prdoc @@ -17,15 +17,20 @@ doc: 4. **staking-async parachain runtime**: Updated to use DAP for slash handling. + NOTE: User-initiated burns (via pallet_balances::burn extrinsic) do NOT go through DAP currently but + they burn directly instead, reducing total issuance immediately. + This is because `pallet_balances::BurnDestination` in the related pallet's Config is commented out. + In the moment we uncomment it, we will introduce a breaking change forcing: + - Runtimes integrating with DAP to specify DAP as BurnDestination (this makes the burn extrinsic redirect to DAP). + - All other runtimes will use `BurnDestination = DirectBurn` (this makes the burn extrinsic actually burn tokens so same behavior as before). + This was not included in this PR to keep the scope limited, especially since user-initiated burns are rare events. + This is the first deliverable of the DAP system. Future PRs will add: + - Make user-initiated burn's destination configurable - pallet-dap-satellite for system chains - Integration with other Westend system chains - XCM-based fund transfers from satellites to the DAP buffer - FundingSource trait for pulling funds from DAP - - The `BurnDestination` type in pallet-balances now uses the `FundingSink` trait. - Runtimes that don't integrate with DAP should use `DirectBurn` for the same behavior - as before. crates: - name: frame-support bump: minor diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index fba2412517eee..b661563bd7fe3 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -208,7 +208,7 @@ pub mod pallet { pallet_prelude::*, traits::{ fungible::Credit, - tokens::{FundingSink, Precision, Preservation}, + tokens::{Precision, Preservation}, VariantCount, VariantCountOf, }, }; @@ -216,12 +216,6 @@ pub mod pallet { pub type CreditOf = Credit<::AccountId, Pallet>; - /// Default implementation of `FundingSink` that burns tokens directly. - /// - /// This reduces total issuance when users call the `burn` extrinsic. - /// Used as the default for `BurnDestination` in production runtimes. - pub struct DirectBurn(PhantomData<(T, I)>); - /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. pub mod config_preludes { use super::*; @@ -255,7 +249,6 @@ pub mod pallet { type WeightInfo = (); type DoneSlashHandler = (); - type BurnDestination = (); } } @@ -345,13 +338,19 @@ pub mod pallet { Self::Balance, >; - /// Handler for user-initiated burns via the `burn` extrinsic. - /// - /// Runtimes can configure this to redirect burned funds to a buffer account - /// (e.g., DAP buffer on Asset Hub). If not specified (or set to `()`), burns - /// reduce total issuance directly. - #[pallet::no_default_bounds] - type BurnDestination: FundingSink; + // TODO(DAP): Uncomment this when we want user-initiated burns to go through DAP on + // runtimes that configure it. When uncommenting, all runtimes will need to specify + // this parameter explicitly: + // - DAP-enabled runtimes: `type BurnDestination = pallet_dap::ReturnToDap;` + // - Other runtimes: `type BurnDestination = DirectBurn;` + // + // /// Handler for user-initiated burns via the `burn` extrinsic. + // /// + // /// Runtimes can configure this to redirect burned funds to a buffer account + // /// (e.g., DAP buffer on Asset Hub). If not specified, burns reduce total issuance + // /// directly. + // #[pallet::no_default_bounds] + // type BurnDestination: FundingSink; } /// The in-code storage version. @@ -871,10 +870,11 @@ pub mod pallet { /// If the origin's account ends up below the existential deposit as a result /// of the burn and `keep_alive` is false, the account will be reaped. /// - /// The burned funds are handled by the runtime's configured `BurnDestination`: - /// - `DirectBurn`: burns directly, reducing total issuance. - /// - `AccumulateInSatellite`: transfers to DAP satellite account. - /// - `ReturnToDap`: transfers to DAP buffer. + /// Currently burns directly, reducing total issuance. + /// + /// TODO(DAP): When `BurnDestination` is uncommented in the Config trait, this should + /// use `T::BurnDestination::return_funds()` instead to allow DAP-enabled runtimes to + /// redirect user-initiated burns to the DAP buffer. #[pallet::call_index(10)] #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] pub fn burn( @@ -882,10 +882,17 @@ pub mod pallet { #[pallet::compact] value: T::Balance, keep_alive: bool, ) -> DispatchResult { + use frame_support::traits::tokens::Fortitude; let source = ensure_signed(origin)?; let preservation = if keep_alive { Preservation::Preserve } else { Preservation::Expendable }; - T::BurnDestination::return_funds(&source, value, preservation)?; + >::burn_from( + &source, + value, + preservation, + Precision::Exact, + Fortitude::Polite, + )?; Ok(()) } } @@ -1449,23 +1456,3 @@ pub mod pallet { } } } - -impl, I: 'static> frame_support::traits::tokens::FundingSink - for pallet::DirectBurn -{ - fn return_funds( - from: &T::AccountId, - amount: T::Balance, - preservation: frame_support::traits::tokens::Preservation, - ) -> DispatchResult { - use frame_support::traits::tokens::{Fortitude, Precision}; - as fungible::Mutate>::burn_from( - from, - amount, - preservation, - Precision::Exact, - Fortitude::Polite, - )?; - Ok(()) - } -} diff --git a/substrate/frame/balances/src/tests/mod.rs b/substrate/frame/balances/src/tests/mod.rs index 712b5fb8d27bf..155f78884d122 100644 --- a/substrate/frame/balances/src/tests/mod.rs +++ b/substrate/frame/balances/src/tests/mod.rs @@ -130,7 +130,6 @@ impl Config for Test { type RuntimeFreezeReason = TestId; type FreezeIdentifier = TestId; type MaxFreezes = VariantCountOf; - type BurnDestination = pallet_balances::DirectBurn; } #[derive(Clone)] diff --git a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs index 1d00b8171c807..4b13091ce18c5 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs @@ -229,7 +229,6 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); - type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { From e6f62438562d1c06bdc4ab5ec299d7db93461036 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 10:15:44 +0100 Subject: [PATCH 05/14] clippy --- substrate/frame/asset-rewards/src/mock.rs | 1 - substrate/frame/assets-freezer/src/mock.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs index 73ddaa9d028e8..320d6c3ec4257 100644 --- a/substrate/frame/asset-rewards/src/mock.rs +++ b/substrate/frame/asset-rewards/src/mock.rs @@ -72,7 +72,6 @@ impl pallet_balances::Config for MockRuntime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); - type BurnDestination = (); } impl pallet_assets::Config for MockRuntime { diff --git a/substrate/frame/assets-freezer/src/mock.rs b/substrate/frame/assets-freezer/src/mock.rs index 3059c2b3b290c..bacca601317d7 100644 --- a/substrate/frame/assets-freezer/src/mock.rs +++ b/substrate/frame/assets-freezer/src/mock.rs @@ -86,7 +86,6 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); - type BurnDestination = (); } impl pallet_assets::Config for Test { From 8216f8b639385c691953bf2e98c8b9a925691ac5 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 10:26:53 +0100 Subject: [PATCH 06/14] pallet-dap: make PalletId configurable by runtime --- .../assets/asset-hub-westend/src/staking.rs | 5 ++++ substrate/frame/dap/src/lib.rs | 30 +++++++++---------- .../runtimes/parachain/src/staking.rs | 5 ++++ substrate/frame/staking-async/src/mock.rs | 5 ++++ 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs index f3fa627b94b7c..503a7323d80e1 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs @@ -311,8 +311,13 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + impl pallet_dap::Config for Runtime { type Currency = Balances; + type PalletId = DapPalletId; } #[derive(Encode, Decode)] diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index ad62620816acd..afbe225aeaa8b 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -44,9 +44,6 @@ pub use pallet::*; const LOG_TARGET: &str = "runtime::dap"; -/// The DAP pallet ID, used to derive the buffer account. -pub const DAP_PALLET_ID: PalletId = PalletId(*b"dap/buff"); - /// Type alias for balance. pub type BalanceOf = <::Currency as Inspect<::AccountId>>::Balance; @@ -70,12 +67,19 @@ pub mod pallet { type Currency: Inspect + Mutate + Balanced; + + /// The pallet ID used to derive the buffer account. + /// + /// Each runtime should configure a unique ID to avoid collisions if multiple + /// DAP instances are used. + #[pallet::constant] + type PalletId: Get; } impl Pallet { - /// Get the DAP buffer account derived from the pallet ID. + /// Get the DAP buffer account pub fn buffer_account() -> T::AccountId { - DAP_PALLET_ID.into_account_truncating() + T::PalletId::get().into_account_truncating() } /// Ensure the buffer account exists by incrementing its provider count. @@ -235,7 +239,7 @@ where mod tests { use super::*; use frame_support::{ - assert_noop, assert_ok, derive_impl, + assert_noop, assert_ok, derive_impl, parameter_types, sp_runtime::traits::AccountIdConversion, traits::{ fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, @@ -265,8 +269,13 @@ mod tests { type AccountStore = System; } + parameter_types! { + pub const DapPalletId: PalletId = PalletId(*b"dap/buff"); + } + impl Config for Test { type Currency = Balances; + type PalletId = DapPalletId; } fn new_test_ext() -> sp_io::TestExternalities { @@ -283,15 +292,6 @@ mod tests { t.into() } - #[test] - fn buffer_account_is_derived_from_pallet_id() { - new_test_ext().execute_with(|| { - let buffer = Dap::buffer_account(); - let expected: u64 = DAP_PALLET_ID.into_account_truncating(); - assert_eq!(buffer, expected); - }); - } - #[test] fn genesis_creates_buffer_account() { new_test_ext().execute_with(|| { diff --git a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs index 3ab1853c827ac..2778d82639d70 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs @@ -470,8 +470,13 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + impl pallet_dap::Config for Runtime { type Currency = Balances; + type PalletId = DapPalletId; } parameter_types! { diff --git a/substrate/frame/staking-async/src/mock.rs b/substrate/frame/staking-async/src/mock.rs index 6bc42e03ba123..5bdd7acd4a6b4 100644 --- a/substrate/frame/staking-async/src/mock.rs +++ b/substrate/frame/staking-async/src/mock.rs @@ -111,8 +111,13 @@ impl pallet_balances::Config for Test { type AccountStore = System; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + impl pallet_dap::Config for Test { type Currency = Balances; + type PalletId = DapPalletId; } parameter_types! { From 708685e2941ef4269ffa0ac1cb11a62ea3ba923d Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 10:32:19 +0100 Subject: [PATCH 07/14] pallet-dap: fix warning --- substrate/frame/dap/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index afbe225aeaa8b..c9ad9ba3052e2 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -78,6 +78,8 @@ pub mod pallet { impl Pallet { /// Get the DAP buffer account + /// NOTE: We may need more accounts in the future, for instance, to manage the strategic + /// reserve. We will add them as necessary, generating them with additional seed. pub fn buffer_account() -> T::AccountId { T::PalletId::get().into_account_truncating() } @@ -240,7 +242,6 @@ mod tests { use super::*; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, - sp_runtime::traits::AccountIdConversion, traits::{ fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, OnUnbalanced, WithdrawReasons, From b7fab2e068a924a70f889a0c797922a2bb87cd09 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 10:45:57 +0100 Subject: [PATCH 08/14] pallet-dap: make return_funds infallible --- substrate/frame/dap/src/lib.rs | 87 ++++++++----------- .../support/src/traits/tokens/funding.rs | 29 ++----- 2 files changed, 45 insertions(+), 71 deletions(-) diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index c9ad9ba3052e2..2d4e3e3153eab 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -139,40 +139,18 @@ pub mod pallet { pub struct ReturnToDap(core::marker::PhantomData); impl FundingSink> for ReturnToDap { - fn return_funds( - source: &T::AccountId, - amount: BalanceOf, - preservation: Preservation, - ) -> Result<(), DispatchError> { + fn return_funds(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { let buffer = Pallet::::buffer_account(); - let credit = T::Currency::withdraw( - source, - amount, - Precision::Exact, - preservation, - Fortitude::Polite, - )?; - - // The buffer account is created at genesis or on_runtime_upgrade, so resolve should - // always succeed. If it somehow fails, log the error and let the credit drop (burn). - let _ = T::Currency::resolve(&buffer, credit).map_err(|c| { - log::error!( - target: LOG_TARGET, - "💸 Failed to resolve {:?} to DAP buffer - funds will be burned instead", - c.peek() - ); - drop(c); - }); - - Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); - - log::debug!( - target: LOG_TARGET, - "Returned {amount:?} from {source:?} to DAP buffer" - ); - - Ok(()) + // Withdraw from source, resolve to buffer, emit event. If withdraw fails, nothing happens. + // If resolve fails (should never happen - buffer pre-created at genesis or via runtime + // upgrade), funds are burned. + T::Currency::withdraw(source, amount, Precision::Exact, preservation, Fortitude::Polite) + .ok() + .map(|credit| T::Currency::resolve(&buffer, credit)) + .map(|_| { + Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }) + }); } } @@ -241,7 +219,7 @@ where mod tests { use super::*; use frame_support::{ - assert_noop, assert_ok, derive_impl, parameter_types, + derive_impl, parameter_types, traits::{ fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, OnUnbalanced, WithdrawReasons, @@ -317,9 +295,9 @@ mod tests { assert_eq!(Balances::free_balance(buffer), 0); // When: return funds from multiple accounts - assert_ok!(ReturnToDap::::return_funds(&1, 20, Preservation::Preserve)); - assert_ok!(ReturnToDap::::return_funds(&2, 50, Preservation::Preserve)); - assert_ok!(ReturnToDap::::return_funds(&3, 100, Preservation::Preserve)); + ReturnToDap::::return_funds(&1, 20, Preservation::Preserve); + ReturnToDap::::return_funds(&2, 50, Preservation::Preserve); + ReturnToDap::::return_funds(&3, 100, Preservation::Preserve); // Then: buffer has accumulated all returns (20 + 50 + 100 = 170) assert_eq!(Balances::free_balance(buffer), 170); @@ -335,17 +313,20 @@ mod tests { } #[test] - fn return_funds_fails_with_insufficient_balance() { + fn return_funds_with_insufficient_balance_is_noop() { new_test_ext().execute_with(|| { - // Given: account 1 has 100 + let buffer = Dap::buffer_account(); + + // Given: account 1 has 100, buffer has 0 assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); // When: try to return 150 (more than balance) - // Then: fails - assert_noop!( - ReturnToDap::::return_funds(&1, 150, Preservation::Preserve), - sp_runtime::TokenError::FundsUnavailable - ); + ReturnToDap::::return_funds(&1, 150, Preservation::Preserve); + + // Then: balances unchanged (infallible no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); }); } @@ -359,7 +340,7 @@ mod tests { assert_eq!(Balances::free_balance(buffer), 0); // When: return 0 from account 1 - assert_ok!(ReturnToDap::::return_funds(&1, 0, Preservation::Preserve)); + ReturnToDap::::return_funds(&1, 0, Preservation::Preserve); // Then: balances unchanged (no-op) assert_eq!(Balances::free_balance(1), 100); @@ -377,7 +358,7 @@ mod tests { assert_eq!(Balances::free_balance(1), 100); // When: return full balance with Expendable (allows going to 0) - assert_ok!(ReturnToDap::::return_funds(&1, 100, Preservation::Expendable)); + ReturnToDap::::return_funds(&1, 100, Preservation::Expendable); // Then: account 1 is empty, buffer has 100 assert_eq!(Balances::free_balance(1), 0); @@ -388,19 +369,23 @@ mod tests { #[test] fn return_funds_with_preserve_respects_existential_deposit() { new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + // Given: account 1 has 100, ED is 1 (from TestDefaultConfig) assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); // When: try to return 100 with Preserve (would go below ED) - // Then: fails because it would kill the account - assert_noop!( - ReturnToDap::::return_funds(&1, 100, Preservation::Preserve), - sp_runtime::TokenError::FundsUnavailable - ); + ReturnToDap::::return_funds(&1, 100, Preservation::Preserve); + + // Then: balances unchanged (infallible - would have killed account) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); // But returning 99 works (leaves 1 for ED) - assert_ok!(ReturnToDap::::return_funds(&1, 99, Preservation::Preserve)); + ReturnToDap::::return_funds(&1, 99, Preservation::Preserve); assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::free_balance(buffer), 99); }); } diff --git a/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs index d4a4d8fe5e74a..571e9b2306380 100644 --- a/substrate/frame/support/src/traits/tokens/funding.rs +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -26,26 +26,23 @@ use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; use core::marker::PhantomData; -use sp_runtime::DispatchResult; /// Trait for returning funds to an issuance system. /// /// Implementations can either burn directly or return to a buffer for reuse. +/// This trait is infallible - implementations must handle any errors internally. pub trait FundingSink { /// Return funds from the given account back to the issuance system. /// /// This could mean burning the funds or transferring them to a buffer account. + /// The operation is infallible - any errors are handled internally. /// /// # Parameters /// - `from`: The account to take funds from /// - `amount`: The amount to return /// - `preservation`: Whether to preserve the source account (Preserve = keep alive, Expendable /// = allow death) - fn return_funds( - from: &AccountId, - amount: Balance, - preservation: Preservation, - ) -> DispatchResult; + fn return_funds(from: &AccountId, amount: Balance, preservation: Preservation); } /// Direct burning implementation of `FundingSink`. @@ -65,24 +62,16 @@ where Currency: fungible::Mutate, AccountId: Eq, { - fn return_funds( - from: &AccountId, - amount: Currency::Balance, - preservation: Preservation, - ) -> DispatchResult { - Currency::burn_from(from, amount, preservation, Precision::Exact, Fortitude::Polite)?; - Ok(()) + fn return_funds(from: &AccountId, amount: Currency::Balance, preservation: Preservation) { + // Best-effort burn. If it fails (e.g., insufficient funds), the funds remain with the + // account. + let _ = + Currency::burn_from(from, amount, preservation, Precision::Exact, Fortitude::Polite); } } /// No-op implementation of `FundingSink` for unit type. /// Used for testing or when no sink behavior is needed. impl FundingSink for () { - fn return_funds( - _from: &AccountId, - _amount: Balance, - _preservation: Preservation, - ) -> DispatchResult { - Ok(()) - } + fn return_funds(_from: &AccountId, _amount: Balance, _preservation: Preservation) {} } From 1203b8fb2364a3af3ff03dae8a32e4cda06d54a3 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 11:00:19 +0100 Subject: [PATCH 09/14] pallet-dap: rename return_funds into fill --- substrate/frame/dap/src/lib.rs | 48 +++++++++---------- .../support/src/traits/tokens/funding.rs | 20 ++++---- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index 2d4e3e3153eab..42b6317e0dad7 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -134,12 +134,12 @@ pub mod pallet { } } -/// Implementation of FundingSink that returns funds to DAP buffer. -/// When using this, returned funds are transferred to the buffer account instead of being burned. +/// Implementation of FundingSink that fills the DAP buffer. +/// Funds are transferred to the buffer account instead of being burned. pub struct ReturnToDap(core::marker::PhantomData); impl FundingSink> for ReturnToDap { - fn return_funds(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { + fn fill(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { let buffer = Pallet::::buffer_account(); // Withdraw from source, resolve to buffer, emit event. If withdraw fails, nothing happens. @@ -280,10 +280,10 @@ mod tests { }); } - // ===== return_funds tests ===== + // ===== fill tests ===== #[test] - fn return_funds_accumulates_from_multiple_sources() { + fn fill_accumulates_from_multiple_sources() { new_test_ext().execute_with(|| { System::set_block_number(1); let buffer = Dap::buffer_account(); @@ -294,12 +294,12 @@ mod tests { assert_eq!(Balances::free_balance(3), 300); assert_eq!(Balances::free_balance(buffer), 0); - // When: return funds from multiple accounts - ReturnToDap::::return_funds(&1, 20, Preservation::Preserve); - ReturnToDap::::return_funds(&2, 50, Preservation::Preserve); - ReturnToDap::::return_funds(&3, 100, Preservation::Preserve); + // When: fill buffer from multiple accounts + ReturnToDap::::fill(&1, 20, Preservation::Preserve); + ReturnToDap::::fill(&2, 50, Preservation::Preserve); + ReturnToDap::::fill(&3, 100, Preservation::Preserve); - // Then: buffer has accumulated all returns (20 + 50 + 100 = 170) + // Then: buffer has accumulated all fills (20 + 50 + 100 = 170) assert_eq!(Balances::free_balance(buffer), 170); assert_eq!(Balances::free_balance(1), 80); assert_eq!(Balances::free_balance(2), 150); @@ -313,7 +313,7 @@ mod tests { } #[test] - fn return_funds_with_insufficient_balance_is_noop() { + fn fill_with_insufficient_balance_is_noop() { new_test_ext().execute_with(|| { let buffer = Dap::buffer_account(); @@ -321,8 +321,8 @@ mod tests { assert_eq!(Balances::free_balance(1), 100); assert_eq!(Balances::free_balance(buffer), 0); - // When: try to return 150 (more than balance) - ReturnToDap::::return_funds(&1, 150, Preservation::Preserve); + // When: try to fill 150 (more than balance) + ReturnToDap::::fill(&1, 150, Preservation::Preserve); // Then: balances unchanged (infallible no-op) assert_eq!(Balances::free_balance(1), 100); @@ -331,7 +331,7 @@ mod tests { } #[test] - fn return_funds_with_zero_amount_succeeds() { + fn fill_with_zero_amount_succeeds() { new_test_ext().execute_with(|| { let buffer = Dap::buffer_account(); @@ -339,8 +339,8 @@ mod tests { assert_eq!(Balances::free_balance(1), 100); assert_eq!(Balances::free_balance(buffer), 0); - // When: return 0 from account 1 - ReturnToDap::::return_funds(&1, 0, Preservation::Preserve); + // When: fill 0 from account 1 + ReturnToDap::::fill(&1, 0, Preservation::Preserve); // Then: balances unchanged (no-op) assert_eq!(Balances::free_balance(1), 100); @@ -349,7 +349,7 @@ mod tests { } #[test] - fn return_funds_with_expendable_allows_full_drain() { + fn fill_with_expendable_allows_full_drain() { new_test_ext().execute_with(|| { System::set_block_number(1); let buffer = Dap::buffer_account(); @@ -357,8 +357,8 @@ mod tests { // Given: account 1 has 100 assert_eq!(Balances::free_balance(1), 100); - // When: return full balance with Expendable (allows going to 0) - ReturnToDap::::return_funds(&1, 100, Preservation::Expendable); + // When: fill full balance with Expendable (allows going to 0) + ReturnToDap::::fill(&1, 100, Preservation::Expendable); // Then: account 1 is empty, buffer has 100 assert_eq!(Balances::free_balance(1), 0); @@ -367,7 +367,7 @@ mod tests { } #[test] - fn return_funds_with_preserve_respects_existential_deposit() { + fn fill_with_preserve_respects_existential_deposit() { new_test_ext().execute_with(|| { let buffer = Dap::buffer_account(); @@ -375,15 +375,15 @@ mod tests { assert_eq!(Balances::free_balance(1), 100); assert_eq!(Balances::free_balance(buffer), 0); - // When: try to return 100 with Preserve (would go below ED) - ReturnToDap::::return_funds(&1, 100, Preservation::Preserve); + // When: try to fill 100 with Preserve (would go below ED) + ReturnToDap::::fill(&1, 100, Preservation::Preserve); // Then: balances unchanged (infallible - would have killed account) assert_eq!(Balances::free_balance(1), 100); assert_eq!(Balances::free_balance(buffer), 0); - // But returning 99 works (leaves 1 for ED) - ReturnToDap::::return_funds(&1, 99, Preservation::Preserve); + // But filling 99 works (leaves 1 for ED) + ReturnToDap::::fill(&1, 99, Preservation::Preserve); assert_eq!(Balances::free_balance(1), 1); assert_eq!(Balances::free_balance(buffer), 99); }); diff --git a/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs index 571e9b2306380..f9da65a3efdeb 100644 --- a/substrate/frame/support/src/traits/tokens/funding.rs +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -27,28 +27,30 @@ use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; use core::marker::PhantomData; -/// Trait for returning funds to an issuance system. +/// Trait for moving funds into an issuance buffer or burning them. /// -/// Implementations can either burn directly or return to a buffer for reuse. +/// Implementations can either burn directly or transfer to a buffer for reuse. /// This trait is infallible - implementations must handle any errors internally. +/// +/// Pairs with future `FundingSource::drain()` for withdrawing from the buffer. pub trait FundingSink { - /// Return funds from the given account back to the issuance system. + /// Fill the sink with funds from the given account. /// /// This could mean burning the funds or transferring them to a buffer account. /// The operation is infallible - any errors are handled internally. /// /// # Parameters /// - `from`: The account to take funds from - /// - `amount`: The amount to return + /// - `amount`: The amount to fill /// - `preservation`: Whether to preserve the source account (Preserve = keep alive, Expendable /// = allow death) - fn return_funds(from: &AccountId, amount: Balance, preservation: Preservation); + fn fill(from: &AccountId, amount: Balance, preservation: Preservation); } /// Direct burning implementation of `FundingSink`. /// -/// This implementation burns tokens directly when funds are returned. -/// Used for traditional burn-on-return systems (e.g., Kusama). +/// This implementation burns tokens directly, reducing total issuance. +/// Used for traditional burn systems (e.g., Kusama). /// /// # Type Parameters /// @@ -62,7 +64,7 @@ where Currency: fungible::Mutate, AccountId: Eq, { - fn return_funds(from: &AccountId, amount: Currency::Balance, preservation: Preservation) { + fn fill(from: &AccountId, amount: Currency::Balance, preservation: Preservation) { // Best-effort burn. If it fails (e.g., insufficient funds), the funds remain with the // account. let _ = @@ -73,5 +75,5 @@ where /// No-op implementation of `FundingSink` for unit type. /// Used for testing or when no sink behavior is needed. impl FundingSink for () { - fn return_funds(_from: &AccountId, _amount: Balance, _preservation: Preservation) {} + fn fill(_from: &AccountId, _amount: Balance, _preservation: Preservation) {} } From b4d2125512a274d6b47a55b126831052ffaaa980 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 11:11:37 +0100 Subject: [PATCH 10/14] Improve prdoc --- prdoc/pr_10576.prdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prdoc/pr_10576.prdoc b/prdoc/pr_10576.prdoc index 50adac72e7d64..b0a1822489400 100644 --- a/prdoc/pr_10576.prdoc +++ b/prdoc/pr_10576.prdoc @@ -26,7 +26,7 @@ doc: This was not included in this PR to keep the scope limited, especially since user-initiated burns are rare events. This is the first deliverable of the DAP system. Future PRs will add: - - Make user-initiated burn's destination configurable + - configurable destination for user-initiated burns - pallet-dap-satellite for system chains - Integration with other Westend system chains - XCM-based fund transfers from satellites to the DAP buffer From 73b1fcc1d02ca8ece6cccd7376984f703004bf32 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 11:56:14 +0100 Subject: [PATCH 11/14] ahm-test: verify that slashed funds are sent to DAP and not burned. --- Cargo.lock | 1 + substrate/frame/staking-async/ahm-test/Cargo.toml | 1 + .../frame/staking-async/ahm-test/src/ah/mock.rs | 13 ++++++++++++- .../frame/staking-async/ahm-test/src/ah/test.rs | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9692ebac7dd80..94ebe4064edf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11414,6 +11414,7 @@ dependencies = [ "log", "pallet-authorship", "pallet-balances", + "pallet-dap", "pallet-election-provider-multi-block", "pallet-offences", "pallet-root-offences", diff --git a/substrate/frame/staking-async/ahm-test/Cargo.toml b/substrate/frame/staking-async/ahm-test/Cargo.toml index 424a8d93f66c3..30c6ea62132e3 100644 --- a/substrate/frame/staking-async/ahm-test/Cargo.toml +++ b/substrate/frame/staking-async/ahm-test/Cargo.toml @@ -31,6 +31,7 @@ pallet-balances = { workspace = true, default-features = true } # pallets that we need in AH frame-election-provider-support = { workspace = true, default-features = true } +pallet-dap = { workspace = true, default-features = true } pallet-election-provider-multi-block = { workspace = true, default-features = true } pallet-staking-async = { workspace = true, default-features = true } pallet-staking-async-rc-client = { workspace = true, default-features = true } diff --git a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs index 1996b14be3487..7ba22bbb67d28 100644 --- a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs +++ b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs @@ -45,6 +45,8 @@ construct_runtime! { MultiBlockVerifier: multi_block::verifier, MultiBlockSigned: multi_block::signed, MultiBlockUnsigned: multi_block::unsigned, + + Dap: pallet_dap, } } @@ -445,7 +447,7 @@ impl pallet_staking_async::Config for Runtime { type EventListeners = (); type Reward = (); type RewardRemainder = (); - type Slash = (); + type Slash = pallet_dap::SlashToDap; type SlashDeferDuration = SlashDeferredDuration; type MaxEraDuration = (); type MaxPruningItems = MaxPruningItems; @@ -474,6 +476,15 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ValidatorSetExportSession; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + +impl pallet_dap::Config for Runtime { + type Currency = Balances; + type PalletId = DapPalletId; +} + parameter_types! { pub static NextRelayDeliveryFails: bool = false; } diff --git a/substrate/frame/staking-async/ahm-test/src/ah/test.rs b/substrate/frame/staking-async/ahm-test/src/ah/test.rs index a47de37dea643..45b8d7ec8ec35 100644 --- a/substrate/frame/staking-async/ahm-test/src/ah/test.rs +++ b/substrate/frame/staking-async/ahm-test/src/ah/test.rs @@ -717,6 +717,11 @@ fn on_offence_current_era_instant_apply() { // flush the events. let _ = staking_events_since_last_call(); + // Record initial state for DAP verification + let dap_buffer = pallet_dap::Pallet::::buffer_account(); + let initial_dap_balance = Balances::free_balance(&dap_buffer); + let initial_total_issuance = Balances::total_issuance(); + assert_ok!(rc_client::Pallet::::relay_new_offence_paged( RuntimeOrigin::root(), vec![ @@ -783,6 +788,15 @@ fn on_offence_current_era_instant_apply() { staking_async::Event::Slashed { staker: 3, amount: 50 } ] ); + + // DAP verification: slashed funds (50 + 50 + 50 = 150) should go to buffer + let final_dap_balance = Balances::free_balance(&dap_buffer); + let final_total_issuance = Balances::total_issuance(); + + // DAP buffer should have received all slashed funds + assert_eq!(final_dap_balance, initial_dap_balance + 150); + // Total issuance should be preserved (funds not burned) + assert_eq!(final_total_issuance, initial_total_issuance); }); } From 3a62099544d9cf8601a0e7470a3fffce34ce0548 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 14:53:32 +0100 Subject: [PATCH 12/14] pallet-treasury: test burn to DAP scenario --- Cargo.lock | 1 + substrate/frame/treasury/Cargo.toml | 1 + substrate/frame/treasury/src/tests.rs | 112 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 94ebe4064edf9..3d1f30dbedc1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14173,6 +14173,7 @@ dependencies = [ "impl-trait-for-tuples", "log", "pallet-balances", + "pallet-dap", "pallet-utility", "parity-scale-codec", "scale-info", diff --git a/substrate/frame/treasury/Cargo.toml b/substrate/frame/treasury/Cargo.toml index c52a98b26dcf2..44c044b13634d 100644 --- a/substrate/frame/treasury/Cargo.toml +++ b/substrate/frame/treasury/Cargo.toml @@ -30,6 +30,7 @@ sp-core = { optional = true, workspace = true } sp-runtime = { workspace = true } [dev-dependencies] +pallet-dap = { workspace = true, default-features = true } pallet-utility = { workspace = true, default-features = true } sp-io = { workspace = true, default-features = true } diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index f2abaa15be90a..234e0fc85811f 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -1046,3 +1046,115 @@ fn multiple_spend_periods_work() { assert_eq!(LastSpendPeriod::::get(), Some(8)); }); } + +/// Tests for BurnDestination = BurnToDap configuration. +/// This module creates a separate mock runtime with DAP integration, reusing +/// most configuration from the parent `Test` runtime. +mod burn_to_dap_tests { + use super::*; + use frame_support::traits::{NeverEnsureOrigin, OnInitialize}; + + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum TestWithDap { + System: frame_system, + Balances: pallet_balances, + Treasury: treasury, + Dap: pallet_dap, + } + ); + + // Reuse frame_system and pallet_balances configs from parent (same as Test) + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for TestWithDap { + type AccountId = u128; + type Lookup = IdentityLookup; + type Block = Block; + type AccountData = pallet_balances::AccountData; + } + + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] + impl pallet_balances::Config for TestWithDap { + type AccountStore = System; + } + + // DAP-specific config + parameter_types! { + pub const DapPalletId: PalletId = PalletId(*b"dap/buff"); + } + + impl pallet_dap::Config for TestWithDap { + type Currency = Balances; + type PalletId = DapPalletId; + } + + // Treasury config: same as Test except BurnDestination redirects to DAP + impl Config for TestWithDap { + type PalletId = TreasuryPalletId; + type Currency = Balances; + type RejectOrigin = frame_system::EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type SpendPeriod = ConstU64<2>; + type Burn = Burn; + type BurnDestination = pallet_dap::BurnToDap; + type WeightInfo = (); + type SpendFunds = (); + type MaxApprovals = ConstU32<100>; + type SpendOrigin = NeverEnsureOrigin; + type AssetKind = u32; + type Beneficiary = u128; + type BeneficiaryLookup = IdentityLookup; + type Paymaster = TestPay; + type BalanceConverter = MulBy>; + type PayoutPeriod = SpendPayoutPeriod; + type BlockNumberProvider = System; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); + } + + fn go_to_block_dap(n: u64) { + ::BlockNumberProvider::set_block_number(n); + >::on_initialize(n); + } + + #[test] + fn treasury_burn_redirected_to_dap_buffer() { + ExtBuilder::default().build().execute_with(|| { + let dap_buffer = pallet_dap::Pallet::::buffer_account(); + + pallet_dap::Pallet::::ensure_buffer_account_exists(); + + let initial_total_issuance = pallet_balances::TotalIssuance::::get(); + + // Given: Treasury has 1025 (1024 pot + 1 ED), DAP buffer is empty + Balances::make_free_balance_be(&Treasury::account_id(), 1025); + assert_eq!(Treasury::pot(), 1024); + assert_eq!(Balances::free_balance(&dap_buffer), 0); + + // When: first spend period passes + go_to_block_dap(2); + // Then: 50% burned (512), pot = 512, DAP = 512 + assert_eq!(Treasury::pot(), 512); + assert_eq!(Balances::free_balance(&dap_buffer), 512); + + // When: second spend period passes + go_to_block_dap(4); + // Then: 50% of 512 burned (256), pot = 256, DAP = 512 + 256 = 768 + assert_eq!(Treasury::pot(), 256); + assert_eq!(Balances::free_balance(&dap_buffer), 768); + + // When: third spend period passes + go_to_block_dap(6); + // Then: 50% of 256 burned (128), pot = 128, DAP = 768 + 128 = 896 + assert_eq!(Treasury::pot(), 128); + assert_eq!(Balances::free_balance(&dap_buffer), 896); + + // And: total issuance is preserved throughout + assert_eq!( + pallet_balances::TotalIssuance::::get(), + initial_total_issuance + 1024 + ); + }); + } +} From 164732f9e937218a4a26c5fb88c6940caa303fd6 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 16:45:44 +0100 Subject: [PATCH 13/14] westend runtimes: remove treasury burn --- Cargo.lock | 1 - .../asset-hub-westend/src/governance/mod.rs | 2 +- polkadot/runtime/westend/src/lib.rs | 2 +- prdoc/pr_10576.prdoc | 28 +++-- .../runtimes/parachain/src/governance/mod.rs | 2 +- .../staking-async/runtimes/rc/src/lib.rs | 2 +- substrate/frame/treasury/Cargo.toml | 1 - substrate/frame/treasury/src/tests.rs | 112 ------------------ 8 files changed, 19 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d1f30dbedc1b..94ebe4064edf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14173,7 +14173,6 @@ dependencies = [ "impl-trait-for-tuples", "log", "pallet-balances", - "pallet-dap", "pallet-utility", "parity-scale-codec", "scale-info", 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 f7522b6c0485b..9eb1535f55211 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 @@ -106,7 +106,7 @@ impl pallet_referenda::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index b39e53ad75e91..dba9eba840958 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -923,7 +923,7 @@ impl pallet_fast_unstake::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; // The asset's interior location for the paying account. This is the Treasury diff --git a/prdoc/pr_10576.prdoc b/prdoc/pr_10576.prdoc index b0a1822489400..662f65d5750b7 100644 --- a/prdoc/pr_10576.prdoc +++ b/prdoc/pr_10576.prdoc @@ -11,26 +11,24 @@ doc: account instead of burning them. The buffer account is created via `inc_providers` at genesis or on runtime upgrade, ensuring it can receive any amount including those below ED. - 3. **AssetHub Westend integration**: The runtime now uses pallet-dap to redirect: - - Treasury unspent funds to the DAP buffer (via `BurnDestination`) - - Staking slashes to the DAP buffer (via `SlashToDap`) + 3. **AssetHub Westend integration**: The runtime now uses pallet-dap to redirect staking slashes + to the DAP buffer (via `SlashToDap`). - 4. **staking-async parachain runtime**: Updated to use DAP for slash handling. + **Treasury burns are now disabled** so no need to integrate with DAP: treasury `Burn` parameter is set + to zero in Westend RC, AssetHub and collective runtimes. This means no treasury funds are burned at + the end of spend periods, preserving total issuance. - NOTE: User-initiated burns (via pallet_balances::burn extrinsic) do NOT go through DAP currently but - they burn directly instead, reducing total issuance immediately. - This is because `pallet_balances::BurnDestination` in the related pallet's Config is commented out. - In the moment we uncomment it, we will introduce a breaking change forcing: - - Runtimes integrating with DAP to specify DAP as BurnDestination (this makes the burn extrinsic redirect to DAP). - - All other runtimes will use `BurnDestination = DirectBurn` (this makes the burn extrinsic actually burn tokens so same behavior as before). - This was not included in this PR to keep the scope limited, especially since user-initiated burns are rare events. + NOTE: User-initiated burns (via pallet_balances::burn extrinsic) do NOT go through DAP currently + but they burn directly instead, reducing total issuance immediately. This is the first deliverable of the DAP system. Future PRs will add: - - configurable destination for user-initiated burns - pallet-dap-satellite for system chains - Integration with other Westend system chains - XCM-based fund transfers from satellites to the DAP buffer - - FundingSource trait for pulling funds from DAP + - A FundingSource trait to pull funds from DAP, enabling the replacement of direct minting with requests for funds from the DAP buffer + - DAP's responsibility to mint according to the issuance curve defined for each chain + - The ability to configure the percentage of funds redirected from DAP to various destinations (validators, nominators, treasury, collators, etc) + - Support for multiple assets in DAP, including native tokens and stablecoins crates: - name: frame-support bump: minor @@ -42,7 +40,11 @@ crates: bump: minor - name: asset-hub-westend-runtime bump: major +- name: westend-runtime + bump: major - name: pallet-staking-async bump: patch - name: pallet-staking-async-parachain-runtime bump: major +- name: pallet-staking-async-rc-runtime + bump: major 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 d0239fe680e75..d4622c8c4fe99 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -111,7 +111,7 @@ impl pallet_referenda::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; // The asset's interior location for the paying account. This is the Treasury diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index 53edb28ceccd9..c090d74aecd3a 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -999,7 +999,7 @@ impl pallet_bags_list::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; // The asset's interior location for the paying account. This is the Treasury diff --git a/substrate/frame/treasury/Cargo.toml b/substrate/frame/treasury/Cargo.toml index 44c044b13634d..c52a98b26dcf2 100644 --- a/substrate/frame/treasury/Cargo.toml +++ b/substrate/frame/treasury/Cargo.toml @@ -30,7 +30,6 @@ sp-core = { optional = true, workspace = true } sp-runtime = { workspace = true } [dev-dependencies] -pallet-dap = { workspace = true, default-features = true } pallet-utility = { workspace = true, default-features = true } sp-io = { workspace = true, default-features = true } diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 234e0fc85811f..f2abaa15be90a 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -1046,115 +1046,3 @@ fn multiple_spend_periods_work() { assert_eq!(LastSpendPeriod::::get(), Some(8)); }); } - -/// Tests for BurnDestination = BurnToDap configuration. -/// This module creates a separate mock runtime with DAP integration, reusing -/// most configuration from the parent `Test` runtime. -mod burn_to_dap_tests { - use super::*; - use frame_support::traits::{NeverEnsureOrigin, OnInitialize}; - - type Block = frame_system::mocking::MockBlock; - - frame_support::construct_runtime!( - pub enum TestWithDap { - System: frame_system, - Balances: pallet_balances, - Treasury: treasury, - Dap: pallet_dap, - } - ); - - // Reuse frame_system and pallet_balances configs from parent (same as Test) - #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] - impl frame_system::Config for TestWithDap { - type AccountId = u128; - type Lookup = IdentityLookup; - type Block = Block; - type AccountData = pallet_balances::AccountData; - } - - #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] - impl pallet_balances::Config for TestWithDap { - type AccountStore = System; - } - - // DAP-specific config - parameter_types! { - pub const DapPalletId: PalletId = PalletId(*b"dap/buff"); - } - - impl pallet_dap::Config for TestWithDap { - type Currency = Balances; - type PalletId = DapPalletId; - } - - // Treasury config: same as Test except BurnDestination redirects to DAP - impl Config for TestWithDap { - type PalletId = TreasuryPalletId; - type Currency = Balances; - type RejectOrigin = frame_system::EnsureRoot; - type RuntimeEvent = RuntimeEvent; - type SpendPeriod = ConstU64<2>; - type Burn = Burn; - type BurnDestination = pallet_dap::BurnToDap; - type WeightInfo = (); - type SpendFunds = (); - type MaxApprovals = ConstU32<100>; - type SpendOrigin = NeverEnsureOrigin; - type AssetKind = u32; - type Beneficiary = u128; - type BeneficiaryLookup = IdentityLookup; - type Paymaster = TestPay; - type BalanceConverter = MulBy>; - type PayoutPeriod = SpendPayoutPeriod; - type BlockNumberProvider = System; - #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = (); - } - - fn go_to_block_dap(n: u64) { - ::BlockNumberProvider::set_block_number(n); - >::on_initialize(n); - } - - #[test] - fn treasury_burn_redirected_to_dap_buffer() { - ExtBuilder::default().build().execute_with(|| { - let dap_buffer = pallet_dap::Pallet::::buffer_account(); - - pallet_dap::Pallet::::ensure_buffer_account_exists(); - - let initial_total_issuance = pallet_balances::TotalIssuance::::get(); - - // Given: Treasury has 1025 (1024 pot + 1 ED), DAP buffer is empty - Balances::make_free_balance_be(&Treasury::account_id(), 1025); - assert_eq!(Treasury::pot(), 1024); - assert_eq!(Balances::free_balance(&dap_buffer), 0); - - // When: first spend period passes - go_to_block_dap(2); - // Then: 50% burned (512), pot = 512, DAP = 512 - assert_eq!(Treasury::pot(), 512); - assert_eq!(Balances::free_balance(&dap_buffer), 512); - - // When: second spend period passes - go_to_block_dap(4); - // Then: 50% of 512 burned (256), pot = 256, DAP = 512 + 256 = 768 - assert_eq!(Treasury::pot(), 256); - assert_eq!(Balances::free_balance(&dap_buffer), 768); - - // When: third spend period passes - go_to_block_dap(6); - // Then: 50% of 256 burned (128), pot = 128, DAP = 768 + 128 = 896 - assert_eq!(Treasury::pot(), 128); - assert_eq!(Balances::free_balance(&dap_buffer), 896); - - // And: total issuance is preserved throughout - assert_eq!( - pallet_balances::TotalIssuance::::get(), - initial_total_issuance + 1024 - ); - }); - } -} From acd52bb89a2347d45daf4f236bbae788b9dddfa3 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 9 Dec 2025 17:15:40 +0100 Subject: [PATCH 14/14] zepter --- substrate/frame/staking-async/ahm-test/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/substrate/frame/staking-async/ahm-test/Cargo.toml b/substrate/frame/staking-async/ahm-test/Cargo.toml index 30c6ea62132e3..608a19a1fcfe3 100644 --- a/substrate/frame/staking-async/ahm-test/Cargo.toml +++ b/substrate/frame/staking-async/ahm-test/Cargo.toml @@ -53,6 +53,8 @@ std = [ try-runtime = [ "pallet-balances/try-runtime", + "pallet-dap/try-runtime", + "pallet-staking/try-runtime", "pallet-staking-async-rc-client/try-runtime",