1#![doc = docify::embed!("src/tests.rs", spend_local_origin_works)]
53#![doc = docify::embed!("src/tests.rs", spend_payout_works)]
56#![cfg_attr(not(feature = "std"), no_std)]
74
75mod benchmarking;
76pub mod migration;
77#[cfg(test)]
78mod tests;
79pub mod weights;
80use core::marker::PhantomData;
81
82#[cfg(feature = "runtime-benchmarks")]
83pub use benchmarking::ArgumentsFactory;
84
85extern crate alloc;
86
87use codec::{Decode, Encode, MaxEncodedLen};
88use scale_info::TypeInfo;
89
90use alloc::{boxed::Box, collections::btree_map::BTreeMap};
91use sp_runtime::{
92 traits::{
93 AccountIdConversion, BlockNumberProvider, CheckedAdd, One, Saturating, StaticLookup,
94 UniqueSaturatedInto, Zero,
95 },
96 PerThing, Permill, RuntimeDebug,
97};
98
99use frame_support::{
100 dispatch::{DispatchResult, DispatchResultWithPostInfo},
101 ensure, print,
102 traits::{
103 tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced,
104 ReservableCurrency, WithdrawReasons,
105 },
106 weights::Weight,
107 BoundedVec, PalletId,
108};
109use frame_system::pallet_prelude::BlockNumberFor as SystemBlockNumberFor;
110
111pub use pallet::*;
112pub use weights::WeightInfo;
113
114pub type BalanceOf<T, I = ()> =
115 <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
116pub type AssetBalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
117pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
118 <T as frame_system::Config>::AccountId,
119>>::PositiveImbalance;
120pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
121 <T as frame_system::Config>::AccountId,
122>>::NegativeImbalance;
123type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
124type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source;
125pub type BlockNumberFor<T, I = ()> =
126 <<T as Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
127
128#[impl_trait_for_tuples::impl_for_tuples(30)]
140pub trait SpendFunds<T: Config<I>, I: 'static = ()> {
141 fn spend_funds(
142 budget_remaining: &mut BalanceOf<T, I>,
143 imbalance: &mut PositiveImbalanceOf<T, I>,
144 total_weight: &mut Weight,
145 missed_any: &mut bool,
146 );
147}
148
149pub type ProposalIndex = u32;
151
152#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
154#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
155pub struct Proposal<AccountId, Balance> {
156 proposer: AccountId,
158 value: Balance,
160 beneficiary: AccountId,
162 bond: Balance,
164}
165
166#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
168#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
169pub enum PaymentState<Id> {
170 Pending,
172 Attempted { id: Id },
174 Failed,
176}
177
178#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
180#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
181pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> {
182 asset_kind: AssetKind,
184 amount: AssetBalance,
186 beneficiary: Beneficiary,
188 valid_from: BlockNumber,
190 expire_at: BlockNumber,
192 status: PaymentState<PaymentId>,
194}
195
196pub type SpendIndex = u32;
198
199#[frame_support::pallet]
200pub mod pallet {
201 use super::*;
202 use frame_support::{
203 dispatch_context::with_context,
204 pallet_prelude::*,
205 traits::tokens::{ConversionFromAssetBalance, PaymentStatus},
206 };
207 use frame_system::pallet_prelude::{ensure_signed, OriginFor};
208
209 #[pallet::pallet]
210 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
211
212 #[pallet::config]
213 pub trait Config<I: 'static = ()>: frame_system::Config {
214 type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
216
217 type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
219
220 type RuntimeEvent: From<Event<Self, I>>
222 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
223
224 #[pallet::constant]
226 type SpendPeriod: Get<BlockNumberFor<Self, I>>;
227
228 #[pallet::constant]
230 type Burn: Get<Permill>;
231
232 #[pallet::constant]
234 type PalletId: Get<PalletId>;
235
236 type BurnDestination: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
238
239 type WeightInfo: WeightInfo;
241
242 type SpendFunds: SpendFunds<Self, I>;
244
245 #[pallet::constant]
252 type MaxApprovals: Get<u32>;
253
254 type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>;
258
259 type AssetKind: Parameter + MaxEncodedLen;
261
262 type Beneficiary: Parameter + MaxEncodedLen;
264
265 type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
267
268 type Paymaster: Pay<Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind>;
270
271 type BalanceConverter: ConversionFromAssetBalance<
275 <Self::Paymaster as Pay>::Balance,
276 Self::AssetKind,
277 BalanceOf<Self, I>,
278 >;
279
280 #[pallet::constant]
282 type PayoutPeriod: Get<BlockNumberFor<Self, I>>;
283
284 #[cfg(feature = "runtime-benchmarks")]
286 type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>;
287
288 type BlockNumberProvider: BlockNumberProvider;
290 }
291
292 #[pallet::extra_constants]
293 impl<T: Config<I>, I: 'static> Pallet<T, I> {
294 fn pot_account() -> T::AccountId {
296 Self::account_id()
297 }
298 }
299
300 #[pallet::storage]
305 pub type ProposalCount<T, I = ()> = StorageValue<_, ProposalIndex, ValueQuery>;
306
307 #[pallet::storage]
312 pub type Proposals<T: Config<I>, I: 'static = ()> = StorageMap<
313 _,
314 Twox64Concat,
315 ProposalIndex,
316 Proposal<T::AccountId, BalanceOf<T, I>>,
317 OptionQuery,
318 >;
319
320 #[pallet::storage]
322 pub type Deactivated<T: Config<I>, I: 'static = ()> =
323 StorageValue<_, BalanceOf<T, I>, ValueQuery>;
324
325 #[pallet::storage]
330 pub type Approvals<T: Config<I>, I: 'static = ()> =
331 StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
332
333 #[pallet::storage]
335 pub type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>;
336
337 #[pallet::storage]
340 pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap<
341 _,
342 Twox64Concat,
343 SpendIndex,
344 SpendStatus<
345 T::AssetKind,
346 AssetBalanceOf<T, I>,
347 T::Beneficiary,
348 BlockNumberFor<T, I>,
349 <T::Paymaster as Pay>::Id,
350 >,
351 OptionQuery,
352 >;
353
354 #[pallet::storage]
356 pub type LastSpendPeriod<T, I = ()> = StorageValue<_, BlockNumberFor<T, I>, OptionQuery>;
357
358 #[pallet::genesis_config]
359 #[derive(frame_support::DefaultNoBound)]
360 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
361 #[serde(skip)]
362 _config: core::marker::PhantomData<(T, I)>,
363 }
364
365 #[pallet::genesis_build]
366 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
367 fn build(&self) {
368 let account_id = Pallet::<T, I>::account_id();
370 let min = T::Currency::minimum_balance();
371 if T::Currency::free_balance(&account_id) < min {
372 let _ = T::Currency::make_free_balance_be(&account_id, min);
373 }
374 }
375 }
376
377 #[pallet::event]
378 #[pallet::generate_deposit(pub(super) fn deposit_event)]
379 pub enum Event<T: Config<I>, I: 'static = ()> {
380 Spending { budget_remaining: BalanceOf<T, I> },
382 Awarded { proposal_index: ProposalIndex, award: BalanceOf<T, I>, account: T::AccountId },
384 Burnt { burnt_funds: BalanceOf<T, I> },
386 Rollover { rollover_balance: BalanceOf<T, I> },
388 Deposit { value: BalanceOf<T, I> },
390 SpendApproved {
392 proposal_index: ProposalIndex,
393 amount: BalanceOf<T, I>,
394 beneficiary: T::AccountId,
395 },
396 UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> },
398 AssetSpendApproved {
400 index: SpendIndex,
401 asset_kind: T::AssetKind,
402 amount: AssetBalanceOf<T, I>,
403 beneficiary: T::Beneficiary,
404 valid_from: BlockNumberFor<T, I>,
405 expire_at: BlockNumberFor<T, I>,
406 },
407 AssetSpendVoided { index: SpendIndex },
409 Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
411 PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
413 SpendProcessed { index: SpendIndex },
416 }
417
418 #[pallet::error]
420 pub enum Error<T, I = ()> {
421 InvalidIndex,
423 TooManyApprovals,
425 InsufficientPermission,
428 ProposalNotApproved,
430 FailedToConvertBalance,
432 SpendExpired,
434 EarlyPayout,
436 AlreadyAttempted,
438 PayoutError,
440 NotAttempted,
442 Inconclusive,
444 }
445
446 #[pallet::hooks]
447 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
448 fn on_initialize(_do_not_use_local_block_number: SystemBlockNumberFor<T>) -> Weight {
451 let block_number = T::BlockNumberProvider::current_block_number();
452 let pot = Self::pot();
453 let deactivated = Deactivated::<T, I>::get();
454 if pot != deactivated {
455 T::Currency::reactivate(deactivated);
456 T::Currency::deactivate(pot);
457 Deactivated::<T, I>::put(&pot);
458 Self::deposit_event(Event::<T, I>::UpdatedInactive {
459 reactivated: deactivated,
460 deactivated: pot,
461 });
462 }
463
464 let last_spend_period = LastSpendPeriod::<T, I>::get()
466 .unwrap_or_else(|| Self::update_last_spend_period());
470 let blocks_since_last_spend_period = block_number.saturating_sub(last_spend_period);
471 let safe_spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
472
473 let (spend_periods_passed, extra_blocks) = (
475 blocks_since_last_spend_period / safe_spend_period,
476 blocks_since_last_spend_period % safe_spend_period,
477 );
478 let new_last_spend_period = block_number.saturating_sub(extra_blocks);
479 if spend_periods_passed > BlockNumberFor::<T, I>::zero() {
480 Self::spend_funds(spend_periods_passed, new_last_spend_period)
481 } else {
482 Weight::zero()
483 }
484 }
485
486 #[cfg(feature = "try-runtime")]
487 fn try_state(_: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
488 Self::do_try_state()?;
489 Ok(())
490 }
491 }
492
493 #[derive(Default)]
494 struct SpendContext<Balance> {
495 spend_in_context: BTreeMap<Balance, Balance>,
496 }
497
498 #[pallet::call]
499 impl<T: Config<I>, I: 'static> Pallet<T, I> {
500 #[pallet::call_index(3)]
518 #[pallet::weight(T::WeightInfo::spend_local())]
519 #[deprecated(
520 note = "The `spend_local` call will be removed by May 2025. Migrate to the new flow and use the `spend` call."
521 )]
522 #[allow(deprecated)]
523 pub fn spend_local(
524 origin: OriginFor<T>,
525 #[pallet::compact] amount: BalanceOf<T, I>,
526 beneficiary: AccountIdLookupOf<T>,
527 ) -> DispatchResult {
528 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
529 ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission);
530
531 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
532 let context = v.or_default();
533
534 let spend = context.spend_in_context.entry(max_amount).or_default();
539
540 if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) {
542 Err(Error::<T, I>::InsufficientPermission)
543 } else {
544 *spend = spend.saturating_add(amount);
545
546 Ok(())
547 }
548 })
549 .unwrap_or(Ok(()))?;
550
551 let beneficiary = T::Lookup::lookup(beneficiary)?;
552 #[allow(deprecated)]
553 let proposal_index = ProposalCount::<T, I>::get();
554 #[allow(deprecated)]
555 Approvals::<T, I>::try_append(proposal_index)
556 .map_err(|_| Error::<T, I>::TooManyApprovals)?;
557 let proposal = Proposal {
558 proposer: beneficiary.clone(),
559 value: amount,
560 beneficiary: beneficiary.clone(),
561 bond: Default::default(),
562 };
563 #[allow(deprecated)]
564 Proposals::<T, I>::insert(proposal_index, proposal);
565 #[allow(deprecated)]
566 ProposalCount::<T, I>::put(proposal_index + 1);
567
568 Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary });
569 Ok(())
570 }
571
572 #[pallet::call_index(4)]
594 #[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))]
595 #[deprecated(
596 note = "The `remove_approval` call will be removed by May 2025. It associated with the deprecated `spend_local` call."
597 )]
598 #[allow(deprecated)]
599 pub fn remove_approval(
600 origin: OriginFor<T>,
601 #[pallet::compact] proposal_id: ProposalIndex,
602 ) -> DispatchResult {
603 T::RejectOrigin::ensure_origin(origin)?;
604
605 #[allow(deprecated)]
606 Approvals::<T, I>::try_mutate(|v| -> DispatchResult {
607 if let Some(index) = v.iter().position(|x| x == &proposal_id) {
608 v.remove(index);
609 Ok(())
610 } else {
611 Err(Error::<T, I>::ProposalNotApproved.into())
612 }
613 })?;
614
615 Ok(())
616 }
617
618 #[pallet::call_index(5)]
645 #[pallet::weight(T::WeightInfo::spend())]
646 pub fn spend(
647 origin: OriginFor<T>,
648 asset_kind: Box<T::AssetKind>,
649 #[pallet::compact] amount: AssetBalanceOf<T, I>,
650 beneficiary: Box<BeneficiaryLookupOf<T, I>>,
651 valid_from: Option<BlockNumberFor<T, I>>,
652 ) -> DispatchResult {
653 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
654 let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?;
655
656 let now = T::BlockNumberProvider::current_block_number();
657 let valid_from = valid_from.unwrap_or(now);
658 let expire_at = valid_from.saturating_add(T::PayoutPeriod::get());
659 ensure!(expire_at > now, Error::<T, I>::SpendExpired);
660
661 let native_amount =
662 T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone())
663 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
664
665 ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
666
667 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
668 let context = v.or_default();
669 let spend = context.spend_in_context.entry(max_amount).or_default();
674
675 if spend.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
677 Err(Error::<T, I>::InsufficientPermission)
678 } else {
679 *spend = spend.saturating_add(native_amount);
680 Ok(())
681 }
682 })
683 .unwrap_or(Ok(()))?;
684
685 let index = SpendCount::<T, I>::get();
686 Spends::<T, I>::insert(
687 index,
688 SpendStatus {
689 asset_kind: *asset_kind.clone(),
690 amount,
691 beneficiary: beneficiary.clone(),
692 valid_from,
693 expire_at,
694 status: PaymentState::Pending,
695 },
696 );
697 SpendCount::<T, I>::put(index + 1);
698
699 Self::deposit_event(Event::AssetSpendApproved {
700 index,
701 asset_kind: *asset_kind,
702 amount,
703 beneficiary,
704 valid_from,
705 expire_at,
706 });
707 Ok(())
708 }
709
710 #[pallet::call_index(6)]
730 #[pallet::weight(T::WeightInfo::payout())]
731 pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
732 ensure_signed(origin)?;
733 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
734 let now = T::BlockNumberProvider::current_block_number();
735 ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout);
736 ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired);
737 ensure!(
738 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
739 Error::<T, I>::AlreadyAttempted
740 );
741
742 let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount)
743 .map_err(|_| Error::<T, I>::PayoutError)?;
744
745 spend.status = PaymentState::Attempted { id };
746 spend.expire_at = now.saturating_add(T::PayoutPeriod::get());
747 Spends::<T, I>::insert(index, spend);
748
749 Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
750
751 Ok(())
752 }
753
754 #[pallet::call_index(7)]
774 #[pallet::weight(T::WeightInfo::check_status())]
775 pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo {
776 use PaymentState as State;
777 use PaymentStatus as Status;
778
779 ensure_signed(origin)?;
780 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
781 let now = T::BlockNumberProvider::current_block_number();
782
783 if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) {
784 Spends::<T, I>::remove(index);
786 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
787 return Ok(Pays::No.into())
788 }
789
790 let payment_id = match spend.status {
791 State::Attempted { id } => id,
792 _ => return Err(Error::<T, I>::NotAttempted.into()),
793 };
794
795 match T::Paymaster::check_payment(payment_id) {
796 Status::Failure => {
797 spend.status = PaymentState::Failed;
798 Spends::<T, I>::insert(index, spend);
799 Self::deposit_event(Event::<T, I>::PaymentFailed { index, payment_id });
800 },
801 Status::Success | Status::Unknown => {
802 Spends::<T, I>::remove(index);
803 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
804 return Ok(Pays::No.into())
805 },
806 Status::InProgress => return Err(Error::<T, I>::Inconclusive.into()),
807 }
808 return Ok(Pays::Yes.into())
809 }
810
811 #[pallet::call_index(8)]
828 #[pallet::weight(T::WeightInfo::void_spend())]
829 pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
830 T::RejectOrigin::ensure_origin(origin)?;
831 let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
832 ensure!(
833 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
834 Error::<T, I>::AlreadyAttempted
835 );
836
837 Spends::<T, I>::remove(index);
838 Self::deposit_event(Event::<T, I>::AssetSpendVoided { index });
839 Ok(())
840 }
841 }
842}
843
844impl<T: Config<I>, I: 'static> Pallet<T, I> {
845 pub fn account_id() -> T::AccountId {
852 T::PalletId::get().into_account_truncating()
853 }
854
855 fn update_last_spend_period() -> BlockNumberFor<T, I> {
859 let block_number = T::BlockNumberProvider::current_block_number();
860 let spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
861 let time_since_last_spend = block_number % spend_period;
862 let last_spend_period = if time_since_last_spend.is_zero() {
865 block_number.saturating_sub(spend_period)
866 } else {
867 block_number.saturating_sub(time_since_last_spend)
869 };
870 LastSpendPeriod::<T, I>::put(last_spend_period);
871 last_spend_period
872 }
873
874 #[deprecated(
876 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
877 )]
878 pub fn proposal_count() -> ProposalIndex {
879 #[allow(deprecated)]
880 ProposalCount::<T, I>::get()
881 }
882
883 #[deprecated(
885 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
886 )]
887 pub fn proposals(index: ProposalIndex) -> Option<Proposal<T::AccountId, BalanceOf<T, I>>> {
888 #[allow(deprecated)]
889 Proposals::<T, I>::get(index)
890 }
891
892 #[deprecated(
894 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
895 )]
896 #[allow(deprecated)]
897 pub fn approvals() -> BoundedVec<ProposalIndex, T::MaxApprovals> {
898 Approvals::<T, I>::get()
899 }
900
901 pub fn spend_funds(
903 spend_periods_passed: BlockNumberFor<T, I>,
904 new_last_spend_period: BlockNumberFor<T, I>,
905 ) -> Weight {
906 LastSpendPeriod::<T, I>::put(new_last_spend_period);
907 let mut total_weight = Weight::zero();
908
909 let mut budget_remaining = Self::pot();
910 Self::deposit_event(Event::Spending { budget_remaining });
911 let account_id = Self::account_id();
912
913 let mut missed_any = false;
914 let mut imbalance = PositiveImbalanceOf::<T, I>::zero();
915 #[allow(deprecated)]
916 let proposals_len = Approvals::<T, I>::mutate(|v| {
917 let proposals_approvals_len = v.len() as u32;
918 v.retain(|&index| {
919 if let Some(p) = Proposals::<T, I>::get(index) {
921 if p.value <= budget_remaining {
922 budget_remaining -= p.value;
923 Proposals::<T, I>::remove(index);
924
925 let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
927 debug_assert!(err_amount.is_zero());
928
929 imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value));
931
932 Self::deposit_event(Event::Awarded {
933 proposal_index: index,
934 award: p.value,
935 account: p.beneficiary,
936 });
937 false
938 } else {
939 missed_any = true;
940 true
941 }
942 } else {
943 false
944 }
945 });
946 proposals_approvals_len
947 });
948
949 total_weight += T::WeightInfo::on_initialize_proposals(proposals_len);
950
951 T::SpendFunds::spend_funds(
953 &mut budget_remaining,
954 &mut imbalance,
955 &mut total_weight,
956 &mut missed_any,
957 );
958
959 if !missed_any && !T::Burn::get().is_zero() {
960 let one_minus_burn = T::Burn::get().left_from_one();
963 let percent_left =
964 one_minus_burn.saturating_pow(spend_periods_passed.unique_saturated_into());
965 let new_budget_remaining = percent_left * budget_remaining;
966 let burn = budget_remaining.saturating_sub(new_budget_remaining);
967 budget_remaining = new_budget_remaining;
968
969 let (debit, credit) = T::Currency::pair(burn);
970 imbalance.subsume(debit);
971 T::BurnDestination::on_unbalanced(credit);
972 Self::deposit_event(Event::Burnt { burnt_funds: burn })
973 }
974
975 if let Err(problem) =
980 T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive)
981 {
982 print("Inconsistent state - couldn't settle imbalance for funds spent by treasury");
983 drop(problem);
985 }
986
987 Self::deposit_event(Event::Rollover { rollover_balance: budget_remaining });
988
989 total_weight
990 }
991
992 pub fn pot() -> BalanceOf<T, I> {
995 T::Currency::free_balance(&Self::account_id())
996 .saturating_sub(T::Currency::minimum_balance())
998 }
999
1000 #[cfg(any(feature = "try-runtime", test))]
1002 fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1003 Self::try_state_proposals()?;
1004 Self::try_state_spends()?;
1005
1006 Ok(())
1007 }
1008
1009 #[cfg(any(feature = "try-runtime", test))]
1017 fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> {
1018 let current_proposal_count = ProposalCount::<T, I>::get();
1019 ensure!(
1020 current_proposal_count as usize >= Proposals::<T, I>::iter().count(),
1021 "Actual number of proposals exceeds `ProposalCount`."
1022 );
1023
1024 Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult {
1025 ensure!(
1026 current_proposal_count as u32 > proposal_index,
1027 "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`."
1028 );
1029 Ok(())
1030 })?;
1031
1032 Approvals::<T, I>::get()
1033 .iter()
1034 .try_for_each(|proposal_index| -> DispatchResult {
1035 ensure!(
1036 Proposals::<T, I>::contains_key(proposal_index),
1037 "Proposal indices in `Approvals` must also be contained in `Proposals`."
1038 );
1039 Ok(())
1040 })?;
1041
1042 Ok(())
1043 }
1044
1045 #[cfg(any(feature = "try-runtime", test))]
1053 fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> {
1054 let current_spend_count = SpendCount::<T, I>::get();
1055 ensure!(
1056 current_spend_count as usize >= Spends::<T, I>::iter().count(),
1057 "Actual number of spends exceeds `SpendCount`."
1058 );
1059
1060 Spends::<T, I>::iter_keys().try_for_each(|spend_index| -> DispatchResult {
1061 ensure!(
1062 current_spend_count > spend_index,
1063 "`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`."
1064 );
1065 Ok(())
1066 })?;
1067
1068 Spends::<T, I>::iter().try_for_each(|(_index, spend)| -> DispatchResult {
1069 ensure!(
1070 spend.valid_from < spend.expire_at,
1071 "Spend cannot expire before it becomes valid."
1072 );
1073 Ok(())
1074 })?;
1075
1076 Ok(())
1077 }
1078}
1079
1080impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
1081 fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
1082 let numeric_amount = amount.peek();
1083
1084 let _ = T::Currency::resolve_creating(&Self::account_id(), amount);
1086
1087 Self::deposit_event(Event::Deposit { value: numeric_amount });
1088 }
1089}
1090
1091pub struct TreasuryAccountId<R>(PhantomData<R>);
1093impl<R> sp_runtime::traits::TypedGet for TreasuryAccountId<R>
1094where
1095 R: crate::Config,
1096{
1097 type Type = <R as frame_system::Config>::AccountId;
1098 fn get() -> Self::Type {
1099 crate::Pallet::<R>::account_id()
1100 }
1101}