diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c74d688c4..5a774ad187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Kusama Treasury: remove funding to the Kappa Sigma Mu Society and disable burn ([polkadot-fellows/runtimes#507](https://github.com/polkadot-fellows/runtimes/pull/507)) +- Kusama Treasury: allow burn parameters to be set via OpenGov ([polkadot-fellows/runtimes#511](https://github.com/polkadot-fellows/runtimes/pull/511)) - Remove Snowbridge create agent and channel extrinsics. ([polkadot-fellows/runtimes#506](https://github.com/polkadot-fellows/runtimes/pull/506)) #### From [#490](https://github.com/polkadot-fellows/runtimes/pull/490) diff --git a/relay/kusama/src/lib.rs b/relay/kusama/src/lib.rs index 4f59efa8e6..042162caff 100644 --- a/relay/kusama/src/lib.rs +++ b/relay/kusama/src/lib.rs @@ -25,7 +25,7 @@ extern crate alloc; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ dynamic_params::{dynamic_pallet_params, dynamic_params}, - traits::EnsureOriginWithArg, + traits::{EnsureOrigin, EnsureOriginWithArg}, weights::constants::{WEIGHT_PROOF_SIZE_PER_KB, WEIGHT_REF_TIME_PER_MICROS}, }; use kusama_runtime_constants::system_parachain::coretime::TIMESLICE_PERIOD; @@ -641,6 +641,15 @@ impl pallet_bags_list::Config for Runtime { type Score = sp_npos_elections::VoteWeight; } +#[derive(Default, MaxEncodedLen, Encode, Decode, TypeInfo, Clone, Eq, PartialEq, Debug)] +pub struct BurnDestinationAccount(pub Option); + +impl BurnDestinationAccount { + pub fn is_set(&self) -> bool { + self.0.is_some() + } +} + /// Dynamic params that can be adjusted at runtime. #[dynamic_params(RuntimeParameters, pallet_parameters::Parameters::)] pub mod dynamic_params { @@ -678,6 +687,17 @@ pub mod dynamic_params { #[codec(index = 4)] pub static UseAuctionSlots: bool = true; } + + /// Parameters used by `pallet-treasury` to handle the burn process. + #[dynamic_pallet_params] + #[codec(index = 1)] + pub mod treasury { + #[codec(index = 0)] + pub static BurnPortion: Permill = Permill::from_percent(0); + + #[codec(index = 1)] + pub static BurnDestination: BurnDestinationAccount = Default::default(); + } } #[cfg(feature = "runtime-benchmarks")] @@ -703,6 +723,8 @@ impl EnsureOriginWithArg for DynamicParamet match key { Inflation(_) => frame_system::ensure_root(origin.clone()), + Treasury(_) => + EitherOf::, GeneralAdmin>::ensure_origin(origin.clone()), } .map_err(|_| origin) } @@ -828,7 +850,6 @@ parameter_types! { pub const ProposalBondMinimum: Balance = 2000 * CENTS; pub const ProposalBondMaximum: Balance = GRAND; pub const SpendPeriod: BlockNumber = 6 * DAYS; - pub const Burn: Permill = Permill::zero(); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; // The asset's interior location for the paying account. This is the Treasury @@ -845,14 +866,46 @@ parameter_types! { pub const MaxPeerInHeartbeats: u32 = 10_000; } +use frame_support::traits::{Currency, OnUnbalanced}; + +pub type BalancesNegativeImbalance = >::NegativeImbalance; +pub struct TreasuryBurnHandler; + +impl OnUnbalanced for TreasuryBurnHandler { + fn on_nonzero_unbalanced(amount: BalancesNegativeImbalance) { + let destination = dynamic_params::treasury::BurnDestination::get(); + + if let BurnDestinationAccount(Some(account)) = destination { + // Must resolve into existing but better to be safe. + Balances::resolve_creating(&account, amount); + } else { + // If no account to destinate the funds to, just drop the + // imbalance. + <() as OnUnbalanced<_>>::on_nonzero_unbalanced(amount) + } + } +} + +impl Get for TreasuryBurnHandler { + fn get() -> Permill { + let destination = dynamic_params::treasury::BurnDestination::get(); + + if destination.is_set() { + dynamic_params::treasury::BurnPortion::get() + } else { + Permill::zero() + } + } +} + impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; type RejectOrigin = EitherOfDiverse, Treasurer>; type RuntimeEvent = RuntimeEvent; type SpendPeriod = SpendPeriod; - type Burn = Burn; - type BurnDestination = (); + type Burn = TreasuryBurnHandler; + type BurnDestination = TreasuryBurnHandler; type MaxApprovals = MaxApprovals; type WeightInfo = weights::pallet_treasury::WeightInfo; type SpendFunds = Bounties; diff --git a/relay/kusama/tests/treasury_burn_handler.rs b/relay/kusama/tests/treasury_burn_handler.rs new file mode 100644 index 0000000000..810e9005e7 --- /dev/null +++ b/relay/kusama/tests/treasury_burn_handler.rs @@ -0,0 +1,166 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! TreasuryBurnHandler helper structure tests. +//! +//! Note: These tests emulate the effects of burning some amount on `pallet_treasury` via +//! [`OnUnbalanced`], not the behaviour itself. + +use frame_support::{ + parameter_types, + traits::{ + tokens::fungible::{Inspect, Mutate}, + Currency, ExistenceRequirement, Get, Imbalance, OnUnbalanced, WithdrawReasons, + }, +}; +use kusama_runtime_constants::currency::UNITS; +use polkadot_primitives::{AccountId, Balance}; +use sp_arithmetic::Permill; +use staging_kusama_runtime::{ + dynamic_params::treasury::{self, BurnDestination, BurnPortion}, + Balances, BurnDestinationAccount, Parameters, RuntimeOrigin, RuntimeParameters, Treasury, + TreasuryBurnHandler, +}; + +parameter_types! { + TreasuryAccount: AccountId = Treasury::account_id(); +} + +const BURN_DESTINATION_ACCOUNT: AccountId = AccountId::new([1u8; 32]); + +const TREASURY_AMOUNT: Balance = 10 * UNITS; +const SURPLUS: Balance = UNITS; + +fn test(pre: impl FnOnce(), test: impl FnOnce(Balance)) { + sp_io::TestExternalities::default().execute_with(|| { + pre(); + + Balances::set_balance(&TreasuryAccount::get(), TREASURY_AMOUNT); + + let amount_to_handle = TreasuryBurnHandler::get() * SURPLUS; + let burn = >::withdraw( + &TreasuryAccount::get(), + SURPLUS, + WithdrawReasons::RESERVE, + ExistenceRequirement::KeepAlive, + ) + .expect("withdrawing of `burn` is within balance limits; qed"); + + // Withdrawn surplus to burn it. + assert_eq!(Balances::balance(&TreasuryAccount::get()), TREASURY_AMOUNT - SURPLUS); + + let (credit, burn) = burn.split(amount_to_handle); + + // Burn amount that's not to handle. + <() as OnUnbalanced<_>>::on_unbalanced(burn); + + assert_eq!(Balances::total_issuance(), TREASURY_AMOUNT - (SURPLUS - amount_to_handle)); + + // Let's handle the `credit` + TreasuryBurnHandler::on_unbalanced(credit); + + test(amount_to_handle); + + // Only the amount to handle was transferred to the burn destination account + // let burn_destination_account = BurnDestination::get(); + let burn_destination_account = BURN_DESTINATION_ACCOUNT; + let burn_destination_account_balance = + >::total_balance(&burn_destination_account); + + assert_eq!(burn_destination_account_balance, amount_to_handle); + }) +} + +#[test] +fn on_burn_parameters_not_set_does_not_handle_burn() { + test( + || {}, + |amount_to_handle| { + // Amount to burn should be zero by default + assert_eq!(amount_to_handle, 0); + }, + ) +} + +#[test] +fn on_burn_portion_not_set_does_not_handle_burn() { + test( + || { + Parameters::set_parameter( + RuntimeOrigin::root(), + RuntimeParameters::Treasury(treasury::Parameters::BurnDestination( + BurnDestination, + Some(BurnDestinationAccount(Some(BURN_DESTINATION_ACCOUNT))), + )), + ) + .expect("parameters are set accordingly; qed"); + }, + |amount_to_handle| { + // Amount to burn should be zero by default + assert_eq!(amount_to_handle, 0); + }, + ) +} + +#[test] +fn on_burn_destination_not_set_does_not_handle_burn() { + let one_percent = Permill::from_percent(1); + test( + || { + Parameters::set_parameter( + RuntimeOrigin::root(), + RuntimeParameters::Treasury(treasury::Parameters::BurnPortion( + BurnPortion, + Some(one_percent), + )), + ) + .expect("parameters are set accordingly; qed"); + }, + |amount_to_handle| { + // Amount to burn should be zero by default + assert_eq!(amount_to_handle, 0); + }, + ) +} + +#[test] +fn on_burn_parameters_set_works() { + let one_percent = Permill::from_percent(1); + test( + || { + Parameters::set_parameter( + RuntimeOrigin::root(), + RuntimeParameters::Treasury(treasury::Parameters::BurnDestination( + BurnDestination, + Some(BurnDestinationAccount(Some(BURN_DESTINATION_ACCOUNT))), + )), + ) + .expect("parameters are set accordingly; qed"); + Parameters::set_parameter( + RuntimeOrigin::root(), + RuntimeParameters::Treasury(treasury::Parameters::BurnPortion( + BurnPortion, + Some(one_percent), + )), + ) + .expect("parameters are set accordingly; qed"); + }, + |amount_to_handle| { + // Amount to burn should be zero by default + assert_eq!(amount_to_handle, one_percent * SURPLUS); + }, + ) +}