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, DecodeWithMemTracking, 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 Debug, PerThing, Permill,
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(
155 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
156)]
157pub struct Proposal<AccountId, Balance> {
158 pub proposer: AccountId,
160 pub value: Balance,
162 pub beneficiary: AccountId,
164 pub bond: Balance,
166}
167
168#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
170#[derive(
171 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
172)]
173pub enum PaymentState<Id> {
174 Pending,
176 Attempted { id: Id },
178 Failed,
180}
181
182#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
184#[derive(
185 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
186)]
187pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> {
188 pub asset_kind: AssetKind,
190 pub amount: AssetBalance,
192 pub beneficiary: Beneficiary,
194 pub valid_from: BlockNumber,
196 pub expire_at: BlockNumber,
198 pub status: PaymentState<PaymentId>,
200}
201
202pub type SpendIndex = u32;
204
205#[frame_support::pallet]
206pub mod pallet {
207 use super::*;
208 use frame_support::{
209 dispatch_context::with_context,
210 pallet_prelude::*,
211 traits::tokens::{ConversionFromAssetBalance, PaymentStatus},
212 };
213 use frame_system::pallet_prelude::{ensure_signed, OriginFor};
214
215 #[pallet::pallet]
216 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
217
218 #[pallet::config]
219 pub trait Config<I: 'static = ()>: frame_system::Config {
220 type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
222
223 type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
225
226 #[allow(deprecated)]
228 type RuntimeEvent: From<Event<Self, I>>
229 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
230
231 #[pallet::constant]
233 type SpendPeriod: Get<BlockNumberFor<Self, I>>;
234
235 #[pallet::constant]
237 type Burn: Get<Permill>;
238
239 #[pallet::constant]
241 type PalletId: Get<PalletId>;
242
243 type BurnDestination: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
245
246 type WeightInfo: WeightInfo;
248
249 type SpendFunds: SpendFunds<Self, I>;
251
252 #[pallet::constant]
259 type MaxApprovals: Get<u32>;
260
261 type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>;
265
266 type AssetKind: Parameter + MaxEncodedLen;
268
269 type Beneficiary: Parameter + MaxEncodedLen;
271
272 type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
274
275 type Paymaster: Pay<Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind>;
277
278 type BalanceConverter: ConversionFromAssetBalance<
282 <Self::Paymaster as Pay>::Balance,
283 Self::AssetKind,
284 BalanceOf<Self, I>,
285 >;
286
287 #[pallet::constant]
289 type PayoutPeriod: Get<BlockNumberFor<Self, I>>;
290
291 #[cfg(feature = "runtime-benchmarks")]
293 type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>;
294
295 type BlockNumberProvider: BlockNumberProvider;
297 }
298
299 #[pallet::extra_constants]
300 impl<T: Config<I>, I: 'static> Pallet<T, I> {
301 fn pot_account() -> T::AccountId {
303 Self::account_id()
304 }
305 }
306
307 #[pallet::storage]
312 pub type ProposalCount<T, I = ()> = StorageValue<_, ProposalIndex, ValueQuery>;
313
314 #[pallet::storage]
319 pub type Proposals<T: Config<I>, I: 'static = ()> = StorageMap<
320 _,
321 Twox64Concat,
322 ProposalIndex,
323 Proposal<T::AccountId, BalanceOf<T, I>>,
324 OptionQuery,
325 >;
326
327 #[pallet::storage]
329 pub type Deactivated<T: Config<I>, I: 'static = ()> =
330 StorageValue<_, BalanceOf<T, I>, ValueQuery>;
331
332 #[pallet::storage]
337 pub type Approvals<T: Config<I>, I: 'static = ()> =
338 StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
339
340 #[pallet::storage]
342 pub type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>;
343
344 #[pallet::storage]
347 pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap<
348 _,
349 Twox64Concat,
350 SpendIndex,
351 SpendStatus<
352 T::AssetKind,
353 AssetBalanceOf<T, I>,
354 T::Beneficiary,
355 BlockNumberFor<T, I>,
356 <T::Paymaster as Pay>::Id,
357 >,
358 OptionQuery,
359 >;
360
361 #[pallet::storage]
363 pub type LastSpendPeriod<T, I = ()> = StorageValue<_, BlockNumberFor<T, I>, OptionQuery>;
364
365 #[pallet::genesis_config]
366 #[derive(frame_support::DefaultNoBound)]
367 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
368 #[serde(skip)]
369 _config: core::marker::PhantomData<(T, I)>,
370 }
371
372 #[pallet::genesis_build]
373 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
374 fn build(&self) {
375 let account_id = Pallet::<T, I>::account_id();
377 let min = T::Currency::minimum_balance();
378 if T::Currency::free_balance(&account_id) < min {
379 let _ = T::Currency::make_free_balance_be(&account_id, min);
380 }
381 }
382 }
383
384 #[pallet::event]
385 #[pallet::generate_deposit(pub(super) fn deposit_event)]
386 pub enum Event<T: Config<I>, I: 'static = ()> {
387 Spending { budget_remaining: BalanceOf<T, I> },
389 Awarded { proposal_index: ProposalIndex, award: BalanceOf<T, I>, account: T::AccountId },
391 Burnt { burnt_funds: BalanceOf<T, I> },
393 Rollover { rollover_balance: BalanceOf<T, I> },
395 Deposit { value: BalanceOf<T, I> },
397 SpendApproved {
399 proposal_index: ProposalIndex,
400 amount: BalanceOf<T, I>,
401 beneficiary: T::AccountId,
402 },
403 UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> },
405 AssetSpendApproved {
407 index: SpendIndex,
408 asset_kind: T::AssetKind,
409 amount: AssetBalanceOf<T, I>,
410 beneficiary: T::Beneficiary,
411 valid_from: BlockNumberFor<T, I>,
412 expire_at: BlockNumberFor<T, I>,
413 },
414 AssetSpendVoided { index: SpendIndex },
416 Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
418 PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
420 SpendProcessed { index: SpendIndex },
423 }
424
425 #[pallet::error]
427 pub enum Error<T, I = ()> {
428 InvalidIndex,
430 TooManyApprovals,
432 InsufficientPermission,
435 ProposalNotApproved,
437 FailedToConvertBalance,
439 SpendExpired,
441 EarlyPayout,
443 AlreadyAttempted,
445 PayoutError,
447 NotAttempted,
449 Inconclusive,
451 }
452
453 #[pallet::hooks]
454 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
455 fn on_initialize(_do_not_use_local_block_number: SystemBlockNumberFor<T>) -> Weight {
458 let block_number = T::BlockNumberProvider::current_block_number();
459 let pot = Self::pot();
460 let deactivated = Deactivated::<T, I>::get();
461 if pot != deactivated {
462 T::Currency::reactivate(deactivated);
463 T::Currency::deactivate(pot);
464 Deactivated::<T, I>::put(&pot);
465 Self::deposit_event(Event::<T, I>::UpdatedInactive {
466 reactivated: deactivated,
467 deactivated: pot,
468 });
469 }
470
471 let last_spend_period = LastSpendPeriod::<T, I>::get()
473 .unwrap_or_else(|| Self::update_last_spend_period());
477 let blocks_since_last_spend_period = block_number.saturating_sub(last_spend_period);
478 let safe_spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
479
480 let (spend_periods_passed, extra_blocks) = (
482 blocks_since_last_spend_period / safe_spend_period,
483 blocks_since_last_spend_period % safe_spend_period,
484 );
485 let new_last_spend_period = block_number.saturating_sub(extra_blocks);
486 if spend_periods_passed > BlockNumberFor::<T, I>::zero() {
487 Self::spend_funds(spend_periods_passed, new_last_spend_period)
488 } else {
489 Weight::zero()
490 }
491 }
492
493 #[cfg(feature = "try-runtime")]
494 fn try_state(_: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
495 Self::do_try_state()?;
496 Ok(())
497 }
498 }
499
500 #[derive(Default)]
501 struct SpendContext<Balance> {
502 spend_in_context: BTreeMap<Balance, Balance>,
503 }
504
505 #[pallet::call]
506 impl<T: Config<I>, I: 'static> Pallet<T, I> {
507 #[pallet::call_index(3)]
525 #[pallet::weight(T::WeightInfo::spend_local())]
526 #[deprecated(
527 note = "The `spend_local` call will be removed by May 2025. Migrate to the new flow and use the `spend` call."
528 )]
529 #[allow(deprecated)]
530 pub fn spend_local(
531 origin: OriginFor<T>,
532 #[pallet::compact] amount: BalanceOf<T, I>,
533 beneficiary: AccountIdLookupOf<T>,
534 ) -> DispatchResult {
535 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
536 ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission);
537
538 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
539 let context = v.or_default();
540
541 let spend = context.spend_in_context.entry(max_amount).or_default();
546
547 if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) {
549 Err(Error::<T, I>::InsufficientPermission)
550 } else {
551 *spend = spend.saturating_add(amount);
552
553 Ok(())
554 }
555 })
556 .unwrap_or(Ok(()))?;
557
558 let beneficiary = T::Lookup::lookup(beneficiary)?;
559 #[allow(deprecated)]
560 let proposal_index = ProposalCount::<T, I>::get();
561 #[allow(deprecated)]
562 Approvals::<T, I>::try_append(proposal_index)
563 .map_err(|_| Error::<T, I>::TooManyApprovals)?;
564 let proposal = Proposal {
565 proposer: beneficiary.clone(),
566 value: amount,
567 beneficiary: beneficiary.clone(),
568 bond: Default::default(),
569 };
570 #[allow(deprecated)]
571 Proposals::<T, I>::insert(proposal_index, proposal);
572 #[allow(deprecated)]
573 ProposalCount::<T, I>::put(proposal_index + 1);
574
575 Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary });
576 Ok(())
577 }
578
579 #[pallet::call_index(4)]
601 #[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))]
602 #[deprecated(
603 note = "The `remove_approval` call will be removed by May 2025. It associated with the deprecated `spend_local` call."
604 )]
605 #[allow(deprecated)]
606 pub fn remove_approval(
607 origin: OriginFor<T>,
608 #[pallet::compact] proposal_id: ProposalIndex,
609 ) -> DispatchResult {
610 T::RejectOrigin::ensure_origin(origin)?;
611
612 #[allow(deprecated)]
613 Approvals::<T, I>::try_mutate(|v| -> DispatchResult {
614 if let Some(index) = v.iter().position(|x| x == &proposal_id) {
615 v.remove(index);
616 Ok(())
617 } else {
618 Err(Error::<T, I>::ProposalNotApproved.into())
619 }
620 })?;
621
622 Ok(())
623 }
624
625 #[pallet::call_index(5)]
652 #[pallet::weight(T::WeightInfo::spend())]
653 pub fn spend(
654 origin: OriginFor<T>,
655 asset_kind: Box<T::AssetKind>,
656 #[pallet::compact] amount: AssetBalanceOf<T, I>,
657 beneficiary: Box<BeneficiaryLookupOf<T, I>>,
658 valid_from: Option<BlockNumberFor<T, I>>,
659 ) -> DispatchResult {
660 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
661 let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?;
662
663 let now = T::BlockNumberProvider::current_block_number();
664 let valid_from = valid_from.unwrap_or(now);
665 let expire_at = valid_from.saturating_add(T::PayoutPeriod::get());
666 ensure!(expire_at > now, Error::<T, I>::SpendExpired);
667
668 let native_amount =
669 T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone())
670 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
671
672 ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
673
674 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
675 let context = v.or_default();
676 let spend = context.spend_in_context.entry(max_amount).or_default();
681
682 if spend.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
684 Err(Error::<T, I>::InsufficientPermission)
685 } else {
686 *spend = spend.saturating_add(native_amount);
687 Ok(())
688 }
689 })
690 .unwrap_or(Ok(()))?;
691
692 let index = SpendCount::<T, I>::get();
693 Spends::<T, I>::insert(
694 index,
695 SpendStatus {
696 asset_kind: *asset_kind.clone(),
697 amount,
698 beneficiary: beneficiary.clone(),
699 valid_from,
700 expire_at,
701 status: PaymentState::Pending,
702 },
703 );
704 SpendCount::<T, I>::put(index + 1);
705
706 Self::deposit_event(Event::AssetSpendApproved {
707 index,
708 asset_kind: *asset_kind,
709 amount,
710 beneficiary,
711 valid_from,
712 expire_at,
713 });
714 Ok(())
715 }
716
717 #[pallet::call_index(6)]
737 #[pallet::weight(T::WeightInfo::payout())]
738 pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
739 ensure_signed(origin)?;
740 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
741 let now = T::BlockNumberProvider::current_block_number();
742 ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout);
743 ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired);
744 ensure!(
745 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
746 Error::<T, I>::AlreadyAttempted
747 );
748
749 let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount)
750 .map_err(|_| Error::<T, I>::PayoutError)?;
751
752 spend.status = PaymentState::Attempted { id };
753 spend.expire_at = now.saturating_add(T::PayoutPeriod::get());
754 Spends::<T, I>::insert(index, spend);
755
756 Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
757
758 Ok(())
759 }
760
761 #[pallet::call_index(7)]
781 #[pallet::weight(T::WeightInfo::check_status())]
782 pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo {
783 use PaymentState as State;
784 use PaymentStatus as Status;
785
786 ensure_signed(origin)?;
787 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
788 let now = T::BlockNumberProvider::current_block_number();
789
790 if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) {
791 Spends::<T, I>::remove(index);
793 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
794 return Ok(Pays::No.into())
795 }
796
797 let payment_id = match spend.status {
798 State::Attempted { id } => id,
799 _ => return Err(Error::<T, I>::NotAttempted.into()),
800 };
801
802 match T::Paymaster::check_payment(payment_id) {
803 Status::Failure => {
804 spend.status = PaymentState::Failed;
805 Spends::<T, I>::insert(index, spend);
806 Self::deposit_event(Event::<T, I>::PaymentFailed { index, payment_id });
807 },
808 Status::Success | Status::Unknown => {
809 Spends::<T, I>::remove(index);
810 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
811 return Ok(Pays::No.into())
812 },
813 Status::InProgress => return Err(Error::<T, I>::Inconclusive.into()),
814 }
815 return Ok(Pays::Yes.into())
816 }
817
818 #[pallet::call_index(8)]
835 #[pallet::weight(T::WeightInfo::void_spend())]
836 pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
837 T::RejectOrigin::ensure_origin(origin)?;
838 let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
839 ensure!(
840 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
841 Error::<T, I>::AlreadyAttempted
842 );
843
844 Spends::<T, I>::remove(index);
845 Self::deposit_event(Event::<T, I>::AssetSpendVoided { index });
846 Ok(())
847 }
848 }
849}
850
851impl<T: Config<I>, I: 'static> Pallet<T, I> {
852 pub fn account_id() -> T::AccountId {
859 T::PalletId::get().into_account_truncating()
860 }
861
862 fn update_last_spend_period() -> BlockNumberFor<T, I> {
866 let block_number = T::BlockNumberProvider::current_block_number();
867 let spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
868 let time_since_last_spend = block_number % spend_period;
869 let last_spend_period = if time_since_last_spend.is_zero() {
872 block_number.saturating_sub(spend_period)
873 } else {
874 block_number.saturating_sub(time_since_last_spend)
876 };
877 LastSpendPeriod::<T, I>::put(last_spend_period);
878 last_spend_period
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 pub fn proposal_count() -> ProposalIndex {
886 #[allow(deprecated)]
887 ProposalCount::<T, I>::get()
888 }
889
890 #[deprecated(
892 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
893 )]
894 pub fn proposals(index: ProposalIndex) -> Option<Proposal<T::AccountId, BalanceOf<T, I>>> {
895 #[allow(deprecated)]
896 Proposals::<T, I>::get(index)
897 }
898
899 #[deprecated(
901 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
902 )]
903 #[allow(deprecated)]
904 pub fn approvals() -> BoundedVec<ProposalIndex, T::MaxApprovals> {
905 Approvals::<T, I>::get()
906 }
907
908 pub fn spend_funds(
910 spend_periods_passed: BlockNumberFor<T, I>,
911 new_last_spend_period: BlockNumberFor<T, I>,
912 ) -> Weight {
913 LastSpendPeriod::<T, I>::put(new_last_spend_period);
914 let mut total_weight = Weight::zero();
915
916 let mut budget_remaining = Self::pot();
917 Self::deposit_event(Event::Spending { budget_remaining });
918 let account_id = Self::account_id();
919
920 let mut missed_any = false;
921 let mut imbalance = PositiveImbalanceOf::<T, I>::zero();
922 #[allow(deprecated)]
923 let proposals_len = Approvals::<T, I>::mutate(|v| {
924 let proposals_approvals_len = v.len() as u32;
925 v.retain(|&index| {
926 if let Some(p) = Proposals::<T, I>::get(index) {
928 if p.value <= budget_remaining {
929 budget_remaining -= p.value;
930 Proposals::<T, I>::remove(index);
931
932 let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
934 debug_assert!(err_amount.is_zero());
935
936 imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value));
938
939 Self::deposit_event(Event::Awarded {
940 proposal_index: index,
941 award: p.value,
942 account: p.beneficiary,
943 });
944 false
945 } else {
946 missed_any = true;
947 true
948 }
949 } else {
950 false
951 }
952 });
953 proposals_approvals_len
954 });
955
956 total_weight += T::WeightInfo::on_initialize_proposals(proposals_len);
957
958 T::SpendFunds::spend_funds(
960 &mut budget_remaining,
961 &mut imbalance,
962 &mut total_weight,
963 &mut missed_any,
964 );
965
966 if !missed_any && !T::Burn::get().is_zero() {
967 let one_minus_burn = T::Burn::get().left_from_one();
970 let percent_left =
971 one_minus_burn.saturating_pow(spend_periods_passed.unique_saturated_into());
972 let new_budget_remaining = percent_left * budget_remaining;
973 let burn = budget_remaining.saturating_sub(new_budget_remaining);
974 budget_remaining = new_budget_remaining;
975
976 let (debit, credit) = T::Currency::pair(burn);
977 imbalance.subsume(debit);
978 T::BurnDestination::on_unbalanced(credit);
979 Self::deposit_event(Event::Burnt { burnt_funds: burn })
980 }
981
982 if let Err(problem) =
987 T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive)
988 {
989 print("Inconsistent state - couldn't settle imbalance for funds spent by treasury");
990 drop(problem);
992 }
993
994 Self::deposit_event(Event::Rollover { rollover_balance: budget_remaining });
995
996 total_weight
997 }
998
999 pub fn pot() -> BalanceOf<T, I> {
1002 T::Currency::free_balance(&Self::account_id())
1003 .saturating_sub(T::Currency::minimum_balance())
1005 }
1006
1007 #[cfg(any(feature = "try-runtime", test))]
1009 fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1010 Self::try_state_proposals()?;
1011 Self::try_state_spends()?;
1012
1013 Ok(())
1014 }
1015
1016 #[cfg(any(feature = "try-runtime", test))]
1024 fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> {
1025 let current_proposal_count = ProposalCount::<T, I>::get();
1026 ensure!(
1027 current_proposal_count as usize >= Proposals::<T, I>::iter().count(),
1028 "Actual number of proposals exceeds `ProposalCount`."
1029 );
1030
1031 Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult {
1032 ensure!(
1033 current_proposal_count as u32 > proposal_index,
1034 "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`."
1035 );
1036 Ok(())
1037 })?;
1038
1039 Approvals::<T, I>::get()
1040 .iter()
1041 .try_for_each(|proposal_index| -> DispatchResult {
1042 ensure!(
1043 Proposals::<T, I>::contains_key(proposal_index),
1044 "Proposal indices in `Approvals` must also be contained in `Proposals`."
1045 );
1046 Ok(())
1047 })?;
1048
1049 Ok(())
1050 }
1051
1052 #[cfg(any(feature = "try-runtime", test))]
1060 fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> {
1061 let current_spend_count = SpendCount::<T, I>::get();
1062 ensure!(
1063 current_spend_count as usize >= Spends::<T, I>::iter().count(),
1064 "Actual number of spends exceeds `SpendCount`."
1065 );
1066
1067 Spends::<T, I>::iter_keys().try_for_each(|spend_index| -> DispatchResult {
1068 ensure!(
1069 current_spend_count > spend_index,
1070 "`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`."
1071 );
1072 Ok(())
1073 })?;
1074
1075 Spends::<T, I>::iter().try_for_each(|(_index, spend)| -> DispatchResult {
1076 ensure!(
1077 spend.valid_from < spend.expire_at,
1078 "Spend cannot expire before it becomes valid."
1079 );
1080 Ok(())
1081 })?;
1082
1083 Ok(())
1084 }
1085}
1086
1087impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
1088 fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
1089 let numeric_amount = amount.peek();
1090
1091 let _ = T::Currency::resolve_creating(&Self::account_id(), amount);
1093
1094 Self::deposit_event(Event::Deposit { value: numeric_amount });
1095 }
1096}
1097
1098pub struct TreasuryAccountId<R>(PhantomData<R>);
1100impl<R> sp_runtime::traits::TypedGet for TreasuryAccountId<R>
1101where
1102 R: crate::Config,
1103{
1104 type Type = <R as frame_system::Config>::AccountId;
1105 fn get() -> Self::Type {
1106 crate::Pallet::<R>::account_id()
1107 }
1108}