1#![cfg_attr(not(feature = "std"), no_std)]
86
87#[cfg(feature = "runtime-benchmarks")]
88mod benchmarking;
89pub mod migrations;
90mod tests;
91pub mod weights;
92
93extern crate alloc;
94
95use alloc::vec::Vec;
96
97use frame_support::traits::{
98 Currency, ExistenceRequirement::AllowDeath, Get, Imbalance, OnUnbalanced, ReservableCurrency,
99};
100
101use sp_runtime::{
102 traits::{AccountIdConversion, BadOrigin, BlockNumberProvider, Saturating, StaticLookup, Zero},
103 Debug, DispatchResult, Permill,
104};
105
106use frame_support::{dispatch::DispatchResultWithPostInfo, traits::EnsureOrigin};
107
108use frame_support::pallet_prelude::*;
109use frame_system::pallet_prelude::{
110 ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
111};
112use scale_info::TypeInfo;
113pub use weights::WeightInfo;
114
115pub use pallet::*;
116
117type BalanceOf<T, I = ()> = pallet_treasury::BalanceOf<T, I>;
118
119type PositiveImbalanceOf<T, I = ()> = pallet_treasury::PositiveImbalanceOf<T, I>;
120
121pub type BountyIndex = u32;
123
124type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
125
126type BlockNumberFor<T, I = ()> =
127 <<T as pallet_treasury::Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
128
129#[derive(
131 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
132)]
133pub struct Bounty<AccountId, Balance, BlockNumber> {
134 pub proposer: AccountId,
136 pub value: Balance,
138 pub fee: Balance,
140 pub curator_deposit: Balance,
142 bond: Balance,
144 status: BountyStatus<AccountId, BlockNumber>,
146}
147
148impl<AccountId: PartialEq + Clone + Ord, Balance, BlockNumber: Clone>
149 Bounty<AccountId, Balance, BlockNumber>
150{
151 pub fn get_status(&self) -> BountyStatus<AccountId, BlockNumber> {
153 self.status.clone()
154 }
155}
156
157#[derive(
159 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
160)]
161pub enum BountyStatus<AccountId, BlockNumber> {
162 Proposed,
164 Approved,
166 Funded,
168 CuratorProposed {
170 curator: AccountId,
172 },
173 Active {
175 curator: AccountId,
177 update_due: BlockNumber,
179 },
180 PendingPayout {
182 curator: AccountId,
184 beneficiary: AccountId,
186 unlock_at: BlockNumber,
188 },
189 ApprovedWithCurator {
191 curator: AccountId,
193 },
194}
195
196pub trait ChildBountyManager<Balance> {
198 fn child_bounties_count(bounty_id: BountyIndex) -> BountyIndex;
200
201 fn children_curator_fees(bounty_id: BountyIndex) -> Balance;
203
204 fn bounty_removed(bounty_id: BountyIndex);
206}
207
208#[frame_support::pallet]
209pub mod pallet {
210 use super::*;
211
212 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
213
214 #[pallet::pallet]
215 #[pallet::storage_version(STORAGE_VERSION)]
216 pub struct Pallet<T, I = ()>(_);
217
218 #[pallet::config]
219 pub trait Config<I: 'static = ()>: frame_system::Config + pallet_treasury::Config<I> {
220 #[pallet::constant]
222 type BountyDepositBase: Get<BalanceOf<Self, I>>;
223
224 #[pallet::constant]
226 type BountyDepositPayoutDelay: Get<BlockNumberFor<Self, I>>;
227
228 #[pallet::constant]
235 type BountyUpdatePeriod: Get<BlockNumberFor<Self, I>>;
236
237 #[pallet::constant]
242 type CuratorDepositMultiplier: Get<Permill>;
243
244 #[pallet::constant]
246 type CuratorDepositMax: Get<Option<BalanceOf<Self, I>>>;
247
248 #[pallet::constant]
250 type CuratorDepositMin: Get<Option<BalanceOf<Self, I>>>;
251
252 #[pallet::constant]
254 type BountyValueMinimum: Get<BalanceOf<Self, I>>;
255
256 #[pallet::constant]
258 type DataDepositPerByte: Get<BalanceOf<Self, I>>;
259
260 #[allow(deprecated)]
262 type RuntimeEvent: From<Event<Self, I>>
263 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
264
265 #[pallet::constant]
269 type MaximumReasonLength: Get<u32>;
270
271 type WeightInfo: WeightInfo;
273
274 type ChildBountyManager: ChildBountyManager<BalanceOf<Self, I>>;
276
277 type OnSlash: OnUnbalanced<pallet_treasury::NegativeImbalanceOf<Self, I>>;
279 }
280
281 #[pallet::error]
282 pub enum Error<T, I = ()> {
283 InsufficientProposersBalance,
285 InvalidIndex,
287 ReasonTooBig,
289 UnexpectedStatus,
291 RequireCurator,
293 InvalidValue,
295 InvalidFee,
297 PendingPayout,
300 Premature,
302 HasActiveChildBounty,
304 TooManyQueued,
306 NotProposer,
308 }
309
310 #[pallet::event]
311 #[pallet::generate_deposit(pub(super) fn deposit_event)]
312 pub enum Event<T: Config<I>, I: 'static = ()> {
313 BountyProposed { index: BountyIndex },
315 BountyRejected { index: BountyIndex, bond: BalanceOf<T, I> },
317 BountyBecameActive { index: BountyIndex },
319 BountyAwarded { index: BountyIndex, beneficiary: T::AccountId },
321 BountyClaimed { index: BountyIndex, payout: BalanceOf<T, I>, beneficiary: T::AccountId },
323 BountyCanceled { index: BountyIndex },
325 BountyExtended { index: BountyIndex },
327 BountyApproved { index: BountyIndex },
329 CuratorProposed { bounty_id: BountyIndex, curator: T::AccountId },
331 CuratorUnassigned { bounty_id: BountyIndex },
333 CuratorAccepted { bounty_id: BountyIndex, curator: T::AccountId },
335 DepositPoked {
337 bounty_id: BountyIndex,
338 proposer: T::AccountId,
339 old_deposit: BalanceOf<T, I>,
340 new_deposit: BalanceOf<T, I>,
341 },
342 }
343
344 #[pallet::storage]
346 pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, BountyIndex, ValueQuery>;
347
348 #[pallet::storage]
350 pub type Bounties<T: Config<I>, I: 'static = ()> = StorageMap<
351 _,
352 Twox64Concat,
353 BountyIndex,
354 Bounty<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T, I>>,
355 >;
356
357 #[pallet::storage]
359 pub type BountyDescriptions<T: Config<I>, I: 'static = ()> =
360 StorageMap<_, Twox64Concat, BountyIndex, BoundedVec<u8, T::MaximumReasonLength>>;
361
362 #[pallet::storage]
364 #[allow(deprecated)]
365 pub type BountyApprovals<T: Config<I>, I: 'static = ()> =
366 StorageValue<_, BoundedVec<BountyIndex, T::MaxApprovals>, ValueQuery>;
367
368 #[pallet::call]
369 impl<T: Config<I>, I: 'static> Pallet<T, I> {
370 #[pallet::call_index(0)]
383 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_bounty(description.len() as u32))]
384 pub fn propose_bounty(
385 origin: OriginFor<T>,
386 #[pallet::compact] value: BalanceOf<T, I>,
387 description: Vec<u8>,
388 ) -> DispatchResult {
389 let proposer = ensure_signed(origin)?;
390 Self::create_bounty(proposer, description, value)?;
391 Ok(())
392 }
393
394 #[pallet::call_index(1)]
402 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty())]
403 pub fn approve_bounty(
404 origin: OriginFor<T>,
405 #[pallet::compact] bounty_id: BountyIndex,
406 ) -> DispatchResult {
407 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
408 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
409 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
410 ensure!(
411 bounty.value <= max_amount,
412 pallet_treasury::Error::<T, I>::InsufficientPermission
413 );
414 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
415
416 bounty.status = BountyStatus::Approved;
417
418 BountyApprovals::<T, I>::try_append(bounty_id)
419 .map_err(|()| Error::<T, I>::TooManyQueued)?;
420
421 Ok(())
422 })?;
423
424 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
425 Ok(())
426 }
427
428 #[pallet::call_index(2)]
435 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_curator())]
436 pub fn propose_curator(
437 origin: OriginFor<T>,
438 #[pallet::compact] bounty_id: BountyIndex,
439 curator: AccountIdLookupOf<T>,
440 #[pallet::compact] fee: BalanceOf<T, I>,
441 ) -> DispatchResult {
442 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
443
444 let curator = T::Lookup::lookup(curator)?;
445 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
446 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
447 ensure!(
448 bounty.value <= max_amount,
449 pallet_treasury::Error::<T, I>::InsufficientPermission
450 );
451 match bounty.status {
452 BountyStatus::Funded => {},
453 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
454 };
455
456 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
457
458 bounty.status = BountyStatus::CuratorProposed { curator: curator.clone() };
459 bounty.fee = fee;
460
461 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
462
463 Ok(())
464 })?;
465 Ok(())
466 }
467
468 #[pallet::call_index(3)]
486 #[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
487 pub fn unassign_curator(
488 origin: OriginFor<T>,
489 #[pallet::compact] bounty_id: BountyIndex,
490 ) -> DispatchResult {
491 let maybe_sender = ensure_signed(origin.clone())
492 .map(Some)
493 .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
494
495 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
496 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
497
498 let slash_curator =
499 |curator: &T::AccountId, curator_deposit: &mut BalanceOf<T, I>| {
500 let imbalance = T::Currency::slash_reserved(curator, *curator_deposit).0;
501 T::OnSlash::on_unbalanced(imbalance);
502 *curator_deposit = Zero::zero();
503 };
504
505 match bounty.status {
506 BountyStatus::Proposed | BountyStatus::Approved | BountyStatus::Funded => {
507 return Err(Error::<T, I>::UnexpectedStatus.into())
509 },
510 BountyStatus::ApprovedWithCurator { ref curator } => {
511 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
514 bounty.status = BountyStatus::Approved;
517 return Ok(());
518 },
519 BountyStatus::CuratorProposed { ref curator } => {
520 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
523 },
524 BountyStatus::Active { ref curator, ref update_due } => {
525 match maybe_sender {
527 None => {
529 slash_curator(curator, &mut bounty.curator_deposit);
530 },
532 Some(sender) => {
533 if sender != *curator {
536 let block_number = Self::treasury_block_number();
537 if *update_due < block_number {
538 slash_curator(curator, &mut bounty.curator_deposit);
539 } else {
541 return Err(Error::<T, I>::Premature.into())
543 }
544 } else {
545 let err_amount =
548 T::Currency::unreserve(curator, bounty.curator_deposit);
549 debug_assert!(err_amount.is_zero());
550 bounty.curator_deposit = Zero::zero();
551 }
553 },
554 }
555 },
556 BountyStatus::PendingPayout { ref curator, .. } => {
557 ensure!(maybe_sender.is_none(), BadOrigin);
561 slash_curator(curator, &mut bounty.curator_deposit);
562 },
564 };
565
566 bounty.status = BountyStatus::Funded;
567 Ok(())
568 })?;
569
570 Self::deposit_event(Event::<T, I>::CuratorUnassigned { bounty_id });
571 Ok(())
572 }
573
574 #[pallet::call_index(4)]
582 #[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
583 pub fn accept_curator(
584 origin: OriginFor<T>,
585 #[pallet::compact] bounty_id: BountyIndex,
586 ) -> DispatchResult {
587 let signer = ensure_signed(origin)?;
588
589 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
590 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
591
592 match bounty.status {
593 BountyStatus::CuratorProposed { ref curator } => {
594 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
595
596 let deposit = Self::calculate_curator_deposit(&bounty.fee);
597 T::Currency::reserve(curator, deposit)?;
598 bounty.curator_deposit = deposit;
599
600 let update_due = Self::treasury_block_number()
601 .saturating_add(T::BountyUpdatePeriod::get());
602 bounty.status =
603 BountyStatus::Active { curator: curator.clone(), update_due };
604
605 Self::deposit_event(Event::<T, I>::CuratorAccepted {
606 bounty_id,
607 curator: signer,
608 });
609 Ok(())
610 },
611 _ => Err(Error::<T, I>::UnexpectedStatus.into()),
612 }
613 })?;
614 Ok(())
615 }
616
617 #[pallet::call_index(5)]
628 #[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
629 pub fn award_bounty(
630 origin: OriginFor<T>,
631 #[pallet::compact] bounty_id: BountyIndex,
632 beneficiary: AccountIdLookupOf<T>,
633 ) -> DispatchResult {
634 let signer = ensure_signed(origin)?;
635 let beneficiary = T::Lookup::lookup(beneficiary)?;
636
637 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
638 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
639
640 ensure!(
642 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
643 Error::<T, I>::HasActiveChildBounty
644 );
645
646 match &bounty.status {
647 BountyStatus::Active { curator, .. } => {
648 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
649 },
650 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
651 }
652 bounty.status = BountyStatus::PendingPayout {
653 curator: signer,
654 beneficiary: beneficiary.clone(),
655 unlock_at: Self::treasury_block_number() + T::BountyDepositPayoutDelay::get(),
656 };
657
658 Ok(())
659 })?;
660
661 Self::deposit_event(Event::<T, I>::BountyAwarded { index: bounty_id, beneficiary });
662 Ok(())
663 }
664
665 #[pallet::call_index(6)]
674 #[pallet::weight(<T as Config<I>>::WeightInfo::claim_bounty())]
675 pub fn claim_bounty(
676 origin: OriginFor<T>,
677 #[pallet::compact] bounty_id: BountyIndex,
678 ) -> DispatchResult {
679 ensure_signed(origin)?; Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
682 let bounty = maybe_bounty.take().ok_or(Error::<T, I>::InvalidIndex)?;
683 if let BountyStatus::PendingPayout { curator, beneficiary, unlock_at } =
684 bounty.status
685 {
686 ensure!(Self::treasury_block_number() >= unlock_at, Error::<T, I>::Premature);
687 let bounty_account = Self::bounty_account_id(bounty_id);
688 let balance = T::Currency::free_balance(&bounty_account);
689 let fee = bounty.fee.min(balance); let payout = balance.saturating_sub(fee);
691 let err_amount = T::Currency::unreserve(&curator, bounty.curator_deposit);
692 debug_assert!(err_amount.is_zero());
693
694 let children_fee = T::ChildBountyManager::children_curator_fees(bounty_id);
697 debug_assert!(children_fee <= fee);
698
699 let final_fee = fee.saturating_sub(children_fee);
700 let res =
701 T::Currency::transfer(&bounty_account, &curator, final_fee, AllowDeath); debug_assert!(res.is_ok());
703 let res =
704 T::Currency::transfer(&bounty_account, &beneficiary, payout, AllowDeath); debug_assert!(res.is_ok());
706
707 *maybe_bounty = None;
708
709 BountyDescriptions::<T, I>::remove(bounty_id);
710 T::ChildBountyManager::bounty_removed(bounty_id);
711
712 Self::deposit_event(Event::<T, I>::BountyClaimed {
713 index: bounty_id,
714 payout,
715 beneficiary,
716 });
717 Ok(())
718 } else {
719 Err(Error::<T, I>::UnexpectedStatus.into())
720 }
721 })?;
722 Ok(())
723 }
724
725 #[pallet::call_index(7)]
735 #[pallet::weight(<T as Config<I>>::WeightInfo::close_bounty_proposed()
736 .max(<T as Config<I>>::WeightInfo::close_bounty_active()))]
737 pub fn close_bounty(
738 origin: OriginFor<T>,
739 #[pallet::compact] bounty_id: BountyIndex,
740 ) -> DispatchResultWithPostInfo {
741 T::RejectOrigin::ensure_origin(origin)?;
742
743 Bounties::<T, I>::try_mutate_exists(
744 bounty_id,
745 |maybe_bounty| -> DispatchResultWithPostInfo {
746 let bounty = maybe_bounty.as_ref().ok_or(Error::<T, I>::InvalidIndex)?;
747
748 ensure!(
750 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
751 Error::<T, I>::HasActiveChildBounty
752 );
753
754 match &bounty.status {
755 BountyStatus::Proposed => {
756 BountyDescriptions::<T, I>::remove(bounty_id);
758 let value = bounty.bond;
759 let imbalance = T::Currency::slash_reserved(&bounty.proposer, value).0;
760 T::OnSlash::on_unbalanced(imbalance);
761 *maybe_bounty = None;
762
763 Self::deposit_event(Event::<T, I>::BountyRejected {
764 index: bounty_id,
765 bond: value,
766 });
767 return Ok(
769 Some(<T as Config<I>>::WeightInfo::close_bounty_proposed()).into()
770 )
771 },
772 BountyStatus::Approved | BountyStatus::ApprovedWithCurator { .. } => {
773 return Err(Error::<T, I>::UnexpectedStatus.into())
776 },
777 BountyStatus::Funded | BountyStatus::CuratorProposed { .. } => {
778 },
780 BountyStatus::Active { curator, .. } => {
781 let err_amount =
783 T::Currency::unreserve(curator, bounty.curator_deposit);
784 debug_assert!(err_amount.is_zero());
785 },
787 BountyStatus::PendingPayout { .. } => {
788 return Err(Error::<T, I>::PendingPayout.into())
793 },
794 }
795
796 let bounty_account = Self::bounty_account_id(bounty_id);
797
798 BountyDescriptions::<T, I>::remove(bounty_id);
799
800 let balance = T::Currency::free_balance(&bounty_account);
801 let res = T::Currency::transfer(
802 &bounty_account,
803 &Self::account_id(),
804 balance,
805 AllowDeath,
806 ); debug_assert!(res.is_ok());
808
809 *maybe_bounty = None;
810 T::ChildBountyManager::bounty_removed(bounty_id);
811
812 Self::deposit_event(Event::<T, I>::BountyCanceled { index: bounty_id });
813 Ok(Some(<T as Config<I>>::WeightInfo::close_bounty_active()).into())
814 },
815 )
816 }
817
818 #[pallet::call_index(8)]
828 #[pallet::weight(<T as Config<I>>::WeightInfo::extend_bounty_expiry())]
829 pub fn extend_bounty_expiry(
830 origin: OriginFor<T>,
831 #[pallet::compact] bounty_id: BountyIndex,
832 _remark: Vec<u8>,
833 ) -> DispatchResult {
834 let signer = ensure_signed(origin)?;
835
836 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
837 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
838
839 match bounty.status {
840 BountyStatus::Active { ref curator, ref mut update_due } => {
841 ensure!(*curator == signer, Error::<T, I>::RequireCurator);
842 *update_due = Self::treasury_block_number()
843 .saturating_add(T::BountyUpdatePeriod::get())
844 .max(*update_due);
845 },
846 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
847 }
848
849 Ok(())
850 })?;
851
852 Self::deposit_event(Event::<T, I>::BountyExtended { index: bounty_id });
853 Ok(())
854 }
855
856 #[pallet::call_index(9)]
868 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty_with_curator())]
869 pub fn approve_bounty_with_curator(
870 origin: OriginFor<T>,
871 #[pallet::compact] bounty_id: BountyIndex,
872 curator: AccountIdLookupOf<T>,
873 #[pallet::compact] fee: BalanceOf<T, I>,
874 ) -> DispatchResult {
875 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
876 let curator = T::Lookup::lookup(curator)?;
877 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
878 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
880 ensure!(
881 bounty.value <= max_amount,
882 pallet_treasury::Error::<T, I>::InsufficientPermission
883 );
884 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
885 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
886
887 BountyApprovals::<T, I>::try_append(bounty_id)
888 .map_err(|()| Error::<T, I>::TooManyQueued)?;
889
890 bounty.status = BountyStatus::ApprovedWithCurator { curator: curator.clone() };
891 bounty.fee = fee;
892
893 Ok(())
894 })?;
895
896 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
897 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
898
899 Ok(())
900 }
901
902 #[pallet::call_index(10)]
918 #[pallet::weight(<T as Config<I>>::WeightInfo::poke_deposit())]
919 pub fn poke_deposit(
920 origin: OriginFor<T>,
921 #[pallet::compact] bounty_id: BountyIndex,
922 ) -> DispatchResultWithPostInfo {
923 ensure_signed(origin)?;
924
925 let deposit_updated = Self::poke_bounty_deposit(bounty_id)?;
926
927 Ok(if deposit_updated { Pays::No } else { Pays::Yes }.into())
928 }
929 }
930
931 #[pallet::hooks]
932 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
933 #[cfg(feature = "try-runtime")]
934 fn try_state(_n: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
935 Self::do_try_state()
936 }
937 }
938}
939
940#[cfg(any(feature = "try-runtime", test))]
941impl<T: Config<I>, I: 'static> Pallet<T, I> {
942 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
946 Self::try_state_bounties_count()?;
947
948 Ok(())
949 }
950
951 fn try_state_bounties_count() -> Result<(), sp_runtime::TryRuntimeError> {
959 let bounties_length = Bounties::<T, I>::iter().count() as u32;
960
961 ensure!(
962 <BountyCount<T, I>>::get() >= bounties_length,
963 "`BountyCount` must be grater or equals the number of `Bounties` in storage"
964 );
965
966 let bounties_description_length = BountyDescriptions::<T, I>::iter().count() as u32;
967 ensure!(
968 <BountyCount<T, I>>::get() >= bounties_description_length,
969 "`BountyCount` must be grater or equals the number of `BountiesDescriptions` in storage."
970 );
971
972 ensure!(
973 bounties_length == bounties_description_length,
974 "Number of `Bounties` in storage must be the same as the Number of `BountiesDescription` in storage."
975 );
976 Ok(())
977 }
978}
979
980impl<T: Config<I>, I: 'static> Pallet<T, I> {
981 pub fn treasury_block_number() -> BlockNumberFor<T, I> {
985 <T as pallet_treasury::Config<I>>::BlockNumberProvider::current_block_number()
986 }
987
988 pub fn calculate_curator_deposit(fee: &BalanceOf<T, I>) -> BalanceOf<T, I> {
990 let mut deposit = T::CuratorDepositMultiplier::get() * *fee;
991
992 if let Some(max_deposit) = T::CuratorDepositMax::get() {
993 deposit = deposit.min(max_deposit)
994 }
995
996 if let Some(min_deposit) = T::CuratorDepositMin::get() {
997 deposit = deposit.max(min_deposit)
998 }
999
1000 deposit
1001 }
1002
1003 pub fn account_id() -> T::AccountId {
1008 T::PalletId::get().into_account_truncating()
1009 }
1010
1011 pub fn bounty_account_id(id: BountyIndex) -> T::AccountId {
1013 T::PalletId::get().into_sub_account_truncating(("bt", id))
1016 }
1017
1018 fn create_bounty(
1019 proposer: T::AccountId,
1020 description: Vec<u8>,
1021 value: BalanceOf<T, I>,
1022 ) -> DispatchResult {
1023 let bounded_description: BoundedVec<_, _> =
1024 description.try_into().map_err(|_| Error::<T, I>::ReasonTooBig)?;
1025 ensure!(value >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
1026
1027 let index = BountyCount::<T, I>::get();
1028
1029 let bond = Self::calculate_bounty_deposit(&bounded_description);
1031 T::Currency::reserve(&proposer, bond)
1032 .map_err(|_| Error::<T, I>::InsufficientProposersBalance)?;
1033
1034 BountyCount::<T, I>::put(index + 1);
1035
1036 let bounty = Bounty {
1037 proposer,
1038 value,
1039 fee: 0u32.into(),
1040 curator_deposit: 0u32.into(),
1041 bond,
1042 status: BountyStatus::Proposed,
1043 };
1044
1045 Bounties::<T, I>::insert(index, &bounty);
1046 BountyDescriptions::<T, I>::insert(index, bounded_description);
1047
1048 Self::deposit_event(Event::<T, I>::BountyProposed { index });
1049
1050 Ok(())
1051 }
1052
1053 fn calculate_bounty_deposit(
1055 description: &BoundedVec<u8, T::MaximumReasonLength>,
1056 ) -> BalanceOf<T, I> {
1057 T::BountyDepositBase::get().saturating_add(
1058 T::DataDepositPerByte::get().saturating_mul((description.len() as u32).into()),
1059 )
1060 }
1061
1062 fn poke_bounty_deposit(bounty_id: BountyIndex) -> Result<bool, DispatchError> {
1066 let mut bounty = Bounties::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1067 let bounty_description =
1068 BountyDescriptions::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1069 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
1071
1072 let new_bond = Self::calculate_bounty_deposit(&bounty_description);
1073 let old_bond = bounty.bond;
1074 if new_bond == old_bond {
1075 return Ok(false);
1076 }
1077 if new_bond > old_bond {
1078 let extra = new_bond.saturating_sub(old_bond);
1079 T::Currency::reserve(&bounty.proposer, extra)?;
1080 } else {
1081 let excess = old_bond.saturating_sub(new_bond);
1082 let remaining_unreserved = T::Currency::unreserve(&bounty.proposer, excess);
1083 if !remaining_unreserved.is_zero() {
1084 defensive!(
1085 "Failed to unreserve full amount. (Requested, Actual)",
1086 (excess, excess.saturating_sub(remaining_unreserved))
1087 );
1088 }
1089 }
1090 bounty.bond = new_bond;
1091 Bounties::<T, I>::insert(bounty_id, &bounty);
1092
1093 Self::deposit_event(Event::<T, I>::DepositPoked {
1094 bounty_id,
1095 proposer: bounty.proposer,
1096 old_deposit: old_bond,
1097 new_deposit: new_bond,
1098 });
1099
1100 Ok(true)
1101 }
1102}
1103
1104impl<T: Config<I>, I: 'static> pallet_treasury::SpendFunds<T, I> for Pallet<T, I> {
1105 fn spend_funds(
1106 budget_remaining: &mut BalanceOf<T, I>,
1107 imbalance: &mut PositiveImbalanceOf<T, I>,
1108 total_weight: &mut Weight,
1109 missed_any: &mut bool,
1110 ) {
1111 let bounties_len = BountyApprovals::<T, I>::mutate(|v| {
1112 let bounties_approval_len = v.len() as u32;
1113 v.retain(|&index| {
1114 Bounties::<T, I>::mutate(index, |bounty| {
1115 if let Some(bounty) = bounty {
1117 if bounty.value <= *budget_remaining {
1118 *budget_remaining -= bounty.value;
1119
1120 if let BountyStatus::ApprovedWithCurator { curator } = &bounty.status {
1122 bounty.status =
1123 BountyStatus::CuratorProposed { curator: curator.clone() };
1124 } else {
1125 bounty.status = BountyStatus::Funded;
1126 }
1127
1128 let err_amount = T::Currency::unreserve(&bounty.proposer, bounty.bond);
1130 debug_assert!(err_amount.is_zero());
1131
1132 imbalance.subsume(T::Currency::deposit_creating(
1134 &Self::bounty_account_id(index),
1135 bounty.value,
1136 ));
1137
1138 Self::deposit_event(Event::<T, I>::BountyBecameActive { index });
1139 false
1140 } else {
1141 *missed_any = true;
1142 true
1143 }
1144 } else {
1145 false
1146 }
1147 })
1148 });
1149 bounties_approval_len
1150 });
1151
1152 *total_weight += <T as pallet::Config<I>>::WeightInfo::spend_funds(bounties_len);
1153 }
1154}
1155
1156impl<Balance: Zero> ChildBountyManager<Balance> for () {
1158 fn child_bounties_count(_bounty_id: BountyIndex) -> BountyIndex {
1159 Default::default()
1160 }
1161
1162 fn children_curator_fees(_bounty_id: BountyIndex) -> Balance {
1163 Zero::zero()
1164 }
1165
1166 fn bounty_removed(_bounty_id: BountyIndex) {}
1167}