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;
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;
125
126#[impl_trait_for_tuples::impl_for_tuples(30)]
138pub trait SpendFunds<T: Config<I>, I: 'static = ()> {
139 fn spend_funds(
140 budget_remaining: &mut BalanceOf<T, I>,
141 imbalance: &mut PositiveImbalanceOf<T, I>,
142 total_weight: &mut Weight,
143 missed_any: &mut bool,
144 );
145}
146
147pub type ProposalIndex = u32;
149
150#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
152#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
153pub struct Proposal<AccountId, Balance> {
154 proposer: AccountId,
156 value: Balance,
158 beneficiary: AccountId,
160 bond: Balance,
162}
163
164#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
166#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
167pub enum PaymentState<Id> {
168 Pending,
170 Attempted { id: Id },
172 Failed,
174}
175
176#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
178#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
179pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> {
180 asset_kind: AssetKind,
182 amount: AssetBalance,
184 beneficiary: Beneficiary,
186 valid_from: BlockNumber,
188 expire_at: BlockNumber,
190 status: PaymentState<PaymentId>,
192}
193
194pub type SpendIndex = u32;
196
197#[frame_support::pallet]
198pub mod pallet {
199 use super::*;
200 use frame_support::{
201 dispatch_context::with_context,
202 pallet_prelude::*,
203 traits::tokens::{ConversionFromAssetBalance, PaymentStatus},
204 };
205 use frame_system::pallet_prelude::*;
206
207 #[pallet::pallet]
208 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
209
210 #[pallet::config]
211 pub trait Config<I: 'static = ()>: frame_system::Config {
212 type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
214
215 type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
217
218 type RuntimeEvent: From<Event<Self, I>>
220 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
221
222 #[pallet::constant]
224 type SpendPeriod: Get<BlockNumberFor<Self>>;
225
226 #[pallet::constant]
228 type Burn: Get<Permill>;
229
230 #[pallet::constant]
232 type PalletId: Get<PalletId>;
233
234 type BurnDestination: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
236
237 type WeightInfo: WeightInfo;
239
240 type SpendFunds: SpendFunds<Self, I>;
242
243 #[pallet::constant]
250 type MaxApprovals: Get<u32>;
251
252 type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>;
256
257 type AssetKind: Parameter + MaxEncodedLen;
259
260 type Beneficiary: Parameter + MaxEncodedLen;
262
263 type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
265
266 type Paymaster: Pay<Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind>;
268
269 type BalanceConverter: ConversionFromAssetBalance<
273 <Self::Paymaster as Pay>::Balance,
274 Self::AssetKind,
275 BalanceOf<Self, I>,
276 >;
277
278 #[pallet::constant]
280 type PayoutPeriod: Get<BlockNumberFor<Self>>;
281
282 #[cfg(feature = "runtime-benchmarks")]
284 type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>;
285
286 type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
288 }
289
290 #[pallet::storage]
295 pub type ProposalCount<T, I = ()> = StorageValue<_, ProposalIndex, ValueQuery>;
296
297 #[pallet::storage]
302 pub type Proposals<T: Config<I>, I: 'static = ()> = StorageMap<
303 _,
304 Twox64Concat,
305 ProposalIndex,
306 Proposal<T::AccountId, BalanceOf<T, I>>,
307 OptionQuery,
308 >;
309
310 #[pallet::storage]
312 pub type Deactivated<T: Config<I>, I: 'static = ()> =
313 StorageValue<_, BalanceOf<T, I>, ValueQuery>;
314
315 #[pallet::storage]
320 pub type Approvals<T: Config<I>, I: 'static = ()> =
321 StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
322
323 #[pallet::storage]
325 pub(crate) type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>;
326
327 #[pallet::storage]
330 pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap<
331 _,
332 Twox64Concat,
333 SpendIndex,
334 SpendStatus<
335 T::AssetKind,
336 AssetBalanceOf<T, I>,
337 T::Beneficiary,
338 BlockNumberFor<T>,
339 <T::Paymaster as Pay>::Id,
340 >,
341 OptionQuery,
342 >;
343
344 #[pallet::storage]
346 pub(crate) type LastSpendPeriod<T, I = ()> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
347
348 #[pallet::genesis_config]
349 #[derive(frame_support::DefaultNoBound)]
350 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
351 #[serde(skip)]
352 _config: core::marker::PhantomData<(T, I)>,
353 }
354
355 #[pallet::genesis_build]
356 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
357 fn build(&self) {
358 let account_id = Pallet::<T, I>::account_id();
360 let min = T::Currency::minimum_balance();
361 if T::Currency::free_balance(&account_id) < min {
362 let _ = T::Currency::make_free_balance_be(&account_id, min);
363 }
364 }
365 }
366
367 #[pallet::event]
368 #[pallet::generate_deposit(pub(super) fn deposit_event)]
369 pub enum Event<T: Config<I>, I: 'static = ()> {
370 Spending { budget_remaining: BalanceOf<T, I> },
372 Awarded { proposal_index: ProposalIndex, award: BalanceOf<T, I>, account: T::AccountId },
374 Burnt { burnt_funds: BalanceOf<T, I> },
376 Rollover { rollover_balance: BalanceOf<T, I> },
378 Deposit { value: BalanceOf<T, I> },
380 SpendApproved {
382 proposal_index: ProposalIndex,
383 amount: BalanceOf<T, I>,
384 beneficiary: T::AccountId,
385 },
386 UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> },
388 AssetSpendApproved {
390 index: SpendIndex,
391 asset_kind: T::AssetKind,
392 amount: AssetBalanceOf<T, I>,
393 beneficiary: T::Beneficiary,
394 valid_from: BlockNumberFor<T>,
395 expire_at: BlockNumberFor<T>,
396 },
397 AssetSpendVoided { index: SpendIndex },
399 Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
401 PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
403 SpendProcessed { index: SpendIndex },
406 }
407
408 #[pallet::error]
410 pub enum Error<T, I = ()> {
411 InvalidIndex,
413 TooManyApprovals,
415 InsufficientPermission,
418 ProposalNotApproved,
420 FailedToConvertBalance,
422 SpendExpired,
424 EarlyPayout,
426 AlreadyAttempted,
428 PayoutError,
430 NotAttempted,
432 Inconclusive,
434 }
435
436 #[pallet::hooks]
437 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
438 fn on_initialize(_do_not_use_local_block_number: BlockNumberFor<T>) -> Weight {
441 let block_number = T::BlockNumberProvider::current_block_number();
442 let pot = Self::pot();
443 let deactivated = Deactivated::<T, I>::get();
444 if pot != deactivated {
445 T::Currency::reactivate(deactivated);
446 T::Currency::deactivate(pot);
447 Deactivated::<T, I>::put(&pot);
448 Self::deposit_event(Event::<T, I>::UpdatedInactive {
449 reactivated: deactivated,
450 deactivated: pot,
451 });
452 }
453
454 let last_spend_period = LastSpendPeriod::<T, I>::get()
456 .unwrap_or_else(|| Self::update_last_spend_period());
460 let blocks_since_last_spend_period = block_number.saturating_sub(last_spend_period);
461 let safe_spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T>::one());
462
463 let (spend_periods_passed, extra_blocks) = (
465 blocks_since_last_spend_period / safe_spend_period,
466 blocks_since_last_spend_period % safe_spend_period,
467 );
468 let new_last_spend_period = block_number.saturating_sub(extra_blocks);
469 if spend_periods_passed > BlockNumberFor::<T>::zero() {
470 Self::spend_funds(spend_periods_passed, new_last_spend_period)
471 } else {
472 Weight::zero()
473 }
474 }
475
476 #[cfg(feature = "try-runtime")]
477 fn try_state(_: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
478 Self::do_try_state()?;
479 Ok(())
480 }
481 }
482
483 #[derive(Default)]
484 struct SpendContext<Balance> {
485 spend_in_context: BTreeMap<Balance, Balance>,
486 }
487
488 #[pallet::call]
489 impl<T: Config<I>, I: 'static> Pallet<T, I> {
490 #[pallet::call_index(3)]
508 #[pallet::weight(T::WeightInfo::spend_local())]
509 #[deprecated(
510 note = "The `spend_local` call will be removed by May 2025. Migrate to the new flow and use the `spend` call."
511 )]
512 #[allow(deprecated)]
513 pub fn spend_local(
514 origin: OriginFor<T>,
515 #[pallet::compact] amount: BalanceOf<T, I>,
516 beneficiary: AccountIdLookupOf<T>,
517 ) -> DispatchResult {
518 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
519 ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission);
520
521 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
522 let context = v.or_default();
523
524 let spend = context.spend_in_context.entry(max_amount).or_default();
529
530 if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) {
532 Err(Error::<T, I>::InsufficientPermission)
533 } else {
534 *spend = spend.saturating_add(amount);
535
536 Ok(())
537 }
538 })
539 .unwrap_or(Ok(()))?;
540
541 let beneficiary = T::Lookup::lookup(beneficiary)?;
542 #[allow(deprecated)]
543 let proposal_index = ProposalCount::<T, I>::get();
544 #[allow(deprecated)]
545 Approvals::<T, I>::try_append(proposal_index)
546 .map_err(|_| Error::<T, I>::TooManyApprovals)?;
547 let proposal = Proposal {
548 proposer: beneficiary.clone(),
549 value: amount,
550 beneficiary: beneficiary.clone(),
551 bond: Default::default(),
552 };
553 #[allow(deprecated)]
554 Proposals::<T, I>::insert(proposal_index, proposal);
555 #[allow(deprecated)]
556 ProposalCount::<T, I>::put(proposal_index + 1);
557
558 Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary });
559 Ok(())
560 }
561
562 #[pallet::call_index(4)]
584 #[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))]
585 #[deprecated(
586 note = "The `remove_approval` call will be removed by May 2025. It associated with the deprecated `spend_local` call."
587 )]
588 #[allow(deprecated)]
589 pub fn remove_approval(
590 origin: OriginFor<T>,
591 #[pallet::compact] proposal_id: ProposalIndex,
592 ) -> DispatchResult {
593 T::RejectOrigin::ensure_origin(origin)?;
594
595 #[allow(deprecated)]
596 Approvals::<T, I>::try_mutate(|v| -> DispatchResult {
597 if let Some(index) = v.iter().position(|x| x == &proposal_id) {
598 v.remove(index);
599 Ok(())
600 } else {
601 Err(Error::<T, I>::ProposalNotApproved.into())
602 }
603 })?;
604
605 Ok(())
606 }
607
608 #[pallet::call_index(5)]
635 #[pallet::weight(T::WeightInfo::spend())]
636 pub fn spend(
637 origin: OriginFor<T>,
638 asset_kind: Box<T::AssetKind>,
639 #[pallet::compact] amount: AssetBalanceOf<T, I>,
640 beneficiary: Box<BeneficiaryLookupOf<T, I>>,
641 valid_from: Option<BlockNumberFor<T>>,
642 ) -> DispatchResult {
643 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
644 let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?;
645
646 let now = T::BlockNumberProvider::current_block_number();
647 let valid_from = valid_from.unwrap_or(now);
648 let expire_at = valid_from.saturating_add(T::PayoutPeriod::get());
649 ensure!(expire_at > now, Error::<T, I>::SpendExpired);
650
651 let native_amount =
652 T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone())
653 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
654
655 ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
656
657 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
658 let context = v.or_default();
659 let spend = context.spend_in_context.entry(max_amount).or_default();
664
665 if spend.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
667 Err(Error::<T, I>::InsufficientPermission)
668 } else {
669 *spend = spend.saturating_add(native_amount);
670 Ok(())
671 }
672 })
673 .unwrap_or(Ok(()))?;
674
675 let index = SpendCount::<T, I>::get();
676 Spends::<T, I>::insert(
677 index,
678 SpendStatus {
679 asset_kind: *asset_kind.clone(),
680 amount,
681 beneficiary: beneficiary.clone(),
682 valid_from,
683 expire_at,
684 status: PaymentState::Pending,
685 },
686 );
687 SpendCount::<T, I>::put(index + 1);
688
689 Self::deposit_event(Event::AssetSpendApproved {
690 index,
691 asset_kind: *asset_kind,
692 amount,
693 beneficiary,
694 valid_from,
695 expire_at,
696 });
697 Ok(())
698 }
699
700 #[pallet::call_index(6)]
720 #[pallet::weight(T::WeightInfo::payout())]
721 pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
722 ensure_signed(origin)?;
723 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
724 let now = T::BlockNumberProvider::current_block_number();
725 ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout);
726 ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired);
727 ensure!(
728 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
729 Error::<T, I>::AlreadyAttempted
730 );
731
732 let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount)
733 .map_err(|_| Error::<T, I>::PayoutError)?;
734
735 spend.status = PaymentState::Attempted { id };
736 Spends::<T, I>::insert(index, spend);
737
738 Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
739
740 Ok(())
741 }
742
743 #[pallet::call_index(7)]
763 #[pallet::weight(T::WeightInfo::check_status())]
764 pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo {
765 use PaymentState as State;
766 use PaymentStatus as Status;
767
768 ensure_signed(origin)?;
769 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
770 let now = T::BlockNumberProvider::current_block_number();
771
772 if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) {
773 Spends::<T, I>::remove(index);
775 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
776 return Ok(Pays::No.into())
777 }
778
779 let payment_id = match spend.status {
780 State::Attempted { id } => id,
781 _ => return Err(Error::<T, I>::NotAttempted.into()),
782 };
783
784 match T::Paymaster::check_payment(payment_id) {
785 Status::Failure => {
786 spend.status = PaymentState::Failed;
787 Spends::<T, I>::insert(index, spend);
788 Self::deposit_event(Event::<T, I>::PaymentFailed { index, payment_id });
789 },
790 Status::Success | Status::Unknown => {
791 Spends::<T, I>::remove(index);
792 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
793 return Ok(Pays::No.into())
794 },
795 Status::InProgress => return Err(Error::<T, I>::Inconclusive.into()),
796 }
797 return Ok(Pays::Yes.into())
798 }
799
800 #[pallet::call_index(8)]
817 #[pallet::weight(T::WeightInfo::void_spend())]
818 pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
819 T::RejectOrigin::ensure_origin(origin)?;
820 let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
821 ensure!(
822 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
823 Error::<T, I>::AlreadyAttempted
824 );
825
826 Spends::<T, I>::remove(index);
827 Self::deposit_event(Event::<T, I>::AssetSpendVoided { index });
828 Ok(())
829 }
830 }
831}
832
833impl<T: Config<I>, I: 'static> Pallet<T, I> {
834 pub fn account_id() -> T::AccountId {
841 T::PalletId::get().into_account_truncating()
842 }
843
844 fn update_last_spend_period() -> BlockNumberFor<T> {
848 let block_number = T::BlockNumberProvider::current_block_number();
849 let spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T>::one());
850 let time_since_last_spend = block_number % spend_period;
851 let last_spend_period = if time_since_last_spend.is_zero() {
854 block_number.saturating_sub(spend_period)
855 } else {
856 block_number.saturating_sub(time_since_last_spend)
858 };
859 LastSpendPeriod::<T, I>::put(last_spend_period);
860 last_spend_period
861 }
862
863 #[deprecated(
865 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
866 )]
867 pub fn proposal_count() -> ProposalIndex {
868 #[allow(deprecated)]
869 ProposalCount::<T, I>::get()
870 }
871
872 #[deprecated(
874 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
875 )]
876 pub fn proposals(index: ProposalIndex) -> Option<Proposal<T::AccountId, BalanceOf<T, I>>> {
877 #[allow(deprecated)]
878 Proposals::<T, I>::get(index)
879 }
880
881 #[deprecated(
883 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
884 )]
885 #[allow(deprecated)]
886 pub fn approvals() -> BoundedVec<ProposalIndex, T::MaxApprovals> {
887 Approvals::<T, I>::get()
888 }
889
890 pub fn spend_funds(
892 spend_periods_passed: BlockNumberFor<T>,
893 new_last_spend_period: BlockNumberFor<T>,
894 ) -> Weight {
895 LastSpendPeriod::<T, I>::put(new_last_spend_period);
896 let mut total_weight = Weight::zero();
897
898 let mut budget_remaining = Self::pot();
899 Self::deposit_event(Event::Spending { budget_remaining });
900 let account_id = Self::account_id();
901
902 let mut missed_any = false;
903 let mut imbalance = PositiveImbalanceOf::<T, I>::zero();
904 #[allow(deprecated)]
905 let proposals_len = Approvals::<T, I>::mutate(|v| {
906 let proposals_approvals_len = v.len() as u32;
907 v.retain(|&index| {
908 if let Some(p) = Proposals::<T, I>::get(index) {
910 if p.value <= budget_remaining {
911 budget_remaining -= p.value;
912 Proposals::<T, I>::remove(index);
913
914 let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
916 debug_assert!(err_amount.is_zero());
917
918 imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value));
920
921 Self::deposit_event(Event::Awarded {
922 proposal_index: index,
923 award: p.value,
924 account: p.beneficiary,
925 });
926 false
927 } else {
928 missed_any = true;
929 true
930 }
931 } else {
932 false
933 }
934 });
935 proposals_approvals_len
936 });
937
938 total_weight += T::WeightInfo::on_initialize_proposals(proposals_len);
939
940 T::SpendFunds::spend_funds(
942 &mut budget_remaining,
943 &mut imbalance,
944 &mut total_weight,
945 &mut missed_any,
946 );
947
948 if !missed_any && !T::Burn::get().is_zero() {
949 let one_minus_burn = T::Burn::get().left_from_one();
952 let percent_left =
953 one_minus_burn.saturating_pow(spend_periods_passed.unique_saturated_into());
954 let new_budget_remaining = percent_left * budget_remaining;
955 let burn = budget_remaining.saturating_sub(new_budget_remaining);
956 budget_remaining = new_budget_remaining;
957
958 let (debit, credit) = T::Currency::pair(burn);
959 imbalance.subsume(debit);
960 T::BurnDestination::on_unbalanced(credit);
961 Self::deposit_event(Event::Burnt { burnt_funds: burn })
962 }
963
964 if let Err(problem) =
969 T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive)
970 {
971 print("Inconsistent state - couldn't settle imbalance for funds spent by treasury");
972 drop(problem);
974 }
975
976 Self::deposit_event(Event::Rollover { rollover_balance: budget_remaining });
977
978 total_weight
979 }
980
981 pub fn pot() -> BalanceOf<T, I> {
984 T::Currency::free_balance(&Self::account_id())
985 .saturating_sub(T::Currency::minimum_balance())
987 }
988
989 #[cfg(any(feature = "try-runtime", test))]
991 fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
992 Self::try_state_proposals()?;
993 Self::try_state_spends()?;
994
995 Ok(())
996 }
997
998 #[cfg(any(feature = "try-runtime", test))]
1006 fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> {
1007 let current_proposal_count = ProposalCount::<T, I>::get();
1008 ensure!(
1009 current_proposal_count as usize >= Proposals::<T, I>::iter().count(),
1010 "Actual number of proposals exceeds `ProposalCount`."
1011 );
1012
1013 Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult {
1014 ensure!(
1015 current_proposal_count as u32 > proposal_index,
1016 "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`."
1017 );
1018 Ok(())
1019 })?;
1020
1021 Approvals::<T, I>::get()
1022 .iter()
1023 .try_for_each(|proposal_index| -> DispatchResult {
1024 ensure!(
1025 Proposals::<T, I>::contains_key(proposal_index),
1026 "Proposal indices in `Approvals` must also be contained in `Proposals`."
1027 );
1028 Ok(())
1029 })?;
1030
1031 Ok(())
1032 }
1033
1034 #[cfg(any(feature = "try-runtime", test))]
1042 fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> {
1043 let current_spend_count = SpendCount::<T, I>::get();
1044 ensure!(
1045 current_spend_count as usize >= Spends::<T, I>::iter().count(),
1046 "Actual number of spends exceeds `SpendCount`."
1047 );
1048
1049 Spends::<T, I>::iter_keys().try_for_each(|spend_index| -> DispatchResult {
1050 ensure!(
1051 current_spend_count > spend_index,
1052 "`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`."
1053 );
1054 Ok(())
1055 })?;
1056
1057 Spends::<T, I>::iter().try_for_each(|(_index, spend)| -> DispatchResult {
1058 ensure!(
1059 spend.valid_from < spend.expire_at,
1060 "Spend cannot expire before it becomes valid."
1061 );
1062 Ok(())
1063 })?;
1064
1065 Ok(())
1066 }
1067}
1068
1069impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
1070 fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
1071 let numeric_amount = amount.peek();
1072
1073 let _ = T::Currency::resolve_creating(&Self::account_id(), amount);
1075
1076 Self::deposit_event(Event::Deposit { value: numeric_amount });
1077 }
1078}
1079
1080pub struct TreasuryAccountId<R>(PhantomData<R>);
1082impl<R> sp_runtime::traits::TypedGet for TreasuryAccountId<R>
1083where
1084 R: crate::Config,
1085{
1086 type Type = <R as frame_system::Config>::AccountId;
1087 fn get() -> Self::Type {
1088 crate::Pallet::<R>::account_id()
1089 }
1090}