Skip to content

Commit

Permalink
Treasury spends various asset kinds (paritytech#1333)
Browse files Browse the repository at this point in the history
### Summary 

This PR introduces new dispatchables to the treasury pallet, allowing
spends of various asset types. The enhanced features of the treasury
pallet, in conjunction with the asset-rate pallet, are set up and
enabled for Westend and Rococo.

### Westend and Rococo runtimes.

Polkadot/Kusams/Rococo Treasury can accept proposals for `spends` of
various asset kinds by specifying the asset's location and ID.

#### Treasury Instance New Dispatchables:
- `spend(AssetKind, AssetBalance, Beneficiary, Option<ValidFrom>)` -
propose and approve a spend;
- `payout(SpendIndex)` - payout an approved spend or retry a failed
payout
- `check_payment(SpendIndex)` - check the status of a payout;
- `void_spend(SpendIndex)` - void previously approved spend;
> existing spend dispatchable renamed to spend_local

in this context, the `AssetKind` parameter contains the asset's location
and it's corresponding `asset_id`, for example:
`USDT` on `AssetHub`,
``` rust
location = MultiLocation(0, X1(Parachain(1000)))
asset_id = MultiLocation(0, X2(PalletInstance(50), GeneralIndex(1984)))
```

the `Beneficiary` parameter is a `MultiLocation` in the context of the
asset's location, for example
``` rust
// the Fellowship salary pallet's location / account
FellowshipSalaryPallet = MultiLocation(1, X2(Parachain(1001), PalletInstance(64)))
// or custom `AccountId`
Alice = MultiLocation(0, AccountId32(network: None, id: [1,...]))
```

the `AssetBalance` represents the amount of the `AssetKind` to be
transferred to the `Beneficiary`. For permission checks, the asset
amount is converted to the native amount and compared against the
maximum spendable amount determined by the commanding spend origin.

the `spend` dispatchable allows for batching spends with different
`ValidFrom` arguments, enabling milestone-based spending. If the
expectations tied to an approved spend are not met, it is possible to
void the spend later using the `void_spend` dispatchable.

Asset Rate Pallet provides the conversion rate from the `AssetKind` to
the native balance.

#### Asset Rate Instance Dispatchables:
- `create(AssetKind, Rate)` - initialize a conversion rate to the native
balance for the given asset
- `update(AssetKind, Rate)` - update the conversion rate to the native
balance for the given asset
- `remove(AssetKind)` - remove an existing conversion rate to the native
balance for the given asset

the pallet's dispatchables can be executed by the Root or Treasurer
origins.

### Treasury Pallet

Treasury Pallet can accept proposals for `spends` of various asset kinds
and pay them out through the implementation of the `Pay` trait.

New Dispatchables:
- `spend(Config::AssetKind, AssetBalance, Config::Beneficiary,
Option<ValidFrom>)` - propose and approve a spend;
- `payout(SpendIndex)` - payout an approved spend or retry a failed
payout;
- `check_payment(SpendIndex)` - check the status of a payout;
- `void_spend(SpendIndex)` - void previously approved spend;
> existing spend dispatchable renamed to spend_local

The parameters' types of the `spend` dispatchable exposed via the
pallet's `Config` and allows to propose and accept a spend of a certain
amount.

An approved spend can be claimed via the `payout` within the
`Config::SpendPeriod`. Clients provide an implementation of the `Pay`
trait which can pay an asset of the `AssetKind` to the `Beneficiary` in
`AssetBalance` units.

The implementation of the Pay trait might not have an immediate final
payment status, for example if implemented over `XCM` and the actual
transfer happens on a remote chain.

The `check_status` dispatchable can be executed to update the spend's
payment state and retry the `payout` if the payment has failed.

---------

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: command-bot <>
  • Loading branch information
muharem and joepetrowski committed Oct 7, 2023
1 parent 19f6ced commit 0d2b961
Show file tree
Hide file tree
Showing 14 changed files with 1,392 additions and 164 deletions.
11 changes: 10 additions & 1 deletion substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use frame_support::{
parameter_types,
traits::{
fungible::{Balanced, Credit, HoldConsideration, ItemOf},
tokens::{nonfungibles_v2::Inspect, GetSalary, PayFromAccount},
tokens::{nonfungibles_v2::Inspect, pay::PayAssetFromAccount, GetSalary, PayFromAccount},
AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Contains, Currency,
EitherOfDiverse, EqualPrivilegeOnly, Imbalance, InsideBoth, InstanceFilter,
KeyOwnerProofSystem, LinearStoragePrice, LockIdentifier, Nothing, OnUnbalanced,
Expand Down Expand Up @@ -1186,6 +1186,7 @@ parameter_types! {
pub const MaximumReasonLength: u32 = 300;
pub const MaxApprovals: u32 = 100;
pub const MaxBalance: Balance = Balance::max_value();
pub const SpendPayoutPeriod: BlockNumber = 30 * DAYS;
}

impl pallet_treasury::Config for Runtime {
Expand All @@ -1211,6 +1212,14 @@ impl pallet_treasury::Config for Runtime {
type WeightInfo = pallet_treasury::weights::SubstrateWeight<Runtime>;
type MaxApprovals = MaxApprovals;
type SpendOrigin = EnsureWithSuccess<EnsureRoot<AccountId>, AccountId, MaxBalance>;
type AssetKind = u32;
type Beneficiary = AccountId;
type BeneficiaryLookup = Indices;
type Paymaster = PayAssetFromAccount<Assets, TreasuryAccount>;
type BalanceConverter = AssetRate;
type PayoutPeriod = SpendPayoutPeriod;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

impl pallet_asset_rate::Config for Runtime {
Expand Down
5 changes: 5 additions & 0 deletions substrate/frame/asset-rate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,9 @@ where
.ok_or(pallet::Error::<T>::UnknownAssetKind.into())?;
Ok(rate.saturating_mul_int(balance))
}
/// Set a conversion rate to `1` for the `asset_id`.
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(asset_id: AssetKindOf<T>) {
pallet::ConversionRateToNative::<T>::set(asset_id.clone(), Some(1.into()));
}
}
23 changes: 22 additions & 1 deletion substrate/frame/bounties/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ use crate as pallet_bounties;

use frame_support::{
assert_noop, assert_ok, parameter_types,
traits::{ConstU32, ConstU64, OnInitialize},
traits::{
tokens::{PayFromAccount, UnityAssetBalanceConversion},
ConstU32, ConstU64, OnInitialize,
},
PalletId,
};

Expand Down Expand Up @@ -104,6 +107,8 @@ parameter_types! {
pub const TreasuryPalletId2: PalletId = PalletId(*b"py/trsr2");
pub static SpendLimit: Balance = u64::MAX;
pub static SpendLimit1: Balance = u64::MAX;
pub TreasuryAccount: u128 = Treasury::account_id();
pub TreasuryInstance1Account: u128 = Treasury1::account_id();
}

impl pallet_treasury::Config for Test {
Expand All @@ -123,6 +128,14 @@ impl pallet_treasury::Config for Test {
type SpendFunds = Bounties;
type MaxApprovals = ConstU32<100>;
type SpendOrigin = frame_system::EnsureRootWithSuccess<Self::AccountId, SpendLimit>;
type AssetKind = ();
type Beneficiary = Self::AccountId;
type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>;
type Paymaster = PayFromAccount<Balances, TreasuryAccount>;
type BalanceConverter = UnityAssetBalanceConversion;
type PayoutPeriod = ConstU64<10>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

impl pallet_treasury::Config<Instance1> for Test {
Expand All @@ -142,6 +155,14 @@ impl pallet_treasury::Config<Instance1> for Test {
type SpendFunds = Bounties1;
type MaxApprovals = ConstU32<100>;
type SpendOrigin = frame_system::EnsureRootWithSuccess<Self::AccountId, SpendLimit1>;
type AssetKind = ();
type Beneficiary = Self::AccountId;
type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>;
type Paymaster = PayFromAccount<Balances, TreasuryInstance1Account>;
type BalanceConverter = UnityAssetBalanceConversion;
type PayoutPeriod = ConstU64<10>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

parameter_types! {
Expand Down
14 changes: 13 additions & 1 deletion substrate/frame/child-bounties/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ use crate as pallet_child_bounties;

use frame_support::{
assert_noop, assert_ok, parameter_types,
traits::{ConstU32, ConstU64, OnInitialize},
traits::{
tokens::{PayFromAccount, UnityAssetBalanceConversion},
ConstU32, ConstU64, OnInitialize,
},
weights::Weight,
PalletId,
};
Expand Down Expand Up @@ -104,6 +107,7 @@ 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 SpendLimit: Balance = u64::MAX;
}

Expand All @@ -124,6 +128,14 @@ impl pallet_treasury::Config for Test {
type SpendFunds = Bounties;
type MaxApprovals = ConstU32<100>;
type SpendOrigin = frame_system::EnsureRootWithSuccess<Self::AccountId, SpendLimit>;
type AssetKind = ();
type Beneficiary = Self::AccountId;
type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>;
type Paymaster = PayFromAccount<Balances, TreasuryAccount>;
type BalanceConverter = UnityAssetBalanceConversion;
type PayoutPeriod = ConstU64<10>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}
parameter_types! {
// This will be 50% of the bounty fee.
Expand Down
3 changes: 2 additions & 1 deletion substrate/frame/support/src/traits/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub mod pay;
pub use misc::{
AssetId, Balance, BalanceStatus, ConversionFromAssetBalance, ConversionToAssetBalance,
ConvertRank, DepositConsequence, ExistenceRequirement, Fortitude, GetSalary, Locker, Precision,
Preservation, Provenance, Restriction, WithdrawConsequence, WithdrawReasons,
Preservation, Provenance, Restriction, UnityAssetBalanceConversion, WithdrawConsequence,
WithdrawReasons,
};
pub use pay::{Pay, PayFromAccount, PaymentStatus};
20 changes: 20 additions & 0 deletions substrate/frame/support/src/traits/tokens/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,26 @@ pub trait ConversionFromAssetBalance<AssetBalance, AssetId, OutBalance> {
balance: AssetBalance,
asset_id: AssetId,
) -> Result<OutBalance, Self::Error>;
/// Ensures that a conversion for the `asset_id` will be successful if done immediately after
/// this call.
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(asset_id: AssetId);
}

/// Implements [`ConversionFromAssetBalance`], enabling a 1:1 conversion of the asset balance
/// value to the balance.
pub struct UnityAssetBalanceConversion;
impl<AssetBalance, AssetId, OutBalance>
ConversionFromAssetBalance<AssetBalance, AssetId, OutBalance> for UnityAssetBalanceConversion
where
AssetBalance: Into<OutBalance>,
{
type Error = ();
fn from_asset_balance(balance: AssetBalance, _: AssetId) -> Result<OutBalance, Self::Error> {
Ok(balance.into())
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(_: AssetId) {}
}

/// Trait to handle NFT locking mechanism to ensure interactions with the asset can be implemented
Expand Down
35 changes: 34 additions & 1 deletion substrate/frame/support/src/traits/tokens/pay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use sp_core::{RuntimeDebug, TypedGet};
use sp_runtime::DispatchError;
use sp_std::fmt::Debug;

use super::{fungible, Balance, Preservation::Expendable};
use super::{fungible, fungibles, Balance, Preservation::Expendable};

/// Can be implemented by `PayFromAccount` using a `fungible` impl, but can also be implemented with
/// XCM/MultiAsset and made generic over assets.
Expand Down Expand Up @@ -107,3 +107,36 @@ impl<A: TypedGet, F: fungible::Mutate<A::Type>> Pay for PayFromAccount<F, A> {
#[cfg(feature = "runtime-benchmarks")]
fn ensure_concluded(_: Self::Id) {}
}

/// Simple implementation of `Pay` for assets which makes a payment from a "pot" - i.e. a single
/// account.
pub struct PayAssetFromAccount<F, A>(sp_std::marker::PhantomData<(F, A)>);
impl<A, F> frame_support::traits::tokens::Pay for PayAssetFromAccount<F, A>
where
A: TypedGet,
F: fungibles::Mutate<A::Type> + fungibles::Create<A::Type>,
{
type Balance = F::Balance;
type Beneficiary = A::Type;
type AssetKind = F::AssetId;
type Id = ();
type Error = DispatchError;
fn pay(
who: &Self::Beneficiary,
asset: Self::AssetKind,
amount: Self::Balance,
) -> Result<Self::Id, Self::Error> {
<F as fungibles::Mutate<_>>::transfer(asset, &A::get(), who, amount, Expendable)?;
Ok(())
}
fn check_payment(_: ()) -> PaymentStatus {
PaymentStatus::Success
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(_: &Self::Beneficiary, asset: Self::AssetKind, amount: Self::Balance) {
<F as fungibles::Create<_>>::create(asset.clone(), A::get(), true, amount).unwrap();
<F as fungibles::Mutate<_>>::mint_into(asset, &A::get(), amount).unwrap();
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_concluded(_: Self::Id) {}
}
24 changes: 23 additions & 1 deletion substrate/frame/tips/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ use sp_storage::Storage;
use frame_support::{
assert_noop, assert_ok, parameter_types,
storage::StoragePrefixedMap,
traits::{ConstU32, ConstU64, SortedMembers, StorageVersion},
traits::{
tokens::{PayFromAccount, UnityAssetBalanceConversion},
ConstU32, ConstU64, SortedMembers, StorageVersion,
},
PalletId,
};

Expand Down Expand Up @@ -123,7 +126,10 @@ parameter_types! {
pub const Burn: Permill = Permill::from_percent(50);
pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry");
pub const TreasuryPalletId2: PalletId = PalletId(*b"py/trsr2");
pub TreasuryAccount: u128 = Treasury::account_id();
pub TreasuryInstance1Account: u128 = Treasury1::account_id();
}

impl pallet_treasury::Config for Test {
type PalletId = TreasuryPalletId;
type Currency = pallet_balances::Pallet<Test>;
Expand All @@ -141,6 +147,14 @@ impl pallet_treasury::Config for Test {
type SpendFunds = ();
type MaxApprovals = ConstU32<100>;
type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;
type AssetKind = ();
type Beneficiary = Self::AccountId;
type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>;
type Paymaster = PayFromAccount<Balances, TreasuryAccount>;
type BalanceConverter = UnityAssetBalanceConversion;
type PayoutPeriod = ConstU64<10>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

impl pallet_treasury::Config<Instance1> for Test {
Expand All @@ -160,6 +174,14 @@ impl pallet_treasury::Config<Instance1> for Test {
type SpendFunds = ();
type MaxApprovals = ConstU32<100>;
type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;
type AssetKind = ();
type Beneficiary = Self::AccountId;
type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>;
type Paymaster = PayFromAccount<Balances, TreasuryInstance1Account>;
type BalanceConverter = UnityAssetBalanceConversion;
type PayoutPeriod = ConstU64<10>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}

parameter_types! {
Expand Down
7 changes: 5 additions & 2 deletions substrate/frame/treasury/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ codec = { package = "parity-scale-codec", version = "3.6.1", default-features =
"derive",
"max-encoded-len",
] }
docify = "0.2.0"
impl-trait-for-tuples = "0.2.2"
scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
serde = { version = "1.0.188", features = ["derive"], optional = true }
Expand All @@ -26,11 +27,12 @@ frame-system = { path = "../system", default-features = false}
pallet-balances = { path = "../balances", default-features = false}
sp-runtime = { path = "../../primitives/runtime", default-features = false}
sp-std = { path = "../../primitives/std", default-features = false}
sp-core = { path = "../../primitives/core", default-features = false, optional = true}

[dev-dependencies]
sp-core = { path = "../../primitives/core" }
sp-io = { path = "../../primitives/io" }
pallet-utility = { path = "../utility" }
sp-core = { path = "../../primitives/core", default-features = false }

[features]
default = [ "std" ]
Expand All @@ -43,12 +45,13 @@ std = [
"pallet-utility/std",
"scale-info/std",
"serde",
"sp-core/std",
"sp-core?/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
]
runtime-benchmarks = [
"dep:sp-core",
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
Expand Down
Loading

0 comments on commit 0d2b961

Please sign in to comment.