mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-14 00:31:07 +00:00
Treasury spends various asset kinds (#1333)
### 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 <>
This commit is contained in:
@@ -21,12 +21,41 @@
|
||||
|
||||
use super::{Pallet as Treasury, *};
|
||||
|
||||
use frame_benchmarking::v1::{account, benchmarks_instance_pallet, BenchmarkError};
|
||||
use frame_benchmarking::{
|
||||
v1::{account, BenchmarkError},
|
||||
v2::*,
|
||||
};
|
||||
use frame_support::{
|
||||
ensure,
|
||||
traits::{EnsureOrigin, OnInitialize, UnfilteredDispatchable},
|
||||
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<AssetKind, Beneficiary> {
|
||||
/// 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<AssetKind, Beneficiary> ArgumentsFactory<AssetKind, Beneficiary> 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;
|
||||
|
||||
@@ -66,81 +95,245 @@ fn assert_last_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>::
|
||||
frame_system::Pallet::<T>::assert_last_event(generic_event.into());
|
||||
}
|
||||
|
||||
benchmarks_instance_pallet! {
|
||||
// Create the arguments for the `spend` dispatchable.
|
||||
fn create_spend_arguments<T: Config<I>, I: 'static>(
|
||||
seed: u32,
|
||||
) -> (T::AssetKind, AssetBalanceOf<T, I>, T::Beneficiary, BeneficiaryLookupOf<T, I>) {
|
||||
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.
|
||||
spend {
|
||||
#[benchmark]
|
||||
fn spend_local() -> Result<(), BenchmarkError> {
|
||||
let (_, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED);
|
||||
let origin = T::SpendOrigin::try_successful_origin();
|
||||
let origin =
|
||||
T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
let beneficiary = T::Lookup::lookup(beneficiary_lookup.clone()).unwrap();
|
||||
let call = Call::<T, I>::spend { amount: value, beneficiary: beneficiary_lookup };
|
||||
}: {
|
||||
if let Ok(origin) = origin.clone() {
|
||||
call.dispatch_bypass_filter(origin)?;
|
||||
}
|
||||
}
|
||||
verify {
|
||||
if origin.is_ok() {
|
||||
assert_last_event::<T, I>(Event::SpendApproved { proposal_index: 0, amount: value, beneficiary }.into())
|
||||
}
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, value, beneficiary_lookup);
|
||||
|
||||
assert_last_event::<T, I>(
|
||||
Event::SpendApproved { proposal_index: 0, amount: value, beneficiary }.into(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
propose_spend {
|
||||
#[benchmark]
|
||||
fn propose_spend() -> Result<(), BenchmarkError> {
|
||||
let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED);
|
||||
// Whitelist caller account from further DB operations.
|
||||
let caller_key = frame_system::Account::<T>::hashed_key_for(&caller);
|
||||
frame_benchmarking::benchmarking::add_to_whitelist(caller_key.into());
|
||||
}: _(RawOrigin::Signed(caller), value, beneficiary_lookup)
|
||||
|
||||
reject_proposal {
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller), value, beneficiary_lookup);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn reject_proposal() -> Result<(), BenchmarkError> {
|
||||
let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED);
|
||||
#[allow(deprecated)]
|
||||
Treasury::<T, _>::propose_spend(
|
||||
RawOrigin::Signed(caller).into(),
|
||||
value,
|
||||
beneficiary_lookup
|
||||
beneficiary_lookup,
|
||||
)?;
|
||||
let proposal_id = Treasury::<T, _>::proposal_count() - 1;
|
||||
let reject_origin =
|
||||
T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
}: _<T::RuntimeOrigin>(reject_origin, proposal_id)
|
||||
|
||||
approve_proposal {
|
||||
let p in 0 .. T::MaxApprovals::get() - 1;
|
||||
#[extrinsic_call]
|
||||
_(reject_origin as T::RuntimeOrigin, proposal_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn approve_proposal(
|
||||
p: Linear<0, { T::MaxApprovals::get() - 1 }>,
|
||||
) -> Result<(), BenchmarkError> {
|
||||
create_approved_proposals::<T, _>(p)?;
|
||||
let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED);
|
||||
#[allow(deprecated)]
|
||||
Treasury::<T, _>::propose_spend(
|
||||
RawOrigin::Signed(caller).into(),
|
||||
value,
|
||||
beneficiary_lookup
|
||||
beneficiary_lookup,
|
||||
)?;
|
||||
let proposal_id = Treasury::<T, _>::proposal_count() - 1;
|
||||
let approve_origin =
|
||||
T::ApproveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
}: _<T::RuntimeOrigin>(approve_origin, proposal_id)
|
||||
|
||||
remove_approval {
|
||||
#[extrinsic_call]
|
||||
_(approve_origin as T::RuntimeOrigin, proposal_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn remove_approval() -> Result<(), BenchmarkError> {
|
||||
let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED);
|
||||
#[allow(deprecated)]
|
||||
Treasury::<T, _>::propose_spend(
|
||||
RawOrigin::Signed(caller).into(),
|
||||
value,
|
||||
beneficiary_lookup
|
||||
beneficiary_lookup,
|
||||
)?;
|
||||
let proposal_id = Treasury::<T, _>::proposal_count() - 1;
|
||||
#[allow(deprecated)]
|
||||
Treasury::<T, I>::approve_proposal(RawOrigin::Root.into(), proposal_id)?;
|
||||
let reject_origin =
|
||||
T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
}: _<T::RuntimeOrigin>(reject_origin, proposal_id)
|
||||
|
||||
on_initialize_proposals {
|
||||
let p in 0 .. T::MaxApprovals::get();
|
||||
#[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::<T, _>();
|
||||
create_approved_proposals::<T, _>(p)?;
|
||||
}: {
|
||||
Treasury::<T, _>::on_initialize(frame_system::pallet_prelude::BlockNumberFor::<T>::zero());
|
||||
|
||||
#[block]
|
||||
{
|
||||
Treasury::<T, _>::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::<T, _>(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::<T>::block_number();
|
||||
let expire_at = valid_from.saturating_add(T::PayoutPeriod::get());
|
||||
assert_last_event::<T, I>(
|
||||
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::<T, _>(SEED);
|
||||
T::BalanceConverter::ensure_successful(asset_kind.clone());
|
||||
Treasury::<T, _>::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::<T, I>::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::<T, I>(Event::Paid { index: 0, payment_id: id }.into());
|
||||
assert!(Treasury::<T, _>::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::<T, _>(SEED);
|
||||
T::BalanceConverter::ensure_successful(asset_kind.clone());
|
||||
Treasury::<T, _>::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::<T, _>::payout(RawOrigin::Signed(caller.clone()).into(), 0u32)?;
|
||||
match Spends::<T, I>::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::<T, I>::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::<T, _>(SEED);
|
||||
T::BalanceConverter::ensure_successful(asset_kind.clone());
|
||||
Treasury::<T, _>::spend(
|
||||
origin,
|
||||
Box::new(asset_kind.clone()),
|
||||
amount,
|
||||
Box::new(beneficiary_lookup),
|
||||
None,
|
||||
)?;
|
||||
assert!(Spends::<T, I>::get(0).is_some());
|
||||
let origin =
|
||||
T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, 0u32);
|
||||
|
||||
assert!(Spends::<T, I>::get(0).is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Treasury, crate::tests::new_test_ext(), crate::tests::Test);
|
||||
|
||||
Reference in New Issue
Block a user