1#![doc = docify::embed!("src/tests.rs", spend_local_origin_works)]
53#![doc = docify::embed!("src/tests.rs", spend_payout_works)]
55#![cfg_attr(not(feature = "std"), no_std)]
72
73mod benchmarking;
74pub mod migration;
75#[cfg(test)]
76mod tests;
77pub mod weights;
78use core::marker::PhantomData;
79
80#[cfg(feature = "runtime-benchmarks")]
81pub use benchmarking::ArgumentsFactory;
82
83extern crate alloc;
84
85use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
86use scale_info::TypeInfo;
87
88use alloc::{boxed::Box, collections::btree_map::BTreeMap};
89use sp_runtime::{
90 traits::{
91 AccountIdConversion, BlockNumberProvider, CheckedAdd, One, Saturating, StaticLookup,
92 UniqueSaturatedInto, Zero,
93 },
94 Debug, PerThing, Permill,
95};
96
97use frame_support::{
98 dispatch::{DispatchResult, DispatchResultWithPostInfo},
99 ensure, print,
100 traits::{
101 tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced,
102 ReservableCurrency, WithdrawReasons,
103 },
104 weights::Weight,
105 BoundedVec, PalletId,
106};
107use frame_system::pallet_prelude::BlockNumberFor as SystemBlockNumberFor;
108
109pub use pallet::*;
110pub use weights::WeightInfo;
111
112pub type BalanceOf<T, I = ()> =
113 <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
114pub type AssetBalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
115pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
116 <T as frame_system::Config>::AccountId,
117>>::PositiveImbalance;
118pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
119 <T as frame_system::Config>::AccountId,
120>>::NegativeImbalance;
121type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
122type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source;
123pub type BlockNumberFor<T, I = ()> =
124 <<T as Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
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(
153 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
154)]
155pub struct Proposal<AccountId, Balance> {
156 pub proposer: AccountId,
158 pub value: Balance,
160 pub beneficiary: AccountId,
162 pub bond: Balance,
164}
165
166#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
168#[derive(
169 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
170)]
171pub enum PaymentState<Id> {
172 Pending,
174 Attempted { id: Id },
176 Failed,
178}
179
180#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
182#[derive(
183 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo,
184)]
185pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> {
186 pub asset_kind: AssetKind,
188 pub amount: AssetBalance,
190 pub beneficiary: Beneficiary,
192 pub valid_from: BlockNumber,
194 pub expire_at: BlockNumber,
196 pub status: PaymentState<PaymentId>,
198}
199
200pub type SpendIndex = u32;
202
203#[frame_support::pallet]
204pub mod pallet {
205 use super::*;
206 use frame_support::{
207 dispatch_context::with_context,
208 pallet_prelude::*,
209 traits::tokens::{ConversionFromAssetBalance, PaymentStatus},
210 };
211 use frame_system::pallet_prelude::{ensure_signed, OriginFor};
212
213 #[pallet::pallet]
214 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
215
216 #[pallet::config]
217 pub trait Config<I: 'static = ()>: frame_system::Config {
218 type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
220
221 type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
223
224 #[allow(deprecated)]
226 type RuntimeEvent: From<Event<Self, I>>
227 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
228
229 #[pallet::constant]
231 type SpendPeriod: Get<BlockNumberFor<Self, I>>;
232
233 #[pallet::constant]
235 type Burn: Get<Permill>;
236
237 #[pallet::constant]
239 type PalletId: Get<PalletId>;
240
241 type BurnDestination: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
243
244 type WeightInfo: WeightInfo;
246
247 type SpendFunds: SpendFunds<Self, I>;
249
250 #[pallet::constant]
257 type MaxApprovals: Get<u32>;
258
259 type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>;
263
264 type AssetKind: Parameter + MaxEncodedLen;
266
267 type Beneficiary: Parameter + MaxEncodedLen;
269
270 type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
272
273 type Paymaster: Pay<Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind>;
275
276 type BalanceConverter: ConversionFromAssetBalance<
280 <Self::Paymaster as Pay>::Balance,
281 Self::AssetKind,
282 BalanceOf<Self, I>,
283 >;
284
285 #[pallet::constant]
287 type PayoutPeriod: Get<BlockNumberFor<Self, I>>;
288
289 #[cfg(feature = "runtime-benchmarks")]
291 type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>;
292
293 type BlockNumberProvider: BlockNumberProvider;
295 }
296
297 #[pallet::extra_constants]
298 impl<T: Config<I>, I: 'static> Pallet<T, I> {
299 fn pot_account() -> T::AccountId {
301 Self::account_id()
302 }
303 }
304
305 #[pallet::storage]
310 pub type ProposalCount<T, I = ()> = StorageValue<_, ProposalIndex, ValueQuery>;
311
312 #[pallet::storage]
317 pub type Proposals<T: Config<I>, I: 'static = ()> = StorageMap<
318 _,
319 Twox64Concat,
320 ProposalIndex,
321 Proposal<T::AccountId, BalanceOf<T, I>>,
322 OptionQuery,
323 >;
324
325 #[pallet::storage]
327 pub type Deactivated<T: Config<I>, I: 'static = ()> =
328 StorageValue<_, BalanceOf<T, I>, ValueQuery>;
329
330 #[pallet::storage]
335 pub type Approvals<T: Config<I>, I: 'static = ()> =
336 StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
337
338 #[pallet::storage]
340 pub type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>;
341
342 #[pallet::storage]
345 pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap<
346 _,
347 Twox64Concat,
348 SpendIndex,
349 SpendStatus<
350 T::AssetKind,
351 AssetBalanceOf<T, I>,
352 T::Beneficiary,
353 BlockNumberFor<T, I>,
354 <T::Paymaster as Pay>::Id,
355 >,
356 OptionQuery,
357 >;
358
359 #[pallet::storage]
361 pub type LastSpendPeriod<T, I = ()> = StorageValue<_, BlockNumberFor<T, I>, OptionQuery>;
362
363 #[pallet::genesis_config]
364 #[derive(frame_support::DefaultNoBound)]
365 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
366 #[serde(skip)]
367 _config: core::marker::PhantomData<(T, I)>,
368 }
369
370 #[pallet::genesis_build]
371 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
372 fn build(&self) {
373 let account_id = Pallet::<T, I>::account_id();
375 let min = T::Currency::minimum_balance();
376 if T::Currency::free_balance(&account_id) < min {
377 let _ = T::Currency::make_free_balance_be(&account_id, min);
378 }
379 }
380 }
381
382 #[pallet::event]
383 #[pallet::generate_deposit(pub(super) fn deposit_event)]
384 pub enum Event<T: Config<I>, I: 'static = ()> {
385 Spending { budget_remaining: BalanceOf<T, I> },
387 Awarded { proposal_index: ProposalIndex, award: BalanceOf<T, I>, account: T::AccountId },
389 Burnt { burnt_funds: BalanceOf<T, I> },
391 Rollover { rollover_balance: BalanceOf<T, I> },
393 Deposit { value: BalanceOf<T, I> },
395 SpendApproved {
397 proposal_index: ProposalIndex,
398 amount: BalanceOf<T, I>,
399 beneficiary: T::AccountId,
400 },
401 UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> },
403 AssetSpendApproved {
405 index: SpendIndex,
406 asset_kind: T::AssetKind,
407 amount: AssetBalanceOf<T, I>,
408 beneficiary: T::Beneficiary,
409 valid_from: BlockNumberFor<T, I>,
410 expire_at: BlockNumberFor<T, I>,
411 },
412 AssetSpendVoided { index: SpendIndex },
414 Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
416 PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
418 SpendProcessed { index: SpendIndex },
421 }
422
423 #[pallet::error]
425 pub enum Error<T, I = ()> {
426 InvalidIndex,
428 TooManyApprovals,
430 InsufficientPermission,
433 ProposalNotApproved,
435 FailedToConvertBalance,
437 SpendExpired,
439 EarlyPayout,
441 AlreadyAttempted,
443 PayoutError,
445 NotAttempted,
447 Inconclusive,
449 }
450
451 #[pallet::hooks]
452 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
453 fn on_initialize(_do_not_use_local_block_number: SystemBlockNumberFor<T>) -> Weight {
456 let block_number = T::BlockNumberProvider::current_block_number();
457 let pot = Self::pot();
458 let deactivated = Deactivated::<T, I>::get();
459 if pot != deactivated {
460 T::Currency::reactivate(deactivated);
461 T::Currency::deactivate(pot);
462 Deactivated::<T, I>::put(&pot);
463 Self::deposit_event(Event::<T, I>::UpdatedInactive {
464 reactivated: deactivated,
465 deactivated: pot,
466 });
467 }
468
469 let last_spend_period = LastSpendPeriod::<T, I>::get()
471 .unwrap_or_else(|| Self::update_last_spend_period());
475 let blocks_since_last_spend_period = block_number.saturating_sub(last_spend_period);
476 let safe_spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
477
478 let (spend_periods_passed, extra_blocks) = (
480 blocks_since_last_spend_period / safe_spend_period,
481 blocks_since_last_spend_period % safe_spend_period,
482 );
483 let new_last_spend_period = block_number.saturating_sub(extra_blocks);
484 if spend_periods_passed > BlockNumberFor::<T, I>::zero() {
485 Self::spend_funds(spend_periods_passed, new_last_spend_period)
486 } else {
487 Weight::zero()
488 }
489 }
490
491 #[cfg(feature = "try-runtime")]
492 fn try_state(_: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
493 Self::do_try_state()?;
494 Ok(())
495 }
496 }
497
498 #[derive(Default)]
499 struct SpendContext<Balance> {
500 spend_in_context: BTreeMap<Balance, Balance>,
501 }
502
503 #[pallet::call]
504 impl<T: Config<I>, I: 'static> Pallet<T, I> {
505 #[pallet::call_index(3)]
523 #[pallet::weight(T::WeightInfo::spend_local())]
524 #[deprecated(
525 note = "The `spend_local` call will be removed by May 2025. Migrate to the new flow and use the `spend` call."
526 )]
527 #[allow(deprecated)]
528 pub fn spend_local(
529 origin: OriginFor<T>,
530 #[pallet::compact] amount: BalanceOf<T, I>,
531 beneficiary: AccountIdLookupOf<T>,
532 ) -> DispatchResult {
533 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
534 ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission);
535
536 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
537 let context = v.or_default();
538
539 let spend = context.spend_in_context.entry(max_amount).or_default();
544
545 if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) {
547 Err(Error::<T, I>::InsufficientPermission)
548 } else {
549 *spend = spend.saturating_add(amount);
550
551 Ok(())
552 }
553 })
554 .unwrap_or(Ok(()))?;
555
556 let beneficiary = T::Lookup::lookup(beneficiary)?;
557 #[allow(deprecated)]
558 let proposal_index = ProposalCount::<T, I>::get();
559 #[allow(deprecated)]
560 Approvals::<T, I>::try_append(proposal_index)
561 .map_err(|_| Error::<T, I>::TooManyApprovals)?;
562 let proposal = Proposal {
563 proposer: beneficiary.clone(),
564 value: amount,
565 beneficiary: beneficiary.clone(),
566 bond: Default::default(),
567 };
568 #[allow(deprecated)]
569 Proposals::<T, I>::insert(proposal_index, proposal);
570 #[allow(deprecated)]
571 ProposalCount::<T, I>::put(proposal_index + 1);
572
573 Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary });
574 Ok(())
575 }
576
577 #[pallet::call_index(4)]
599 #[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))]
600 #[deprecated(
601 note = "The `remove_approval` call will be removed by May 2025. It associated with the deprecated `spend_local` call."
602 )]
603 #[allow(deprecated)]
604 pub fn remove_approval(
605 origin: OriginFor<T>,
606 #[pallet::compact] proposal_id: ProposalIndex,
607 ) -> DispatchResult {
608 T::RejectOrigin::ensure_origin(origin)?;
609
610 #[allow(deprecated)]
611 Approvals::<T, I>::try_mutate(|v| -> DispatchResult {
612 if let Some(index) = v.iter().position(|x| x == &proposal_id) {
613 v.remove(index);
614 Ok(())
615 } else {
616 Err(Error::<T, I>::ProposalNotApproved.into())
617 }
618 })?;
619
620 Ok(())
621 }
622
623 #[pallet::call_index(5)]
650 #[pallet::weight(T::WeightInfo::spend())]
651 pub fn spend(
652 origin: OriginFor<T>,
653 asset_kind: Box<T::AssetKind>,
654 #[pallet::compact] amount: AssetBalanceOf<T, I>,
655 beneficiary: Box<BeneficiaryLookupOf<T, I>>,
656 valid_from: Option<BlockNumberFor<T, I>>,
657 ) -> DispatchResult {
658 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
659 let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?;
660
661 let now = T::BlockNumberProvider::current_block_number();
662 let valid_from = valid_from.unwrap_or(now);
663 let expire_at = valid_from.saturating_add(T::PayoutPeriod::get());
664 ensure!(expire_at > now, Error::<T, I>::SpendExpired);
665
666 let native_amount =
667 T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone())
668 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
669
670 ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
671
672 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
673 let context = v.or_default();
674 let spend = context.spend_in_context.entry(max_amount).or_default();
679
680 if spend.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
682 Err(Error::<T, I>::InsufficientPermission)
683 } else {
684 *spend = spend.saturating_add(native_amount);
685 Ok(())
686 }
687 })
688 .unwrap_or(Ok(()))?;
689
690 let index = SpendCount::<T, I>::get();
691 Spends::<T, I>::insert(
692 index,
693 SpendStatus {
694 asset_kind: *asset_kind.clone(),
695 amount,
696 beneficiary: beneficiary.clone(),
697 valid_from,
698 expire_at,
699 status: PaymentState::Pending,
700 },
701 );
702 SpendCount::<T, I>::put(index + 1);
703
704 Self::deposit_event(Event::AssetSpendApproved {
705 index,
706 asset_kind: *asset_kind,
707 amount,
708 beneficiary,
709 valid_from,
710 expire_at,
711 });
712 Ok(())
713 }
714
715 #[pallet::call_index(6)]
735 #[pallet::weight(T::WeightInfo::payout())]
736 pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
737 ensure_signed(origin)?;
738 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
739 let now = T::BlockNumberProvider::current_block_number();
740 ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout);
741 ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired);
742 ensure!(
743 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
744 Error::<T, I>::AlreadyAttempted
745 );
746
747 let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount)
748 .map_err(|_| Error::<T, I>::PayoutError)?;
749
750 spend.status = PaymentState::Attempted { id };
751 spend.expire_at = now.saturating_add(T::PayoutPeriod::get());
752 Spends::<T, I>::insert(index, spend);
753
754 Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
755
756 Ok(())
757 }
758
759 #[pallet::call_index(7)]
779 #[pallet::weight(T::WeightInfo::check_status())]
780 pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo {
781 use PaymentState as State;
782 use PaymentStatus as Status;
783
784 ensure_signed(origin)?;
785 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
786 let now = T::BlockNumberProvider::current_block_number();
787
788 if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) {
789 Spends::<T, I>::remove(index);
791 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
792 return Ok(Pays::No.into());
793 }
794
795 let payment_id = match spend.status {
796 State::Attempted { id } => id,
797 _ => return Err(Error::<T, I>::NotAttempted.into()),
798 };
799
800 match T::Paymaster::check_payment(payment_id) {
801 Status::Failure => {
802 spend.status = PaymentState::Failed;
803 Spends::<T, I>::insert(index, spend);
804 Self::deposit_event(Event::<T, I>::PaymentFailed { index, payment_id });
805 },
806 Status::Success | Status::Unknown => {
807 Spends::<T, I>::remove(index);
808 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
809 return Ok(Pays::No.into());
810 },
811 Status::InProgress => return Err(Error::<T, I>::Inconclusive.into()),
812 }
813 return Ok(Pays::Yes.into());
814 }
815
816 #[pallet::call_index(8)]
833 #[pallet::weight(T::WeightInfo::void_spend())]
834 pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
835 T::RejectOrigin::ensure_origin(origin)?;
836 let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
837 ensure!(
838 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
839 Error::<T, I>::AlreadyAttempted
840 );
841
842 Spends::<T, I>::remove(index);
843 Self::deposit_event(Event::<T, I>::AssetSpendVoided { index });
844 Ok(())
845 }
846 }
847}
848
849impl<T: Config<I>, I: 'static> Pallet<T, I> {
850 pub fn account_id() -> T::AccountId {
857 T::PalletId::get().into_account_truncating()
858 }
859
860 fn update_last_spend_period() -> BlockNumberFor<T, I> {
864 let block_number = T::BlockNumberProvider::current_block_number();
865 let spend_period = T::SpendPeriod::get().max(BlockNumberFor::<T, I>::one());
866 let time_since_last_spend = block_number % spend_period;
867 let last_spend_period = if time_since_last_spend.is_zero() {
870 block_number.saturating_sub(spend_period)
871 } else {
872 block_number.saturating_sub(time_since_last_spend)
874 };
875 LastSpendPeriod::<T, I>::put(last_spend_period);
876 last_spend_period
877 }
878
879 #[deprecated(
881 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
882 )]
883 pub fn proposal_count() -> ProposalIndex {
884 #[allow(deprecated)]
885 ProposalCount::<T, I>::get()
886 }
887
888 #[deprecated(
890 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
891 )]
892 pub fn proposals(index: ProposalIndex) -> Option<Proposal<T::AccountId, BalanceOf<T, I>>> {
893 #[allow(deprecated)]
894 Proposals::<T, I>::get(index)
895 }
896
897 #[deprecated(
899 note = "This function will be removed by May 2025. Configure pallet to use PayFromAccount for Paymaster type instead"
900 )]
901 #[allow(deprecated)]
902 pub fn approvals() -> BoundedVec<ProposalIndex, T::MaxApprovals> {
903 Approvals::<T, I>::get()
904 }
905
906 pub fn spend_funds(
908 spend_periods_passed: BlockNumberFor<T, I>,
909 new_last_spend_period: BlockNumberFor<T, I>,
910 ) -> Weight {
911 LastSpendPeriod::<T, I>::put(new_last_spend_period);
912 let mut total_weight = Weight::zero();
913
914 let mut budget_remaining = Self::pot();
915 Self::deposit_event(Event::Spending { budget_remaining });
916 let account_id = Self::account_id();
917
918 let mut missed_any = false;
919 let mut imbalance = PositiveImbalanceOf::<T, I>::zero();
920 #[allow(deprecated)]
921 let proposals_len = Approvals::<T, I>::mutate(|v| {
922 let proposals_approvals_len = v.len() as u32;
923 v.retain(|&index| {
924 if let Some(p) = Proposals::<T, I>::get(index) {
926 if p.value <= budget_remaining {
927 budget_remaining -= p.value;
928 Proposals::<T, I>::remove(index);
929
930 let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
932 debug_assert!(err_amount.is_zero());
933
934 imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value));
936
937 Self::deposit_event(Event::Awarded {
938 proposal_index: index,
939 award: p.value,
940 account: p.beneficiary,
941 });
942 false
943 } else {
944 missed_any = true;
945 true
946 }
947 } else {
948 false
949 }
950 });
951 proposals_approvals_len
952 });
953
954 total_weight += T::WeightInfo::on_initialize_proposals(proposals_len);
955
956 T::SpendFunds::spend_funds(
958 &mut budget_remaining,
959 &mut imbalance,
960 &mut total_weight,
961 &mut missed_any,
962 );
963
964 if !missed_any && !T::Burn::get().is_zero() {
965 let one_minus_burn = T::Burn::get().left_from_one();
968 let percent_left =
969 one_minus_burn.saturating_pow(spend_periods_passed.unique_saturated_into());
970 let new_budget_remaining = percent_left * budget_remaining;
971 let burn = budget_remaining.saturating_sub(new_budget_remaining);
972 budget_remaining = new_budget_remaining;
973
974 let (debit, credit) = T::Currency::pair(burn);
975 imbalance.subsume(debit);
976 T::BurnDestination::on_unbalanced(credit);
977 Self::deposit_event(Event::Burnt { burnt_funds: burn })
978 }
979
980 if let Err(problem) =
985 T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive)
986 {
987 print("Inconsistent state - couldn't settle imbalance for funds spent by treasury");
988 drop(problem);
990 }
991
992 Self::deposit_event(Event::Rollover { rollover_balance: budget_remaining });
993
994 total_weight
995 }
996
997 pub fn pot() -> BalanceOf<T, I> {
1000 T::Currency::free_balance(&Self::account_id())
1001 .saturating_sub(T::Currency::minimum_balance())
1003 }
1004
1005 #[cfg(any(feature = "try-runtime", test))]
1007 fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1008 Self::try_state_proposals()?;
1009 Self::try_state_spends()?;
1010
1011 Ok(())
1012 }
1013
1014 #[cfg(any(feature = "try-runtime", test))]
1022 fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> {
1023 let current_proposal_count = ProposalCount::<T, I>::get();
1024 ensure!(
1025 current_proposal_count as usize >= Proposals::<T, I>::iter().count(),
1026 "Actual number of proposals exceeds `ProposalCount`."
1027 );
1028
1029 Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult {
1030 ensure!(
1031 current_proposal_count as u32 > proposal_index,
1032 "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`."
1033 );
1034 Ok(())
1035 })?;
1036
1037 Approvals::<T, I>::get()
1038 .iter()
1039 .try_for_each(|proposal_index| -> DispatchResult {
1040 ensure!(
1041 Proposals::<T, I>::contains_key(proposal_index),
1042 "Proposal indices in `Approvals` must also be contained in `Proposals`."
1043 );
1044 Ok(())
1045 })?;
1046
1047 Ok(())
1048 }
1049
1050 #[cfg(any(feature = "try-runtime", test))]
1058 fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> {
1059 let current_spend_count = SpendCount::<T, I>::get();
1060 ensure!(
1061 current_spend_count as usize >= Spends::<T, I>::iter().count(),
1062 "Actual number of spends exceeds `SpendCount`."
1063 );
1064
1065 Spends::<T, I>::iter_keys().try_for_each(|spend_index| -> DispatchResult {
1066 ensure!(
1067 current_spend_count > spend_index,
1068 "`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`."
1069 );
1070 Ok(())
1071 })?;
1072
1073 Spends::<T, I>::iter().try_for_each(|(_index, spend)| -> DispatchResult {
1074 ensure!(
1075 spend.valid_from < spend.expire_at,
1076 "Spend cannot expire before it becomes valid."
1077 );
1078 Ok(())
1079 })?;
1080
1081 Ok(())
1082 }
1083}
1084
1085impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
1086 fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
1087 let numeric_amount = amount.peek();
1088
1089 let _ = T::Currency::resolve_creating(&Self::account_id(), amount);
1091
1092 Self::deposit_event(Event::Deposit { value: numeric_amount });
1093 }
1094}
1095
1096pub struct TreasuryAccountId<R>(PhantomData<R>);
1098impl<R> sp_runtime::traits::TypedGet for TreasuryAccountId<R>
1099where
1100 R: crate::Config,
1101{
1102 type Type = <R as frame_system::Config>::AccountId;
1103 fn get() -> Self::Type {
1104 crate::Pallet::<R>::account_id()
1105 }
1106}