1#![doc = docify::embed!("src/tests.rs", spend_local_origin_works)]
54#![doc = docify::embed!("src/tests.rs", spend_payout_works)]
57#![cfg_attr(not(feature = "std"), no_std)]
75#![allow(clippy::expect_used)]
76
77mod benchmarking;
78#[cfg(test)]
79mod tests;
80pub mod weights;
81use core::marker::PhantomData;
82
83#[cfg(feature = "runtime-benchmarks")]
84pub use benchmarking::ArgumentsFactory;
85
86use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
87use scale_info::TypeInfo;
88
89use sp_runtime::{
90 traits::{AccountIdConversion, CheckedAdd, Saturating, StaticLookup, Zero},
91 Permill, RuntimeDebug,
92};
93extern crate alloc;
94use alloc::{boxed::Box, collections::btree_map::BTreeMap};
95
96use frame_support::{
97 dispatch::{DispatchResult, DispatchResultWithPostInfo},
98 ensure, print,
99 traits::{
100 tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced,
101 ReservableCurrency, WithdrawReasons,
102 },
103 weights::Weight,
104 PalletId,
105};
106
107pub use pallet::*;
108pub use weights::WeightInfo;
109
110pub type BalanceOf<T, I = ()> =
111 <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
112pub type AssetBalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
113pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
114 <T as frame_system::Config>::AccountId,
115>>::PositiveImbalance;
116pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
117 <T as frame_system::Config>::AccountId,
118>>::NegativeImbalance;
119type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
120type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source;
121
122#[impl_trait_for_tuples::impl_for_tuples(30)]
134pub trait SpendFunds<T: Config<I>, I: 'static = ()> {
135 fn spend_funds(
136 budget_remaining: &mut BalanceOf<T, I>,
137 imbalance: &mut PositiveImbalanceOf<T, I>,
138 total_weight: &mut Weight,
139 missed_any: &mut bool,
140 );
141}
142
143pub type ProposalIndex = u32;
145
146#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
148#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
149pub struct Proposal<AccountId, Balance> {
150 proposer: AccountId,
152 value: Balance,
154 beneficiary: AccountId,
156 bond: Balance,
158}
159
160#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
162#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
163pub enum PaymentState<Id> {
164 Pending,
166 Attempted { id: Id },
168 Failed,
170}
171
172#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
174#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
175pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> {
176 asset_kind: AssetKind,
178 amount: AssetBalance,
180 beneficiary: Beneficiary,
182 valid_from: BlockNumber,
184 expire_at: BlockNumber,
186 status: PaymentState<PaymentId>,
188}
189
190pub type SpendIndex = u32;
192
193#[frame_support::pallet]
194pub mod pallet {
195 use super::*;
196 use frame_support::{
197 dispatch_context::with_context,
198 pallet_prelude::*,
199 traits::tokens::{ConversionFromAssetBalance, PaymentStatus},
200 };
201 use frame_system::pallet_prelude::*;
202
203 #[pallet::pallet]
204 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
205
206 #[pallet::config]
207 pub trait Config<I: 'static = ()>: frame_system::Config {
208 type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
210
211 type ApproveOrigin: EnsureOrigin<Self::RuntimeOrigin>;
213
214 type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
216
217 #[allow(deprecated)]
219 type RuntimeEvent: From<Event<Self, I>>
220 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
221
222 type OnSlash: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
224
225 #[pallet::constant]
228 type ProposalBond: Get<Permill>;
229
230 #[pallet::constant]
232 type ProposalBondMinimum: Get<BalanceOf<Self, I>>;
233
234 #[pallet::constant]
236 type ProposalBondMaximum: Get<Option<BalanceOf<Self, I>>>;
237
238 #[pallet::constant]
240 type SpendPeriod: Get<BlockNumberFor<Self>>;
241
242 #[pallet::constant]
244 type Burn: Get<Permill>;
245
246 #[pallet::constant]
248 type PalletId: Get<PalletId>;
249
250 type BurnDestination: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
252
253 type WeightInfo: WeightInfo;
255
256 type SpendFunds: SpendFunds<Self, I>;
258
259 #[pallet::constant]
263 type MaxApprovals: Get<u32>;
264
265 type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>;
269
270 type AssetKind: Parameter + MaxEncodedLen;
272
273 type Beneficiary: Parameter + MaxEncodedLen;
275
276 type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
278
279 type Paymaster: Pay<Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind>;
281
282 type BalanceConverter: ConversionFromAssetBalance<
286 <Self::Paymaster as Pay>::Balance,
287 Self::AssetKind,
288 BalanceOf<Self, I>,
289 >;
290
291 #[pallet::constant]
293 type PayoutPeriod: Get<BlockNumberFor<Self>>;
294
295 #[cfg(feature = "runtime-benchmarks")]
297 type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>;
298 }
299
300 #[pallet::storage]
302 #[pallet::getter(fn proposal_count)]
303 pub(crate) type ProposalCount<T, I = ()> = StorageValue<_, ProposalIndex, ValueQuery>;
304
305 #[pallet::storage]
307 #[pallet::getter(fn proposals)]
308 pub type Proposals<T: Config<I>, I: 'static = ()> = StorageMap<
309 _,
310 Twox64Concat,
311 ProposalIndex,
312 Proposal<T::AccountId, BalanceOf<T, I>>,
313 OptionQuery,
314 >;
315
316 #[pallet::storage]
318 pub type Deactivated<T: Config<I>, I: 'static = ()> =
319 StorageValue<_, BalanceOf<T, I>, ValueQuery>;
320
321 #[pallet::storage]
323 #[pallet::getter(fn approvals)]
324 pub type Approvals<T: Config<I>, I: 'static = ()> =
325 StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
326
327 #[pallet::storage]
329 pub(crate) type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>;
330
331 #[pallet::storage]
334 pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap<
335 _,
336 Twox64Concat,
337 SpendIndex,
338 SpendStatus<
339 T::AssetKind,
340 AssetBalanceOf<T, I>,
341 T::Beneficiary,
342 BlockNumberFor<T>,
343 <T::Paymaster as Pay>::Id,
344 >,
345 OptionQuery,
346 >;
347
348 #[pallet::genesis_config]
349 #[derive(frame_support::DefaultNoBound)]
350 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
351 #[serde(skip)]
352 _config: 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 Proposed { proposal_index: ProposalIndex },
372 Spending { budget_remaining: BalanceOf<T, I> },
374 Awarded { proposal_index: ProposalIndex, award: BalanceOf<T, I>, account: T::AccountId },
376 Rejected { proposal_index: ProposalIndex, slashed: BalanceOf<T, I> },
378 Burnt { burnt_funds: BalanceOf<T, I> },
380 Rollover { rollover_balance: BalanceOf<T, I> },
382 Deposit { value: BalanceOf<T, I> },
384 SpendApproved {
386 proposal_index: ProposalIndex,
387 amount: BalanceOf<T, I>,
388 beneficiary: T::AccountId,
389 },
390 UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> },
392 AssetSpendApproved {
394 index: SpendIndex,
395 asset_kind: T::AssetKind,
396 amount: AssetBalanceOf<T, I>,
397 beneficiary: T::Beneficiary,
398 valid_from: BlockNumberFor<T>,
399 expire_at: BlockNumberFor<T>,
400 },
401 AssetSpendVoided { index: SpendIndex },
403 Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
405 PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id },
407 SpendProcessed { index: SpendIndex },
410 }
411
412 #[pallet::error]
414 pub enum Error<T, I = ()> {
415 InsufficientProposersBalance,
417 InvalidIndex,
419 TooManyApprovals,
421 InsufficientPermission,
424 ProposalNotApproved,
426 FailedToConvertBalance,
428 SpendExpired,
430 EarlyPayout,
432 AlreadyAttempted,
434 PayoutError,
436 NotAttempted,
438 Inconclusive,
440 }
441
442 #[pallet::hooks]
443 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
444 fn on_initialize(n: frame_system::pallet_prelude::BlockNumberFor<T>) -> Weight {
447 let pot = Self::pot();
448 let deactivated = Deactivated::<T, I>::get();
449 if pot != deactivated {
450 T::Currency::reactivate(deactivated);
451 T::Currency::deactivate(pot);
452 Deactivated::<T, I>::put(pot);
453 Self::deposit_event(Event::<T, I>::UpdatedInactive {
454 reactivated: deactivated,
455 deactivated: pot,
456 });
457 }
458
459 if (n % T::SpendPeriod::get()).is_zero() {
461 Self::spend_funds()
462 } else {
463 Weight::zero()
464 }
465 }
466
467 #[cfg(feature = "try-runtime")]
468 fn try_state(
469 _: frame_system::pallet_prelude::BlockNumberFor<T>,
470 ) -> Result<(), sp_runtime::TryRuntimeError> {
471 Self::do_try_state()?;
472 Ok(())
473 }
474 }
475
476 #[derive(Default)]
477 struct SpendContext<Balance> {
478 spend_in_context: BTreeMap<Balance, Balance>,
479 }
480
481 #[pallet::call]
482 impl<T: Config<I>, I: 'static> Pallet<T, I> {
483 #[pallet::call_index(0)]
500 #[pallet::weight(T::WeightInfo::propose_spend())]
501 #[allow(deprecated)]
502 #[deprecated(
503 note = "`propose_spend` will be removed in February 2024. Use `spend` instead."
504 )]
505 pub fn propose_spend(
506 origin: OriginFor<T>,
507 #[pallet::compact] value: BalanceOf<T, I>,
508 beneficiary: AccountIdLookupOf<T>,
509 ) -> DispatchResult {
510 let proposer = ensure_signed(origin)?;
511 let beneficiary = T::Lookup::lookup(beneficiary)?;
512
513 let bond = Self::calculate_bond(value);
514 T::Currency::reserve(&proposer, bond)
515 .map_err(|_| Error::<T, I>::InsufficientProposersBalance)?;
516
517 let c = Self::proposal_count();
518 <ProposalCount<T, I>>::put(c + 1);
519 <Proposals<T, I>>::insert(c, Proposal { proposer, value, beneficiary, bond });
520
521 Self::deposit_event(Event::Proposed { proposal_index: c });
522 Ok(())
523 }
524
525 #[pallet::call_index(1)]
541 #[pallet::weight((T::WeightInfo::reject_proposal(), DispatchClass::Operational))]
542 #[allow(deprecated)]
543 #[deprecated(
544 note = "`reject_proposal` will be removed in February 2024. Use `spend` instead."
545 )]
546 pub fn reject_proposal(
547 origin: OriginFor<T>,
548 #[pallet::compact] proposal_id: ProposalIndex,
549 ) -> DispatchResult {
550 T::RejectOrigin::ensure_origin(origin)?;
551
552 let proposal =
553 <Proposals<T, I>>::take(proposal_id).ok_or(Error::<T, I>::InvalidIndex)?;
554 let value = proposal.bond;
555 let imbalance = T::Currency::slash_reserved(&proposal.proposer, value).0;
556 T::OnSlash::on_unbalanced(imbalance);
557
558 Self::deposit_event(Event::<T, I>::Rejected {
559 proposal_index: proposal_id,
560 slashed: value,
561 });
562 Ok(())
563 }
564
565 #[pallet::call_index(2)]
583 #[pallet::weight((T::WeightInfo::approve_proposal(T::MaxApprovals::get()), DispatchClass::Operational))]
584 #[allow(deprecated)]
585 #[deprecated(
586 note = "`approve_proposal` will be removed in February 2024. Use `spend` instead."
587 )]
588 pub fn approve_proposal(
589 origin: OriginFor<T>,
590 #[pallet::compact] proposal_id: ProposalIndex,
591 ) -> DispatchResult {
592 T::ApproveOrigin::ensure_origin(origin)?;
593
594 ensure!(<Proposals<T, I>>::contains_key(proposal_id), Error::<T, I>::InvalidIndex);
595 Approvals::<T, I>::try_append(proposal_id)
596 .map_err(|_| Error::<T, I>::TooManyApprovals)?;
597 Ok(())
598 }
599
600 #[pallet::call_index(3)]
618 #[pallet::weight(T::WeightInfo::spend_local())]
619 pub fn spend_local(
620 origin: OriginFor<T>,
621 #[pallet::compact] amount: BalanceOf<T, I>,
622 beneficiary: AccountIdLookupOf<T>,
623 ) -> DispatchResult {
624 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
625 ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission);
626
627 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
628 let context = v.or_default();
629
630 let spend = context.spend_in_context.entry(max_amount).or_default();
635
636 if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) {
638 Err(Error::<T, I>::InsufficientPermission)
639 } else {
640 *spend = spend.saturating_add(amount);
641
642 Ok(())
643 }
644 })
645 .unwrap_or(Ok(()))?;
646
647 let beneficiary = T::Lookup::lookup(beneficiary)?;
648 let proposal_index = Self::proposal_count();
649 Approvals::<T, I>::try_append(proposal_index)
650 .map_err(|_| Error::<T, I>::TooManyApprovals)?;
651 let proposal = Proposal {
652 proposer: beneficiary.clone(),
653 value: amount,
654 beneficiary: beneficiary.clone(),
655 bond: Default::default(),
656 };
657 Proposals::<T, I>::insert(proposal_index, proposal);
658 ProposalCount::<T, I>::put(proposal_index + 1);
659
660 Self::deposit_event(Event::SpendApproved { proposal_index, amount, beneficiary });
661 Ok(())
662 }
663
664 #[pallet::call_index(4)]
686 #[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))]
687 pub fn remove_approval(
688 origin: OriginFor<T>,
689 #[pallet::compact] proposal_id: ProposalIndex,
690 ) -> DispatchResult {
691 T::RejectOrigin::ensure_origin(origin)?;
692
693 Approvals::<T, I>::try_mutate(|v| -> DispatchResult {
694 if let Some(index) = v.iter().position(|x| x == &proposal_id) {
695 v.remove(index);
696 Ok(())
697 } else {
698 Err(Error::<T, I>::ProposalNotApproved.into())
699 }
700 })?;
701
702 Ok(())
703 }
704
705 #[pallet::call_index(5)]
732 #[pallet::weight(T::WeightInfo::spend())]
733 pub fn spend(
734 origin: OriginFor<T>,
735 asset_kind: Box<T::AssetKind>,
736 #[pallet::compact] amount: AssetBalanceOf<T, I>,
737 beneficiary: Box<BeneficiaryLookupOf<T, I>>,
738 valid_from: Option<BlockNumberFor<T>>,
739 ) -> DispatchResult {
740 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
741 let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?;
742
743 let now = frame_system::Pallet::<T>::block_number();
744 let valid_from = valid_from.unwrap_or(now);
745 let expire_at = valid_from.saturating_add(T::PayoutPeriod::get());
746 ensure!(expire_at > now, Error::<T, I>::SpendExpired);
747
748 let native_amount =
749 T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone())
750 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
751
752 ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
753
754 with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
755 let context = v.or_default();
756 let spend = context.spend_in_context.entry(max_amount).or_default();
761
762 if spend.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
764 Err(Error::<T, I>::InsufficientPermission)
765 } else {
766 *spend = spend.saturating_add(native_amount);
767 Ok(())
768 }
769 })
770 .unwrap_or(Ok(()))?;
771
772 let index = SpendCount::<T, I>::get();
773 Spends::<T, I>::insert(
774 index,
775 SpendStatus {
776 asset_kind: *asset_kind.clone(),
777 amount,
778 beneficiary: beneficiary.clone(),
779 valid_from,
780 expire_at,
781 status: PaymentState::Pending,
782 },
783 );
784 SpendCount::<T, I>::put(index + 1);
785
786 Self::deposit_event(Event::AssetSpendApproved {
787 index,
788 asset_kind: *asset_kind,
789 amount,
790 beneficiary,
791 valid_from,
792 expire_at,
793 });
794 Ok(())
795 }
796
797 #[pallet::call_index(6)]
817 #[pallet::weight(T::WeightInfo::payout())]
818 pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
819 ensure_signed(origin)?;
820 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
821 let now = frame_system::Pallet::<T>::block_number();
822 ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout);
823 ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired);
824 ensure!(
825 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
826 Error::<T, I>::AlreadyAttempted
827 );
828
829 let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount)
830 .map_err(|_| Error::<T, I>::PayoutError)?;
831
832 spend.status = PaymentState::Attempted { id };
833 Spends::<T, I>::insert(index, spend);
834
835 Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id });
836
837 Ok(())
838 }
839
840 #[pallet::call_index(7)]
860 #[pallet::weight(T::WeightInfo::check_status())]
861 #[allow(clippy::useless_conversion)]
862 pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo {
863 use PaymentState as State;
864 use PaymentStatus as Status;
865
866 ensure_signed(origin)?;
867 let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
868 let now = frame_system::Pallet::<T>::block_number();
869
870 if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) {
871 Spends::<T, I>::remove(index);
873 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
874 return Ok(Pays::No.into());
875 }
876
877 let payment_id = match spend.status {
878 State::Attempted { id } => id,
879 _ => return Err(Error::<T, I>::NotAttempted.into()),
880 };
881
882 match T::Paymaster::check_payment(payment_id) {
883 Status::Failure => {
884 spend.status = PaymentState::Failed;
885 Spends::<T, I>::insert(index, spend);
886 Self::deposit_event(Event::<T, I>::PaymentFailed { index, payment_id });
887 },
888 Status::Success | Status::Unknown => {
889 Spends::<T, I>::remove(index);
890 Self::deposit_event(Event::<T, I>::SpendProcessed { index });
891 return Ok(Pays::No.into());
892 },
893 Status::InProgress => return Err(Error::<T, I>::Inconclusive.into()),
894 }
895 return Ok(Pays::Yes.into());
896 }
897
898 #[pallet::call_index(8)]
915 #[pallet::weight(T::WeightInfo::void_spend())]
916 pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult {
917 T::RejectOrigin::ensure_origin(origin)?;
918 let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?;
919 ensure!(
920 matches!(spend.status, PaymentState::Pending | PaymentState::Failed),
921 Error::<T, I>::AlreadyAttempted
922 );
923
924 Spends::<T, I>::remove(index);
925 Self::deposit_event(Event::<T, I>::AssetSpendVoided { index });
926 Ok(())
927 }
928 }
929}
930
931impl<T: Config<I>, I: 'static> Pallet<T, I> {
932 pub fn account_id() -> T::AccountId {
939 T::PalletId::get().into_account_truncating()
940 }
941
942 fn calculate_bond(value: BalanceOf<T, I>) -> BalanceOf<T, I> {
944 let mut r = T::ProposalBondMinimum::get().max(T::ProposalBond::get() * value);
945 if let Some(m) = T::ProposalBondMaximum::get() {
946 r = r.min(m);
947 }
948 r
949 }
950
951 pub fn spend_funds() -> Weight {
953 let mut total_weight = Weight::zero();
954
955 let mut budget_remaining = Self::pot();
956 Self::deposit_event(Event::Spending { budget_remaining });
957 let account_id = Self::account_id();
958
959 let mut missed_any = false;
960 let mut imbalance = <PositiveImbalanceOf<T, I>>::zero();
961 let proposals_len = Approvals::<T, I>::mutate(|v| {
962 let proposals_approvals_len = v.len() as u32;
963 v.retain(|&index| {
964 if let Some(p) = Self::proposals(index) {
966 if p.value <= budget_remaining {
967 budget_remaining -= p.value;
968 <Proposals<T, I>>::remove(index);
969
970 let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
972 debug_assert!(err_amount.is_zero());
973
974 imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value));
976
977 Self::deposit_event(Event::Awarded {
978 proposal_index: index,
979 award: p.value,
980 account: p.beneficiary,
981 });
982 false
983 } else {
984 missed_any = true;
985 true
986 }
987 } else {
988 false
989 }
990 });
991 proposals_approvals_len
992 });
993
994 total_weight += T::WeightInfo::on_initialize_proposals(proposals_len);
995
996 T::SpendFunds::spend_funds(
998 &mut budget_remaining,
999 &mut imbalance,
1000 &mut total_weight,
1001 &mut missed_any,
1002 );
1003
1004 if !missed_any {
1005 let burn = (T::Burn::get() * budget_remaining).min(budget_remaining);
1007 budget_remaining -= burn;
1008
1009 let (debit, credit) = T::Currency::pair(burn);
1010 imbalance.subsume(debit);
1011 T::BurnDestination::on_unbalanced(credit);
1012 Self::deposit_event(Event::Burnt { burnt_funds: burn })
1013 }
1014
1015 if let Err(problem) =
1020 T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive)
1021 {
1022 print("Inconsistent state - couldn't settle imbalance for funds spent by treasury");
1023 drop(problem);
1025 }
1026
1027 Self::deposit_event(Event::Rollover { rollover_balance: budget_remaining });
1028
1029 total_weight
1030 }
1031
1032 pub fn pot() -> BalanceOf<T, I> {
1035 T::Currency::free_balance(&Self::account_id())
1036 .saturating_sub(T::Currency::minimum_balance())
1038 }
1039
1040 #[cfg(any(feature = "try-runtime", test))]
1042 fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1043 Self::try_state_proposals()?;
1044 Self::try_state_spends()?;
1045
1046 Ok(())
1047 }
1048
1049 #[cfg(any(feature = "try-runtime", test))]
1057 fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> {
1058 let current_proposal_count = ProposalCount::<T, I>::get();
1059 ensure!(
1060 current_proposal_count as usize >= Proposals::<T, I>::iter().count(),
1061 "Actual number of proposals exceeds `ProposalCount`."
1062 );
1063
1064 Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult {
1065 ensure!(
1066 current_proposal_count > proposal_index,
1067 "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`."
1068 );
1069 Ok(())
1070 })?;
1071
1072 Approvals::<T, I>::get()
1073 .iter()
1074 .try_for_each(|proposal_index| -> DispatchResult {
1075 ensure!(
1076 Proposals::<T, I>::contains_key(proposal_index),
1077 "Proposal indices in `Approvals` must also be contained in `Proposals`."
1078 );
1079 Ok(())
1080 })?;
1081
1082 Ok(())
1083 }
1084
1085 #[cfg(any(feature = "try-runtime", test))]
1092 fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> {
1093 let current_spend_count = SpendCount::<T, I>::get();
1094 ensure!(
1095 current_spend_count as usize >= Spends::<T, I>::iter().count(),
1096 "Actual number of spends exceeds `SpendCount`."
1097 );
1098
1099 Spends::<T, I>::iter_keys().try_for_each(|spend_index| -> DispatchResult {
1100 ensure!(
1101 current_spend_count > spend_index,
1102 "`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`."
1103 );
1104 Ok(())
1105 })?;
1106
1107 Spends::<T, I>::iter().try_for_each(|(_index, spend)| -> DispatchResult {
1108 ensure!(
1109 spend.valid_from < spend.expire_at,
1110 "Spend cannot expire before it becomes valid."
1111 );
1112 Ok(())
1113 })?;
1114
1115 Ok(())
1116 }
1117}
1118
1119impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
1120 fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
1121 let numeric_amount = amount.peek();
1122
1123 T::Currency::resolve_creating(&Self::account_id(), amount);
1125
1126 Self::deposit_event(Event::Deposit { value: numeric_amount });
1127 }
1128}
1129
1130pub struct TreasuryAccountId<R>(PhantomData<R>);
1132impl<R> sp_runtime::traits::TypedGet for TreasuryAccountId<R>
1133where
1134 R: crate::Config,
1135{
1136 type Type = <R as frame_system::Config>::AccountId;
1137 fn get() -> Self::Type {
1138 <crate::Pallet<R>>::account_id()
1139 }
1140}