-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Pallets: Treasury spend() should use Pay trait
#13607
Changes from 5 commits
a027594
29194c3
11cd988
57aacbe
67b9f7a
2b20924
3602844
dd99d7c
5f52015
1925806
f94e20a
293552f
a88fb24
3aaed66
a33d4dc
414e8e2
2f60a1b
182c733
0916aff
fdcd66a
5673e79
c6f9c5a
bb372a2
a56897d
69cc489
e07ee86
304f536
e7ecdbf
e689272
3cd5760
d97886d
805d595
bbf83fa
8aa1c3c
aabfadf
592b81b
b295ea7
1e85a67
eb4b2fe
1fdccb7
310578d
22e43a1
d96d541
dc891cb
3bd3c8e
1018464
c04955d
b5b5d0e
65c71a0
62b0901
5750f0b
e26dee6
0739c29
ac27c84
87b905c
5bc0af9
1fca0e9
7d712e2
db02823
415561d
449d484
4741037
9cad108
b644329
8a9903b
f07e8b0
34c859b
9237568
1ecd8cc
c029123
83a2097
d79cab0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,8 +75,10 @@ use sp_std::{collections::btree_map::BTreeMap, prelude::*}; | |
| use frame_support::{ | ||
| print, | ||
| traits::{ | ||
| Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, | ||
| ReservableCurrency, WithdrawReasons, | ||
| tokens::{Balance, BalanceConversion, Pay, PaymentStatus}, | ||
| Currency, | ||
| ExistenceRequirement::KeepAlive, | ||
| Get, Imbalance, OnUnbalanced, ReservableCurrency, WithdrawReasons, | ||
| }, | ||
| weights::Weight, | ||
| PalletId, | ||
|
|
@@ -85,6 +87,7 @@ use frame_support::{ | |
| pub use pallet::*; | ||
| pub use weights::WeightInfo; | ||
|
|
||
| pub type PayBalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance; | ||
| pub type BalanceOf<T, I = ()> = | ||
| <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; | ||
| pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency< | ||
|
|
@@ -93,6 +96,7 @@ pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currenc | |
| pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency< | ||
| <T as frame_system::Config>::AccountId, | ||
| >>::NegativeImbalance; | ||
|
|
||
| type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source; | ||
|
|
||
| /// A trait to allow the Treasury Pallet to spend it's funds for other purposes. | ||
|
|
@@ -107,7 +111,7 @@ type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup | |
| /// * `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<T: Config<I>, I: 'static = ()> { | ||
| pub trait SpendFundsLocal<T: Config<I>, I: 'static = ()> { | ||
| fn spend_funds( | ||
| budget_remaining: &mut BalanceOf<T, I>, | ||
| imbalance: &mut PositiveImbalanceOf<T, I>, | ||
|
|
@@ -116,8 +120,15 @@ pub trait SpendFunds<T: Config<I>, I: 'static = ()> { | |
| ); | ||
| } | ||
|
|
||
| #[impl_trait_for_tuples::impl_for_tuples(30)] | ||
| pub trait SpendFunds<T: Config<I>, I: 'static = ()> { | ||
| fn spend_funds(total_weight: &mut Weight, total_spent: T::Balance, total_missed: u32); | ||
| } | ||
|
|
||
| /// An index of a proposal. Just a `u32`. | ||
| pub type ProposalIndex = u32; | ||
| /// A count of proposals. Just a `u32`. | ||
| pub type ProposalsCount = u32; | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
|
|
||
| /// A spending proposal. | ||
| #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] | ||
|
|
@@ -133,10 +144,30 @@ pub struct Proposal<AccountId, Balance> { | |
| bond: Balance, | ||
| } | ||
|
|
||
| /// A spending proposal. | ||
| #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] | ||
| #[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] | ||
| pub struct PendingPayment<AccountId, Balance, AssetKind, AssetBalance, PaymentId> { | ||
| /// The account proposing it. | ||
| proposer: AccountId, | ||
| /// The asset_id of the amount to be paid | ||
| asset_id: AssetKind, | ||
| /// The (total) amount that should be paid. | ||
| value: AssetBalance, | ||
| /// The account to whom the payment should be made if the proposal is accepted. | ||
| beneficiary: AccountId, | ||
| /// The amount to be paid, but normalized to the native asset class | ||
| normalized_value: Balance, | ||
| // payment_status: PaymentStatus, | ||
| payment_id: Option<PaymentId>, | ||
| } | ||
|
|
||
| #[frame_support::pallet] | ||
| pub mod pallet { | ||
| use super::*; | ||
| use frame_support::{dispatch_context::with_context, pallet_prelude::*}; | ||
| use frame_support::{ | ||
| dispatch_context::with_context, pallet_prelude::*, traits::tokens::AssetId, | ||
| }; | ||
| use frame_system::pallet_prelude::*; | ||
|
|
||
| #[pallet::pallet] | ||
|
|
@@ -153,6 +184,25 @@ pub mod pallet { | |
| /// Origin from which rejections must come. | ||
| type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>; | ||
|
|
||
| type Balance: Balance; | ||
|
|
||
| /// The identifier for what asset should be spent. | ||
| type AssetKind: AssetId; | ||
|
|
||
| /// Means by which we can make payments to accounts. This also defines the currency and the | ||
| /// balance which we use to denote that currency. | ||
| type Paymaster: Pay< | ||
| Beneficiary = <Self as frame_system::Config>::AccountId, | ||
| AssetKind = Self::AssetKind, | ||
| >; | ||
|
|
||
| type BalanceConverter: BalanceConversion< | ||
| PayBalanceOf<Self, I>, | ||
| Self::AssetKind, | ||
| Self::Balance, | ||
| Error = Error<Self, I>, | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| >; | ||
|
|
||
| /// The overarching event type. | ||
| type RuntimeEvent: From<Event<Self, I>> | ||
| + IsType<<Self as frame_system::Config>::RuntimeEvent>; | ||
|
|
@@ -194,6 +244,9 @@ pub mod pallet { | |
| /// Runtime hooks to external pallet using treasury to compute spend funds. | ||
| type SpendFunds: SpendFunds<Self, I>; | ||
|
|
||
| /// Runtime hooks to external pallet using treasury to compute spend funds. | ||
| type SpendFundsLocal: SpendFundsLocal<Self, I>; | ||
|
|
||
| /// 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. | ||
|
|
@@ -203,7 +256,12 @@ pub mod pallet { | |
| /// The origin required for approving spends from the treasury outside of the proposal | ||
| /// process. The `Success` value is the maximum amount that this origin is allowed to | ||
| /// spend at a time. | ||
| type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>; | ||
| type SpendOriginLocal: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>; | ||
|
|
||
| /// The origin required for approving spends from the treasury outside of the proposal | ||
| /// process. The `Success` value is the maximum amount that this origin is allowed to | ||
| /// spend at a time. | ||
| type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = PayBalanceOf<Self, I>>; | ||
| } | ||
|
|
||
| /// Number of proposals that have been made. | ||
|
|
@@ -222,6 +280,23 @@ pub mod pallet { | |
| OptionQuery, | ||
| >; | ||
|
|
||
| /// Proposals that have been made. | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| #[pallet::storage] | ||
| #[pallet::getter(fn pending_payments)] | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| pub type PendingPayments<T: Config<I>, I: 'static = ()> = CountedStorageMap< | ||
| _, | ||
| Twox64Concat, | ||
| ProposalIndex, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. legacy
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept it as ProposalIndex because of #13607 (comment)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, but why the alternative would be ProposalCount?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the time when that comment was made, the count was used for the Proposals storage map. a027594#diff-21571fa193fececabaa733a8fea9e3c6478e1cdcb86f58e3f325418b2555e81aR237 I could introduce |
||
| PendingPayment< | ||
| T::AccountId, | ||
| T::Balance, | ||
| T::AssetKind, | ||
| PayBalanceOf<T, I>, | ||
| <T::Paymaster as Pay>::Id, | ||
| >, | ||
| OptionQuery, | ||
| >; | ||
|
|
||
| /// The amount which has been reported as inactive to Currency. | ||
| #[pallet::storage] | ||
| pub type Deactivated<T: Config<I>, I: 'static = ()> = | ||
|
|
@@ -286,14 +361,36 @@ pub mod pallet { | |
| Rollover { rollover_balance: BalanceOf<T, I> }, | ||
| /// Some funds have been deposited. | ||
| Deposit { value: BalanceOf<T, I> }, | ||
| /// We have ended a spend period and will now allocate funds. | ||
| ProcessingProposals { waiting_proposals: ProposalIndex }, | ||
| /// Spending has finished; this is the number of proposals rolled over till next | ||
| /// T::SpendPeriod. | ||
| RolloverPayments { rollover_proposals: ProposalsCount, allocated_proposals: ProposalsCount }, | ||
| /// A new spend proposal has been approved. | ||
| SpendApproved { | ||
| proposal_index: ProposalIndex, | ||
| amount: BalanceOf<T, I>, | ||
| beneficiary: T::AccountId, | ||
| }, | ||
| QueuedPayment { | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| proposal_index: ProposalIndex, | ||
| amount: PayBalanceOf<T, I>, | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| beneficiary: T::AccountId, | ||
| }, | ||
| /// The inactive funds of the pallet have been updated. | ||
| UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> }, | ||
| /// The proposal was paid successfully | ||
| ProposalPaymentSuccess { | ||
| proposal_index: ProposalIndex, | ||
| asset_id: T::AssetKind, | ||
| amount: PayBalanceOf<T, I>, | ||
| }, | ||
| // The proposal payment failed. Payment will be retried in next spend period. | ||
| ProposalPaymentFailure { | ||
| proposal_index: ProposalIndex, | ||
| asset_id: T::AssetKind, | ||
| amount: PayBalanceOf<T, I>, | ||
| }, | ||
| } | ||
|
|
||
| /// Error for the treasury pallet. | ||
|
|
@@ -310,6 +407,8 @@ pub mod pallet { | |
| InsufficientPermission, | ||
| /// Proposal has not been approved. | ||
| ProposalNotApproved, | ||
| /// Unable to convert asset to native balance | ||
| BalanceConversionFailed, | ||
|
tonyalaribe marked this conversation as resolved.
|
||
| } | ||
|
|
||
| #[pallet::hooks] | ||
|
|
@@ -328,10 +427,9 @@ pub mod pallet { | |
| deactivated: pot, | ||
| }); | ||
| } | ||
|
|
||
| // Check to see if we should spend some funds! | ||
| if (n % T::SpendPeriod::get()).is_zero() { | ||
| Self::spend_funds() | ||
| Self::spend_funds().saturating_add(Self::spend_funds_local()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there might be not enough weight capacity left for |
||
| } else { | ||
| Weight::zero() | ||
| } | ||
|
|
@@ -431,29 +529,25 @@ pub mod pallet { | |
| /// beneficiary. | ||
| #[pallet::call_index(3)] | ||
| #[pallet::weight(T::WeightInfo::spend())] | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| pub fn spend( | ||
| pub fn spend_local( | ||
| origin: OriginFor<T>, | ||
| #[pallet::compact] amount: BalanceOf<T, I>, | ||
| beneficiary: AccountIdLookupOf<T>, | ||
| ) -> DispatchResult { | ||
| let max_amount = T::SpendOrigin::ensure_origin(origin)?; | ||
| let max_amount = T::SpendOriginLocal::ensure_origin(origin)?; | ||
| ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission); | ||
|
|
||
| with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| { | ||
| let context = v.or_default(); | ||
|
|
||
| // We group based on `max_amount`, to dinstinguish between different kind of | ||
| // origins. (assumes that all origins have different `max_amount`) | ||
| // | ||
| // Worst case is that we reject some "valid" request. | ||
| let spend = context.spend_in_context.entry(max_amount).or_default(); | ||
|
|
||
| // Ensure that we don't overflow nor use more than `max_amount` | ||
| if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) { | ||
| Err(Error::<T, I>::InsufficientPermission) | ||
| } else { | ||
| *spend = spend.saturating_add(amount); | ||
|
|
||
| Ok(()) | ||
| } | ||
| }) | ||
|
|
@@ -476,6 +570,63 @@ pub mod pallet { | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Propose and approve a spend of treasury funds. | ||
|
tonyalaribe marked this conversation as resolved.
|
||
| /// | ||
| /// - `origin`: Must be `SpendOrigin` with the `Success` value being at least `amount`. | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. | ||
|
tonyalaribe marked this conversation as resolved.
|
||
| /// - `beneficiary`: The destination account for the transfer. | ||
| /// | ||
| /// NOTE: For record-keeping purposes, the proposer is deemed to be equivalent to the | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| /// beneficiary. | ||
| #[pallet::call_index(5)] | ||
| #[pallet::weight(T::WeightInfo::spend())] | ||
|
tonyalaribe marked this conversation as resolved.
|
||
| pub fn spend( | ||
| origin: OriginFor<T>, | ||
| asset_id: T::AssetKind, | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| #[pallet::compact] amount: PayBalanceOf<T, I>, | ||
| beneficiary: AccountIdLookupOf<T>, | ||
| ) -> DispatchResult { | ||
| let max_amount = T::SpendOrigin::ensure_origin(origin)?; | ||
|
tonyalaribe marked this conversation as resolved.
|
||
| ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission); | ||
|
|
||
| with_context::<SpendContext<PayBalanceOf<T, I>>, _>(|v| { | ||
| let context = v.or_default(); | ||
|
|
||
| // We group based on `max_amount`, to dinstinguish between different kind of | ||
| // origins. (assumes that all origins have different `max_amount`) | ||
| // | ||
| // Worst case is that we reject some "valid" request. | ||
| let spend = context.spend_in_context.entry(max_amount).or_default(); | ||
|
|
||
| // Ensure that we don't overflow nor use more than `max_amount` | ||
| if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) { | ||
| Err(Error::<T, I>::InsufficientPermission) | ||
| } else { | ||
| *spend = spend.saturating_add(amount); | ||
|
|
||
| Ok(()) | ||
| } | ||
| }) | ||
| .unwrap_or(Ok(()))?; | ||
|
|
||
| let beneficiary = T::Lookup::lookup(beneficiary)?; | ||
|
|
||
| let proposal = PendingPayment { | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| proposer: beneficiary.clone(), | ||
| asset_id, | ||
| value: amount, | ||
| beneficiary: beneficiary.clone(), | ||
| normalized_value: T::BalanceConverter::to_asset_balance(amount, asset_id)?, | ||
| payment_id: None, | ||
| }; | ||
|
|
||
| let proposal_index = PendingPayments::<T, I>::count(); | ||
| PendingPayments::<T, I>::insert(proposal_index, proposal); | ||
|
|
||
| Self::deposit_event(Event::QueuedPayment { proposal_index, amount, beneficiary }); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Force a previously approved proposal to be removed from the approval queue. | ||
| /// The original deposit will no longer be returned. | ||
| /// | ||
|
|
@@ -534,6 +685,72 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
| /// Spend some money! returns number of approvals before spend. | ||
| pub fn spend_funds() -> Weight { | ||
| let mut total_weight = Weight::zero(); | ||
| let mut total_spent = T::Balance::zero(); | ||
| let mut missed_proposals: u32 = 0; | ||
| let proposals_len = PendingPayments::<T, I>::count(); | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
|
|
||
| Self::deposit_event(Event::ProcessingProposals { waiting_proposals: proposals_len }); | ||
|
|
||
| for key in PendingPayments::<T, I>::iter_keys() { | ||
| if let Some(mut p) = PendingPayments::<T, I>::get(key) { | ||
| match p.payment_id { | ||
| None => | ||
| if let Ok(id) = T::Paymaster::pay(&p.beneficiary, p.asset_id, p.value) { | ||
| total_spent += p.normalized_value; | ||
|
tonyalaribe marked this conversation as resolved.
Outdated
|
||
| p.payment_id = Some(id); | ||
| PendingPayments::<T, I>::set(key, Some(p)); | ||
| } else { | ||
| missed_proposals = missed_proposals.saturating_add(1); | ||
| }, | ||
| Some(payment_id) => match T::Paymaster::check_payment(payment_id) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should be timing out after some
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pallet_xcm does not timeout now. but I think it should be on both sides
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what do you propose? Also, the treasury pallet knows nothing about the XCM timeouts nor does it even know anything about XCM.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the treasury pallet does not need to know about XCM to have a timeout for some async status checks it does.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a concept of retries, so we retry a payment a given number of times. |
||
| PaymentStatus::Failure => { | ||
| // try again in the next `T::SpendPeriod`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the payment statuses should be check with a different interval, not SpendPeriod. if we separate the pay job and the check status one, we will be able to have more accurate weights for these jobs.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. let me explore this more |
||
| missed_proposals = missed_proposals.saturating_add(1); | ||
| Self::deposit_event(Event::ProposalPaymentFailure { | ||
| proposal_index: key, | ||
| asset_id: p.asset_id, | ||
| amount: p.value, | ||
| }); | ||
| // Force the payment to none, so a fresh payment is sent during the next | ||
| // T::SpendPeriod. | ||
| p.payment_id = None; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. which means we retry endlessly, looks wrong to me.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's no harm. This gives time for governance to solve the reason for the failure. But what would you propose?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if its not recoverable fail, how governance will solve it? |
||
| PendingPayments::<T, I>::set(key, Some(p)); | ||
| }, | ||
| PaymentStatus::Success => { | ||
| PendingPayments::<T, I>::remove(key); | ||
| Self::deposit_event(Event::ProposalPaymentSuccess { | ||
| proposal_index: key, | ||
| asset_id: p.asset_id, | ||
| amount: p.value, | ||
| }); | ||
| }, | ||
| // PaymentStatus::InProgress and PaymentStatus::Unknown indicate that the | ||
|
tonyalaribe marked this conversation as resolved.
|
||
| // proposal status is inconclusive, and might still be successful or failed | ||
| // in the future. | ||
| _ => {}, | ||
| }, | ||
| } | ||
| } else { | ||
| } | ||
| } | ||
|
|
||
| total_weight += T::WeightInfo::on_initialize_proposals(proposals_len); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. on_initialize_proposals is not relevant here |
||
|
|
||
| // Call Runtime hooks to external pallet using treasury to compute spend funds. | ||
| // We could trigger burning of funds in the spendFunds hook as well. | ||
| T::SpendFunds::spend_funds(&mut total_weight, total_spent, missed_proposals); | ||
|
|
||
| Self::deposit_event(Event::RolloverPayments { | ||
| rollover_proposals: missed_proposals, | ||
| allocated_proposals: proposals_len.saturating_sub(missed_proposals), | ||
| }); | ||
|
|
||
| total_weight | ||
| } | ||
|
|
||
| /// Spend some money! returns number of approvals before spend. | ||
| pub fn spend_funds_local() -> Weight { | ||
| let mut total_weight = Weight::zero(); | ||
|
|
||
| let mut budget_remaining = Self::pot(); | ||
| Self::deposit_event(Event::Spending { budget_remaining }); | ||
|
|
@@ -577,7 +794,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
| 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( | ||
| T::SpendFundsLocal::spend_funds( | ||
| &mut budget_remaining, | ||
| &mut imbalance, | ||
| &mut total_weight, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.