diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8bc1f3664..06135aa18 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -103,7 +103,7 @@ jobs:
chmod +x ./try-runtime
- name: Try Runtime for Laos Sigma
run: |
- RUST_LOG=try-runtime,info ./try-runtime --runtime ./target/release/wbuild/laos-runtime/laos_runtime.wasm on-runtime-upgrade --disable-spec-version-check --checks=all live --uri ws://159.223.241.51:9944
+ RUST_LOG=try-runtime,info ./try-runtime --runtime ./target/release/wbuild/laos-runtime/laos_runtime.wasm on-runtime-upgrade --disable-spec-version-check --checks=all live --uri wss://rpc.laossigma.laosfoundation.io
- name: Try Runtime for Laos
run: |
RUST_LOG=try-runtime,info ./try-runtime --runtime ./target/release/wbuild/laos-runtime/laos_runtime.wasm on-runtime-upgrade --disable-spec-version-check --checks=all live --uri wss://rpc.laos.laosfoundation.io
diff --git a/Cargo.lock b/Cargo.lock
index 3b062de05..5da53f5d1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5043,7 +5043,7 @@ dependencies = [
[[package]]
name = "laos"
-version = "0.24.0"
+version = "0.24.1"
dependencies = [
"clap",
"cumulus-client-cli",
@@ -5128,7 +5128,7 @@ dependencies = [
[[package]]
name = "laos-runtime"
-version = "0.24.0"
+version = "0.24.1"
dependencies = [
"assert-json-diff",
"cumulus-pallet-aura-ext",
@@ -5189,7 +5189,7 @@ dependencies = [
"pallet-timestamp",
"pallet-transaction-payment",
"pallet-transaction-payment-rpc-runtime-api",
- "pallet-treasury",
+ "pallet-treasury 36.0.1",
"pallet-utility",
"pallet-vesting",
"pallet-xcm",
@@ -7199,7 +7199,7 @@ dependencies = [
"frame-support",
"frame-system",
"log",
- "pallet-treasury",
+ "pallet-treasury 36.0.1 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2407)",
"parity-scale-codec",
"scale-info",
"sp-core",
@@ -7235,7 +7235,7 @@ dependencies = [
"frame-system",
"log",
"pallet-bounties",
- "pallet-treasury",
+ "pallet-treasury 36.0.1 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2407)",
"parity-scale-codec",
"scale-info",
"sp-core",
@@ -8117,7 +8117,7 @@ dependencies = [
"frame-support",
"frame-system",
"log",
- "pallet-treasury",
+ "pallet-treasury 36.0.1 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2407)",
"parity-scale-codec",
"scale-info",
"serde",
@@ -8169,6 +8169,26 @@ dependencies = [
"sp-weights",
]
+[[package]]
+name = "pallet-treasury"
+version = "36.0.1"
+dependencies = [
+ "docify",
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "impl-trait-for-tuples",
+ "log",
+ "pallet-balances",
+ "pallet-utility",
+ "parity-scale-codec",
+ "scale-info",
+ "serde",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+]
+
[[package]]
name = "pallet-treasury"
version = "36.0.1"
@@ -9446,7 +9466,7 @@ dependencies = [
"pallet-staking-reward-fn",
"pallet-timestamp",
"pallet-transaction-payment",
- "pallet-treasury",
+ "pallet-treasury 36.0.1 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2407)",
"pallet-vesting",
"parity-scale-codec",
"polkadot-primitives",
@@ -10730,7 +10750,7 @@ dependencies = [
"pallet-tips",
"pallet-transaction-payment",
"pallet-transaction-payment-rpc-runtime-api",
- "pallet-treasury",
+ "pallet-treasury 36.0.1 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2407)",
"pallet-utility",
"pallet-vesting",
"pallet-whitelist",
@@ -15551,7 +15571,7 @@ dependencies = [
"pallet-timestamp",
"pallet-transaction-payment",
"pallet-transaction-payment-rpc-runtime-api",
- "pallet-treasury",
+ "pallet-treasury 36.0.1 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2407)",
"pallet-utility",
"pallet-vesting",
"pallet-whitelist",
diff --git a/Cargo.toml b/Cargo.toml
index a6828bfde..58725002b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ repository = "https://github.com/freeverseio/laos.git"
homepage = "https://www.laosfoundation.io"
authors = ["Freeverse"]
edition = "2021"
-version = "0.24.0"
+version = "0.24.1"
[workspace]
resolver = "2"
@@ -122,7 +122,7 @@ pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch
pallet-identity = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
pallet-democracy = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
pallet-elections-phragmen = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
-pallet-treasury = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
+## pallet-treasury = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
pallet-collective = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
pallet-membership = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false }
@@ -233,6 +233,7 @@ pallet-asset-metadata-extender = { path = "./pallets/asset-metadata-extender", d
pallet-parachain-staking = { path = "./pallets/parachain-staking", default-features = false }
pallet-evm-precompile-parachain-staking = { path = "./precompiles/parachain-staking", default-features = false }
pallet-precompiles-benchmark = { path = "./pallets/precompiles-benchmark", default-features = false}
+pallet-treasury = { path = "./pallets/treasury", default-features = false }
# Primitives
laos-primitives = { path = "./primitives", default-features = false }
diff --git a/e2e-tests/utils/constants.ts b/e2e-tests/utils/constants.ts
index d8958bb08..09b545718 100644
--- a/e2e-tests/utils/constants.ts
+++ b/e2e-tests/utils/constants.ts
@@ -8,7 +8,7 @@ import ParachainStaking from "../build/contracts/ParachainStaking.sol/ParachainS
// Runtime specs
export const RUNTIME_SPEC_NAME = "laos";
-export const RUNTIME_SPEC_VERSION = 2400;
+export const RUNTIME_SPEC_VERSION = 2401;
export const RUNTIME_IMPL_VERSION = 0;
// Nodes endpoints
diff --git a/pallets/treasury/Cargo.toml b/pallets/treasury/Cargo.toml
new file mode 100644
index 000000000..ac949a5c5
--- /dev/null
+++ b/pallets/treasury/Cargo.toml
@@ -0,0 +1,63 @@
+[package]
+name = "pallet-treasury"
+version = "36.0.1"
+authors.workspace = true
+edition.workspace = true
+license = "Apache-2.0"
+homepage = "https://substrate.io"
+repository.workspace = true
+description = "FRAME pallet to manage treasury (polkadot v1.15.0)"
+readme = "README.md"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+parity-scale-codec = { features = [
+ "derive",
+ "max-encoded-len",
+], workspace = true }
+docify = { version = "0.2.8" }
+impl-trait-for-tuples = { version = "0.2.2" }
+scale-info = { features = ["derive"], workspace = true }
+serde = { features = ["derive"], optional = true, workspace = true, default-features = true }
+frame-benchmarking = { optional = true, workspace = true }
+frame-support.workspace = true
+frame-system.workspace = true
+pallet-balances.workspace = true
+sp-runtime.workspace = true
+sp-core = { optional = true, workspace = true }
+log = { workspace = true }
+
+[dev-dependencies]
+sp-io = { default-features = true, workspace = true }
+pallet-utility = { default-features = true, workspace = true }
+
+[features]
+default = ["std"]
+std = [
+ "parity-scale-codec/std",
+ "frame-benchmarking?/std",
+ "frame-support/std",
+ "frame-system/std",
+ "log/std",
+ "pallet-balances/std",
+ "scale-info/std",
+ "serde",
+ "sp-core?/std",
+ "sp-runtime/std",
+]
+runtime-benchmarks = [
+ "dep:sp-core",
+ "frame-benchmarking/runtime-benchmarks",
+ "frame-support/runtime-benchmarks",
+ "frame-system/runtime-benchmarks",
+ "pallet-balances/runtime-benchmarks",
+ "sp-runtime/runtime-benchmarks",
+]
+try-runtime = [
+ "frame-support/try-runtime",
+ "frame-system/try-runtime",
+ "pallet-balances/try-runtime",
+ "sp-runtime/try-runtime",
+]
diff --git a/pallets/treasury/src/benchmarking.rs b/pallets/treasury/src/benchmarking.rs
new file mode 100644
index 000000000..d8372bbe4
--- /dev/null
+++ b/pallets/treasury/src/benchmarking.rs
@@ -0,0 +1,368 @@
+// Copyright 2023-2024 Freeverse.io
+// This file is part of LAOS.
+
+// LAOS is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// LAOS is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with LAOS. If not, see .
+
+//! Treasury pallet benchmarking.
+
+#![cfg(feature = "runtime-benchmarks")]
+
+use super::{Pallet as Treasury, *};
+
+use frame_benchmarking::{
+ v1::{account, BenchmarkError},
+ v2::*,
+};
+use frame_support::{
+ assert_err, assert_ok, 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 `spend_local`.
+fn setup_proposal, I: 'static>(
+ u: u32,
+) -> (T::AccountId, BalanceOf, AccountIdLookupOf) {
+ let caller = account("caller", u, SEED);
+ let value: BalanceOf = T::Currency::minimum_balance() * 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> {
+ let spender = T::SpendOrigin::try_successful_origin();
+
+ for i in 0..n {
+ let (_, value, lookup) = setup_proposal::(i);
+
+ if let Ok(origin) = &spender {
+ Treasury::::spend_local(origin.clone(), value, lookup)?;
+ }
+ }
+
+ if spender.is_ok() {
+ ensure!(Approvals::::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.
+#[allow(clippy::type_complexity)]
+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 remove_approval() -> Result<(), BenchmarkError> {
+ let (spend_exists, proposal_id) =
+ if let Ok(origin) = T::SpendOrigin::try_successful_origin() {
+ let (_, value, beneficiary_lookup) = setup_proposal::(SEED);
+ Treasury::::spend_local(origin, value, beneficiary_lookup)?;
+ let proposal_id = Treasury::::proposal_count() - 1;
+
+ (true, proposal_id)
+ } else {
+ (false, 0)
+ };
+
+ let reject_origin =
+ T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+
+ #[block]
+ {
+ let res =
+ Treasury::::remove_approval(reject_origin as T::RuntimeOrigin, proposal_id);
+
+ if spend_exists {
+ assert_ok!(res);
+ } else {
+ assert_err!(res, Error::::ProposalNotApproved);
+ }
+ }
+
+ 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(())
+ }
+
+ /// 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() -> 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 (asset_kind, amount, beneficiary, beneficiary_lookup) =
+ create_spend_arguments::(SEED);
+ T::BalanceConverter::ensure_successful(asset_kind.clone());
+
+ let spend_exists = if let Ok(origin) = T::SpendOrigin::try_successful_origin() {
+ Treasury::::spend(
+ origin,
+ Box::new(asset_kind.clone()),
+ amount,
+ Box::new(beneficiary_lookup),
+ None,
+ )?;
+
+ true
+ } else {
+ false
+ };
+
+ T::Paymaster::ensure_successful(&beneficiary, asset_kind, amount);
+ let caller: T::AccountId = account("caller", 0, SEED);
+
+ #[block]
+ {
+ let res = Treasury::::payout(RawOrigin::Signed(caller.clone()).into(), 0u32);
+
+ if spend_exists {
+ assert_ok!(res);
+ } else {
+ assert_err!(res, crate::Error::::InvalidIndex);
+ }
+ }
+
+ if spend_exists {
+ 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 (asset_kind, amount, beneficiary, beneficiary_lookup) =
+ create_spend_arguments::(SEED);
+
+ T::BalanceConverter::ensure_successful(asset_kind.clone());
+ T::Paymaster::ensure_successful(&beneficiary, asset_kind.clone(), amount);
+ let caller: T::AccountId = account("caller", 0, SEED);
+
+ let spend_exists = if let Ok(origin) = T::SpendOrigin::try_successful_origin() {
+ Treasury::::spend(
+ origin,
+ Box::new(asset_kind),
+ amount,
+ Box::new(beneficiary_lookup),
+ None,
+ )?;
+
+ 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"),
+ };
+
+ true
+ } else {
+ false
+ };
+
+ #[block]
+ {
+ let res =
+ Treasury::::check_status(RawOrigin::Signed(caller.clone()).into(), 0u32);
+
+ if spend_exists {
+ assert_ok!(res);
+ } else {
+ assert_err!(res, crate::Error::::InvalidIndex);
+ }
+ }
+
+ if let Some(s) = Spends::::get(0) {
+ assert!(!matches!(s.status, PaymentState::Attempted { .. }));
+ }
+
+ Ok(())
+ }
+
+ #[benchmark]
+ fn void_spend() -> Result<(), BenchmarkError> {
+ let (asset_kind, amount, _, beneficiary_lookup) = create_spend_arguments::(SEED);
+ T::BalanceConverter::ensure_successful(asset_kind.clone());
+ let spend_exists = if let Ok(origin) = T::SpendOrigin::try_successful_origin() {
+ Treasury::::spend(
+ origin,
+ Box::new(asset_kind.clone()),
+ amount,
+ Box::new(beneficiary_lookup),
+ None,
+ )?;
+ assert!(Spends::::get(0).is_some());
+
+ true
+ } else {
+ false
+ };
+
+ let origin =
+ T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+
+ #[block]
+ {
+ let res = Treasury::::void_spend(origin as T::RuntimeOrigin, 0u32);
+
+ if spend_exists {
+ assert_ok!(res);
+ } else {
+ assert_err!(res, crate::Error::::InvalidIndex);
+ }
+ }
+
+ assert!(Spends::::get(0).is_none());
+ Ok(())
+ }
+
+ impl_benchmark_test_suite!(
+ Treasury,
+ crate::tests::ExtBuilder::default().build(),
+ crate::tests::Test
+ );
+
+ mod no_spend_origin_tests {
+ use super::*;
+
+ impl_benchmark_test_suite!(
+ Treasury,
+ crate::tests::ExtBuilder::default().spend_origin_succesful_origin_err().build(),
+ crate::tests::Test,
+ benchmarks_path = benchmarking
+ );
+ }
+}
diff --git a/pallets/treasury/src/lib.rs b/pallets/treasury/src/lib.rs
new file mode 100644
index 000000000..a84c1adba
--- /dev/null
+++ b/pallets/treasury/src/lib.rs
@@ -0,0 +1,979 @@
+// Copyright 2023-2024 Freeverse.io
+// This file is part of LAOS.
+
+// LAOS is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// LAOS is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with LAOS. If not, see .
+//!
+//! # 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)]
+
+mod benchmarking;
+#[cfg(test)]
+mod tests;
+pub mod weights;
+use core::marker::PhantomData;
+
+#[cfg(feature = "runtime-benchmarks")]
+pub use benchmarking::ArgumentsFactory;
+
+extern crate alloc;
+pub mod migration;
+
+use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
+use scale_info::TypeInfo;
+
+use alloc::{boxed::Box, collections::btree_map::BTreeMap};
+use sp_runtime::{
+ traits::{AccountIdConversion, CheckedAdd, Saturating, StaticLookup, Zero},
+ Permill, RuntimeDebug,
+};
+
+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 rejections must come.
+ type RejectOrigin: EnsureOrigin;
+
+ /// The overarching event type.
+ type RuntimeEvent: From>
+ + IsType<::RuntimeEvent>;
+
+ /// 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: core::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 = ()> {
+ /// 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 },
+ /// 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 {
+ /// 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 {
+ /// 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()
+ }
+
+ /// 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 > 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.
+ 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/migration.rs b/pallets/treasury/src/migration.rs
new file mode 100644
index 000000000..af02f83fc
--- /dev/null
+++ b/pallets/treasury/src/migration.rs
@@ -0,0 +1,133 @@
+// Copyright 2023-2024 Freeverse.io
+// This file is part of LAOS.
+
+// LAOS is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// LAOS is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with LAOS. If not, see .
+
+//! Treasury pallet migrations.
+use super::*;
+use alloc::collections::BTreeSet;
+#[cfg(feature = "try-runtime")]
+use alloc::vec::Vec;
+use core::marker::PhantomData;
+use frame_support::{
+ defensive,
+ traits::{Get, OnRuntimeUpgrade},
+};
+
+/// The log target for this pallet.
+const LOG_TARGET: &str = "runtime::treasury";
+
+pub mod cleanup_proposals {
+ use super::*;
+
+ /// Migration to cleanup unapproved proposals to return the bonds back to the proposers.
+ /// Proposals can no longer be created and the `Proposal` storage item will be removed in the
+ /// future.
+ ///
+ /// `UnreserveWeight` returns `Weight` of `unreserve_balance` operation which is perfomed during
+ /// this migration.
+ pub struct Migration(PhantomData<(T, I, UnreserveWeight)>);
+
+ impl, I: 'static, UnreserveWeight: Get> OnRuntimeUpgrade
+ for Migration
+ {
+ fn on_runtime_upgrade() -> frame_support::weights::Weight {
+ let mut approval_index = BTreeSet::new();
+ for approval in Approvals::::get().iter() {
+ approval_index.insert(*approval);
+ }
+
+ let mut proposals_processed = 0;
+ for (proposal_index, p) in Proposals::::iter() {
+ if !approval_index.contains(&proposal_index) {
+ let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
+ if err_amount.is_zero() {
+ Proposals::::remove(proposal_index);
+ log::info!(
+ target: LOG_TARGET,
+ "Released bond amount of {:?} to proposer {:?}",
+ p.bond,
+ p.proposer,
+ );
+ } else {
+ defensive!(
+ "err_amount is non zero for proposal {:?}",
+ (proposal_index, err_amount)
+ );
+ Proposals::::mutate_extant(proposal_index, |proposal| {
+ proposal.value = err_amount;
+ });
+ log::info!(
+ target: LOG_TARGET,
+ "Released partial bond amount of {:?} to proposer {:?}",
+ p.bond - err_amount,
+ p.proposer,
+ );
+ }
+ proposals_processed += 1;
+ }
+ }
+
+ log::info!(
+ target: LOG_TARGET,
+ "Migration for pallet-treasury finished, released {} proposal bonds.",
+ proposals_processed,
+ );
+
+ // calculate and return migration weights
+ let approvals_read = 1;
+ T::DbWeight::get()
+ .reads_writes(proposals_processed + approvals_read, proposals_processed) +
+ UnreserveWeight::get() * proposals_processed
+ }
+
+ #[cfg(feature = "try-runtime")]
+ fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> {
+ let value = (
+ Proposals::::iter_values().count() as u32,
+ Approvals::::get().len() as u32,
+ );
+ log::info!(
+ target: LOG_TARGET,
+ "Proposals and Approvals count {:?}",
+ value,
+ );
+ Ok(value.encode())
+ }
+
+ #[cfg(feature = "try-runtime")]
+ fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> {
+ let (old_proposals_count, old_approvals_count) =
+ <(u32, u32)>::decode(&mut &state[..]).expect("Known good");
+ let new_proposals_count = Proposals::::iter_values().count() as u32;
+ let new_approvals_count = Approvals::::get().len() as u32;
+
+ log::info!(
+ target: LOG_TARGET,
+ "Proposals and Approvals count {:?}",
+ (new_proposals_count, new_approvals_count),
+ );
+
+ ensure!(
+ new_proposals_count <= old_proposals_count,
+ "Proposals after migration should be less or equal to old proposals"
+ );
+ ensure!(
+ new_approvals_count == old_approvals_count,
+ "Approvals after migration should remain the same"
+ );
+ Ok(())
+ }
+ }
+}
diff --git a/pallets/treasury/src/tests.rs b/pallets/treasury/src/tests.rs
new file mode 100644
index 000000000..32e807931
--- /dev/null
+++ b/pallets/treasury/src/tests.rs
@@ -0,0 +1,964 @@
+// Copyright 2023-2024 Freeverse.io
+// This file is part of LAOS.
+
+// LAOS is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// LAOS is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with LAOS. If not, see .
+
+//! 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;
+}
+
+#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
+impl pallet_balances::Config for Test {
+ type AccountStore = System;
+}
+
+impl pallet_utility::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type RuntimeCall = RuntimeCall;
+ type PalletsOrigin = OriginCaller;
+ type WeightInfo = ();
+}
+
+thread_local! {
+ pub static PAID: RefCell> = const {RefCell::new(BTreeMap::new())};
+ pub static STATUS: RefCell> = const {RefCell::new(BTreeMap::new())};
+ pub static LAST_ID: RefCell = const {RefCell::new(0u64)};
+
+ #[cfg(feature = "runtime-benchmarks")]
+ pub static TEST_SPEND_ORIGIN_TRY_SUCCESFUL_ORIGIN_ERR: RefCell = const {RefCell::new(false)};
+}
+
+/// 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 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),
+ frame_system::RawOrigin::Signed(14) => Ok(500),
+ r => Err(RuntimeOrigin::from(r)),
+ })
+ }
+ #[cfg(feature = "runtime-benchmarks")]
+ fn try_successful_origin() -> Result {
+ if TEST_SPEND_ORIGIN_TRY_SUCCESFUL_ORIGIN_ERR.with(|i| *i.borrow()) {
+ Err(())
+ } else {
+ Ok(frame_system::RawOrigin::Root.into())
+ }
+ }
+}
+
+pub struct MulBy(PhantomData);
+impl> ConversionFromAssetBalance for MulBy {
+ type Error = ();
+ fn from_asset_balance(balance: u64, _asset_id: u32) -> Result {
+ 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 RejectOrigin = frame_system::EnsureRoot;
+ type RuntimeEvent = RuntimeEvent;
+ 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