From fc2c4a24e4cc2d96cc86b3a11841f7af8ec668e7 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Tue, 25 Nov 2025 14:42:30 +0100 Subject: [PATCH 01/30] frame-support: add FundingSource and FundingSink traits - FundingSource: trait for requesting funds from an issuance system - this could mean minting new tokens or transferring from a given issuance buffer's account - FundingSink: trait for returning funds to an issuance system - this could mean burning tokens or transferring to a given issuance buffer's account --- substrate/frame/support/src/traits/tokens.rs | 2 + .../support/src/traits/tokens/funding.rs | 105 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 substrate/frame/support/src/traits/tokens/funding.rs diff --git a/substrate/frame/support/src/traits/tokens.rs b/substrate/frame/support/src/traits/tokens.rs index be982cd31e33a..d335e5fc0107b 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, DirectMint, FundingSink, FundingSource}; 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..5056a0adf72db --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -0,0 +1,105 @@ +// 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 funding sources and sinks in token issuance systems. +//! +//! This module provides abstractions for pulling funds (rewards, payments) and returning funds +//! (burns, slashing) in a way that can be configured differently per runtime. +//! +//! Two main patterns: +//! - **Direct mint/burn**: Traditional approach where funds are created/destroyed on demand +//! - **Buffer-based**: Funds are pre-minted into a buffer and distributed from there + +use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; +use core::marker::PhantomData; +use sp_runtime::{DispatchError, DispatchResult}; + +/// Trait for requesting funds from an issuance system. +/// +/// Implementations can either mint directly or pull from a pre-minted buffer. +pub trait FundingSource { + /// Request funds to be transferred to the beneficiary. + /// + /// Returns the actual amount transferred, which may be less than requested + /// if the source has insufficient funds. + fn request_funds(beneficiary: &AccountId, amount: Balance) -> Result; +} + +/// 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. + fn return_funds(from: &AccountId, amount: Balance) -> DispatchResult; +} + +/// Direct minting implementation of `FundingSource`. +/// +/// This implementation mints tokens directly when funds are requested. +/// Used for traditional mint-on-demand systems (e.g., Kusama). +/// +/// # Type Parameters +/// +/// * `Currency` - The currency type that implements `Mutate` +/// * `AccountId` - The account identifier type +pub struct DirectMint(PhantomData<(Currency, AccountId)>); + +impl FundingSource + for DirectMint +where + Currency: fungible::Mutate, + AccountId: Eq, +{ + fn request_funds( + beneficiary: &AccountId, + amount: Currency::Balance, + ) -> Result { + Currency::mint_into(beneficiary, amount)?; + Ok(amount) + } +} + +/// 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) -> DispatchResult { + Currency::burn_from( + from, + amount, + Preservation::Expendable, + Precision::Exact, + Fortitude::Polite, + )?; + Ok(()) + } +} From 1a376c6a3e0504d4bd6e61487acfc1cb07ba0348 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 28 Nov 2025 15:10:28 +0100 Subject: [PATCH 02/30] pallet-dap: implement FundingSource and basic DAP pallet structure Minimal initial implementation of pallet-dap. Only FundingSink is functional and allows replacing burns in other pallets with returns to DAP buffer. --- Cargo.lock | 16 +++ Cargo.toml | 2 + substrate/frame/dap/Cargo.toml | 57 +++++++++ substrate/frame/dap/src/lib.rs | 218 +++++++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 substrate/frame/dap/Cargo.toml create mode 100644 substrate/frame/dap/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d129f4eea452c..9a6aae112f82a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12193,6 +12193,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" 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/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..2b9a6a60fb157 --- /dev/null +++ b/substrate/frame/dap/src/lib.rs @@ -0,0 +1,218 @@ +// 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 +//! +//! Minimal initial implementation: only `FundingSink` (return_funds) is functional. +//! This allows replacing burns in other pallets with returns to DAP buffer. +//! +//! 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::{Inspect, Mutate}, + tokens::{FundingSink, FundingSource, Preservation}, + }, + 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; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency type. + type Currency: Inspect + Mutate; + } + + impl Pallet { + /// Get the DAP buffer account derived from the pallet ID. + pub fn buffer_account() -> T::AccountId { + DAP_PALLET_ID.into_account_truncating() + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Funds returned to DAP buffer. + FundsReturned { from: T::AccountId, amount: BalanceOf }, + } + + #[pallet::error] + pub enum Error { + /// FundingSource not yet implemented. + NotImplemented, + } +} + +/// Implementation of FundingSource - NOT YET IMPLEMENTED. +/// Will panic if called. +pub struct PullFromDap(core::marker::PhantomData); + +impl FundingSource> for PullFromDap { + fn request_funds( + _beneficiary: &T::AccountId, + _amount: BalanceOf, + ) -> Result, DispatchError> { + unimplemented!("PullFromDap::request_funds not yet implemented") + } +} + +/// 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) -> Result<(), DispatchError> { + let buffer = Pallet::::buffer_account(); + + T::Currency::transfer(source, &buffer, amount, Preservation::Preserve)?; + + Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Returned {amount:?} from {source:?} to DAP buffer" + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + assert_noop, assert_ok, derive_impl, sp_runtime::traits::AccountIdConversion, + traits::tokens::FundingSink, + }; + 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(); + 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 return_funds_transfers_to_buffer() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + 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 30 from account 1 + assert_ok!(ReturnToDap::::return_funds(&1, 30)); + + // Then: account 1 has 70, buffer has 30 + assert_eq!(Balances::free_balance(1), 70); + assert_eq!(Balances::free_balance(buffer), 30); + // ...and an event is emitted + System::assert_last_event(Event::::FundsReturned { from: 1, amount: 30 }.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), + sp_runtime::TokenError::FundsUnavailable + ); + }); + } + + #[test] + #[should_panic(expected = "not yet implemented")] + fn pull_from_dap_panics() { + new_test_ext().execute_with(|| { + let _ = PullFromDap::::request_funds(&1, 10); + }); + } +} From 23136726122c34c8800a8e120467fe4eb73660e6 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 1 Dec 2025 10:14:48 +0100 Subject: [PATCH 03/30] staking: redirect slash to DAP for WAH and staking-async testnet --- Cargo.lock | 2 ++ .../assets/asset-hub-westend/Cargo.toml | 4 +++ .../assets/asset-hub-westend/src/lib.rs | 3 ++ .../assets/asset-hub-westend/src/staking.rs | 6 +++- substrate/frame/dap/src/lib.rs | 29 +++++++++++++++++-- substrate/frame/staking-async/Cargo.toml | 1 + substrate/frame/staking-async/src/mock.rs | 7 ++++- 7 files changed, 48 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a6aae112f82a..37b3fd75d9ae5 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", @@ -13647,6 +13648,7 @@ dependencies = [ "log", "pallet-bags-list", "pallet-balances", + "pallet-dap", "pallet-staking-async-rc-client", "parity-scale-codec", "rand 0.8.5", 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/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index a876fa545b4b0..4cc9e8904d801 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -1397,6 +1397,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/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index 2b9a6a60fb157..b0b7c382c88ad 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -32,8 +32,9 @@ extern crate alloc; use frame_support::{ pallet_prelude::*, traits::{ - fungible::{Inspect, Mutate}, + fungible::{Balanced, Credit, Inspect, Mutate}, tokens::{FundingSink, FundingSource, Preservation}, + Imbalance, OnUnbalanced, }, PalletId, }; @@ -60,7 +61,9 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { /// The currency type. - type Currency: Inspect + Mutate; + type Currency: Inspect + + Mutate + + Balanced; } impl Pallet { @@ -118,6 +121,28 @@ impl FundingSink> for ReturnToDap { } } +/// Type alias for credit (negative imbalance - funds that were slashed/removed). +pub type CreditOf = Credit<::AccountId, ::Currency>; + +/// Implementation of OnUnbalanced that deposits slashed funds into the DAP buffer. +/// Use this as `type Slash = SlashToDap` in staking config. +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(); + + // Resolve the imbalance by depositing into the buffer account + let _ = T::Currency::resolve(&buffer, amount); + + log::debug!( + target: LOG_TARGET, + "Deposited slash of {numeric_amount:?} to DAP buffer" + ); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/substrate/frame/staking-async/Cargo.toml b/substrate/frame/staking-async/Cargo.toml index a852d8e87571b..fd8d75129468c 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 } 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 = (); } From 931cd52ea8e909ebe60abfb2e68efabe4c1814fe Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 1 Dec 2025 11:19:48 +0100 Subject: [PATCH 04/30] treasury: redirect burns to DAP --- Cargo.lock | 1 + .../asset-hub-westend/src/governance/mod.rs | 2 +- substrate/frame/dap/src/lib.rs | 30 +++++++++++++++++-- .../runtimes/parachain/Cargo.toml | 4 +++ .../runtimes/parachain/src/governance/mod.rs | 2 +- .../runtimes/parachain/src/lib.rs | 3 ++ .../runtimes/parachain/src/staking.rs | 6 +++- 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37b3fd75d9ae5..f20c9534f620a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13729,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", 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/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index b0b7c382c88ad..622185a125489 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -34,7 +34,7 @@ use frame_support::{ traits::{ fungible::{Balanced, Credit, Inspect, Mutate}, tokens::{FundingSink, FundingSource, Preservation}, - Imbalance, OnUnbalanced, + Currency, Imbalance, OnUnbalanced, }, PalletId, }; @@ -122,10 +122,11 @@ impl FundingSink> for ReturnToDap { } /// 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 that deposits slashed funds into the DAP buffer. -/// Use this as `type Slash = SlashToDap` in staking config. +/// Implementation of OnUnbalanced for the fungible::Balanced trait. +/// Use this as `type Slash = SlashToDap` in staking-async config. pub struct SlashToDap(core::marker::PhantomData); impl OnUnbalanced> for SlashToDap { @@ -143,6 +144,29 @@ impl OnUnbalanced> for SlashToDap { } } +/// Implementation of OnUnbalanced for the old Currency trait (still used by treasury). +/// Use this as `type BurnDestination = BurnToDap` e.g. in treasury config. +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::*; 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 3346ff10736ea..dddeeff03ec12 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs @@ -1196,6 +1196,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(); } From baf8ea22245a6f012ddb4efca6a6a6ab4212d8cd Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:17:29 +0000 Subject: [PATCH 05/30] Update from github-actions[bot] running command 'prdoc --audience runtime_dev --bump major' --- prdoc/pr_10481.prdoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 prdoc/pr_10481.prdoc diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc new file mode 100644 index 0000000000000..544638b3756dc --- /dev/null +++ b/prdoc/pr_10481.prdoc @@ -0,0 +1,16 @@ +title: '[DRAFT] PoC DAP' +doc: +- audience: Runtime Dev + description: |- + Initial PoC PR to play around with Dynamic Allocation Proposal here: https://hackmd.io/a0KSL4XHQ1WvcGZlKykGzg + + So far, I am just currently removing treasury burn and staking slashing, redirecting them to the DAP / issuance buffer. +crates: +- name: frame-support + bump: major +- name: pallet-dap + bump: major +- name: asset-hub-westend-runtime + bump: major +- name: pallet-staking-async + bump: major From 2e601e77da41517752cd8adc81ee9f41ab600cbe Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 1 Dec 2025 13:24:58 +0100 Subject: [PATCH 06/30] zepter --- substrate/frame/staking-async/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/substrate/frame/staking-async/Cargo.toml b/substrate/frame/staking-async/Cargo.toml index fd8d75129468c..d3a22dfc6a902 100644 --- a/substrate/frame/staking-async/Cargo.toml +++ b/substrate/frame/staking-async/Cargo.toml @@ -80,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", @@ -90,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", ] From f6a542ef6c4e7158c8343c45cc2f1ad900945762 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 3 Dec 2025 14:49:17 +0100 Subject: [PATCH 07/30] pallet-dap-satellite: introduce minimal implementation and use it coretime-westend runtime --- Cargo.lock | 18 + Cargo.toml | 2 + .../coretime/coretime-westend/Cargo.toml | 4 + .../coretime/coretime-westend/src/coretime.rs | 86 +---- .../coretime/coretime-westend/src/lib.rs | 7 + prdoc/pr_10481.prdoc | 21 +- substrate/frame/dap-satellite/Cargo.toml | 60 +++ substrate/frame/dap-satellite/src/lib.rs | 365 ++++++++++++++++++ 8 files changed, 488 insertions(+), 75 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 f20c9534f620a..bf55b2f2e81f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3919,6 +3919,7 @@ dependencies = [ "pallet-balances", "pallet-broker", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-proxy", @@ -12210,6 +12211,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" 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/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/coretime.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs index c9cd7f80a61ae..0707b7cedde5e 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 then periodically 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"); - }, - } - } + // With DAP satellite, revenue is already accumulated in the satellite account via + // CoretimeRevenueToSatellite (OnRevenue handler). The satellite pallet then sends + // 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 671ccb8c9c0d5..419c978aa4fb1 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs @@ -612,6 +612,10 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + pub struct BrokerMigrationV4BlockConversion; impl pallet_broker::migration::v4::BlockToRelayHeightConversion @@ -667,6 +671,9 @@ construct_runtime!( // The main stage. Broker: pallet_broker = 50, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub + DapSatellite: pallet_dap_satellite = 60, + // Sudo Sudo: pallet_sudo = 100, } diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index 544638b3756dc..2471f995b00f5 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -1,16 +1,33 @@ -title: '[DRAFT] PoC DAP' +title: 'Introduced pallet-dap' doc: - audience: Runtime Dev description: |- Initial PoC PR to play around with Dynamic Allocation Proposal here: https://hackmd.io/a0KSL4XHQ1WvcGZlKykGzg - So far, I am just currently removing treasury burn and staking slashing, redirecting them to the DAP / issuance buffer. + Initial version supports the following: + - Introduced FundingSink and FundingSource traits in frame-support + - Introduced pallet-dap with minimal implementation of FundingSink trait only. This pallet is meant to be + deployed on AssetHub as the central DAP pallet. + - Redirected treasury unspent to pallet-dap instead of burning + - Redirected staking slashes to pallet-dap instead of burning + - Introduced pallet-dap-satellite, to collect funds that otherwise would be burned (e.g. tx fees, + coretime revenues, ...) into a local satellite account. These funds are accumulated locally and + will be sent to pallet-dap periodically in future iterations. This pallet is meant to be deployed + on system chains like Coretime, People and BridheHub and on the RelayChain. It should not be deployed + on AssetHub, which hosts the central DAP pallet. + + asset-hub-westend-runtime and staking-async test runtime have been modified to include pallet-dap and to send part of slashing and treasury's unspent funds to pallet-dap. + coretime-westend-runtime has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. crates: - name: frame-support bump: major - name: pallet-dap bump: major +- name: pallet-dap-satellite + bump: major - name: asset-hub-westend-runtime bump: major - name: pallet-staking-async bump: major +- name: coretime-westend-runtime + bump: major 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..2430ec06364f9 --- /dev/null +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -0,0 +1,365 @@ +// 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) │ │ +//! └───────────────────────┼───────────────────────┘ +//! │ +//! ▼ +//! ┌─────────────────┐ +//! │ 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. +//! - `SinkToSatellite`: Implementation of `OnUnbalanced` for the old `Currency` trait, useful for +//! fee handlers and other pallets that use imbalances. +//! +//! **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_coretime::Config for Runtime { +//! type FundingSink = pallet_dap_satellite::AccumulateInSatellite; +//! } +//! +//! // For fee handlers using OnUnbalanced +//! type FeeDestination = pallet_dap_satellite::SinkToSatellite; +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Balanced, Credit, Inspect, Mutate}, + tokens::{FundingSink, Preservation}, + Currency, Imbalance, OnUnbalanced, + }, + PalletId, +}; + +pub use pallet::*; + +const LOG_TARGET: &str = "runtime::dap-satellite"; + +/// The DAP Satellite pallet ID, used to derive the satellite account. +pub const DAP_SATELLITE_PALLET_ID: PalletId = PalletId(*b"dap/satl"); + +/// 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; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency type. + type Currency: Inspect + + Mutate + + Balanced; + } + + 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 { + DAP_SATELLITE_PALLET_ID.into_account_truncating() + } + } + + #[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_coretime::Config for Runtime { +/// type FundingSink = AccumulateInSatellite; +/// } +/// ``` +pub struct AccumulateInSatellite(core::marker::PhantomData); + +impl FundingSink> for AccumulateInSatellite { + fn return_funds(source: &T::AccountId, amount: BalanceOf) -> Result<(), DispatchError> { + let satellite = Pallet::::satellite_account(); + + T::Currency::transfer(source, &satellite, amount, Preservation::Preserve)?; + + Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Accumulated {amount:?} from {source:?} in satellite account" + ); + + Ok(()) + } +} + +/// 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. +/// +/// # 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(); + + // Resolve the imbalance by depositing into the satellite account + let _ = T::Currency::resolve(&satellite, amount); + + log::debug!( + target: LOG_TARGET, + "Deposited {numeric_amount:?} to satellite account (fungible)" + ); + } +} + +/// 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). +/// +/// # 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::{ + assert_noop, assert_ok, derive_impl, sp_runtime::traits::AccountIdConversion, + traits::tokens::FundingSink, + }; + use sp_runtime::BuildStorage; + + 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; + } + + 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(); + t.into() + } + + #[test] + fn satellite_account_is_derived_from_pallet_id() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + let expected: u64 = DAP_SATELLITE_PALLET_ID.into_account_truncating(); + assert_eq!(satellite, expected); + }); + } + + #[test] + fn accumulate_in_satellite_transfers_to_satellite_account() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + 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: accumulate 30 from account 1 + assert_ok!(AccumulateInSatellite::::return_funds(&1, 30)); + + // Then: account 1 has 70, satellite has 30 + assert_eq!(Balances::free_balance(1), 70); + assert_eq!(Balances::free_balance(satellite), 30); + // ...and an event is emitted + System::assert_last_event( + Event::::FundsAccumulated { from: 1, amount: 30 }.into(), + ); + }); + } + + #[test] + fn accumulate_multiple_times_adds_up() { + 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: accumulate from multiple accounts + assert_ok!(AccumulateInSatellite::::return_funds(&1, 20)); + assert_ok!(AccumulateInSatellite::::return_funds(&2, 50)); + assert_ok!(AccumulateInSatellite::::return_funds(&3, 100)); + + // Then: satellite has accumulated all funds + assert_eq!(Balances::free_balance(satellite), 170); + assert_eq!(Balances::free_balance(1), 80); + assert_eq!(Balances::free_balance(2), 150); + assert_eq!(Balances::free_balance(3), 200); + }); + } + + #[test] + fn accumulate_fails_with_insufficient_balance() { + new_test_ext().execute_with(|| { + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: try to accumulate 150 (more than balance) + // Then: fails + assert_noop!( + AccumulateInSatellite::::return_funds(&1, 150), + sp_runtime::TokenError::FundsUnavailable + ); + }); + } +} From d7f413a20dc0aacf4a2eb29e945c2e34b549e78f Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 3 Dec 2025 21:02:36 +0100 Subject: [PATCH 08/30] westend-runtime: integrate with pallet-dap-satellite Integrate westend-runtime with pallet-dap-satellite. Preserve the pre-existing logic for which: - 100% of tx fees -> block author - 100% of tips -> block author This is an overkill for Westend but is just an exercise in preparation of what we will do for Polkadot and Kusama (e.g. X% / 100 -X% split) --- Cargo.lock | 1 + polkadot/runtime/westend/Cargo.toml | 4 +++ polkadot/runtime/westend/src/lib.rs | 51 ++++++++++++++++++++++++++--- prdoc/pr_10481.prdoc | 6 ++-- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf55b2f2e81f7..beba1547443da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27552,6 +27552,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/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 bdef88df7e8e1..e88cb21a3777b 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -35,10 +35,11 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::HoldConsideration, tokens::UnityOrOuterConversion, AsEnsureOriginWithArg, - ConstU32, Contains, EitherOf, EitherOfDiverse, EnsureOriginWithArg, FromContains, - InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, Nothing, ProcessMessage, - ProcessMessageError, VariantCountOf, WithdrawReasons, + fungible::{Credit, HoldConsideration}, + tokens::UnityOrOuterConversion, + AsEnsureOriginWithArg, ConstU32, Contains, EitherOf, EitherOfDiverse, EnsureOriginWithArg, + FromContains, Imbalance, InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, Nothing, + OnUnbalanced, ProcessMessage, ProcessMessageError, VariantCountOf, WithdrawReasons, }, weights::{ConstantMultiplier, WeightMeter}, PalletId, @@ -479,11 +480,43 @@ 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) + /// Polkadot/Kusama: configurable (e.g., 80% to DAP, 20% to author) + pub const DapSatelliteFeePercent: u32 = 0; +} + +/// Fee handler that splits fees between DAP satellite and block author. +/// - `DapSatelliteFeePercent`% of fees go to DAP satellite +/// - (100 - `DapSatelliteFeePercent`)% of fees go to block author +/// - 100% of tips go to block author +pub struct DealWithFeesSatellite; +impl OnUnbalanced> for DealWithFeesSatellite { + fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { + if let Some(fees) = fees_then_tips.next() { + let dap_percent = DapSatelliteFeePercent::get(); + let author_percent = 100u32.saturating_sub(dap_percent); + let mut split = fees.ration(dap_percent, author_percent); + if let Some(tips) = fees_then_tips.next() { + // For tips: 100% to author + tips.merge_into(&mut split.1); + } + // Send configured % to DAP satellite (if any) + if dap_percent > 0 { + as OnUnbalanced<_>>::on_unbalanced( + split.0, + ); + } + // Send remainder + tips to author + as OnUnbalanced<_>>::on_unbalanced(split.1); + } + } } 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 +1780,10 @@ impl pallet_root_offences::Config for Runtime { type ReportOffence = Offences; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + parameter_types! { pub MbmServiceWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; } @@ -2027,6 +2064,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/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index 2471f995b00f5..b6f3087e5fc4b 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -16,8 +16,10 @@ doc: on system chains like Coretime, People and BridheHub and on the RelayChain. It should not be deployed on AssetHub, which hosts the central DAP pallet. - asset-hub-westend-runtime and staking-async test runtime have been modified to include pallet-dap and to send part of slashing and treasury's unspent funds to pallet-dap. - coretime-westend-runtime has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. + Runtime changes: + - westend-runtime has been updated to include pallet-dap-satellite. It preserves the original behavior for which 100% of tx fees are redirected to the block author. + - asset-hub-westend-runtime and staking-async test runtime have been modified to include pallet-dap and to send part of slashing and treasury's unspent funds to pallet-dap. + - coretime-westend-runtime has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. crates: - name: frame-support bump: major From b894db3c85ea86cdc125c00759f1998480835ed6 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 3 Dec 2025 22:32:27 +0100 Subject: [PATCH 09/30] Integrated People, Bridge Hub, Collective Westend with DAP satellite We preserve the original behavior in Westend i.e. 100% fees to staking pot. But while doing so, we make it configurable. --- Cargo.lock | 3 ++ .../bridge-hubs/bridge-hub-westend/Cargo.toml | 4 ++ .../bridge-hubs/bridge-hub-westend/src/lib.rs | 40 ++++++++++++++++- .../collectives-westend/Cargo.toml | 4 ++ .../collectives-westend/src/lib.rs | 40 +++++++++++++++-- .../runtimes/people/people-westend/Cargo.toml | 4 ++ .../runtimes/people/people-westend/src/lib.rs | 44 +++++++++++++++++-- prdoc/pr_10481.prdoc | 26 ++++++++--- 8 files changed, 150 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index beba1547443da..8464482c66527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2855,6 +2855,7 @@ dependencies = [ "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-session", @@ -3473,6 +3474,7 @@ dependencies = [ "pallet-collective", "pallet-collective-content", "pallet-core-fellowship", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-preimage", @@ -14935,6 +14937,7 @@ dependencies = [ "pallet-authorship", "pallet-balances", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-identity", "pallet-message-queue", "pallet-migrations", 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 6e55033bc20ba..ca1d37e096a32 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 @@ -63,7 +63,10 @@ use frame_support::{ dispatch::DispatchClass, genesis_builder_helper::{build_state, get_preset}, parameter_types, - traits::{ConstBool, ConstU32, ConstU64, ConstU8, Get, TransformOrigin}, + traits::{ + fungible::Credit, ConstBool, ConstU32, ConstU64, ConstU8, Get, Imbalance, OnUnbalanced, + TransformOrigin, + }, weights::{ConstantMultiplier, Weight}, PalletId, }; @@ -359,12 +362,38 @@ impl pallet_balances::Config for Runtime { 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; +} + +/// Handles fees with a configurable split between DAP satellite and staking pot. +/// `DapSatelliteFeePercent` of fees go to DAP, the remainder goes to staking pot. +/// Tips always go 100% to staking pot. +pub struct DealWithFeesSatellite; +impl OnUnbalanced> for DealWithFeesSatellite { + fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { + if let Some(fees) = fees_then_tips.next() { + let dap_percent = DapSatelliteFeePercent::get(); + let staking_percent = 100u32.saturating_sub(dap_percent); + let mut split = fees.ration(dap_percent, staking_percent); + if let Some(tips) = fees_then_tips.next() { + // Tips go 100% to staking pot. + tips.merge_into(&mut split.1); + } + if dap_percent > 0 { + as OnUnbalanced<_>>::on_unbalanced( + split.0, + ); + } + as OnUnbalanced<_>>::on_unbalanced(split.1); + } + } } 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 +581,10 @@ impl pallet_utility::Config for Runtime { type WeightInfo = weights::pallet_utility::WeightInfo; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -583,6 +616,9 @@ construct_runtime!( Utility: pallet_utility = 40, Multisig: pallet_multisig = 36, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 60, + // Bridging stuff. BridgeRelayers: pallet_bridge_relayers = 41, BridgeRococoGrandpa: pallet_bridge_grandpa:: = 42, 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 85521a8db084e..246b4f042449d 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -77,8 +77,9 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::HoldConsideration, ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, - InstanceFilter, LinearStoragePrice, TransformOrigin, + fungible::{Credit, HoldConsideration}, + ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, Imbalance, InstanceFilter, + LinearStoragePrice, OnUnbalanced, TransformOrigin, }, weights::{ConstantMultiplier, Weight}, PalletId, @@ -239,12 +240,38 @@ impl pallet_balances::Config for Runtime { 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; +} + +/// Handles fees with a configurable split between DAP satellite and staking pot. +/// `DapSatelliteFeePercent` of fees go to DAP, the remainder goes to staking pot. +/// Tips always go 100% to staking pot. +pub struct DealWithFeesSatellite; +impl OnUnbalanced> for DealWithFeesSatellite { + fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { + if let Some(fees) = fees_then_tips.next() { + let dap_percent = DapSatelliteFeePercent::get(); + let staking_percent = 100u32.saturating_sub(dap_percent); + let mut split = fees.ration(dap_percent, staking_percent); + if let Some(tips) = fees_then_tips.next() { + // Tips go 100% to staking pot. + tips.merge_into(&mut split.1); + } + if dap_percent > 0 { + as OnUnbalanced<_>>::on_unbalanced( + split.0, + ); + } + as OnUnbalanced<_>>::on_unbalanced(split.1); + } + } } 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; @@ -748,6 +775,9 @@ construct_runtime!( StateTrieMigration: pallet_state_trie_migration = 80, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 85, + // The Secretary Collective // pub type SecretaryCollectiveInstance = pallet_ranked_collective::instance3; SecretaryCollective: pallet_ranked_collective:: = 90, @@ -1383,6 +1413,10 @@ impl pallet_state_trie_migration::Config for Runtime { type MaxKeyLen = MigrationMaxKeyLen; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + frame_support::ord_parameter_types! { pub const MigController: AccountId = AccountId::from(hex_literal::hex!("8458ed39dc4b6f6c7255f7bc42be50c2967db126357c999d44e12ca7ac80dc52")); pub const RootMigController: AccountId = AccountId::from(hex_literal::hex!("8458ed39dc4b6f6c7255f7bc42be50c2967db126357c999d44e12ca7ac80dc52")); 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 ed613f38589b8..8baae68321f28 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs @@ -35,8 +35,8 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, Everything, InstanceFilter, - TransformOrigin, + fungible::Credit, ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, Everything, + Imbalance, InstanceFilter, OnUnbalanced, TransformOrigin, }, weights::{ConstantMultiplier, Weight}, PalletId, @@ -249,12 +249,43 @@ impl pallet_balances::Config for Runtime { parameter_types! { /// Relay Chain `TransactionByteFee` / 10. pub const TransactionByteFee: Balance = MILLICENTS; + /// Percentage of fees that go to DAP satellite (0-100). + /// The remainder goes to the staking pot. Tips always go 100% to staking pot. + /// Set to 0 to preserve original behavior (100% to staking pot). + pub const DapSatelliteFeePercent: u32 = 0; +} + +/// Fee handler that splits fees between DAP satellite and staking pot. +/// - `DapSatelliteFeePercent`% of fees go to DAP satellite +/// - (100 - `DapSatelliteFeePercent`)% of fees go to staking pot +/// - 100% of tips go to staking pot +pub struct DealWithFeesSatellite; +impl OnUnbalanced> for DealWithFeesSatellite { + fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { + if let Some(fees) = fees_then_tips.next() { + let dap_percent = DapSatelliteFeePercent::get(); + let staking_percent = 100u32.saturating_sub(dap_percent); + let mut split = fees.ration(dap_percent, staking_percent); + if let Some(tips) = fees_then_tips.next() { + // Tips: 100% to staking pot + tips.merge_into(&mut split.1); + } + // Send configured % to DAP satellite (if any) + if dap_percent > 0 { + as OnUnbalanced<_>>::on_unbalanced( + split.0, + ); + } + // Send remainder + tips to staking pot + as OnUnbalanced<_>>::on_unbalanced(split.1); + } + } } 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; @@ -578,6 +609,10 @@ impl pallet_migrations::Config for Runtime { type WeightInfo = weights::pallet_migrations::WeightInfo; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -614,6 +649,9 @@ construct_runtime!( // The main stage. Identity: pallet_identity = 50, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 60, + // Migrations pallet MultiBlockMigrations: pallet_migrations = 98, diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index b6f3087e5fc4b..50b1a6ea1b1c8 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -8,21 +8,25 @@ doc: - Introduced FundingSink and FundingSource traits in frame-support - Introduced pallet-dap with minimal implementation of FundingSink trait only. This pallet is meant to be deployed on AssetHub as the central DAP pallet. - - Redirected treasury unspent to pallet-dap instead of burning - - Redirected staking slashes to pallet-dap instead of burning - Introduced pallet-dap-satellite, to collect funds that otherwise would be burned (e.g. tx fees, coretime revenues, ...) into a local satellite account. These funds are accumulated locally and will be sent to pallet-dap periodically in future iterations. This pallet is meant to be deployed - on system chains like Coretime, People and BridheHub and on the RelayChain. It should not be deployed + on system chains like Coretime, People and BridgeHub and on the RelayChain. It should not be deployed on AssetHub, which hosts the central DAP pallet. Runtime changes: - - westend-runtime has been updated to include pallet-dap-satellite. It preserves the original behavior for which 100% of tx fees are redirected to the block author. - - asset-hub-westend-runtime and staking-async test runtime have been modified to include pallet-dap and to send part of slashing and treasury's unspent funds to pallet-dap. + - AH: redirected treasury unspent to pallet-dap instead of burning + - AH: redirected staking slashes to pallet-dap instead of burning + - RC: westend-runtime has been updated to include pallet-dap-satellite. It preserves the original behavior for which 100% of tx fees are redirected to the block author. - coretime-westend-runtime has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. + - people-westend-runtime, bridge-hub-westend-runtime and collective-westend-runtime have been updated to include + pallet-dap-satellite. We have preserved the original behavior for which 100% of tx fees are redirected to the staking pot. + + Each of these chains with DapSatellite now includes a DealWithFeesSatellite struct that splits fees: + - X% goes to the DAP satellite, while (100-X)% is allocated to the staking pot via DealWithFees. crates: - name: frame-support - bump: major + bump: minor - name: pallet-dap bump: major - name: pallet-dap-satellite @@ -30,6 +34,14 @@ crates: - name: asset-hub-westend-runtime bump: major - name: pallet-staking-async - bump: major + bump: patch - name: coretime-westend-runtime bump: major +- name: westend-runtime + bump: major +- name: bridge-hub-westend-runtime + bump: major +- name: collectives-westend-runtime + bump: major +- name: people-westend-runtime + bump: major From a980371c4d60f3fd7d575a0c78ac36575ac0655c Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 3 Dec 2025 23:18:23 +0100 Subject: [PATCH 10/30] dap-satellite: introduce a configurable fee handler splitting fees between satellite and another destination --- .../bridge-hubs/bridge-hub-westend/src/lib.rs | 31 ++--------- .../collectives-westend/src/lib.rs | 31 ++--------- .../runtimes/people/people-westend/src/lib.rs | 31 ++--------- polkadot/runtime/westend/src/lib.rs | 36 +++---------- prdoc/pr_10481.prdoc | 10 ++-- substrate/frame/dap-satellite/src/lib.rs | 52 +++++++++++++++++++ 6 files changed, 75 insertions(+), 116 deletions(-) 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 ca1d37e096a32..aa5dac9e94a2f 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 @@ -63,10 +63,7 @@ use frame_support::{ dispatch::DispatchClass, genesis_builder_helper::{build_state, get_preset}, parameter_types, - traits::{ - fungible::Credit, ConstBool, ConstU32, ConstU64, ConstU8, Get, Imbalance, OnUnbalanced, - TransformOrigin, - }, + traits::{ConstBool, ConstU32, ConstU64, ConstU8, Get, TransformOrigin}, weights::{ConstantMultiplier, Weight}, PalletId, }; @@ -366,29 +363,9 @@ parameter_types! { pub const DapSatelliteFeePercent: u32 = 0; } -/// Handles fees with a configurable split between DAP satellite and staking pot. -/// `DapSatelliteFeePercent` of fees go to DAP, the remainder goes to staking pot. -/// Tips always go 100% to staking pot. -pub struct DealWithFeesSatellite; -impl OnUnbalanced> for DealWithFeesSatellite { - fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { - if let Some(fees) = fees_then_tips.next() { - let dap_percent = DapSatelliteFeePercent::get(); - let staking_percent = 100u32.saturating_sub(dap_percent); - let mut split = fees.ration(dap_percent, staking_percent); - if let Some(tips) = fees_then_tips.next() { - // Tips go 100% to staking pot. - tips.merge_into(&mut split.1); - } - if dap_percent > 0 { - as OnUnbalanced<_>>::on_unbalanced( - split.0, - ); - } - as OnUnbalanced<_>>::on_unbalanced(split.1); - } - } -} +/// 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; diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs index 246b4f042449d..dd2a0c713f8d9 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -77,9 +77,8 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::{Credit, HoldConsideration}, - ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, Imbalance, InstanceFilter, - LinearStoragePrice, OnUnbalanced, TransformOrigin, + fungible::HoldConsideration, ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, + InstanceFilter, LinearStoragePrice, TransformOrigin, }, weights::{ConstantMultiplier, Weight}, PalletId, @@ -244,29 +243,9 @@ parameter_types! { pub const DapSatelliteFeePercent: u32 = 0; } -/// Handles fees with a configurable split between DAP satellite and staking pot. -/// `DapSatelliteFeePercent` of fees go to DAP, the remainder goes to staking pot. -/// Tips always go 100% to staking pot. -pub struct DealWithFeesSatellite; -impl OnUnbalanced> for DealWithFeesSatellite { - fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { - if let Some(fees) = fees_then_tips.next() { - let dap_percent = DapSatelliteFeePercent::get(); - let staking_percent = 100u32.saturating_sub(dap_percent); - let mut split = fees.ration(dap_percent, staking_percent); - if let Some(tips) = fees_then_tips.next() { - // Tips go 100% to staking pot. - tips.merge_into(&mut split.1); - } - if dap_percent > 0 { - as OnUnbalanced<_>>::on_unbalanced( - split.0, - ); - } - as OnUnbalanced<_>>::on_unbalanced(split.1); - } - } -} +/// 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; diff --git a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs index 8baae68321f28..b670dc39f01fa 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs @@ -35,8 +35,8 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::Credit, ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, Everything, - Imbalance, InstanceFilter, OnUnbalanced, TransformOrigin, + ConstBool, ConstU32, ConstU64, ConstU8, EitherOfDiverse, Everything, InstanceFilter, + TransformOrigin, }, weights::{ConstantMultiplier, Weight}, PalletId, @@ -256,31 +256,8 @@ parameter_types! { } /// Fee handler that splits fees between DAP satellite and staking pot. -/// - `DapSatelliteFeePercent`% of fees go to DAP satellite -/// - (100 - `DapSatelliteFeePercent`)% of fees go to staking pot -/// - 100% of tips go to staking pot -pub struct DealWithFeesSatellite; -impl OnUnbalanced> for DealWithFeesSatellite { - fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { - if let Some(fees) = fees_then_tips.next() { - let dap_percent = DapSatelliteFeePercent::get(); - let staking_percent = 100u32.saturating_sub(dap_percent); - let mut split = fees.ration(dap_percent, staking_percent); - if let Some(tips) = fees_then_tips.next() { - // Tips: 100% to staking pot - tips.merge_into(&mut split.1); - } - // Send configured % to DAP satellite (if any) - if dap_percent > 0 { - as OnUnbalanced<_>>::on_unbalanced( - split.0, - ); - } - // Send remainder + tips to staking pot - as OnUnbalanced<_>>::on_unbalanced(split.1); - } - } -} +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index e88cb21a3777b..87d6adf701b45 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -35,11 +35,10 @@ use frame_support::{ genesis_builder_helper::{build_state, get_preset}, parameter_types, traits::{ - fungible::{Credit, HoldConsideration}, - tokens::UnityOrOuterConversion, - AsEnsureOriginWithArg, ConstU32, Contains, EitherOf, EitherOfDiverse, EnsureOriginWithArg, - FromContains, Imbalance, InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, Nothing, - OnUnbalanced, ProcessMessage, ProcessMessageError, VariantCountOf, WithdrawReasons, + fungible::HoldConsideration, tokens::UnityOrOuterConversion, AsEnsureOriginWithArg, + ConstU32, Contains, EitherOf, EitherOfDiverse, EnsureOriginWithArg, FromContains, + InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, Nothing, ProcessMessage, + ProcessMessageError, VariantCountOf, WithdrawReasons, }, weights::{ConstantMultiplier, WeightMeter}, PalletId, @@ -488,31 +487,8 @@ parameter_types! { } /// Fee handler that splits fees between DAP satellite and block author. -/// - `DapSatelliteFeePercent`% of fees go to DAP satellite -/// - (100 - `DapSatelliteFeePercent`)% of fees go to block author -/// - 100% of tips go to block author -pub struct DealWithFeesSatellite; -impl OnUnbalanced> for DealWithFeesSatellite { - fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { - if let Some(fees) = fees_then_tips.next() { - let dap_percent = DapSatelliteFeePercent::get(); - let author_percent = 100u32.saturating_sub(dap_percent); - let mut split = fees.ration(dap_percent, author_percent); - if let Some(tips) = fees_then_tips.next() { - // For tips: 100% to author - tips.merge_into(&mut split.1); - } - // Send configured % to DAP satellite (if any) - if dap_percent > 0 { - as OnUnbalanced<_>>::on_unbalanced( - split.0, - ); - } - // Send remainder + tips to author - as OnUnbalanced<_>>::on_unbalanced(split.1); - } - } -} +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index 50b1a6ea1b1c8..e4d7134a5cae1 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -10,9 +10,10 @@ doc: deployed on AssetHub as the central DAP pallet. - Introduced pallet-dap-satellite, to collect funds that otherwise would be burned (e.g. tx fees, coretime revenues, ...) into a local satellite account. These funds are accumulated locally and - will be sent to pallet-dap periodically in future iterations. This pallet is meant to be deployed - on system chains like Coretime, People and BridgeHub and on the RelayChain. It should not be deployed - on AssetHub, which hosts the central DAP pallet. + will be sent to pallet-dap periodically in future iterations. The pallet implements a configurable fee handler + that splits fees between DAP satellite and another destination. + This pallet is meant to be deployed on system chains like Coretime, People and BridgeHub and on the RelayChain. + It should not be deployed on AssetHub, which hosts the central DAP pallet. Runtime changes: - AH: redirected treasury unspent to pallet-dap instead of burning @@ -21,9 +22,6 @@ doc: - coretime-westend-runtime has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. - people-westend-runtime, bridge-hub-westend-runtime and collective-westend-runtime have been updated to include pallet-dap-satellite. We have preserved the original behavior for which 100% of tx fees are redirected to the staking pot. - - Each of these chains with DapSatellite now includes a DealWithFeesSatellite struct that splits fees: - - X% goes to the DAP satellite, while (100-X)% is allocated to the staking pot via DealWithFees. crates: - name: frame-support bump: minor diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 2430ec06364f9..2d014dc86d432 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -198,6 +198,58 @@ impl OnUnbalanced> for SlashToSatellite { } } +/// 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 From 95f07dbf621e7c45503a38219573ee9d8ee9b95e Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Wed, 3 Dec 2025 23:46:08 +0100 Subject: [PATCH 11/30] fix umbrella crate --- Cargo.lock | 2 ++ umbrella/Cargo.toml | 18 ++++++++++++++++++ umbrella/src/lib.rs | 8 ++++++++ 3 files changed, 28 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8464482c66527..54dcd7e43a11c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16610,6 +16610,8 @@ dependencies = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 5814cd70c40b4..3ff16fe85d96a 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -85,6 +85,8 @@ 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", "pallet-derivatives?/std", @@ -281,6 +283,8 @@ 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", "pallet-derivatives?/runtime-benchmarks", @@ -422,6 +426,8 @@ 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", "pallet-derivatives?/try-runtime", @@ -635,6 +641,8 @@ runtime-full = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", @@ -1415,6 +1423,16 @@ default-features = false optional = true path = "../substrate/frame/core-fellowship" +[dependencies.pallet-dap] +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 125939c2edf76..a0bd94d04169b 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -443,6 +443,14 @@ 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 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 5dafb04c700762221a92249023a1774d3f574d48 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Thu, 4 Dec 2025 09:39:08 +0100 Subject: [PATCH 12/30] fix prdoc --- prdoc/pr_10481.prdoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index e4d7134a5cae1..d3ef611c30e7e 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -26,8 +26,10 @@ crates: - name: frame-support bump: minor - name: pallet-dap - bump: major + bump: patch - name: pallet-dap-satellite + bump: patch +- name: polkadot-sdk bump: major - name: asset-hub-westend-runtime bump: major From 4be39f5dedb5bc6cd8c2642033e63e272259389d Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Thu, 4 Dec 2025 15:14:36 +0100 Subject: [PATCH 13/30] pallet-balances: integration with DAP --- .../assets/asset-hub-westend/src/lib.rs | 1 + prdoc/pr_10481.prdoc | 13 ++++--- substrate/frame/balances/src/lib.rs | 32 ++++++++++------ substrate/frame/balances/src/tests/mod.rs | 5 ++- substrate/frame/dap-satellite/src/lib.rs | 22 +++++++---- substrate/frame/dap/src/lib.rs | 12 ++++-- .../support/src/traits/tokens/funding.rs | 38 ++++++++++++++----- 7 files changed, 85 insertions(+), 38 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 4cc9e8904d801..f1096872a1bf1 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/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index d3ef611c30e7e..ce4f7143610e4 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -16,15 +16,18 @@ doc: It should not be deployed on AssetHub, which hosts the central DAP pallet. Runtime changes: - - AH: redirected treasury unspent to pallet-dap instead of burning - - AH: redirected staking slashes to pallet-dap instead of burning + - asset-hub-westend: redirected treasury unspent to pallet-dap instead of burning + - asset-hub-westend: redirected staking slashes to pallet-dap instead of burning + - asset-hub-westend: all user-initiated burns go to DAP buffer instead of reducing total issuance. - RC: westend-runtime has been updated to include pallet-dap-satellite. It preserves the original behavior for which 100% of tx fees are redirected to the block author. - - coretime-westend-runtime has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. - - people-westend-runtime, bridge-hub-westend-runtime and collective-westend-runtime have been updated to include - pallet-dap-satellite. We have preserved the original behavior for which 100% of tx fees are redirected to the staking pot. + - coretime-westend has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. + - people-westend, bridge-hub-westend and collective-westend have been updated to include pallet-dap-satellite. + The original behavior for which 100% of tx fees are redirected to the staking pot has been preserved. crates: - name: frame-support bump: minor +- name: pallet-balances + bump: major - name: pallet-dap bump: patch - name: pallet-dap-satellite diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index ebdb70f83dace..617d03703bc68 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -206,7 +206,11 @@ 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::*; @@ -245,6 +249,7 @@ pub mod pallet { type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = (); } } @@ -333,6 +338,14 @@ pub mod pallet { Self::AccountId, Self::Balance, >; + + /// Handler for user-initiated burns via the `burn` extrinsic. + /// + /// By default, burns is no-op. Runtimes can configure this + /// to redirect burned funds to a buffer account instead (e.g., DAP buffer on Asset Hub) or + /// to reduce total issuance (`DirectBurn`). + #[pallet::no_default_bounds] + type BurnDestination: FundingSink; } /// The in-code storage version. @@ -852,8 +865,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`: + /// - By default (`()`), is no-op - used for testing. + /// - Runtimes configure alternative destinations (e.g., DAP buffer) or `DirectBurn` (total + /// issuance is reduced). #[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 +877,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(()) } } diff --git a/substrate/frame/balances/src/tests/mod.rs b/substrate/frame/balances/src/tests/mod.rs index 155f78884d122..577a60d8c155c 100644 --- a/substrate/frame/balances/src/tests/mod.rs +++ b/substrate/frame/balances/src/tests/mod.rs @@ -29,8 +29,8 @@ use frame_support::{ dispatch::{DispatchInfo, GetDispatchInfo}, parameter_types, traits::{ - fungible, ConstU32, ConstU8, Imbalance as ImbalanceT, OnUnbalanced, StorageMapShim, - StoredMap, VariantCount, VariantCountOf, WhitelistedStorageKeys, + fungible, tokens::DirectBurn, ConstU32, ConstU8, Imbalance as ImbalanceT, OnUnbalanced, + StorageMapShim, StoredMap, VariantCount, VariantCountOf, WhitelistedStorageKeys, }, weights::{IdentityFee, Weight}, }; @@ -130,6 +130,7 @@ impl Config for Test { type RuntimeFreezeReason = TestId; type FreezeIdentifier = TestId; type MaxFreezes = VariantCountOf; + type BurnDestination = DirectBurn; } #[derive(Clone)] diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 2d014dc86d432..3ce27d60d190d 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -149,10 +149,14 @@ pub mod pallet { pub struct AccumulateInSatellite(core::marker::PhantomData); impl FundingSink> for AccumulateInSatellite { - fn return_funds(source: &T::AccountId, amount: BalanceOf) -> Result<(), DispatchError> { + fn return_funds( + source: &T::AccountId, + amount: BalanceOf, + preservation: Preservation, + ) -> Result<(), DispatchError> { let satellite = Pallet::::satellite_account(); - T::Currency::transfer(source, &satellite, amount, Preservation::Preserve)?; + T::Currency::transfer(source, &satellite, amount, preservation)?; Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }); @@ -366,7 +370,7 @@ mod tests { assert_eq!(Balances::free_balance(satellite), 0); // When: accumulate 30 from account 1 - assert_ok!(AccumulateInSatellite::::return_funds(&1, 30)); + assert_ok!(AccumulateInSatellite::::return_funds(&1, 30, Preservation::Preserve)); // Then: account 1 has 70, satellite has 30 assert_eq!(Balances::free_balance(1), 70); @@ -388,9 +392,13 @@ mod tests { assert_eq!(Balances::free_balance(satellite), 0); // When: accumulate from multiple accounts - assert_ok!(AccumulateInSatellite::::return_funds(&1, 20)); - assert_ok!(AccumulateInSatellite::::return_funds(&2, 50)); - assert_ok!(AccumulateInSatellite::::return_funds(&3, 100)); + assert_ok!(AccumulateInSatellite::::return_funds(&1, 20, Preservation::Preserve)); + assert_ok!(AccumulateInSatellite::::return_funds(&2, 50, Preservation::Preserve)); + assert_ok!(AccumulateInSatellite::::return_funds( + &3, + 100, + Preservation::Preserve + )); // Then: satellite has accumulated all funds assert_eq!(Balances::free_balance(satellite), 170); @@ -409,7 +417,7 @@ mod tests { // When: try to accumulate 150 (more than balance) // Then: fails assert_noop!( - AccumulateInSatellite::::return_funds(&1, 150), + AccumulateInSatellite::::return_funds(&1, 150, Preservation::Preserve), sp_runtime::TokenError::FundsUnavailable ); }); diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index 622185a125489..68bb5b2a5ca1d 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -105,10 +105,14 @@ impl FundingSource> for PullFromDap { pub struct ReturnToDap(core::marker::PhantomData); impl FundingSink> for ReturnToDap { - fn return_funds(source: &T::AccountId, amount: BalanceOf) -> Result<(), DispatchError> { + fn return_funds( + source: &T::AccountId, + amount: BalanceOf, + preservation: Preservation, + ) -> Result<(), DispatchError> { let buffer = Pallet::::buffer_account(); - T::Currency::transfer(source, &buffer, amount, Preservation::Preserve)?; + T::Currency::transfer(source, &buffer, amount, preservation)?; Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); @@ -232,7 +236,7 @@ mod tests { assert_eq!(Balances::free_balance(buffer), 0); // When: return 30 from account 1 - assert_ok!(ReturnToDap::::return_funds(&1, 30)); + assert_ok!(ReturnToDap::::return_funds(&1, 30, Preservation::Preserve)); // Then: account 1 has 70, buffer has 30 assert_eq!(Balances::free_balance(1), 70); @@ -251,7 +255,7 @@ mod tests { // When: try to return 150 (more than balance) // Then: fails assert_noop!( - ReturnToDap::::return_funds(&1, 150), + ReturnToDap::::return_funds(&1, 150, Preservation::Preserve), sp_runtime::TokenError::FundsUnavailable ); }); diff --git a/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs index 5056a0adf72db..5a464d33c76e0 100644 --- a/substrate/frame/support/src/traits/tokens/funding.rs +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -46,7 +46,17 @@ 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. - fn return_funds(from: &AccountId, amount: Balance) -> DispatchResult; + /// + /// # 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 minting implementation of `FundingSource`. @@ -92,14 +102,24 @@ where Currency: fungible::Mutate, AccountId: Eq, { - fn return_funds(from: &AccountId, amount: Currency::Balance) -> DispatchResult { - Currency::burn_from( - from, - amount, - Preservation::Expendable, - Precision::Exact, - Fortitude::Polite, - )?; + 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 is configured. +impl FundingSink for () { + fn return_funds( + _from: &AccountId, + _amount: Balance, + _preservation: Preservation, + ) -> DispatchResult { Ok(()) } } From fdaedfc885445bd11cdc3d05d5b1b68a42ee3346 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Thu, 4 Dec 2025 15:56:59 +0100 Subject: [PATCH 14/30] Improve prdoc --- prdoc/pr_10481.prdoc | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index ce4f7143610e4..c7adf91a1fe64 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -1,28 +1,28 @@ -title: 'Introduced pallet-dap' +title: 'First iteration of Dynamic Allocation Pool (DAP): burns -> DAP buffer/satellite' doc: - audience: Runtime Dev description: |- - Initial PoC PR to play around with Dynamic Allocation Proposal here: https://hackmd.io/a0KSL4XHQ1WvcGZlKykGzg + This initial version makes it possible to collect funds that would otherwise be burned into + - the DAP buffer on AssetHub + - the DAP satellite on system chains and the RelayChain + This is fully configurable, meaning that chains can choose to redirect all, some, or none of the funds that would be burned into DAP. - Initial version supports the following: - - Introduced FundingSink and FundingSource traits in frame-support - - Introduced pallet-dap with minimal implementation of FundingSink trait only. This pallet is meant to be - deployed on AssetHub as the central DAP pallet. - - Introduced pallet-dap-satellite, to collect funds that otherwise would be burned (e.g. tx fees, - coretime revenues, ...) into a local satellite account. These funds are accumulated locally and - will be sent to pallet-dap periodically in future iterations. The pallet implements a configurable fee handler - that splits fees between DAP satellite and another destination. - This pallet is meant to be deployed on system chains like Coretime, People and BridgeHub and on the RelayChain. - It should not be deployed on AssetHub, which hosts the central DAP pallet. + The mechanism to periodically transfer funds from the DAP satellite to the DAP buffer via XCM is not yet in place. - Runtime changes: - - asset-hub-westend: redirected treasury unspent to pallet-dap instead of burning - - asset-hub-westend: redirected staking slashes to pallet-dap instead of burning - - asset-hub-westend: all user-initiated burns go to DAP buffer instead of reducing total issuance. - - RC: westend-runtime has been updated to include pallet-dap-satellite. It preserves the original behavior for which 100% of tx fees are redirected to the block author. - - coretime-westend has been updated to include pallet-dap-satellite. Coretime revenues are now accumulated in the pallet-dap-satellite account instead of being burned. - - people-westend, bridge-hub-westend and collective-westend have been updated to include pallet-dap-satellite. - The original behavior for which 100% of tx fees are redirected to the staking pot has been preserved. + Westend RelayChain and system chains are now integrated with DAP / DAP satellite. + - On 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. + - On RelayChain, DAP satellite is included, but the original behavior of redirecting 100% of tx fees to the block author is preserved. + - On Coretime, revenues that would be transferred daily to XCM and burned, are now redirected to the DAP satellite instead. + - On other system chains (People, BridgeHub, Collective), DAP satellite is included, but the original behavior of redirecting 100% of tx fees to the staking pot is preserved. + + In the next iterations, we will introduce the following: + - Make it possible to replace direct minting with requesting funds from DAP buffer. + - Make DAP responsible to mint according to the issuance curve defined per chain. + - Make it possible to configure the percentage of funds redirected from DAP to different destinations (validators, nominators, treasury, collators, ...). + - Support for multiple assets in DAP: native token and stablecoin. + - The periodic transfer of funds from DAP satellite to DAP buffer via XCM. crates: - name: frame-support bump: minor From 9c3edd43b9eececa07d1d22094ea636d3a094e0f Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 5 Dec 2025 12:15:31 +0100 Subject: [PATCH 15/30] non DAP runtimes: use BurnDestination = DirectBurn --- .../assets/asset-hub-rococo/src/lib.rs | 1 + .../bridge-hubs/bridge-hub-rococo/src/lib.rs | 1 + .../bridge-hubs/bridge-hub-westend/src/lib.rs | 1 + .../collectives-westend/src/lib.rs | 1 + .../coretime/coretime-rococo/src/lib.rs | 1 + .../coretime/coretime-westend/src/lib.rs | 1 + .../runtimes/people/people-rococo/src/lib.rs | 1 + .../runtimes/people/people-westend/src/lib.rs | 1 + .../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/src/lib.rs | 1 + prdoc/pr_10481.prdoc | 36 ++++++++++++++++++ substrate/bin/node/runtime/src/lib.rs | 1 + substrate/frame/balances/src/lib.rs | 38 ++++++++++++++++--- substrate/frame/balances/src/tests/mod.rs | 6 +-- .../runtimes/parachain/src/lib.rs | 1 + .../staking-async/runtimes/rc/src/lib.rs | 1 + .../support/src/traits/tokens/funding.rs | 2 +- substrate/test-utils/runtime/src/lib.rs | 1 + .../parachain/runtime/src/configs/mod.rs | 1 + .../solochain/runtime/src/configs/mod.rs | 1 + 25 files changed, 94 insertions(+), 10 deletions(-) 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 bde01f335b19c..5d3f16fc73217 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/bridge-hubs/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs index 7dbcbb0975eb4..86ec8c089ff40 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/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs index aa5dac9e94a2f..5162b7fe5d0ee 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,6 +354,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs index dd2a0c713f8d9..a09d5098e14c6 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -234,6 +234,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs index 8e59eda372c01..027323ff67083 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/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs index 419c978aa4fb1..5af6190c2cdc1 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs @@ -267,6 +267,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs index dec1be2786bd2..7689a9b04c120 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/src/lib.rs b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs index b670dc39f01fa..c41f608de1f79 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs @@ -244,6 +244,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index 7cfd9467592ae..7ac57a7a48c97 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..bb26562b5081b 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..985b4e3dcc52c 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..c0e3ae92db941 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 2f224384a904f..f3b90e58318c4 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..71f252c7400b7 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/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 87d6adf701b45..ca82da46fdf15 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! { diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index c7adf91a1fe64..654dc6d010348 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -17,6 +17,10 @@ doc: - On Coretime, revenues that would be transferred daily to XCM and burned, are now redirected to the DAP satellite instead. - On other system chains (People, BridgeHub, Collective), DAP satellite is included, but the original behavior of redirecting 100% of tx fees to the staking pot is preserved. + [Breaking change]: all runtimes must now explicitly configure `BurnDestination`: + - Non-DAP runtimes should use `pallet_balances::DirectBurn` for direct burning. + - DAP-integrated runtimes use `pallet_dap::ReturnToDap` (on AssetHub) or `pallet_dap_satellite::AccumulateInSatellite` (on other system chains). + In the next iterations, we will introduce the following: - Make it possible to replace direct minting with requesting funds from DAP buffer. - Make DAP responsible to mint according to the issuance curve defined per chain. @@ -48,3 +52,35 @@ crates: bump: major - name: people-westend-runtime bump: major +- name: rococo-runtime + bump: major +- name: bridge-hub-rococo-runtime + bump: major +- name: people-rococo-runtime + bump: major +- name: coretime-rococo-runtime + bump: major +- name: asset-hub-rococo-runtime + bump: major +- name: kitchensink-runtime + bump: major +- name: polkadot-test-runtime + bump: major +- name: cumulus-test-runtime + bump: major +- name: rococo-parachain-runtime + bump: major +- name: penpal-runtime + bump: major +- name: ya-parachain-runtime + bump: major +- name: parachain-template-runtime + bump: major +- name: solochain-template-runtime + bump: major +- name: substrate-test-runtime + bump: major +- name: staking-async-rc-runtime + bump: major +- name: staking-async-parachain-runtime + bump: major diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 3a54a6bb09f13..ebebbb80c3da0 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/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index 617d03703bc68..fba2412517eee 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -216,6 +216,12 @@ 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::*; @@ -341,9 +347,9 @@ pub mod pallet { /// Handler for user-initiated burns via the `burn` extrinsic. /// - /// By default, burns is no-op. Runtimes can configure this - /// to redirect burned funds to a buffer account instead (e.g., DAP buffer on Asset Hub) or - /// to reduce total issuance (`DirectBurn`). + /// 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; } @@ -866,9 +872,9 @@ pub mod pallet { /// of the burn and `keep_alive` is false, the account will be reaped. /// /// The burned funds are handled by the runtime's configured `BurnDestination`: - /// - By default (`()`), is no-op - used for testing. - /// - Runtimes configure alternative destinations (e.g., DAP buffer) or `DirectBurn` (total - /// issuance is reduced). + /// - `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( @@ -1443,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 577a60d8c155c..712b5fb8d27bf 100644 --- a/substrate/frame/balances/src/tests/mod.rs +++ b/substrate/frame/balances/src/tests/mod.rs @@ -29,8 +29,8 @@ use frame_support::{ dispatch::{DispatchInfo, GetDispatchInfo}, parameter_types, traits::{ - fungible, tokens::DirectBurn, ConstU32, ConstU8, Imbalance as ImbalanceT, OnUnbalanced, - StorageMapShim, StoredMap, VariantCount, VariantCountOf, WhitelistedStorageKeys, + fungible, ConstU32, ConstU8, Imbalance as ImbalanceT, OnUnbalanced, StorageMapShim, + StoredMap, VariantCount, VariantCountOf, WhitelistedStorageKeys, }, weights::{IdentityFee, Weight}, }; @@ -130,7 +130,7 @@ impl Config for Test { type RuntimeFreezeReason = TestId; type FreezeIdentifier = TestId; type MaxFreezes = VariantCountOf; - type BurnDestination = DirectBurn; + 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 dddeeff03ec12..fec011cb20af5 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/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index 950010ee6cc14..dcab54b5e325d 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/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs index 5a464d33c76e0..423e44b7fa733 100644 --- a/substrate/frame/support/src/traits/tokens/funding.rs +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -113,7 +113,7 @@ where } /// No-op implementation of `FundingSink` for unit type. -/// Used for testing or when no sink is configured. +/// Used for testing or when no sink behavior is needed. impl FundingSink for () { fn return_funds( _from: &AccountId, diff --git a/substrate/test-utils/runtime/src/lib.rs b/substrate/test-utils/runtime/src/lib.rs index a04b05021bb46..5b26839c166f2 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 { diff --git a/templates/parachain/runtime/src/configs/mod.rs b/templates/parachain/runtime/src/configs/mod.rs index 2ac558ea2a310..6e75638f1dd2b 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..8a879ac49b2a9 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 becef1ec6ad09ba48d8cd5261a4b1995b93dce16 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 5 Dec 2025 14:47:38 +0100 Subject: [PATCH 16/30] fix prdoc --- prdoc/pr_10481.prdoc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index 654dc6d010348..9668f11aed342 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -72,15 +72,13 @@ crates: bump: major - name: penpal-runtime bump: major -- name: ya-parachain-runtime - bump: major - name: parachain-template-runtime bump: major - name: solochain-template-runtime bump: major - name: substrate-test-runtime bump: major -- name: staking-async-rc-runtime +- name: pallet-staking-async-rc-runtime bump: major -- name: staking-async-parachain-runtime +- name: pallet-staking-async-parachain-runtime bump: major From 71be1b2b58988b2b59ea7f296f348ed501f6fdf3 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 5 Dec 2025 16:20:37 +0100 Subject: [PATCH 17/30] fix more runtimes --- polkadot/xcm/xcm-builder/src/tests/pay/mock.rs | 1 + prdoc/pr_10481.prdoc | 2 ++ substrate/frame/asset-rewards/src/mock.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index d8f8e15f5eb05..7cc00bb216c37 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/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index 9668f11aed342..a9d47f95497e3 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -30,6 +30,8 @@ doc: crates: - name: frame-support bump: minor +- name: pallet-asset-rewards + bump: major - name: pallet-balances bump: major - name: pallet-dap diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs index 320d6c3ec4257..93132841f11df 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 { From e96376507d747a145663ae7c1b8a4e5873d0dfda Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 5 Dec 2025 16:30:38 +0100 Subject: [PATCH 18/30] fix contracts mock runtime --- substrate/frame/contracts/mock-network/src/parachain.rs | 1 + substrate/frame/contracts/mock-network/src/relay_chain.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/substrate/frame/contracts/mock-network/src/parachain.rs b/substrate/frame/contracts/mock-network/src/parachain.rs index ad43ac42a7508..3994ff0d23a92 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..ec3dc8def08c6 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 { From ceb10345c119d41f910ba8eef1754126a66a1826 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 5 Dec 2025 16:48:08 +0100 Subject: [PATCH 19/30] fix more runtimes --- prdoc/pr_10481.prdoc | 2 ++ substrate/frame/asset-rewards/src/mock.rs | 2 +- substrate/frame/assets-freezer/src/mock.rs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index a9d47f95497e3..26314452c3de8 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -30,6 +30,8 @@ doc: crates: - name: frame-support bump: minor +- name: pallet-assets-freezer + bump: major - name: pallet-asset-rewards bump: major - name: pallet-balances diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs index 93132841f11df..73ddaa9d028e8 100644 --- a/substrate/frame/asset-rewards/src/mock.rs +++ b/substrate/frame/asset-rewards/src/mock.rs @@ -72,7 +72,7 @@ impl pallet_balances::Config for MockRuntime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); - type BurnDestination = pallet_balances::DirectBurn; + 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 928ca73036f6ecb25cb66e9992ca9b34eb10fd64 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 5 Dec 2025 18:43:59 +0100 Subject: [PATCH 20/30] fix NIS pallet --- substrate/frame/nis/src/mock.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/substrate/frame/nis/src/mock.rs b/substrate/frame/nis/src/mock.rs index 0e71e43f56bd7..afaf457588209 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 = (); } impl pallet_balances::Config for Test { @@ -92,6 +93,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); + type BurnDestination = (); } parameter_types! { From 27b7fdbbda3967d7597522ff09285920576f0517 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Fri, 5 Dec 2025 20:53:53 +0100 Subject: [PATCH 21/30] fix xcm-builder mock runtime --- polkadot/xcm/xcm-builder/src/tests/pay/mock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index 7cc00bb216c37..5d509fa83e19e 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 = (); } parameter_types! { From eb0f384f37d910c366037a4f00b4bed93ef6b7dd Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 11:13:24 +0100 Subject: [PATCH 22/30] dap, dap-satellite: use withdraw + resolve instead of transfer --- substrate/frame/dap-satellite/src/lib.rs | 14 ++++++++++++-- substrate/frame/dap/src/lib.rs | 18 ++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 3ce27d60d190d..64b98bd9e7ad0 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -84,7 +84,7 @@ use frame_support::{ pallet_prelude::*, traits::{ fungible::{Balanced, Credit, Inspect, Mutate}, - tokens::{FundingSink, Preservation}, + tokens::{Fortitude, FundingSink, Precision, Preservation}, Currency, Imbalance, OnUnbalanced, }, PalletId, @@ -156,7 +156,17 @@ impl FundingSink> for AccumulateInSatellit ) -> Result<(), DispatchError> { let satellite = Pallet::::satellite_account(); - T::Currency::transfer(source, &satellite, amount, preservation)?; + // Similarly to pallet-dap, we use withdraw + resolve instead of transfer to avoid the ED + // requirement for the destination account. + let credit = T::Currency::withdraw( + source, + amount, + Precision::Exact, + preservation, + Fortitude::Polite, + )?; + + let _ = T::Currency::resolve(&satellite, credit); Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }); diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index 68bb5b2a5ca1d..c975aefc7d6b1 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -33,7 +33,7 @@ use frame_support::{ pallet_prelude::*, traits::{ fungible::{Balanced, Credit, Inspect, Mutate}, - tokens::{FundingSink, FundingSource, Preservation}, + tokens::{Fortitude, FundingSink, FundingSource, Precision, Preservation}, Currency, Imbalance, OnUnbalanced, }, PalletId, @@ -112,7 +112,21 @@ impl FundingSink> for ReturnToDap { ) -> Result<(), DispatchError> { let buffer = Pallet::::buffer_account(); - T::Currency::transfer(source, &buffer, amount, preservation)?; + // We use withdraw + resolve instead of transfer to avoid the ED requirement for the + // destination account. This way, we can also avoid the migration on production and the + // genesis configuration's update for benchmark / tests to ensure the destination + // accounts pre-exists. + // This imbalance-based approach is the same used e.g. for the StakingPot in system + // parachains. + let credit = T::Currency::withdraw( + source, + amount, + Precision::Exact, + preservation, + Fortitude::Polite, + )?; + + let _ = T::Currency::resolve(&buffer, credit); Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); From 62c36fb5919bcbbee129883d547f79dddc55d5cf Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 11:49:17 +0100 Subject: [PATCH 23/30] Use DirectBurn in mock/test runtimes too --- polkadot/xcm/xcm-builder/src/tests/pay/mock.rs | 2 +- substrate/frame/asset-rewards/src/mock.rs | 2 +- substrate/frame/assets-freezer/src/mock.rs | 2 +- substrate/frame/nis/src/mock.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index 5d509fa83e19e..1275c00761511 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 = (); + 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 73ddaa9d028e8..8911841bcd388 100644 --- a/substrate/frame/asset-rewards/src/mock.rs +++ b/substrate/frame/asset-rewards/src/mock.rs @@ -72,7 +72,7 @@ impl pallet_balances::Config for MockRuntime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); - type BurnDestination = (); + 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 3059c2b3b290c..af8ec237e906b 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 = (); + 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 afaf457588209..9abe7cb56411d 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 = (); + 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 = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { From 0d95fd2962703270e140866748d25b48be3f660e Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 13:18:06 +0100 Subject: [PATCH 24/30] prdoc --- prdoc/pr_10481.prdoc | 26 +++++++++++++++++--------- substrate/frame/dap/src/lib.rs | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index 26314452c3de8..99d9df2772ef8 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -33,7 +33,7 @@ crates: - name: pallet-assets-freezer bump: major - name: pallet-asset-rewards - bump: major + bump: patch - name: pallet-balances bump: major - name: pallet-dap @@ -41,7 +41,7 @@ crates: - name: pallet-dap-satellite bump: patch - name: polkadot-sdk - bump: major + bump: minor - name: asset-hub-westend-runtime bump: major - name: pallet-staking-async @@ -57,15 +57,15 @@ crates: - name: people-westend-runtime bump: major - name: rococo-runtime - bump: major + bump: minor - name: bridge-hub-rococo-runtime - bump: major + bump: minor - name: people-rococo-runtime - bump: major + bump: minor - name: coretime-rococo-runtime - bump: major + bump: minor - name: asset-hub-rococo-runtime - bump: major + bump: minor - name: kitchensink-runtime bump: major - name: polkadot-test-runtime @@ -73,9 +73,9 @@ crates: - name: cumulus-test-runtime bump: major - name: rococo-parachain-runtime - bump: major + bump: minor - name: penpal-runtime - bump: major + bump: minor - name: parachain-template-runtime bump: major - name: solochain-template-runtime @@ -86,3 +86,11 @@ crates: bump: major - name: pallet-staking-async-parachain-runtime bump: major +- name: staging-xcm-builder + bump: major +- name: pallet-nis + bump: major +- name: pallet-contracts-mock-network + bump: major +- name: yet-another-parachain-runtime + bump: major diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index c975aefc7d6b1..e9d6adc844fd9 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -115,7 +115,7 @@ impl FundingSink> for ReturnToDap { // We use withdraw + resolve instead of transfer to avoid the ED requirement for the // destination account. This way, we can also avoid the migration on production and the // genesis configuration's update for benchmark / tests to ensure the destination - // accounts pre-exists. + // account pre-exists. // This imbalance-based approach is the same used e.g. for the StakingPot in system // parachains. let credit = T::Currency::withdraw( From dfd82ccf57676a61766a860315833b76c2e20b29 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 15:17:42 +0100 Subject: [PATCH 25/30] dap, dap-satellite: more tests --- substrate/frame/dap-satellite/src/lib.rs | 226 ++++++++++++++++++++--- substrate/frame/dap/src/lib.rs | 174 +++++++++++++++-- 2 files changed, 363 insertions(+), 37 deletions(-) diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 64b98bd9e7ad0..a67a0442ac8b5 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -319,10 +319,15 @@ where mod tests { use super::*; use frame_support::{ - assert_noop, assert_ok, derive_impl, sp_runtime::traits::AccountIdConversion, - traits::tokens::FundingSink, + assert_noop, assert_ok, 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; @@ -369,31 +374,10 @@ mod tests { }); } - #[test] - fn accumulate_in_satellite_transfers_to_satellite_account() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - 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: accumulate 30 from account 1 - assert_ok!(AccumulateInSatellite::::return_funds(&1, 30, Preservation::Preserve)); - - // Then: account 1 has 70, satellite has 30 - assert_eq!(Balances::free_balance(1), 70); - assert_eq!(Balances::free_balance(satellite), 30); - // ...and an event is emitted - System::assert_last_event( - Event::::FundsAccumulated { from: 1, amount: 30 }.into(), - ); - }); - } + // ===== accumulate to satellite / returns_funds tests ===== #[test] - fn accumulate_multiple_times_adds_up() { + fn accumulate_in_satellite_transfers_to_satellite_account() { new_test_ext().execute_with(|| { System::set_block_number(1); let satellite = DapSatellite::satellite_account(); @@ -412,9 +396,20 @@ mod tests { // Then: satellite has accumulated all funds assert_eq!(Balances::free_balance(satellite), 170); + // ... accounts have their balance correctly update 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(), + ); }); } @@ -432,4 +427,185 @@ mod tests { ); }); } + + // ===== 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/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index e9d6adc844fd9..ad176fdf6dc6e 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -189,8 +189,12 @@ where mod tests { use super::*; use frame_support::{ - assert_noop, assert_ok, derive_impl, sp_runtime::traits::AccountIdConversion, - traits::tokens::FundingSink, + 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; @@ -239,24 +243,35 @@ mod tests { }); } + // ===== return_funds tests ===== + #[test] - fn return_funds_transfers_to_buffer() { + fn return_funds_accumulates_from_multiple_sources() { new_test_ext().execute_with(|| { System::set_block_number(1); let buffer = Dap::buffer_account(); - // Given: account 1 has 100, buffer has 0 + // 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 30 from account 1 - assert_ok!(ReturnToDap::::return_funds(&1, 30, Preservation::Preserve)); - - // Then: account 1 has 70, buffer has 30 - assert_eq!(Balances::free_balance(1), 70); - assert_eq!(Balances::free_balance(buffer), 30); - // ...and an event is emitted - System::assert_last_event(Event::::FundsReturned { from: 1, amount: 30 }.into()); + // 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()); }); } @@ -275,6 +290,141 @@ mod tests { }); } + #[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); + }); + } + + // ===== request_funds tests ===== + #[test] #[should_panic(expected = "not yet implemented")] fn pull_from_dap_panics() { From c2008537548e6b648b62c2d98ef85a2061ff7e74 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 15:40:32 +0100 Subject: [PATCH 26/30] dap,dap-satellite: better error handling --- substrate/frame/dap-satellite/src/lib.rs | 28 +++++++++++++++++-- substrate/frame/dap/src/lib.rs | 34 +++++++++++++++++++----- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index a67a0442ac8b5..0dd23dac8c7cb 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -132,6 +132,12 @@ pub mod pallet { /// Funds accumulated in satellite account. FundsAccumulated { from: T::AccountId, amount: BalanceOf }, } + + #[pallet::error] + pub enum Error { + /// Failed to deposit funds to satellite account. + ResolveFailed, + } } /// Implementation of `FundingSink` that accumulates funds in the satellite account. @@ -166,7 +172,17 @@ impl FundingSink> for AccumulateInSatellit Fortitude::Polite, )?; - let _ = T::Currency::resolve(&satellite, credit); + // Handle resolve failure: if it fails, the credit is dropped and funds are burned + if let Err(remaining) = T::Currency::resolve(&satellite, credit) { + let remaining_amount = remaining.peek(); + if !remaining_amount.is_zero() { + log::error!( + target: LOG_TARGET, + "💸 Failed to resolve {remaining_amount:?} to satellite account - funds will be burned!" + ); + return Err(Error::::ResolveFailed.into()); + } + } Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }); @@ -203,7 +219,15 @@ impl OnUnbalanced> for SlashToSatellite { let numeric_amount = amount.peek(); // Resolve the imbalance by depositing into the satellite account - let _ = T::Currency::resolve(&satellite, amount); + 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, diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index ad176fdf6dc6e..6aab2d6a179a0 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -84,11 +84,13 @@ pub mod pallet { pub enum Error { /// FundingSource not yet implemented. NotImplemented, + /// Failed to deposit funds to DAP buffer. + ResolveFailed, } } /// Implementation of FundingSource - NOT YET IMPLEMENTED. -/// Will panic if called. +/// Returns `Error::NotImplemented` if called. pub struct PullFromDap(core::marker::PhantomData); impl FundingSource> for PullFromDap { @@ -96,7 +98,7 @@ impl FundingSource> for PullFromDap { _beneficiary: &T::AccountId, _amount: BalanceOf, ) -> Result, DispatchError> { - unimplemented!("PullFromDap::request_funds not yet implemented") + Err(Error::::NotImplemented.into()) } } @@ -126,7 +128,16 @@ impl FundingSink> for ReturnToDap { Fortitude::Polite, )?; - let _ = T::Currency::resolve(&buffer, credit); + if let Err(remaining) = T::Currency::resolve(&buffer, credit) { + let remaining_amount = remaining.peek(); + if !remaining_amount.is_zero() { + log::error!( + target: LOG_TARGET, + "💸 Failed to resolve {remaining_amount:?} to DAP buffer - funds will be burned!" + ); + return Err(Error::::ResolveFailed.into()); + } + } Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); @@ -153,7 +164,15 @@ impl OnUnbalanced> for SlashToDap { let numeric_amount = amount.peek(); // Resolve the imbalance by depositing into the buffer account - let _ = T::Currency::resolve(&buffer, amount); + 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, @@ -426,10 +445,11 @@ mod tests { // ===== request_funds tests ===== #[test] - #[should_panic(expected = "not yet implemented")] - fn pull_from_dap_panics() { + fn pull_from_dap_returns_not_implemented_error() { new_test_ext().execute_with(|| { - let _ = PullFromDap::::request_funds(&1, 10); + // When: request_funds is called + // Then: returns NotImplemented error + assert_noop!(PullFromDap::::request_funds(&1, 10), Error::::NotImplemented); }); } } From 3cca6cf6d75e9ab91973ab7008c6197bd82eaccc Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 16:01:32 +0100 Subject: [PATCH 27/30] dap,dap-satellite: improve documentation --- substrate/frame/dap-satellite/src/lib.rs | 6 ++++++ substrate/frame/dap/src/lib.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 0dd23dac8c7cb..7afa146984f62 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -204,6 +204,9 @@ pub type CreditOf = Credit<::AccountId, = Credit<::AccountId, ` 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 { @@ -183,6 +186,9 @@ impl OnUnbalanced> for SlashToDap { /// 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 From c7d2eb5410158423603bae0b929e3798b62f6e04 Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 16:03:50 +0100 Subject: [PATCH 28/30] prdoc --- prdoc/pr_10481.prdoc | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc index 99d9df2772ef8..011b385ec6baa 100644 --- a/prdoc/pr_10481.prdoc +++ b/prdoc/pr_10481.prdoc @@ -17,6 +17,9 @@ doc: - On Coretime, revenues that would be transferred daily to XCM and burned, are now redirected to the DAP satellite instead. - On other system chains (People, BridgeHub, Collective), DAP satellite is included, but the original behavior of redirecting 100% of tx fees to the staking pot is preserved. + Events: `FundsReturned` (pallet-dap) and `FundsAccumulated` (pallet-dap-satellite) are 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. + [Breaking change]: all runtimes must now explicitly configure `BurnDestination`: - Non-DAP runtimes should use `pallet_balances::DirectBurn` for direct burning. - DAP-integrated runtimes use `pallet_dap::ReturnToDap` (on AssetHub) or `pallet_dap_satellite::AccumulateInSatellite` (on other system chains). @@ -31,7 +34,7 @@ crates: - name: frame-support bump: minor - name: pallet-assets-freezer - bump: major + bump: patch - name: pallet-asset-rewards bump: patch - name: pallet-balances @@ -87,10 +90,10 @@ crates: - name: pallet-staking-async-parachain-runtime bump: major - name: staging-xcm-builder - bump: major + bump: patch - name: pallet-nis - bump: major + bump: patch - name: pallet-contracts-mock-network - bump: major + bump: minor - name: yet-another-parachain-runtime - bump: major + bump: minor From 4d163d365173bec78caf363610a887af62ca988a Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 21:26:53 +0100 Subject: [PATCH 29/30] dap, dap-satellite: log w/o error out if resolve fails --- substrate/frame/dap-satellite/src/lib.rs | 28 ++++++++++-------------- substrate/frame/dap/src/lib.rs | 24 ++++++++++---------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index 7afa146984f62..fe0a44913371f 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -132,12 +132,6 @@ pub mod pallet { /// Funds accumulated in satellite account. FundsAccumulated { from: T::AccountId, amount: BalanceOf }, } - - #[pallet::error] - pub enum Error { - /// Failed to deposit funds to satellite account. - ResolveFailed, - } } /// Implementation of `FundingSink` that accumulates funds in the satellite account. @@ -172,17 +166,17 @@ impl FundingSink> for AccumulateInSatellit Fortitude::Polite, )?; - // Handle resolve failure: if it fails, the credit is dropped and funds are burned - if let Err(remaining) = T::Currency::resolve(&satellite, credit) { - let remaining_amount = remaining.peek(); - if !remaining_amount.is_zero() { - log::error!( - target: LOG_TARGET, - "💸 Failed to resolve {remaining_amount:?} to satellite account - funds will be burned!" - ); - return Err(Error::::ResolveFailed.into()); - } - } + // Following the same pattern as pallet-dap: if resolve fails (e.g., satellite account + // doesn't exist or amount < ED), the credit is dropped which burns the funds. + let _ = T::Currency::resolve(&satellite, credit).map_err(|c| { + log::warn!( + target: LOG_TARGET, + "💸 Failed to resolve {:?} to satellite account (account may not exist or amount < ED) \ + - funds will be burned instead", + c.peek() + ); + drop(c); + }); Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }); diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index 8a48381dcd343..b277786f8ccc8 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -84,8 +84,6 @@ pub mod pallet { pub enum Error { /// FundingSource not yet implemented. NotImplemented, - /// Failed to deposit funds to DAP buffer. - ResolveFailed, } } @@ -128,16 +126,18 @@ impl FundingSink> for ReturnToDap { Fortitude::Polite, )?; - if let Err(remaining) = T::Currency::resolve(&buffer, credit) { - let remaining_amount = remaining.peek(); - if !remaining_amount.is_zero() { - log::error!( - target: LOG_TARGET, - "💸 Failed to resolve {remaining_amount:?} to DAP buffer - funds will be burned!" - ); - return Err(Error::::ResolveFailed.into()); - } - } + // Following the same pattern as `ResolveTo` used by StakingPot: if resolve fails + // (e.g., buffer account doesn't exist or amount < ED), the credit is dropped which + // burns the funds. + let _ = T::Currency::resolve(&buffer, credit).map_err(|c| { + log::warn!( + target: LOG_TARGET, + "💸 Failed to resolve {:?} to DAP buffer (account may not exist or amount < ED) \ + - funds will be burned instead", + c.peek() + ); + drop(c); + }); Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); From ba3ce5e18b9cdb6dff41c0a0e71894c2ee4f572c Mon Sep 17 00:00:00 2001 From: Paolo La Camera Date: Mon, 8 Dec 2025 22:23:24 +0100 Subject: [PATCH 30/30] dap, dap-satellite: account creation at genesis / on_runtime_upgrade --- substrate/frame/dap-satellite/src/lib.rs | 72 +++++++++++++++++++--- substrate/frame/dap/src/lib.rs | 77 ++++++++++++++++++++---- 2 files changed, 128 insertions(+), 21 deletions(-) diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs index fe0a44913371f..f4b1cdf1a2ad3 100644 --- a/substrate/frame/dap-satellite/src/lib.rs +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -106,7 +106,12 @@ 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] @@ -124,6 +129,47 @@ pub mod pallet { pub fn satellite_account() -> T::AccountId { DAP_SATELLITE_PALLET_ID.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] @@ -156,8 +202,6 @@ impl FundingSink> for AccumulateInSatellit ) -> Result<(), DispatchError> { let satellite = Pallet::::satellite_account(); - // Similarly to pallet-dap, we use withdraw + resolve instead of transfer to avoid the ED - // requirement for the destination account. let credit = T::Currency::withdraw( source, amount, @@ -166,13 +210,12 @@ impl FundingSink> for AccumulateInSatellit Fortitude::Polite, )?; - // Following the same pattern as pallet-dap: if resolve fails (e.g., satellite account - // doesn't exist or amount < ED), the credit is dropped which burns the funds. + // The satellite 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(&satellite, credit).map_err(|c| { - log::warn!( + log::error!( target: LOG_TARGET, - "💸 Failed to resolve {:?} to satellite account (account may not exist or amount < ED) \ - - funds will be burned instead", + "💸 Failed to resolve {:?} to satellite account - funds will be burned instead", c.peek() ); drop(c); @@ -215,7 +258,8 @@ impl OnUnbalanced> for SlashToSatellite { let satellite = Pallet::::satellite_account(); let numeric_amount = amount.peek(); - // Resolve the imbalance by depositing into the satellite account + // 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() { @@ -386,6 +430,9 @@ mod tests { } .assimilate_storage(&mut t) .unwrap(); + crate::pallet::GenesisConfig::::default() + .assimilate_storage(&mut t) + .unwrap(); t.into() } @@ -398,6 +445,15 @@ mod tests { }); } + #[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)); + }); + } + // ===== accumulate to satellite / returns_funds tests ===== #[test] diff --git a/substrate/frame/dap/src/lib.rs b/substrate/frame/dap/src/lib.rs index b277786f8ccc8..8ff9ea244fd54 100644 --- a/substrate/frame/dap/src/lib.rs +++ b/substrate/frame/dap/src/lib.rs @@ -55,7 +55,12 @@ 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] @@ -71,6 +76,47 @@ pub mod pallet { 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] @@ -112,12 +158,6 @@ impl FundingSink> for ReturnToDap { ) -> Result<(), DispatchError> { let buffer = Pallet::::buffer_account(); - // We use withdraw + resolve instead of transfer to avoid the ED requirement for the - // destination account. This way, we can also avoid the migration on production and the - // genesis configuration's update for benchmark / tests to ensure the destination - // account pre-exists. - // This imbalance-based approach is the same used e.g. for the StakingPot in system - // parachains. let credit = T::Currency::withdraw( source, amount, @@ -126,14 +166,12 @@ impl FundingSink> for ReturnToDap { Fortitude::Polite, )?; - // Following the same pattern as `ResolveTo` used by StakingPot: if resolve fails - // (e.g., buffer account doesn't exist or amount < ED), the credit is dropped which - // burns the funds. + // 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::warn!( + log::error!( target: LOG_TARGET, - "💸 Failed to resolve {:?} to DAP buffer (account may not exist or amount < ED) \ - - funds will be burned instead", + "💸 Failed to resolve {:?} to DAP buffer - funds will be burned instead", c.peek() ); drop(c); @@ -166,7 +204,8 @@ impl OnUnbalanced> for SlashToDap { let buffer = Pallet::::buffer_account(); let numeric_amount = amount.peek(); - // Resolve the imbalance by depositing into the buffer account + // 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() { @@ -256,6 +295,9 @@ mod tests { } .assimilate_storage(&mut t) .unwrap(); + crate::pallet::GenesisConfig::::default() + .assimilate_storage(&mut t) + .unwrap(); t.into() } @@ -268,6 +310,15 @@ mod tests { }); } + #[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]