diff --git a/Cargo.lock b/Cargo.lock index 7cfde057041e3..495fd7c866c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6019,6 +6019,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-multi-asset-treasury" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "pallet-assets", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-multisig" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 80dd2b90f85c2..458e062c7fc8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,7 @@ members = [ "frame/lottery", "frame/membership", "frame/merkle-mountain-range", + "frame/multi-asset-treasury", "frame/multisig", "frame/nicks", "frame/node-authorization", diff --git a/bin/node/runtime/src/assets_api.rs b/bin/node/runtime/src/assets_api.rs new file mode 100644 index 0000000000000..cf1a663d70300 --- /dev/null +++ b/bin/node/runtime/src/assets_api.rs @@ -0,0 +1,34 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +//! Runtime API definition for assets. + +use codec::Codec; +use sp_std::vec::Vec; + +sp_api::decl_runtime_apis! { + pub trait AssetsApi + where + AccountId: Codec, + AssetBalance: Codec, + AssetId: Codec, + { + /// Returns the list of `AssetId`s and corresponding balance that an `AccountId` has. + fn account_balances(account: AccountId) -> Vec<(AssetId, AssetBalance)>; + } +} diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 063a15085849c..d4cff6e6ade54 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -108,6 +108,9 @@ use sp_runtime::generic::Era; /// Generated voter bag information. mod voter_bags; +/// Runtime API definition for assets. +pub mod assets_api; + // Make the WASM binary available. #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); @@ -2093,6 +2096,18 @@ impl_runtime_apis! { } } + impl assets_api::AssetsApi< + Block, + AccountId, + Balance, + u32, + > for Runtime + { + fn account_balances(account: AccountId) -> Vec<(u32, Balance)> { + Assets::account_balances(account) + } + } + impl pallet_contracts::ContractsApi for Runtime { fn call( diff --git a/frame/assets/src/functions.rs b/frame/assets/src/functions.rs index b6c5ca87ab710..2f1e46f1fb64a 100644 --- a/frame/assets/src/functions.rs +++ b/frame/assets/src/functions.rs @@ -931,4 +931,11 @@ impl, I: 'static> Pallet { Ok(()) }) } + + /// Returns all the non-zero balances for all assets of the given `account`. + pub fn account_balances(account: T::AccountId) -> Vec<(T::AssetId, T::Balance)> { + Asset::::iter_keys() + .filter_map(|id| Self::maybe_balance(id, account.clone()).map(|balance| (id, balance))) + .collect::>() + } } diff --git a/frame/assets/src/impl_fungibles.rs b/frame/assets/src/impl_fungibles.rs index 2e7ba0e2eef99..58ad095ace338 100644 --- a/frame/assets/src/impl_fungibles.rs +++ b/frame/assets/src/impl_fungibles.rs @@ -17,11 +17,14 @@ //! Implementations for fungibles trait. -use frame_support::traits::tokens::{ - Fortitude, - Precision::{self, BestEffort}, - Preservation::{self, Expendable}, - Provenance::{self, Minted}, +use frame_support::traits::{ + fungibles::Credit, + tokens::{ + Fortitude, + Precision::{self, BestEffort}, + Preservation::{self, Expendable}, + Provenance::{self, Minted}, + }, }; use super::*; @@ -258,3 +261,69 @@ impl, I: 'static> fungibles::InspectEnumerable for Pa Asset::::iter_keys() } } + +impl, I: 'static> fungibles::MutateHold<::AccountId> + for Pallet +{ +} + +impl, I: 'static> fungibles::InspectHold<::AccountId> + for Pallet +{ + type Reason = T::HoldIdentifier; + + fn total_balance_on_hold(asset: T::AssetId, who: &T::AccountId) -> T::Balance { + Zero::zero() + } + fn reducible_total_balance_on_hold( + asset: T::AssetId, + who: &T::AccountId, + force: Fortitude, + ) -> T::Balance { + // // The total balance must never drop below the freeze requirements if we're not forcing: + // let a = Self::account(who); + // let unavailable = if force == Force { + // Self::Balance::zero() + // } else { + // // The freeze lock applies to the total balance, so we can discount the free balance + // // from the amount which the total reserved balance must provide to satisfy it. + // a.frozen.saturating_sub(a.free) + // }; + // a.reserved.saturating_sub(unavailable) + Zero::zero() + } + fn balance_on_hold(asset: T::AssetId, reason: &Self::Reason, who: &T::AccountId) -> T::Balance { + // Holds::::get(who) + // .iter() + // .find(|x| &x.id == reason) + // .map_or_else(Zero::zero, |x| x.amount) + Zero::zero() + } + fn hold_available(asset: T::AssetId, reason: &Self::Reason, who: &T::AccountId) -> bool { + false + } +} + +impl, I: 'static> fungibles::UnbalancedHold<::AccountId> + for Pallet +{ + fn set_balance_on_hold( + asset: T::AssetId, + reason: &T::HoldIdentifier, + who: &T::AccountId, + amount: T::Balance, + ) -> DispatchResult { + Ok(()) + } +} + +impl, I: 'static> fungibles::BalancedHold for Pallet { + fn slash( + asset: T::AssetId, + reason: &T::HoldIdentifier, + who: &T::AccountId, + amount: T::Balance, + ) -> (Credit, T::Balance) { + (>::zero(asset), Zero::zero()) + } +} diff --git a/frame/assets/src/lib.rs b/frame/assets/src/lib.rs index 94542f9564997..e3abd0a668f2e 100644 --- a/frame/assets/src/lib.rs +++ b/frame/assets/src/lib.rs @@ -229,6 +229,8 @@ pub mod pallet { + MaxEncodedLen + TypeInfo; + type HoldIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + /// Max number of items to destroy per `destroy_accounts` and `destroy_approvals` call. /// /// Must be configured to result in a weight that makes each call fit in a block. diff --git a/frame/assets/src/tests.rs b/frame/assets/src/tests.rs index 13d5c607a34f2..51ad636e06766 100644 --- a/frame/assets/src/tests.rs +++ b/frame/assets/src/tests.rs @@ -37,11 +37,14 @@ fn asset_ids() -> Vec { fn basic_minting_should_work() { new_test_ext().execute_with(|| { assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 1, 1, true, 1)); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); assert_eq!(Assets::balance(0, 1), 100); assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); assert_eq!(Assets::balance(0, 2), 100); - assert_eq!(asset_ids(), vec![0, 999]); + assert_eq!(asset_ids(), vec![0, 1, 999]); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 1, 1, 100)); + assert_eq!(Assets::account_balances(1), vec![(0, 100), (999, 100), (1, 100)]); }); } diff --git a/frame/multi-asset-treasury/Cargo.toml b/frame/multi-asset-treasury/Cargo.toml new file mode 100644 index 0000000000000..402e960d71948 --- /dev/null +++ b/frame/multi-asset-treasury/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "pallet-multi-asset-treasury" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet to manage treasury" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = [ + "derive", + "max-encoded-len", +] } +impl-trait-for-tuples = "0.2.2" +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +serde = { version = "1.0.136", features = ["derive"], optional = true } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +pallet-balances = { version = "4.0.0-dev", default-features = false, path = "../balances" } +pallet-assets = { version = "4.0.0-dev", default-features = false, path = "../assets" } +sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } + +[dev-dependencies] +sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-io = { version = "7.0.0", path = "../../primitives/io" } + +[features] +default = ["std"] +std = [ + "frame-benchmarking?/std", + "codec/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-assets/std", + "scale-info/std", + "serde", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/multi-asset-treasury/README.md b/frame/multi-asset-treasury/README.md new file mode 100644 index 0000000000000..4945d79d14296 --- /dev/null +++ b/frame/multi-asset-treasury/README.md @@ -0,0 +1,31 @@ +# 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. + +## 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 if 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. diff --git a/frame/multi-asset-treasury/src/benchmarking.rs b/frame/multi-asset-treasury/src/benchmarking.rs new file mode 100644 index 0000000000000..667008e09eea4 --- /dev/null +++ b/frame/multi-asset-treasury/src/benchmarking.rs @@ -0,0 +1,144 @@ +// 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. + +//! Treasury pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::{Pallet as Treasury, *}; + +use frame_benchmarking::v1::{account, benchmarks_instance_pallet, BenchmarkError}; +use frame_support::{ + dispatch::UnfilteredDispatchable, + ensure, + traits::{EnsureOrigin, OnInitialize}, +}; +use frame_system::RawOrigin; +use sp_runtime::Saturating; + +const SEED: u32 = 0; + +// Create the pre-requisite information needed to create a treasury `propose_spend`. +fn setup_proposal, I: 'static>( + u: u32, +) -> (T::AccountId, BalanceOf, AccountIdLookupOf) { + let caller = account("caller", u, SEED); + let value: BalanceOf = T::ProposalBondMinimum::get().saturating_mul(100u32.into()); + let _ = T::Currency::set_balance(&caller, value); + let beneficiary = account("beneficiary", u, SEED); + let beneficiary_lookup = T::Lookup::unlookup(beneficiary); + (caller, value, beneficiary_lookup) +} + +// Create proposals that are approved for use in `on_initialize`. +fn create_approved_proposals, I: 'static>(n: u32) -> Result<(), &'static str> { + for i in 0..n { + let (caller, value, lookup) = setup_proposal::(i); + Treasury::::propose_spend(RawOrigin::Signed(caller).into(), value, lookup)?; + let proposal_id = >::get() - 1; + Treasury::::approve_proposal(RawOrigin::Root.into(), proposal_id)?; + } + ensure!(>::get().len() == n as usize, "Not all approved"); + Ok(()) +} + +fn setup_pot_account, I: 'static>() { + let pot_account = Treasury::::account_id(); + let value = >::minimum_balance() + .saturating_mul(1_000_000_000u32.into()); + let _ = T::Currency::set_balance(&pot_account, value); +} + +fn assert_last_event, I: 'static>(generic_event: >::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +benchmarks_instance_pallet! { + // This benchmark is short-circuited if `SpendOrigin` cannot provide + // a successful origin, in which case `spend` is un-callable and can use weight=0. + spend { + let (_, value, beneficiary_lookup) = setup_proposal::(SEED); + let origin = T::SpendOrigin::try_successful_origin(); + let beneficiary = T::Lookup::lookup(beneficiary_lookup.clone()).unwrap(); + let call = Call::::spend { amount: value, beneficiary: beneficiary_lookup }; + }: { + if let Ok(origin) = origin.clone() { + call.dispatch_bypass_filter(origin)?; + } + } + verify { + if origin.is_ok() { + assert_last_event::(Event::SpendApproved { proposal_index: 0, amount: value, beneficiary }.into()) + } + } + + propose_spend { + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); + // Whitelist caller account from further DB operations. + let caller_key = frame_system::Account::::hashed_key_for(&caller); + frame_benchmarking::benchmarking::add_to_whitelist(caller_key.into()); + }: _(RawOrigin::Signed(caller), value, beneficiary_lookup) + + reject_proposal { + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); + Treasury::::propose_spend( + RawOrigin::Signed(caller).into(), + value, + beneficiary_lookup + )?; + let proposal_id = Treasury::::proposal_count() - 1; + let reject_origin = + T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(reject_origin, proposal_id) + + approve_proposal { + let p in 0 .. T::MaxApprovals::get() - 1; + create_approved_proposals::(p)?; + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); + Treasury::::propose_spend( + RawOrigin::Signed(caller).into(), + value, + beneficiary_lookup + )?; + let proposal_id = Treasury::::proposal_count() - 1; + let approve_origin = + T::ApproveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(approve_origin, proposal_id) + + remove_approval { + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); + Treasury::::propose_spend( + RawOrigin::Signed(caller).into(), + value, + beneficiary_lookup + )?; + let proposal_id = Treasury::::proposal_count() - 1; + Treasury::::approve_proposal(RawOrigin::Root.into(), proposal_id)?; + let reject_origin = + T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(reject_origin, proposal_id) + + on_initialize_proposals { + let p in 0 .. T::MaxApprovals::get(); + setup_pot_account::(); + create_approved_proposals::(p)?; + }: { + Treasury::::on_initialize(T::BlockNumber::zero()); + } + + impl_benchmark_test_suite!(Treasury, crate::tests::new_test_ext(), crate::tests::Test); +} diff --git a/frame/multi-asset-treasury/src/lib.rs b/frame/multi-asset-treasury/src/lib.rs new file mode 100644 index 0000000000000..23c6539876a77 --- /dev/null +++ b/frame/multi-asset-treasury/src/lib.rs @@ -0,0 +1,549 @@ +// 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. + +//! # 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 frame_support::{ + print, + traits::{ + fungible::{self, HandleImbalanceDrop, *}, + fungibles::{ + self, Balanced as FunsBalanced, BalancedHold as FunsBalancedHold, Credit, Debt, + Inspect as FunsInspect, Mutate as FunsMutate, MutateHold as FunsMutateHold, + Unbalanced as FunsUnbalanced, *, + }, + tokens::{AssetId, Precision, Preservation}, + Get, + }, + weights::Weight, + PalletId, +}; +pub use pallet::*; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{AccountIdConversion, StaticLookup, Zero}, + DispatchError, Permill, RuntimeDebug, +}; +use sp_std::prelude::*; +pub use weights::WeightInfo; + +pub type BalanceOf = <>::Assets as fungibles::Inspect< + ::AccountId, +>>::Balance; + +pub type DebtOf = Debt<::AccountId, >::Assets>; +pub type CreditOf = + Credit<::AccountId, >::Assets>; +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 Vec<(T::AssetId, BalanceOf)>, + // imbalance: &mut DebtOf, + total_weight: &mut Weight, + missed_any: &mut bool, + ) { + } +} + +/// A trait that allows us get all the balances which a given account has. +pub trait AccountBalances +where + AccountId: codec::Codec, + AssetBalance: codec::Codec, + AssetId: codec::Codec, +{ + /// Returns the list of `AssetId`s and corresponding balance that an `AccountId` has. + fn account_balances(account: AccountId) -> Vec<(AssetId, AssetBalance)>; +} + +/// 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, + /// The asset which we are refering to. + asset_id: AssetId, +} + +#[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 { + type AssetId: AssetId + PartialOrd; + + type Assets: FunsInspect + + FunsMutate + + FunsUnbalanced + + FunsBalanced + + FunsMutateHold + + FunsBalancedHold; + + type AccountBalances: AccountBalances, Self::AssetId>; + + /// The name for the reserve ID. + #[pallet::constant] + type HoldReason: Get<>::Reason>; + + /// The name for the reserve ID. + #[pallet::constant] + type HoldAssetReason: Get<>::Reason>; + + /// Origin from which approvals must come. + type ApproveOrigin: EnsureOrigin; + + /// Origin from which rejections must come. + type RejectOrigin: EnsureOrigin; + + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// Handler for the unbalanced decrease when slashing for a rejected proposal or bounty. + type OnSlash: HandleImbalanceDrop>; + + /// 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: HandleImbalanceDrop>; + + /// 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, + >; + + /// The amount which has been reported as inactive to Currency. + #[pallet::storage] + pub type Deactivated, I: 'static = ()> = + StorageValue<_, BalanceOf, ValueQuery>; + + /// 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 = >::minimum_balance(); + // if T::Currency::balance(&account_id) < min { + // let _ = T::Currency::set_balance(&account_id, min); + // } + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// We have ended a spend period and will now allocate funds. + Spending { budget_remaining: 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_balances: Vec<(T::AssetId, 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, + }, + /// The inactive funds of the pallet have been updated. + UpdatedInactive { reactivated: BalanceOf, deactivated: BalanceOf }, + } + + /// 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, + /// Invalid asset_id + InvalidAssetId, + /// Insufficient balance in the pot for given asset. + InsufficientAssetBalance, + } + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet { + /// ## Complexity + /// - `O(A)` where `A` is the number of approvals + fn on_initialize(n: T::BlockNumber) -> Weight { + let pot = Self::pot(); + let deactivated = Deactivated::::get(); + // if pot != deactivated { + // >::reactivate(deactivated); + // >::deactivate(pot); + // Deactivated::::put(&pot); + // Self::deposit_event(Event::::UpdatedInactive { + // reactivated: deactivated, + // deactivated: pot, + // }); + // } + + // 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 { + /// Propose and approve a spend of treasury funds. + /// + /// - `origin`: Must be `SpendOrigin` with the `Success` value being at least `amount`. + /// - `asset`: An identifier for the given asset which we're spending. It could refer to the + /// native asset or an actual asset. + /// - `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::call_index(3)] + #[pallet::weight(T::WeightInfo::spend())] + pub fn spend( + origin: OriginFor, + asset_id: T::AssetId, + #[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: amount, + asset_id, + }; + Proposals::::insert(proposal_index, proposal); + ProposalCount::::put(proposal_index + 1); + + Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary }); + 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 + } + + pub fn spend_funds() -> Weight { + let mut total_weight = Weight::zero(); + let mut budgets_remaining = Self::pot(); + let account_id = Self::account_id(); + + let mut missed_any = false; + let proposals_len = Approvals::::mutate(|v| { + let proposals_approvals_len = v.len() as u32; + v.retain(|&index| { + if let Some(p) = Self::proposals(index) { + if let Ok(_) = Self::process_proposal(p, &mut budgets_remaining) { + return false + } + } + missed_any = true; + true + }); + 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 budgets_remaining, + // &mut imbalance, + &mut total_weight, + &mut missed_any, + ); + + if !missed_any { + // burn some proportion of the remaining budget if we run a surplus. + // TODO: how should we track burn configs? Is it per token? + + let burn_rate = T::Burn::get(); + for i in 1..budgets_remaining.len() { + let (asset_id, budget_remaining) = budgets_remaining[i]; + let burn = (burn_rate * budget_remaining).min(budget_remaining); + budgets_remaining[i] = (asset_id, budget_remaining - burn); + + let (debit, credit) = + >::pair(asset_id, burn); + T::BurnDestination::handle(credit); + Self::deposit_event(Event::Burnt { burnt_funds: burn }) + } + } + + // // Thus account is kept alive; qed; + // if let Err(problem) = >::settle( + // &account_id, + // imbalance, + // Preservation::Preserve, + // ) { + // 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_balances: budgets_remaining }); + + total_weight + } + + fn process_proposal( + p: Proposal>, + balances: &mut Vec<(T::AssetId, BalanceOf)>, + ) -> Result<(), DispatchError> { + let (asset_id, budget_remaining) = balances + .iter() + .find(|(asset_id, value)| *asset_id == p.asset_id) + .ok_or(Error::::InvalidAssetId)?; + + if *budget_remaining <= p.value { + return Err(Error::::InsufficientAssetBalance.into()) + } + + // return their deposit. + let released_amount = T::Assets::release( + p.asset_id, + &T::HoldAssetReason::get(), + &p.proposer, + p.bond, + Precision::Exact, + ); + println!("released_amount {:?}", released_amount); + debug_assert!(released_amount.is_ok()); + + // provide the allocation. Rely on the T::Assets::OnDropDebt to handle the resulting Debt. + T::Assets::deposit(p.asset_id, &p.beneficiary, p.value, Precision::Exact)?; + Ok(()) + } + + // /// 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::reducible_balance( + // &Self::account_id(), + // Preservation::Protect, + // Fortitude::Polite, + // ) + // } + pub fn pot() -> Vec<(T::AssetId, BalanceOf)> { + , T::AssetId>>::account_balances(Self::account_id()) + } +} + +// impl, I: 'static> HandleImbalanceDrop> for Pallet { +// fn handle(amount: CreditOf) { +// let numeric_amount = amount.peek(); + +// // Must resolve into existing but better to be safe. +// let _ = T::Currency::resolve(&Self::account_id(), amount); + +// Self::deposit_event(Event::Deposit { value: numeric_amount }); +// } +// } diff --git a/frame/multi-asset-treasury/src/tests.rs b/frame/multi-asset-treasury/src/tests.rs new file mode 100644 index 0000000000000..6699d3e20c118 --- /dev/null +++ b/frame/multi-asset-treasury/src/tests.rs @@ -0,0 +1,562 @@ +// 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. + +//! Treasury pallet tests. + +#![cfg(test)] + +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BadOrigin, BlakeTwo256, IdentityLookup}, +}; + +use frame_support::{ + assert_noop, assert_ok, + pallet_prelude::GenesisBuild, + parameter_types, + traits::{ConstU32, ConstU64, OnInitialize}, + PalletId, +}; + +use super::*; +use crate as treasury; +use frame_support::traits::AsEnsureOriginWithArg; +use frame_system::EnsureRoot; +use pallet_assets::{self}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Treasury: treasury::{Pallet, Call, Storage, Config, Event}, + Assets: pallet_assets::{Pallet, Call, Config, Storage, Event}, + } +); + +type AccountId = u128; + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; // u64 is not enough to hold bytes used to generate bounty account + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +#[derive( + Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, Debug, TypeInfo, +)] +pub enum HoldIdentifier { + Treasury, +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type HoldIdentifier = HoldIdentifier; + type MaxHolds = ConstU32<10>; +} + +type AssetId = u32; + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + // type Balance = Balance; + type Balance = u64; + type AssetId = AssetId; + type AssetIdParameter = AssetId; + type Currency = Balances; + type HoldIdentifier = HoldIdentifier; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = EnsureRoot; + type AssetDeposit = ConstU64<2>; + type AssetAccountDeposit = ConstU64<2>; + type MetadataDepositBase = ConstU64<0>; + type MetadataDepositPerByte = ConstU64<0>; + type ApprovalDeposit = ConstU64<0>; + type StringLimit = ConstU32<20>; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type WeightInfo = (); + type RemoveItemsLimit = ConstU32<1000>; + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +parameter_types! { + pub const ProposalBond: Permill = Permill::from_percent(5); + pub const Burn: Permill = Permill::from_percent(50); + pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub const HoldReason: HoldIdentifier = HoldIdentifier::Treasury; +} +pub struct TestSpendOrigin; +impl frame_support::traits::EnsureOrigin for TestSpendOrigin { + type Success = u64; + fn try_origin(o: RuntimeOrigin) -> Result { + Result::, RuntimeOrigin>::from(o).and_then(|o| match o { + frame_system::RawOrigin::Root => Ok(u64::max_value()), + frame_system::RawOrigin::Signed(10) => Ok(5), + frame_system::RawOrigin::Signed(11) => Ok(10), + frame_system::RawOrigin::Signed(12) => Ok(20), + frame_system::RawOrigin::Signed(13) => Ok(50), + r => Err(RuntimeOrigin::from(r)), + }) + } + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(RuntimeOrigin::root()) + } +} + +pub struct AssetBalances; +impl crate::AccountBalances for AssetBalances { + fn account_balances(account: AccountId) -> Vec<(u32, u64)> { + Assets::account_balances(account) + } +} + +impl Config for Test { + type PalletId = TreasuryPalletId; + // type Currency = pallet_balances::Pallet; + type Assets = pallet_assets::Pallet; + type AccountBalances = AssetBalances; + type AssetId = AssetId; + type HoldAssetReason = HoldReason; + type ApproveOrigin = frame_system::EnsureRoot; + type RejectOrigin = frame_system::EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type OnSlash = (); + type ProposalBond = ProposalBond; + type ProposalBondMinimum = ConstU64<1>; + type ProposalBondMaximum = (); + type SpendPeriod = ConstU64<2>; + type Burn = Burn; + type BurnDestination = (); // Just gets burned. + type WeightInfo = (); + type SpendFunds = (); + type MaxApprovals = ConstU32<100>; + type SpendOrigin = TestSpendOrigin; + type HoldReason = HoldReason; +} + +impl, I: 'static> SpendFunds for () {} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + // Total issuance will be 200 with treasury account initialized at ED. + balances: vec![(0, 100), (1, 98), (2, 1)], + } + .assimilate_storage(&mut t) + .unwrap(); + GenesisBuild::::assimilate_storage(&crate::GenesisConfig, &mut t).unwrap(); + t.into() +} + +// #[test] +// fn genesis_config_works() { +// new_test_ext().execute_with(|| { +// assert_eq!(Treasury::pot(), 0); +// assert_eq!(Treasury::proposal_count(), 0); + +// assert_eq!(Balances::free_balance(0), 100); +// assert_eq!(Balances::free_balance(1), 98); +// assert_eq!(Balances::free_balance(2), 1); +// }); +// } + +// #[test] +// fn spend_origin_permissioning_works() { +// new_test_ext().execute_with(|| { +// assert_noop!(Treasury::spend(RuntimeOrigin::signed(1), 1, 1), BadOrigin); +// assert_noop!( +// Treasury::spend(RuntimeOrigin::signed(10), 6, 1), +// Error::::InsufficientPermission +// ); +// assert_noop!( +// Treasury::spend(RuntimeOrigin::signed(11), 11, 1), +// Error::::InsufficientPermission +// ); +// assert_noop!( +// Treasury::spend(RuntimeOrigin::signed(12), 21, 1), +// Error::::InsufficientPermission +// ); +// assert_noop!( +// Treasury::spend(RuntimeOrigin::signed(13), 51, 1), +// Error::::InsufficientPermission +// ); +// }); +// } + +#[test] +fn spend_origin_works() { + new_test_ext().execute_with(|| { + // Check that accumulate works when we have Some value in Dummy already. + // Balances::set_balance(&Treasury::account_id(), 101); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, Treasury::account_id(), true, 1)); + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 1, Treasury::account_id(), true, 1)); + assert_ok!(Assets::mint( + RuntimeOrigin::signed(Treasury::account_id()), + 0, + Treasury::account_id(), + 100 + )); + assert_ok!(Assets::mint( + RuntimeOrigin::signed(Treasury::account_id()), + 1, + Treasury::account_id(), + 100 + )); + assert_eq!(Treasury::pot(), vec![(0, 100), (1, 100)]); // The missing 1 unit is the ExistentialDeposit + + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 0, 5, 6)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 0, 5, 6)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 0, 5, 6)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 0, 5, 6)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(11), 0, 10, 6)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(12), 0, 20, 6)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(13), 0, 50, 6)); + // // Pot should be unchanged until `on_initialize` and hence `spend_funds` runs. + assert_eq!(Treasury::pot(), [(0, 100), (1, 100)]); + assert_eq!(Assets::balance(0, Treasury::account_id()), 100); + // assert_eq!(Balances::free_balance(6), 0); + + >::on_initialize(1); + assert_eq!(Treasury::pot(), [(0, 100), (1, 100)]); + assert_eq!(Assets::balance(0, Treasury::account_id()), 100); + // assert_eq!(Balances::free_balance(6), 0); + + >::on_initialize(2); + assert_eq!(Treasury::pot(), [(0, 100), (1, 100)]); + assert_eq!(Assets::balance(0, Treasury::account_id()), 100); + // assert_eq!(Balances::free_balance(6), 100); + // assert_eq!(Treasury::pot(), 0); + }); +} + +// #[test] +// fn minting_works() { +// new_test_ext().execute_with(|| { +// // Check that accumulate works when we have Some value in Dummy already. +// Balances::set_balance(&Treasury::account_id(), 101); +// assert_eq!(Treasury::pot(), 100); +// }); +// } + +// #[test] +// fn spend_proposal_takes_min_deposit() { +// new_test_ext().execute_with(|| { +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3)); +// assert_eq!(Balances::free_balance(0), 99); +// assert_eq!(Balances::reserved_balance(0), 1); +// }); +// } + +// #[test] +// fn spend_proposal_takes_proportional_deposit() { +// new_test_ext().execute_with(|| { +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_eq!(Balances::free_balance(0), 95); +// assert_eq!(Balances::reserved_balance(0), 5); +// }); +// } + +// #[test] +// fn spend_proposal_fails_when_proposer_poor() { +// new_test_ext().execute_with(|| { +// assert_noop!( +// Treasury::propose_spend(RuntimeOrigin::signed(2), 100, 3), +// Error::::InsufficientProposersBalance, +// ); +// }); +// } + +// #[test] +// fn accepted_spend_proposal_ignored_outside_spend_period() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 0)); + +// >::on_initialize(1); +// assert_eq!(Balances::free_balance(3), 0); +// assert_eq!(Treasury::pot(), 100); +// }); +// } + +// #[test] +// fn unused_pot_should_diminish() { +// new_test_ext().execute_with(|| { +// let init_total_issuance = Balances::total_issuance(); +// Balances::set_balance(&Treasury::account_id(), 101); +// assert_eq!(Balances::total_issuance(), init_total_issuance + 100); + +// >::on_initialize(2); +// assert_eq!(Treasury::pot(), 50); +// assert_eq!(Balances::total_issuance(), init_total_issuance + 50); +// }); +// } + +// #[test] +// fn rejected_spend_proposal_ignored_on_spend_period() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_ok!(Treasury::reject_proposal(RuntimeOrigin::root(), 0)); + +// >::on_initialize(2); +// assert_eq!(Balances::free_balance(3), 0); +// assert_eq!(Treasury::pot(), 50); +// }); +// } + +// #[test] +// fn reject_already_rejected_spend_proposal_fails() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_ok!(Treasury::reject_proposal(RuntimeOrigin::root(), 0)); +// assert_noop!( +// Treasury::reject_proposal(RuntimeOrigin::root(), 0), +// Error::::InvalidIndex +// ); +// }); +// } + +// #[test] +// fn reject_non_existent_spend_proposal_fails() { +// new_test_ext().execute_with(|| { +// assert_noop!( +// Treasury::reject_proposal(RuntimeOrigin::root(), 0), +// Error::::InvalidIndex +// ); +// }); +// } + +// #[test] +// fn accept_non_existent_spend_proposal_fails() { +// new_test_ext().execute_with(|| { +// assert_noop!( +// Treasury::approve_proposal(RuntimeOrigin::root(), 0), +// Error::::InvalidIndex +// ); +// }); +// } + +// #[test] +// fn accept_already_rejected_spend_proposal_fails() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_ok!(Treasury::reject_proposal(RuntimeOrigin::root(), 0)); +// assert_noop!( +// Treasury::approve_proposal(RuntimeOrigin::root(), 0), +// Error::::InvalidIndex +// ); +// }); +// } + +// #[test] +// fn accepted_spend_proposal_enacted_on_spend_period() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); +// assert_eq!(Treasury::pot(), 100); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 0)); + +// >::on_initialize(2); +// assert_eq!(Balances::free_balance(3), 100); +// assert_eq!(Treasury::pot(), 0); +// }); +// } + +// #[test] +// fn pot_underflow_should_not_diminish() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); +// assert_eq!(Treasury::pot(), 100); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 150, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 0)); + +// >::on_initialize(2); +// assert_eq!(Treasury::pot(), 100); // Pot hasn't changed + +// let _ = Balances::mint_into(&Treasury::account_id(), 100).unwrap(); +// >::on_initialize(4); +// assert_eq!(Balances::free_balance(3), 150); // Fund has been spent +// assert_eq!(Treasury::pot(), 25); // Pot has finally changed +// }); +// } + +// // Treasury account doesn't get deleted if amount approved to spend is all its free balance. +// // i.e. pot should not include existential deposit needed for account survival. +// #[test] +// fn treasury_account_doesnt_get_deleted() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); +// assert_eq!(Treasury::pot(), 100); +// let treasury_balance = Balances::free_balance(&Treasury::account_id()); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), treasury_balance, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 0)); + +// >::on_initialize(2); +// assert_eq!(Treasury::pot(), 100); // Pot hasn't changed + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), Treasury::pot(), 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 1)); + +// >::on_initialize(4); +// assert_eq!(Treasury::pot(), 0); // Pot is emptied +// assert_eq!(Balances::free_balance(Treasury::account_id()), 1); // but the account is still there +// }); +// } + +// // In case treasury account is not existing then it works fine. +// // This is useful for chain that will just update runtime. +// #[test] +// fn inexistent_account_works() { +// let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); +// pallet_balances::GenesisConfig:: { balances: vec![(0, 100), (1, 99), (2, 1)] } +// .assimilate_storage(&mut t) +// .unwrap(); +// // Treasury genesis config is not build thus treasury account does not exist +// let mut t: sp_io::TestExternalities = t.into(); + +// t.execute_with(|| { +// assert_eq!(Balances::free_balance(Treasury::account_id()), 0); // Account does not exist +// assert_eq!(Treasury::pot(), 0); // Pot is empty + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 99, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 0)); +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 1)); +// >::on_initialize(2); +// assert_eq!(Treasury::pot(), 0); // Pot hasn't changed +// assert_eq!(Balances::free_balance(3), 0); // Balance of `3` hasn't changed + +// Balances::set_balance(&Treasury::account_id(), 100); +// assert_eq!(Treasury::pot(), 99); // Pot now contains funds +// assert_eq!(Balances::free_balance(Treasury::account_id()), 100); // Account does exist + +// >::on_initialize(4); + +// assert_eq!(Treasury::pot(), 0); // Pot has changed +// assert_eq!(Balances::free_balance(3), 99); // Balance of `3` has changed +// }); +// } + +// #[test] +// fn genesis_funding_works() { +// let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); +// let initial_funding = 100; +// pallet_balances::GenesisConfig:: { +// // Total issuance will be 200 with treasury account initialized with 100. +// balances: vec![(0, 100), (Treasury::account_id(), initial_funding)], +// } +// .assimilate_storage(&mut t) +// .unwrap(); +// GenesisBuild::::assimilate_storage(&crate::GenesisConfig, &mut t).unwrap(); +// let mut t: sp_io::TestExternalities = t.into(); + +// t.execute_with(|| { +// assert_eq!(Balances::free_balance(Treasury::account_id()), initial_funding); +// assert_eq!(Treasury::pot(), initial_funding - ::AccountId>>::minimum_balance()); }); +// } + +// #[test] +// fn max_approvals_limited() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), u32::MAX.into()); +// Balances::set_balance(&0, u32::MAX.into()); + +// for _ in 0..::MaxApprovals::get() { +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 0)); +// } + +// // One too many will fail +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_noop!( +// Treasury::approve_proposal(RuntimeOrigin::root(), 0), +// Error::::TooManyApprovals +// ); +// }); +// } + +// #[test] +// fn remove_already_removed_approval_fails() { +// new_test_ext().execute_with(|| { +// Balances::set_balance(&Treasury::account_id(), 101); + +// assert_ok!(Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3)); +// assert_ok!(Treasury::approve_proposal(RuntimeOrigin::root(), 0)); +// assert_eq!(Treasury::approvals(), vec![0]); +// assert_ok!(Treasury::remove_approval(RuntimeOrigin::root(), 0)); +// assert_eq!(Treasury::approvals(), vec![]); + +// assert_noop!( +// Treasury::remove_approval(RuntimeOrigin::root(), 0), +// Error::::ProposalNotApproved +// ); +// }); +// } diff --git a/frame/multi-asset-treasury/src/weights.rs b/frame/multi-asset-treasury/src/weights.rs new file mode 100644 index 0000000000000..10bab00836836 --- /dev/null +++ b/frame/multi-asset-treasury/src/weights.rs @@ -0,0 +1,252 @@ +// 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. + +//! Autogenerated weights for pallet_treasury +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-01-25, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm2`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_treasury +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/treasury/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_treasury. +pub trait WeightInfo { + fn spend() -> Weight; + fn propose_spend() -> Weight; + fn reject_proposal() -> Weight; + fn approve_proposal(p: u32, ) -> Weight; + fn remove_approval() -> Weight; + fn on_initialize_proposals(p: u32, ) -> Weight; +} + +/// Weights for pallet_treasury using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Treasury ProposalCount (r:1 w:1) + /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:0 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `1396` + // Minimum execution time: 14_277 nanoseconds. + Weight::from_parts(14_749_000, 1396) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Treasury ProposalCount (r:1 w:1) + /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:0 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + fn propose_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `175` + // Estimated: `499` + // Minimum execution time: 23_297 nanoseconds. + Weight::from_parts(23_585_000, 499) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Treasury Proposals (r:1 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn reject_proposal() -> Weight { + // Proof Size summary in bytes: + // Measured: `365` + // Estimated: `5186` + // Minimum execution time: 23_996 nanoseconds. + Weight::from_parts(24_548_000, 5186) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Treasury Proposals (r:1 w:0) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// The range of component `p` is `[0, 99]`. + fn approve_proposal(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `501 + p * (8 ±0)` + // Estimated: `3480` + // Minimum execution time: 9_366 nanoseconds. + Weight::from_parts(11_731_455, 3480) + // Standard Error: 761 + .saturating_add(Weight::from_ref_time(21_665).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + fn remove_approval() -> Weight { + // Proof Size summary in bytes: + // Measured: `127` + // Estimated: `897` + // Minimum execution time: 7_012 nanoseconds. + Weight::from_parts(7_270_000, 897) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Treasury Deactivated (r:1 w:1) + /// Proof: Treasury Deactivated (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:100 w:100) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: System Account (r:200 w:200) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Bounties BountyApprovals (r:1 w:1) + /// Proof: Bounties BountyApprovals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// The range of component `p` is `[0, 100]`. + fn on_initialize_proposals(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `415 + p * (314 ±0)` + // Estimated: `2305 + p * (7789 ±0)` + // Minimum execution time: 37_834 nanoseconds. + Weight::from_parts(47_496_917, 2305) + // Standard Error: 12_505 + .saturating_add(Weight::from_ref_time(26_902_474).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_proof_size(7789).saturating_mul(p.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Treasury ProposalCount (r:1 w:1) + /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:0 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `1396` + // Minimum execution time: 14_277 nanoseconds. + Weight::from_parts(14_749_000, 1396) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Treasury ProposalCount (r:1 w:1) + /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:0 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + fn propose_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `175` + // Estimated: `499` + // Minimum execution time: 23_297 nanoseconds. + Weight::from_parts(23_585_000, 499) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Treasury Proposals (r:1 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn reject_proposal() -> Weight { + // Proof Size summary in bytes: + // Measured: `365` + // Estimated: `5186` + // Minimum execution time: 23_996 nanoseconds. + Weight::from_parts(24_548_000, 5186) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Treasury Proposals (r:1 w:0) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// The range of component `p` is `[0, 99]`. + fn approve_proposal(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `501 + p * (8 ±0)` + // Estimated: `3480` + // Minimum execution time: 9_366 nanoseconds. + Weight::from_parts(11_731_455, 3480) + // Standard Error: 761 + .saturating_add(Weight::from_ref_time(21_665).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + fn remove_approval() -> Weight { + // Proof Size summary in bytes: + // Measured: `127` + // Estimated: `897` + // Minimum execution time: 7_012 nanoseconds. + Weight::from_parts(7_270_000, 897) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Treasury Deactivated (r:1 w:1) + /// Proof: Treasury Deactivated (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:100 w:100) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: System Account (r:200 w:200) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Bounties BountyApprovals (r:1 w:1) + /// Proof: Bounties BountyApprovals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// The range of component `p` is `[0, 100]`. + fn on_initialize_proposals(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `415 + p * (314 ±0)` + // Estimated: `2305 + p * (7789 ±0)` + // Minimum execution time: 37_834 nanoseconds. + Weight::from_parts(47_496_917, 2305) + // Standard Error: 12_505 + .saturating_add(Weight::from_ref_time(26_902_474).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_proof_size(7789).saturating_mul(p.into())) + } +} diff --git a/frame/treasury/src/lib.rs b/frame/treasury/src/lib.rs index ce70f0356ec62..cde17f8c9e2ba 100644 --- a/frame/treasury/src/lib.rs +++ b/frame/treasury/src/lib.rs @@ -67,10 +67,10 @@ use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{ - traits::{AccountIdConversion, StaticLookup, Zero}, + traits::{AccountIdConversion, CheckedAdd, Saturating, StaticLookup, Zero}, Permill, RuntimeDebug, }; -use sp_std::prelude::*; +use sp_std::{collections::btree_map::BTreeMap, prelude::*}; use frame_support::{ print, @@ -138,7 +138,7 @@ pub struct Proposal { #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::pallet_prelude::*; + use frame_support::{dispatch_context::with_context, pallet_prelude::*}; use frame_system::pallet_prelude::*; #[pallet::pallet] @@ -355,6 +355,11 @@ pub mod pallet { } } + #[derive(Default)] + struct SpendContext { + spend_in_context: BTreeMap, + } + #[pallet::call] impl, I: 'static> Pallet { /// Put forward a suggestion for spending. A deposit proportional to the value @@ -453,9 +458,29 @@ pub mod pallet { beneficiary: AccountIdLookupOf, ) -> DispatchResult { let max_amount = T::SpendOrigin::ensure_origin(origin)?; - let beneficiary = T::Lookup::lookup(beneficiary)?; - ensure!(amount <= max_amount, Error::::InsufficientPermission); + + with_context::>, _>(|v| { + let context = v.or_default(); + + // We group based on `max_amount`, to dinstinguish between different kind of + // origins. (assumes that all origins have different `max_amount`) + // + // Worst case is that we reject some "valid" request. + let spend = context.spend_in_context.entry(max_amount).or_default(); + + // Ensure that we don't overflow nor use more than `max_amount` + if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) { + Err(Error::::InsufficientPermission) + } else { + *spend = spend.saturating_add(amount); + + Ok(()) + } + }) + .unwrap_or(Ok(()))?; + + let beneficiary = T::Lookup::lookup(beneficiary)?; let proposal_index = Self::proposal_count(); Approvals::::try_append(proposal_index) .map_err(|_| Error::::TooManyApprovals)?; diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index 6900dba617982..4b04bf29e04fd 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -22,11 +22,11 @@ use sp_core::H256; use sp_runtime::{ testing::Header, - traits::{BadOrigin, BlakeTwo256, IdentityLookup}, + traits::{BadOrigin, BlakeTwo256, Dispatchable, IdentityLookup}, }; use frame_support::{ - assert_noop, assert_ok, + assert_err_ignore_postinfo, assert_noop, assert_ok, pallet_prelude::GenesisBuild, parameter_types, traits::{ConstU32, ConstU64, OnInitialize}, @@ -38,6 +38,8 @@ use crate as treasury; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; +type UtilityCall = pallet_utility::Call; +type TreasuryCall = crate::Call; frame_support::construct_runtime!( pub enum Test where @@ -48,6 +50,7 @@ frame_support::construct_runtime!( System: frame_system::{Pallet, Call, Config, Storage, Event}, Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, Treasury: treasury::{Pallet, Call, Storage, Config, Event}, + Utility: pallet_utility, } ); @@ -101,6 +104,13 @@ impl pallet_balances::Config for Test { type MaxHolds = ConstU32<10>; } +impl pallet_utility::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const Burn: Permill = Permill::from_percent(50); @@ -493,3 +503,28 @@ fn remove_already_removed_approval_fails() { ); }); } + +#[test] +fn spending_in_batch_respects_max_total() { + new_test_ext().execute_with(|| { + // Respect the `max_total` for the given origin. + assert_ok!(RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend { amount: 2, beneficiary: 100 }), + RuntimeCall::from(TreasuryCall::spend { amount: 2, beneficiary: 101 }) + ] + }) + .dispatch(RuntimeOrigin::signed(10))); + + assert_err_ignore_postinfo!( + RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend { amount: 2, beneficiary: 100 }), + RuntimeCall::from(TreasuryCall::spend { amount: 4, beneficiary: 101 }) + ] + }) + .dispatch(RuntimeOrigin::signed(10)), + Error::::InsufficientPermission + ); + }) +}