1#![cfg_attr(not(feature = "std"), no_std)]
54
55mod benchmarking;
56#[cfg(test)]
57mod tests;
58pub mod weights;
59use core::marker::PhantomData;
60
61extern crate alloc;
62
63use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
64use scale_info::TypeInfo;
65
66use sp_runtime::{
67 traits::{AccountIdConversion, Saturating, StaticLookup, Zero},
68 Permill, RuntimeDebug,
69};
70
71use frame_support::{
72 dispatch::DispatchResult,
73 ensure, print,
74 traits::{
75 Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced,
76 ReservableCurrency, WithdrawReasons,
77 },
78 weights::Weight,
79 PalletId,
80};
81
82pub use pallet::*;
83pub use weights::WeightInfo;
84
85pub type BalanceOf<T, I = ()> =
86 <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
87pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
88 <T as frame_system::Config>::AccountId,
89>>::PositiveImbalance;
90pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<
91 <T as frame_system::Config>::AccountId,
92>>::NegativeImbalance;
93type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
94
95#[impl_trait_for_tuples::impl_for_tuples(30)]
107pub trait SpendFunds<T: Config<I>, I: 'static = ()> {
108 fn spend_funds(
109 budget_remaining: &mut BalanceOf<T, I>,
110 imbalance: &mut PositiveImbalanceOf<T, I>,
111 total_weight: &mut Weight,
112 missed_any: &mut bool,
113 );
114}
115
116pub type ProposalIndex = u32;
118
119#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
121#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)]
122pub struct Proposal<AccountId, Balance> {
123 proposer: AccountId,
125 value: Balance,
127 beneficiary: AccountId,
129 bond: Balance,
131}
132
133#[frame_support::pallet]
134pub mod pallet {
135 use super::*;
136 use frame_support::pallet_prelude::*;
137 use frame_system::pallet_prelude::*;
138
139 #[pallet::pallet]
140 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
141
142 #[pallet::config]
143 pub trait Config<I: 'static = ()>: frame_system::Config {
144 type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
146
147 type ApproveOrigin: EnsureOrigin<Self::RuntimeOrigin>;
149
150 type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
152
153 #[allow(deprecated)]
155 type RuntimeEvent: From<Event<Self, I>>
156 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
157
158 type OnSlash: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
160
161 #[pallet::constant]
164 type ProposalBond: Get<Permill>;
165
166 #[pallet::constant]
168 type ProposalBondMinimum: Get<BalanceOf<Self, I>>;
169
170 #[pallet::constant]
172 type ProposalBondMaximum: Get<Option<BalanceOf<Self, I>>>;
173
174 #[pallet::constant]
176 type SpendPeriod: Get<BlockNumberFor<Self>>;
177
178 #[pallet::constant]
180 type Burn: Get<Permill>;
181
182 #[pallet::constant]
184 type PalletId: Get<PalletId>;
185
186 type BurnDestination: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
188
189 type WeightInfo: WeightInfo;
191
192 type SpendFunds: SpendFunds<Self, I>;
194
195 #[pallet::constant]
199 type MaxApprovals: Get<u32>;
200 }
201
202 #[pallet::storage]
204 #[pallet::getter(fn proposal_count)]
205 pub(crate) type ProposalCount<T, I = ()> = StorageValue<_, ProposalIndex, ValueQuery>;
206
207 #[pallet::storage]
209 #[pallet::getter(fn proposals)]
210 pub type Proposals<T: Config<I>, I: 'static = ()> = StorageMap<
211 _,
212 Twox64Concat,
213 ProposalIndex,
214 Proposal<T::AccountId, BalanceOf<T, I>>,
215 OptionQuery,
216 >;
217
218 #[pallet::storage]
220 pub type Deactivated<T: Config<I>, I: 'static = ()> =
221 StorageValue<_, BalanceOf<T, I>, ValueQuery>;
222
223 #[pallet::storage]
225 #[pallet::getter(fn approvals)]
226 pub type Approvals<T: Config<I>, I: 'static = ()> =
227 StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>;
228
229 #[pallet::genesis_config]
230 #[derive(frame_support::DefaultNoBound)]
231 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
232 #[serde(skip)]
233 _config: core::marker::PhantomData<(T, I)>,
234 }
235
236 #[pallet::genesis_build]
237 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
238 fn build(&self) {
239 let account_id = <Pallet<T, I>>::account_id();
241 let min = T::Currency::minimum_balance();
242 if T::Currency::free_balance(&account_id) < min {
243 let _ = T::Currency::make_free_balance_be(&account_id, min);
244 }
245 }
246 }
247
248 #[pallet::event]
249 #[pallet::generate_deposit(pub(super) fn deposit_event)]
250 #[repr(u8)]
251 pub enum Event<T: Config<I>, I: 'static = ()> {
252 Proposed { proposal_index: ProposalIndex } = 0,
254 Spending { budget_remaining: BalanceOf<T, I> } = 1,
256 Awarded {
258 proposal_index: ProposalIndex,
259 award: BalanceOf<T, I>,
260 account: T::AccountId,
261 } = 2,
262 Rejected {
264 proposal_index: ProposalIndex,
265 slashed: BalanceOf<T, I>,
266 } = 3,
267 Burnt { burnt_funds: BalanceOf<T, I> } = 4,
269 Rollover { rollover_balance: BalanceOf<T, I> } = 5,
271 Deposit { value: BalanceOf<T, I> } = 6,
273 UpdatedInactive {
276 reactivated: BalanceOf<T, I>,
277 deactivated: BalanceOf<T, I>,
278 } = 8,
279 }
280
281 #[pallet::error]
283 pub enum Error<T, I = ()> {
284 InsufficientProposersBalance,
286 InvalidIndex,
288 TooManyApprovals,
290 InsufficientPermission,
293 ProposalNotApproved,
295 }
296
297 #[pallet::hooks]
298 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
299 fn on_initialize(n: frame_system::pallet_prelude::BlockNumberFor<T>) -> Weight {
302 let pot = Self::pot();
303 let deactivated = Deactivated::<T, I>::get();
304 if pot != deactivated {
305 T::Currency::reactivate(deactivated);
306 T::Currency::deactivate(pot);
307 Deactivated::<T, I>::put(pot);
308 Self::deposit_event(Event::<T, I>::UpdatedInactive {
309 reactivated: deactivated,
310 deactivated: pot,
311 });
312 }
313
314 if (n % T::SpendPeriod::get()).is_zero() {
316 Self::spend_funds()
317 } else {
318 Weight::zero()
319 }
320 }
321
322 #[cfg(feature = "try-runtime")]
323 fn try_state(
324 _: frame_system::pallet_prelude::BlockNumberFor<T>,
325 ) -> Result<(), sp_runtime::TryRuntimeError> {
326 Self::do_try_state()?;
327 Ok(())
328 }
329 }
330
331 #[pallet::call]
332 impl<T: Config<I>, I: 'static> Pallet<T, I> {
333 #[pallet::call_index(0)]
350 #[pallet::weight(T::WeightInfo::propose_spend())]
351 #[allow(deprecated)]
352 #[deprecated(
353 note = "`propose_spend` will be removed in February 2024. Use `spend` instead."
354 )]
355 pub fn propose_spend(
356 origin: OriginFor<T>,
357 #[pallet::compact] value: BalanceOf<T, I>,
358 beneficiary: AccountIdLookupOf<T>,
359 ) -> DispatchResult {
360 let proposer = ensure_signed(origin)?;
361 let beneficiary = T::Lookup::lookup(beneficiary)?;
362
363 let bond = Self::calculate_bond(value);
364 T::Currency::reserve(&proposer, bond)
365 .map_err(|_| Error::<T, I>::InsufficientProposersBalance)?;
366
367 let c = Self::proposal_count();
368 <ProposalCount<T, I>>::put(c + 1);
369 <Proposals<T, I>>::insert(
370 c,
371 Proposal {
372 proposer,
373 value,
374 beneficiary,
375 bond,
376 },
377 );
378
379 Self::deposit_event(Event::Proposed { proposal_index: c });
380 Ok(())
381 }
382
383 #[pallet::call_index(1)]
399 #[pallet::weight((T::WeightInfo::reject_proposal(), DispatchClass::Operational))]
400 #[allow(deprecated)]
401 #[deprecated(
402 note = "`reject_proposal` will be removed in February 2024. Use `spend` instead."
403 )]
404 pub fn reject_proposal(
405 origin: OriginFor<T>,
406 #[pallet::compact] proposal_id: ProposalIndex,
407 ) -> DispatchResult {
408 T::RejectOrigin::ensure_origin(origin)?;
409
410 let proposal =
411 <Proposals<T, I>>::take(proposal_id).ok_or(Error::<T, I>::InvalidIndex)?;
412 let value = proposal.bond;
413 let imbalance = T::Currency::slash_reserved(&proposal.proposer, value).0;
414 T::OnSlash::on_unbalanced(imbalance);
415
416 Self::deposit_event(Event::<T, I>::Rejected {
417 proposal_index: proposal_id,
418 slashed: value,
419 });
420 Ok(())
421 }
422
423 #[pallet::call_index(2)]
441 #[pallet::weight((T::WeightInfo::approve_proposal(T::MaxApprovals::get()), DispatchClass::Operational))]
442 #[allow(deprecated)]
443 #[deprecated(
444 note = "`approve_proposal` will be removed in February 2024. Use `spend` instead."
445 )]
446 pub fn approve_proposal(
447 origin: OriginFor<T>,
448 #[pallet::compact] proposal_id: ProposalIndex,
449 ) -> DispatchResult {
450 T::ApproveOrigin::ensure_origin(origin)?;
451
452 ensure!(
453 <Proposals<T, I>>::contains_key(proposal_id),
454 Error::<T, I>::InvalidIndex
455 );
456 Approvals::<T, I>::try_append(proposal_id)
457 .map_err(|_| Error::<T, I>::TooManyApprovals)?;
458 Ok(())
459 }
460 }
461}
462
463impl<T: Config<I>, I: 'static> Pallet<T, I> {
464 pub fn account_id() -> T::AccountId {
471 T::PalletId::get().into_account_truncating()
472 }
473
474 fn calculate_bond(value: BalanceOf<T, I>) -> BalanceOf<T, I> {
476 let mut r = T::ProposalBondMinimum::get().max(T::ProposalBond::get() * value);
477 if let Some(m) = T::ProposalBondMaximum::get() {
478 r = r.min(m);
479 }
480 r
481 }
482
483 pub fn spend_funds() -> Weight {
485 let mut total_weight = Weight::zero();
486
487 let mut budget_remaining = Self::pot();
488 Self::deposit_event(Event::Spending { budget_remaining });
489 let account_id = Self::account_id();
490
491 let mut missed_any = false;
492 let mut imbalance = <PositiveImbalanceOf<T, I>>::zero();
493 let proposals_len = Approvals::<T, I>::mutate(|v| {
494 let proposals_approvals_len = v.len() as u32;
495 v.retain(|&index| {
496 if let Some(p) = Self::proposals(index) {
498 if p.value <= budget_remaining {
499 budget_remaining -= p.value;
500 <Proposals<T, I>>::remove(index);
501
502 let err_amount = T::Currency::unreserve(&p.proposer, p.bond);
504 debug_assert!(err_amount.is_zero());
505
506 imbalance.subsume(T::Currency::deposit_creating(&p.beneficiary, p.value));
508
509 Self::deposit_event(Event::Awarded {
510 proposal_index: index,
511 award: p.value,
512 account: p.beneficiary,
513 });
514 false
515 } else {
516 missed_any = true;
517 true
518 }
519 } else {
520 false
521 }
522 });
523 proposals_approvals_len
524 });
525
526 total_weight += T::WeightInfo::on_initialize_proposals(proposals_len);
527
528 T::SpendFunds::spend_funds(
530 &mut budget_remaining,
531 &mut imbalance,
532 &mut total_weight,
533 &mut missed_any,
534 );
535
536 if !missed_any {
537 let burn = (T::Burn::get() * budget_remaining).min(budget_remaining);
539 budget_remaining -= burn;
540
541 let (debit, credit) = T::Currency::pair(burn);
542 imbalance.subsume(debit);
543 T::BurnDestination::on_unbalanced(credit);
544 Self::deposit_event(Event::Burnt { burnt_funds: burn })
545 }
546
547 if let Err(problem) =
552 T::Currency::settle(&account_id, imbalance, WithdrawReasons::TRANSFER, KeepAlive)
553 {
554 print("Inconsistent state - couldn't settle imbalance for funds spent by treasury");
555 drop(problem);
557 }
558
559 Self::deposit_event(Event::Rollover {
560 rollover_balance: budget_remaining,
561 });
562
563 total_weight
564 }
565
566 pub fn pot() -> BalanceOf<T, I> {
569 T::Currency::free_balance(&Self::account_id())
570 .saturating_sub(T::Currency::minimum_balance())
572 }
573
574 #[cfg(any(feature = "try-runtime", test))]
576 fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
577 Self::try_state_proposals()?;
578 Ok(())
579 }
580
581 #[cfg(any(feature = "try-runtime", test))]
589 fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> {
590 let current_proposal_count = ProposalCount::<T, I>::get();
591 ensure!(
592 current_proposal_count as usize >= Proposals::<T, I>::iter().count(),
593 "Actual number of proposals exceeds `ProposalCount`."
594 );
595
596 Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult {
597 ensure!(
598 current_proposal_count > proposal_index,
599 "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`."
600 );
601 Ok(())
602 })?;
603
604 Approvals::<T, I>::get()
605 .iter()
606 .try_for_each(|proposal_index| -> DispatchResult {
607 ensure!(
608 Proposals::<T, I>::contains_key(proposal_index),
609 "Proposal indices in `Approvals` must also be contained in `Proposals`."
610 );
611 Ok(())
612 })?;
613
614 Ok(())
615 }
616}
617
618impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
619 fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
620 let numeric_amount = amount.peek();
621
622 T::Currency::resolve_creating(&Self::account_id(), amount);
624
625 Self::deposit_event(Event::Deposit {
626 value: numeric_amount,
627 });
628 }
629}
630
631pub struct TreasuryAccountId<R>(PhantomData<R>);
633impl<R> sp_runtime::traits::TypedGet for TreasuryAccountId<R>
634where
635 R: crate::Config,
636{
637 type Type = <R as frame_system::Config>::AccountId;
638 fn get() -> Self::Type {
639 <crate::Pallet<R>>::account_id()
640 }
641}