diff --git a/Cargo.lock b/Cargo.lock index d129f4eea452c..54dcd7e43a11c 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", @@ -2854,6 +2855,7 @@ dependencies = [ "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-session", @@ -3472,6 +3474,7 @@ dependencies = [ "pallet-collective", "pallet-collective-content", "pallet-core-fellowship", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-preimage", @@ -3918,6 +3921,7 @@ dependencies = [ "pallet-balances", "pallet-broker", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-message-queue", "pallet-multisig", "pallet-proxy", @@ -12193,6 +12197,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" @@ -13631,6 +13668,7 @@ dependencies = [ "log", "pallet-bags-list", "pallet-balances", + "pallet-dap", "pallet-staking-async-rc-client", "parity-scale-codec", "rand 0.8.5", @@ -13711,6 +13749,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-conviction-voting", + "pallet-dap", "pallet-delegated-staking", "pallet-election-provider-multi-block", "pallet-fast-unstake", @@ -14898,6 +14937,7 @@ dependencies = [ "pallet-authorship", "pallet-balances", "pallet-collator-selection", + "pallet-dap-satellite", "pallet-identity", "pallet-message-queue", "pallet-migrations", @@ -16570,6 +16610,8 @@ dependencies = [ "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", + "pallet-dap", + "pallet-dap-satellite", "pallet-delegated-staking", "pallet-democracy", "pallet-derivatives", @@ -27515,6 +27557,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..ad88665865d8a 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 852e7e1196e33..6d19f732c3a51 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -120,6 +120,7 @@ cumulus-primitives-aura = { workspace = true } cumulus-primitives-core = { workspace = true } cumulus-primitives-utility = { workspace = true } pallet-collator-selection = { workspace = true } +pallet-dap = { workspace = true } pallet-message-queue = { workspace = true } parachain-info = { workspace = true } parachains-common = { workspace = true } @@ -173,6 +174,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", "pallet-delegated-staking/runtime-benchmarks", "pallet-election-provider-multi-block/runtime-benchmarks", "pallet-fast-unstake/runtime-benchmarks", @@ -246,6 +248,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-conviction-voting/try-runtime", + "pallet-dap/try-runtime", "pallet-delegated-staking/try-runtime", "pallet-election-provider-multi-block/try-runtime", "pallet-fast-unstake/try-runtime", @@ -326,6 +329,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-conviction-voting/std", + "pallet-dap/std", "pallet-delegated-staking/std", "pallet-election-provider-multi-block/std", "pallet-fast-unstake/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs index 3b0c006c39ce8..f7522b6c0485b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/governance/mod.rs @@ -131,7 +131,7 @@ impl pallet_treasury::Config for Runtime { type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; type Burn = Burn; - type BurnDestination = (); + type BurnDestination = pallet_dap::BurnToDap; type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = (); diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 75c38fc9d1f36..944ac08e0d14d 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -245,6 +245,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_dap::ReturnToDap; } parameter_types! { @@ -1398,6 +1399,9 @@ construct_runtime!( Treasury: pallet_treasury = 94, AssetRate: pallet_asset_rate = 95, + // Dynamic Allocation Pool / Issuance Buffer + Dap: pallet_dap = 100, + // TODO: the pallet instance should be removed once all pools have migrated // to the new account IDs. AssetConversionMigration: pallet_asset_conversion_ops = 200, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs index 9571e38a71a3f..f3fa627b94b7c 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs @@ -278,7 +278,7 @@ impl pallet_staking_async::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type CurrencyToVote = sp_staking::currency_to_vote::SaturatingCurrencyToVote; type RewardRemainder = (); - type Slash = (); + type Slash = pallet_dap::SlashToDap; type Reward = (); type SessionsPerEra = SessionsPerEra; type BondingDuration = BondingDuration; @@ -311,6 +311,10 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +impl pallet_dap::Config for Runtime { + type Currency = Balances; +} + #[derive(Encode, Decode)] // Call indices taken from westend-next runtime. pub enum RelayChainRuntimePallets { diff --git a/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..ec85366c8e4b2 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..1a622fc1fa813 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,10 @@ impl pallet_utility::Config for Runtime { type WeightInfo = weights::pallet_utility::WeightInfo; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -583,6 +594,9 @@ construct_runtime!( Utility: pallet_utility = 40, Multisig: pallet_multisig = 36, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 60, + // Bridging stuff. BridgeRelayers: pallet_bridge_relayers = 41, BridgeRococoGrandpa: pallet_bridge_grandpa:: = 42, diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml index 7c466c22693ec..2cdfd290f2f2a 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/Cargo.toml @@ -34,6 +34,7 @@ pallet-authorship = { workspace = true } pallet-balances = { workspace = true } pallet-collective = { workspace = true } pallet-core-fellowship = { workspace = true } +pallet-dap-satellite = { workspace = true } pallet-multisig = { workspace = true } pallet-preimage = { workspace = true } pallet-proxy = { workspace = true } @@ -120,6 +121,7 @@ runtime-benchmarks = [ "pallet-collective-content/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-core-fellowship/runtime-benchmarks", + "pallet-dap-satellite/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", @@ -165,6 +167,7 @@ try-runtime = [ "pallet-collective-content/try-runtime", "pallet-collective/try-runtime", "pallet-core-fellowship/try-runtime", + "pallet-dap-satellite/try-runtime", "pallet-message-queue/try-runtime", "pallet-multisig/try-runtime", "pallet-preimage/try-runtime", @@ -213,6 +216,7 @@ std = [ "pallet-collective-content/std", "pallet-collective/std", "pallet-core-fellowship/std", + "pallet-dap-satellite/std", "pallet-message-queue/std", "pallet-multisig/std", "pallet-preimage/std", diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs index 47e8e24a8efb9..06adb44e66c6d 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/lib.rs @@ -234,17 +234,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 WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; type FeeMultiplierUpdate = SlowAdjustingFeeUpdate; @@ -748,6 +755,9 @@ construct_runtime!( StateTrieMigration: pallet_state_trie_migration = 80, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 85, + // The Secretary Collective // pub type SecretaryCollectiveInstance = pallet_ranked_collective::instance3; SecretaryCollective: pallet_ranked_collective:: = 90, @@ -1383,6 +1393,10 @@ impl pallet_state_trie_migration::Config for Runtime { type MaxKeyLen = MigrationMaxKeyLen; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + frame_support::ord_parameter_types! { pub const MigController: AccountId = AccountId::from(hex_literal::hex!("8458ed39dc4b6f6c7255f7bc42be50c2967db126357c999d44e12ca7ac80dc52")); pub const RootMigController: AccountId = AccountId::from(hex_literal::hex!("8458ed39dc4b6f6c7255f7bc42be50c2967db126357c999d44e12ca7ac80dc52")); diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/lib.rs index 040d99280605f..e5ee138fad9c7 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..0707b7cedde5e 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs @@ -18,69 +18,22 @@ use crate::{xcm_config::LocationToAccountId, *}; use codec::{Decode, Encode}; use cumulus_pallet_parachain_system::RelaychainDataProvider; use cumulus_primitives_core::relay_chain; -use frame_support::{ - parameter_types, - traits::{ - fungible::{Balanced, Credit, Inspect}, - tokens::{Fortitude, Preservation}, - DefensiveResult, OnUnbalanced, - }, -}; -use frame_system::Pallet as System; +use frame_support::parameter_types; use pallet_broker::{ CoreAssignment, CoreIndex, CoretimeInterface, PartsOf57600, RCBlockNumberOf, TaskId, Timeslice, }; use parachains_common::{AccountId, Balance}; -use sp_runtime::traits::{AccountIdConversion, MaybeConvert}; +use sp_runtime::traits::MaybeConvert; use westend_runtime_constants::system_parachain::coretime; use xcm::latest::prelude::*; -use xcm_executor::traits::{ConvertLocation, TransactAsset}; - -pub struct BurnCoretimeRevenue; -impl OnUnbalanced> for BurnCoretimeRevenue { - fn on_nonzero_unbalanced(amount: Credit) { - let acc = RevenueAccumulationAccount::get(); - if !System::::account_exists(&acc) { - System::::inc_providers(&acc); - } - Balances::resolve(&acc, amount).defensive_ok(); - } -} +use xcm_executor::traits::ConvertLocation; -type AssetTransactor = ::AssetTransactor; - -fn burn_at_relay(stash: &AccountId, value: Balance) -> Result<(), XcmError> { - let dest = Location::parent(); - let stash_location = - Junction::AccountId32 { network: None, id: stash.clone().into() }.into_location(); - let asset = Asset { id: AssetId(Location::parent()), fun: Fungible(value) }; - let dummy_xcm_context = XcmContext { origin: None, message_id: [0; 32], topic: None }; - - let withdrawn = AssetTransactor::withdraw_asset(&asset, &stash_location, None)?; - - AssetTransactor::can_check_out(&dest, &asset, &dummy_xcm_context)?; - - let parent_assets = Into::::into(withdrawn) - .reanchored(&dest, &Here.into()) - .defensive_map_err(|_| XcmError::ReanchorFailed)?; - - PolkadotXcm::send_xcm( - Here, - Location::parent(), - Xcm(vec![ - Instruction::UnpaidExecution { - weight_limit: WeightLimit::Unlimited, - check_origin: None, - }, - ReceiveTeleportedAsset(parent_assets.clone()), - BurnAsset(parent_assets), - ]), - )?; - - AssetTransactor::check_out(&dest, &asset, &dummy_xcm_context); - - Ok(()) -} +/// Coretime revenue handler that sends funds to the DAP satellite account. +/// +/// Previously, revenue was accumulated in a stash account and then burned at the relay chain. +/// With DAP, revenue is accumulated in the satellite account and then periodically sent +/// to the DAP buffer on AssetHub via XCM. +pub type CoretimeRevenueToSatellite = pallet_dap_satellite::SlashToSatellite; /// A type containing the encoding of the coretime pallet in the Relay chain runtime. Used to /// construct any remote calls. The codec index must correspond to the index of `Coretime` in the @@ -112,7 +65,6 @@ enum CoretimeProviderCalls { parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); pub const MinimumCreditPurchase: Balance = UNITS / 10; - pub RevenueAccumulationAccount: AccountId = BrokerPalletId::get().into_sub_account_truncating(b"burnstash"); pub const MinimumEndPrice: Balance = UNITS; } @@ -287,21 +239,9 @@ impl CoretimeInterface for CoretimeAllocator { } fn on_new_timeslice(_timeslice: Timeslice) { - let stash = RevenueAccumulationAccount::get(); - let value = - Balances::reducible_balance(&stash, Preservation::Expendable, Fortitude::Polite); - - if value > 0 { - tracing::debug!(target: "runtime::coretime", %value, "Going to burn stashed tokens at RC"); - match burn_at_relay(&stash, value) { - Ok(()) => { - tracing::debug!(target: "runtime::coretime", %value, "Successfully burnt tokens"); - }, - Err(err) => { - tracing::error!(target: "runtime::coretime", error=?err, "burn_at_relay failed"); - }, - } - } + // With DAP satellite, revenue is already accumulated in the satellite account via + // CoretimeRevenueToSatellite (OnRevenue handler). The satellite pallet then sends + // accumulated funds to AssetHub DAP via XCM } } @@ -317,7 +257,7 @@ impl MaybeConvert for SovereignAccountOf { impl pallet_broker::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; - type OnRevenue = BurnCoretimeRevenue; + type OnRevenue = CoretimeRevenueToSatellite; type TimeslicePeriod = ConstU32<{ coretime::TIMESLICE_PERIOD }>; // We don't actually need any leases at launch but set to 10 in case we want to sudo some in. type MaxLeasedCores = ConstU32<10>; diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs index 153b4e01dc381..c5b25b46d6046 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/lib.rs @@ -267,6 +267,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { @@ -612,6 +613,10 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + pub struct BrokerMigrationV4BlockConversion; impl pallet_broker::migration::v4::BlockToRelayHeightConversion @@ -667,6 +672,9 @@ construct_runtime!( // The main stage. Broker: pallet_broker = 50, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub + DapSatellite: pallet_dap_satellite = 60, + // Sudo Sudo: pallet_sudo = 100, } diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs index ccc1ae56c685f..b713de6aa581b 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..63f9ea3046dbf 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/lib.rs @@ -244,17 +244,26 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_dap_satellite::AccumulateInSatellite; } parameter_types! { /// Relay Chain `TransactionByteFee` / 10. pub const TransactionByteFee: Balance = MILLICENTS; + /// Percentage of fees that go to DAP satellite (0-100). + /// The remainder goes to the staking pot. Tips always go 100% to staking pot. + /// Set to 0 to preserve original behavior (100% to staking pot). + pub const DapSatelliteFeePercent: u32 = 0; } +/// Fee handler that splits fees between DAP satellite and staking pot. +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; @@ -578,6 +587,10 @@ impl pallet_migrations::Config for Runtime { type WeightInfo = weights::pallet_migrations::WeightInfo; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime @@ -614,6 +627,9 @@ construct_runtime!( // The main stage. Identity: pallet_identity = 50, + // DAP Satellite - collects funds for eventual transfer to DAP on AssetHub. + DapSatellite: pallet_dap_satellite = 60, + // Migrations pallet MultiBlockMigrations: pallet_migrations = 98, diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index 01f6dd1700d0d..ead3d0936bdd5 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -433,6 +433,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs index 12a322534da5a..bb26562b5081b 100644 --- a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs @@ -265,6 +265,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs index 92df3d950cb0e..985b4e3dcc52c 100644 --- a/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/yet-another-parachain/src/lib.rs @@ -307,6 +307,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/cumulus/test/runtime/src/lib.rs b/cumulus/test/runtime/src/lib.rs index 8acbc49a33834..c0e3ae92db941 100644 --- a/cumulus/test/runtime/src/lib.rs +++ b/cumulus/test/runtime/src/lib.rs @@ -334,6 +334,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_transaction_payment::Config for Runtime { diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 80426212d4c29..fe4978df7f108 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -419,6 +419,7 @@ impl pallet_balances::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; type MaxFreezes = ConstU32<1>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { @@ -1319,6 +1320,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<1>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index b4a368c8d8a19..71f252c7400b7 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -275,6 +275,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/polkadot/runtime/westend/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..4ed86dccadc0d 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,20 @@ parameter_types! { /// This value increases the priority of `Operational` transactions by adding /// a "virtual tip" that's equal to the `OperationalFeeMultiplier * final_fee`. pub const OperationalFeeMultiplier: u8 = 5; + /// Percentage of fees that go to DAP satellite (0-100). + /// The remainder goes to block author. Tips always go 100% to author. + /// Westend: 0% to DAP (preserving original behavior of 100% to author) + /// Polkadot/Kusama: configurable (e.g., 80% to DAP, 20% to author) + pub const DapSatelliteFeePercent: u32 = 0; } +/// Fee handler that splits fees between DAP satellite and block author. +type DealWithFeesSatellite = + pallet_dap_satellite::DealWithFeesSplit>; + impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = FungibleAdapter; type OperationalFeeMultiplier = OperationalFeeMultiplier; type WeightToFee = WeightToFee; type LengthToFee = ConstantMultiplier; @@ -1747,6 +1757,10 @@ impl pallet_root_offences::Config for Runtime { type ReportOffence = Offences; } +impl pallet_dap_satellite::Config for Runtime { + type Currency = Balances; +} + parameter_types! { pub MbmServiceWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; } @@ -2027,6 +2041,10 @@ mod runtime { #[runtime::pallet_index(105)] pub type RootOffences = pallet_root_offences; + // DAP Satellite - collects funds for transfer to DAP on AssetHub + #[runtime::pallet_index(106)] + pub type DapSatellite = pallet_dap_satellite; + // BEEFY Bridges support. #[runtime::pallet_index(200)] pub type Beefy = pallet_beefy; diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index d8f8e15f5eb05..1275c00761511 100644 --- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs +++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs @@ -85,6 +85,7 @@ impl pallet_balances::Config for Test { type FreezeIdentifier = (); type MaxFreezes = ConstU32<0>; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/prdoc/pr_10481.prdoc b/prdoc/pr_10481.prdoc new file mode 100644 index 0000000000000..011b385ec6baa --- /dev/null +++ b/prdoc/pr_10481.prdoc @@ -0,0 +1,99 @@ +title: 'First iteration of Dynamic Allocation Pool (DAP): burns -> DAP buffer/satellite' +doc: +- audience: Runtime Dev + description: |- + This initial version makes it possible to collect funds that would otherwise be burned into + - the DAP buffer on AssetHub + - the DAP satellite on system chains and the RelayChain + This is fully configurable, meaning that chains can choose to redirect all, some, or none of the funds that would be burned into DAP. + + The mechanism to periodically transfer funds from the DAP satellite to the DAP buffer via XCM is not yet in place. + + Westend RelayChain and system chains are now integrated with DAP / DAP satellite. + - On AssetHub: + - treasury unspent and staking slashes are now redirected to the DAP buffer instead of being burned. + - All user-initiated burns also go to the DAP buffer instead of reducing total issuance. + - On RelayChain, DAP satellite is included, but the original behavior of redirecting 100% of tx fees to the block author is preserved. + - On Coretime, revenues that would be transferred daily to XCM and burned, are now redirected to the DAP satellite instead. + - On other system chains (People, BridgeHub, Collective), DAP satellite is included, but the original behavior of redirecting 100% of tx fees to the staking pot is preserved. + + Events: `FundsReturned` (pallet-dap) and `FundsAccumulated` (pallet-dap-satellite) are emitted only for explicit user actions (e.g., `burn` extrinsic via `FundingSink`). + Automatic system operations like fee handling and slashes via `OnUnbalanced` do NOT emit events to avoid bloating blocks with tons of events per block. + + [Breaking change]: all runtimes must now explicitly configure `BurnDestination`: + - Non-DAP runtimes should use `pallet_balances::DirectBurn` for direct burning. + - DAP-integrated runtimes use `pallet_dap::ReturnToDap` (on AssetHub) or `pallet_dap_satellite::AccumulateInSatellite` (on other system chains). + + In the next iterations, we will introduce the following: + - Make it possible to replace direct minting with requesting funds from DAP buffer. + - Make DAP responsible to mint according to the issuance curve defined per chain. + - Make it possible to configure the percentage of funds redirected from DAP to different destinations (validators, nominators, treasury, collators, ...). + - Support for multiple assets in DAP: native token and stablecoin. + - The periodic transfer of funds from DAP satellite to DAP buffer via XCM. +crates: +- name: frame-support + bump: minor +- name: pallet-assets-freezer + bump: patch +- name: pallet-asset-rewards + bump: patch +- name: pallet-balances + bump: major +- name: pallet-dap + bump: patch +- name: pallet-dap-satellite + bump: patch +- name: polkadot-sdk + bump: minor +- name: asset-hub-westend-runtime + bump: major +- name: pallet-staking-async + bump: patch +- name: coretime-westend-runtime + bump: major +- name: westend-runtime + bump: major +- name: bridge-hub-westend-runtime + bump: major +- name: collectives-westend-runtime + bump: major +- name: people-westend-runtime + bump: major +- name: rococo-runtime + bump: minor +- name: bridge-hub-rococo-runtime + bump: minor +- name: people-rococo-runtime + bump: minor +- name: coretime-rococo-runtime + bump: minor +- name: asset-hub-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: parachain-template-runtime + bump: major +- name: solochain-template-runtime + bump: major +- name: substrate-test-runtime + bump: major +- name: pallet-staking-async-rc-runtime + bump: major +- name: pallet-staking-async-parachain-runtime + bump: major +- name: staging-xcm-builder + bump: patch +- name: pallet-nis + bump: patch +- name: pallet-contracts-mock-network + bump: minor +- name: yet-another-parachain-runtime + bump: minor diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 8eb710dfa99e4..1ccc565754c73 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -598,6 +598,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs index 320d6c3ec4257..8911841bcd388 100644 --- a/substrate/frame/asset-rewards/src/mock.rs +++ b/substrate/frame/asset-rewards/src/mock.rs @@ -72,6 +72,7 @@ impl pallet_balances::Config for MockRuntime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_assets::Config for MockRuntime { diff --git a/substrate/frame/assets-freezer/src/mock.rs b/substrate/frame/assets-freezer/src/mock.rs index bacca601317d7..af8ec237e906b 100644 --- a/substrate/frame/assets-freezer/src/mock.rs +++ b/substrate/frame/assets-freezer/src/mock.rs @@ -86,6 +86,7 @@ impl pallet_balances::Config for Test { type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_assets::Config for Test { diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index ebdb70f83dace..fba2412517eee 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -206,12 +206,22 @@ pub mod pallet { use codec::HasCompact; use frame_support::{ pallet_prelude::*, - traits::{fungible::Credit, tokens::Precision, VariantCount, VariantCountOf}, + traits::{ + fungible::Credit, + tokens::{FundingSink, Precision, Preservation}, + VariantCount, VariantCountOf, + }, }; use frame_system::pallet_prelude::*; pub type CreditOf = Credit<::AccountId, Pallet>; + /// Default implementation of `FundingSink` that burns tokens directly. + /// + /// This reduces total issuance when users call the `burn` extrinsic. + /// Used as the default for `BurnDestination` in production runtimes. + pub struct DirectBurn(PhantomData<(T, I)>); + /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. pub mod config_preludes { use super::*; @@ -245,6 +255,7 @@ pub mod pallet { type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = (); } } @@ -333,6 +344,14 @@ pub mod pallet { Self::AccountId, Self::Balance, >; + + /// Handler for user-initiated burns via the `burn` extrinsic. + /// + /// Runtimes can configure this to redirect burned funds to a buffer account + /// (e.g., DAP buffer on Asset Hub). If not specified (or set to `()`), burns + /// reduce total issuance directly. + #[pallet::no_default_bounds] + type BurnDestination: FundingSink; } /// The in-code storage version. @@ -852,8 +871,10 @@ pub mod pallet { /// If the origin's account ends up below the existential deposit as a result /// of the burn and `keep_alive` is false, the account will be reaped. /// - /// Unlike sending funds to a _burn_ address, which merely makes the funds inaccessible, - /// this `burn` operation will reduce total issuance by the amount _burned_. + /// The burned funds are handled by the runtime's configured `BurnDestination`: + /// - `DirectBurn`: burns directly, reducing total issuance. + /// - `AccumulateInSatellite`: transfers to DAP satellite account. + /// - `ReturnToDap`: transfers to DAP buffer. #[pallet::call_index(10)] #[pallet::weight(if *keep_alive {T::WeightInfo::burn_allow_death() } else {T::WeightInfo::burn_keep_alive()})] pub fn burn( @@ -862,14 +883,9 @@ pub mod pallet { keep_alive: bool, ) -> DispatchResult { let source = ensure_signed(origin)?; - let preservation = if keep_alive { Preserve } else { Expendable }; - >::burn_from( - &source, - value, - preservation, - Precision::Exact, - Polite, - )?; + let preservation = + if keep_alive { Preservation::Preserve } else { Preservation::Expendable }; + T::BurnDestination::return_funds(&source, value, preservation)?; Ok(()) } } @@ -1433,3 +1449,23 @@ pub mod pallet { } } } + +impl, I: 'static> frame_support::traits::tokens::FundingSink + for pallet::DirectBurn +{ + fn return_funds( + from: &T::AccountId, + amount: T::Balance, + preservation: frame_support::traits::tokens::Preservation, + ) -> DispatchResult { + use frame_support::traits::tokens::{Fortitude, Precision}; + as fungible::Mutate>::burn_from( + from, + amount, + preservation, + Precision::Exact, + Fortitude::Polite, + )?; + Ok(()) + } +} diff --git a/substrate/frame/balances/src/tests/mod.rs b/substrate/frame/balances/src/tests/mod.rs index 155f78884d122..712b5fb8d27bf 100644 --- a/substrate/frame/balances/src/tests/mod.rs +++ b/substrate/frame/balances/src/tests/mod.rs @@ -130,6 +130,7 @@ impl Config for Test { type RuntimeFreezeReason = TestId; type FreezeIdentifier = TestId; type MaxFreezes = VariantCountOf; + type BurnDestination = pallet_balances::DirectBurn; } #[derive(Clone)] diff --git a/substrate/frame/contracts/mock-network/src/parachain.rs b/substrate/frame/contracts/mock-network/src/parachain.rs index ad43ac42a7508..3994ff0d23a92 100644 --- a/substrate/frame/contracts/mock-network/src/parachain.rs +++ b/substrate/frame/contracts/mock-network/src/parachain.rs @@ -97,6 +97,7 @@ impl pallet_balances::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; type WeightInfo = (); type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/contracts/mock-network/src/relay_chain.rs b/substrate/frame/contracts/mock-network/src/relay_chain.rs index 0e60e3df6e19d..ec3dc8def08c6 100644 --- a/substrate/frame/contracts/mock-network/src/relay_chain.rs +++ b/substrate/frame/contracts/mock-network/src/relay_chain.rs @@ -90,6 +90,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl shared::Config for Runtime { 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..f4b1cdf1a2ad3 --- /dev/null +++ b/substrate/frame/dap-satellite/src/lib.rs @@ -0,0 +1,691 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # DAP Satellite Pallet +//! +//! This pallet is meant to be used on **system chains other than AssetHub** (e.g., Coretime, +//! People, BridgeHub) or on the **Relay Chain**. It should NOT be deployed on AssetHub, which +//! hosts the central DAP pallet (`pallet-dap`). +//! +//! ## Purpose +//! +//! The DAP Satellite collects funds that would otherwise be burned (e.g., transaction fees, +//! coretime revenue, slashing) into a local satellite account. These funds are accumulated +//! locally and will eventually be transferred via XCM to the central DAP buffer on AssetHub. +//! +//! ## Architecture +//! +//! ```text +//! ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +//! │ Relay Chain │ │ Coretime Chain │ │ People Chain │ +//! │ DAPSatellite │ │ DAPSatellite │ │ DAPSatellite │ +//! └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ +//! │ │ │ +//! │ XCM (periodic) │ │ +//! └───────────────────────┼───────────────────────┘ +//! │ +//! ▼ +//! ┌─────────────────┐ +//! │ AssetHub │ +//! │ pallet-dap │ +//! │ (central) │ +//! └─────────────────┘ +//! ``` +//! +//! ## Implementation +//! +//! This is a minimal implementation that only accumulates funds locally. The periodic XCM +//! transfer to AssetHub is NOT yet implemented. +//! +//! In this first iteration, the pallet provides the following components: +//! - `AccumulateInSatellite`: Implementation of `FundingSink` that transfers funds to the satellite +//! account instead of burning them. +//! - `SinkToSatellite`: Implementation of `OnUnbalanced` for the old `Currency` trait, useful for +//! fee handlers and other pallets that use imbalances. +//! +//! **TODO:** +//! - Periodic XCM transfer to AssetHub DAP buffer +//! - Configuration for XCM period and destination +//! - Weight accounting for XCM operations +//! +//! ## Usage +//! +//! On system chains (not AssetHub) or Relay Chain, configure pallets to use the satellite: +//! +//! ```ignore +//! // In runtime configuration for Coretime/People/BridgeHub/RelayChain +//! impl pallet_coretime::Config for Runtime { +//! type FundingSink = pallet_dap_satellite::AccumulateInSatellite; +//! } +//! +//! // For fee handlers using OnUnbalanced +//! type FeeDestination = pallet_dap_satellite::SinkToSatellite; +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Balanced, Credit, Inspect, Mutate}, + tokens::{Fortitude, FundingSink, Precision, Preservation}, + Currency, Imbalance, OnUnbalanced, + }, + PalletId, +}; + +pub use pallet::*; + +const LOG_TARGET: &str = "runtime::dap-satellite"; + +/// The DAP Satellite pallet ID, used to derive the satellite account. +pub const DAP_SATELLITE_PALLET_ID: PalletId = PalletId(*b"dap/satl"); + +/// Type alias for balance. +pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::sp_runtime::traits::AccountIdConversion; + + /// The in-code storage version. + const STORAGE_VERSION: frame_support::traits::StorageVersion = + frame_support::traits::StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency type. + type Currency: Inspect + + Mutate + + Balanced; + } + + impl Pallet { + /// Get the satellite account derived from the pallet ID. + /// + /// This account accumulates funds locally before they are sent to AssetHub. + pub fn satellite_account() -> T::AccountId { + DAP_SATELLITE_PALLET_ID.into_account_truncating() + } + + /// 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_coretime::Config for Runtime { +/// type FundingSink = AccumulateInSatellite; +/// } +/// ``` +pub struct AccumulateInSatellite(core::marker::PhantomData); + +impl FundingSink> for AccumulateInSatellite { + fn return_funds( + source: &T::AccountId, + amount: BalanceOf, + preservation: Preservation, + ) -> Result<(), DispatchError> { + let satellite = Pallet::::satellite_account(); + + let credit = T::Currency::withdraw( + source, + amount, + Precision::Exact, + preservation, + Fortitude::Polite, + )?; + + // The satellite account is created at genesis or on_runtime_upgrade, so resolve should + // always succeed. If it somehow fails, log the error and let the credit drop (burn). + let _ = T::Currency::resolve(&satellite, credit).map_err(|c| { + log::error!( + target: LOG_TARGET, + "💸 Failed to resolve {:?} to satellite account - funds will be burned instead", + c.peek() + ); + drop(c); + }); + + Pallet::::deposit_event(Event::FundsAccumulated { from: source.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Accumulated {amount:?} from {source:?} in satellite account" + ); + + Ok(()) + } +} + +/// Type alias for credit (negative imbalance - funds that were removed). +/// This is for the `fungible::Balanced` trait. +pub type CreditOf = Credit<::AccountId, ::Currency>; + +/// Implementation of `OnUnbalanced` for the `fungible::Balanced` trait. +/// +/// Use this on system chains (not AssetHub) or Relay Chain to collect funds from +/// imbalances (e.g., slashing) that would otherwise be burned. +/// +/// 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::{ + assert_noop, assert_ok, derive_impl, parameter_types, + sp_runtime::traits::AccountIdConversion, + traits::{ + fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, + OnUnbalanced, WithdrawReasons, + }, + }; + use sp_runtime::BuildStorage; + use std::cell::Cell; + + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + DapSatellite: crate, + } + ); + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; + } + + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] + impl pallet_balances::Config for Test { + type AccountStore = System; + } + + impl Config for Test { + type Currency = Balances; + } + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 200), (3, 300)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + 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 = DAP_SATELLITE_PALLET_ID.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)); + }); + } + + // ===== accumulate to satellite / returns_funds tests ===== + + #[test] + fn accumulate_in_satellite_transfers_to_satellite_account() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let satellite = DapSatellite::satellite_account(); + + // Given: accounts have balances, satellite is empty + assert_eq!(Balances::free_balance(satellite), 0); + + // When: accumulate from multiple accounts + assert_ok!(AccumulateInSatellite::::return_funds(&1, 20, Preservation::Preserve)); + assert_ok!(AccumulateInSatellite::::return_funds(&2, 50, Preservation::Preserve)); + assert_ok!(AccumulateInSatellite::::return_funds( + &3, + 100, + Preservation::Preserve + )); + + // Then: satellite has accumulated all funds + assert_eq!(Balances::free_balance(satellite), 170); + // ... accounts have their balance correctly update + assert_eq!(Balances::free_balance(1), 80); + assert_eq!(Balances::free_balance(2), 150); + assert_eq!(Balances::free_balance(3), 200); + // ... and events are emitted + System::assert_has_event( + Event::::FundsAccumulated { from: 1, amount: 20 }.into(), + ); + System::assert_has_event( + Event::::FundsAccumulated { from: 2, amount: 50 }.into(), + ); + System::assert_has_event( + Event::::FundsAccumulated { from: 3, amount: 100 }.into(), + ); + }); + } + + #[test] + fn accumulate_fails_with_insufficient_balance() { + new_test_ext().execute_with(|| { + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: try to accumulate 150 (more than balance) + // Then: fails + assert_noop!( + AccumulateInSatellite::::return_funds(&1, 150, Preservation::Preserve), + sp_runtime::TokenError::FundsUnavailable + ); + }); + } + + // ===== SlashToSatellite tests ===== + + #[test] + fn slash_to_satellite_deposits_to_satellite() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: multiple slashes occur + let credit1 = >::issue(30); + SlashToSatellite::::on_unbalanced(credit1); + + let credit2 = >::issue(20); + SlashToSatellite::::on_unbalanced(credit2); + + let credit3 = >::issue(50); + SlashToSatellite::::on_unbalanced(credit3); + + // Then: satellite has accumulated all slashes (30 + 20 + 50 = 100) + assert_eq!(Balances::free_balance(satellite), 100); + }); + } + + // ===== SinkToSatellite tests ===== + + #[test] + fn sink_to_satellite_deposits_to_satellite() { + new_test_ext().execute_with(|| { + let satellite = DapSatellite::satellite_account(); + + // Given: accounts have balances, satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: multiple sinks occur from different accounts + let imbalance1 = >::withdraw( + &1, + 30, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + SinkToSatellite::::on_unbalanced(imbalance1); + + let imbalance2 = >::withdraw( + &2, + 50, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + SinkToSatellite::::on_unbalanced(imbalance2); + + // Then: satellite has accumulated all sinks (30 + 50 = 80) + assert_eq!(Balances::free_balance(satellite), 80); + assert_eq!(Balances::free_balance(1), 70); + assert_eq!(Balances::free_balance(2), 150); + }); + } + + // ===== DealWithFeesSplit tests ===== + + // Thread-local storage for tracking what OtherHandler receives + thread_local! { + static OTHER_HANDLER_RECEIVED: Cell = const { Cell::new(0) }; + } + + /// Mock handler that tracks how much it receives + struct MockOtherHandler; + impl OnUnbalanced> for MockOtherHandler { + fn on_unbalanced(amount: CreditOf) { + OTHER_HANDLER_RECEIVED.with(|r| r.set(r.get() + amount.peek())); + // Drop the credit (it would normally be handled by the real handler) + drop(amount); + } + } + + fn reset_other_handler() { + OTHER_HANDLER_RECEIVED.with(|r| r.set(0)); + } + + fn get_other_handler_received() -> u64 { + OTHER_HANDLER_RECEIVED.with(|r| r.get()) + } + + parameter_types! { + pub const ZeroPercent: u32 = 0; + pub const FiftyPercent: u32 = 50; + pub const HundredPercent: u32 = 100; + } + + #[test] + fn deal_with_fees_split_zero_percent_to_dap() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fees of 100 with 0% to DAP (all to other handler) + tips of 50 + // Tips should ALWAYS go to other handler, regardless of DAP percent + let fees = >::issue(100); + let tips = >::issue(50); + as OnUnbalanced<_>>::on_unbalanceds( + [fees, tips].into_iter(), + ); + + // Then: satellite gets 0, other handler gets 150 (100% fees + tips) + assert_eq!(Balances::free_balance(satellite), 0); + assert_eq!(get_other_handler_received(), 150); + }); + } + + #[test] + fn deal_with_fees_split_hundred_percent_to_dap() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fees of 100 with 100% to DAP + tips of 50 + // Tips should ALWAYS go to other handler, regardless of DAP percent + let fees = >::issue(100); + let tips = >::issue(50); + as OnUnbalanced<_>>::on_unbalanceds( + [fees, tips].into_iter(), + ); + + // Then: satellite gets 100 (fees), other handler gets 50 (tips) + assert_eq!(Balances::free_balance(satellite), 100); + assert_eq!(get_other_handler_received(), 50); + }); + } + + #[test] + fn deal_with_fees_split_fifty_percent() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: fees of 100 with 50% to DAP + tips of 40 + // Fees split 50/50, tips 100% to other handler + let fees = >::issue(100); + let tips = >::issue(40); + as OnUnbalanced<_>>::on_unbalanceds( + [fees, tips].into_iter(), + ); + + // Then: satellite gets 50 (half of fees), other handler gets 90 (half of fees + tips) + assert_eq!(Balances::free_balance(satellite), 50); + assert_eq!(get_other_handler_received(), 90); + }); + } + + #[test] + fn deal_with_fees_split_handles_empty_iterator() { + new_test_ext().execute_with(|| { + reset_other_handler(); + let satellite = DapSatellite::satellite_account(); + + // Given: satellite has 0 + assert_eq!(Balances::free_balance(satellite), 0); + + // When: no fees, no tips (empty iterator) + as OnUnbalanced<_>>::on_unbalanceds( + core::iter::empty(), + ); + + // Then: nothing happens + assert_eq!(Balances::free_balance(satellite), 0); + assert_eq!(get_other_handler_received(), 0); + }); + } +} diff --git a/substrate/frame/dap/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..8ff9ea244fd54 --- /dev/null +++ b/substrate/frame/dap/src/lib.rs @@ -0,0 +1,512 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Dynamic Allocation Pool (DAP) Pallet +//! +//! Minimal initial implementation: only `FundingSink` (return_funds) is functional. +//! This allows replacing burns in other pallets with returns to DAP buffer. +//! +//! Future phases will add: +//! - `FundingSource` (request_funds) for pulling funds +//! - Issuance curve and minting logic +//! - Distribution rules and scheduling + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Balanced, Credit, Inspect, Mutate}, + tokens::{Fortitude, FundingSink, FundingSource, Precision, Preservation}, + Currency, Imbalance, OnUnbalanced, + }, + PalletId, +}; + +pub use pallet::*; + +const LOG_TARGET: &str = "runtime::dap"; + +/// The DAP pallet ID, used to derive the buffer account. +pub const DAP_PALLET_ID: PalletId = PalletId(*b"dap/buff"); + +/// Type alias for balance. +pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::sp_runtime::traits::AccountIdConversion; + + /// The in-code storage version. + const STORAGE_VERSION: frame_support::traits::StorageVersion = + frame_support::traits::StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency type. + type Currency: Inspect + + Mutate + + Balanced; + } + + impl Pallet { + /// Get the DAP buffer account derived from the pallet ID. + pub fn buffer_account() -> T::AccountId { + DAP_PALLET_ID.into_account_truncating() + } + + /// Ensure the buffer account exists by incrementing its provider count. + /// + /// This is called at genesis and on runtime upgrade. + /// It's idempotent - calling it multiple times is safe. + pub fn ensure_buffer_account_exists() { + let buffer = Self::buffer_account(); + if !frame_system::Pallet::::account_exists(&buffer) { + frame_system::Pallet::::inc_providers(&buffer); + log::info!( + target: LOG_TARGET, + "Created DAP buffer account: {buffer:?}" + ); + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> Weight { + // Create the buffer account if it doesn't exist (for chains upgrading to DAP). + Self::ensure_buffer_account_exists(); + // Weight: 1 read (account_exists) + potentially 1 write (inc_providers) + T::DbWeight::get().reads_writes(1, 1) + } + } + + /// Genesis config for the DAP pallet. + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + #[serde(skip)] + _phantom: core::marker::PhantomData, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + // Create the buffer account at genesis so it can receive funds of any amount. + Pallet::::ensure_buffer_account_exists(); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Funds returned to DAP buffer. + FundsReturned { from: T::AccountId, amount: BalanceOf }, + } + + #[pallet::error] + pub enum Error { + /// FundingSource not yet implemented. + NotImplemented, + } +} + +/// Implementation of FundingSource - NOT YET IMPLEMENTED. +/// Returns `Error::NotImplemented` if called. +pub struct PullFromDap(core::marker::PhantomData); + +impl FundingSource> for PullFromDap { + fn request_funds( + _beneficiary: &T::AccountId, + _amount: BalanceOf, + ) -> Result, DispatchError> { + Err(Error::::NotImplemented.into()) + } +} + +/// Implementation of FundingSink that returns funds to DAP buffer. +/// When using this, returned funds are transferred to the buffer account instead of being burned. +pub struct ReturnToDap(core::marker::PhantomData); + +impl FundingSink> for ReturnToDap { + fn return_funds( + source: &T::AccountId, + amount: BalanceOf, + preservation: Preservation, + ) -> Result<(), DispatchError> { + let buffer = Pallet::::buffer_account(); + + let credit = T::Currency::withdraw( + source, + amount, + Precision::Exact, + preservation, + Fortitude::Polite, + )?; + + // The buffer account is created at genesis or on_runtime_upgrade, so resolve should + // always succeed. If it somehow fails, log the error and let the credit drop (burn). + let _ = T::Currency::resolve(&buffer, credit).map_err(|c| { + log::error!( + target: LOG_TARGET, + "💸 Failed to resolve {:?} to DAP buffer - funds will be burned instead", + c.peek() + ); + drop(c); + }); + + Pallet::::deposit_event(Event::FundsReturned { from: source.clone(), amount }); + + log::debug!( + target: LOG_TARGET, + "Returned {amount:?} from {source:?} to DAP buffer" + ); + + Ok(()) + } +} + +/// Type alias for credit (negative imbalance - funds that were slashed/removed). +/// This is for the `fungible::Balanced` trait as used by staking-async. +pub type CreditOf = Credit<::AccountId, ::Currency>; + +/// Implementation of OnUnbalanced for the fungible::Balanced trait. +/// Use this as `type Slash = SlashToDap` in staking-async config. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via fee splitting). +pub struct SlashToDap(core::marker::PhantomData); + +impl OnUnbalanced> for SlashToDap { + fn on_nonzero_unbalanced(amount: CreditOf) { + let buffer = Pallet::::buffer_account(); + let numeric_amount = amount.peek(); + + // The buffer account is created at genesis or on_runtime_upgrade, so resolve should + // always succeed. If it somehow fails, log the error. + if let Err(remaining) = T::Currency::resolve(&buffer, amount) { + let remaining_amount = remaining.peek(); + if !remaining_amount.is_zero() { + log::error!( + target: LOG_TARGET, + "💸 Failed to deposit slash to DAP buffer - {remaining_amount:?} will be burned!" + ); + } + } + + log::debug!( + target: LOG_TARGET, + "Deposited slash of {numeric_amount:?} to DAP buffer" + ); + } +} + +/// Implementation of OnUnbalanced for the old Currency trait (still used by treasury). +/// Use this as `type BurnDestination = BurnToDap` e.g. in treasury config. +/// +/// Note: This handler does NOT emit events because it can be called very frequently +/// (e.g., for every fee-paying transaction via fee splitting). +pub struct BurnToDap(core::marker::PhantomData<(T, C)>); + +impl OnUnbalanced for BurnToDap +where + T: Config, + C: Currency, +{ + fn on_nonzero_unbalanced(amount: C::NegativeImbalance) { + let buffer = Pallet::::buffer_account(); + let numeric_amount = amount.peek(); + + // Resolve the imbalance by depositing into the buffer account + C::resolve_creating(&buffer, amount); + + log::debug!( + target: LOG_TARGET, + "Deposited burn of {numeric_amount:?} to DAP buffer" + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + assert_noop, assert_ok, derive_impl, + sp_runtime::traits::AccountIdConversion, + traits::{ + fungible::Balanced, tokens::FundingSink, Currency as CurrencyT, ExistenceRequirement, + OnUnbalanced, WithdrawReasons, + }, + }; + use sp_runtime::BuildStorage; + + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + Dap: crate, + } + ); + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; + } + + #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] + impl pallet_balances::Config for Test { + type AccountStore = System; + } + + impl Config for Test { + type Currency = Balances; + } + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 200), (3, 300)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + crate::pallet::GenesisConfig::::default() + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } + + #[test] + fn buffer_account_is_derived_from_pallet_id() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + let expected: u64 = DAP_PALLET_ID.into_account_truncating(); + assert_eq!(buffer, expected); + }); + } + + #[test] + fn genesis_creates_buffer_account() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + // Buffer account should exist after genesis (created via inc_providers) + assert!(System::account_exists(&buffer)); + }); + } + + // ===== return_funds tests ===== + + #[test] + fn return_funds_accumulates_from_multiple_sources() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let buffer = Dap::buffer_account(); + + // Given: accounts have balances, buffer has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(2), 200); + assert_eq!(Balances::free_balance(3), 300); + assert_eq!(Balances::free_balance(buffer), 0); + + // When: return funds from multiple accounts + assert_ok!(ReturnToDap::::return_funds(&1, 20, Preservation::Preserve)); + assert_ok!(ReturnToDap::::return_funds(&2, 50, Preservation::Preserve)); + assert_ok!(ReturnToDap::::return_funds(&3, 100, Preservation::Preserve)); + + // Then: buffer has accumulated all returns (20 + 50 + 100 = 170) + assert_eq!(Balances::free_balance(buffer), 170); + assert_eq!(Balances::free_balance(1), 80); + assert_eq!(Balances::free_balance(2), 150); + assert_eq!(Balances::free_balance(3), 200); + + // ...and all three events are emitted + System::assert_has_event(Event::::FundsReturned { from: 1, amount: 20 }.into()); + System::assert_has_event(Event::::FundsReturned { from: 2, amount: 50 }.into()); + System::assert_has_event(Event::::FundsReturned { from: 3, amount: 100 }.into()); + }); + } + + #[test] + fn return_funds_fails_with_insufficient_balance() { + new_test_ext().execute_with(|| { + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: try to return 150 (more than balance) + // Then: fails + assert_noop!( + ReturnToDap::::return_funds(&1, 150, Preservation::Preserve), + sp_runtime::TokenError::FundsUnavailable + ); + }); + } + + #[test] + fn return_funds_with_zero_amount_succeeds() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: account 1 has 100, buffer has 0 + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); + + // When: return 0 from account 1 + assert_ok!(ReturnToDap::::return_funds(&1, 0, Preservation::Preserve)); + + // Then: balances unchanged (no-op) + assert_eq!(Balances::free_balance(1), 100); + assert_eq!(Balances::free_balance(buffer), 0); + }); + } + + #[test] + fn return_funds_with_expendable_allows_full_drain() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let buffer = Dap::buffer_account(); + + // Given: account 1 has 100 + assert_eq!(Balances::free_balance(1), 100); + + // When: return full balance with Expendable (allows going to 0) + assert_ok!(ReturnToDap::::return_funds(&1, 100, Preservation::Expendable)); + + // Then: account 1 is empty, buffer has 100 + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(buffer), 100); + }); + } + + #[test] + fn return_funds_with_preserve_respects_existential_deposit() { + new_test_ext().execute_with(|| { + // Given: account 1 has 100, ED is 1 (from TestDefaultConfig) + assert_eq!(Balances::free_balance(1), 100); + + // When: try to return 100 with Preserve (would go below ED) + // Then: fails because it would kill the account + assert_noop!( + ReturnToDap::::return_funds(&1, 100, Preservation::Preserve), + sp_runtime::TokenError::FundsUnavailable + ); + + // But returning 99 works (leaves 1 for ED) + assert_ok!(ReturnToDap::::return_funds(&1, 99, Preservation::Preserve)); + assert_eq!(Balances::free_balance(1), 1); + }); + } + + // ===== SlashToDap tests ===== + + #[test] + fn slash_to_dap_accumulates_multiple_slashes_to_buffer() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: multiple slashes occur via OnUnbalanced (simulating a staking slash) + let credit1 = >::issue(30); + SlashToDap::::on_unbalanced(credit1); + + let credit2 = >::issue(20); + SlashToDap::::on_unbalanced(credit2); + + let credit3 = >::issue(50); + SlashToDap::::on_unbalanced(credit3); + + // Then: buffer has accumulated all slashes (30 + 20 + 50 = 100) + assert_eq!(Balances::free_balance(buffer), 100); + }); + } + + #[test] + fn slash_to_dap_handles_zero_amount() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: slash with zero amount + let credit = >::issue(0); + SlashToDap::::on_unbalanced(credit); + + // Then: buffer still has 0 (no-op) + assert_eq!(Balances::free_balance(buffer), 0); + }); + } + + // ===== BurnToDap tests ===== + + #[test] + fn burn_to_dap_accumulates_multiple_burns_to_buffer() { + new_test_ext().execute_with(|| { + let buffer = Dap::buffer_account(); + + // Given: accounts have balances, buffer has 0 + assert_eq!(Balances::free_balance(buffer), 0); + + // When: create multiple negative imbalances (simulating treasury burns) and send to DAP + let imbalance1 = >::withdraw( + &1, + 30, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + BurnToDap::::on_unbalanced(imbalance1); + + let imbalance2 = >::withdraw( + &2, + 50, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) + .unwrap(); + BurnToDap::::on_unbalanced(imbalance2); + + // Then: buffer has accumulated all burns (30 + 50 = 80) + assert_eq!(Balances::free_balance(buffer), 80); + assert_eq!(Balances::free_balance(1), 70); + assert_eq!(Balances::free_balance(2), 150); + }); + } + + // ===== request_funds tests ===== + + #[test] + fn pull_from_dap_returns_not_implemented_error() { + new_test_ext().execute_with(|| { + // When: request_funds is called + // Then: returns NotImplemented error + assert_noop!(PullFromDap::::request_funds(&1, 10), Error::::NotImplemented); + }); + } +} diff --git a/substrate/frame/nis/src/mock.rs b/substrate/frame/nis/src/mock.rs index 0e71e43f56bd7..9abe7cb56411d 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/runtimes/parachain/Cargo.toml b/substrate/frame/staking-async/runtimes/parachain/Cargo.toml index 990079b22a658..c4ead65e064fe 100644 --- a/substrate/frame/staking-async/runtimes/parachain/Cargo.toml +++ b/substrate/frame/staking-async/runtimes/parachain/Cargo.toml @@ -118,6 +118,7 @@ cumulus-primitives-aura = { workspace = true } cumulus-primitives-core = { workspace = true } cumulus-primitives-utility = { workspace = true } pallet-collator-selection = { workspace = true } +pallet-dap = { workspace = true } pallet-message-queue = { workspace = true } parachain-info = { workspace = true } parachains-common = { workspace = true } @@ -169,6 +170,7 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", "pallet-conviction-voting/runtime-benchmarks", + "pallet-dap/runtime-benchmarks", "pallet-delegated-staking/runtime-benchmarks", "pallet-election-provider-multi-block/runtime-benchmarks", "pallet-fast-unstake/runtime-benchmarks", @@ -234,6 +236,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-conviction-voting/try-runtime", + "pallet-dap/try-runtime", "pallet-delegated-staking/try-runtime", "pallet-election-provider-multi-block/try-runtime", "pallet-fast-unstake/try-runtime", @@ -305,6 +308,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-conviction-voting/std", + "pallet-dap/std", "pallet-delegated-staking/std", "pallet-election-provider-multi-block/std", "pallet-fast-unstake/std", diff --git a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs index 6ad74378e50b6..d0239fe680e75 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/governance/mod.rs @@ -138,7 +138,7 @@ impl pallet_treasury::Config for Runtime { type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; type Burn = Burn; - type BurnDestination = (); + type BurnDestination = pallet_dap::BurnToDap; type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = (); diff --git a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs index ad42ee31662e0..1d00b8171c807 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs @@ -229,6 +229,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = frame_support::traits::VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { @@ -1196,6 +1197,9 @@ construct_runtime!( Treasury: pallet_treasury = 96, AssetRate: pallet_asset_rate = 97, + // Dynamic Allocation Pool / Issuance buffer + Dap: pallet_dap = 98, + // Balances. Vesting: pallet_vesting = 100, diff --git a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs index b344a8e047c4d..3ab1853c827ac 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs @@ -436,7 +436,7 @@ impl pallet_staking_async::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type CurrencyToVote = sp_staking::currency_to_vote::SaturatingCurrencyToVote; type RewardRemainder = (); - type Slash = (); + type Slash = pallet_dap::SlashToDap; type Reward = (); type SessionsPerEra = SessionsPerEra; type BondingDuration = BondingDuration; @@ -470,6 +470,10 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; } +impl pallet_dap::Config for Runtime { + type Currency = Balances; +} + parameter_types! { pub StakingXcmDestination: Location = Location::parent(); } diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index 53edb28ceccd9..246b63a83119e 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -451,6 +451,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/substrate/frame/staking-async/src/mock.rs b/substrate/frame/staking-async/src/mock.rs index c14e5c026accb..6bc42e03ba123 100644 --- a/substrate/frame/staking-async/src/mock.rs +++ b/substrate/frame/staking-async/src/mock.rs @@ -52,6 +52,7 @@ frame_support::construct_runtime!( Balances: pallet_balances, Staking: pallet_staking_async, VoterBagsList: pallet_bags_list::, + Dap: pallet_dap, } ); @@ -110,6 +111,10 @@ impl pallet_balances::Config for Test { type AccountStore = System; } +impl pallet_dap::Config for Test { + type Currency = Balances; +} + parameter_types! { pub static RewardRemainderUnbalanced: u128 = 0; } @@ -454,7 +459,7 @@ impl crate::pallet::pallet::Config for Test { type RcClientInterface = session_mock::Session; type CurrencyBalance = Balance; type CurrencyToVote = SaturatingCurrencyToVote; - type Slash = (); + type Slash = pallet_dap::SlashToDap; type WeightInfo = (); } diff --git a/substrate/frame/support/src/traits/tokens.rs b/substrate/frame/support/src/traits/tokens.rs index be982cd31e33a..d335e5fc0107b 100644 --- a/substrate/frame/support/src/traits/tokens.rs +++ b/substrate/frame/support/src/traits/tokens.rs @@ -19,6 +19,7 @@ pub mod asset_ops; pub mod currency; +pub mod funding; pub mod fungible; pub mod fungibles; pub mod imbalance; @@ -27,6 +28,7 @@ pub mod nonfungible; pub mod nonfungible_v2; pub mod nonfungibles; pub mod nonfungibles_v2; +pub use funding::{DirectBurn, DirectMint, FundingSink, FundingSource}; pub use imbalance::Imbalance; pub mod pay; pub mod transfer; diff --git a/substrate/frame/support/src/traits/tokens/funding.rs b/substrate/frame/support/src/traits/tokens/funding.rs new file mode 100644 index 0000000000000..423e44b7fa733 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/funding.rs @@ -0,0 +1,125 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits for funding sources and sinks in token issuance systems. +//! +//! This module provides abstractions for pulling funds (rewards, payments) and returning funds +//! (burns, slashing) in a way that can be configured differently per runtime. +//! +//! Two main patterns: +//! - **Direct mint/burn**: Traditional approach where funds are created/destroyed on demand +//! - **Buffer-based**: Funds are pre-minted into a buffer and distributed from there + +use crate::traits::tokens::{fungible, Fortitude, Precision, Preservation}; +use core::marker::PhantomData; +use sp_runtime::{DispatchError, DispatchResult}; + +/// Trait for requesting funds from an issuance system. +/// +/// Implementations can either mint directly or pull from a pre-minted buffer. +pub trait FundingSource { + /// Request funds to be transferred to the beneficiary. + /// + /// Returns the actual amount transferred, which may be less than requested + /// if the source has insufficient funds. + fn request_funds(beneficiary: &AccountId, amount: Balance) -> Result; +} + +/// Trait for returning funds to an issuance system. +/// +/// Implementations can either burn directly or return to a buffer for reuse. +pub trait FundingSink { + /// Return funds from the given account back to the issuance system. + /// + /// This could mean burning the funds or transferring them to a buffer account. + /// + /// # Parameters + /// - `from`: The account to take funds from + /// - `amount`: The amount to return + /// - `preservation`: Whether to preserve the source account (Preserve = keep alive, Expendable + /// = allow death) + fn return_funds( + from: &AccountId, + amount: Balance, + preservation: Preservation, + ) -> DispatchResult; +} + +/// Direct minting implementation of `FundingSource`. +/// +/// This implementation mints tokens directly when funds are requested. +/// Used for traditional mint-on-demand systems (e.g., Kusama). +/// +/// # Type Parameters +/// +/// * `Currency` - The currency type that implements `Mutate` +/// * `AccountId` - The account identifier type +pub struct DirectMint(PhantomData<(Currency, AccountId)>); + +impl FundingSource + for DirectMint +where + Currency: fungible::Mutate, + AccountId: Eq, +{ + fn request_funds( + beneficiary: &AccountId, + amount: Currency::Balance, + ) -> Result { + Currency::mint_into(beneficiary, amount)?; + Ok(amount) + } +} + +/// Direct burning implementation of `FundingSink`. +/// +/// This implementation burns tokens directly when funds are returned. +/// Used for traditional burn-on-return systems (e.g., Kusama). +/// +/// # Type Parameters +/// +/// * `Currency` - The currency type that implements `Mutate` +/// * `AccountId` - The account identifier type +pub struct DirectBurn(PhantomData<(Currency, AccountId)>); + +impl FundingSink + for DirectBurn +where + Currency: fungible::Mutate, + AccountId: Eq, +{ + fn return_funds( + from: &AccountId, + amount: Currency::Balance, + preservation: Preservation, + ) -> DispatchResult { + Currency::burn_from(from, amount, preservation, Precision::Exact, Fortitude::Polite)?; + Ok(()) + } +} + +/// No-op implementation of `FundingSink` for unit type. +/// Used for testing or when no sink behavior is needed. +impl FundingSink for () { + fn return_funds( + _from: &AccountId, + _amount: Balance, + _preservation: Preservation, + ) -> DispatchResult { + Ok(()) + } +} diff --git a/substrate/test-utils/runtime/src/lib.rs b/substrate/test-utils/runtime/src/lib.rs index a04b05021bb46..5b26839c166f2 100644 --- a/substrate/test-utils/runtime/src/lib.rs +++ b/substrate/test-utils/runtime/src/lib.rs @@ -427,6 +427,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } impl pallet_utility::Config for Runtime { diff --git a/templates/parachain/runtime/src/configs/mod.rs b/templates/parachain/runtime/src/configs/mod.rs index 2ac558ea2a310..6e75638f1dd2b 100644 --- a/templates/parachain/runtime/src/configs/mod.rs +++ b/templates/parachain/runtime/src/configs/mod.rs @@ -175,6 +175,7 @@ impl pallet_balances::Config for Runtime { type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = VariantCountOf; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { diff --git a/templates/solochain/runtime/src/configs/mod.rs b/templates/solochain/runtime/src/configs/mod.rs index b8810a068036c..8a879ac49b2a9 100644 --- a/templates/solochain/runtime/src/configs/mod.rs +++ b/templates/solochain/runtime/src/configs/mod.rs @@ -142,6 +142,7 @@ impl pallet_balances::Config for Runtime { type RuntimeHoldReason = RuntimeHoldReason; type RuntimeFreezeReason = RuntimeFreezeReason; type DoneSlashHandler = (); + type BurnDestination = pallet_balances::DirectBurn; } parameter_types! { 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;