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 DispatchResult, Permill, RuntimeDebug,
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,
132 Decode,
133 DecodeWithMemTracking,
134 Clone,
135 PartialEq,
136 Eq,
137 RuntimeDebug,
138 TypeInfo,
139 MaxEncodedLen,
140)]
141pub struct Bounty<AccountId, Balance, BlockNumber> {
142 pub proposer: AccountId,
144 pub value: Balance,
146 pub fee: Balance,
148 pub curator_deposit: Balance,
150 bond: Balance,
152 status: BountyStatus<AccountId, BlockNumber>,
154}
155
156impl<AccountId: PartialEq + Clone + Ord, Balance, BlockNumber: Clone>
157 Bounty<AccountId, Balance, BlockNumber>
158{
159 pub fn get_status(&self) -> BountyStatus<AccountId, BlockNumber> {
161 self.status.clone()
162 }
163}
164
165#[derive(
167 Encode,
168 Decode,
169 DecodeWithMemTracking,
170 Clone,
171 PartialEq,
172 Eq,
173 RuntimeDebug,
174 TypeInfo,
175 MaxEncodedLen,
176)]
177pub enum BountyStatus<AccountId, BlockNumber> {
178 Proposed,
180 Approved,
182 Funded,
184 CuratorProposed {
186 curator: AccountId,
188 },
189 Active {
191 curator: AccountId,
193 update_due: BlockNumber,
195 },
196 PendingPayout {
198 curator: AccountId,
200 beneficiary: AccountId,
202 unlock_at: BlockNumber,
204 },
205 ApprovedWithCurator {
207 curator: AccountId,
209 },
210}
211
212pub trait ChildBountyManager<Balance> {
214 fn child_bounties_count(bounty_id: BountyIndex) -> BountyIndex;
216
217 fn children_curator_fees(bounty_id: BountyIndex) -> Balance;
219
220 fn bounty_removed(bounty_id: BountyIndex);
222}
223
224#[frame_support::pallet]
225pub mod pallet {
226 use super::*;
227
228 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
229
230 #[pallet::pallet]
231 #[pallet::storage_version(STORAGE_VERSION)]
232 pub struct Pallet<T, I = ()>(_);
233
234 #[pallet::config]
235 pub trait Config<I: 'static = ()>: frame_system::Config + pallet_treasury::Config<I> {
236 #[pallet::constant]
238 type BountyDepositBase: Get<BalanceOf<Self, I>>;
239
240 #[pallet::constant]
242 type BountyDepositPayoutDelay: Get<BlockNumberFor<Self, I>>;
243
244 #[pallet::constant]
251 type BountyUpdatePeriod: Get<BlockNumberFor<Self, I>>;
252
253 #[pallet::constant]
258 type CuratorDepositMultiplier: Get<Permill>;
259
260 #[pallet::constant]
262 type CuratorDepositMax: Get<Option<BalanceOf<Self, I>>>;
263
264 #[pallet::constant]
266 type CuratorDepositMin: Get<Option<BalanceOf<Self, I>>>;
267
268 #[pallet::constant]
270 type BountyValueMinimum: Get<BalanceOf<Self, I>>;
271
272 #[pallet::constant]
274 type DataDepositPerByte: Get<BalanceOf<Self, I>>;
275
276 #[allow(deprecated)]
278 type RuntimeEvent: From<Event<Self, I>>
279 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
280
281 #[pallet::constant]
285 type MaximumReasonLength: Get<u32>;
286
287 type WeightInfo: WeightInfo;
289
290 type ChildBountyManager: ChildBountyManager<BalanceOf<Self, I>>;
292
293 type OnSlash: OnUnbalanced<pallet_treasury::NegativeImbalanceOf<Self, I>>;
295 }
296
297 #[pallet::error]
298 pub enum Error<T, I = ()> {
299 InsufficientProposersBalance,
301 InvalidIndex,
303 ReasonTooBig,
305 UnexpectedStatus,
307 RequireCurator,
309 InvalidValue,
311 InvalidFee,
313 PendingPayout,
316 Premature,
318 HasActiveChildBounty,
320 TooManyQueued,
322 NotProposer,
324 }
325
326 #[pallet::event]
327 #[pallet::generate_deposit(pub(super) fn deposit_event)]
328 pub enum Event<T: Config<I>, I: 'static = ()> {
329 BountyProposed { index: BountyIndex },
331 BountyRejected { index: BountyIndex, bond: BalanceOf<T, I> },
333 BountyBecameActive { index: BountyIndex },
335 BountyAwarded { index: BountyIndex, beneficiary: T::AccountId },
337 BountyClaimed { index: BountyIndex, payout: BalanceOf<T, I>, beneficiary: T::AccountId },
339 BountyCanceled { index: BountyIndex },
341 BountyExtended { index: BountyIndex },
343 BountyApproved { index: BountyIndex },
345 CuratorProposed { bounty_id: BountyIndex, curator: T::AccountId },
347 CuratorUnassigned { bounty_id: BountyIndex },
349 CuratorAccepted { bounty_id: BountyIndex, curator: T::AccountId },
351 DepositPoked {
353 bounty_id: BountyIndex,
354 proposer: T::AccountId,
355 old_deposit: BalanceOf<T, I>,
356 new_deposit: BalanceOf<T, I>,
357 },
358 }
359
360 #[pallet::storage]
362 pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, BountyIndex, ValueQuery>;
363
364 #[pallet::storage]
366 pub type Bounties<T: Config<I>, I: 'static = ()> = StorageMap<
367 _,
368 Twox64Concat,
369 BountyIndex,
370 Bounty<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T, I>>,
371 >;
372
373 #[pallet::storage]
375 pub type BountyDescriptions<T: Config<I>, I: 'static = ()> =
376 StorageMap<_, Twox64Concat, BountyIndex, BoundedVec<u8, T::MaximumReasonLength>>;
377
378 #[pallet::storage]
380 #[allow(deprecated)]
381 pub type BountyApprovals<T: Config<I>, I: 'static = ()> =
382 StorageValue<_, BoundedVec<BountyIndex, T::MaxApprovals>, ValueQuery>;
383
384 #[pallet::call]
385 impl<T: Config<I>, I: 'static> Pallet<T, I> {
386 #[pallet::call_index(0)]
399 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_bounty(description.len() as u32))]
400 pub fn propose_bounty(
401 origin: OriginFor<T>,
402 #[pallet::compact] value: BalanceOf<T, I>,
403 description: Vec<u8>,
404 ) -> DispatchResult {
405 let proposer = ensure_signed(origin)?;
406 Self::create_bounty(proposer, description, value)?;
407 Ok(())
408 }
409
410 #[pallet::call_index(1)]
418 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty())]
419 pub fn approve_bounty(
420 origin: OriginFor<T>,
421 #[pallet::compact] bounty_id: BountyIndex,
422 ) -> DispatchResult {
423 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
424 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
425 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
426 ensure!(
427 bounty.value <= max_amount,
428 pallet_treasury::Error::<T, I>::InsufficientPermission
429 );
430 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
431
432 bounty.status = BountyStatus::Approved;
433
434 BountyApprovals::<T, I>::try_append(bounty_id)
435 .map_err(|()| Error::<T, I>::TooManyQueued)?;
436
437 Ok(())
438 })?;
439
440 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
441 Ok(())
442 }
443
444 #[pallet::call_index(2)]
451 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_curator())]
452 pub fn propose_curator(
453 origin: OriginFor<T>,
454 #[pallet::compact] bounty_id: BountyIndex,
455 curator: AccountIdLookupOf<T>,
456 #[pallet::compact] fee: BalanceOf<T, I>,
457 ) -> DispatchResult {
458 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
459
460 let curator = T::Lookup::lookup(curator)?;
461 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
462 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
463 ensure!(
464 bounty.value <= max_amount,
465 pallet_treasury::Error::<T, I>::InsufficientPermission
466 );
467 match bounty.status {
468 BountyStatus::Funded => {},
469 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
470 };
471
472 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
473
474 bounty.status = BountyStatus::CuratorProposed { curator: curator.clone() };
475 bounty.fee = fee;
476
477 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
478
479 Ok(())
480 })?;
481 Ok(())
482 }
483
484 #[pallet::call_index(3)]
502 #[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
503 pub fn unassign_curator(
504 origin: OriginFor<T>,
505 #[pallet::compact] bounty_id: BountyIndex,
506 ) -> DispatchResult {
507 let maybe_sender = ensure_signed(origin.clone())
508 .map(Some)
509 .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
510
511 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
512 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
513
514 let slash_curator =
515 |curator: &T::AccountId, curator_deposit: &mut BalanceOf<T, I>| {
516 let imbalance = T::Currency::slash_reserved(curator, *curator_deposit).0;
517 T::OnSlash::on_unbalanced(imbalance);
518 *curator_deposit = Zero::zero();
519 };
520
521 match bounty.status {
522 BountyStatus::Proposed | BountyStatus::Approved | BountyStatus::Funded => {
523 return Err(Error::<T, I>::UnexpectedStatus.into())
525 },
526 BountyStatus::ApprovedWithCurator { ref curator } => {
527 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
530 bounty.status = BountyStatus::Approved;
533 return Ok(());
534 },
535 BountyStatus::CuratorProposed { ref curator } => {
536 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
539 },
540 BountyStatus::Active { ref curator, ref update_due } => {
541 match maybe_sender {
543 None => {
545 slash_curator(curator, &mut bounty.curator_deposit);
546 },
548 Some(sender) => {
549 if sender != *curator {
552 let block_number = Self::treasury_block_number();
553 if *update_due < block_number {
554 slash_curator(curator, &mut bounty.curator_deposit);
555 } else {
557 return Err(Error::<T, I>::Premature.into())
559 }
560 } else {
561 let err_amount =
564 T::Currency::unreserve(curator, bounty.curator_deposit);
565 debug_assert!(err_amount.is_zero());
566 bounty.curator_deposit = Zero::zero();
567 }
569 },
570 }
571 },
572 BountyStatus::PendingPayout { ref curator, .. } => {
573 ensure!(maybe_sender.is_none(), BadOrigin);
577 slash_curator(curator, &mut bounty.curator_deposit);
578 },
580 };
581
582 bounty.status = BountyStatus::Funded;
583 Ok(())
584 })?;
585
586 Self::deposit_event(Event::<T, I>::CuratorUnassigned { bounty_id });
587 Ok(())
588 }
589
590 #[pallet::call_index(4)]
598 #[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
599 pub fn accept_curator(
600 origin: OriginFor<T>,
601 #[pallet::compact] bounty_id: BountyIndex,
602 ) -> DispatchResult {
603 let signer = ensure_signed(origin)?;
604
605 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
606 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
607
608 match bounty.status {
609 BountyStatus::CuratorProposed { ref curator } => {
610 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
611
612 let deposit = Self::calculate_curator_deposit(&bounty.fee);
613 T::Currency::reserve(curator, deposit)?;
614 bounty.curator_deposit = deposit;
615
616 let update_due = Self::treasury_block_number()
617 .saturating_add(T::BountyUpdatePeriod::get());
618 bounty.status =
619 BountyStatus::Active { curator: curator.clone(), update_due };
620
621 Self::deposit_event(Event::<T, I>::CuratorAccepted {
622 bounty_id,
623 curator: signer,
624 });
625 Ok(())
626 },
627 _ => Err(Error::<T, I>::UnexpectedStatus.into()),
628 }
629 })?;
630 Ok(())
631 }
632
633 #[pallet::call_index(5)]
644 #[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
645 pub fn award_bounty(
646 origin: OriginFor<T>,
647 #[pallet::compact] bounty_id: BountyIndex,
648 beneficiary: AccountIdLookupOf<T>,
649 ) -> DispatchResult {
650 let signer = ensure_signed(origin)?;
651 let beneficiary = T::Lookup::lookup(beneficiary)?;
652
653 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
654 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
655
656 ensure!(
658 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
659 Error::<T, I>::HasActiveChildBounty
660 );
661
662 match &bounty.status {
663 BountyStatus::Active { curator, .. } => {
664 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
665 },
666 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
667 }
668 bounty.status = BountyStatus::PendingPayout {
669 curator: signer,
670 beneficiary: beneficiary.clone(),
671 unlock_at: Self::treasury_block_number() + T::BountyDepositPayoutDelay::get(),
672 };
673
674 Ok(())
675 })?;
676
677 Self::deposit_event(Event::<T, I>::BountyAwarded { index: bounty_id, beneficiary });
678 Ok(())
679 }
680
681 #[pallet::call_index(6)]
690 #[pallet::weight(<T as Config<I>>::WeightInfo::claim_bounty())]
691 pub fn claim_bounty(
692 origin: OriginFor<T>,
693 #[pallet::compact] bounty_id: BountyIndex,
694 ) -> DispatchResult {
695 ensure_signed(origin)?; Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
698 let bounty = maybe_bounty.take().ok_or(Error::<T, I>::InvalidIndex)?;
699 if let BountyStatus::PendingPayout { curator, beneficiary, unlock_at } =
700 bounty.status
701 {
702 ensure!(Self::treasury_block_number() >= unlock_at, Error::<T, I>::Premature);
703 let bounty_account = Self::bounty_account_id(bounty_id);
704 let balance = T::Currency::free_balance(&bounty_account);
705 let fee = bounty.fee.min(balance); let payout = balance.saturating_sub(fee);
707 let err_amount = T::Currency::unreserve(&curator, bounty.curator_deposit);
708 debug_assert!(err_amount.is_zero());
709
710 let children_fee = T::ChildBountyManager::children_curator_fees(bounty_id);
713 debug_assert!(children_fee <= fee);
714
715 let final_fee = fee.saturating_sub(children_fee);
716 let res =
717 T::Currency::transfer(&bounty_account, &curator, final_fee, AllowDeath); debug_assert!(res.is_ok());
719 let res =
720 T::Currency::transfer(&bounty_account, &beneficiary, payout, AllowDeath); debug_assert!(res.is_ok());
722
723 *maybe_bounty = None;
724
725 BountyDescriptions::<T, I>::remove(bounty_id);
726 T::ChildBountyManager::bounty_removed(bounty_id);
727
728 Self::deposit_event(Event::<T, I>::BountyClaimed {
729 index: bounty_id,
730 payout,
731 beneficiary,
732 });
733 Ok(())
734 } else {
735 Err(Error::<T, I>::UnexpectedStatus.into())
736 }
737 })?;
738 Ok(())
739 }
740
741 #[pallet::call_index(7)]
751 #[pallet::weight(<T as Config<I>>::WeightInfo::close_bounty_proposed()
752 .max(<T as Config<I>>::WeightInfo::close_bounty_active()))]
753 pub fn close_bounty(
754 origin: OriginFor<T>,
755 #[pallet::compact] bounty_id: BountyIndex,
756 ) -> DispatchResultWithPostInfo {
757 T::RejectOrigin::ensure_origin(origin)?;
758
759 Bounties::<T, I>::try_mutate_exists(
760 bounty_id,
761 |maybe_bounty| -> DispatchResultWithPostInfo {
762 let bounty = maybe_bounty.as_ref().ok_or(Error::<T, I>::InvalidIndex)?;
763
764 ensure!(
766 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
767 Error::<T, I>::HasActiveChildBounty
768 );
769
770 match &bounty.status {
771 BountyStatus::Proposed => {
772 BountyDescriptions::<T, I>::remove(bounty_id);
774 let value = bounty.bond;
775 let imbalance = T::Currency::slash_reserved(&bounty.proposer, value).0;
776 T::OnSlash::on_unbalanced(imbalance);
777 *maybe_bounty = None;
778
779 Self::deposit_event(Event::<T, I>::BountyRejected {
780 index: bounty_id,
781 bond: value,
782 });
783 return Ok(
785 Some(<T as Config<I>>::WeightInfo::close_bounty_proposed()).into()
786 )
787 },
788 BountyStatus::Approved | BountyStatus::ApprovedWithCurator { .. } => {
789 return Err(Error::<T, I>::UnexpectedStatus.into())
792 },
793 BountyStatus::Funded | BountyStatus::CuratorProposed { .. } => {
794 },
796 BountyStatus::Active { curator, .. } => {
797 let err_amount =
799 T::Currency::unreserve(curator, bounty.curator_deposit);
800 debug_assert!(err_amount.is_zero());
801 },
803 BountyStatus::PendingPayout { .. } => {
804 return Err(Error::<T, I>::PendingPayout.into())
809 },
810 }
811
812 let bounty_account = Self::bounty_account_id(bounty_id);
813
814 BountyDescriptions::<T, I>::remove(bounty_id);
815
816 let balance = T::Currency::free_balance(&bounty_account);
817 let res = T::Currency::transfer(
818 &bounty_account,
819 &Self::account_id(),
820 balance,
821 AllowDeath,
822 ); debug_assert!(res.is_ok());
824
825 *maybe_bounty = None;
826 T::ChildBountyManager::bounty_removed(bounty_id);
827
828 Self::deposit_event(Event::<T, I>::BountyCanceled { index: bounty_id });
829 Ok(Some(<T as Config<I>>::WeightInfo::close_bounty_active()).into())
830 },
831 )
832 }
833
834 #[pallet::call_index(8)]
844 #[pallet::weight(<T as Config<I>>::WeightInfo::extend_bounty_expiry())]
845 pub fn extend_bounty_expiry(
846 origin: OriginFor<T>,
847 #[pallet::compact] bounty_id: BountyIndex,
848 _remark: Vec<u8>,
849 ) -> DispatchResult {
850 let signer = ensure_signed(origin)?;
851
852 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
853 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
854
855 match bounty.status {
856 BountyStatus::Active { ref curator, ref mut update_due } => {
857 ensure!(*curator == signer, Error::<T, I>::RequireCurator);
858 *update_due = Self::treasury_block_number()
859 .saturating_add(T::BountyUpdatePeriod::get())
860 .max(*update_due);
861 },
862 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
863 }
864
865 Ok(())
866 })?;
867
868 Self::deposit_event(Event::<T, I>::BountyExtended { index: bounty_id });
869 Ok(())
870 }
871
872 #[pallet::call_index(9)]
884 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty_with_curator())]
885 pub fn approve_bounty_with_curator(
886 origin: OriginFor<T>,
887 #[pallet::compact] bounty_id: BountyIndex,
888 curator: AccountIdLookupOf<T>,
889 #[pallet::compact] fee: BalanceOf<T, I>,
890 ) -> DispatchResult {
891 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
892 let curator = T::Lookup::lookup(curator)?;
893 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
894 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
896 ensure!(
897 bounty.value <= max_amount,
898 pallet_treasury::Error::<T, I>::InsufficientPermission
899 );
900 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
901 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
902
903 BountyApprovals::<T, I>::try_append(bounty_id)
904 .map_err(|()| Error::<T, I>::TooManyQueued)?;
905
906 bounty.status = BountyStatus::ApprovedWithCurator { curator: curator.clone() };
907 bounty.fee = fee;
908
909 Ok(())
910 })?;
911
912 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
913 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
914
915 Ok(())
916 }
917
918 #[pallet::call_index(10)]
934 #[pallet::weight(<T as Config<I>>::WeightInfo::poke_deposit())]
935 pub fn poke_deposit(
936 origin: OriginFor<T>,
937 #[pallet::compact] bounty_id: BountyIndex,
938 ) -> DispatchResultWithPostInfo {
939 ensure_signed(origin)?;
940
941 let deposit_updated = Self::poke_bounty_deposit(bounty_id)?;
942
943 Ok(if deposit_updated { Pays::No } else { Pays::Yes }.into())
944 }
945 }
946
947 #[pallet::hooks]
948 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
949 #[cfg(feature = "try-runtime")]
950 fn try_state(_n: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
951 Self::do_try_state()
952 }
953 }
954}
955
956#[cfg(any(feature = "try-runtime", test))]
957impl<T: Config<I>, I: 'static> Pallet<T, I> {
958 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
962 Self::try_state_bounties_count()?;
963
964 Ok(())
965 }
966
967 fn try_state_bounties_count() -> Result<(), sp_runtime::TryRuntimeError> {
975 let bounties_length = Bounties::<T, I>::iter().count() as u32;
976
977 ensure!(
978 <BountyCount<T, I>>::get() >= bounties_length,
979 "`BountyCount` must be grater or equals the number of `Bounties` in storage"
980 );
981
982 let bounties_description_length = BountyDescriptions::<T, I>::iter().count() as u32;
983 ensure!(
984 <BountyCount<T, I>>::get() >= bounties_description_length,
985 "`BountyCount` must be grater or equals the number of `BountiesDescriptions` in storage."
986 );
987
988 ensure!(
989 bounties_length == bounties_description_length,
990 "Number of `Bounties` in storage must be the same as the Number of `BountiesDescription` in storage."
991 );
992 Ok(())
993 }
994}
995
996impl<T: Config<I>, I: 'static> Pallet<T, I> {
997 pub fn treasury_block_number() -> BlockNumberFor<T, I> {
1001 <T as pallet_treasury::Config<I>>::BlockNumberProvider::current_block_number()
1002 }
1003
1004 pub fn calculate_curator_deposit(fee: &BalanceOf<T, I>) -> BalanceOf<T, I> {
1006 let mut deposit = T::CuratorDepositMultiplier::get() * *fee;
1007
1008 if let Some(max_deposit) = T::CuratorDepositMax::get() {
1009 deposit = deposit.min(max_deposit)
1010 }
1011
1012 if let Some(min_deposit) = T::CuratorDepositMin::get() {
1013 deposit = deposit.max(min_deposit)
1014 }
1015
1016 deposit
1017 }
1018
1019 pub fn account_id() -> T::AccountId {
1024 T::PalletId::get().into_account_truncating()
1025 }
1026
1027 pub fn bounty_account_id(id: BountyIndex) -> T::AccountId {
1029 T::PalletId::get().into_sub_account_truncating(("bt", id))
1032 }
1033
1034 fn create_bounty(
1035 proposer: T::AccountId,
1036 description: Vec<u8>,
1037 value: BalanceOf<T, I>,
1038 ) -> DispatchResult {
1039 let bounded_description: BoundedVec<_, _> =
1040 description.try_into().map_err(|_| Error::<T, I>::ReasonTooBig)?;
1041 ensure!(value >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
1042
1043 let index = BountyCount::<T, I>::get();
1044
1045 let bond = Self::calculate_bounty_deposit(&bounded_description);
1047 T::Currency::reserve(&proposer, bond)
1048 .map_err(|_| Error::<T, I>::InsufficientProposersBalance)?;
1049
1050 BountyCount::<T, I>::put(index + 1);
1051
1052 let bounty = Bounty {
1053 proposer,
1054 value,
1055 fee: 0u32.into(),
1056 curator_deposit: 0u32.into(),
1057 bond,
1058 status: BountyStatus::Proposed,
1059 };
1060
1061 Bounties::<T, I>::insert(index, &bounty);
1062 BountyDescriptions::<T, I>::insert(index, bounded_description);
1063
1064 Self::deposit_event(Event::<T, I>::BountyProposed { index });
1065
1066 Ok(())
1067 }
1068
1069 fn calculate_bounty_deposit(
1071 description: &BoundedVec<u8, T::MaximumReasonLength>,
1072 ) -> BalanceOf<T, I> {
1073 T::BountyDepositBase::get().saturating_add(
1074 T::DataDepositPerByte::get().saturating_mul((description.len() as u32).into()),
1075 )
1076 }
1077
1078 fn poke_bounty_deposit(bounty_id: BountyIndex) -> Result<bool, DispatchError> {
1082 let mut bounty = Bounties::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1083 let bounty_description =
1084 BountyDescriptions::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1085 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
1087
1088 let new_bond = Self::calculate_bounty_deposit(&bounty_description);
1089 let old_bond = bounty.bond;
1090 if new_bond == old_bond {
1091 return Ok(false);
1092 }
1093 if new_bond > old_bond {
1094 let extra = new_bond.saturating_sub(old_bond);
1095 T::Currency::reserve(&bounty.proposer, extra)?;
1096 } else {
1097 let excess = old_bond.saturating_sub(new_bond);
1098 let remaining_unreserved = T::Currency::unreserve(&bounty.proposer, excess);
1099 if !remaining_unreserved.is_zero() {
1100 defensive!(
1101 "Failed to unreserve full amount. (Requested, Actual)",
1102 (excess, excess.saturating_sub(remaining_unreserved))
1103 );
1104 }
1105 }
1106 bounty.bond = new_bond;
1107 Bounties::<T, I>::insert(bounty_id, &bounty);
1108
1109 Self::deposit_event(Event::<T, I>::DepositPoked {
1110 bounty_id,
1111 proposer: bounty.proposer,
1112 old_deposit: old_bond,
1113 new_deposit: new_bond,
1114 });
1115
1116 Ok(true)
1117 }
1118}
1119
1120impl<T: Config<I>, I: 'static> pallet_treasury::SpendFunds<T, I> for Pallet<T, I> {
1121 fn spend_funds(
1122 budget_remaining: &mut BalanceOf<T, I>,
1123 imbalance: &mut PositiveImbalanceOf<T, I>,
1124 total_weight: &mut Weight,
1125 missed_any: &mut bool,
1126 ) {
1127 let bounties_len = BountyApprovals::<T, I>::mutate(|v| {
1128 let bounties_approval_len = v.len() as u32;
1129 v.retain(|&index| {
1130 Bounties::<T, I>::mutate(index, |bounty| {
1131 if let Some(bounty) = bounty {
1133 if bounty.value <= *budget_remaining {
1134 *budget_remaining -= bounty.value;
1135
1136 if let BountyStatus::ApprovedWithCurator { curator } = &bounty.status {
1138 bounty.status =
1139 BountyStatus::CuratorProposed { curator: curator.clone() };
1140 } else {
1141 bounty.status = BountyStatus::Funded;
1142 }
1143
1144 let err_amount = T::Currency::unreserve(&bounty.proposer, bounty.bond);
1146 debug_assert!(err_amount.is_zero());
1147
1148 imbalance.subsume(T::Currency::deposit_creating(
1150 &Self::bounty_account_id(index),
1151 bounty.value,
1152 ));
1153
1154 Self::deposit_event(Event::<T, I>::BountyBecameActive { index });
1155 false
1156 } else {
1157 *missed_any = true;
1158 true
1159 }
1160 } else {
1161 false
1162 }
1163 })
1164 });
1165 bounties_approval_len
1166 });
1167
1168 *total_weight += <T as pallet::Config<I>>::WeightInfo::spend_funds(bounties_len);
1169 }
1170}
1171
1172impl<Balance: Zero> ChildBountyManager<Balance> for () {
1174 fn child_bounties_count(_bounty_id: BountyIndex) -> BountyIndex {
1175 Default::default()
1176 }
1177
1178 fn children_curator_fees(_bounty_id: BountyIndex) -> Balance {
1179 Zero::zero()
1180 }
1181
1182 fn bounty_removed(_bounty_id: BountyIndex) {}
1183}