diff --git a/Cargo.lock b/Cargo.lock index d9bed10199..13009284c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,7 +1476,7 @@ dependencies = [ "pallet-time-release", "pallet-timestamp", "pallet-transaction-payment", - "pallet-treasury", + "pallet-treasury 27.0.0", "pallet-utility", "parity-scale-codec", "scale-info", @@ -3793,7 +3793,7 @@ dependencies = [ "pallet-timestamp", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", - "pallet-treasury", + "pallet-treasury 27.0.0", "pallet-utility", "parity-scale-codec", "polkadot-parachain-primitives", @@ -7400,7 +7400,7 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-treasury", + "pallet-treasury 38.1.0", "parity-scale-codec", "scale-info", "sp-core", @@ -7467,7 +7467,7 @@ dependencies = [ "frame-system", "log", "pallet-bounties", - "pallet-treasury", + "pallet-treasury 38.1.0", "parity-scale-codec", "scale-info", "sp-core", @@ -8538,7 +8538,7 @@ dependencies = [ "frame-support", "frame-system", "log", - "pallet-treasury", + "pallet-treasury 38.1.0", "parity-scale-codec", "scale-info", "serde", @@ -8591,6 +8591,28 @@ dependencies = [ "sp-weights", ] +[[package]] +name = "pallet-treasury" +version = "27.0.0" +dependencies = [ + "common-primitives", + "common-runtime", + "docify", + "frame-benchmarking", + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "pallet-balances", + "pallet-utility", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-treasury" version = "38.1.0" @@ -9892,7 +9914,7 @@ dependencies = [ "pallet-staking-reward-fn", "pallet-timestamp", "pallet-transaction-payment", - "pallet-treasury", + "pallet-treasury 38.1.0", "pallet-vesting", "parity-scale-codec", "polkadot-primitives", @@ -11173,7 +11195,7 @@ dependencies = [ "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", - "pallet-treasury", + "pallet-treasury 38.1.0", "pallet-utility", "pallet-vesting", "pallet-whitelist", @@ -16486,7 +16508,7 @@ dependencies = [ "pallet-timestamp", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", - "pallet-treasury", + "pallet-treasury 38.1.0", "pallet-utility", "pallet-vesting", "pallet-whitelist", diff --git a/Cargo.toml b/Cargo.toml index 3f5181b191..23984a9080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,8 @@ pretty_assertions = { version = "1.3.0" } smallvec = "1.11.0" numtoa = "0.2.4" enumflags2 = "0.7.7" +docify = "0.2.8" +impl-trait-for-tuples = "0.2.2" serde = { version = "1.0", default-features = false } serde_json = { version = "1.0", default-features = false } serial_test = { version = "2.0.0", default-features = false } @@ -74,7 +76,6 @@ pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk", branch = "st pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } -pallet-treasury = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-utility = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } pallet-proxy = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 8f71576e6e..f195f4d878 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -2834,7 +2834,7 @@ "node_modules/@frequency-chain/api-augment": { "version": "0.0.0", "resolved": "file:../js/api-augment/dist/frequency-chain-api-augment-0.0.0.tgz", - "integrity": "sha512-+zKbsryiElGzLrXuQIr64R70ddwTYLvUpnn24CXiZrhx0nRLXbXgI4zJ0YZo7kPEicqT8VmT+xjBxh6TuSaKVw==", + "integrity": "sha512-/ZbKZO+qy9WKFidMkNIQdFbNxRnUw47PQPKX6RTxN9ETYLhUrWMpMCdXsngVk9RMmEeFuS4IsBX42COlL8Zplw==", "license": "Apache-2.0", "dependencies": { "@polkadot/api": "^15.8.1", diff --git a/e2e/sudo/sudo.test.ts b/e2e/sudo/sudo.test.ts index efcaf65dc2..5477034b35 100644 --- a/e2e/sudo/sudo.test.ts +++ b/e2e/sudo/sudo.test.ts @@ -183,87 +183,76 @@ describe('Sudo required', function () { }); }); - /* - * This test is skipped. - * BACKGROUND: - * Under the old Treasury proposal model, anyone could submit a proposal, which would - * require the proposer to have a certain amount of tokens locked as a bond. - * - * Once it is determined what the new model will be for Treasury proposals on Frequency, this section - * can be removed, or updated with new tests relevant to the way Treasury proposals will work. - */ - // describe.skip('Capacity should not be affected by a hold being slashed', function () { - // it('stake should fail when overlapping tokens are on hold', async function () { - // const accountBalance: bigint = 122n * DOLLARS; - // const stakeBalance: bigint = 100n * DOLLARS; - // const spendBalance: bigint = 20n * DOLLARS; - // const proposalBond: bigint = 100n * DOLLARS; - - // // Setup some keys and a provider for capacity staking - // const stakeKeys: KeyringPair = createKeys('StakeKeys'); - // const stakeProviderId: u64 = await createMsaAndProvider( - // fundingSource, - // stakeKeys, - // 'StakeProvider', - // accountBalance - // ); - - // // Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS - // const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance); - // const { target: proposalEvent } = await proposalExt.signAndSend(); - // assert.notEqual(proposalEvent, undefined, 'should return a Proposal event'); - - // // Confirm that the tokens were reserved/hold in the stakeKeys account using the query API - // const stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); - // assert.equal( - // stakedAcctInfo.data.reserved, - // proposalBond, - // `expected ${proposalBond} reserved balance, got ${stakedAcctInfo.data.reserved}` - // ); - - // // Create a stake that will result in overlapping tokens being frozen - // // stake will allow only the balance not on hold to be staked - // await assert.rejects(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance)); - - // // Slash the provider - // // TODO: skipping this test for now because of Treasury proposal changes - // // const slashExt = ExtrinsicHelper.rejectProposal(sudoKey, proposalEvent?.data.proposalIndex); - // // const { target: slashEvent } = await slashExt.sudoSignAndSend(); - // // assert.notEqual(slashEvent, undefined, 'should return a Treasury event'); - - // // Confirm that the tokens were slashed from the stakeKeys account using the query API - // stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); - // assert.equal( - // stakedAcctInfo.data.reserved, - // 0n, - // `expected 0 reserved balance, got ${stakedAcctInfo.data.reserved}` - // ); - // }); - - // it('proposal should fail when overlapping tokens are on hold', async function () { - // const accountBalance: bigint = 122n * DOLLARS; - // const stakeBalance: bigint = 100n * DOLLARS; - // const spendBalance: bigint = 20n * DOLLARS; - - // // Setup some keys and a provider for capacity staking - // const stakeKeys: KeyringPair = createKeys('StakeKeys'); - // const stakeProviderId: u64 = await createMsaAndProvider( - // fundingSource, - // stakeKeys, - // 'StakeProvider', - // accountBalance - // ); - - // // Create a stake that will result in overlapping tokens being frozen - // await assert.doesNotReject(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance)); - - // // TODO: skipping this test for now because of Treasury proposal changes - // // Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS - // // The proposal should fail because the stakeKeys account has overlapping tokens frozen - // const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance); - // await assert.rejects(proposalExt.signAndSend()); - // }); - // }); + describe('Capacity should not be affected by a hold being slashed', function () { + it('stake should fail when overlapping tokens are on hold', async function () { + const accountBalance: bigint = 122n * DOLLARS; + const stakeBalance: bigint = 100n * DOLLARS; + const spendBalance: bigint = 20n * DOLLARS; + const proposalBond: bigint = 100n * DOLLARS; + + // Setup some keys and a provider for capacity staking + const stakeKeys: KeyringPair = createKeys('StakeKeys'); + const stakeProviderId: u64 = await createMsaAndProvider( + fundingSource, + stakeKeys, + 'StakeProvider', + accountBalance + ); + + // Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS + const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance); + const { target: proposalEvent } = await proposalExt.signAndSend(); + assert.notEqual(proposalEvent, undefined, 'should return a Proposal event'); + + // Confirm that the tokens were reserved/hold in the stakeKeys account using the query API + let stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); + assert.equal( + stakedAcctInfo.data.reserved, + proposalBond, + `expected ${proposalBond} reserved balance, got ${stakedAcctInfo.data.reserved}` + ); + + // Create a stake that will result in overlapping tokens being frozen + // stake will allow only the balance not on hold to be staked + await assert.rejects(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance)); + + // Slash the provider + const slashExt = ExtrinsicHelper.rejectProposal(sudoKey, proposalEvent?.data.proposalIndex); + const { target: slashEvent } = await slashExt.sudoSignAndSend(); + assert.notEqual(slashEvent, undefined, 'should return a Treasury event'); + + // Confirm that the tokens were slashed from the stakeKeys account using the query API + stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); + assert.equal( + stakedAcctInfo.data.reserved, + 0n, + `expected 0 reserved balance, got ${stakedAcctInfo.data.reserved}` + ); + }); + + it('proposal should fail when overlapping tokens are on hold', async function () { + const accountBalance: bigint = 122n * DOLLARS; + const stakeBalance: bigint = 100n * DOLLARS; + const spendBalance: bigint = 20n * DOLLARS; + + // Setup some keys and a provider for capacity staking + const stakeKeys: KeyringPair = createKeys('StakeKeys'); + const stakeProviderId: u64 = await createMsaAndProvider( + fundingSource, + stakeKeys, + 'StakeProvider', + accountBalance + ); + + // Create a stake that will result in overlapping tokens being frozen + await assert.doesNotReject(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, stakeBalance)); + + // Create a treasury proposal which will result in a hold with minimum bond = 100 DOLLARS + // The proposal should fail because the stakeKeys account has overlapping tokens frozen + const proposalExt = ExtrinsicHelper.submitProposal(stakeKeys, spendBalance); + await assert.rejects(proposalExt.signAndSend()); + }); + }); }); }); }); diff --git a/pallets/frequency-tx-payment/src/tests/pallet_tests.rs b/pallets/frequency-tx-payment/src/tests/pallet_tests.rs index 344b1dc709..732bfc21ab 100644 --- a/pallets/frequency-tx-payment/src/tests/pallet_tests.rs +++ b/pallets/frequency-tx-payment/src/tests/pallet_tests.rs @@ -14,6 +14,7 @@ use pallet_frequency_tx_payment::Call as FrequencyTxPaymentCall; use pallet_msa::Call as MsaCall; #[test] +#[allow(deprecated)] fn transaction_payment_validate_is_succesful() { let balance_factor = 10; @@ -39,6 +40,7 @@ fn transaction_payment_validate_is_succesful() { } #[test] +#[allow(deprecated)] fn transaction_payment_validate_errors_when_balance_is_cannot_pay_for_fee() { let balance_factor = 1; @@ -67,6 +69,7 @@ fn transaction_payment_validate_errors_when_balance_is_cannot_pay_for_fee() { } #[test] +#[allow(deprecated)] fn transaction_payment_with_token_and_no_overcharge_post_dispatch_refund_is_succesful() { let balance_factor = 10; diff --git a/pallets/handles/src/tests/signed_extension_tests.rs b/pallets/handles/src/tests/signed_extension_tests.rs index 3d57101e7d..4ead98a5a2 100644 --- a/pallets/handles/src/tests/signed_extension_tests.rs +++ b/pallets/handles/src/tests/signed_extension_tests.rs @@ -42,6 +42,7 @@ fn signed_extension_retire_handle_success() { /// Assert that retiring a handle w/o existing one fails the signed extension HandlesSignedExtension #[test] +#[allow(deprecated)] fn signed_extension_retire_handle_failure() { new_test_ext().execute_with(|| { let alice = sr25519::Pair::from_seed(&[0; 32]); @@ -62,6 +63,7 @@ fn signed_extension_retire_handle_failure() { } #[test] +#[allow(deprecated)] fn test_early_retirement_should_fail() { new_test_ext().execute_with(|| { let base_handle_str = "test1"; diff --git a/pallets/stateful-storage/src/tests/delete_page_tests.rs b/pallets/stateful-storage/src/tests/delete_page_tests.rs index 829b92d1a6..c26c6b352d 100644 --- a/pallets/stateful-storage/src/tests/delete_page_tests.rs +++ b/pallets/stateful-storage/src/tests/delete_page_tests.rs @@ -5,11 +5,7 @@ use crate::{ types::*, Config, Error, Event as StatefulEvent, }; -use common_primitives::{ - msa::{MessageSourceId, SchemaId}, - stateful_storage::{PageHash, PageId}, - utils::wrap_binary_data, -}; +use common_primitives::{stateful_storage::PageHash, utils::wrap_binary_data}; use frame_support::{assert_err, assert_ok, assert_storage_noop}; use parity_scale_codec::Encode; #[allow(unused_imports)] diff --git a/pallets/treasury/Cargo.toml b/pallets/treasury/Cargo.toml new file mode 100644 index 0000000000..dee67cef9d --- /dev/null +++ b/pallets/treasury/Cargo.toml @@ -0,0 +1,74 @@ +[package] +authors = ["Frequency"] +description = "FRAME pallet to manage treasury" +edition = "2021" +homepage = "https://substrate.io" +license = "Apache-2.0" +name = "pallet-treasury" +publish = false +repository = "https://github.com/frequency-chain/frequency/" +version = "27.0.0" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] } +docify = { workspace = true } +impl-trait-for-tuples = { workspace = true } +scale-info = { workspace = true, features = ["derive"] } +serde = { features = ["derive"], optional = true, workspace = true, default-features = true } +frame-benchmarking = { workspace = true, default-features = false, optional = true } +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +pallet-balances = { workspace = true, default-features = false } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-core = { workspace = true, optional = true } + +# Frequency related dependencies +common-primitives = { default-features = false, path = "../../common/primitives" } + +[dev-dependencies] +sp-io = { workspace = true } +pallet-utility = { workspace = true } +common-runtime = { path = "../../runtime/common", default-features = false } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-utility/std", + "scale-info/std", + "serde", + "sp-core?/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "common-primitives/std", + "common-runtime/std", +] +runtime-benchmarks = [ + "dep:sp-core", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-utility/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "common-primitives/runtime-benchmarks", + "common-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-utility/try-runtime", + "sp-runtime/try-runtime", +] +test = [] diff --git a/pallets/treasury/README.md b/pallets/treasury/README.md new file mode 100644 index 0000000000..4945d79d14 --- /dev/null +++ b/pallets/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/pallets/treasury/src/benchmarking.rs b/pallets/treasury/src/benchmarking.rs new file mode 100644 index 0000000000..27aa272ce0 --- /dev/null +++ b/pallets/treasury/src/benchmarking.rs @@ -0,0 +1,342 @@ +// 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. +#![allow(clippy::unwrap_used, clippy::expect_used)] +//! Treasury pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::{Pallet as Treasury, *}; + +use frame_benchmarking::{ + v1::{account, BenchmarkError}, + v2::*, +}; +use frame_support::{ + ensure, + traits::{ + tokens::{ConversionFromAssetBalance, PaymentStatus}, + EnsureOrigin, OnInitialize, + }, +}; +use frame_system::RawOrigin; +use sp_core::crypto::FromEntropy; + +/// Trait describing factory functions for dispatchables' parameters. +pub trait ArgumentsFactory { + /// Factory function for an asset kind. + fn create_asset_kind(seed: u32) -> AssetKind; + /// Factory function for a beneficiary. + fn create_beneficiary(seed: [u8; 32]) -> Beneficiary; +} + +/// Implementation that expects the parameters implement the [`FromEntropy`] trait. +impl ArgumentsFactory for () +where + AssetKind: FromEntropy, + Beneficiary: FromEntropy, +{ + fn create_asset_kind(seed: u32) -> AssetKind { + AssetKind::from_entropy(&mut seed.encode().as_slice()).unwrap() + } + fn create_beneficiary(seed: [u8; 32]) -> Beneficiary { + Beneficiary::from_entropy(&mut seed.as_slice()).unwrap() + } +} + +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::make_free_balance_be(&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); + #[allow(deprecated)] + Treasury::::propose_spend(RawOrigin::Signed(caller).into(), value, lookup)?; + let proposal_id = >::get() - 1; + Approvals::::try_append(proposal_id).unwrap(); + } + ensure!(>::get().len() == n as usize, "Not all approved"); + Ok(()) +} + +fn setup_pot_account, I: 'static>() { + let pot_account = Treasury::::account_id(); + let value = T::Currency::minimum_balance().saturating_mul(1_000_000_000u32.into()); + let _ = T::Currency::make_free_balance_be(&pot_account, value); +} + +fn assert_last_event, I: 'static>(generic_event: >::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +// Create the arguments for the `spend` dispatchable. +fn create_spend_arguments, I: 'static>( + seed: u32, +) -> (T::AssetKind, AssetBalanceOf, T::Beneficiary, BeneficiaryLookupOf) { + let asset_kind = T::BenchmarkHelper::create_asset_kind(seed); + let beneficiary = T::BenchmarkHelper::create_beneficiary([seed.try_into().unwrap(); 32]); + let beneficiary_lookup = T::BeneficiaryLookup::unlookup(beneficiary.clone()); + (asset_kind, 100u32.into(), beneficiary, beneficiary_lookup) +} + +#[instance_benchmarks] +mod benchmarks { + use super::*; + + // This benchmark is short-circuited if `SpendOrigin` cannot provide + // a successful origin, in which case `spend` is un-callable and can use weight=0. + #[benchmark] + fn spend_local() -> Result<(), BenchmarkError> { + let (_, value, beneficiary_lookup) = setup_proposal::(SEED); + let origin = + T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let beneficiary = T::Lookup::lookup(beneficiary_lookup.clone()).unwrap(); + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, value, beneficiary_lookup); + + assert_last_event::( + Event::SpendApproved { proposal_index: 0, amount: value, beneficiary }.into(), + ); + Ok(()) + } + + #[benchmark] + fn propose_spend() -> Result<(), BenchmarkError> { + 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()); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), value, beneficiary_lookup); + + Ok(()) + } + + #[benchmark] + fn reject_proposal() -> Result<(), BenchmarkError> { + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); + #[allow(deprecated)] + 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)?; + + #[extrinsic_call] + _(reject_origin as T::RuntimeOrigin, proposal_id); + + Ok(()) + } + + #[benchmark] + fn approve_proposal( + p: Linear<0, { T::MaxApprovals::get() - 1 }>, + ) -> Result<(), BenchmarkError> { + let approve_origin = + T::ApproveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + create_approved_proposals::(p)?; + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); + #[allow(deprecated)] + Treasury::::propose_spend( + RawOrigin::Signed(caller).into(), + value, + beneficiary_lookup, + )?; + let proposal_id = Treasury::::proposal_count() - 1; + + #[extrinsic_call] + _(approve_origin as T::RuntimeOrigin, proposal_id); + + Ok(()) + } + + #[benchmark] + fn remove_approval() -> Result<(), BenchmarkError> { + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); + #[allow(deprecated)] + Treasury::::propose_spend( + RawOrigin::Signed(caller).into(), + value, + beneficiary_lookup, + )?; + let proposal_id = Treasury::::proposal_count() - 1; + Approvals::::try_append(proposal_id).unwrap(); + let reject_origin = + T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(reject_origin as T::RuntimeOrigin, proposal_id); + + Ok(()) + } + + #[benchmark] + fn on_initialize_proposals( + p: Linear<0, { T::MaxApprovals::get() - 1 }>, + ) -> Result<(), BenchmarkError> { + setup_pot_account::(); + create_approved_proposals::(p)?; + + #[block] + { + Treasury::::on_initialize(0u32.into()); + } + + Ok(()) + } + + #[benchmark] + fn spend() -> Result<(), BenchmarkError> { + let origin = + T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let (asset_kind, amount, beneficiary, beneficiary_lookup) = + create_spend_arguments::(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + + #[extrinsic_call] + _( + origin as T::RuntimeOrigin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + ); + + let valid_from = frame_system::Pallet::::block_number(); + let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); + assert_last_event::( + Event::AssetSpendApproved { + index: 0, + asset_kind, + amount, + beneficiary, + valid_from, + expire_at, + } + .into(), + ); + Ok(()) + } + + #[benchmark] + fn payout() -> Result<(), BenchmarkError> { + let origin = T::SpendOrigin::try_successful_origin().map_err(|_| "No origin")?; + let (asset_kind, amount, beneficiary, beneficiary_lookup) = + create_spend_arguments::(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + Treasury::::spend( + origin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + )?; + T::Paymaster::ensure_successful(&beneficiary, asset_kind, amount); + let caller: T::AccountId = account("caller", 0, SEED); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), 0u32); + + let id = match Spends::::get(0).unwrap().status { + PaymentState::Attempted { id, .. } => { + assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure); + id + }, + _ => panic!("No payout attempt made"), + }; + assert_last_event::(Event::Paid { index: 0, payment_id: id }.into()); + assert!(Treasury::::payout(RawOrigin::Signed(caller).into(), 0u32).is_err()); + Ok(()) + } + + #[benchmark] + fn check_status() -> Result<(), BenchmarkError> { + let origin = T::SpendOrigin::try_successful_origin().map_err(|_| "No origin")?; + let (asset_kind, amount, beneficiary, beneficiary_lookup) = + create_spend_arguments::(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + Treasury::::spend( + origin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + )?; + T::Paymaster::ensure_successful(&beneficiary, asset_kind, amount); + let caller: T::AccountId = account("caller", 0, SEED); + Treasury::::payout(RawOrigin::Signed(caller.clone()).into(), 0u32)?; + match Spends::::get(0).unwrap().status { + PaymentState::Attempted { id, .. } => { + T::Paymaster::ensure_concluded(id); + }, + _ => panic!("No payout attempt made"), + }; + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), 0u32); + + if let Some(s) = Spends::::get(0) { + assert!(!matches!(s.status, PaymentState::Attempted { .. })); + } + Ok(()) + } + + #[benchmark] + fn void_spend() -> Result<(), BenchmarkError> { + let origin = T::SpendOrigin::try_successful_origin().map_err(|_| "No origin")?; + let (asset_kind, amount, _, beneficiary_lookup) = create_spend_arguments::(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + Treasury::::spend( + origin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + )?; + assert!(Spends::::get(0).is_some()); + let origin = + T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, 0u32); + + assert!(Spends::::get(0).is_none()); + Ok(()) + } + + impl_benchmark_test_suite!( + Treasury, + crate::tests::ExtBuilder::default().build(), + crate::tests::Test + ); +} diff --git a/pallets/treasury/src/lib.rs b/pallets/treasury/src/lib.rs new file mode 100644 index 0000000000..be94c6cc51 --- /dev/null +++ b/pallets/treasury/src/lib.rs @@ -0,0 +1,1138 @@ +// 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. +//! This is copied from https://github.com/paritytech/polkadot-sdk/tree/release-polkadot-v1.13.0 +//! Git Hash d5160c1d567cc73c7df6c816d41e21aa3adb188d +//! > Made with *Substrate*, for *Polkadot*. +//! +//! [![github]](https://github.com/paritytech/substrate/frame/fast-unstake) - +//! [![polkadot]](https://polkadot.network) +//! +//! [polkadot]: https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white +//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! +//! # 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 and claim expenditures (aka spends). The chain will need to provide a method to approve +//! spends (e.g. public referendum) and a method for collecting funds (e.g. inflation, fees). +//! +//! By way of example, stakeholders 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. +//! - **Pot:** Unspent funds accumulated by the treasury pallet. +//! - **Spend** An approved proposal for transferring a specific amount of funds to a designated +//! beneficiary. +//! +//! ### Example +//! +//! 1. Multiple local spends approved by spend origins and received by a beneficiary. +#![doc = docify::embed!("src/tests.rs", spend_local_origin_works)] +//! +//! 2. Approve a spend of some asset kind and claim it. +#![doc = docify::embed!("src/tests.rs", spend_payout_works)] +//! +//! ## Pallet API +//! +//! See the [`pallet`] module for more information about the interfaces this pallet exposes, +//! including its configuration trait, dispatchables, storage items, events and errors. +//! +//! ## Low Level / Implementation Details +//! +//! Spends can be initiated using either the `spend_local` or `spend` dispatchable. The +//! `spend_local` dispatchable enables the creation of spends using the native currency of the +//! chain, utilizing the funds stored in the pot. These spends are automatically paid out every +//! [`pallet::Config::SpendPeriod`]. On the other hand, the `spend` dispatchable allows spending of +//! any asset kind managed by the treasury, with payment facilitated by a designated +//! [`pallet::Config::Paymaster`]. To claim these spends, the `payout` dispatchable should be called +//! within some temporal bounds, starting from the moment they become valid and within one +//! [`pallet::Config::PayoutPeriod`]. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::expect_used)] + +mod benchmarking; +#[cfg(test)] +mod tests; +pub mod weights; +use core::marker::PhantomData; + +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::ArgumentsFactory; + +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +use sp_runtime::{ + traits::{AccountIdConversion, CheckedAdd, Saturating, StaticLookup, Zero}, + Permill, RuntimeDebug, +}; +use sp_std::{collections::btree_map::BTreeMap, prelude::*}; + +use frame_support::{ + dispatch::{DispatchResult, DispatchResultWithPostInfo}, + ensure, print, + traits::{ + tokens::Pay, 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 AssetBalanceOf = <>::Paymaster as Pay>::Balance; +pub type PositiveImbalanceOf = <>::Currency as Currency< + ::AccountId, +>>::PositiveImbalance; +pub type NegativeImbalanceOf = <>::Currency as Currency< + ::AccountId, +>>::NegativeImbalance; +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; +type BeneficiaryLookupOf = <>::BeneficiaryLookup 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, +} + +/// The state of the payment claim. +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +pub enum PaymentState { + /// Pending claim. + Pending, + /// Payment attempted with a payment identifier. + Attempted { id: Id }, + /// Payment failed. + Failed, +} + +/// Info regarding an approved treasury spend. +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +pub struct SpendStatus { + // The kind of asset to be spent. + asset_kind: AssetKind, + /// The asset amount of the spend. + amount: AssetBalance, + /// The beneficiary of the spend. + beneficiary: Beneficiary, + /// The block number from which the spend can be claimed. + valid_from: BlockNumber, + /// The block number by which the spend has to be claimed. + expire_at: BlockNumber, + /// The status of the payout/claim. + status: PaymentState, +} + +/// Index of an approved treasury spend. +pub type SpendIndex = u32; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + dispatch_context::with_context, + pallet_prelude::*, + traits::tokens::{ConversionFromAssetBalance, PaymentStatus}, + }; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + 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 RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// 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 in a native asset that this origin + /// is allowed to spend at a time. + type SpendOrigin: EnsureOrigin>; + + /// Type parameter representing the asset kinds to be spent from the treasury. + type AssetKind: Parameter + MaxEncodedLen; + + /// Type parameter used to identify the beneficiaries eligible to receive treasury spends. + type Beneficiary: Parameter + MaxEncodedLen; + + /// Converting trait to take a source type and convert to [`Self::Beneficiary`]. + type BeneficiaryLookup: StaticLookup; + + /// Type for processing spends of [Self::AssetKind] in favor of [`Self::Beneficiary`]. + type Paymaster: Pay; + + /// Type for converting the balance of an [Self::AssetKind] to the balance of the native + /// asset, solely for the purpose of asserting the result against the maximum allowed spend + /// amount of the [`Self::SpendOrigin`]. + type BalanceConverter: ConversionFromAssetBalance< + ::Balance, + Self::AssetKind, + BalanceOf, + >; + + /// The period during which an approved treasury spend has to be claimed. + #[pallet::constant] + type PayoutPeriod: Get>; + + /// Helper type for benchmarks. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: ArgumentsFactory; + } + + /// 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>; + + /// The count of spends that have been made. + #[pallet::storage] + pub(crate) type SpendCount = StorageValue<_, SpendIndex, ValueQuery>; + + /// Spends that have been approved and being processed. + // Hasher: Twox safe since `SpendIndex` is an internal count based index. + #[pallet::storage] + pub type Spends, I: 'static = ()> = StorageMap< + _, + Twox64Concat, + SpendIndex, + SpendStatus< + T::AssetKind, + AssetBalanceOf, + T::Beneficiary, + BlockNumberFor, + ::Id, + >, + OptionQuery, + >; + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig, I: 'static = ()> { + #[serde(skip)] + _config: sp_std::marker::PhantomData<(T, I)>, + } + + #[pallet::genesis_build] + impl, I: 'static> BuildGenesisConfig 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, + }, + /// The inactive funds of the pallet have been updated. + UpdatedInactive { reactivated: BalanceOf, deactivated: BalanceOf }, + /// A new asset spend proposal has been approved. + AssetSpendApproved { + index: SpendIndex, + asset_kind: T::AssetKind, + amount: AssetBalanceOf, + beneficiary: T::Beneficiary, + valid_from: BlockNumberFor, + expire_at: BlockNumberFor, + }, + /// An approved spend was voided. + AssetSpendVoided { index: SpendIndex }, + /// A payment happened. + Paid { index: SpendIndex, payment_id: ::Id }, + /// A payment failed and can be retried. + PaymentFailed { index: SpendIndex, payment_id: ::Id }, + /// A spend was processed and removed from the storage. It might have been successfully + /// paid or it may have expired. + SpendProcessed { index: SpendIndex }, + } + + /// Error for the treasury pallet. + #[pallet::error] + pub enum Error { + /// Proposer's balance is too low. + InsufficientProposersBalance, + /// No proposal, bounty or spend 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, + /// The balance of the asset kind is not convertible to the balance of the native asset. + FailedToConvertBalance, + /// The spend has expired and cannot be claimed. + SpendExpired, + /// The spend is not yet eligible for payout. + EarlyPayout, + /// The payment has already been attempted. + AlreadyAttempted, + /// There was some issue with the mechanism of payment. + PayoutError, + /// The payout was not yet attempted/claimed. + NotAttempted, + /// The payment has neither failed nor succeeded yet. + Inconclusive, + } + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet { + /// ## Complexity + /// - `O(A)` where `A` is the number of approvals + fn on_initialize(n: frame_system::pallet_prelude::BlockNumberFor) -> Weight { + let pot = Self::pot(); + let deactivated = Deactivated::::get(); + if pot != deactivated { + T::Currency::reactivate(deactivated); + T::Currency::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() + } + } + + #[cfg(feature = "try-runtime")] + fn try_state( + _: frame_system::pallet_prelude::BlockNumberFor, + ) -> Result<(), sp_runtime::TryRuntimeError> { + Self::do_try_state()?; + Ok(()) + } + } + + #[derive(Default)] + struct SpendContext { + spend_in_context: BTreeMap, + } + + #[pallet::call] + impl, I: 'static> Pallet { + /// Put forward a suggestion for spending. + /// + /// ## Dispatch Origin + /// + /// Must be signed. + /// + /// ## Details + /// 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) + /// + /// ## Events + /// + /// Emits [`Event::Proposed`] if successful. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::propose_spend())] + #[allow(deprecated)] + #[deprecated( + note = "`propose_spend` will be removed in February 2024. Use `spend` instead." + )] + 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. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::RejectOrigin`]. + /// + /// ## Details + /// The original deposit will be slashed. + /// + /// ### Complexity + /// - O(1) + /// + /// ## Events + /// + /// Emits [`Event::Rejected`] if successful. + #[pallet::call_index(1)] + #[pallet::weight((T::WeightInfo::reject_proposal(), DispatchClass::Operational))] + #[allow(deprecated)] + #[deprecated( + note = "`reject_proposal` will be removed in February 2024. Use `spend` instead." + )] + 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. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::ApproveOrigin`]. + /// + /// ## Details + /// + /// At a later time, the proposal will be allocated to the beneficiary and the original + /// deposit will be returned. + /// + /// ### Complexity + /// - O(1). + /// + /// ## Events + /// + /// No events are emitted from this dispatch. + #[pallet::call_index(2)] + #[pallet::weight((T::WeightInfo::approve_proposal(T::MaxApprovals::get()), DispatchClass::Operational))] + #[allow(deprecated)] + #[deprecated( + note = "`approve_proposal` will be removed in February 2024. Use `spend` instead." + )] + 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. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::SpendOrigin`] with the `Success` value being at least `amount`. + /// + /// ### Details + /// NOTE: For record-keeping purposes, the proposer is deemed to be equivalent to the + /// beneficiary. + /// + /// ### Parameters + /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. + /// - `beneficiary`: The destination account for the transfer. + /// + /// ## Events + /// + /// Emits [`Event::SpendApproved`] if successful. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::spend_local())] + pub fn spend_local( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + beneficiary: AccountIdLookupOf, + ) -> DispatchResult { + let max_amount = T::SpendOrigin::ensure_origin(origin)?; + ensure!(amount <= max_amount, Error::::InsufficientPermission); + + with_context::>, _>(|v| { + let context = v.or_default(); + + // We group based on `max_amount`, to distinguish 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)?; + 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. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::RejectOrigin`]. + /// + /// ## Details + /// + /// The original deposit will no longer be returned. + /// + /// ### Parameters + /// - `proposal_id`: The index of a proposal + /// + /// ### Complexity + /// - O(A) where `A` is the number of approvals + /// + /// ### Errors + /// - [`Error::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::call_index(4)] + #[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(()) + } + + /// Propose and approve a spend of treasury funds. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::SpendOrigin`] with the `Success` value being at least + /// `amount` of `asset_kind` in the native asset. The amount of `asset_kind` is converted + /// for assertion using the [`Config::BalanceConverter`]. + /// + /// ## Details + /// + /// Create an approved spend for transferring a specific `amount` of `asset_kind` to a + /// designated beneficiary. The spend must be claimed using the `payout` dispatchable within + /// the [`Config::PayoutPeriod`]. + /// + /// ### Parameters + /// - `asset_kind`: An indicator of the specific asset class to be spent. + /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. + /// - `beneficiary`: The beneficiary of the spend. + /// - `valid_from`: The block number from which the spend can be claimed. It can refer to + /// the past if the resulting spend has not yet expired according to the + /// [`Config::PayoutPeriod`]. If `None`, the spend can be claimed immediately after + /// approval. + /// + /// ## Events + /// + /// Emits [`Event::AssetSpendApproved`] if successful. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::spend())] + pub fn spend( + origin: OriginFor, + asset_kind: Box, + #[pallet::compact] amount: AssetBalanceOf, + beneficiary: Box>, + valid_from: Option>, + ) -> DispatchResult { + let max_amount = T::SpendOrigin::ensure_origin(origin)?; + let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?; + + let now = frame_system::Pallet::::block_number(); + let valid_from = valid_from.unwrap_or(now); + let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); + ensure!(expire_at > now, Error::::SpendExpired); + + let native_amount = + T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone()) + .map_err(|_| Error::::FailedToConvertBalance)?; + + ensure!(native_amount <= max_amount, Error::::InsufficientPermission); + + with_context::>, _>(|v| { + let context = v.or_default(); + // We group based on `max_amount`, to distinguish 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(&native_amount).map(|s| s > max_amount).unwrap_or(true) { + Err(Error::::InsufficientPermission) + } else { + *spend = spend.saturating_add(native_amount); + Ok(()) + } + }) + .unwrap_or(Ok(()))?; + + let index = SpendCount::::get(); + Spends::::insert( + index, + SpendStatus { + asset_kind: *asset_kind.clone(), + amount, + beneficiary: beneficiary.clone(), + valid_from, + expire_at, + status: PaymentState::Pending, + }, + ); + SpendCount::::put(index + 1); + + Self::deposit_event(Event::AssetSpendApproved { + index, + asset_kind: *asset_kind, + amount, + beneficiary, + valid_from, + expire_at, + }); + Ok(()) + } + + /// Claim a spend. + /// + /// ## Dispatch Origin + /// + /// Must be signed. + /// + /// ## Details + /// + /// Spends must be claimed within some temporal bounds. A spend may be claimed within one + /// [`Config::PayoutPeriod`] from the `valid_from` block. + /// In case of a payout failure, the spend status must be updated with the `check_status` + /// dispatchable before retrying with the current function. + /// + /// ### Parameters + /// - `index`: The spend index. + /// + /// ## Events + /// + /// Emits [`Event::Paid`] if successful. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::payout())] + pub fn payout(origin: OriginFor, index: SpendIndex) -> DispatchResult { + ensure_signed(origin)?; + let mut spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; + let now = frame_system::Pallet::::block_number(); + ensure!(now >= spend.valid_from, Error::::EarlyPayout); + ensure!(spend.expire_at > now, Error::::SpendExpired); + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + Error::::AlreadyAttempted + ); + + let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount) + .map_err(|_| Error::::PayoutError)?; + + spend.status = PaymentState::Attempted { id }; + Spends::::insert(index, spend); + + Self::deposit_event(Event::::Paid { index, payment_id: id }); + + Ok(()) + } + + /// Check the status of the spend and remove it from the storage if processed. + /// + /// ## Dispatch Origin + /// + /// Must be signed. + /// + /// ## Details + /// + /// The status check is a prerequisite for retrying a failed payout. + /// If a spend has either succeeded or expired, it is removed from the storage by this + /// function. In such instances, transaction fees are refunded. + /// + /// ### Parameters + /// - `index`: The spend index. + /// + /// ## Events + /// + /// Emits [`Event::PaymentFailed`] if the spend payout has failed. + /// Emits [`Event::SpendProcessed`] if the spend payout has succeed. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::check_status())] + pub fn check_status(origin: OriginFor, index: SpendIndex) -> DispatchResultWithPostInfo { + use PaymentState as State; + use PaymentStatus as Status; + + ensure_signed(origin)?; + let mut spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; + let now = frame_system::Pallet::::block_number(); + + if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) { + // spend has expired and no further status update is expected. + Spends::::remove(index); + Self::deposit_event(Event::::SpendProcessed { index }); + return Ok(Pays::No.into()) + } + + let payment_id = match spend.status { + State::Attempted { id } => id, + _ => return Err(Error::::NotAttempted.into()), + }; + + match T::Paymaster::check_payment(payment_id) { + Status::Failure => { + spend.status = PaymentState::Failed; + Spends::::insert(index, spend); + Self::deposit_event(Event::::PaymentFailed { index, payment_id }); + }, + Status::Success | Status::Unknown => { + Spends::::remove(index); + Self::deposit_event(Event::::SpendProcessed { index }); + return Ok(Pays::No.into()) + }, + Status::InProgress => return Err(Error::::Inconclusive.into()), + } + return Ok(Pays::Yes.into()) + } + + /// Void previously approved spend. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::RejectOrigin`]. + /// + /// ## Details + /// + /// A spend void is only possible if the payout has not been attempted yet. + /// + /// ### Parameters + /// - `index`: The spend index. + /// + /// ## Events + /// + /// Emits [`Event::AssetSpendVoided`] if successful. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::void_spend())] + pub fn void_spend(origin: OriginFor, index: SpendIndex) -> DispatchResult { + T::RejectOrigin::ensure_origin(origin)?; + let spend = Spends::::get(index).ok_or(Error::::InvalidIndex)?; + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + Error::::AlreadyAttempted + ); + + Spends::::remove(index); + Self::deposit_event(Event::::AssetSpendVoided { index }); + 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::zero(); + + 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()) + } + + /// Ensure the correctness of the state of this pallet. + #[cfg(any(feature = "try-runtime", test))] + fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { + Self::try_state_proposals()?; + Self::try_state_spends()?; + + Ok(()) + } + + /// ### Invariants of proposal storage items + /// + /// 1. [`ProposalCount`] >= Number of elements in [`Proposals`]. + /// 2. Each entry in [`Proposals`] should be saved under a key strictly less than current + /// [`ProposalCount`]. + /// 3. Each [`ProposalIndex`] contained in [`Approvals`] should exist in [`Proposals`]. + /// Note, that this automatically implies [`Approvals`].count() <= [`Proposals`].count(). + #[cfg(any(feature = "try-runtime", test))] + fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> { + let current_proposal_count = ProposalCount::::get(); + ensure!( + current_proposal_count as usize >= Proposals::::iter().count(), + "Actual number of proposals exceeds `ProposalCount`." + ); + + Proposals::::iter_keys().try_for_each(|proposal_index| -> DispatchResult { + ensure!( + current_proposal_count as u32 > proposal_index, + "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`." + ); + Ok(()) + })?; + + Approvals::::get() + .iter() + .try_for_each(|proposal_index| -> DispatchResult { + ensure!( + Proposals::::contains_key(proposal_index), + "Proposal indices in `Approvals` must also be contained in `Proposals`." + ); + Ok(()) + })?; + + Ok(()) + } + + /// ## Invariants of spend storage items + /// + /// 1. [`SpendCount`] >= Number of elements in [`Spends`]. + /// 2. Each entry in [`Spends`] should be saved under a key strictly less than current + /// [`SpendCount`]. + /// 3. For each spend entry contained in [`Spends`] we should have spend.expire_at + /// > spend.valid_from. + #[cfg(any(feature = "try-runtime", test))] + fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> { + let current_spend_count = SpendCount::::get(); + ensure!( + current_spend_count as usize >= Spends::::iter().count(), + "Actual number of spends exceeds `SpendCount`." + ); + + Spends::::iter_keys().try_for_each(|spend_index| -> DispatchResult { + ensure!( + current_spend_count > spend_index, + "`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`." + ); + Ok(()) + })?; + + Spends::::iter().try_for_each(|(_index, spend)| -> DispatchResult { + ensure!( + spend.valid_from < spend.expire_at, + "Spend cannot expire before it becomes valid." + ); + Ok(()) + })?; + + Ok(()) + } +} + +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 }); + } +} + +/// TypedGet implementation to get the AccountId of the Treasury. +pub struct TreasuryAccountId(PhantomData); +impl sp_runtime::traits::TypedGet for TreasuryAccountId +where + R: crate::Config, +{ + type Type = ::AccountId; + fn get() -> Self::Type { + >::account_id() + } +} diff --git a/pallets/treasury/src/tests.rs b/pallets/treasury/src/tests.rs new file mode 100644 index 0000000000..ba33f2811c --- /dev/null +++ b/pallets/treasury/src/tests.rs @@ -0,0 +1,1144 @@ +// 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 core::{cell::RefCell, marker::PhantomData}; +use sp_runtime::{ + traits::{BadOrigin, Dispatchable, IdentityLookup}, + BuildStorage, +}; + +use frame_support::{ + assert_err_ignore_postinfo, assert_noop, assert_ok, derive_impl, + pallet_prelude::Pays, + parameter_types, + traits::{ + tokens::{ConversionFromAssetBalance, PaymentStatus}, + ConstU32, ConstU64, OnInitialize, + }, + PalletId, +}; + +use super::*; +use crate as treasury; + +type Block = frame_system::mocking::MockBlock; +type UtilityCall = pallet_utility::Call; +type TreasuryCall = crate::Call; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Treasury: treasury, + Utility: pallet_utility, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountId = u128; // u64 is not enough to hold bytes used to generate bounty account + type Lookup = IdentityLookup; + type Block = Block; + type AccountData = pallet_balances::AccountData; +} +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 RuntimeHoldReason = (); + type RuntimeFreezeReason = (); + type DoneSlashHandler = (); +} + +impl pallet_utility::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + +thread_local! { + pub static PAID: RefCell> = RefCell::new(BTreeMap::new()); + pub static STATUS: RefCell> = RefCell::new(BTreeMap::new()); + pub static LAST_ID: RefCell = RefCell::new(0u64); +} + +/// paid balance for a given account and asset ids +fn paid(who: u128, asset_id: u32) -> u64 { + PAID.with(|p| p.borrow().get(&(who, asset_id)).cloned().unwrap_or(0)) +} + +/// reduce paid balance for a given account and asset ids +fn unpay(who: u128, asset_id: u32, amount: u64) { + PAID.with(|p| p.borrow_mut().entry((who, asset_id)).or_default().saturating_reduce(amount)) +} + +/// set status for a given payment id +fn set_status(id: u64, s: PaymentStatus) { + STATUS.with(|m| m.borrow_mut().insert(id, s)); +} + +pub struct TestPay; +impl Pay for TestPay { + type Beneficiary = u128; + type Balance = u64; + type Id = u64; + type AssetKind = u32; + type Error = (); + + fn pay( + who: &Self::Beneficiary, + asset_kind: Self::AssetKind, + amount: Self::Balance, + ) -> Result { + PAID.with(|paid| *paid.borrow_mut().entry((*who, asset_kind)).or_default() += amount); + Ok(LAST_ID.with(|lid| { + let x = *lid.borrow(); + lid.replace(x + 1); + x + })) + } + fn check_payment(id: Self::Id) -> PaymentStatus { + STATUS.with(|s| s.borrow().get(&id).cloned().unwrap_or(PaymentStatus::Unknown)) + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(_: &Self::Beneficiary, _: Self::AssetKind, _: Self::Balance) {} + #[cfg(feature = "runtime-benchmarks")] + fn ensure_concluded(id: Self::Id) { + set_status(id, PaymentStatus::Failure) + } +} + +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 TreasuryAccount: u128 = Treasury::account_id(); + pub const SpendPayoutPeriod: u64 = 5; +} +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 MulBy(PhantomData); +impl> ConversionFromAssetBalance for MulBy { + type Error = (); + fn from_asset_balance(balance: u64, _asset_id: u32) -> Result { + return balance.checked_mul(N::get()).ok_or(()) + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(_: u32) {} +} + +impl Config for Test { + type PalletId = TreasuryPalletId; + type Currency = pallet_balances::Pallet; + 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 AssetKind = u32; + type Beneficiary = u128; + type BeneficiaryLookup = IdentityLookup; + type Paymaster = TestPay; + type BalanceConverter = MulBy>; + type PayoutPeriod = SpendPayoutPeriod; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +pub struct ExtBuilder {} + +impl Default for ExtBuilder { + fn default() -> Self { + Self {} + } +} + +impl ExtBuilder { + pub fn build(self) -> 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(); + crate::GenesisConfig::::default().assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +fn get_payment_id(i: SpendIndex) -> Option { + let spend = Spends::::get(i).expect("no spend"); + match spend.status { + PaymentState::Attempted { id } => Some(id), + _ => None, + } +} + +#[test] +fn genesis_config_works() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(Treasury::pot(), 0); + assert_eq!(Treasury::proposal_count(), 0); + }); +} + +#[test] +fn spend_local_origin_permissioning_works() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!(Treasury::spend_local(RuntimeOrigin::signed(1), 1, 1), BadOrigin); + assert_noop!( + Treasury::spend_local(RuntimeOrigin::signed(10), 6, 1), + Error::::InsufficientPermission + ); + assert_noop!( + Treasury::spend_local(RuntimeOrigin::signed(11), 11, 1), + Error::::InsufficientPermission + ); + assert_noop!( + Treasury::spend_local(RuntimeOrigin::signed(12), 21, 1), + Error::::InsufficientPermission + ); + assert_noop!( + Treasury::spend_local(RuntimeOrigin::signed(13), 51, 1), + Error::::InsufficientPermission + ); + }); +} + +#[docify::export] +#[test] +fn spend_local_origin_works() { + ExtBuilder::default().build().execute_with(|| { + // Check that accumulate works when we have Some value in Dummy already. + Balances::make_free_balance_be(&Treasury::account_id(), 101); + // approve spend of some amount to beneficiary `6`. + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(11), 10, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(12), 20, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(13), 50, 6)); + // free balance of `6` is zero, spend period has not passed. + >::on_initialize(1); + assert_eq!(Balances::free_balance(6), 0); + // free balance of `6` is `100`, spend period has passed. + >::on_initialize(2); + assert_eq!(Balances::free_balance(6), 100); + // `100` spent, `1` burned. + assert_eq!(Treasury::pot(), 0); + }); +} + +#[test] +fn minting_works() { + ExtBuilder::default().build().execute_with(|| { + // Check that accumulate works when we have Some value in Dummy already. + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Treasury::pot(), 100); + }); +} + +#[test] +fn spend_proposal_takes_min_deposit() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!({ + #[allow(deprecated)] + 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() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!({ + #[allow(deprecated)] + 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() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + { + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(2), 100, 3) + }, + Error::::InsufficientProposersBalance, + ); + }); +} + +#[test] +fn accepted_spend_proposal_ignored_outside_spend_period() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_ok!({ + #[allow(deprecated)] + 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() { + ExtBuilder::default().build().execute_with(|| { + let init_total_issuance = Balances::total_issuance(); + Balances::make_free_balance_be(&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() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_ok!({ + #[allow(deprecated)] + 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() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_ok!({ + #[allow(deprecated)] + Treasury::reject_proposal(RuntimeOrigin::root(), 0) + }); + assert_noop!( + { + #[allow(deprecated)] + Treasury::reject_proposal(RuntimeOrigin::root(), 0) + }, + Error::::InvalidIndex + ); + }); +} + +#[test] +fn reject_non_existent_spend_proposal_fails() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + { + #[allow(deprecated)] + Treasury::reject_proposal(RuntimeOrigin::root(), 0) + }, + Error::::InvalidIndex + ); + }); +} + +#[test] +fn accept_non_existent_spend_proposal_fails() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + { + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }, + Error::::InvalidIndex + ); + }); +} + +#[test] +fn accept_already_rejected_spend_proposal_fails() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_ok!({ + #[allow(deprecated)] + Treasury::reject_proposal(RuntimeOrigin::root(), 0) + }); + assert_noop!( + { + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }, + Error::::InvalidIndex + ); + }); +} + +#[test] +fn accepted_spend_proposal_enacted_on_spend_period() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Treasury::pot(), 100); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_ok!({ + #[allow(deprecated)] + 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() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Treasury::pot(), 100); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 150, 3) + }); + assert_ok!({ + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }); + + >::on_initialize(2); + assert_eq!(Treasury::pot(), 100); // Pot hasn't changed + + let _ = Balances::deposit_into_existing(&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() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Treasury::pot(), 100); + let treasury_balance = Balances::free_balance(&Treasury::account_id()); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), treasury_balance, 3) + }); + assert_ok!({ + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }); + + >::on_initialize(2); + assert_eq!(Treasury::pot(), 100); // Pot hasn't changed + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), Treasury::pot(), 3) + }); + assert_ok!({ + #[allow(deprecated)] + 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!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 99, 3) + }); + assert_ok!({ + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }); + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3) + }); + assert_ok!({ + #[allow(deprecated)] + 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::make_free_balance_be(&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(); + crate::GenesisConfig::::default().assimilate_storage(&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 - Balances::minimum_balance()); + }); +} + +#[test] +fn max_approvals_limited() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), u64::MAX); + Balances::make_free_balance_be(&0, u64::MAX); + + for _ in 0..::MaxApprovals::get() { + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_ok!({ + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }); + } + + // One too many will fail + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_noop!( + { + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }, + Error::::TooManyApprovals + ); + }); +} + +#[test] +fn remove_already_removed_approval_fails() { + ExtBuilder::default().build().execute_with(|| { + Balances::make_free_balance_be(&Treasury::account_id(), 101); + + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) + }); + assert_ok!({ + #[allow(deprecated)] + 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 + ); + }); +} + +#[test] +fn spending_local_in_batch_respects_max_total() { + ExtBuilder::default().build().execute_with(|| { + // Respect the `max_total` for the given origin. + assert_ok!(RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend_local { amount: 2, beneficiary: 100 }), + RuntimeCall::from(TreasuryCall::spend_local { amount: 2, beneficiary: 101 }) + ] + }) + .dispatch(RuntimeOrigin::signed(10))); + + assert_err_ignore_postinfo!( + RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend_local { amount: 2, beneficiary: 100 }), + RuntimeCall::from(TreasuryCall::spend_local { amount: 4, beneficiary: 101 }) + ] + }) + .dispatch(RuntimeOrigin::signed(10)), + Error::::InsufficientPermission + ); + }) +} + +#[test] +fn spending_in_batch_respects_max_total() { + ExtBuilder::default().build().execute_with(|| { + // Respect the `max_total` for the given origin. + assert_ok!(RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 1, + beneficiary: Box::new(100), + valid_from: None, + }), + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 1, + beneficiary: Box::new(101), + valid_from: None, + }) + ] + }) + .dispatch(RuntimeOrigin::signed(10))); + + assert_err_ignore_postinfo!( + RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 2, + beneficiary: Box::new(100), + valid_from: None, + }), + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 2, + beneficiary: Box::new(101), + valid_from: None, + }) + ] + }) + .dispatch(RuntimeOrigin::signed(10)), + Error::::InsufficientPermission + ); + }) +} + +#[test] +fn spend_origin_works() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 3, Box::new(6), None), + Error::::InsufficientPermission + ); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(11), Box::new(1), 5, Box::new(6), None)); + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(11), Box::new(1), 6, Box::new(6), None), + Error::::InsufficientPermission + ); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(12), Box::new(1), 10, Box::new(6), None)); + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(12), Box::new(1), 11, Box::new(6), None), + Error::::InsufficientPermission + ); + + assert_eq!(SpendCount::::get(), 4); + assert_eq!(Spends::::iter().count(), 4); + }); +} + +#[test] +fn spend_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + + assert_eq!(SpendCount::::get(), 1); + assert_eq!( + Spends::::get(0).unwrap(), + SpendStatus { + asset_kind: 1, + amount: 2, + beneficiary: 6, + valid_from: 1, + expire_at: 6, + status: PaymentState::Pending, + } + ); + System::assert_last_event( + Event::::AssetSpendApproved { + index: 0, + asset_kind: 1, + amount: 2, + beneficiary: 6, + valid_from: 1, + expire_at: 6, + } + .into(), + ); + }); +} + +#[test] +fn spend_expires() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(::PayoutPeriod::get(), 5); + + // spend `0` expires in 5 blocks after the creating. + System::set_block_number(1); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + System::set_block_number(6); + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::SpendExpired); + + // spend cannot be approved since its already expired. + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), Some(0)), + Error::::SpendExpired + ); + }); +} + +#[docify::export] +#[test] +fn spend_payout_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + // approve a `2` coins spend of asset `1` to beneficiary `6`, the spend valid from now. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + // payout the spend. + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + // beneficiary received `2` coins of asset `1`. + assert_eq!(paid(6, 1), 2); + assert_eq!(SpendCount::::get(), 1); + let payment_id = get_payment_id(0).expect("no payment attempt"); + System::assert_last_event(Event::::Paid { index: 0, payment_id }.into()); + set_status(payment_id, PaymentStatus::Success); + // the payment succeed. + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + System::assert_last_event(Event::::SpendProcessed { index: 0 }.into()); + // cannot payout the same spend twice. + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::InvalidIndex); + }); +} + +#[test] +fn payout_retry_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_eq!(paid(6, 1), 2); + let payment_id = get_payment_id(0).expect("no payment attempt"); + // spend payment is failed + set_status(payment_id, PaymentStatus::Failure); + unpay(6, 1, 2); + // cannot payout a spend in the attempted state + assert_noop!( + Treasury::payout(RuntimeOrigin::signed(1), 0), + Error::::AlreadyAttempted + ); + // check status and update it to retry the payout again + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + System::assert_last_event(Event::::PaymentFailed { index: 0, payment_id }.into()); + // the payout can be retried now + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_eq!(paid(6, 1), 2); + }); +} + +#[test] +fn spend_valid_from_works() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(::PayoutPeriod::get(), 5); + System::set_block_number(1); + + // spend valid from block `2`. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(2) + )); + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::::EarlyPayout); + System::set_block_number(2); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + System::set_block_number(5); + // spend approved even if `valid_from` in the past since the payout period has not passed. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(4) + )); + // spend paid. + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + }); +} + +#[test] +fn void_spend_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + // spend cannot be voided if already attempted. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(1) + )); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_noop!( + Treasury::void_spend(RuntimeOrigin::root(), 0), + Error::::AlreadyAttempted + ); + + // void spend. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(10) + )); + assert_ok!(Treasury::void_spend(RuntimeOrigin::root(), 1)); + assert_eq!(Spends::::get(1), None); + }); +} + +#[test] +fn check_status_works() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(::PayoutPeriod::get(), 5); + System::set_block_number(1); + + // spend `0` expired and can be removed. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + System::set_block_number(7); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 0).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::::SpendProcessed { index: 0 }.into()); + + // spend `1` payment failed and expired hence can be removed. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_noop!( + Treasury::check_status(RuntimeOrigin::signed(1), 1), + Error::::NotAttempted + ); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + let payment_id = get_payment_id(1).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::Failure); + // spend expired. + System::set_block_number(13); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 1).unwrap(); + assert_eq!(info.pays_fee, Pays::Yes); + System::assert_last_event(Event::::PaymentFailed { index: 1, payment_id }.into()); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 1).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::::SpendProcessed { index: 1 }.into()); + + // spend `2` payment succeed. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 2)); + let payment_id = get_payment_id(2).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::Success); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 2).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::::SpendProcessed { index: 2 }.into()); + + // spend `3` payment in process. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 3)); + let payment_id = get_payment_id(3).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::InProgress); + assert_noop!( + Treasury::check_status(RuntimeOrigin::signed(1), 3), + Error::::Inconclusive + ); + + // spend `4` removed since the payment status is unknown. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 4)); + let payment_id = get_payment_id(4).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::Unknown); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 4).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::::SpendProcessed { index: 4 }.into()); + }); +} + +#[test] +fn try_state_proposals_invariant_1_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Add a proposal using `propose_spend` + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3) + }); + assert_eq!(Proposals::::iter().count(), 1); + assert_eq!(ProposalCount::::get(), 1); + // Check invariant 1 holds + assert!(ProposalCount::::get() as usize >= Proposals::::iter().count()); + // Break invariant 1 by decreasing `ProposalCount` + ProposalCount::::put(0); + // Invariant 1 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Actual number of proposals exceeds `ProposalCount`.")) + ); + }); +} + +#[test] +fn try_state_proposals_invariant_2_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Add a proposal using `propose_spend` + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3) + }); + assert_eq!(Proposals::::iter().count(), 1); + let current_proposal_count = ProposalCount::::get(); + assert_eq!(current_proposal_count, 1); + // Check invariant 2 holds + assert!( + Proposals::::iter_keys() + .all(|proposal_index| { + proposal_index < current_proposal_count + }) + ); + // Break invariant 2 by inserting the proposal under key = 1 + let proposal = Proposals::::take(0).unwrap(); + Proposals::::insert(1, proposal); + // Invariant 2 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`.")) + ); + }); +} + +#[test] +fn try_state_proposals_invariant_3_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Add a proposal using `propose_spend` + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 10, 3) + }); + assert_eq!(Proposals::::iter().count(), 1); + // Approve the proposal + assert_ok!({ + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }); + assert_eq!(Approvals::::get().len(), 1); + // Check invariant 3 holds + assert!(Approvals::::get() + .iter() + .all(|proposal_index| { Proposals::::contains_key(proposal_index) })); + // Break invariant 3 by adding another key to `Approvals` + let mut approvals_modified = Approvals::::get(); + approvals_modified.try_push(2).unwrap(); + Approvals::::put(approvals_modified); + // Invariant 3 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Proposal indices in `Approvals` must also be contained in `Proposals`.")) + ); + }); +} + +#[test] +fn try_state_spends_invariant_1_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Propose and approve a spend + assert_ok!({ + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + }); + assert_eq!(Spends::::iter().count(), 1); + assert_eq!(SpendCount::::get(), 1); + // Check invariant 1 holds + assert!(SpendCount::::get() as usize >= Spends::::iter().count()); + // Break invariant 1 by decreasing `SpendCount` + SpendCount::::put(0); + // Invariant 1 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Actual number of spends exceeds `SpendCount`.")) + ); + }); +} + +#[test] +fn try_state_spends_invariant_2_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Propose and approve a spend + assert_ok!({ + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + }); + assert_eq!(Spends::::iter().count(), 1); + let current_spend_count = SpendCount::::get(); + assert_eq!(current_spend_count, 1); + // Check invariant 2 holds + assert!( + Spends::::iter_keys() + .all(|spend_index| { + spend_index < current_spend_count + }) + ); + // Break invariant 2 by inserting the spend under key = 1 + let spend = Spends::::take(0).unwrap(); + Spends::::insert(1, spend); + // Invariant 2 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`.")) + ); + }); +} + +#[test] +fn try_state_spends_invariant_3_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Propose and approve a spend + assert_ok!({ + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + }); + assert_eq!(Spends::::iter().count(), 1); + let current_spend_count = SpendCount::::get(); + assert_eq!(current_spend_count, 1); + // Check invariant 3 holds + assert!(Spends::::iter_values() + .all(|SpendStatus { valid_from, expire_at, .. }| { valid_from < expire_at })); + // Break invariant 3 by reversing spend.expire_at and spend.valid_from + let spend = Spends::::take(0).unwrap(); + Spends::::insert( + 0, + SpendStatus { valid_from: spend.expire_at, expire_at: spend.valid_from, ..spend }, + ); + // Invariant 3 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Spend cannot expire before it becomes valid.")) + ); + }); +} diff --git a/pallets/treasury/src/weights.rs b/pallets/treasury/src/weights.rs new file mode 100644 index 0000000000..82277e2d28 --- /dev/null +++ b/pallets/treasury/src/weights.rs @@ -0,0 +1,367 @@ +// 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 32.0.0 +//! DATE: 2024-04-09, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-anb7yjbi-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/production/substrate-node +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_treasury +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./substrate/frame/treasury/src/weights.rs +// --header=./substrate/HEADER-APACHE2 +// --template=./substrate/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_treasury`. +pub trait WeightInfo { + fn spend_local() -> 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; + fn spend() -> Weight; + fn payout() -> Weight; + fn check_status() -> Weight; + fn void_spend() -> 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_local() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1887` + // Minimum execution time: 11_910_000 picoseconds. + Weight::from_parts(12_681_000, 1887) + .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: `177` + // Estimated: `1489` + // Minimum execution time: 24_704_000 picoseconds. + Weight::from_parts(25_484_000, 1489) + .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: `335` + // Estimated: `3593` + // Minimum execution time: 26_632_000 picoseconds. + Weight::from_parts(27_325_000, 3593) + .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: `504 + p * (8 ±0)` + // Estimated: `3573` + // Minimum execution time: 8_436_000 picoseconds. + Weight::from_parts(11_268_438, 3573) + // Standard Error: 1_039 + .saturating_add(Weight::from_parts(70_903, 0).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: `161` + // Estimated: `1887` + // Minimum execution time: 6_372_000 picoseconds. + Weight::from_parts(6_567_000, 1887) + .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:99 w:99) + /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:198 w:198) + /// 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, 99]`. + fn on_initialize_proposals(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `451 + p * (251 ±0)` + // Estimated: `1887 + p * (5206 ±0)` + // Minimum execution time: 33_150_000 picoseconds. + Weight::from_parts(41_451_020, 1887) + // Standard Error: 19_018 + .saturating_add(Weight::from_parts(34_410_759, 0).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_parts(0, 5206).saturating_mul(p.into())) + } + /// Storage: `AssetRate::ConversionRateToNative` (r:1 w:0) + /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Treasury::SpendCount` (r:1 w:1) + /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Treasury::Spends` (r:0 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `3501` + // Minimum execution time: 14_233_000 picoseconds. + Weight::from_parts(14_842_000, 3501) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:1 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:2 w:2) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `709` + // Estimated: `6208` + // Minimum execution time: 58_857_000 picoseconds. + Weight::from_parts(61_291_000, 6208) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn check_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `198` + // Estimated: `3538` + // Minimum execution time: 12_116_000 picoseconds. + Weight::from_parts(12_480_000, 3538) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn void_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `198` + // Estimated: `3538` + // Minimum execution time: 10_834_000 picoseconds. + Weight::from_parts(11_427_000, 3538) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// 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_local() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1887` + // Minimum execution time: 11_910_000 picoseconds. + Weight::from_parts(12_681_000, 1887) + .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: `177` + // Estimated: `1489` + // Minimum execution time: 24_704_000 picoseconds. + Weight::from_parts(25_484_000, 1489) + .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: `335` + // Estimated: `3593` + // Minimum execution time: 26_632_000 picoseconds. + Weight::from_parts(27_325_000, 3593) + .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: `504 + p * (8 ±0)` + // Estimated: `3573` + // Minimum execution time: 8_436_000 picoseconds. + Weight::from_parts(11_268_438, 3573) + // Standard Error: 1_039 + .saturating_add(Weight::from_parts(70_903, 0).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: `161` + // Estimated: `1887` + // Minimum execution time: 6_372_000 picoseconds. + Weight::from_parts(6_567_000, 1887) + .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:99 w:99) + /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:198 w:198) + /// 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, 99]`. + fn on_initialize_proposals(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `451 + p * (251 ±0)` + // Estimated: `1887 + p * (5206 ±0)` + // Minimum execution time: 33_150_000 picoseconds. + Weight::from_parts(41_451_020, 1887) + // Standard Error: 19_018 + .saturating_add(Weight::from_parts(34_410_759, 0).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_parts(0, 5206).saturating_mul(p.into())) + } + /// Storage: `AssetRate::ConversionRateToNative` (r:1 w:0) + /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Treasury::SpendCount` (r:1 w:1) + /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Treasury::Spends` (r:0 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `3501` + // Minimum execution time: 14_233_000 picoseconds. + Weight::from_parts(14_842_000, 3501) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:1 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:2 w:2) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `709` + // Estimated: `6208` + // Minimum execution time: 58_857_000 picoseconds. + Weight::from_parts(61_291_000, 6208) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn check_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `198` + // Estimated: `3538` + // Minimum execution time: 12_116_000 picoseconds. + Weight::from_parts(12_480_000, 3538) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Treasury::Spends` (r:1 w:1) + /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + fn void_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `198` + // Estimated: `3538` + // Minimum execution time: 10_834_000 picoseconds. + Weight::from_parts(11_427_000, 3538) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 2a3a70a143..83908150cc 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -35,13 +35,13 @@ pallet-session = { workspace = true } pallet-sudo = { workspace = true } pallet-timestamp = { workspace = true } pallet-transaction-payment = { workspace = true } -pallet-treasury = { workspace = true } pallet-utility = { workspace = true } pallet-proxy = { workspace = true } # cumulus cumulus-primitives-core = { workspace = true } pallet-collator-selection = { workspace = true } pallet-time-release = { path = "../../pallets/time-release", default-features = false } +pallet-treasury = { path = "../../pallets/treasury", default-features = false } [features] default = ["std"] diff --git a/runtime/common/src/weights/mod.rs b/runtime/common/src/weights/mod.rs index 01abce7988..c305f149bb 100644 --- a/runtime/common/src/weights/mod.rs +++ b/runtime/common/src/weights/mod.rs @@ -32,6 +32,5 @@ pub mod pallet_scheduler; pub mod pallet_session; pub mod pallet_timestamp; pub mod pallet_transaction_payment; -pub mod pallet_treasury; pub mod pallet_utility; pub mod rocksdb_weights; diff --git a/runtime/common/src/weights/pallet_treasury.rs b/runtime/common/src/weights/pallet_treasury.rs deleted file mode 100644 index 013bc02585..0000000000 --- a/runtime/common/src/weights/pallet_treasury.rs +++ /dev/null @@ -1,171 +0,0 @@ -// 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 32.0.0 -//! DATE: 2024-04-09, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-anb7yjbi-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` - -// Executed Command: -// ./target/production/substrate-node -// benchmark -// pallet -// --chain=dev -// --steps=50 -// --repeat=20 -// --pallet=pallet_treasury -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* -// --wasm-execution=compiled -// --heap-pages=4096 -// --output=./substrate/frame/treasury/src/weights.rs -// --header=./substrate/HEADER-APACHE2 -// --template=./substrate/.maintain/frame-weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for `pallet_treasury`. -pub trait WeightInfo { - fn spend_local() -> Weight; - fn remove_approval() -> Weight; - fn on_initialize_proposals(p: u32, ) -> Weight; - fn spend() -> Weight; - fn payout() -> Weight; - fn check_status() -> Weight; - fn void_spend() -> Weight; -} - -/// Weights for `pallet_treasury` using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl pallet_treasury::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_local() -> Weight { - // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `1887` - // Minimum execution time: 11_910_000 picoseconds. - Weight::from_parts(12_681_000, 1887) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(3_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: `161` - // Estimated: `1887` - // Minimum execution time: 6_372_000 picoseconds. - Weight::from_parts(6_567_000, 1887) - .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:99 w:99) - /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:198 w:198) - /// 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, 99]`. - fn on_initialize_proposals(p: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `451 + p * (251 ±0)` - // Estimated: `1887 + p * (5206 ±0)` - // Minimum execution time: 33_150_000 picoseconds. - Weight::from_parts(41_451_020, 1887) - // Standard Error: 19_018 - .saturating_add(Weight::from_parts(34_410_759, 0).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_parts(0, 5206).saturating_mul(p.into())) - } - /// Storage: `AssetRate::ConversionRateToNative` (r:1 w:0) - /// Proof: `AssetRate::ConversionRateToNative` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) - /// Storage: `Treasury::SpendCount` (r:1 w:1) - /// Proof: `Treasury::SpendCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Treasury::Spends` (r:0 w:1) - /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) - fn spend() -> Weight { - // Proof Size summary in bytes: - // Measured: `140` - // Estimated: `3501` - // Minimum execution time: 14_233_000 picoseconds. - Weight::from_parts(14_842_000, 3501) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `Treasury::Spends` (r:1 w:1) - /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) - /// Storage: `Assets::Asset` (r:1 w:1) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Assets::Account` (r:2 w:2) - /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn payout() -> Weight { - // Proof Size summary in bytes: - // Measured: `709` - // Estimated: `6208` - // Minimum execution time: 58_857_000 picoseconds. - Weight::from_parts(61_291_000, 6208) - .saturating_add(RocksDbWeight::get().reads(5_u64)) - .saturating_add(RocksDbWeight::get().writes(5_u64)) - } - /// Storage: `Treasury::Spends` (r:1 w:1) - /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) - fn check_status() -> Weight { - // Proof Size summary in bytes: - // Measured: `198` - // Estimated: `3538` - // Minimum execution time: 12_116_000 picoseconds. - Weight::from_parts(12_480_000, 3538) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `Treasury::Spends` (r:1 w:1) - /// Proof: `Treasury::Spends` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) - fn void_spend() -> Weight { - // Proof Size summary in bytes: - // Measured: `198` - // Estimated: `3538` - // Minimum execution time: 10_834_000 picoseconds. - Weight::from_parts(11_427_000, 3538) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } -} diff --git a/runtime/common/src/weights/pallet_utility.rs b/runtime/common/src/weights/pallet_utility.rs index d4d4e12e2b..7ec9d4019a 100644 --- a/runtime/common/src/weights/pallet_utility.rs +++ b/runtime/common/src/weights/pallet_utility.rs @@ -78,26 +78,3 @@ impl pallet_utility::WeightInfo for SubstrateWeight } } - -#[cfg(test)] -mod tests { - use frame_support::{traits::Get, weights::Weight, dispatch::DispatchClass}; - use crate::constants::{MAXIMUM_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO}; - use crate::weights::extrinsic_weights::ExtrinsicBaseWeight; - - struct BlockWeights; - impl Get for BlockWeights { - fn get() -> frame_system::limits::BlockWeights { - frame_system::limits::BlockWeights::builder() - .base_block(Weight::zero()) - .for_class(DispatchClass::all(), |weights| { - weights.base_extrinsic = ExtrinsicBaseWeight::get().into(); - }) - .for_class(DispatchClass::non_mandatory(), |weights| { - weights.max_total = Some(NORMAL_DISPATCH_RATIO * MAXIMUM_BLOCK_WEIGHT); - }) - .build_or_panic() - } - } - -} diff --git a/runtime/frequency/Cargo.toml b/runtime/frequency/Cargo.toml index 45c8005bd8..55bc89ea13 100644 --- a/runtime/frequency/Cargo.toml +++ b/runtime/frequency/Cargo.toml @@ -48,7 +48,6 @@ pallet-multisig = { workspace = true } pallet-timestamp = { workspace = true } pallet-transaction-payment = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } -pallet-treasury = { workspace = true } pallet-utility = { workspace = true } pallet-proxy = { workspace = true } @@ -62,14 +61,14 @@ sp-genesis-builder = { workspace = true } static_assertions = { workspace = true } sp-offchain = { workspace = true } -# turn on features = ["serde"]? see psdk bridges/primitives/runtime/Cargo.toml for 1.14 upgrade, but it was in 1.13 so maybe not -sp-runtime = { workspace = true } +sp-runtime = { features = ["serde"], workspace = true } sp-session = { workspace = true } sp-std = { workspace = true } sp-transaction-pool = { workspace = true } sp-version = { workspace = true } # Ported Pallet from ORML pallet-time-release = { path = "../../pallets/time-release", default-features = false } +pallet-treasury = { path = "../../pallets/treasury", default-features = false } # Frequency common-primitives = { default-features = false, path = "../../common/primitives" } common-runtime = { path = "../common", default-features = false } diff --git a/runtime/frequency/src/lib.rs b/runtime/frequency/src/lib.rs index e543e88b1c..3c2840a012 100644 --- a/runtime/frequency/src/lib.rs +++ b/runtime/frequency/src/lib.rs @@ -22,14 +22,14 @@ use common_runtime::constants::currency::UNITS; use cumulus_pallet_parachain_system::{ DefaultCoreSelector, RelayNumberMonotonicallyIncreases, RelaychainDataProvider, }; -use frame_support::traits::{EitherOf, MapSuccess}; -use frame_system::EnsureRootWithSuccess; +#[cfg(any(feature = "runtime-benchmarks", feature = "test"))] +use frame_support::traits::MapSuccess; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; +#[cfg(any(feature = "runtime-benchmarks", feature = "test"))] +use sp_runtime::traits::Replace; use sp_runtime::{ generic, impl_opaque_keys, - traits::{ - AccountIdConversion, BlakeTwo256, Block as BlockT, ConvertInto, IdentityLookup, Replace, - }, + traits::{AccountIdConversion, BlakeTwo256, Block as BlockT, ConvertInto, IdentityLookup}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, DispatchError, }; @@ -946,7 +946,15 @@ impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; type RuntimeEvent = RuntimeEvent; - type WeightInfo = weights::pallet_treasury::SubstrateWeight; + type WeightInfo = pallet_treasury::weights::SubstrateWeight; + + /// Who approves treasury proposals? + /// - Root (sudo or governance) + /// - 3/5ths of the Frequency Council + type ApproveOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, + >; /// Who rejects treasury proposals? /// - Root (sudo or governance) @@ -956,19 +964,26 @@ impl pallet_treasury::Config for Runtime { pallet_collective::EnsureProportionMoreThan, >; - // Legacy config was as following: - // type ApproveOrigin = EitherOfDiverse< - // EnsureRoot, - // pallet_collective::EnsureProportionAtLeast, - // >; - /// Spending Origin - type SpendOrigin = EitherOf< - EnsureRootWithSuccess, - MapSuccess< - pallet_collective::EnsureProportionAtLeast, - Replace, - >, - >; + /// Spending funds outside of the proposal? + /// Nobody + #[cfg(not(feature = "runtime-benchmarks"))] + type SpendOrigin = frame_support::traits::NeverEnsureOrigin; + #[cfg(feature = "runtime-benchmarks")] + type SpendOrigin = MapSuccess, Replace>; + + /// Rejected proposals lose their bond + /// This takes the slashed amount and is often set to the Treasury + /// We burn it so there is no incentive to the treasury to reject to enrich itself + type OnSlash = (); + + /// Bond 5% of a treasury proposal + type ProposalBond = ProposalBondPercent; + + /// Minimum bond of 100 Tokens + type ProposalBondMinimum = ProposalBondMinimum; + + /// Max bond of 1_000 Tokens + type ProposalBondMaximum = ProposalBondMaximum; /// Pay out on a 4-week basis type SpendPeriod = SpendPeriod; @@ -996,7 +1011,6 @@ impl pallet_treasury::Config for Runtime { type PayoutPeriod = PayoutSpendPeriod; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); - type BlockNumberProvider = System; } // See https://paritytech.github.io/substrate/master/pallet_transaction_payment/index.html for @@ -1380,7 +1394,6 @@ mod benches { [pallet_collective, TechnicalCommittee] [pallet_preimage, Preimage] [pallet_democracy, Democracy] - [pallet_treasury, Treasury] [pallet_scheduler, Scheduler] [pallet_session, SessionBench::] [pallet_timestamp, Timestamp] @@ -1397,6 +1410,7 @@ mod benches { [pallet_stateful_storage, StatefulStorage] [pallet_handles, Handles] [pallet_time_release, TimeRelease] + [pallet_treasury, Treasury] [pallet_capacity, Capacity] [pallet_frequency_tx_payment, FrequencyTxPayment] [pallet_passkey, Passkey] diff --git a/scripts/run_benchmarks.sh b/scripts/run_benchmarks.sh index 7f587eb3a8..b94a8995c3 100755 --- a/scripts/run_benchmarks.sh +++ b/scripts/run_benchmarks.sh @@ -16,7 +16,6 @@ ALL_EXTERNAL_PALLETS=( \ pallet_scheduler \ pallet_session \ pallet_timestamp \ - pallet_treasury \ pallet_transaction_payment \ pallet_utility \ pallet_proxy \ @@ -31,6 +30,7 @@ ALL_CUSTOM_PALLETS=( \ capacity \ frequency-tx-payment \ passkey \ + treasury \ ) declare -a CUSTOM_PALLETS