// This file is part of Substrate. // Copyright (C) 2017-2022 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. //! # Treasury Pallet //! //! The Treasury pallet provides a "pot" of funds that can be managed by stakeholders in the system //! and a structure for making spending proposals from this pot. //! //! - [`Config`] //! - [`Call`] //! //! ## Overview //! //! The Treasury Pallet itself provides the pot to store funds, and a means for stakeholders to //! propose, approve, and deny expenditures. The chain will need to provide a method (e.g. //! inflation, fees) for collecting funds. //! //! By way of example, the Council could vote to fund the Treasury with a portion of the block //! reward and use the funds to pay developers. //! //! //! ### Terminology //! //! - **Proposal:** A suggestion to allocate funds from the pot to a beneficiary. //! - **Beneficiary:** An account who will receive the funds from a proposal iff the proposal is //! approved. //! - **Deposit:** Funds that a proposer must lock when making a proposal. The deposit will be //! returned or slashed if the proposal is approved or rejected respectively. //! - **Pot:** Unspent funds accumulated by the treasury pallet. //! //! ## Interface //! //! ### Dispatchable Functions //! //! General spending/proposal protocol: //! - `propose_spend` - Make a spending proposal and stake the required deposit. //! - `reject_proposal` - Reject a proposal, slashing the deposit. //! - `approve_proposal` - Accept the proposal, returning the deposit. //! - `remove_approval` - Remove an approval, the deposit will no longer be returned. //! //! ## GenesisConfig //! //! The Treasury pallet depends on the [`GenesisConfig`]. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; #[cfg(test)] mod tests; pub mod weights; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{ traits::{AccountIdConversion, Saturating, StaticLookup, Zero}, Permill, RuntimeDebug, }; use sp_std::prelude::*; use frame_support::{ print, traits::{ Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, ReservableCurrency, WithdrawReasons, }, weights::Weight, PalletId, }; pub use pallet::*; pub use weights::WeightInfo; pub type BalanceOf = <>::Currency as Currency<::AccountId>>::Balance; pub type PositiveImbalanceOf = <>::Currency as Currency< ::AccountId, >>::PositiveImbalance; pub type NegativeImbalanceOf = <>::Currency as Currency< ::AccountId, >>::NegativeImbalance; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; /// A trait to allow the Treasury Pallet to spend it's funds for other purposes. /// There is an expectation that the implementer of this trait will correctly manage /// the mutable variables passed to it: /// * `budget_remaining`: How much available funds that can be spent by the treasury. As funds are /// spent, you must correctly deduct from this value. /// * `imbalance`: Any imbalances that you create should be subsumed in here to maximize efficiency /// of updating the total issuance. (i.e. `deposit_creating`) /// * `total_weight`: Track any weight that your `spend_fund` implementation uses by updating this /// value. /// * `missed_any`: If there were items that you want to spend on, but there were not enough funds, /// mark this value as `true`. This will prevent the treasury from burning the excess funds. #[impl_trait_for_tuples::impl_for_tuples(30)] pub trait SpendFunds, I: 'static = ()> { fn spend_funds( budget_remaining: &mut BalanceOf, imbalance: &mut PositiveImbalanceOf, total_weight: &mut Weight, missed_any: &mut bool, ); } /// An index of a proposal. Just a `u32`. pub type ProposalIndex = u32; /// A spending proposal. #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] pub struct Proposal { /// The account proposing it. proposer: AccountId, /// The (total) amount that should be paid if the proposal is accepted. value: Balance, /// The account to whom the payment should be made if the proposal is accepted. beneficiary: AccountId, /// The amount held on deposit (reserved) for making this proposal. bond: Balance, } #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(PhantomData<(T, I)>); #[pallet::config] pub trait Config: frame_system::Config { /// The staking balance. type Currency: Currency + ReservableCurrency; /// Origin from which approvals must come. type ApproveOrigin: EnsureOrigin; /// Origin from which rejections must come. type RejectOrigin: EnsureOrigin; /// The overarching event type. type Event: From> + IsType<::Event>; /// Handler for the unbalanced decrease when slashing for a rejected proposal or bounty. type OnSlash: OnUnbalanced>; /// Fraction of a proposal's value that should be bonded in order to place the proposal. /// An accepted proposal gets these back. A rejected proposal does not. #[pallet::constant] type ProposalBond: Get; /// Minimum amount of funds that should be placed in a deposit for making a proposal. #[pallet::constant] type ProposalBondMinimum: Get>; /// Maximum amount of funds that should be placed in a deposit for making a proposal. #[pallet::constant] type ProposalBondMaximum: Get>>; /// Period between successive spends. #[pallet::constant] type SpendPeriod: Get; /// Percentage of spare funds (if any) that are burnt per spend period. #[pallet::constant] type Burn: Get; /// The treasury's pallet id, used for deriving its sovereign account ID. #[pallet::constant] type PalletId: Get; /// Handler for the unbalanced decrease when treasury funds are burned. type BurnDestination: OnUnbalanced>; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; /// Runtime hooks to external pallet using treasury to compute spend funds. type SpendFunds: SpendFunds; /// The maximum number of approvals that can wait in the spending queue. /// /// NOTE: This parameter is also used within the Bounties Pallet extension if enabled. #[pallet::constant] type MaxApprovals: Get; /// The origin required for approving spends from the treasury outside of the proposal /// process. The `Success` value is the maximum amount that this origin is allowed to /// spend at a time. type SpendOrigin: EnsureOrigin>; } /// Number of proposals that have been made. #[pallet::storage] #[pallet::getter(fn proposal_count)] pub(crate) type ProposalCount = StorageValue<_, ProposalIndex, ValueQuery>; /// Proposals that have been made. #[pallet::storage] #[pallet::getter(fn proposals)] pub type Proposals, I: 'static = ()> = StorageMap< _, Twox64Concat, ProposalIndex, Proposal>, OptionQuery, >; /// Proposal indices that have been approved but not yet awarded. #[pallet::storage] #[pallet::getter(fn approvals)] pub type Approvals, I: 'static = ()> = StorageValue<_, BoundedVec, ValueQuery>; #[pallet::genesis_config] pub struct GenesisConfig; #[cfg(feature = "std")] impl Default for GenesisConfig { fn default() -> Self { Self } } #[cfg(feature = "std")] impl GenesisConfig { /// Direct implementation of `GenesisBuild::assimilate_storage`. #[deprecated( note = "use ` as GenesisBuild>::assimilate_storage` instead" )] pub fn assimilate_storage, I: 'static>( &self, storage: &mut sp_runtime::Storage, ) -> Result<(), String> { >::assimilate_storage(self, storage) } } #[pallet::genesis_build] impl, I: 'static> GenesisBuild for GenesisConfig { fn build(&self) { // Create Treasury account let account_id = >::account_id(); let min = T::Currency::minimum_balance(); if T::Currency::free_balance(&account_id) < min { let _ = T::Currency::make_free_balance_be(&account_id, min); } } } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { /// New proposal. Proposed { proposal_index: ProposalIndex }, /// We have ended a spend period and will now allocate funds. Spending { budget_remaining: BalanceOf }, /// Some funds have been allocated. Awarded { proposal_index: ProposalIndex, award: BalanceOf, account: T::AccountId }, /// A proposal was rejected; funds were slashed. Rejected { proposal_index: ProposalIndex, slashed: BalanceOf }, /// Some of our funds have been burnt. Burnt { burnt_funds: BalanceOf }, /// Spending has finished; this is the amount that rolls over until next spend. Rollover { rollover_balance: BalanceOf }, /// Some funds have been deposited. Deposit { value: BalanceOf }, /// A new spend proposal has been approved. SpendApproved { proposal_index: ProposalIndex, amount: BalanceOf, beneficiary: T::AccountId, }, } /// Error for the treasury pallet. #[pallet::error] pub enum Error { /// Proposer's balance is too low. InsufficientProposersBalance, /// No proposal or bounty at that index. InvalidIndex, /// Too many approvals in the queue. TooManyApprovals, /// The spend origin is valid but the amount it is allowed to spend is lower than the /// amount to be spent. InsufficientPermission, /// Proposal has not been approved. ProposalNotApproved, } #[pallet::hooks] impl, I: 'static> Hooks> for Pallet { /// # /// - Complexity: `O(A)` where `A` is the number of approvals /// - Db reads and writes: `Approvals`, `pot account data` /// - Db reads and writes per approval: `Proposals`, `proposer account data`, `beneficiary /// account data` /// - The weight is overestimated if some approvals got missed. /// # fn on_initialize(n: T::BlockNumber) -> Weight { // Check to see if we should spend some funds! if (n % T::SpendPeriod::get()).is_zero() { Self::spend_funds() } else { Weight::zero() } } } #[pallet::call] impl, I: 'static> Pallet { /// Put forward a suggestion for spending. A deposit proportional to the value /// is reserved and slashed if the proposal is rejected. It is returned once the /// proposal is awarded. /// /// # /// - Complexity: O(1) /// - DbReads: `ProposalCount`, `origin account` /// - DbWrites: `ProposalCount`, `Proposals`, `origin account` /// # #[pallet::weight(T::WeightInfo::propose_spend())] pub fn propose_spend( origin: OriginFor, #[pallet::compact] value: BalanceOf, beneficiary: AccountIdLookupOf, ) -> DispatchResult { let proposer = ensure_signed(origin)?; let beneficiary = T::Lookup::lookup(beneficiary)?; let bond = Self::calculate_bond(value); T::Currency::reserve(&proposer, bond) .map_err(|_| Error::::InsufficientProposersBalance)?; let c = Self::proposal_count(); >::put(c + 1); >::insert(c, Proposal { proposer, value, beneficiary, bond }); Self::deposit_event(Event::Proposed { proposal_index: c }); Ok(()) } /// Reject a proposed spend. The original deposit will be slashed. /// /// May only be called from `T::RejectOrigin`. /// /// # /// - Complexity: O(1) /// - DbReads: `Proposals`, `rejected proposer account` /// - DbWrites: `Proposals`, `rejected proposer account` /// # #[pallet::weight((T::WeightInfo::reject_proposal(), DispatchClass::Operational))] pub fn reject_proposal( origin: OriginFor, #[pallet::compact] proposal_id: ProposalIndex, ) -> DispatchResult { T::RejectOrigin::ensure_origin(origin)?; let proposal = >::take(&proposal_id).ok_or(Error::::InvalidIndex)?; let value = proposal.bond; let imbalance = T::Currency::slash_reserved(&proposal.proposer, value).0; T::OnSlash::on_unbalanced(imbalance); Self::deposit_event(Event::::Rejected { proposal_index: proposal_id, slashed: value, }); Ok(()) } /// Approve a proposal. At a later time, the proposal will be allocated to the beneficiary /// and the original deposit will be returned. /// /// May only be called from `T::ApproveOrigin`. /// /// # /// - Complexity: O(1). /// - DbReads: `Proposals`, `Approvals` /// - DbWrite: `Approvals` /// # #[pallet::weight((T::WeightInfo::approve_proposal(T::MaxApprovals::get()), DispatchClass::Operational))] pub fn approve_proposal( origin: OriginFor, #[pallet::compact] proposal_id: ProposalIndex, ) -> DispatchResult { T::ApproveOrigin::ensure_origin(origin)?; ensure!(>::contains_key(proposal_id), Error::::InvalidIndex); Approvals::::try_append(proposal_id) .map_err(|_| Error::::TooManyApprovals)?; Ok(()) } /// Propose and approve a spend of treasury funds. /// /// - `origin`: Must be `SpendOrigin` with the `Success` value being at least `amount`. /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. /// - `beneficiary`: The destination account for the transfer. /// /// NOTE: For record-keeping purposes, the proposer is deemed to be equivalent to the /// beneficiary. #[pallet::weight(T::WeightInfo::spend())] pub fn spend( origin: OriginFor, #[pallet::compact] amount: BalanceOf, beneficiary: AccountIdLookupOf, ) -> DispatchResult { let max_amount = T::SpendOrigin::ensure_origin(origin)?; let beneficiary = T::Lookup::lookup(beneficiary)?; ensure!(amount <= max_amount, Error::::InsufficientPermission); let proposal_index = Self::proposal_count(); Approvals::::try_append(proposal_index) .map_err(|_| Error::::TooManyApprovals)?; let proposal = Proposal { proposer: beneficiary.clone(), value: amount, beneficiary: beneficiary.clone(), bond: Default::default(), }; Proposals::::insert(proposal_index, proposal); ProposalCount::::put(proposal_index + 1); Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary }); Ok(()) } /// Force a previously approved proposal to be removed from the approval queue. /// The original deposit will no longer be returned. /// /// May only be called from `T::RejectOrigin`. /// - `proposal_id`: The index of a proposal /// /// # /// - Complexity: O(A) where `A` is the number of approvals /// - Db reads and writes: `Approvals` /// # /// /// Errors: /// - `ProposalNotApproved`: The `proposal_id` supplied was not found in the approval queue, /// i.e., the proposal has not been approved. This could also mean the proposal does not /// exist altogether, thus there is no way it would have been approved in the first place. #[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))] pub fn remove_approval( origin: OriginFor, #[pallet::compact] proposal_id: ProposalIndex, ) -> DispatchResult { T::RejectOrigin::ensure_origin(origin)?; Approvals::::try_mutate(|v| -> DispatchResult { if let Some(index) = v.iter().position(|x| x == &proposal_id) { v.remove(index); Ok(()) } else { Err(Error::::ProposalNotApproved.into()) } })?; Ok(()) } } } impl, I: 'static> Pallet { // Add public immutables and private mutables. /// The account ID of the treasury pot. /// /// This actually does computation. If you need to keep using it, then make sure you cache the /// value and only call this once. pub fn account_id() -> T::AccountId { T::PalletId::get().into_account_truncating() } /// The needed bond for a proposal whose spend is `value`. fn calculate_bond(value: BalanceOf) -> BalanceOf { let mut r = T::ProposalBondMinimum::get().max(T::ProposalBond::get() * value); if let Some(m) = T::ProposalBondMaximum::get() { r = r.min(m); } r } /// Spend some money! returns number of approvals before spend. pub fn spend_funds() -> Weight { let mut total_weight = Weight::new(); let mut budget_remaining = Self::pot(); Self::deposit_event(Event::Spending { budget_remaining }); let account_id = Self::account_id(); let mut missed_any = false; let mut imbalance = >::zero(); let proposals_len = Approvals::::mutate(|v| { let proposals_approvals_len = v.len() as u32; v.retain(|&index| { // Should always be true, but shouldn't panic if false or we're screwed. if let Some(p) = Self::proposals(index) { if p.value <= budget_remaining { budget_remaining -= p.value; >::remove(index); // return their deposit. let err_amount = T::Currency::unreserve(&p.proposer, p.bond); debug_assert!(err_amount.is_zero()); // provide the allocation. imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value)); Self::deposit_event(Event::Awarded { proposal_index: index, award: p.value, account: p.beneficiary, }); false } else { missed_any = true; true } } else { false } }); proposals_approvals_len }); total_weight += T::WeightInfo::on_initialize_proposals(proposals_len); // Call Runtime hooks to external pallet using treasury to compute spend funds. T::SpendFunds::spend_funds( &mut budget_remaining, &mut imbalance, &mut total_weight, &mut missed_any, ); if !missed_any { // burn some proportion of the remaining budget if we run a surplus. let burn = (T::Burn::get() * budget_remaining).min(budget_remaining); budget_remaining -= burn; let (debit, credit) = T::Currency::pair(burn); imbalance.subsume(debit); T::BurnDestination::on_unbalanced(credit); Self::deposit_event(Event::Burnt { burnt_funds: burn }) } // Must never be an error, but better to be safe. // proof: budget_remaining is account free balance minus ED; // Thus we can't spend more than account free balance minus ED; // Thus account is kept alive; qed; if let Err(problem) = T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive) { print("Inconsistent state - couldn't settle imbalance for funds spent by treasury"); // Nothing else to do here. drop(problem); } Self::deposit_event(Event::Rollover { rollover_balance: budget_remaining }); total_weight } /// Return the amount of money in the pot. // The existential deposit is not part of the pot so treasury account never gets deleted. pub fn pot() -> BalanceOf { T::Currency::free_balance(&Self::account_id()) // Must never be less than 0 but better be safe. .saturating_sub(T::Currency::minimum_balance()) } } impl, I: 'static> OnUnbalanced> for Pallet { fn on_nonzero_unbalanced(amount: NegativeImbalanceOf) { let numeric_amount = amount.peek(); // Must resolve into existing but better to be safe. let _ = T::Currency::resolve_creating(&Self::account_id(), amount); Self::deposit_event(Event::Deposit { value: numeric_amount }); } }