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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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", From 03b602c50cbb0dd7c81d2ca0f4f1942a82a46259 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 20:55:25 +0100 Subject: [PATCH 15/27] Fix typo in documentation --- substrate/frame/balances/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index b661563bd7fe3..c7b259ad02e4b 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -873,7 +873,7 @@ pub mod pallet { /// 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 + /// use `T::BurnDestination::fill()` 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()})] From 32e55833f5b1f48e05a75c6fa9859a146f347d55 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 23:06:58 +0100 Subject: [PATCH 16/27] asset-hub-westend: cleaner way to remove treasury burns --- .../runtimes/assets/asset-hub-westend/src/governance/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 5814cbeb3f845..9c00123b55c89 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,6 @@ impl pallet_referenda::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; @@ -139,8 +138,8 @@ impl pallet_treasury::Config for Runtime { type RejectOrigin = EitherOfDiverse, Treasurer>; type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; - type Burn = Burn; - type BurnDestination = pallet_dap::BurnToDap; + type Burn = (); + type BurnDestination = (); type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = (); From b8e8645ab03c054fa52c02d408a82b7073253910 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 11:42:08 +0100 Subject: [PATCH 17/27] Introduce dap-satellite + BurnDestination in pallet-balances's Config --- Cargo.lock | 23 + Cargo.toml | 2 + .../assets/asset-hub-rococo/src/lib.rs | 1 + .../assets/asset-hub-westend/src/lib.rs | 1 + .../bridge-hubs/bridge-hub-rococo/src/lib.rs | 1 + .../bridge-hubs/bridge-hub-westend/Cargo.toml | 4 + .../bridge-hubs/bridge-hub-westend/src/lib.rs | 21 +- .../collectives-westend/Cargo.toml | 4 + .../collectives-westend/src/lib.rs | 22 +- .../coretime/coretime-rococo/src/lib.rs | 1 + .../coretime/coretime-westend/Cargo.toml | 4 + .../coretime/coretime-westend/src/lib.rs | 18 +- .../runtimes/people/people-rococo/src/lib.rs | 1 + .../runtimes/people/people-westend/Cargo.toml | 4 + .../runtimes/people/people-westend/src/lib.rs | 18 +- .../runtimes/testing/penpal/src/lib.rs | 1 + .../testing/rococo-parachain/src/lib.rs | 1 + .../testing/yet-another-parachain/src/lib.rs | 1 + cumulus/test/runtime/src/lib.rs | 1 + polkadot/runtime/rococo/src/lib.rs | 2 + polkadot/runtime/test-runtime/src/lib.rs | 1 + polkadot/runtime/westend/Cargo.toml | 4 + polkadot/runtime/westend/src/lib.rs | 24 +- .../xcm/xcm-builder/src/tests/pay/mock.rs | 1 + substrate/bin/node/runtime/src/lib.rs | 1 + substrate/frame/asset-rewards/src/mock.rs | 1 + substrate/frame/assets-freezer/src/mock.rs | 1 + substrate/frame/balances/src/lib.rs | 46 +- substrate/frame/balances/src/tests/mod.rs | 1 + .../contracts/mock-network/src/parachain.rs | 1 + .../contracts/mock-network/src/relay_chain.rs | 1 + substrate/frame/dap-satellite/Cargo.toml | 60 ++ substrate/frame/dap-satellite/src/lib.rs | 677 ++++++++++++++++++ substrate/frame/nis/src/mock.rs | 2 + .../staking-async/runtimes/rc/src/lib.rs | 1 + umbrella/Cargo.toml | 9 + umbrella/src/lib.rs | 4 + 37 files changed, 934 insertions(+), 32 deletions(-) create mode 100644 substrate/frame/dap-satellite/Cargo.toml create mode 100644 substrate/frame/dap-satellite/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8979963a7fb4a..f6553796af187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2856,6 +2856,7 @@ dependencies = [ "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-session", @@ -3474,6 +3475,7 @@ dependencies = [ "pallet-collective", "pallet-collective-content", "pallet-core-fellowship", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-preimage", @@ -3920,6 +3922,7 @@ dependencies = [ "pallet-balances", "pallet-broker", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-proxy", @@ -12214,6 +12217,23 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-dap-satellite" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-dap", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-default-config-example" version = "10.0.0" @@ -14922,6 +14942,7 @@ dependencies = [ "pallet-authorship", "pallet-balances", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-identity", "pallet-message-queue", "pallet-migrations", @@ -16595,6 +16616,7 @@ dependencies = [ "pallet-conviction-voting", "pallet-core-fellowship", "pallet-dap", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", @@ -27540,6 +27562,7 @@ dependencies = [ "pallet-beefy", "pallet-beefy-mmr", "pallet-conviction-voting", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", diff --git a/Cargo.toml b/Cargo.toml index 83c71a9e1c586..5f0276a474900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -356,6 +356,7 @@ members = [ "substrate/frame/conviction-voting", "substrate/frame/core-fellowship", "substrate/frame/dap", + "substrate/frame/dap-satellite", "substrate/frame/delegated-staking", "substrate/frame/democracy", "substrate/frame/derivatives", @@ -986,6 +987,7 @@ pallet-contracts-uapi = { path = "substrate/frame/contracts/uapi", default-featu 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-dap-satellite = { path = "substrate/frame/dap-satellite", 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-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index 811e119d6b89c..0b887f0b82206 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -227,6 +227,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = ConstU32<50>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { 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 5471dee2ade78..3e9ae5401bdd2 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! { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs index f61813c49a2f2..4afcdf58aa6d9 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs @@ -364,6 +364,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml index 3e4b90b014618..73f2a8b44b3f7 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml @@ -31,6 +31,7 @@ frame-try-runtime = { optional = true, workspace = true } pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-message-queue = { workspace = true } pallet-multisig = { workspace = true } pallet-session = { workspace = true } @@ -174,6 +175,7 @@ std = [ "pallet-bridge-parachains/std", "pallet-bridge-relayers/std", "pallet-collator-selection/std", + "pallet-dap-satellite/std", "pallet-message-queue/std", "pallet-multisig/std", "pallet-session/std", @@ -254,6 +256,7 @@ runtime-benchmarks = [ "pallet-bridge-parachains/runtime-benchmarks", "pallet-bridge-relayers/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-session/runtime-benchmarks", @@ -304,6 +307,7 @@ try-runtime = [ "pallet-bridge-parachains/try-runtime", "pallet-bridge-relayers/try-runtime", "pallet-collator-selection/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", "pallet-session/try-runtime", diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs index 65ca1b2a4b49d..b0ff0610cae48 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs @@ -354,17 +354,24 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = MILLICENTS; + /// Percentage of fees to send to DAP satellite (0 = all to staking pot, 100 = all to DAP). + pub const DapSatelliteFeePercent: u32 = 0; } +/// Fee handler that splits fees between DAP satellite and staking pot. +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -552,6 +559,15 @@ impl pallet_utility::Config for Runtime { type WeightInfo = weights::pallet_utility::WeightInfo; } +parameter_types! { + pub const DapSatellitePalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -590,6 +606,9 @@ construct_runtime!( BridgeRococoMessages: pallet_bridge_messages:: = 44, XcmOverBridgeHubRococo: pallet_xcm_bridge_hub:: = 45, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 60, + EthereumInboundQueue: snowbridge_pallet_inbound_queue = 80, EthereumOutboundQueue: snowbridge_pallet_outbound_queue = 81, EthereumBeaconClient: snowbridge_pallet_ethereum_client = 82, diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml index 7c466c22693ec..2cdfd290f2f2a 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml @@ -34,6 +34,7 @@ pallet-authorship = { workspace = true } pallet-balances = { workspace = true } pallet-collective = { workspace = true } pallet-core-fellowship = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-multisig = { workspace = true } pallet-preimage = { workspace = true } pallet-proxy = { workspace = true } @@ -120,6 +121,7 @@ runtime-benchmarks = [ "pallet-collective-content/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-core-fellowship/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", @@ -165,6 +167,7 @@ try-runtime = [ "pallet-collective-content/try-runtime", "pallet-collective/try-runtime", "pallet-core-fellowship/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", "pallet-preimage/try-runtime", @@ -213,6 +216,7 @@ std = [ "pallet-collective-content/std", "pallet-collective/std", "pallet-core-fellowship/std", + "pallet-dap-satellite/std", "pallet-message-queue/std", "pallet-multisig/std", "pallet-preimage/std", diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs index 47e8e24a8efb9..cac9fee160919 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -234,17 +234,27 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = MILLICENTS; + pub const DapSatelliteFeePercent: u32 = 0; } +pub type DealWithFeesSatellite = pallet_dap_satellite::DealWithFeesSplit< + R, + pallet_balances::Pallet, + DapSatelliteFeePercent, + ToParentTreasury, + pallet_dap_satellite::AccumulateInSatellite, +>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter>; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; type FeeMultiplierUpdate = SlowAdjustingFeeUpdate; @@ -683,6 +693,15 @@ impl pallet_asset_rate::Config for Runtime { type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::AssetRateArguments; } +parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -747,6 +766,7 @@ construct_runtime!( AmbassadorContent: pallet_collective_content:: = 75, StateTrieMigration: pallet_state_trie_migration = 80, + DapSatellite: pallet_dap_satellite = 85, // The Secretary Collective // pub type SecretaryCollectiveInstance = pallet_ranked_collective::instance3; diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs index 040d99280605f..7a77a96611534 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs @@ -267,6 +267,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml b/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml index a40bebb5747c0..976a3fc50766c 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml @@ -31,6 +31,7 @@ pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } pallet-broker = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-message-queue = { workspace = true } pallet-multisig = { workspace = true } pallet-proxy = { workspace = true } @@ -113,6 +114,7 @@ std = [ "pallet-balances/std", "pallet-broker/std", "pallet-collator-selection/std", + "pallet-dap-satellite/std", "pallet-message-queue/std", "pallet-multisig/std", "pallet-proxy/std", @@ -167,6 +169,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-broker/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", @@ -202,6 +205,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-broker/try-runtime", "pallet-collator-selection/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", "pallet-proxy/try-runtime", diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs index 153b4e01dc381..afd8fc0179c29 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs @@ -267,17 +267,22 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = MILLICENTS; + pub const DapSatelliteFeePercent: Perbill = Perbill::from_percent(0); } +pub type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -612,6 +617,15 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } +parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + pub struct BrokerMigrationV4BlockConversion; impl pallet_broker::migration::v4::BlockToRelayHeightConversion @@ -667,6 +681,8 @@ construct_runtime!( // The main stage. Broker: pallet_broker = 50, + DapSatellite: pallet_dap_satellite = 60, + // Sudo Sudo: pallet_sudo = 100, } diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs index ccc1ae56c685f..f3d532a7fbbdb 100644 --- a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs @@ -241,6 +241,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/people/people-westend/Cargo.toml b/cumulus/parachains/runtimes/people/people-westend/Cargo.toml index 5dc76682c9092..00f945f36ccab 100644 --- a/cumulus/parachains/runtimes/people/people-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/people/people-westend/Cargo.toml @@ -30,6 +30,7 @@ frame-try-runtime = { optional = true, workspace = true } pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-identity = { workspace = true } pallet-message-queue = { workspace = true } pallet-migrations = { workspace = true } @@ -112,6 +113,7 @@ std = [ "pallet-authorship/std", "pallet-balances/std", "pallet-collator-selection/std", + "pallet-dap-satellite/std", "pallet-identity/std", "pallet-message-queue/std", "pallet-migrations/std", @@ -167,6 +169,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-identity/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-migrations/runtime-benchmarks", @@ -202,6 +205,7 @@ try-runtime = [ "pallet-authorship/try-runtime", "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-identity/try-runtime", "pallet-message-queue/try-runtime", "pallet-migrations/try-runtime", diff --git a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs index 5e44d12b96e93..88155d139cd32 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs @@ -244,17 +244,26 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10. pub const TransactionByteFee: Balance = MILLICENTS; + pub const DapSatelliteFeePercent: u8 = 0; } +type DealWithFeesSatellite = pallet_dap_satellite::DealWithFeesSplit< + DealWithFees, + Balances, + DapSatellite, + DapSatelliteFeePercent, +>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -387,6 +396,7 @@ parameter_types! { pub const SessionLength: BlockNumber = 6 * HOURS; // StakingAdmin pluralistic body. pub const StakingAdminBodyId: BodyId = BodyId::Defense; + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); } /// We allow Root and the `StakingAdmin` to execute privileged collator selection operations. @@ -578,6 +588,11 @@ impl pallet_migrations::Config for Runtime { type WeightInfo = weights::pallet_migrations::WeightInfo; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -613,6 +628,7 @@ construct_runtime!( // The main stage. Identity: pallet_identity = 50, + DapSatellite: pallet_dap_satellite = 60, // Migrations pallet MultiBlockMigrations: pallet_migrations = 98, diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index 01f6dd1700d0d..4614401803174 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -433,6 +433,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs index 12a322534da5a..6430baa47e1bf 100644 --- a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs @@ -265,6 +265,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs index 92df3d950cb0e..03c04a376dd6a 100644 --- a/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs @@ -307,6 +307,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/cumulus/test/runtime/src/lib.rs b/cumulus/test/runtime/src/lib.rs index 8acbc49a33834..79a849c2e20c4 100644 --- a/cumulus/test/runtime/src/lib.rs +++ b/cumulus/test/runtime/src/lib.rs @@ -334,6 +334,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 80426212d4c29..60fd0c9d7e987 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -419,6 +419,7 @@ impl pallet_balances::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; type MaxFreezes = ConstU32<1>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { @@ -1319,6 +1320,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<1>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index b4a368c8d8a19..f7e955238bd46 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -275,6 +275,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml index c907ceb65665c..cca82e92654fa 100644 --- a/polkadot/runtime/westend/Cargo.toml +++ b/polkadot/runtime/westend/Cargo.toml @@ -60,6 +60,7 @@ pallet-balances = { workspace = true } pallet-beefy = { workspace = true } pallet-beefy-mmr = { workspace = true } pallet-conviction-voting = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-delegated-staking = { workspace = true } pallet-election-provider-multi-phase = { workspace = true } pallet-fast-unstake = { workspace = true } @@ -156,6 +157,7 @@ std = [ "pallet-beefy-mmr/std", "pallet-beefy/std", "pallet-conviction-voting/std", + "pallet-dap-satellite/std", "pallet-delegated-staking/std", "pallet-election-provider-multi-phase/std", "pallet-election-provider-support-benchmarking?/std", @@ -246,6 +248,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-beefy-mmr/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-delegated-staking/runtime-benchmarks", "pallet-election-provider-multi-phase/runtime-benchmarks", "pallet-election-provider-support-benchmarking/runtime-benchmarks", @@ -312,6 +315,7 @@ try-runtime = [ "pallet-beefy-mmr/try-runtime", "pallet-beefy/try-runtime", "pallet-conviction-voting/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-delegated-staking/try-runtime", "pallet-election-provider-multi-phase/try-runtime", "pallet-fast-unstake/try-runtime", diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index dba9eba840958..7ce0ed254ceff 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -408,6 +408,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { @@ -479,11 +480,19 @@ parameter_types! { /// This value increases the priority of `Operational` transactions by adding /// a "virtual tip" that's equal to the `OperationalFeeMultiplier * final_fee`. pub const OperationalFeeMultiplier: u8 = 5; + /// Percentage of fees that go to DAP satellite (0-100). + /// The remainder goes to block author. Tips always go 100% to author. + /// Westend: 0% to DAP (preserving original behavior of 100% to author) + pub const DapSatelliteFeePercent: u32 = 0; } +/// Fee handler that splits fees between DAP satellite and block author. +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = FungibleAdapter; type OperationalFeeMultiplier = OperationalFeeMultiplier; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -1747,6 +1756,15 @@ impl pallet_root_offences::Config for Runtime { type ReportOffence = Offences; } +parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + parameter_types! { pub MbmServiceWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; } @@ -2027,6 +2045,10 @@ mod runtime { #[runtime::pallet_index(105)] pub type RootOffences = pallet_root_offences; + // DAP Satellite - collects funds for transfer to DAP on AssetHub + #[runtime::pallet_index(106)] + pub type DapSatellite = pallet_dap_satellite; + // BEEFY Bridges support. #[runtime::pallet_index(200)] pub type Beefy = pallet_beefy; diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index d8f8e15f5eb05..1275c00761511 100644 --- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs +++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs @@ -85,6 +85,7 @@ impl pallet_balances::Config for Test { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index d884b150948c4..18d69a86fd278 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -598,6 +598,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs index 320d6c3ec4257..66807de25e651 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 = pallet_balances::DirectBurn; } 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..af8ec237e906b 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 = pallet_balances::DirectBurn; } impl pallet_assets::Config for Test { diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index c7b259ad02e4b..0ea58af2c23d6 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -166,7 +166,7 @@ use frame_support::{ tokens::{ fungible, BalanceStatus as Status, DepositConsequence, Fortitude::{self, Force, Polite}, - IdAmount, + FundingSink, IdAmount, Preservation::{Expendable, Preserve, Protect}, WithdrawConsequence, }, @@ -186,6 +186,7 @@ use sp_runtime::{ ArithmeticError, DispatchError, FixedPointOperand, Perbill, RuntimeDebug, TokenError, }; +pub use frame_support::traits::tokens::DirectBurn; pub use types::{ AccountData, AdjustmentDirection, BalanceLock, DustCleaner, ExtraFlags, Reasons, ReserveData, }; @@ -249,6 +250,7 @@ pub mod pallet { type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = (); } } @@ -338,19 +340,18 @@ pub mod pallet { Self::Balance, >; - // 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; + /// 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) or burn them directly. + /// + /// - DAP-enabled runtimes on AssetHub: `type BurnDestination = + /// pallet_dap::ReturnToDap;` + /// - DAP satellite runtimes: `type BurnDestination = + /// pallet_dap_satellite::AccumulateInSatellite;` + /// - Other runtimes: `type BurnDestination = DirectBurn;` + #[pallet::no_default_bounds] + type BurnDestination: FundingSink; } /// The in-code storage version. @@ -870,11 +871,9 @@ 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. /// - /// Currently burns directly, reducing total issuance. - /// - /// TODO(DAP): When `BurnDestination` is uncommented in the Config trait, this should - /// use `T::BurnDestination::fill()` instead to allow DAP-enabled runtimes to - /// redirect user-initiated burns to the DAP buffer. + /// The behavior depends on the runtime's `BurnDestination` configuration: + /// - DAP-enabled runtimes: funds are transferred to the DAP buffer + /// - Other runtimes: funds are burned directly, reducing total issuance #[pallet::call_index(10)] #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] pub fn burn( @@ -882,17 +881,10 @@ 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 }; - >::burn_from( - &source, - value, - preservation, - Precision::Exact, - Fortitude::Polite, - )?; + T::BurnDestination::fill(&source, value, preservation); Ok(()) } } diff --git a/substrate/frame/balances/src/tests/mod.rs b/substrate/frame/balances/src/tests/mod.rs index 155f78884d122..c0587cf024eec 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 = crate::DirectBurn, AccountId>; } #[derive(Clone)] diff --git a/substrate/frame/contracts/mock-network/src/parachain.rs b/substrate/frame/contracts/mock-network/src/parachain.rs index ad43ac42a7508..899e3aca7892c 100644 --- a/substrate/frame/contracts/mock-network/src/parachain.rs +++ b/substrate/frame/contracts/mock-network/src/parachain.rs @@ -97,6 +97,7 @@ impl pallet_balances::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/contracts/mock-network/src/relay_chain.rs b/substrate/frame/contracts/mock-network/src/relay_chain.rs index 0e60e3df6e19d..37e6167faa558 100644 --- a/substrate/frame/contracts/mock-network/src/relay_chain.rs +++ b/substrate/frame/contracts/mock-network/src/relay_chain.rs @@ -90,6 +90,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl shared::Config for Runtime { diff --git a/substrate/frame/dap-satellite/Cargo.toml b/substrate/frame/dap-satellite/Cargo.toml new file mode 100644 index 0000000000000..42987d9b60388 --- /dev/null +++ b/substrate/frame/dap-satellite/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "pallet-dap-satellite" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +description = "FRAME pallet for DAP Satellite - collects funds for periodic transfer to DAP on AssetHub" + +[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 } +pallet-dap = { 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", + "pallet-dap/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-dap/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs new file mode 100644 index 0000000000000..1ea27a6515036 --- /dev/null +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -0,0 +1,677 @@ +// 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. + +//! # DAP Satellite Pallet +//! +//! This pallet is meant to be used on **system chains other than AssetHub** (e.g., Coretime, +//! People, BridgeHub) or on the **Relay Chain**. It should NOT be deployed on AssetHub, which +//! hosts the central DAP pallet (`pallet-dap`). +//! +//! ## Purpose +//! +//! The DAP Satellite collects funds that would otherwise be burned (e.g., transaction fees, +//! coretime revenue, slashing) into a local satellite account. These funds are accumulated +//! locally and will eventually be transferred via XCM to the central DAP buffer on AssetHub. +//! +//! ## Architecture +//! +//! ```text +//! +-----------------+ +-----------------+ +-----------------+ +//! | Relay Chain | | Coretime Chain | | People Chain | +//! | DAPSatellite | | DAPSatellite | | DAPSatellite | +//! +--------+--------+ +--------+--------+ +--------+--------+ +//! | | | +//! | XCM (periodic) | | +//! +-----------------------+-----------------------+ +//! | +//! v +//! +-----------------+ +//! | AssetHub | +//! | pallet-dap | +//! | (central) | +//! +-----------------+ +//! ``` +//! +//! ## Implementation +//! +//! This is a minimal implementation that only accumulates funds locally. The periodic XCM +//! transfer to AssetHub is NOT yet implemented. +//! +//! In this first iteration, the pallet provides the following components: +//! - `AccumulateInSatellite`: Implementation of `FundingSink` that transfers funds to the satellite +//! account instead of burning them. +//! - `SlashToSatellite`: Implementation of `OnUnbalanced` for the new `fungible::Balanced` trait, +//! useful for staking slashes. +//! +//! **TODO:** +//! - Periodic XCM transfer to AssetHub DAP buffer +//! - Configuration for XCM period and destination +//! - Weight accounting for XCM operations +//! +//! ## Usage +//! +//! On system chains (not AssetHub) or Relay Chain, configure pallets to use the satellite: +//! +//! ```ignore +//! // In runtime configuration for Coretime/People/BridgeHub/RelayChain +//! impl pallet_balances::Config for Runtime { +//! type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; +//! } +//! +//! // For fee handlers using OnUnbalanced +//! type FeeDestination = pallet_dap_satellite::SlashToSatellite; +//! ``` + +#![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-satellite"; + +/// 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; + + /// The pallet ID used to derive the satellite account. + /// + /// Each runtime should configure a unique ID to avoid collisions if multiple + /// DAP satellite instances are used. + #[pallet::constant] + type PalletId: Get; + } + + impl Pallet { + /// Get the satellite account derived from the pallet ID. + /// + /// This account accumulates funds locally before they are sent to AssetHub. + pub fn satellite_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Ensure the satellite 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_satellite_account_exists() { + let satellite = Self::satellite_account(); + if !frame_system::Pallet::::account_exists(&satellite) { + frame_system::Pallet::::inc_providers(&satellite); + log::info!( + target: LOG_TARGET, + "Created DAP satellite account: {satellite:?}" + ); + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> Weight { + // Create the satellite account if it doesn't exist (for chains upgrading to DAP). + Self::ensure_satellite_account_exists(); + // Weight: 1 read (account_exists) + potentially 1 write (inc_providers) + T::DbWeight::get().reads_writes(1, 1) + } + } + + /// Genesis config for the DAP Satellite 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 satellite account at genesis so it can receive funds of any amount. + Pallet::::ensure_satellite_account_exists(); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Funds accumulated in satellite account. + FundsAccumulated { from: T::AccountId, amount: BalanceOf }, + } +} + +/// Implementation of `FundingSink` that accumulates funds in the satellite account. +/// +/// Use this on system chains (not AssetHub) or Relay Chain to collect funds that would +/// otherwise be burned. The funds will eventually be transferred to AssetHub DAP via XCM. +/// +/// # Example +/// +/// ```ignore +/// impl pallet_balances::Config for Runtime { +/// type BurnDestination = AccumulateInSatellite; +/// } +/// ``` +pub struct AccumulateInSatellite(core::marker::PhantomData); + +impl FundingSink> for AccumulateInSatellite { + fn fill(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { + let satellite = Pallet::::satellite_account(); + + // Withdraw from source, resolve to satellite, emit event. If withdraw fails, nothing + // happens. If resolve fails (should never happen - satellite 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(&satellite, credit)) + .map(|_| { + Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }) + }); + } +} + +/// Type alias for credit (negative imbalance - funds that were removed). +/// This is for the `fungible::Balanced` trait. +pub type CreditOf = Credit<::AccountId, ::Currency>; + +/// Implementation of `OnUnbalanced` for the `fungible::Balanced` trait. +/// +/// Use this on system chains (not AssetHub) or Relay Chain to collect funds from +/// imbalances (e.g., slashing) that would otherwise be burned. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via `DealWithFeesSplit`). +/// +/// # Example +/// +/// ```ignore +/// impl pallet_staking::Config for Runtime { +/// type Slash = SlashToSatellite; +/// } +/// ``` +pub struct SlashToSatellite(core::marker::PhantomData); + +impl OnUnbalanced> for SlashToSatellite { + fn on_nonzero_unbalanced(amount: CreditOf) { + let satellite = Pallet::::satellite_account(); + let numeric_amount = amount.peek(); + + // The satellite 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(&satellite, amount) { + let remaining_amount = remaining.peek(); + if !remaining_amount.is_zero() { + log::error!( + target: LOG_TARGET, + "Failed to deposit to satellite account - {remaining_amount:?} will be burned!" + ); + } + } + + log::debug!( + target: LOG_TARGET, + "Deposited {numeric_amount:?} to satellite account (fungible)" + ); + } +} + +/// A configurable fee handler that splits fees between DAP satellite and another destination. +/// +/// - `DapPercent`: Percentage of fees (0-100) to send to DAP satellite +/// - `OtherHandler`: Where to send the remaining fees (e.g., `ToAuthor`, `DealWithFees`) +/// +/// Tips always go 100% to `OtherHandler`. +/// +/// # Example +/// +/// ```ignore +/// parameter_types! { +/// pub const DapSatelliteFeePercent: u32 = 0; // 0% to DAP, 100% to staking +/// } +/// +/// type DealWithFeesSatellite = pallet_dap_satellite::DealWithFeesSplit< +/// Runtime, +/// DapSatelliteFeePercent, +/// DealWithFees, // Or ToAuthor for relay chain +/// >; +/// +/// impl pallet_transaction_payment::Config for Runtime { +/// type OnChargeTransaction = FungibleAdapter; +/// } +/// ``` +pub struct DealWithFeesSplit( + core::marker::PhantomData<(T, DapPercent, OtherHandler)>, +); + +impl OnUnbalanced> + for DealWithFeesSplit +where + T: Config, + DapPercent: Get, + OtherHandler: OnUnbalanced>, +{ + fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { + if let Some(fees) = fees_then_tips.next() { + let dap_percent = DapPercent::get(); + let other_percent = 100u32.saturating_sub(dap_percent); + let mut split = fees.ration(dap_percent, other_percent); + if let Some(tips) = fees_then_tips.next() { + // Tips go 100% to other handler. + tips.merge_into(&mut split.1); + } + if dap_percent > 0 { + as OnUnbalanced<_>>::on_unbalanced(split.0); + } + OtherHandler::on_unbalanced(split.1); + } + } +} + +/// Implementation of `OnUnbalanced` for the old `Currency` trait. +/// +/// Use this on system chains (not AssetHub) or Relay Chain for pallets that still use +/// the legacy `Currency` trait (e.g., fee handlers, treasury burns). +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction). +/// +/// # Example +/// +/// ```ignore +/// // For fee handling +/// type FeeDestination = SinkToSatellite; +/// +/// // For treasury burns +/// impl pallet_treasury::Config for Runtime { +/// type BurnDestination = SinkToSatellite; +/// } +/// ``` +pub struct SinkToSatellite(core::marker::PhantomData<(T, C)>); + +impl OnUnbalanced for SinkToSatellite +where + T: Config, + C: Currency, +{ + fn on_nonzero_unbalanced(amount: C::NegativeImbalance) { + let satellite = Pallet::::satellite_account(); + let numeric_amount = amount.peek(); + + // Resolve the imbalance by depositing into the satellite account + C::resolve_creating(&satellite, amount); + + log::debug!( + target: LOG_TARGET, + "Deposited {numeric_amount:?} to satellite account (Currency trait)" + ); + } +} + +// TODO: Implement periodic XCM transfer to AssetHub DAP buffer +// +// Future implementation will add: +// 1. `on_initialize` hook to mark XCM as pending at configured intervals +// 2. `on_poll` hook to execute XCM transfer when pending and weight available +// 3. Configuration for: +// - `XcmPeriod`: How often to send accumulated funds (e.g., every 14400 blocks = ~1 day) +// - `AssetHubLocation`: XCM destination for AssetHub +// - `DapBufferBeneficiary`: The DAP buffer account on AssetHub +// 4. XCM message construction: +// - Burn from local satellite account +// - Teleport to AssetHub +// - Deposit into DAP buffer account + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + derive_impl, parameter_types, + sp_runtime::traits::AccountIdConversion, + traits::{ + fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, + OnUnbalanced, WithdrawReasons, + }, + }; + use sp_runtime::BuildStorage; + use std::cell::Cell; + + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + DapSatellite: 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; + } + + parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); + } + + impl Config for Test { + type Currency = Balances; + type PalletId = DapSatellitePalletId; + } + + 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 satellite_account_is_derived_from_pallet_id() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + let expected: u64 = DapSatellitePalletId::get().into_account_truncating(); + assert_eq!(satellite, expected); + }); + } + + #[test] + fn genesis_creates_satellite_account() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + // Satellite account should exist after genesis (created via inc_providers) + assert!(System::account_exists(&satellite)); + }); + } + + // ===== fill tests ===== + + #[test] + fn fill_accumulates_from_multiple_sources() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let satellite = DapSatellite::satellite_account(); + + // Given: accounts have balances, satellite is empty + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fill from multiple accounts + AccumulateInSatellite::::fill(&1, 20, Preservation::Preserve); + AccumulateInSatellite::::fill(&2, 50, Preservation::Preserve); + AccumulateInSatellite::::fill(&3, 100, Preservation::Preserve); + + // Then: satellite has accumulated all funds + assert_eq!(Balances::free_balance(satellite), 170); + // ... accounts have their balance correctly updated + assert_eq!(Balances::free_balance(1), 80); + assert_eq!(Balances::free_balance(2), 150); + assert_eq!(Balances::free_balance(3), 200); + // ... and events are emitted + System::assert_has_event( + Event::::FundsAccumulated { from: 1, amount: 20 }.into(), + ); + System::assert_has_event( + Event::::FundsAccumulated { from: 2, amount: 50 }.into(), + ); + System::assert_has_event( + Event::::FundsAccumulated { from: 3, amount: 100 }.into(), + ); + }); + } + + #[test] + fn fill_with_insufficient_balance_is_noop() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100, satellite has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + + // When: try to fill 150 (more than balance) + AccumulateInSatellite::::fill(&1, 150, Preservation::Preserve); + + // Then: balances unchanged (infallible no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + }); + } + + // ===== SlashToSatellite tests ===== + + #[test] + fn slash_to_satellite_deposits_to_satellite() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: multiple slashes occur + let credit1 = >::issue(30); + SlashToSatellite::::on_unbalanced(credit1); + + let credit2 = >::issue(20); + SlashToSatellite::::on_unbalanced(credit2); + + let credit3 = >::issue(50); + SlashToSatellite::::on_unbalanced(credit3); + + // Then: satellite has accumulated all slashes (30 + 20 + 50 = 100) + assert_eq!(Balances::free_balance(satellite), 100); + }); + } + + // ===== SinkToSatellite tests ===== + + #[test] + fn sink_to_satellite_deposits_to_satellite() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: accounts have balances, satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: multiple sinks occur from different accounts + let imbalance1 = >::withdraw( + &1, + 30, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + SinkToSatellite::::on_unbalanced(imbalance1); + + let imbalance2 = >::withdraw( + &2, + 50, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + SinkToSatellite::::on_unbalanced(imbalance2); + + // Then: satellite has accumulated all sinks (30 + 50 = 80) + assert_eq!(Balances::free_balance(satellite), 80); + assert_eq!(Balances::free_balance(1), 70); + assert_eq!(Balances::free_balance(2), 150); + }); + } + + // ===== DealWithFeesSplit tests ===== + + // Thread-local storage for tracking what OtherHandler receives + thread_local! { + static OTHER_HANDLER_RECEIVED: Cell = const { Cell::new(0) }; + } + + /// Mock handler that tracks how much it receives + struct MockOtherHandler; + impl OnUnbalanced> for MockOtherHandler { + fn on_unbalanced(amount: CreditOf) { + OTHER_HANDLER_RECEIVED.with(|r| r.set(r.get() + amount.peek())); + // Drop the credit (it would normally be handled by the real handler) + drop(amount); + } + } + + fn reset_other_handler() { + OTHER_HANDLER_RECEIVED.with(|r| r.set(0)); + } + + fn get_other_handler_received() -> u64 { + OTHER_HANDLER_RECEIVED.with(|r| r.get()) + } + + parameter_types! { + pub const ZeroPercent: u32 = 0; + pub const FiftyPercent: u32 = 50; + pub const HundredPercent: u32 = 100; + } + + #[test] + fn deal_with_fees_split_zero_percent_to_dap() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fees of 100 with 0% to DAP (all to other handler) + tips of 50 + // Tips should ALWAYS go to other handler, regardless of DAP percent + let fees = >::issue(100); + let tips = >::issue(50); + as OnUnbalanced<_>>::on_unbalanceds( + [fees, tips].into_iter(), + ); + + // Then: satellite gets 0, other handler gets 150 (100% fees + tips) + assert_eq!(Balances::free_balance(satellite), 0); + assert_eq!(get_other_handler_received(), 150); + }); + } + + #[test] + fn deal_with_fees_split_hundred_percent_to_dap() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fees of 100 with 100% to DAP + tips of 50 + // Tips should ALWAYS go to other handler, regardless of DAP percent + let fees = >::issue(100); + let tips = >::issue(50); + as OnUnbalanced<_>>::on_unbalanceds( + [fees, tips].into_iter(), + ); + + // Then: satellite gets 100 (fees), other handler gets 50 (tips) + assert_eq!(Balances::free_balance(satellite), 100); + assert_eq!(get_other_handler_received(), 50); + }); + } + + #[test] + fn deal_with_fees_split_fifty_percent() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fees of 100 with 50% to DAP + tips of 40 + // Fees split 50/50, tips 100% to other handler + let fees = >::issue(100); + let tips = >::issue(40); + as OnUnbalanced<_>>::on_unbalanceds( + [fees, tips].into_iter(), + ); + + // Then: satellite gets 50 (half of fees), other handler gets 90 (half of fees + tips) + assert_eq!(Balances::free_balance(satellite), 50); + assert_eq!(get_other_handler_received(), 90); + }); + } + + #[test] + fn deal_with_fees_split_handles_empty_iterator() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: no fees, no tips (empty iterator) + as OnUnbalanced<_>>::on_unbalanceds( + core::iter::empty(), + ); + + // Then: nothing happens + assert_eq!(Balances::free_balance(satellite), 0); + assert_eq!(get_other_handler_received(), 0); + }); + } +} diff --git a/substrate/frame/nis/src/mock.rs b/substrate/frame/nis/src/mock.rs index 0e71e43f56bd7..abba508d3de76 100644 --- a/substrate/frame/nis/src/mock.rs +++ b/substrate/frame/nis/src/mock.rs @@ -71,6 +71,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_balances::Config for Test { @@ -92,6 +93,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index c090d74aecd3a..4bf045a8d4e76 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -451,6 +451,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 925236a4d4df6..3ff16fe85d96a 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-satellite?/std", "pallet-dap?/std", "pallet-delegated-staking?/std", "pallet-democracy?/std", @@ -282,6 +283,7 @@ runtime-benchmarks = [ "pallet-contracts?/runtime-benchmarks", "pallet-conviction-voting?/runtime-benchmarks", "pallet-core-fellowship?/runtime-benchmarks", + "pallet-dap-satellite?/runtime-benchmarks", "pallet-dap?/runtime-benchmarks", "pallet-delegated-staking?/runtime-benchmarks", "pallet-democracy?/runtime-benchmarks", @@ -424,6 +426,7 @@ try-runtime = [ "pallet-contracts?/try-runtime", "pallet-conviction-voting?/try-runtime", "pallet-core-fellowship?/try-runtime", + "pallet-dap-satellite?/try-runtime", "pallet-dap?/try-runtime", "pallet-delegated-staking?/try-runtime", "pallet-democracy?/try-runtime", @@ -639,6 +642,7 @@ runtime-full = [ "pallet-conviction-voting", "pallet-core-fellowship", "pallet-dap", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", @@ -1424,6 +1428,11 @@ default-features = false optional = true path = "../substrate/frame/dap" +[dependencies.pallet-dap-satellite] +default-features = false +optional = true +path = "../substrate/frame/dap-satellite" + [dependencies.pallet-delegated-staking] default-features = false optional = true diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index bb383efc5bcd0..a0bd94d04169b 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -447,6 +447,10 @@ pub use pallet_core_fellowship; #[cfg(feature = "pallet-dap")] pub use pallet_dap; +/// FRAME pallet for DAP Satellite - collects funds for periodic transfer to DAP on AssetHub. +#[cfg(feature = "pallet-dap-satellite")] +pub use pallet_dap_satellite; + /// FRAME delegated staking pallet. #[cfg(feature = "pallet-delegated-staking")] pub use pallet_delegated_staking; From 29a228d59db98b9fce23896e9c195b1c4efc62d3 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 12:06:07 +0100 Subject: [PATCH 18/27] prdoc --- prdoc/pr_10597.prdoc | 84 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 prdoc/pr_10597.prdoc diff --git a/prdoc/pr_10597.prdoc b/prdoc/pr_10597.prdoc new file mode 100644 index 0000000000000..7c0ab1b45e4f0 --- /dev/null +++ b/prdoc/pr_10597.prdoc @@ -0,0 +1,84 @@ +title: 'Introduce pallet-dap-satellite for system chain fund collection' +doc: +- audience: Runtime Dev + description: |- + This PR introduces pallet-dap-satellite, allowing system chains different from AssetHub + to collect funds (to be periodically sent to DAP on AssetHub in future PRs) instead of + burning them directly. + + **Changes:** + + - **pallet-dap-satellite**: New pallet that implements `FundingSink` by accumulating funds + in a satellite account. + + - **[breaking change] pallet-balances**: `BurnDestination` is now a required config item, + allowing each runtime to choose whether user-initiated burns lead to direct burning or + redirecting to a buffer. Runtimes must explicitly configure this: + - Non-DAP runtimes should use `pallet_balances::DirectBurn` + - DAP-integrated runtimes use `pallet_dap::ReturnToDap` (on AssetHub) or + `pallet_dap_satellite::AccumulateInSatellite` (on other system chains) + + **Runtime integrations:** + + - User-initiated burns redirect to DAP for asset-hub-westend + - User-initiated burns redirect to DAP satellite for: westend (relay chain), + bridge-hub-westend, collectives-westend, coretime-westend, people-westend + - Remaining runtimes (rococo ecosystem, test runtimes) use `DirectBurn` which burns + tokens directly +crates: +- name: frame-support + bump: minor +- name: pallet-balances + bump: major +- name: pallet-dap-satellite + bump: patch +- name: polkadot-sdk + bump: minor +- name: asset-hub-westend-runtime + bump: major +- name: westend-runtime + bump: major +- name: bridge-hub-westend-runtime + bump: major +- name: collectives-westend-runtime + bump: major +- name: coretime-westend-runtime + bump: major +- name: people-westend-runtime + bump: major +- name: rococo-runtime + bump: minor +- name: asset-hub-rococo-runtime + bump: minor +- name: bridge-hub-rococo-runtime + bump: minor +- name: people-rococo-runtime + bump: minor +- name: coretime-rococo-runtime + bump: minor +- name: kitchensink-runtime + bump: major +- name: polkadot-test-runtime + bump: major +- name: cumulus-test-runtime + bump: major +- name: rococo-parachain-runtime + bump: minor +- name: penpal-runtime + bump: minor +- name: yet-another-parachain-runtime + bump: minor +- name: pallet-asset-rewards + bump: patch +- name: pallet-assets-freezer + bump: patch +- name: pallet-nis + bump: patch +- name: pallet-contracts-mock-network + bump: minor +- name: pallet-staking-async + bump: patch +- name: pallet-staking-async-rc-runtime + bump: major +- name: pallet-staking-async-parachain-runtime + bump: major From ec647a75cf037d796edb359331bb884da2ff03f3 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 12:16:56 +0100 Subject: [PATCH 19/27] pallet-dap-satellite: add more tests --- substrate/frame/dap-satellite/src/lib.rs | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 1ea27a6515036..64a6cabc65d50 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -494,6 +494,65 @@ mod tests { }); } + #[test] + fn fill_with_zero_amount_succeeds() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100, satellite has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fill 0 from account 1 + AccumulateInSatellite::::fill(&1, 0, Preservation::Preserve); + + // Then: balances unchanged (no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + }); + } + + #[test] + fn fill_with_expendable_allows_full_drain() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: fill full balance with Expendable (allows going to 0) + AccumulateInSatellite::::fill(&1, 100, Preservation::Expendable); + + // Then: account 1 is empty, satellite has 100 + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(satellite), 100); + }); + } + + #[test] + fn fill_with_preserve_respects_existential_deposit() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100, ED is 1 (from TestDefaultConfig) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + + // When: try to fill 100 with Preserve (would go below ED) + AccumulateInSatellite::::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(satellite), 0); + + // But filling 99 works (leaves 1 for ED) + AccumulateInSatellite::::fill(&1, 99, Preservation::Preserve); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::free_balance(satellite), 99); + }); + } + // ===== SlashToSatellite tests ===== #[test] @@ -519,6 +578,23 @@ mod tests { }); } + #[test] + fn slash_to_satellite_handles_zero_amount() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: slash with zero amount + let credit = >::issue(0); + SlashToSatellite::::on_unbalanced(credit); + + // Then: satellite still has 0 (no-op) + assert_eq!(Balances::free_balance(satellite), 0); + }); + } + // ===== SinkToSatellite tests ===== #[test] From 264da353ef9684c33d1230407e72468b97f9c401 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 12:58:30 +0100 Subject: [PATCH 20/27] coretime-westend: integrate OnRevenue with DAP satellite --- .../coretime/coretime-westend/src/coretime.rs | 86 +++---------------- .../coretime/coretime-westend/src/lib.rs | 3 +- 2 files changed, 15 insertions(+), 74 deletions(-) diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs index c9cd7f80a61ae..bde63fa63adb8 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs @@ -18,69 +18,22 @@ use crate::{xcm_config::LocationToAccountId, *}; use codec::{Decode, Encode}; use cumulus_pallet_parachain_system::RelaychainDataProvider; use cumulus_primitives_core::relay_chain; -use frame_support::{ - parameter_types, - traits::{ - fungible::{Balanced, Credit, Inspect}, - tokens::{Fortitude, Preservation}, - DefensiveResult, OnUnbalanced, - }, -}; -use frame_system::Pallet as System; +use frame_support::parameter_types; use pallet_broker::{ CoreAssignment, CoreIndex, CoretimeInterface, PartsOf57600, RCBlockNumberOf, TaskId, Timeslice, }; use parachains_common::{AccountId, Balance}; -use sp_runtime::traits::{AccountIdConversion, MaybeConvert}; +use sp_runtime::traits::MaybeConvert; use westend_runtime_constants::system_parachain::coretime; use xcm::latest::prelude::*; -use xcm_executor::traits::{ConvertLocation, TransactAsset}; - -pub struct BurnCoretimeRevenue; -impl OnUnbalanced> for BurnCoretimeRevenue { - fn on_nonzero_unbalanced(amount: Credit) { - let acc = RevenueAccumulationAccount::get(); - if !System::::account_exists(&acc) { - System::::inc_providers(&acc); - } - Balances::resolve(&acc, amount).defensive_ok(); - } -} +use xcm_executor::traits::ConvertLocation; -type AssetTransactor = ::AssetTransactor; - -fn burn_at_relay(stash: &AccountId, value: Balance) -> Result<(), XcmError> { - let dest = Location::parent(); - let stash_location = - Junction::AccountId32 { network: None, id: stash.clone().into() }.into_location(); - let asset = Asset { id: AssetId(Location::parent()), fun: Fungible(value) }; - let dummy_xcm_context = XcmContext { origin: None, message_id: [0; 32], topic: None }; - - let withdrawn = AssetTransactor::withdraw_asset(&asset, &stash_location, None)?; - - AssetTransactor::can_check_out(&dest, &asset, &dummy_xcm_context)?; - - let parent_assets = Into::::into(withdrawn) - .reanchored(&dest, &Here.into()) - .defensive_map_err(|_| XcmError::ReanchorFailed)?; - - PolkadotXcm::send_xcm( - Here, - Location::parent(), - Xcm(vec![ - Instruction::UnpaidExecution { - weight_limit: WeightLimit::Unlimited, - check_origin: None, - }, - ReceiveTeleportedAsset(parent_assets.clone()), - BurnAsset(parent_assets), - ]), - )?; - - AssetTransactor::check_out(&dest, &asset, &dummy_xcm_context); - - Ok(()) -} +/// Coretime revenue handler that sends funds to the DAP satellite account. +/// +/// Previously, revenue was accumulated in a stash account and then burned at the relay chain. +/// With DAP, revenue is accumulated in the satellite account and will eventually be sent +/// to the DAP buffer on AssetHub via XCM. +pub type CoretimeRevenueToSatellite = pallet_dap_satellite::SlashToSatellite; /// A type containing the encoding of the coretime pallet in the Relay chain runtime. Used to /// construct any remote calls. The codec index must correspond to the index of `Coretime` in the @@ -112,7 +65,6 @@ enum CoretimeProviderCalls { parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); pub const MinimumCreditPurchase: Balance = UNITS / 10; - pub RevenueAccumulationAccount: AccountId = BrokerPalletId::get().into_sub_account_truncating(b"burnstash"); pub const MinimumEndPrice: Balance = UNITS; } @@ -287,21 +239,9 @@ impl CoretimeInterface for CoretimeAllocator { } fn on_new_timeslice(_timeslice: Timeslice) { - let stash = RevenueAccumulationAccount::get(); - let value = - Balances::reducible_balance(&stash, Preservation::Expendable, Fortitude::Polite); - - if value > 0 { - tracing::debug!(target: "runtime::coretime", %value, "Going to burn stashed tokens at RC"); - match burn_at_relay(&stash, value) { - Ok(()) => { - tracing::debug!(target: "runtime::coretime", %value, "Successfully burnt tokens"); - }, - Err(err) => { - tracing::error!(target: "runtime::coretime", error=?err, "burn_at_relay failed"); - }, - } - } + // Revenue is now accumulated in the DAP satellite account via + // CoretimeRevenueToSatellite (OnRevenue handler). The satellite pallet will + // eventually send accumulated funds to AssetHub DAP via XCM. } } @@ -317,7 +257,7 @@ impl MaybeConvert for SovereignAccountOf { impl pallet_broker::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; - type OnRevenue = BurnCoretimeRevenue; + type OnRevenue = CoretimeRevenueToSatellite; type TimeslicePeriod = ConstU32<{ coretime::TIMESLICE_PERIOD }>; // We don't actually need any leases at launch but set to 10 in case we want to sudo some in. type MaxLeasedCores = ConstU32<10>; diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs index afd8fc0179c29..0f504ad43084e 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs @@ -273,7 +273,8 @@ impl pallet_balances::Config for Runtime { parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = MILLICENTS; - pub const DapSatelliteFeePercent: Perbill = Perbill::from_percent(0); + /// Percentage of fees to send to DAP satellite (0-100). Currently 0% - all fees go to collators. + pub const DapSatelliteFeePercent: u32 = 0; } pub type DealWithFeesSatellite = From c2f1fc24ec51cc2c6bb9c2b6bb52ee418d767d8a Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 14:33:03 +0100 Subject: [PATCH 21/27] Fix couple of test runtimes --- substrate/frame/assets-freezer/src/mock.rs | 2 +- substrate/frame/nis/src/mock.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/substrate/frame/assets-freezer/src/mock.rs b/substrate/frame/assets-freezer/src/mock.rs index af8ec237e906b..7d87c0a7e1f1d 100644 --- a/substrate/frame/assets-freezer/src/mock.rs +++ b/substrate/frame/assets-freezer/src/mock.rs @@ -86,7 +86,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); - type BurnDestination = pallet_balances::DirectBurn; + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_assets::Config for Test { diff --git a/substrate/frame/nis/src/mock.rs b/substrate/frame/nis/src/mock.rs index abba508d3de76..2528e06556a6f 100644 --- a/substrate/frame/nis/src/mock.rs +++ b/substrate/frame/nis/src/mock.rs @@ -71,7 +71,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); - type BurnDestination = pallet_balances::DirectBurn; + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_balances::Config for Test { @@ -93,7 +93,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); - type BurnDestination = pallet_balances::DirectBurn; + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { From e39b961b3240a6a0343090057fd3b7df05c5c6df Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 15:08:35 +0100 Subject: [PATCH 22/27] Fix nis mock runtime --- substrate/frame/nis/src/mock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/nis/src/mock.rs b/substrate/frame/nis/src/mock.rs index 2528e06556a6f..739d088c193ee 100644 --- a/substrate/frame/nis/src/mock.rs +++ b/substrate/frame/nis/src/mock.rs @@ -93,7 +93,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); - type BurnDestination = pallet_balances::DirectBurn; + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { From ae028932a65e247bd58cf05ed7690360cc31e8d3 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 21:00:32 +0100 Subject: [PATCH 23/27] fix CI --- .../collectives/collectives-westend/src/lib.rs | 11 +++-------- .../runtimes/people/people-westend/src/lib.rs | 10 +++------- substrate/frame/balances/src/lib.rs | 9 +++++++++ substrate/test-utils/runtime/src/lib.rs | 1 + 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs index cac9fee160919..dd465991a8e70 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -243,18 +243,13 @@ parameter_types! { pub const DapSatelliteFeePercent: u32 = 0; } -pub type DealWithFeesSatellite = pallet_dap_satellite::DealWithFeesSplit< - R, - pallet_balances::Pallet, - DapSatelliteFeePercent, - ToParentTreasury, - pallet_dap_satellite::AccumulateInSatellite, ->; +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; type FeeMultiplierUpdate = SlowAdjustingFeeUpdate; diff --git a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs index 88155d139cd32..fdb982f41bcd9 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs @@ -250,15 +250,11 @@ impl pallet_balances::Config for Runtime { parameter_types! { /// Relay Chain `TransactionByteFee` / 10. pub const TransactionByteFee: Balance = MILLICENTS; - pub const DapSatelliteFeePercent: u8 = 0; + pub const DapSatelliteFeePercent: u32 = 0; } -type DealWithFeesSatellite = pallet_dap_satellite::DealWithFeesSplit< - DealWithFees, - Balances, - DapSatellite, - DapSatelliteFeePercent, ->; +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index 0ea58af2c23d6..e642e6aa8f8f4 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -884,6 +884,15 @@ pub mod pallet { let source = ensure_signed(origin)?; let preservation = if keep_alive { Preservation::Preserve } else { Preservation::Expendable }; + + // Check that the user has sufficient reducible balance + let reducible = >::reducible_balance( + &source, + preservation, + Fortitude::Polite, + ); + ensure!(reducible >= value, TokenError::FundsUnavailable); + T::BurnDestination::fill(&source, value, preservation); Ok(()) } diff --git a/substrate/test-utils/runtime/src/lib.rs b/substrate/test-utils/runtime/src/lib.rs index a04b05021bb46..04a6e5b15d3a9 100644 --- a/substrate/test-utils/runtime/src/lib.rs +++ b/substrate/test-utils/runtime/src/lib.rs @@ -427,6 +427,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_utility::Config for Runtime { From 50256869d3e22221d22364ea7bf954ed03ad60c1 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 10 Dec 2025 22:23:39 +0100 Subject: [PATCH 24/27] pallet-balances: introduce BurnHandler trait for configurable burn behavior - Add BurnHandler trait called after decrease_balance in burn_from - FundingSink now extends BurnHandler - DirectBurn::on_burned reduces total issuance (traditional burning) - DAP handlers use increase_balance to credit buffer (tokens preserved) - Override burn_from in pallet-balances to call T::BurnDestination::on_burned --- .../xcm/xcm-builder/src/tests/pay/mock.rs | 2 +- prdoc/pr_10597.prdoc | 32 +++++++++---- substrate/frame/balances/src/impl_fungible.rs | 21 +++++++++ substrate/frame/balances/src/lib.rs | 27 +++++------ substrate/frame/dap-satellite/src/lib.rs | 23 ++++++++- substrate/frame/dap/src/lib.rs | 25 ++++++++-- .../runtimes/parachain/src/lib.rs | 1 + substrate/frame/support/src/traits/tokens.rs | 2 +- .../support/src/traits/tokens/funding.rs | 47 +++++++++++++++++-- .../parachain/runtime/src/configs/mod.rs | 1 + .../solochain/runtime/src/configs/mod.rs | 1 + 11 files changed, 149 insertions(+), 33 deletions(-) diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index 1275c00761511..46d8ecfac736d 100644 --- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs +++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs @@ -85,7 +85,7 @@ impl pallet_balances::Config for Test { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); - type BurnDestination = pallet_balances::DirectBurn; + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/prdoc/pr_10597.prdoc b/prdoc/pr_10597.prdoc index 7c0ab1b45e4f0..0aed3e4aa22eb 100644 --- a/prdoc/pr_10597.prdoc +++ b/prdoc/pr_10597.prdoc @@ -1,4 +1,4 @@ -title: 'Introduce pallet-dap-satellite for system chain fund collection' +title: 'Introduce pallet-dap-satellite for system chain fund collection and redirect all burns for native tokens to DAP/ DAP satellite on Westend runtimes' doc: - audience: Runtime Dev description: |- @@ -11,19 +11,27 @@ doc: - **pallet-dap-satellite**: New pallet that implements `FundingSink` by accumulating funds in a satellite account. - - **[breaking change] pallet-balances**: `BurnDestination` is now a required config item, - allowing each runtime to choose whether user-initiated burns lead to direct burning or - redirecting to a buffer. Runtimes must explicitly configure this: - - Non-DAP runtimes should use `pallet_balances::DirectBurn` + - **[breaking change] pallet-balances**: `BurnDestination` is now a required config item + that must implement `BurnHandler`. The `BurnHandler::on_burned` method is called after + `decrease_balance` removes funds from an account, allowing each runtime to choose whether + burns lead to direct burning (reducing total issuance) or redirecting to a buffer. + Runtimes must explicitly configure this: + - Non-DAP runtimes should use `frame_support::traits::tokens::DirectBurn` - DAP-integrated runtimes use `pallet_dap::ReturnToDap` (on AssetHub) or `pallet_dap_satellite::AccumulateInSatellite` (on other system chains) + Note that non-native asset burns via pallet-assets do not use pallet-balances's implementation + of BurnHandler but they rely on the default one: each asset in pallet-assets has its own supply + that should decrease when burned. + + - **frame-support**: New `BurnHandler` trait that `FundingSink` now extends. `DirectBurn` + implements `BurnHandler::on_burned` to reduce total issuance. **Runtime integrations:** - - User-initiated burns redirect to DAP for asset-hub-westend - - User-initiated burns redirect to DAP satellite for: westend (relay chain), + - User and pallets-initiated burns for native tokens redirect to DAP for asset-hub-westend + - User and pallets-initiated burns for native tokens redirect to DAP satellite for: westend (relay chain), bridge-hub-westend, collectives-westend, coretime-westend, people-westend - - Remaining runtimes (rococo ecosystem, test runtimes) use `DirectBurn` which burns + - Remaining runtimes (rococo ecosystem, test runtimes) use `DirectBurn` which burns native tokens directly crates: - name: frame-support @@ -82,3 +90,11 @@ crates: bump: major - name: pallet-staking-async-parachain-runtime bump: major +- name: pallet-dap + bump: minor +- name: staging-xcm-builder + bump: patch +- name: solochain-template-runtime + bump: major +- name: parachain-template-runtime + bump: major diff --git a/substrate/frame/balances/src/impl_fungible.rs b/substrate/frame/balances/src/impl_fungible.rs index 03454c52eed2a..4af64a33f1b0a 100644 --- a/substrate/frame/balances/src/impl_fungible.rs +++ b/substrate/frame/balances/src/impl_fungible.rs @@ -20,11 +20,13 @@ use super::*; use frame_support::traits::{ tokens::{ Fortitude, + Precision::{self, BestEffort}, Preservation::{self, Preserve, Protect}, Provenance::{self, Minted}, }, AccountTouch, }; +use sp_runtime::{ArithmeticError, TokenError}; impl, I: 'static> fungible::Inspect for Pallet { type Balance = T::Balance; @@ -189,6 +191,25 @@ impl, I: 'static> fungible::Unbalanced for Pallet, I: 'static> fungible::Mutate for Pallet { + fn burn_from( + who: &T::AccountId, + amount: Self::Balance, + preservation: Preservation, + precision: Precision, + force: Fortitude, + ) -> Result { + use fungible::{Inspect, Unbalanced}; + let actual = Self::reducible_balance(who, preservation, force).min(amount); + ensure!(actual == amount || precision == BestEffort, TokenError::FundsUnavailable); + Self::total_issuance().checked_sub(&actual).ok_or(ArithmeticError::Overflow)?; + let actual = Self::decrease_balance(who, actual, BestEffort, preservation, force)?; + // Use configurable handler instead of directly reducing total issuance. + // For DirectBurn: reduces total issuance (traditional burning) + // For DAP satellite: credits buffer account (tokens preserved) + T::BurnDestination::on_burned(who, actual); + Self::done_burn_from(who, actual); + Ok(actual) + } fn done_mint_into(who: &T::AccountId, amount: Self::Balance) { Self::deposit_event(Event::::Minted { who: who.clone(), amount }); } diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index e642e6aa8f8f4..db464529f2aa8 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -164,9 +164,9 @@ use frame_support::{ pallet_prelude::DispatchResult, traits::{ tokens::{ - fungible, BalanceStatus as Status, DepositConsequence, + fungible, BalanceStatus as Status, BurnHandler, DepositConsequence, Fortitude::{self, Force, Polite}, - FundingSink, IdAmount, + IdAmount, Preservation::{Expendable, Preserve, Protect}, WithdrawConsequence, }, @@ -340,18 +340,20 @@ pub mod pallet { Self::Balance, >; - /// Handler for user-initiated burns via the `burn` extrinsic. + /// Handler for burned funds. /// - /// Runtimes can configure this to redirect burned funds to a buffer account - /// (e.g., DAP buffer on Asset Hub) or burn them directly. + /// This is called by `burn_from` after the source account's balance has been decreased. + /// Runtimes can configure this to either: + /// - Reduce total issuance (traditional burning) + /// - Credit to a buffer account (DAP-style systems) /// /// - DAP-enabled runtimes on AssetHub: `type BurnDestination = /// pallet_dap::ReturnToDap;` /// - DAP satellite runtimes: `type BurnDestination = /// pallet_dap_satellite::AccumulateInSatellite;` - /// - Other runtimes: `type BurnDestination = DirectBurn;` + /// - Other runtimes: `type BurnDestination = DirectBurn;` #[pallet::no_default_bounds] - type BurnDestination: FundingSink; + type BurnDestination: BurnHandler; } /// The in-code storage version. @@ -884,16 +886,13 @@ pub mod pallet { let source = ensure_signed(origin)?; let preservation = if keep_alive { Preservation::Preserve } else { Preservation::Expendable }; - - // Check that the user has sufficient reducible balance - let reducible = >::reducible_balance( + >::burn_from( &source, + value, preservation, + Precision::Exact, Fortitude::Polite, - ); - ensure!(reducible >= value, TokenError::FundsUnavailable); - - T::BurnDestination::fill(&source, value, preservation); + )?; Ok(()) } } diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 64a6cabc65d50..596a4ef082b38 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -83,8 +83,8 @@ extern crate alloc; use frame_support::{ pallet_prelude::*, traits::{ - fungible::{Balanced, Credit, Inspect, Mutate}, - tokens::{Fortitude, FundingSink, Precision, Preservation}, + fungible::{Balanced, Credit, Inspect, Mutate, Unbalanced}, + tokens::{BurnHandler, Fortitude, FundingSink, Precision, Preservation}, Currency, Imbalance, OnUnbalanced, }, PalletId, @@ -116,6 +116,7 @@ pub mod pallet { /// The currency type. type Currency: Inspect + Mutate + + Unbalanced + Balanced; /// The pallet ID used to derive the satellite account. @@ -198,6 +199,24 @@ pub mod pallet { /// ``` pub struct AccumulateInSatellite(core::marker::PhantomData); +impl BurnHandler> for AccumulateInSatellite { + fn on_burned(who: &T::AccountId, amount: BalanceOf) { + let satellite = Pallet::::satellite_account(); + + // Credit the satellite account. The source account's balance has already been decreased + // by `burn_from` before this is called. We use `increase_balance` which doesn't affect + // total issuance (keeping total issuance unchanged = funds preserved, not destroyed). + let _ = T::Currency::increase_balance(&satellite, amount, Precision::BestEffort); + + Pallet::::deposit_event(Event::FundsAccumulated { from: who.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Redirected burn of {amount:?} from {who:?} to satellite account" + ); + } +} + impl FundingSink> for AccumulateInSatellite { fn fill(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { let satellite = Pallet::::satellite_account(); diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index 42b6317e0dad7..3c9d89a21db6d 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -33,8 +33,8 @@ extern crate alloc; use frame_support::{ pallet_prelude::*, traits::{ - fungible::{Balanced, Credit, Inspect, Mutate}, - tokens::{Fortitude, FundingSink, Precision, Preservation}, + fungible::{Balanced, Credit, Inspect, Mutate, Unbalanced}, + tokens::{BurnHandler, Fortitude, FundingSink, Precision, Preservation}, Currency, Imbalance, OnUnbalanced, }, PalletId, @@ -66,6 +66,7 @@ pub mod pallet { /// The currency type. type Currency: Inspect + Mutate + + Unbalanced + Balanced; /// The pallet ID used to derive the buffer account. @@ -134,10 +135,28 @@ pub mod pallet { } } -/// Implementation of FundingSink that fills the DAP buffer. +/// Implementation of BurnHandler and FundingSink that fills the DAP buffer. /// Funds are transferred to the buffer account instead of being burned. pub struct ReturnToDap(core::marker::PhantomData); +impl BurnHandler> for ReturnToDap { + fn on_burned(who: &T::AccountId, amount: BalanceOf) { + let buffer = Pallet::::buffer_account(); + + // Credit the buffer account. The source account's balance has already been decreased + // by `burn_from` before this is called. We use `increase_balance` which doesn't affect + // total issuance (keeping total issuance unchanged = funds preserved, not destroyed). + let _ = T::Currency::increase_balance(&buffer, amount, Precision::BestEffort); + + Pallet::::deposit_event(Event::FundsReturned { from: who.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Redirected burn of {amount:?} from {who:?} to DAP buffer" + ); + } +} + impl FundingSink> for ReturnToDap { fn fill(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { let buffer = Pallet::::buffer_account(); diff --git a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs index 4b13091ce18c5..efefddf80cb3c 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! { diff --git a/substrate/frame/support/src/traits/tokens.rs b/substrate/frame/support/src/traits/tokens.rs index 55460aca99b97..be45c809c0b15 100644 --- a/substrate/frame/support/src/traits/tokens.rs +++ b/substrate/frame/support/src/traits/tokens.rs @@ -28,7 +28,7 @@ pub mod nonfungible; pub mod nonfungible_v2; pub mod nonfungibles; pub mod nonfungibles_v2; -pub use funding::{DirectBurn, FundingSink}; +pub use funding::{BurnHandler, 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 index f9da65a3efdeb..9de076c029acb 100644 --- a/substrate/frame/support/src/traits/tokens/funding.rs +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -26,6 +26,28 @@ use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; use core::marker::PhantomData; +use sp_runtime::Saturating; + +/// Trait for handling burned funds. +/// +/// This trait is used by `pallet_balances::burn_from` to handle funds after they have been +/// removed from the source account. Implementations can either: +/// - Reduce total issuance (traditional burning) +/// - Credit to a buffer account (DAP-style systems) +/// +/// The key distinction from `FundingSink::fill` is that `on_burned` is called AFTER +/// the funds have already been removed from the source account via `decrease_balance`. +pub trait BurnHandler { + /// Handle funds that have been burned from an account. + /// + /// Called by `burn_from` after the source account's balance has been decreased. + /// The implementation should either: + /// - Reduce total issuance (for actual burning) + /// - Credit the amount to a buffer account (for DAP systems) + /// + /// This operation is infallible. + fn on_burned(who: &AccountId, amount: Balance); +} /// Trait for moving funds into an issuance buffer or burning them. /// @@ -33,7 +55,7 @@ use core::marker::PhantomData; /// This trait is infallible - implementations must handle any errors internally. /// /// Pairs with future `FundingSource::drain()` for withdrawing from the buffer. -pub trait FundingSink { +pub trait FundingSink: BurnHandler { /// Fill the sink with funds from the given account. /// /// This could mean burning the funds or transferring them to a buffer account. @@ -47,21 +69,33 @@ pub trait FundingSink { fn fill(from: &AccountId, amount: Balance, preservation: Preservation); } -/// Direct burning implementation of `FundingSink`. +/// Direct burning implementation of `BurnHandler` and `FundingSink`. /// /// This implementation burns tokens directly, reducing total issuance. /// Used for traditional burn systems (e.g., Kusama). /// /// # Type Parameters /// -/// * `Currency` - The currency type that implements `Mutate` +/// * `Currency` - The currency type that implements `Mutate` and `Unbalanced` /// * `AccountId` - The account identifier type pub struct DirectBurn(PhantomData<(Currency, AccountId)>); +impl BurnHandler + for DirectBurn +where + Currency: fungible::Unbalanced, + AccountId: Eq, +{ + fn on_burned(_who: &AccountId, amount: Currency::Balance) { + // Reduce total issuance - funds are permanently destroyed + Currency::set_total_issuance(Currency::total_issuance().saturating_sub(amount)); + } +} + impl FundingSink for DirectBurn where - Currency: fungible::Mutate, + Currency: fungible::Mutate + fungible::Unbalanced, AccountId: Eq, { fn fill(from: &AccountId, amount: Currency::Balance, preservation: Preservation) { @@ -72,6 +106,11 @@ where } } +/// No-op implementation of `BurnHandler` for unit type. +impl BurnHandler for () { + fn on_burned(_who: &AccountId, _amount: Balance) {} +} + /// No-op implementation of `FundingSink` for unit type. /// Used for testing or when no sink behavior is needed. impl FundingSink for () { diff --git a/templates/parachain/runtime/src/configs/mod.rs b/templates/parachain/runtime/src/configs/mod.rs index 2ac558ea2a310..8dd75091de890 100644 --- a/templates/parachain/runtime/src/configs/mod.rs +++ b/templates/parachain/runtime/src/configs/mod.rs @@ -175,6 +175,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/templates/solochain/runtime/src/configs/mod.rs b/templates/solochain/runtime/src/configs/mod.rs index b8810a068036c..6a7faea0cc532 100644 --- a/templates/solochain/runtime/src/configs/mod.rs +++ b/templates/solochain/runtime/src/configs/mod.rs @@ -142,6 +142,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { From ebc0ce3ae3072a19e58a34f1e410b66a031ecd8b Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Thu, 11 Dec 2025 22:41:08 +0100 Subject: [PATCH 25/27] asset-conversion: fix tests --- substrate/frame/asset-conversion/ops/src/mock.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/substrate/frame/asset-conversion/ops/src/mock.rs b/substrate/frame/asset-conversion/ops/src/mock.rs index 1d38ab615745a..519e072951c9c 100644 --- a/substrate/frame/asset-conversion/ops/src/mock.rs +++ b/substrate/frame/asset-conversion/ops/src/mock.rs @@ -60,6 +60,7 @@ impl frame_system::Config for Test { #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for Test { type AccountStore = System; + type BurnDestination = pallet_balances::DirectBurn; } #[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)] From 8a47d50088c837c23dfe30b239e044a0c58f5992 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 12 Dec 2025 09:20:19 +0100 Subject: [PATCH 26/27] fix parachain mock runtime --- polkadot/runtime/parachains/src/mock.rs | 1 + prdoc/pr_10597.prdoc | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/polkadot/runtime/parachains/src/mock.rs b/polkadot/runtime/parachains/src/mock.rs index cba63ae7b1b04..20c75ad4f56da 100644 --- a/polkadot/runtime/parachains/src/mock.rs +++ b/polkadot/runtime/parachains/src/mock.rs @@ -153,6 +153,7 @@ impl pallet_balances::Config for Test { type Balance = Balance; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/prdoc/pr_10597.prdoc b/prdoc/pr_10597.prdoc index 0aed3e4aa22eb..cc104878504e0 100644 --- a/prdoc/pr_10597.prdoc +++ b/prdoc/pr_10597.prdoc @@ -98,3 +98,7 @@ crates: bump: major - name: parachain-template-runtime bump: major +- name: pallet-asset-conversion-ops + bump: patch +- name: polkadot-runtime-parachains + bump: patch From 137b6f5d082c9b00e37ae10e38ce230b4819dbe7 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 12 Dec 2025 14:59:02 +0100 Subject: [PATCH 27/27] Fix more tests --- prdoc/pr_10597.prdoc | 2 ++ substrate/frame/revive/src/tests.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/prdoc/pr_10597.prdoc b/prdoc/pr_10597.prdoc index cc104878504e0..4b0afb2e441fb 100644 --- a/prdoc/pr_10597.prdoc +++ b/prdoc/pr_10597.prdoc @@ -102,3 +102,5 @@ crates: bump: patch - name: polkadot-runtime-parachains bump: patch +- name: pallet-revive + bump: patch diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index 0af794f6ec7e3..08c349ff0f875 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -289,6 +289,7 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ExistentialDeposit; type ReserveIdentifier = [u8; 8]; type AccountStore = System; + type BurnDestination = pallet_balances::DirectBurn; } #[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)]