diff --git a/Cargo.lock b/Cargo.lock index d129f4eea452c..94ebe4064edf9 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", @@ -11413,6 +11414,7 @@ dependencies = [ "log", "pallet-authorship", "pallet-balances", + "pallet-dap", "pallet-election-provider-multi-block", "pallet-offences", "pallet-root-offences", @@ -12193,6 +12195,22 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-dap" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-default-config-example" version = "10.0.0" @@ -13631,6 +13649,7 @@ dependencies = [ "log", "pallet-bags-list", "pallet-balances", + "pallet-dap", "pallet-staking-async-rc-client", "parity-scale-codec", "rand 0.8.5", @@ -13711,6 +13730,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-conviction-voting", + "pallet-dap", "pallet-delegated-staking", "pallet-election-provider-multi-block", "pallet-fast-unstake", @@ -16570,6 +16590,7 @@ dependencies = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", diff --git a/Cargo.toml b/Cargo.toml index afae7745fd78d..83c71a9e1c586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -355,6 +355,7 @@ members = [ "substrate/frame/contracts/uapi", "substrate/frame/conviction-voting", "substrate/frame/core-fellowship", + "substrate/frame/dap", "substrate/frame/delegated-staking", "substrate/frame/democracy", "substrate/frame/derivatives", @@ -984,6 +985,7 @@ pallet-contracts-proc-macro = { path = "substrate/frame/contracts/proc-macro", d pallet-contracts-uapi = { path = "substrate/frame/contracts/uapi", default-features = false } pallet-conviction-voting = { path = "substrate/frame/conviction-voting", default-features = false } pallet-core-fellowship = { path = "substrate/frame/core-fellowship", default-features = false } +pallet-dap = { path = "substrate/frame/dap", default-features = false } pallet-default-config-example = { path = "substrate/frame/examples/default-config", default-features = false } pallet-delegated-staking = { path = "substrate/frame/delegated-staking", default-features = false } pallet-democracy = { path = "substrate/frame/democracy", default-features = false } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml index 852e7e1196e33..6d19f732c3a51 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -120,6 +120,7 @@ cumulus-primitives-aura = { workspace = true } cumulus-primitives-core = { workspace = true } cumulus-primitives-utility = { workspace = true } pallet-collator-selection = { workspace = true } +pallet-dap = { workspace = true } pallet-message-queue = { workspace = true } parachain-info = { workspace = true } parachains-common = { workspace = true } @@ -173,6 +174,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", "pallet-delegated-staking/runtime-benchmarks", "pallet-election-provider-multi-block/runtime-benchmarks", "pallet-fast-unstake/runtime-benchmarks", @@ -246,6 +248,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-conviction-voting/try-runtime", + "pallet-dap/try-runtime", "pallet-delegated-staking/try-runtime", "pallet-election-provider-multi-block/try-runtime", "pallet-fast-unstake/try-runtime", @@ -326,6 +329,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-conviction-voting/std", + "pallet-dap/std", "pallet-delegated-staking/std", "pallet-election-provider-multi-block/std", "pallet-fast-unstake/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 3b0c006c39ce8..9eb1535f55211 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -106,7 +106,7 @@ impl pallet_referenda::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; @@ -131,7 +131,7 @@ impl pallet_treasury::Config for Runtime { type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; type Burn = Burn; - type BurnDestination = (); + type BurnDestination = pallet_dap::BurnToDap; type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = (); diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 75c38fc9d1f36..eda924b33f3bb 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -1398,6 +1398,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..503a7323d80e1 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,15 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + +impl pallet_dap::Config for Runtime { + type Currency = Balances; + type PalletId = DapPalletId; +} + #[derive(Encode, Decode)] // Call indices taken from westend-next runtime. pub enum RelayChainRuntimePallets { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index b39e53ad75e91..dba9eba840958 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -923,7 +923,7 @@ impl pallet_fast_unstake::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; // The asset's interior location for the paying account. This is the Treasury diff --git a/prdoc/pr_10576.prdoc b/prdoc/pr_10576.prdoc new file mode 100644 index 0000000000000..662f65d5750b7 --- /dev/null +++ b/prdoc/pr_10576.prdoc @@ -0,0 +1,50 @@ +title: 'Introduce FundingSink trait and pallet-dap for AssetHub' +doc: +- audience: Runtime Dev + description: |- + This PR introduces the foundation for the Dynamic Allocation Pool (DAP) system: + + 1. **FundingSink trait** (frame-support): A new trait for returning funds to an issuance system. + Implementations can either burn directly (`DirectBurn`) or return to a buffer for reuse. + + 2. **pallet-dap**: A new pallet that implements `FundingSink` by collecting funds into a buffer + account instead of burning them. The buffer account is created via `inc_providers` at genesis + or on runtime upgrade, ensuring it can receive any amount including those below ED. + + 3. **AssetHub Westend integration**: The runtime now uses pallet-dap to redirect staking slashes + to the DAP buffer (via `SlashToDap`). + + **Treasury burns are now disabled** so no need to integrate with DAP: treasury `Burn` parameter is set + to zero in Westend RC, AssetHub and collective runtimes. This means no treasury funds are burned at + the end of spend periods, preserving total issuance. + + NOTE: User-initiated burns (via pallet_balances::burn extrinsic) do NOT go through DAP currently + but they burn directly instead, reducing total issuance immediately. + + This is the first deliverable of the DAP system. Future PRs will add: + - pallet-dap-satellite for system chains + - Integration with other Westend system chains + - XCM-based fund transfers from satellites to the DAP buffer + - A FundingSource trait to pull funds from DAP, enabling the replacement of direct minting with requests for funds from the DAP buffer + - DAP's responsibility to mint according to the issuance curve defined for each chain + - The ability to configure the percentage of funds redirected from DAP to various destinations (validators, nominators, treasury, collators, etc) + - Support for multiple assets in DAP, including native tokens and stablecoins +crates: +- name: frame-support + bump: minor +- name: pallet-balances + bump: major +- name: pallet-dap + bump: patch +- name: polkadot-sdk + bump: minor +- name: asset-hub-westend-runtime + bump: major +- name: westend-runtime + bump: major +- name: pallet-staking-async + bump: patch +- name: pallet-staking-async-parachain-runtime + bump: major +- name: pallet-staking-async-rc-runtime + bump: major diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index ebdb70f83dace..b661563bd7fe3 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::{Precision, Preservation}, + VariantCount, VariantCountOf, + }, }; use frame_system::pallet_prelude::*; @@ -333,6 +337,20 @@ pub mod pallet { Self::AccountId, Self::Balance, >; + + // TODO(DAP): Uncomment this when we want user-initiated burns to go through DAP on + // runtimes that configure it. When uncommenting, all runtimes will need to specify + // this parameter explicitly: + // - DAP-enabled runtimes: `type BurnDestination = pallet_dap::ReturnToDap;` + // - Other runtimes: `type BurnDestination = DirectBurn;` + // + // /// Handler for user-initiated burns via the `burn` extrinsic. + // /// + // /// Runtimes can configure this to redirect burned funds to a buffer account + // /// (e.g., DAP buffer on Asset Hub). If not specified, burns reduce total issuance + // /// directly. + // #[pallet::no_default_bounds] + // type BurnDestination: FundingSink; } /// The in-code storage version. @@ -852,8 +870,11 @@ pub mod pallet { /// If the origin's account ends up below the existential deposit as a result /// of the burn and `keep_alive` is false, the account will be reaped. /// - /// Unlike sending funds to a _burn_ address, which merely makes the funds inaccessible, - /// this `burn` operation will reduce total issuance by the amount _burned_. + /// Currently burns directly, reducing total issuance. + /// + /// TODO(DAP): When `BurnDestination` is uncommented in the Config trait, this should + /// use `T::BurnDestination::return_funds()` instead to allow DAP-enabled runtimes to + /// redirect user-initiated burns to the DAP buffer. #[pallet::call_index(10)] #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] pub fn burn( @@ -861,14 +882,16 @@ pub mod pallet { #[pallet::compact] value: T::Balance, keep_alive: bool, ) -> DispatchResult { + use frame_support::traits::tokens::Fortitude; let source = ensure_signed(origin)?; - let preservation = if keep_alive { Preserve } else { Expendable }; + let preservation = + if keep_alive { Preservation::Preserve } else { Preservation::Expendable }; >::burn_from( &source, value, preservation, Precision::Exact, - Polite, + Fortitude::Polite, )?; Ok(()) } 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..42b6317e0dad7 --- /dev/null +++ b/substrate/frame/dap/src/lib.rs @@ -0,0 +1,469 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Dynamic Allocation Pool (DAP) Pallet +//! +//! This pallet implements `FundingSink` to collect funds into a buffer account instead of burning +//! them. The buffer account is created via `inc_providers` at genesis or on runtime upgrade, +//! ensuring it can receive any amount including those below ED. +//! +//! Future phases will add: +//! - `FundingSource` (request_funds) for pulling funds +//! - Issuance curve and minting logic +//! - Distribution rules and scheduling + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Balanced, Credit, Inspect, Mutate}, + tokens::{Fortitude, FundingSink, Precision, Preservation}, + Currency, Imbalance, OnUnbalanced, + }, + PalletId, +}; + +pub use pallet::*; + +const LOG_TARGET: &str = "runtime::dap"; + +/// Type alias for balance. +pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::sp_runtime::traits::AccountIdConversion; + + /// The in-code storage version. + const STORAGE_VERSION: frame_support::traits::StorageVersion = + frame_support::traits::StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency type. + type Currency: Inspect + + Mutate + + Balanced; + + /// The pallet ID used to derive the buffer account. + /// + /// Each runtime should configure a unique ID to avoid collisions if multiple + /// DAP instances are used. + #[pallet::constant] + type PalletId: Get; + } + + impl Pallet { + /// Get the DAP buffer account + /// NOTE: We may need more accounts in the future, for instance, to manage the strategic + /// reserve. We will add them as necessary, generating them with additional seed. + pub fn buffer_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Ensure the buffer account exists by incrementing its provider count. + /// + /// This is called at genesis and on runtime upgrade. + /// It's idempotent - calling it multiple times is safe. + pub fn ensure_buffer_account_exists() { + let buffer = Self::buffer_account(); + if !frame_system::Pallet::::account_exists(&buffer) { + frame_system::Pallet::::inc_providers(&buffer); + log::info!( + target: LOG_TARGET, + "Created DAP buffer account: {buffer:?}" + ); + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> Weight { + // Create the buffer account if it doesn't exist (for chains upgrading to DAP). + Self::ensure_buffer_account_exists(); + // Weight: 1 read (account_exists) + potentially 1 write (inc_providers) + T::DbWeight::get().reads_writes(1, 1) + } + } + + /// Genesis config for the DAP pallet. + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + #[serde(skip)] + _phantom: core::marker::PhantomData, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + // Create the buffer account at genesis so it can receive funds of any amount. + Pallet::::ensure_buffer_account_exists(); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Funds returned to DAP buffer. + FundsReturned { from: T::AccountId, amount: BalanceOf }, + } +} + +/// Implementation of FundingSink that fills the DAP buffer. +/// Funds are transferred to the buffer account instead of being burned. +pub struct ReturnToDap(core::marker::PhantomData); + +impl FundingSink> for ReturnToDap { + fn fill(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { + let buffer = Pallet::::buffer_account(); + + // Withdraw from source, resolve to buffer, emit event. If withdraw fails, nothing happens. + // If resolve fails (should never happen - buffer pre-created at genesis or via runtime + // upgrade), funds are burned. + T::Currency::withdraw(source, amount, Precision::Exact, preservation, Fortitude::Polite) + .ok() + .map(|credit| T::Currency::resolve(&buffer, credit)) + .map(|_| { + Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }) + }); + } +} + +/// Type alias for credit (negative imbalance - funds that were slashed/removed). +/// This is for the `fungible::Balanced` trait as used by staking-async. +pub type CreditOf = Credit<::AccountId, ::Currency>; + +/// Implementation of OnUnbalanced for the fungible::Balanced trait. +/// Use this as `type Slash = SlashToDap` in staking-async config. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via fee splitting). +pub struct SlashToDap(core::marker::PhantomData); + +impl OnUnbalanced> for SlashToDap { + fn on_nonzero_unbalanced(amount: CreditOf) { + let buffer = Pallet::::buffer_account(); + let numeric_amount = amount.peek(); + + // The buffer account is created at genesis or on_runtime_upgrade, so resolve should + // always succeed. If it somehow fails, log the error. + if let Err(remaining) = T::Currency::resolve(&buffer, amount) { + let remaining_amount = remaining.peek(); + if !remaining_amount.is_zero() { + log::error!( + target: LOG_TARGET, + "💸 Failed to deposit slash to DAP buffer - {remaining_amount:?} will be burned!" + ); + } + } + + log::debug!( + target: LOG_TARGET, + "Deposited slash of {numeric_amount:?} to DAP buffer" + ); + } +} + +/// Implementation of OnUnbalanced for the old Currency trait (still used by treasury). +/// Use this as `type BurnDestination = BurnToDap` e.g. in treasury config. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via fee splitting). +pub struct BurnToDap(core::marker::PhantomData<(T, C)>); + +impl OnUnbalanced for BurnToDap +where + T: Config, + C: Currency, +{ + fn on_nonzero_unbalanced(amount: C::NegativeImbalance) { + let buffer = Pallet::::buffer_account(); + let numeric_amount = amount.peek(); + + // Resolve the imbalance by depositing into the buffer account + C::resolve_creating(&buffer, amount); + + log::debug!( + target: LOG_TARGET, + "Deposited burn of {numeric_amount:?} to DAP buffer" + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + derive_impl, parameter_types, + traits::{ + fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, + OnUnbalanced, WithdrawReasons, + }, + }; + use sp_runtime::BuildStorage; + + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + Dap: crate, + } + ); + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; + } + + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] + impl pallet_balances::Config for Test { + type AccountStore = System; + } + + parameter_types! { + pub const DapPalletId: PalletId = PalletId(*b"dap/buff"); + } + + impl Config for Test { + type Currency = Balances; + type PalletId = DapPalletId; + } + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 200), (3, 300)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + crate::pallet::GenesisConfig::::default() + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } + + #[test] + fn 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)); + }); + } + + // ===== fill tests ===== + + #[test] + fn fill_accumulates_from_multiple_sources() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let buffer = Dap::buffer_account(); + + // Given: accounts have balances, buffer has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(2), 200); + assert_eq!(Balances::free_balance(3), 300); + assert_eq!(Balances::free_balance(buffer), 0); + + // When: fill buffer from multiple accounts + ReturnToDap::::fill(&1, 20, Preservation::Preserve); + ReturnToDap::::fill(&2, 50, Preservation::Preserve); + ReturnToDap::::fill(&3, 100, Preservation::Preserve); + + // Then: buffer has accumulated all fills (20 + 50 + 100 = 170) + assert_eq!(Balances::free_balance(buffer), 170); + assert_eq!(Balances::free_balance(1), 80); + assert_eq!(Balances::free_balance(2), 150); + assert_eq!(Balances::free_balance(3), 200); + + // ...and all three events are emitted + System::assert_has_event(Event::::FundsReturned { from: 1, amount: 20 }.into()); + System::assert_has_event(Event::::FundsReturned { from: 2, amount: 50 }.into()); + System::assert_has_event(Event::::FundsReturned { from: 3, amount: 100 }.into()); + }); + } + + #[test] + fn fill_with_insufficient_balance_is_noop() { + 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: try to fill 150 (more than balance) + ReturnToDap::::fill(&1, 150, Preservation::Preserve); + + // Then: balances unchanged (infallible no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); + }); + } + + #[test] + fn fill_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: fill 0 from account 1 + ReturnToDap::::fill(&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 fill_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: fill full balance with Expendable (allows going to 0) + ReturnToDap::::fill(&1, 100, Preservation::Expendable); + + // Then: account 1 is empty, buffer has 100 + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(buffer), 100); + }); + } + + #[test] + fn fill_with_preserve_respects_existential_deposit() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: account 1 has 100, ED is 1 (from TestDefaultConfig) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); + + // When: try to fill 100 with Preserve (would go below ED) + ReturnToDap::::fill(&1, 100, Preservation::Preserve); + + // Then: balances unchanged (infallible - would have killed account) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); + + // But filling 99 works (leaves 1 for ED) + ReturnToDap::::fill(&1, 99, Preservation::Preserve); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::free_balance(buffer), 99); + }); + } + + // ===== SlashToDap tests ===== + + #[test] + fn slash_to_dap_accumulates_multiple_slashes_to_buffer() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: multiple slashes occur via OnUnbalanced (simulating a staking slash) + let credit1 = >::issue(30); + SlashToDap::::on_unbalanced(credit1); + + let credit2 = >::issue(20); + SlashToDap::::on_unbalanced(credit2); + + let credit3 = >::issue(50); + SlashToDap::::on_unbalanced(credit3); + + // Then: buffer has accumulated all slashes (30 + 20 + 50 = 100) + assert_eq!(Balances::free_balance(buffer), 100); + }); + } + + #[test] + fn slash_to_dap_handles_zero_amount() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: slash with zero amount + let credit = >::issue(0); + SlashToDap::::on_unbalanced(credit); + + // Then: buffer still has 0 (no-op) + assert_eq!(Balances::free_balance(buffer), 0); + }); + } + + // ===== BurnToDap tests ===== + + #[test] + fn burn_to_dap_accumulates_multiple_burns_to_buffer() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: accounts have balances, buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: create multiple negative imbalances (simulating treasury burns) and send to DAP + let imbalance1 = >::withdraw( + &1, + 30, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + BurnToDap::::on_unbalanced(imbalance1); + + let imbalance2 = >::withdraw( + &2, + 50, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + BurnToDap::::on_unbalanced(imbalance2); + + // Then: buffer has accumulated all burns (30 + 50 = 80) + assert_eq!(Balances::free_balance(buffer), 80); + assert_eq!(Balances::free_balance(1), 70); + assert_eq!(Balances::free_balance(2), 150); + }); + } +} diff --git a/substrate/frame/staking-async/Cargo.toml b/substrate/frame/staking-async/Cargo.toml index a852d8e87571b..d3a22dfc6a902 100644 --- a/substrate/frame/staking-async/Cargo.toml +++ b/substrate/frame/staking-async/Cargo.toml @@ -42,6 +42,7 @@ frame-benchmarking = { workspace = true, default-features = true } frame-support = { features = ["experimental"], workspace = true, default-features = true } pallet-bags-list = { workspace = true, default-features = true } pallet-balances = { workspace = true, default-features = true } +pallet-dap = { workspace = true, default-features = true } rand_chacha = { workspace = true, default-features = true } sp-tracing = { workspace = true, default-features = true } substrate-test-utils = { workspace = true } @@ -79,6 +80,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", "pallet-balances/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", "pallet-staking-async-rc-client/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "sp-staking/runtime-benchmarks", @@ -89,6 +91,7 @@ try-runtime = [ "frame-system/try-runtime", "pallet-bags-list/try-runtime", "pallet-balances/try-runtime", + "pallet-dap/try-runtime", "pallet-staking-async-rc-client/try-runtime", "sp-runtime/try-runtime", ] diff --git a/substrate/frame/staking-async/ahm-test/Cargo.toml b/substrate/frame/staking-async/ahm-test/Cargo.toml index 424a8d93f66c3..608a19a1fcfe3 100644 --- a/substrate/frame/staking-async/ahm-test/Cargo.toml +++ b/substrate/frame/staking-async/ahm-test/Cargo.toml @@ -31,6 +31,7 @@ pallet-balances = { workspace = true, default-features = true } # pallets that we need in AH frame-election-provider-support = { workspace = true, default-features = true } +pallet-dap = { workspace = true, default-features = true } pallet-election-provider-multi-block = { workspace = true, default-features = true } pallet-staking-async = { workspace = true, default-features = true } pallet-staking-async-rc-client = { workspace = true, default-features = true } @@ -52,6 +53,8 @@ std = [ try-runtime = [ "pallet-balances/try-runtime", + "pallet-dap/try-runtime", + "pallet-staking/try-runtime", "pallet-staking-async-rc-client/try-runtime", diff --git a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs index 1996b14be3487..7ba22bbb67d28 100644 --- a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs +++ b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs @@ -45,6 +45,8 @@ construct_runtime! { MultiBlockVerifier: multi_block::verifier, MultiBlockSigned: multi_block::signed, MultiBlockUnsigned: multi_block::unsigned, + + Dap: pallet_dap, } } @@ -445,7 +447,7 @@ impl pallet_staking_async::Config for Runtime { type EventListeners = (); type Reward = (); type RewardRemainder = (); - type Slash = (); + type Slash = pallet_dap::SlashToDap; type SlashDeferDuration = SlashDeferredDuration; type MaxEraDuration = (); type MaxPruningItems = MaxPruningItems; @@ -474,6 +476,15 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ValidatorSetExportSession; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + +impl pallet_dap::Config for Runtime { + type Currency = Balances; + type PalletId = DapPalletId; +} + parameter_types! { pub static NextRelayDeliveryFails: bool = false; } diff --git a/substrate/frame/staking-async/ahm-test/src/ah/test.rs b/substrate/frame/staking-async/ahm-test/src/ah/test.rs index a47de37dea643..45b8d7ec8ec35 100644 --- a/substrate/frame/staking-async/ahm-test/src/ah/test.rs +++ b/substrate/frame/staking-async/ahm-test/src/ah/test.rs @@ -717,6 +717,11 @@ fn on_offence_current_era_instant_apply() { // flush the events. let _ = staking_events_since_last_call(); + // Record initial state for DAP verification + let dap_buffer = pallet_dap::Pallet::::buffer_account(); + let initial_dap_balance = Balances::free_balance(&dap_buffer); + let initial_total_issuance = Balances::total_issuance(); + assert_ok!(rc_client::Pallet::::relay_new_offence_paged( RuntimeOrigin::root(), vec![ @@ -783,6 +788,15 @@ fn on_offence_current_era_instant_apply() { staking_async::Event::Slashed { staker: 3, amount: 50 } ] ); + + // DAP verification: slashed funds (50 + 50 + 50 = 150) should go to buffer + let final_dap_balance = Balances::free_balance(&dap_buffer); + let final_total_issuance = Balances::total_issuance(); + + // DAP buffer should have received all slashed funds + assert_eq!(final_dap_balance, initial_dap_balance + 150); + // Total issuance should be preserved (funds not burned) + assert_eq!(final_total_issuance, initial_total_issuance); }); } 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..d4622c8c4fe99 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -111,7 +111,7 @@ impl pallet_referenda::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; // The asset's interior location for the paying account. This is the Treasury @@ -138,7 +138,7 @@ impl pallet_treasury::Config for Runtime { type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; type Burn = Burn; - type BurnDestination = (); + type BurnDestination = pallet_dap::BurnToDap; type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = (); diff --git a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs index ad42ee31662e0..4b13091ce18c5 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..2778d82639d70 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,15 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + +impl pallet_dap::Config for Runtime { + type Currency = Balances; + type PalletId = DapPalletId; +} + parameter_types! { pub StakingXcmDestination: Location = Location::parent(); } diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index 53edb28ceccd9..c090d74aecd3a 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -999,7 +999,7 @@ impl pallet_bags_list::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); + pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; // The asset's interior location for the paying account. This is the Treasury diff --git a/substrate/frame/staking-async/src/mock.rs b/substrate/frame/staking-async/src/mock.rs index c14e5c026accb..5bdd7acd4a6b4 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,15 @@ impl pallet_balances::Config for Test { type AccountStore = System; } +parameter_types! { + pub const DapPalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/buff"); +} + +impl pallet_dap::Config for Test { + type Currency = Balances; + type PalletId = DapPalletId; +} + parameter_types! { pub static RewardRemainderUnbalanced: u128 = 0; } @@ -454,7 +464,7 @@ impl crate::pallet::pallet::Config for Test { type RcClientInterface = session_mock::Session; type CurrencyBalance = Balance; type CurrencyToVote = SaturatingCurrencyToVote; - type Slash = (); + type Slash = pallet_dap::SlashToDap; type WeightInfo = (); } diff --git a/substrate/frame/support/src/traits/tokens.rs b/substrate/frame/support/src/traits/tokens.rs index be982cd31e33a..55460aca99b97 100644 --- a/substrate/frame/support/src/traits/tokens.rs +++ b/substrate/frame/support/src/traits/tokens.rs @@ -19,6 +19,7 @@ pub mod asset_ops; pub mod currency; +pub mod funding; pub mod fungible; pub mod fungibles; pub mod imbalance; @@ -27,6 +28,7 @@ pub mod nonfungible; pub mod nonfungible_v2; pub mod nonfungibles; pub mod nonfungibles_v2; +pub use funding::{DirectBurn, FundingSink}; pub use imbalance::Imbalance; pub mod pay; pub mod transfer; diff --git a/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs new file mode 100644 index 0000000000000..f9da65a3efdeb --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -0,0 +1,79 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for returning funds to an issuance system. +//! +//! This module provides abstractions for returning funds (burns, slashing) in a way that can be +//! configured differently per runtime. +//! +//! Two main patterns: +//! - **Direct burn**: Traditional approach where funds are destroyed on demand +//! - **Buffer-based**: Funds are returned to a buffer for reuse + +use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; +use core::marker::PhantomData; + +/// Trait for moving funds into an issuance buffer or burning them. +/// +/// Implementations can either burn directly or transfer to a buffer for reuse. +/// This trait is infallible - implementations must handle any errors internally. +/// +/// Pairs with future `FundingSource::drain()` for withdrawing from the buffer. +pub trait FundingSink { + /// Fill the sink with funds from the given account. + /// + /// This could mean burning the funds or transferring them to a buffer account. + /// The operation is infallible - any errors are handled internally. + /// + /// # Parameters + /// - `from`: The account to take funds from + /// - `amount`: The amount to fill + /// - `preservation`: Whether to preserve the source account (Preserve = keep alive, Expendable + /// = allow death) + fn fill(from: &AccountId, amount: Balance, preservation: Preservation); +} + +/// Direct burning implementation of `FundingSink`. +/// +/// This implementation burns tokens directly, reducing total issuance. +/// Used for traditional burn systems (e.g., Kusama). +/// +/// # Type Parameters +/// +/// * `Currency` - The currency type that implements `Mutate` +/// * `AccountId` - The account identifier type +pub struct DirectBurn(PhantomData<(Currency, AccountId)>); + +impl FundingSink + for DirectBurn +where + Currency: fungible::Mutate, + AccountId: Eq, +{ + fn fill(from: &AccountId, amount: Currency::Balance, preservation: Preservation) { + // Best-effort burn. If it fails (e.g., insufficient funds), the funds remain with the + // account. + let _ = + Currency::burn_from(from, amount, preservation, Precision::Exact, Fortitude::Polite); + } +} + +/// No-op implementation of `FundingSink` for unit type. +/// Used for testing or when no sink behavior is needed. +impl FundingSink for () { + fn fill(_from: &AccountId, _amount: Balance, _preservation: Preservation) {} +} diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 5814cd70c40b4..925236a4d4df6 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -85,6 +85,7 @@ std = [ "pallet-contracts?/std", "pallet-conviction-voting?/std", "pallet-core-fellowship?/std", + "pallet-dap?/std", "pallet-delegated-staking?/std", "pallet-democracy?/std", "pallet-derivatives?/std", @@ -281,6 +282,7 @@ runtime-benchmarks = [ "pallet-contracts?/runtime-benchmarks", "pallet-conviction-voting?/runtime-benchmarks", "pallet-core-fellowship?/runtime-benchmarks", + "pallet-dap?/runtime-benchmarks", "pallet-delegated-staking?/runtime-benchmarks", "pallet-democracy?/runtime-benchmarks", "pallet-derivatives?/runtime-benchmarks", @@ -422,6 +424,7 @@ try-runtime = [ "pallet-contracts?/try-runtime", "pallet-conviction-voting?/try-runtime", "pallet-core-fellowship?/try-runtime", + "pallet-dap?/try-runtime", "pallet-delegated-staking?/try-runtime", "pallet-democracy?/try-runtime", "pallet-derivatives?/try-runtime", @@ -635,6 +638,7 @@ runtime-full = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", @@ -1415,6 +1419,11 @@ default-features = false optional = true path = "../substrate/frame/core-fellowship" +[dependencies.pallet-dap] +default-features = false +optional = true +path = "../substrate/frame/dap" + [dependencies.pallet-delegated-staking] default-features = false optional = true diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index 125939c2edf76..bb383efc5bcd0 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -443,6 +443,10 @@ pub use pallet_conviction_voting; #[cfg(feature = "pallet-core-fellowship")] pub use pallet_core_fellowship; +/// FRAME pallet for Dynamic Allocation Pool (DAP). +#[cfg(feature = "pallet-dap")] +pub use pallet_dap; + /// FRAME delegated staking pallet. #[cfg(feature = "pallet-delegated-staking")] pub use pallet_delegated_staking;