diff --git a/Cargo.lock b/Cargo.lock index 0c53df094f697..f6553796af187 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", @@ -2855,6 +2856,7 @@ dependencies = [ "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-session", @@ -3473,6 +3475,7 @@ dependencies = [ "pallet-collective", "pallet-collective-content", "pallet-core-fellowship", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-preimage", @@ -3919,6 +3922,7 @@ dependencies = [ "pallet-balances", "pallet-broker", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-proxy", @@ -11416,6 +11420,7 @@ dependencies = [ "log", "pallet-authorship", "pallet-balances", + "pallet-dap", "pallet-election-provider-multi-block", "pallet-offences", "pallet-root-offences", @@ -12196,6 +12201,39 @@ 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-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" @@ -13634,6 +13672,7 @@ dependencies = [ "log", "pallet-bags-list", "pallet-balances", + "pallet-dap", "pallet-staking-async-rc-client", "parity-scale-codec", "rand 0.8.5", @@ -13714,6 +13753,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-conviction-voting", + "pallet-dap", "pallet-delegated-staking", "pallet-election-provider-multi-block", "pallet-fast-unstake", @@ -14902,6 +14942,7 @@ dependencies = [ "pallet-authorship", "pallet-balances", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-identity", "pallet-message-queue", "pallet-migrations", @@ -16574,6 +16615,8 @@ dependencies = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", @@ -27519,6 +27562,7 @@ dependencies = [ "pallet-beefy", "pallet-beefy-mmr", "pallet-conviction-voting", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", diff --git a/Cargo.toml b/Cargo.toml index afae7745fd78d..5f0276a474900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -355,6 +355,8 @@ members = [ "substrate/frame/contracts/uapi", "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", @@ -984,6 +986,8 @@ 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-dap-satellite = { path = "substrate/frame/dap-satellite", default-features = false } pallet-default-config-example = { path = "substrate/frame/examples/default-config", default-features = false } pallet-delegated-staking = { path = "substrate/frame/delegated-staking", default-features = false } pallet-democracy = { path = "substrate/frame/democracy", default-features = false } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index 811e119d6b89c..0b887f0b82206 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -227,6 +227,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = ConstU32<50>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml index 23bc88a16ccf0..dd3decd4850fc 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -121,6 +121,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 } @@ -174,6 +175,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", @@ -248,6 +250,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", @@ -329,6 +332,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 24d3845f7bb30..9c00123b55c89 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -106,7 +106,6 @@ impl pallet_referenda::Config for Runtime { parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::from_perthousand(2); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; @@ -139,7 +138,7 @@ impl pallet_treasury::Config for Runtime { type RejectOrigin = EitherOfDiverse, Treasurer>; type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; - type Burn = Burn; + type Burn = (); type BurnDestination = (); type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; 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 bbc9e455b9248..3e9ae5401bdd2 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -245,6 +245,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_dap::ReturnToDap; } parameter_types! { @@ -1399,6 +1400,9 @@ construct_runtime!( AssetRate: pallet_asset_rate = 95, MultiAssetBounties: pallet_multi_asset_bounties = 96, + // 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/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs index f61813c49a2f2..4afcdf58aa6d9 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/lib.rs @@ -364,6 +364,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml index 3e4b90b014618..73f2a8b44b3f7 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml @@ -31,6 +31,7 @@ frame-try-runtime = { optional = true, workspace = true } pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-message-queue = { workspace = true } pallet-multisig = { workspace = true } pallet-session = { workspace = true } @@ -174,6 +175,7 @@ std = [ "pallet-bridge-parachains/std", "pallet-bridge-relayers/std", "pallet-collator-selection/std", + "pallet-dap-satellite/std", "pallet-message-queue/std", "pallet-multisig/std", "pallet-session/std", @@ -254,6 +256,7 @@ runtime-benchmarks = [ "pallet-bridge-parachains/runtime-benchmarks", "pallet-bridge-relayers/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-session/runtime-benchmarks", @@ -304,6 +307,7 @@ try-runtime = [ "pallet-bridge-parachains/try-runtime", "pallet-bridge-relayers/try-runtime", "pallet-collator-selection/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", "pallet-session/try-runtime", diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs index 65ca1b2a4b49d..b0ff0610cae48 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs @@ -354,17 +354,24 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = MILLICENTS; + /// Percentage of fees to send to DAP satellite (0 = all to staking pot, 100 = all to DAP). + pub const DapSatelliteFeePercent: u32 = 0; } +/// Fee handler that splits fees between DAP satellite and staking pot. +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -552,6 +559,15 @@ impl pallet_utility::Config for Runtime { type WeightInfo = weights::pallet_utility::WeightInfo; } +parameter_types! { + pub const DapSatellitePalletId: frame_support::PalletId = frame_support::PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -590,6 +606,9 @@ construct_runtime!( BridgeRococoMessages: pallet_bridge_messages:: = 44, XcmOverBridgeHubRococo: pallet_xcm_bridge_hub:: = 45, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 60, + EthereumInboundQueue: snowbridge_pallet_inbound_queue = 80, EthereumOutboundQueue: snowbridge_pallet_outbound_queue = 81, EthereumBeaconClient: snowbridge_pallet_ethereum_client = 82, diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml index 7c466c22693ec..2cdfd290f2f2a 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml @@ -34,6 +34,7 @@ pallet-authorship = { workspace = true } pallet-balances = { workspace = true } pallet-collective = { workspace = true } pallet-core-fellowship = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-multisig = { workspace = true } pallet-preimage = { workspace = true } pallet-proxy = { workspace = true } @@ -120,6 +121,7 @@ runtime-benchmarks = [ "pallet-collective-content/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-core-fellowship/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", @@ -165,6 +167,7 @@ try-runtime = [ "pallet-collective-content/try-runtime", "pallet-collective/try-runtime", "pallet-core-fellowship/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", "pallet-preimage/try-runtime", @@ -213,6 +216,7 @@ std = [ "pallet-collective-content/std", "pallet-collective/std", "pallet-core-fellowship/std", + "pallet-dap-satellite/std", "pallet-message-queue/std", "pallet-multisig/std", "pallet-preimage/std", diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs index 47e8e24a8efb9..dd465991a8e70 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -234,17 +234,22 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = MILLICENTS; + pub const DapSatelliteFeePercent: u32 = 0; } +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; type FeeMultiplierUpdate = SlowAdjustingFeeUpdate; @@ -683,6 +688,15 @@ impl pallet_asset_rate::Config for Runtime { type BenchmarkHelper = polkadot_runtime_common::impls::benchmarks::AssetRateArguments; } +parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -747,6 +761,7 @@ construct_runtime!( AmbassadorContent: pallet_collective_content:: = 75, StateTrieMigration: pallet_state_trie_migration = 80, + DapSatellite: pallet_dap_satellite = 85, // The Secretary Collective // pub type SecretaryCollectiveInstance = pallet_ranked_collective::instance3; diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs index 040d99280605f..7a77a96611534 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs @@ -267,6 +267,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml b/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml index a40bebb5747c0..976a3fc50766c 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/Cargo.toml @@ -31,6 +31,7 @@ pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } pallet-broker = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-message-queue = { workspace = true } pallet-multisig = { workspace = true } pallet-proxy = { workspace = true } @@ -113,6 +114,7 @@ std = [ "pallet-balances/std", "pallet-broker/std", "pallet-collator-selection/std", + "pallet-dap-satellite/std", "pallet-message-queue/std", "pallet-multisig/std", "pallet-proxy/std", @@ -167,6 +169,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-broker/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", @@ -202,6 +205,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-broker/try-runtime", "pallet-collator-selection/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", "pallet-proxy/try-runtime", diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs index c9cd7f80a61ae..bde63fa63adb8 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs @@ -18,69 +18,22 @@ use crate::{xcm_config::LocationToAccountId, *}; use codec::{Decode, Encode}; use cumulus_pallet_parachain_system::RelaychainDataProvider; use cumulus_primitives_core::relay_chain; -use frame_support::{ - parameter_types, - traits::{ - fungible::{Balanced, Credit, Inspect}, - tokens::{Fortitude, Preservation}, - DefensiveResult, OnUnbalanced, - }, -}; -use frame_system::Pallet as System; +use frame_support::parameter_types; use pallet_broker::{ CoreAssignment, CoreIndex, CoretimeInterface, PartsOf57600, RCBlockNumberOf, TaskId, Timeslice, }; use parachains_common::{AccountId, Balance}; -use sp_runtime::traits::{AccountIdConversion, MaybeConvert}; +use sp_runtime::traits::MaybeConvert; use westend_runtime_constants::system_parachain::coretime; use xcm::latest::prelude::*; -use xcm_executor::traits::{ConvertLocation, TransactAsset}; - -pub struct BurnCoretimeRevenue; -impl OnUnbalanced> for BurnCoretimeRevenue { - fn on_nonzero_unbalanced(amount: Credit) { - let acc = RevenueAccumulationAccount::get(); - if !System::::account_exists(&acc) { - System::::inc_providers(&acc); - } - Balances::resolve(&acc, amount).defensive_ok(); - } -} +use xcm_executor::traits::ConvertLocation; -type AssetTransactor = ::AssetTransactor; - -fn burn_at_relay(stash: &AccountId, value: Balance) -> Result<(), XcmError> { - let dest = Location::parent(); - let stash_location = - Junction::AccountId32 { network: None, id: stash.clone().into() }.into_location(); - let asset = Asset { id: AssetId(Location::parent()), fun: Fungible(value) }; - let dummy_xcm_context = XcmContext { origin: None, message_id: [0; 32], topic: None }; - - let withdrawn = AssetTransactor::withdraw_asset(&asset, &stash_location, None)?; - - AssetTransactor::can_check_out(&dest, &asset, &dummy_xcm_context)?; - - let parent_assets = Into::::into(withdrawn) - .reanchored(&dest, &Here.into()) - .defensive_map_err(|_| XcmError::ReanchorFailed)?; - - PolkadotXcm::send_xcm( - Here, - Location::parent(), - Xcm(vec![ - Instruction::UnpaidExecution { - weight_limit: WeightLimit::Unlimited, - check_origin: None, - }, - ReceiveTeleportedAsset(parent_assets.clone()), - BurnAsset(parent_assets), - ]), - )?; - - AssetTransactor::check_out(&dest, &asset, &dummy_xcm_context); - - Ok(()) -} +/// Coretime revenue handler that sends funds to the DAP satellite account. +/// +/// Previously, revenue was accumulated in a stash account and then burned at the relay chain. +/// With DAP, revenue is accumulated in the satellite account and will eventually be sent +/// to the DAP buffer on AssetHub via XCM. +pub type CoretimeRevenueToSatellite = pallet_dap_satellite::SlashToSatellite; /// A type containing the encoding of the coretime pallet in the Relay chain runtime. Used to /// construct any remote calls. The codec index must correspond to the index of `Coretime` in the @@ -112,7 +65,6 @@ enum CoretimeProviderCalls { parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); pub const MinimumCreditPurchase: Balance = UNITS / 10; - pub RevenueAccumulationAccount: AccountId = BrokerPalletId::get().into_sub_account_truncating(b"burnstash"); pub const MinimumEndPrice: Balance = UNITS; } @@ -287,21 +239,9 @@ impl CoretimeInterface for CoretimeAllocator { } fn on_new_timeslice(_timeslice: Timeslice) { - let stash = RevenueAccumulationAccount::get(); - let value = - Balances::reducible_balance(&stash, Preservation::Expendable, Fortitude::Polite); - - if value > 0 { - tracing::debug!(target: "runtime::coretime", %value, "Going to burn stashed tokens at RC"); - match burn_at_relay(&stash, value) { - Ok(()) => { - tracing::debug!(target: "runtime::coretime", %value, "Successfully burnt tokens"); - }, - Err(err) => { - tracing::error!(target: "runtime::coretime", error=?err, "burn_at_relay failed"); - }, - } - } + // Revenue is now accumulated in the DAP satellite account via + // CoretimeRevenueToSatellite (OnRevenue handler). The satellite pallet will + // eventually send accumulated funds to AssetHub DAP via XCM. } } @@ -317,7 +257,7 @@ impl MaybeConvert for SovereignAccountOf { impl pallet_broker::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; - type OnRevenue = BurnCoretimeRevenue; + type OnRevenue = CoretimeRevenueToSatellite; type TimeslicePeriod = ConstU32<{ coretime::TIMESLICE_PERIOD }>; // We don't actually need any leases at launch but set to 10 in case we want to sudo some in. type MaxLeasedCores = ConstU32<10>; diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs index 153b4e01dc381..0f504ad43084e 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs @@ -267,17 +267,23 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10 pub const TransactionByteFee: Balance = MILLICENTS; + /// Percentage of fees to send to DAP satellite (0-100). Currently 0% - all fees go to collators. + pub const DapSatelliteFeePercent: u32 = 0; } +pub type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -612,6 +618,15 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } +parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + pub struct BrokerMigrationV4BlockConversion; impl pallet_broker::migration::v4::BlockToRelayHeightConversion @@ -667,6 +682,8 @@ construct_runtime!( // The main stage. Broker: pallet_broker = 50, + DapSatellite: pallet_dap_satellite = 60, + // Sudo Sudo: pallet_sudo = 100, } diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs index ccc1ae56c685f..f3d532a7fbbdb 100644 --- a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs @@ -241,6 +241,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/people/people-westend/Cargo.toml b/cumulus/parachains/runtimes/people/people-westend/Cargo.toml index 5dc76682c9092..00f945f36ccab 100644 --- a/cumulus/parachains/runtimes/people/people-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/people/people-westend/Cargo.toml @@ -30,6 +30,7 @@ frame-try-runtime = { optional = true, workspace = true } pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-identity = { workspace = true } pallet-message-queue = { workspace = true } pallet-migrations = { workspace = true } @@ -112,6 +113,7 @@ std = [ "pallet-authorship/std", "pallet-balances/std", "pallet-collator-selection/std", + "pallet-dap-satellite/std", "pallet-identity/std", "pallet-message-queue/std", "pallet-migrations/std", @@ -167,6 +169,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-identity/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-migrations/runtime-benchmarks", @@ -202,6 +205,7 @@ try-runtime = [ "pallet-authorship/try-runtime", "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-identity/try-runtime", "pallet-message-queue/try-runtime", "pallet-migrations/try-runtime", diff --git a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs index 5e44d12b96e93..fdb982f41bcd9 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs @@ -244,17 +244,22 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10. pub const TransactionByteFee: Balance = MILLICENTS; + pub const DapSatelliteFeePercent: u32 = 0; } +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = - pallet_transaction_payment::FungibleAdapter>; + pallet_transaction_payment::FungibleAdapter; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -387,6 +392,7 @@ parameter_types! { pub const SessionLength: BlockNumber = 6 * HOURS; // StakingAdmin pluralistic body. pub const StakingAdminBodyId: BodyId = BodyId::Defense; + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); } /// We allow Root and the `StakingAdmin` to execute privileged collator selection operations. @@ -578,6 +584,11 @@ impl pallet_migrations::Config for Runtime { type WeightInfo = weights::pallet_migrations::WeightInfo; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -613,6 +624,7 @@ construct_runtime!( // The main stage. Identity: pallet_identity = 50, + DapSatellite: pallet_dap_satellite = 60, // Migrations pallet MultiBlockMigrations: pallet_migrations = 98, diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index 01f6dd1700d0d..4614401803174 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -433,6 +433,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs index 12a322534da5a..6430baa47e1bf 100644 --- a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs @@ -265,6 +265,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs index 92df3d950cb0e..03c04a376dd6a 100644 --- a/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs @@ -307,6 +307,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/cumulus/test/runtime/src/lib.rs b/cumulus/test/runtime/src/lib.rs index 8acbc49a33834..79a849c2e20c4 100644 --- a/cumulus/test/runtime/src/lib.rs +++ b/cumulus/test/runtime/src/lib.rs @@ -334,6 +334,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/polkadot/runtime/parachains/src/mock.rs b/polkadot/runtime/parachains/src/mock.rs index cba63ae7b1b04..20c75ad4f56da 100644 --- a/polkadot/runtime/parachains/src/mock.rs +++ b/polkadot/runtime/parachains/src/mock.rs @@ -153,6 +153,7 @@ impl pallet_balances::Config for Test { type Balance = Balance; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 80426212d4c29..60fd0c9d7e987 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -419,6 +419,7 @@ impl pallet_balances::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; type MaxFreezes = ConstU32<1>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { @@ -1319,6 +1320,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<1>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index b4a368c8d8a19..f7e955238bd46 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -275,6 +275,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml index c907ceb65665c..cca82e92654fa 100644 --- a/polkadot/runtime/westend/Cargo.toml +++ b/polkadot/runtime/westend/Cargo.toml @@ -60,6 +60,7 @@ pallet-balances = { workspace = true } pallet-beefy = { workspace = true } pallet-beefy-mmr = { workspace = true } pallet-conviction-voting = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-delegated-staking = { workspace = true } pallet-election-provider-multi-phase = { workspace = true } pallet-fast-unstake = { workspace = true } @@ -156,6 +157,7 @@ std = [ "pallet-beefy-mmr/std", "pallet-beefy/std", "pallet-conviction-voting/std", + "pallet-dap-satellite/std", "pallet-delegated-staking/std", "pallet-election-provider-multi-phase/std", "pallet-election-provider-support-benchmarking?/std", @@ -246,6 +248,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-beefy-mmr/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-delegated-staking/runtime-benchmarks", "pallet-election-provider-multi-phase/runtime-benchmarks", "pallet-election-provider-support-benchmarking/runtime-benchmarks", @@ -312,6 +315,7 @@ try-runtime = [ "pallet-beefy-mmr/try-runtime", "pallet-beefy/try-runtime", "pallet-conviction-voting/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-delegated-staking/try-runtime", "pallet-election-provider-multi-phase/try-runtime", "pallet-fast-unstake/try-runtime", diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index b39e53ad75e91..7ce0ed254ceff 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -408,6 +408,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { @@ -479,11 +480,19 @@ parameter_types! { /// This value increases the priority of `Operational` transactions by adding /// a "virtual tip" that's equal to the `OperationalFeeMultiplier * final_fee`. pub const OperationalFeeMultiplier: u8 = 5; + /// Percentage of fees that go to DAP satellite (0-100). + /// The remainder goes to block author. Tips always go 100% to author. + /// Westend: 0% to DAP (preserving original behavior of 100% to author) + pub const DapSatelliteFeePercent: u32 = 0; } +/// Fee handler that splits fees between DAP satellite and block author. +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = FungibleAdapter; type OperationalFeeMultiplier = OperationalFeeMultiplier; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -923,7 +932,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 @@ -1747,6 +1756,15 @@ impl pallet_root_offences::Config for Runtime { type ReportOffence = Offences; } +parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); +} + +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; + type PalletId = DapSatellitePalletId; +} + parameter_types! { pub MbmServiceWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; } @@ -2027,6 +2045,10 @@ mod runtime { #[runtime::pallet_index(105)] pub type RootOffences = pallet_root_offences; + // DAP Satellite - collects funds for transfer to DAP on AssetHub + #[runtime::pallet_index(106)] + pub type DapSatellite = pallet_dap_satellite; + // BEEFY Bridges support. #[runtime::pallet_index(200)] pub type Beefy = pallet_beefy; diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index d8f8e15f5eb05..46d8ecfac736d 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_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/prdoc/pr_10597.prdoc b/prdoc/pr_10597.prdoc new file mode 100644 index 0000000000000..cc104878504e0 --- /dev/null +++ b/prdoc/pr_10597.prdoc @@ -0,0 +1,104 @@ +title: 'Introduce pallet-dap-satellite for system chain fund collection and redirect all burns for native tokens to DAP/ DAP satellite on Westend runtimes' +doc: +- audience: Runtime Dev + description: |- + This PR introduces pallet-dap-satellite, allowing system chains different from AssetHub + to collect funds (to be periodically sent to DAP on AssetHub in future PRs) instead of + burning them directly. + + **Changes:** + + - **pallet-dap-satellite**: New pallet that implements `FundingSink` by accumulating funds + in a satellite account. + + - **[breaking change] pallet-balances**: `BurnDestination` is now a required config item + that must implement `BurnHandler`. The `BurnHandler::on_burned` method is called after + `decrease_balance` removes funds from an account, allowing each runtime to choose whether + burns lead to direct burning (reducing total issuance) or redirecting to a buffer. + Runtimes must explicitly configure this: + - Non-DAP runtimes should use `frame_support::traits::tokens::DirectBurn` + - DAP-integrated runtimes use `pallet_dap::ReturnToDap` (on AssetHub) or + `pallet_dap_satellite::AccumulateInSatellite` (on other system chains) + Note that non-native asset burns via pallet-assets do not use pallet-balances's implementation + of BurnHandler but they rely on the default one: each asset in pallet-assets has its own supply + that should decrease when burned. + + - **frame-support**: New `BurnHandler` trait that `FundingSink` now extends. `DirectBurn` + implements `BurnHandler::on_burned` to reduce total issuance. + + **Runtime integrations:** + + - User and pallets-initiated burns for native tokens redirect to DAP for asset-hub-westend + - User and pallets-initiated burns for native tokens redirect to DAP satellite for: westend (relay chain), + bridge-hub-westend, collectives-westend, coretime-westend, people-westend + - Remaining runtimes (rococo ecosystem, test runtimes) use `DirectBurn` which burns native + tokens directly +crates: +- name: frame-support + bump: minor +- name: pallet-balances + bump: major +- name: pallet-dap-satellite + bump: patch +- name: polkadot-sdk + bump: minor +- name: asset-hub-westend-runtime + bump: major +- name: westend-runtime + bump: major +- name: bridge-hub-westend-runtime + bump: major +- name: collectives-westend-runtime + bump: major +- name: coretime-westend-runtime + bump: major +- name: people-westend-runtime + bump: major +- name: rococo-runtime + bump: minor +- name: asset-hub-rococo-runtime + bump: minor +- name: bridge-hub-rococo-runtime + bump: minor +- name: people-rococo-runtime + bump: minor +- name: coretime-rococo-runtime + bump: minor +- name: kitchensink-runtime + bump: major +- name: polkadot-test-runtime + bump: major +- name: cumulus-test-runtime + bump: major +- name: rococo-parachain-runtime + bump: minor +- name: penpal-runtime + bump: minor +- name: yet-another-parachain-runtime + bump: minor +- name: pallet-asset-rewards + bump: patch +- name: pallet-assets-freezer + bump: patch +- name: pallet-nis + bump: patch +- name: pallet-contracts-mock-network + bump: minor +- name: pallet-staking-async + bump: patch +- name: pallet-staking-async-rc-runtime + bump: major +- name: pallet-staking-async-parachain-runtime + bump: major +- name: pallet-dap + bump: minor +- name: staging-xcm-builder + bump: patch +- name: solochain-template-runtime + bump: major +- name: parachain-template-runtime + bump: major +- name: pallet-asset-conversion-ops + bump: patch +- name: polkadot-runtime-parachains + bump: patch diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index d884b150948c4..18d69a86fd278 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -598,6 +598,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/asset-conversion/ops/src/mock.rs b/substrate/frame/asset-conversion/ops/src/mock.rs index 1d38ab615745a..519e072951c9c 100644 --- a/substrate/frame/asset-conversion/ops/src/mock.rs +++ b/substrate/frame/asset-conversion/ops/src/mock.rs @@ -60,6 +60,7 @@ impl frame_system::Config for Test { #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for Test { type AccountStore = System; + type BurnDestination = pallet_balances::DirectBurn; } #[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)] diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs index 320d6c3ec4257..66807de25e651 100644 --- a/substrate/frame/asset-rewards/src/mock.rs +++ b/substrate/frame/asset-rewards/src/mock.rs @@ -72,6 +72,7 @@ impl pallet_balances::Config for MockRuntime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_assets::Config for MockRuntime { diff --git a/substrate/frame/assets-freezer/src/mock.rs b/substrate/frame/assets-freezer/src/mock.rs index bacca601317d7..7d87c0a7e1f1d 100644 --- a/substrate/frame/assets-freezer/src/mock.rs +++ b/substrate/frame/assets-freezer/src/mock.rs @@ -86,6 +86,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_assets::Config for Test { diff --git a/substrate/frame/balances/src/impl_fungible.rs b/substrate/frame/balances/src/impl_fungible.rs index 03454c52eed2a..4af64a33f1b0a 100644 --- a/substrate/frame/balances/src/impl_fungible.rs +++ b/substrate/frame/balances/src/impl_fungible.rs @@ -20,11 +20,13 @@ use super::*; use frame_support::traits::{ tokens::{ Fortitude, + Precision::{self, BestEffort}, Preservation::{self, Preserve, Protect}, Provenance::{self, Minted}, }, AccountTouch, }; +use sp_runtime::{ArithmeticError, TokenError}; impl, I: 'static> fungible::Inspect for Pallet { type Balance = T::Balance; @@ -189,6 +191,25 @@ impl, I: 'static> fungible::Unbalanced for Pallet, I: 'static> fungible::Mutate for Pallet { + fn burn_from( + who: &T::AccountId, + amount: Self::Balance, + preservation: Preservation, + precision: Precision, + force: Fortitude, + ) -> Result { + use fungible::{Inspect, Unbalanced}; + let actual = Self::reducible_balance(who, preservation, force).min(amount); + ensure!(actual == amount || precision == BestEffort, TokenError::FundsUnavailable); + Self::total_issuance().checked_sub(&actual).ok_or(ArithmeticError::Overflow)?; + let actual = Self::decrease_balance(who, actual, BestEffort, preservation, force)?; + // Use configurable handler instead of directly reducing total issuance. + // For DirectBurn: reduces total issuance (traditional burning) + // For DAP satellite: credits buffer account (tokens preserved) + T::BurnDestination::on_burned(who, actual); + Self::done_burn_from(who, actual); + Ok(actual) + } fn done_mint_into(who: &T::AccountId, amount: Self::Balance) { Self::deposit_event(Event::::Minted { who: who.clone(), amount }); } diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index ebdb70f83dace..db464529f2aa8 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -164,7 +164,7 @@ use frame_support::{ pallet_prelude::DispatchResult, traits::{ tokens::{ - fungible, BalanceStatus as Status, DepositConsequence, + fungible, BalanceStatus as Status, BurnHandler, DepositConsequence, Fortitude::{self, Force, Polite}, IdAmount, Preservation::{Expendable, Preserve, Protect}, @@ -186,6 +186,7 @@ use sp_runtime::{ ArithmeticError, DispatchError, FixedPointOperand, Perbill, RuntimeDebug, TokenError, }; +pub use frame_support::traits::tokens::DirectBurn; pub use types::{ AccountData, AdjustmentDirection, BalanceLock, DustCleaner, ExtraFlags, Reasons, ReserveData, }; @@ -206,7 +207,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::*; @@ -245,6 +250,7 @@ pub mod pallet { type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = (); } } @@ -333,6 +339,21 @@ pub mod pallet { Self::AccountId, Self::Balance, >; + + /// Handler for burned funds. + /// + /// This is called by `burn_from` after the source account's balance has been decreased. + /// Runtimes can configure this to either: + /// - Reduce total issuance (traditional burning) + /// - Credit to a buffer account (DAP-style systems) + /// + /// - DAP-enabled runtimes on AssetHub: `type BurnDestination = + /// pallet_dap::ReturnToDap;` + /// - DAP satellite runtimes: `type BurnDestination = + /// pallet_dap_satellite::AccumulateInSatellite;` + /// - Other runtimes: `type BurnDestination = DirectBurn;` + #[pallet::no_default_bounds] + type BurnDestination: BurnHandler; } /// The in-code storage version. @@ -852,8 +873,9 @@ pub mod pallet { /// If the origin's account ends up below the existential deposit as a result /// of the burn and `keep_alive` is false, the account will be reaped. /// - /// 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 behavior depends on the runtime's `BurnDestination` configuration: + /// - DAP-enabled runtimes: funds are transferred to the DAP buffer + /// - Other runtimes: funds are burned directly, reducing total issuance #[pallet::call_index(10)] #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] pub fn burn( @@ -862,13 +884,14 @@ pub mod pallet { keep_alive: bool, ) -> DispatchResult { 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/balances/src/tests/mod.rs b/substrate/frame/balances/src/tests/mod.rs index 155f78884d122..c0587cf024eec 100644 --- a/substrate/frame/balances/src/tests/mod.rs +++ b/substrate/frame/balances/src/tests/mod.rs @@ -130,6 +130,7 @@ impl Config for Test { type RuntimeFreezeReason = TestId; type FreezeIdentifier = TestId; type MaxFreezes = VariantCountOf; + type BurnDestination = crate::DirectBurn, AccountId>; } #[derive(Clone)] diff --git a/substrate/frame/contracts/mock-network/src/parachain.rs b/substrate/frame/contracts/mock-network/src/parachain.rs index ad43ac42a7508..899e3aca7892c 100644 --- a/substrate/frame/contracts/mock-network/src/parachain.rs +++ b/substrate/frame/contracts/mock-network/src/parachain.rs @@ -97,6 +97,7 @@ impl pallet_balances::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/contracts/mock-network/src/relay_chain.rs b/substrate/frame/contracts/mock-network/src/relay_chain.rs index 0e60e3df6e19d..37e6167faa558 100644 --- a/substrate/frame/contracts/mock-network/src/relay_chain.rs +++ b/substrate/frame/contracts/mock-network/src/relay_chain.rs @@ -90,6 +90,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl shared::Config for Runtime { diff --git a/substrate/frame/dap-satellite/Cargo.toml b/substrate/frame/dap-satellite/Cargo.toml new file mode 100644 index 0000000000000..42987d9b60388 --- /dev/null +++ b/substrate/frame/dap-satellite/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "pallet-dap-satellite" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +description = "FRAME pallet for DAP Satellite - collects funds for periodic transfer to DAP on AssetHub" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { features = ["derive", "max-encoded-len"], workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +log = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +sp-runtime = { workspace = true } + +[dev-dependencies] +pallet-balances = { workspace = true, default-features = true } +pallet-dap = { workspace = true, default-features = true } +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-dap/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/dap-satellite/src/lib.rs b/substrate/frame/dap-satellite/src/lib.rs new file mode 100644 index 0000000000000..596a4ef082b38 --- /dev/null +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -0,0 +1,772 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # DAP Satellite Pallet +//! +//! This pallet is meant to be used on **system chains other than AssetHub** (e.g., Coretime, +//! People, BridgeHub) or on the **Relay Chain**. It should NOT be deployed on AssetHub, which +//! hosts the central DAP pallet (`pallet-dap`). +//! +//! ## Purpose +//! +//! The DAP Satellite collects funds that would otherwise be burned (e.g., transaction fees, +//! coretime revenue, slashing) into a local satellite account. These funds are accumulated +//! locally and will eventually be transferred via XCM to the central DAP buffer on AssetHub. +//! +//! ## Architecture +//! +//! ```text +//! +-----------------+ +-----------------+ +-----------------+ +//! | Relay Chain | | Coretime Chain | | People Chain | +//! | DAPSatellite | | DAPSatellite | | DAPSatellite | +//! +--------+--------+ +--------+--------+ +--------+--------+ +//! | | | +//! | XCM (periodic) | | +//! +-----------------------+-----------------------+ +//! | +//! v +//! +-----------------+ +//! | AssetHub | +//! | pallet-dap | +//! | (central) | +//! +-----------------+ +//! ``` +//! +//! ## Implementation +//! +//! This is a minimal implementation that only accumulates funds locally. The periodic XCM +//! transfer to AssetHub is NOT yet implemented. +//! +//! In this first iteration, the pallet provides the following components: +//! - `AccumulateInSatellite`: Implementation of `FundingSink` that transfers funds to the satellite +//! account instead of burning them. +//! - `SlashToSatellite`: Implementation of `OnUnbalanced` for the new `fungible::Balanced` trait, +//! useful for staking slashes. +//! +//! **TODO:** +//! - Periodic XCM transfer to AssetHub DAP buffer +//! - Configuration for XCM period and destination +//! - Weight accounting for XCM operations +//! +//! ## Usage +//! +//! On system chains (not AssetHub) or Relay Chain, configure pallets to use the satellite: +//! +//! ```ignore +//! // In runtime configuration for Coretime/People/BridgeHub/RelayChain +//! impl pallet_balances::Config for Runtime { +//! type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; +//! } +//! +//! // For fee handlers using OnUnbalanced +//! type FeeDestination = pallet_dap_satellite::SlashToSatellite; +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Balanced, Credit, Inspect, Mutate, Unbalanced}, + tokens::{BurnHandler, Fortitude, FundingSink, Precision, Preservation}, + Currency, Imbalance, OnUnbalanced, + }, + PalletId, +}; + +pub use pallet::*; + +const LOG_TARGET: &str = "runtime::dap-satellite"; + +/// Type alias for balance. +pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::sp_runtime::traits::AccountIdConversion; + + /// The in-code storage version. + const STORAGE_VERSION: frame_support::traits::StorageVersion = + frame_support::traits::StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency type. + type Currency: Inspect + + Mutate + + Unbalanced + + Balanced; + + /// The pallet ID used to derive the satellite account. + /// + /// Each runtime should configure a unique ID to avoid collisions if multiple + /// DAP satellite instances are used. + #[pallet::constant] + type PalletId: Get; + } + + impl Pallet { + /// Get the satellite account derived from the pallet ID. + /// + /// This account accumulates funds locally before they are sent to AssetHub. + pub fn satellite_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Ensure the satellite account exists by incrementing its provider count. + /// + /// This is called at genesis and on runtime upgrade. + /// It's idempotent - calling it multiple times is safe. + pub fn ensure_satellite_account_exists() { + let satellite = Self::satellite_account(); + if !frame_system::Pallet::::account_exists(&satellite) { + frame_system::Pallet::::inc_providers(&satellite); + log::info!( + target: LOG_TARGET, + "Created DAP satellite account: {satellite:?}" + ); + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> Weight { + // Create the satellite account if it doesn't exist (for chains upgrading to DAP). + Self::ensure_satellite_account_exists(); + // Weight: 1 read (account_exists) + potentially 1 write (inc_providers) + T::DbWeight::get().reads_writes(1, 1) + } + } + + /// Genesis config for the DAP Satellite pallet. + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + #[serde(skip)] + _phantom: core::marker::PhantomData, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + // Create the satellite account at genesis so it can receive funds of any amount. + Pallet::::ensure_satellite_account_exists(); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Funds accumulated in satellite account. + FundsAccumulated { from: T::AccountId, amount: BalanceOf }, + } +} + +/// Implementation of `FundingSink` that accumulates funds in the satellite account. +/// +/// Use this on system chains (not AssetHub) or Relay Chain to collect funds that would +/// otherwise be burned. The funds will eventually be transferred to AssetHub DAP via XCM. +/// +/// # Example +/// +/// ```ignore +/// impl pallet_balances::Config for Runtime { +/// type BurnDestination = AccumulateInSatellite; +/// } +/// ``` +pub struct AccumulateInSatellite(core::marker::PhantomData); + +impl BurnHandler> for AccumulateInSatellite { + fn on_burned(who: &T::AccountId, amount: BalanceOf) { + let satellite = Pallet::::satellite_account(); + + // Credit the satellite account. The source account's balance has already been decreased + // by `burn_from` before this is called. We use `increase_balance` which doesn't affect + // total issuance (keeping total issuance unchanged = funds preserved, not destroyed). + let _ = T::Currency::increase_balance(&satellite, amount, Precision::BestEffort); + + Pallet::::deposit_event(Event::FundsAccumulated { from: who.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Redirected burn of {amount:?} from {who:?} to satellite account" + ); + } +} + +impl FundingSink> for AccumulateInSatellite { + fn fill(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { + let satellite = Pallet::::satellite_account(); + + // Withdraw from source, resolve to satellite, emit event. If withdraw fails, nothing + // happens. If resolve fails (should never happen - satellite pre-created at genesis or via + // runtime upgrade), funds are burned. + T::Currency::withdraw(source, amount, Precision::Exact, preservation, Fortitude::Polite) + .ok() + .map(|credit| T::Currency::resolve(&satellite, credit)) + .map(|_| { + Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }) + }); + } +} + +/// Type alias for credit (negative imbalance - funds that were removed). +/// This is for the `fungible::Balanced` trait. +pub type CreditOf = Credit<::AccountId, ::Currency>; + +/// Implementation of `OnUnbalanced` for the `fungible::Balanced` trait. +/// +/// Use this on system chains (not AssetHub) or Relay Chain to collect funds from +/// imbalances (e.g., slashing) that would otherwise be burned. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via `DealWithFeesSplit`). +/// +/// # Example +/// +/// ```ignore +/// impl pallet_staking::Config for Runtime { +/// type Slash = SlashToSatellite; +/// } +/// ``` +pub struct SlashToSatellite(core::marker::PhantomData); + +impl OnUnbalanced> for SlashToSatellite { + fn on_nonzero_unbalanced(amount: CreditOf) { + let satellite = Pallet::::satellite_account(); + let numeric_amount = amount.peek(); + + // The satellite account is created at genesis or on_runtime_upgrade, so resolve should + // always succeed. If it somehow fails, log the error. + if let Err(remaining) = T::Currency::resolve(&satellite, amount) { + let remaining_amount = remaining.peek(); + if !remaining_amount.is_zero() { + log::error!( + target: LOG_TARGET, + "Failed to deposit to satellite account - {remaining_amount:?} will be burned!" + ); + } + } + + log::debug!( + target: LOG_TARGET, + "Deposited {numeric_amount:?} to satellite account (fungible)" + ); + } +} + +/// A configurable fee handler that splits fees between DAP satellite and another destination. +/// +/// - `DapPercent`: Percentage of fees (0-100) to send to DAP satellite +/// - `OtherHandler`: Where to send the remaining fees (e.g., `ToAuthor`, `DealWithFees`) +/// +/// Tips always go 100% to `OtherHandler`. +/// +/// # Example +/// +/// ```ignore +/// parameter_types! { +/// pub const DapSatelliteFeePercent: u32 = 0; // 0% to DAP, 100% to staking +/// } +/// +/// type DealWithFeesSatellite = pallet_dap_satellite::DealWithFeesSplit< +/// Runtime, +/// DapSatelliteFeePercent, +/// DealWithFees, // Or ToAuthor for relay chain +/// >; +/// +/// impl pallet_transaction_payment::Config for Runtime { +/// type OnChargeTransaction = FungibleAdapter; +/// } +/// ``` +pub struct DealWithFeesSplit( + core::marker::PhantomData<(T, DapPercent, OtherHandler)>, +); + +impl OnUnbalanced> + for DealWithFeesSplit +where + T: Config, + DapPercent: Get, + OtherHandler: OnUnbalanced>, +{ + fn on_unbalanceds(mut fees_then_tips: impl Iterator>) { + if let Some(fees) = fees_then_tips.next() { + let dap_percent = DapPercent::get(); + let other_percent = 100u32.saturating_sub(dap_percent); + let mut split = fees.ration(dap_percent, other_percent); + if let Some(tips) = fees_then_tips.next() { + // Tips go 100% to other handler. + tips.merge_into(&mut split.1); + } + if dap_percent > 0 { + as OnUnbalanced<_>>::on_unbalanced(split.0); + } + OtherHandler::on_unbalanced(split.1); + } + } +} + +/// Implementation of `OnUnbalanced` for the old `Currency` trait. +/// +/// Use this on system chains (not AssetHub) or Relay Chain for pallets that still use +/// the legacy `Currency` trait (e.g., fee handlers, treasury burns). +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction). +/// +/// # Example +/// +/// ```ignore +/// // For fee handling +/// type FeeDestination = SinkToSatellite; +/// +/// // For treasury burns +/// impl pallet_treasury::Config for Runtime { +/// type BurnDestination = SinkToSatellite; +/// } +/// ``` +pub struct SinkToSatellite(core::marker::PhantomData<(T, C)>); + +impl OnUnbalanced for SinkToSatellite +where + T: Config, + C: Currency, +{ + fn on_nonzero_unbalanced(amount: C::NegativeImbalance) { + let satellite = Pallet::::satellite_account(); + let numeric_amount = amount.peek(); + + // Resolve the imbalance by depositing into the satellite account + C::resolve_creating(&satellite, amount); + + log::debug!( + target: LOG_TARGET, + "Deposited {numeric_amount:?} to satellite account (Currency trait)" + ); + } +} + +// TODO: Implement periodic XCM transfer to AssetHub DAP buffer +// +// Future implementation will add: +// 1. `on_initialize` hook to mark XCM as pending at configured intervals +// 2. `on_poll` hook to execute XCM transfer when pending and weight available +// 3. Configuration for: +// - `XcmPeriod`: How often to send accumulated funds (e.g., every 14400 blocks = ~1 day) +// - `AssetHubLocation`: XCM destination for AssetHub +// - `DapBufferBeneficiary`: The DAP buffer account on AssetHub +// 4. XCM message construction: +// - Burn from local satellite account +// - Teleport to AssetHub +// - Deposit into DAP buffer account + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + derive_impl, parameter_types, + sp_runtime::traits::AccountIdConversion, + traits::{ + fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, + OnUnbalanced, WithdrawReasons, + }, + }; + use sp_runtime::BuildStorage; + use std::cell::Cell; + + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + DapSatellite: crate, + } + ); + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; + } + + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] + impl pallet_balances::Config for Test { + type AccountStore = System; + } + + parameter_types! { + pub const DapSatellitePalletId: PalletId = PalletId(*b"dap/satl"); + } + + impl Config for Test { + type Currency = Balances; + type PalletId = DapSatellitePalletId; + } + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 200), (3, 300)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + crate::pallet::GenesisConfig::::default() + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } + + #[test] + fn satellite_account_is_derived_from_pallet_id() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + let expected: u64 = DapSatellitePalletId::get().into_account_truncating(); + assert_eq!(satellite, expected); + }); + } + + #[test] + fn genesis_creates_satellite_account() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + // Satellite account should exist after genesis (created via inc_providers) + assert!(System::account_exists(&satellite)); + }); + } + + // ===== fill tests ===== + + #[test] + fn fill_accumulates_from_multiple_sources() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let satellite = DapSatellite::satellite_account(); + + // Given: accounts have balances, satellite is empty + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fill from multiple accounts + AccumulateInSatellite::::fill(&1, 20, Preservation::Preserve); + AccumulateInSatellite::::fill(&2, 50, Preservation::Preserve); + AccumulateInSatellite::::fill(&3, 100, Preservation::Preserve); + + // Then: satellite has accumulated all funds + assert_eq!(Balances::free_balance(satellite), 170); + // ... accounts have their balance correctly updated + assert_eq!(Balances::free_balance(1), 80); + assert_eq!(Balances::free_balance(2), 150); + assert_eq!(Balances::free_balance(3), 200); + // ... and events are emitted + System::assert_has_event( + Event::::FundsAccumulated { from: 1, amount: 20 }.into(), + ); + System::assert_has_event( + Event::::FundsAccumulated { from: 2, amount: 50 }.into(), + ); + System::assert_has_event( + Event::::FundsAccumulated { from: 3, amount: 100 }.into(), + ); + }); + } + + #[test] + fn fill_with_insufficient_balance_is_noop() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100, satellite has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + + // When: try to fill 150 (more than balance) + AccumulateInSatellite::::fill(&1, 150, Preservation::Preserve); + + // Then: balances unchanged (infallible no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + }); + } + + #[test] + fn fill_with_zero_amount_succeeds() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100, satellite has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fill 0 from account 1 + AccumulateInSatellite::::fill(&1, 0, Preservation::Preserve); + + // Then: balances unchanged (no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + }); + } + + #[test] + fn fill_with_expendable_allows_full_drain() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: fill full balance with Expendable (allows going to 0) + AccumulateInSatellite::::fill(&1, 100, Preservation::Expendable); + + // Then: account 1 is empty, satellite has 100 + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(satellite), 100); + }); + } + + #[test] + fn fill_with_preserve_respects_existential_deposit() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: account 1 has 100, ED is 1 (from TestDefaultConfig) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + + // When: try to fill 100 with Preserve (would go below ED) + AccumulateInSatellite::::fill(&1, 100, Preservation::Preserve); + + // Then: balances unchanged (infallible - would have killed account) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(satellite), 0); + + // But filling 99 works (leaves 1 for ED) + AccumulateInSatellite::::fill(&1, 99, Preservation::Preserve); + assert_eq!(Balances::free_balance(1), 1); + assert_eq!(Balances::free_balance(satellite), 99); + }); + } + + // ===== SlashToSatellite tests ===== + + #[test] + 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); + }); + } + + #[test] + fn slash_to_satellite_handles_zero_amount() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: slash with zero amount + let credit = >::issue(0); + SlashToSatellite::::on_unbalanced(credit); + + // Then: satellite still has 0 (no-op) + assert_eq!(Balances::free_balance(satellite), 0); + }); + } + + // ===== SinkToSatellite tests ===== + + #[test] + 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/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..3c9d89a21db6d --- /dev/null +++ b/substrate/frame/dap/src/lib.rs @@ -0,0 +1,488 @@ +// 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, Unbalanced}, + tokens::{BurnHandler, 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 + + Unbalanced + + 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 BurnHandler and FundingSink that fills the DAP buffer. +/// Funds are transferred to the buffer account instead of being burned. +pub struct ReturnToDap(core::marker::PhantomData); + +impl BurnHandler> for ReturnToDap { + fn on_burned(who: &T::AccountId, amount: BalanceOf) { + let buffer = Pallet::::buffer_account(); + + // Credit the buffer account. The source account's balance has already been decreased + // by `burn_from` before this is called. We use `increase_balance` which doesn't affect + // total issuance (keeping total issuance unchanged = funds preserved, not destroyed). + let _ = T::Currency::increase_balance(&buffer, amount, Precision::BestEffort); + + Pallet::::deposit_event(Event::FundsReturned { from: who.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Redirected burn of {amount:?} from {who:?} to DAP buffer" + ); + } +} + +impl FundingSink> for ReturnToDap { + fn fill(source: &T::AccountId, amount: BalanceOf, preservation: Preservation) { + let buffer = Pallet::::buffer_account(); + + // 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/nis/src/mock.rs b/substrate/frame/nis/src/mock.rs index 0e71e43f56bd7..739d088c193ee 100644 --- a/substrate/frame/nis/src/mock.rs +++ b/substrate/frame/nis/src/mock.rs @@ -71,6 +71,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_balances::Config for Test { @@ -92,6 +93,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/staking-async/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..efefddf80cb3c 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs @@ -229,6 +229,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { @@ -1196,6 +1197,9 @@ construct_runtime!( Treasury: pallet_treasury = 96, AssetRate: pallet_asset_rate = 97, + // Dynamic Allocation Pool / Issuance buffer + Dap: pallet_dap = 98, + // Balances. Vesting: pallet_vesting = 100, diff --git a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs index b344a8e047c4d..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..4bf045a8d4e76 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -451,6 +451,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { @@ -999,7 +1000,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..be45c809c0b15 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::{BurnHandler, DirectBurn, FundingSink}; pub use imbalance::Imbalance; pub mod pay; pub mod transfer; diff --git a/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs new file mode 100644 index 0000000000000..9de076c029acb --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -0,0 +1,118 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for returning funds to an issuance system. +//! +//! This module provides abstractions for returning funds (burns, slashing) in a way that can be +//! configured differently per runtime. +//! +//! Two main patterns: +//! - **Direct burn**: Traditional approach where funds are destroyed on demand +//! - **Buffer-based**: Funds are returned to a buffer for reuse + +use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; +use core::marker::PhantomData; +use sp_runtime::Saturating; + +/// Trait for handling burned funds. +/// +/// This trait is used by `pallet_balances::burn_from` to handle funds after they have been +/// removed from the source account. Implementations can either: +/// - Reduce total issuance (traditional burning) +/// - Credit to a buffer account (DAP-style systems) +/// +/// The key distinction from `FundingSink::fill` is that `on_burned` is called AFTER +/// the funds have already been removed from the source account via `decrease_balance`. +pub trait BurnHandler { + /// Handle funds that have been burned from an account. + /// + /// Called by `burn_from` after the source account's balance has been decreased. + /// The implementation should either: + /// - Reduce total issuance (for actual burning) + /// - Credit the amount to a buffer account (for DAP systems) + /// + /// This operation is infallible. + fn on_burned(who: &AccountId, amount: Balance); +} + +/// Trait for moving funds into an issuance buffer or burning them. +/// +/// 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: BurnHandler { + /// 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 `BurnHandler` and `FundingSink`. +/// +/// This implementation burns tokens directly, reducing total issuance. +/// Used for traditional burn systems (e.g., Kusama). +/// +/// # Type Parameters +/// +/// * `Currency` - The currency type that implements `Mutate` and `Unbalanced` +/// * `AccountId` - The account identifier type +pub struct DirectBurn(PhantomData<(Currency, AccountId)>); + +impl BurnHandler + for DirectBurn +where + Currency: fungible::Unbalanced, + AccountId: Eq, +{ + fn on_burned(_who: &AccountId, amount: Currency::Balance) { + // Reduce total issuance - funds are permanently destroyed + Currency::set_total_issuance(Currency::total_issuance().saturating_sub(amount)); + } +} + +impl FundingSink + for DirectBurn +where + Currency: fungible::Mutate + fungible::Unbalanced, + 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 `BurnHandler` for unit type. +impl BurnHandler for () { + fn on_burned(_who: &AccountId, _amount: Balance) {} +} + +/// No-op implementation of `FundingSink` for unit type. +/// Used for testing or when no sink behavior is needed. +impl FundingSink for () { + fn fill(_from: &AccountId, _amount: Balance, _preservation: Preservation) {} +} diff --git a/substrate/test-utils/runtime/src/lib.rs b/substrate/test-utils/runtime/src/lib.rs index a04b05021bb46..04a6e5b15d3a9 100644 --- a/substrate/test-utils/runtime/src/lib.rs +++ b/substrate/test-utils/runtime/src/lib.rs @@ -427,6 +427,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_utility::Config for Runtime { diff --git a/templates/parachain/runtime/src/configs/mod.rs b/templates/parachain/runtime/src/configs/mod.rs index 2ac558ea2a310..8dd75091de890 100644 --- a/templates/parachain/runtime/src/configs/mod.rs +++ b/templates/parachain/runtime/src/configs/mod.rs @@ -175,6 +175,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/templates/solochain/runtime/src/configs/mod.rs b/templates/solochain/runtime/src/configs/mod.rs index b8810a068036c..6a7faea0cc532 100644 --- a/templates/solochain/runtime/src/configs/mod.rs +++ b/templates/solochain/runtime/src/configs/mod.rs @@ -142,6 +142,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { 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;