1#![recursion_limit = "512"]
86#![cfg_attr(not(feature = "std"), no_std)]
87
88#[cfg(feature = "runtime-benchmarks")]
89mod benchmarking;
90pub mod migrations;
91mod tests;
92pub mod weights;
93
94extern crate alloc;
95
96use alloc::vec::Vec;
97
98use frame_support::traits::{
99 fungibles::{Inspect as FungiblesInspect, Mutate as FungiblesMutate},
100 tokens::{Fortitude, Preservation},
101 Currency,
102 ExistenceRequirement::AllowDeath,
103 Get, Imbalance, OnUnbalanced, ReservableCurrency,
104};
105
106use sp_runtime::{
107 traits::{AccountIdConversion, BadOrigin, BlockNumberProvider, Saturating, StaticLookup, Zero},
108 Debug, DispatchResult, Permill,
109};
110
111use frame_support::{
112 dispatch::DispatchResultWithPostInfo, pallet_prelude::*, traits::EnsureOrigin,
113};
114use frame_system::pallet_prelude::{
115 ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
116};
117use scale_info::TypeInfo;
118pub use weights::WeightInfo;
119
120pub use pallet::*;
121
122type BalanceOf<T, I = ()> = pallet_treasury::BalanceOf<T, I>;
123
124type PositiveImbalanceOf<T, I = ()> = pallet_treasury::PositiveImbalanceOf<T, I>;
125
126pub type BountyIndex = u32;
128
129type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
130
131type BlockNumberFor<T, I = ()> =
132 <<T as pallet_treasury::Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
133
134#[derive(
136 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
137)]
138pub struct Bounty<AccountId, Balance, BlockNumber> {
139 pub proposer: AccountId,
141 pub value: Balance,
143 pub fee: Balance,
145 pub curator_deposit: Balance,
147 bond: Balance,
149 status: BountyStatus<AccountId, BlockNumber>,
151}
152
153impl<AccountId: PartialEq + Clone + Ord, Balance, BlockNumber: Clone>
154 Bounty<AccountId, Balance, BlockNumber>
155{
156 pub fn get_status(&self) -> BountyStatus<AccountId, BlockNumber> {
158 self.status.clone()
159 }
160}
161
162#[derive(
164 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
165)]
166pub enum BountyStatus<AccountId, BlockNumber> {
167 Proposed,
169 Approved,
171 Funded,
173 CuratorProposed {
175 curator: AccountId,
177 },
178 Active {
180 curator: AccountId,
182 update_due: BlockNumber,
184 },
185 PendingPayout {
187 curator: AccountId,
189 beneficiary: AccountId,
191 unlock_at: BlockNumber,
193 },
194 ApprovedWithCurator {
196 curator: AccountId,
198 },
199}
200
201pub trait ChildBountyManager<Balance> {
203 fn child_bounties_count(bounty_id: BountyIndex) -> BountyIndex;
205
206 fn children_curator_fees(bounty_id: BountyIndex) -> Balance;
208
209 fn bounty_removed(bounty_id: BountyIndex);
211}
212
213pub trait TransferAllAssets<AccountId> {
215 fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> DispatchResult;
219}
220
221impl<AccountId> TransferAllAssets<AccountId> for () {
222 fn force_transfer_all_assets(_: &AccountId, _: &AccountId) -> DispatchResult {
223 Ok(())
224 }
225}
226
227pub struct TransferAllFungibles<AccountId, Fungibles, RelevantAssets>(
232 core::marker::PhantomData<(AccountId, Fungibles, RelevantAssets)>,
233);
234impl<AccountId, Fungibles, RelevantAssets> TransferAllAssets<AccountId>
235 for TransferAllFungibles<AccountId, Fungibles, RelevantAssets>
236where
237 Fungibles: FungiblesMutate<AccountId>,
238 RelevantAssets: Get<Vec<<Fungibles as FungiblesInspect<AccountId>>::AssetId>>,
239 AccountId: Eq,
240{
241 fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> DispatchResult {
242 let assets_twice =
245 RelevantAssets::get().into_iter().chain(RelevantAssets::get().into_iter());
246
247 for id in assets_twice {
248 let balance = Fungibles::reducible_balance(
249 id.clone(),
250 from,
251 Preservation::Expendable,
252 Fortitude::Force,
253 );
254 if balance.is_zero() {
255 continue;
256 }
257
258 let _ = Fungibles::transfer(id, from, to, balance, Preservation::Expendable);
260 }
261 Ok(())
262 }
263}
264
265#[frame_support::pallet]
266pub mod pallet {
267 use super::*;
268
269 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
270
271 #[pallet::pallet]
272 #[pallet::storage_version(STORAGE_VERSION)]
273 pub struct Pallet<T, I = ()>(_);
274
275 #[pallet::config]
276 pub trait Config<I: 'static = ()>: frame_system::Config + pallet_treasury::Config<I> {
277 #[pallet::constant]
279 type BountyDepositBase: Get<BalanceOf<Self, I>>;
280
281 #[pallet::constant]
283 type BountyDepositPayoutDelay: Get<BlockNumberFor<Self, I>>;
284
285 #[pallet::constant]
292 type BountyUpdatePeriod: Get<BlockNumberFor<Self, I>>;
293
294 #[pallet::constant]
299 type CuratorDepositMultiplier: Get<Permill>;
300
301 #[pallet::constant]
303 type CuratorDepositMax: Get<Option<BalanceOf<Self, I>>>;
304
305 #[pallet::constant]
307 type CuratorDepositMin: Get<Option<BalanceOf<Self, I>>>;
308
309 #[pallet::constant]
311 type BountyValueMinimum: Get<BalanceOf<Self, I>>;
312
313 #[pallet::constant]
315 type DataDepositPerByte: Get<BalanceOf<Self, I>>;
316
317 #[allow(deprecated)]
319 type RuntimeEvent: From<Event<Self, I>>
320 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
321
322 #[pallet::constant]
326 type MaximumReasonLength: Get<u32>;
327
328 type WeightInfo: WeightInfo;
330
331 type ChildBountyManager: ChildBountyManager<BalanceOf<Self, I>>;
333
334 type OnSlash: OnUnbalanced<pallet_treasury::NegativeImbalanceOf<Self, I>>;
336
337 type TransferAllAssets: TransferAllAssets<Self::AccountId>;
342 }
343
344 #[pallet::error]
345 pub enum Error<T, I = ()> {
346 InsufficientProposersBalance,
348 InvalidIndex,
350 ReasonTooBig,
352 UnexpectedStatus,
354 RequireCurator,
356 InvalidValue,
358 InvalidFee,
360 PendingPayout,
363 Premature,
365 HasActiveChildBounty,
367 TooManyQueued,
369 NotProposer,
371 }
372
373 #[pallet::event]
374 #[pallet::generate_deposit(pub(super) fn deposit_event)]
375 pub enum Event<T: Config<I>, I: 'static = ()> {
376 BountyProposed { index: BountyIndex },
378 BountyRejected { index: BountyIndex, bond: BalanceOf<T, I> },
380 BountyBecameActive { index: BountyIndex },
382 BountyAwarded { index: BountyIndex, beneficiary: T::AccountId },
384 BountyClaimed { index: BountyIndex, payout: BalanceOf<T, I>, beneficiary: T::AccountId },
386 BountyCanceled { index: BountyIndex },
388 BountyExtended { index: BountyIndex },
390 BountyApproved { index: BountyIndex },
392 CuratorProposed { bounty_id: BountyIndex, curator: T::AccountId },
394 CuratorUnassigned { bounty_id: BountyIndex },
396 CuratorAccepted { bounty_id: BountyIndex, curator: T::AccountId },
398 DepositPoked {
400 bounty_id: BountyIndex,
401 proposer: T::AccountId,
402 old_deposit: BalanceOf<T, I>,
403 new_deposit: BalanceOf<T, I>,
404 },
405 }
406
407 #[pallet::storage]
409 pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, BountyIndex, ValueQuery>;
410
411 #[pallet::storage]
413 pub type Bounties<T: Config<I>, I: 'static = ()> = StorageMap<
414 _,
415 Twox64Concat,
416 BountyIndex,
417 Bounty<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T, I>>,
418 >;
419
420 #[pallet::storage]
422 pub type BountyDescriptions<T: Config<I>, I: 'static = ()> =
423 StorageMap<_, Twox64Concat, BountyIndex, BoundedVec<u8, T::MaximumReasonLength>>;
424
425 #[pallet::storage]
427 #[allow(deprecated)]
428 pub type BountyApprovals<T: Config<I>, I: 'static = ()> =
429 StorageValue<_, BoundedVec<BountyIndex, T::MaxApprovals>, ValueQuery>;
430
431 #[pallet::call]
432 impl<T: Config<I>, I: 'static> Pallet<T, I> {
433 #[pallet::call_index(0)]
446 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_bounty(description.len() as u32))]
447 pub fn propose_bounty(
448 origin: OriginFor<T>,
449 #[pallet::compact] value: BalanceOf<T, I>,
450 description: Vec<u8>,
451 ) -> DispatchResult {
452 let proposer = ensure_signed(origin)?;
453 Self::create_bounty(proposer, description, value)?;
454 Ok(())
455 }
456
457 #[pallet::call_index(1)]
465 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty())]
466 pub fn approve_bounty(
467 origin: OriginFor<T>,
468 #[pallet::compact] bounty_id: BountyIndex,
469 ) -> DispatchResult {
470 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
471 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
472 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
473 ensure!(
474 bounty.value <= max_amount,
475 pallet_treasury::Error::<T, I>::InsufficientPermission
476 );
477 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
478
479 bounty.status = BountyStatus::Approved;
480
481 BountyApprovals::<T, I>::try_append(bounty_id)
482 .map_err(|()| Error::<T, I>::TooManyQueued)?;
483
484 Ok(())
485 })?;
486
487 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
488 Ok(())
489 }
490
491 #[pallet::call_index(2)]
498 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_curator())]
499 pub fn propose_curator(
500 origin: OriginFor<T>,
501 #[pallet::compact] bounty_id: BountyIndex,
502 curator: AccountIdLookupOf<T>,
503 #[pallet::compact] fee: BalanceOf<T, I>,
504 ) -> DispatchResult {
505 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
506
507 let curator = T::Lookup::lookup(curator)?;
508 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
509 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
510 ensure!(
511 bounty.value <= max_amount,
512 pallet_treasury::Error::<T, I>::InsufficientPermission
513 );
514 match bounty.status {
515 BountyStatus::Funded => {},
516 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
517 };
518
519 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
520
521 bounty.status = BountyStatus::CuratorProposed { curator: curator.clone() };
522 bounty.fee = fee;
523
524 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
525
526 Ok(())
527 })?;
528 Ok(())
529 }
530
531 #[pallet::call_index(3)]
549 #[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
550 pub fn unassign_curator(
551 origin: OriginFor<T>,
552 #[pallet::compact] bounty_id: BountyIndex,
553 ) -> DispatchResult {
554 let maybe_sender = ensure_signed(origin.clone())
555 .map(Some)
556 .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
557
558 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
559 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
560
561 let slash_curator =
562 |curator: &T::AccountId, curator_deposit: &mut BalanceOf<T, I>| {
563 let imbalance = T::Currency::slash_reserved(curator, *curator_deposit).0;
564 T::OnSlash::on_unbalanced(imbalance);
565 *curator_deposit = Zero::zero();
566 };
567
568 match bounty.status {
569 BountyStatus::Proposed | BountyStatus::Approved | BountyStatus::Funded => {
570 return Err(Error::<T, I>::UnexpectedStatus.into());
572 },
573 BountyStatus::ApprovedWithCurator { ref curator } => {
574 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
577 bounty.status = BountyStatus::Approved;
580 return Ok(());
581 },
582 BountyStatus::CuratorProposed { ref curator } => {
583 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
586 },
587 BountyStatus::Active { ref curator, ref update_due } => {
588 match maybe_sender {
590 None => {
592 slash_curator(curator, &mut bounty.curator_deposit);
593 },
595 Some(sender) => {
596 if sender != *curator {
599 let block_number = Self::treasury_block_number();
600 if *update_due < block_number {
601 slash_curator(curator, &mut bounty.curator_deposit);
602 } else {
604 return Err(Error::<T, I>::Premature.into());
606 }
607 } else {
608 let err_amount =
611 T::Currency::unreserve(curator, bounty.curator_deposit);
612 debug_assert!(err_amount.is_zero());
613 bounty.curator_deposit = Zero::zero();
614 }
616 },
617 }
618 },
619 BountyStatus::PendingPayout { ref curator, .. } => {
620 ensure!(maybe_sender.is_none(), BadOrigin);
624 slash_curator(curator, &mut bounty.curator_deposit);
625 },
627 };
628
629 bounty.status = BountyStatus::Funded;
630 Ok(())
631 })?;
632
633 Self::deposit_event(Event::<T, I>::CuratorUnassigned { bounty_id });
634 Ok(())
635 }
636
637 #[pallet::call_index(4)]
645 #[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
646 pub fn accept_curator(
647 origin: OriginFor<T>,
648 #[pallet::compact] bounty_id: BountyIndex,
649 ) -> DispatchResult {
650 let signer = ensure_signed(origin)?;
651
652 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
653 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
654
655 match bounty.status {
656 BountyStatus::CuratorProposed { ref curator } => {
657 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
658
659 let deposit = Self::calculate_curator_deposit(&bounty.fee);
660 T::Currency::reserve(curator, deposit)?;
661 bounty.curator_deposit = deposit;
662
663 let update_due = Self::treasury_block_number()
664 .saturating_add(T::BountyUpdatePeriod::get());
665 bounty.status =
666 BountyStatus::Active { curator: curator.clone(), update_due };
667
668 Self::deposit_event(Event::<T, I>::CuratorAccepted {
669 bounty_id,
670 curator: signer,
671 });
672 Ok(())
673 },
674 _ => Err(Error::<T, I>::UnexpectedStatus.into()),
675 }
676 })?;
677 Ok(())
678 }
679
680 #[pallet::call_index(5)]
691 #[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
692 pub fn award_bounty(
693 origin: OriginFor<T>,
694 #[pallet::compact] bounty_id: BountyIndex,
695 beneficiary: AccountIdLookupOf<T>,
696 ) -> DispatchResult {
697 let signer = ensure_signed(origin)?;
698 let beneficiary = T::Lookup::lookup(beneficiary)?;
699
700 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
701 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
702
703 ensure!(
705 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
706 Error::<T, I>::HasActiveChildBounty
707 );
708
709 match &bounty.status {
710 BountyStatus::Active { curator, .. } => {
711 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
712 },
713 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
714 }
715 bounty.status = BountyStatus::PendingPayout {
716 curator: signer,
717 beneficiary: beneficiary.clone(),
718 unlock_at: Self::treasury_block_number() + T::BountyDepositPayoutDelay::get(),
719 };
720
721 Ok(())
722 })?;
723
724 Self::deposit_event(Event::<T, I>::BountyAwarded { index: bounty_id, beneficiary });
725 Ok(())
726 }
727
728 #[pallet::call_index(6)]
737 #[pallet::weight(<T as Config<I>>::WeightInfo::claim_bounty())]
738 pub fn claim_bounty(
739 origin: OriginFor<T>,
740 #[pallet::compact] bounty_id: BountyIndex,
741 ) -> DispatchResult {
742 ensure_signed(origin)?; Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
745 let bounty = maybe_bounty.take().ok_or(Error::<T, I>::InvalidIndex)?;
746 if let BountyStatus::PendingPayout { curator, beneficiary, unlock_at } =
747 bounty.status
748 {
749 ensure!(Self::treasury_block_number() >= unlock_at, Error::<T, I>::Premature);
750 let bounty_account = Self::bounty_account_id(bounty_id);
751 let balance = T::Currency::free_balance(&bounty_account);
752 let fee = bounty.fee.min(balance); let payout = balance.saturating_sub(fee);
754 let err_amount = T::Currency::unreserve(&curator, bounty.curator_deposit);
755 debug_assert!(err_amount.is_zero());
756
757 let children_fee = T::ChildBountyManager::children_curator_fees(bounty_id);
760 debug_assert!(children_fee <= fee);
761
762 let final_fee = fee.saturating_sub(children_fee);
763 let res =
764 T::Currency::transfer(&bounty_account, &curator, final_fee, AllowDeath); debug_assert!(res.is_ok());
766 let res =
767 T::Currency::transfer(&bounty_account, &beneficiary, payout, AllowDeath); debug_assert!(res.is_ok());
769
770 *maybe_bounty = None;
771
772 BountyDescriptions::<T, I>::remove(bounty_id);
773 T::ChildBountyManager::bounty_removed(bounty_id);
774
775 Self::deposit_event(Event::<T, I>::BountyClaimed {
776 index: bounty_id,
777 payout,
778 beneficiary,
779 });
780 Ok(())
781 } else {
782 Err(Error::<T, I>::UnexpectedStatus.into())
783 }
784 })?;
785 Ok(())
786 }
787
788 #[pallet::call_index(7)]
798 #[pallet::weight(<T as Config<I>>::WeightInfo::close_bounty_proposed()
799 .max(<T as Config<I>>::WeightInfo::close_bounty_active()))]
800 pub fn close_bounty(
801 origin: OriginFor<T>,
802 #[pallet::compact] bounty_id: BountyIndex,
803 ) -> DispatchResultWithPostInfo {
804 T::RejectOrigin::ensure_origin(origin)?;
805
806 Bounties::<T, I>::try_mutate_exists(
807 bounty_id,
808 |maybe_bounty| -> DispatchResultWithPostInfo {
809 let bounty = maybe_bounty.as_ref().ok_or(Error::<T, I>::InvalidIndex)?;
810
811 ensure!(
813 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
814 Error::<T, I>::HasActiveChildBounty
815 );
816
817 match &bounty.status {
818 BountyStatus::Proposed => {
819 BountyDescriptions::<T, I>::remove(bounty_id);
821 let value = bounty.bond;
822 let imbalance = T::Currency::slash_reserved(&bounty.proposer, value).0;
823 T::OnSlash::on_unbalanced(imbalance);
824 *maybe_bounty = None;
825
826 Self::deposit_event(Event::<T, I>::BountyRejected {
827 index: bounty_id,
828 bond: value,
829 });
830 return Ok(
832 Some(<T as Config<I>>::WeightInfo::close_bounty_proposed()).into()
833 );
834 },
835 BountyStatus::Approved | BountyStatus::ApprovedWithCurator { .. } => {
836 return Err(Error::<T, I>::UnexpectedStatus.into());
839 },
840 BountyStatus::Funded | BountyStatus::CuratorProposed { .. } => {
841 },
843 BountyStatus::Active { curator, .. } => {
844 let err_amount =
846 T::Currency::unreserve(curator, bounty.curator_deposit);
847 debug_assert!(err_amount.is_zero());
848 },
850 BountyStatus::PendingPayout { .. } => {
851 return Err(Error::<T, I>::PendingPayout.into());
856 },
857 }
858
859 let bounty_account = Self::bounty_account_id(bounty_id);
860
861 BountyDescriptions::<T, I>::remove(bounty_id);
862
863 T::TransferAllAssets::force_transfer_all_assets(
864 &bounty_account,
865 &Self::account_id(),
866 )?;
867
868 *maybe_bounty = None;
869 T::ChildBountyManager::bounty_removed(bounty_id);
870
871 Self::deposit_event(Event::<T, I>::BountyCanceled { index: bounty_id });
872 Ok(Some(<T as Config<I>>::WeightInfo::close_bounty_active()).into())
873 },
874 )
875 }
876
877 #[pallet::call_index(8)]
887 #[pallet::weight(<T as Config<I>>::WeightInfo::extend_bounty_expiry())]
888 pub fn extend_bounty_expiry(
889 origin: OriginFor<T>,
890 #[pallet::compact] bounty_id: BountyIndex,
891 _remark: Vec<u8>,
892 ) -> DispatchResult {
893 let signer = ensure_signed(origin)?;
894
895 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
896 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
897
898 match bounty.status {
899 BountyStatus::Active { ref curator, ref mut update_due } => {
900 ensure!(*curator == signer, Error::<T, I>::RequireCurator);
901 *update_due = Self::treasury_block_number()
902 .saturating_add(T::BountyUpdatePeriod::get())
903 .max(*update_due);
904 },
905 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
906 }
907
908 Ok(())
909 })?;
910
911 Self::deposit_event(Event::<T, I>::BountyExtended { index: bounty_id });
912 Ok(())
913 }
914
915 #[pallet::call_index(9)]
927 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty_with_curator())]
928 pub fn approve_bounty_with_curator(
929 origin: OriginFor<T>,
930 #[pallet::compact] bounty_id: BountyIndex,
931 curator: AccountIdLookupOf<T>,
932 #[pallet::compact] fee: BalanceOf<T, I>,
933 ) -> DispatchResult {
934 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
935 let curator = T::Lookup::lookup(curator)?;
936 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
937 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
939 ensure!(
940 bounty.value <= max_amount,
941 pallet_treasury::Error::<T, I>::InsufficientPermission
942 );
943 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
944 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
945
946 BountyApprovals::<T, I>::try_append(bounty_id)
947 .map_err(|()| Error::<T, I>::TooManyQueued)?;
948
949 bounty.status = BountyStatus::ApprovedWithCurator { curator: curator.clone() };
950 bounty.fee = fee;
951
952 Ok(())
953 })?;
954
955 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
956 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
957
958 Ok(())
959 }
960
961 #[pallet::call_index(10)]
977 #[pallet::weight(<T as Config<I>>::WeightInfo::poke_deposit())]
978 pub fn poke_deposit(
979 origin: OriginFor<T>,
980 #[pallet::compact] bounty_id: BountyIndex,
981 ) -> DispatchResultWithPostInfo {
982 ensure_signed(origin)?;
983
984 let deposit_updated = Self::poke_bounty_deposit(bounty_id)?;
985
986 Ok(if deposit_updated { Pays::No } else { Pays::Yes }.into())
987 }
988 }
989
990 #[pallet::hooks]
991 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
992 #[cfg(feature = "try-runtime")]
993 fn try_state(_n: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
994 Self::do_try_state()
995 }
996 }
997}
998
999#[cfg(any(feature = "try-runtime", test))]
1000impl<T: Config<I>, I: 'static> Pallet<T, I> {
1001 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1005 Self::try_state_bounties_count()?;
1006
1007 Ok(())
1008 }
1009
1010 fn try_state_bounties_count() -> Result<(), sp_runtime::TryRuntimeError> {
1018 let bounties_length = Bounties::<T, I>::iter().count() as u32;
1019
1020 ensure!(
1021 <BountyCount<T, I>>::get() >= bounties_length,
1022 "`BountyCount` must be grater or equals the number of `Bounties` in storage"
1023 );
1024
1025 let bounties_description_length = BountyDescriptions::<T, I>::iter().count() as u32;
1026 ensure!(
1027 <BountyCount<T, I>>::get() >= bounties_description_length,
1028 "`BountyCount` must be grater or equals the number of `BountiesDescriptions` in storage."
1029 );
1030
1031 ensure!(
1032 bounties_length == bounties_description_length,
1033 "Number of `Bounties` in storage must be the same as the Number of `BountiesDescription` in storage."
1034 );
1035 Ok(())
1036 }
1037}
1038
1039impl<T: Config<I>, I: 'static> Pallet<T, I> {
1040 pub fn treasury_block_number() -> BlockNumberFor<T, I> {
1044 <T as pallet_treasury::Config<I>>::BlockNumberProvider::current_block_number()
1045 }
1046
1047 pub fn calculate_curator_deposit(fee: &BalanceOf<T, I>) -> BalanceOf<T, I> {
1049 let mut deposit = T::CuratorDepositMultiplier::get() * *fee;
1050
1051 if let Some(max_deposit) = T::CuratorDepositMax::get() {
1052 deposit = deposit.min(max_deposit)
1053 }
1054
1055 if let Some(min_deposit) = T::CuratorDepositMin::get() {
1056 deposit = deposit.max(min_deposit)
1057 }
1058
1059 deposit
1060 }
1061
1062 pub fn account_id() -> T::AccountId {
1067 T::PalletId::get().into_account_truncating()
1068 }
1069
1070 pub fn bounty_account_id(id: BountyIndex) -> T::AccountId {
1072 T::PalletId::get().into_sub_account_truncating(("bt", id))
1075 }
1076
1077 fn create_bounty(
1078 proposer: T::AccountId,
1079 description: Vec<u8>,
1080 value: BalanceOf<T, I>,
1081 ) -> DispatchResult {
1082 let bounded_description: BoundedVec<_, _> =
1083 description.try_into().map_err(|_| Error::<T, I>::ReasonTooBig)?;
1084 ensure!(value >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
1085
1086 let index = BountyCount::<T, I>::get();
1087
1088 let bond = Self::calculate_bounty_deposit(&bounded_description);
1090 T::Currency::reserve(&proposer, bond)
1091 .map_err(|_| Error::<T, I>::InsufficientProposersBalance)?;
1092
1093 BountyCount::<T, I>::put(index + 1);
1094
1095 let bounty = Bounty {
1096 proposer,
1097 value,
1098 fee: 0u32.into(),
1099 curator_deposit: 0u32.into(),
1100 bond,
1101 status: BountyStatus::Proposed,
1102 };
1103
1104 Bounties::<T, I>::insert(index, &bounty);
1105 BountyDescriptions::<T, I>::insert(index, bounded_description);
1106
1107 Self::deposit_event(Event::<T, I>::BountyProposed { index });
1108
1109 Ok(())
1110 }
1111
1112 fn calculate_bounty_deposit(
1114 description: &BoundedVec<u8, T::MaximumReasonLength>,
1115 ) -> BalanceOf<T, I> {
1116 T::BountyDepositBase::get().saturating_add(
1117 T::DataDepositPerByte::get().saturating_mul((description.len() as u32).into()),
1118 )
1119 }
1120
1121 fn poke_bounty_deposit(bounty_id: BountyIndex) -> Result<bool, DispatchError> {
1125 let mut bounty = Bounties::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1126 let bounty_description =
1127 BountyDescriptions::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1128 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
1130
1131 let new_bond = Self::calculate_bounty_deposit(&bounty_description);
1132 let old_bond = bounty.bond;
1133 if new_bond == old_bond {
1134 return Ok(false);
1135 }
1136 if new_bond > old_bond {
1137 let extra = new_bond.saturating_sub(old_bond);
1138 T::Currency::reserve(&bounty.proposer, extra)?;
1139 } else {
1140 let excess = old_bond.saturating_sub(new_bond);
1141 let remaining_unreserved = T::Currency::unreserve(&bounty.proposer, excess);
1142 if !remaining_unreserved.is_zero() {
1143 defensive!(
1144 "Failed to unreserve full amount. (Requested, Actual)",
1145 (excess, excess.saturating_sub(remaining_unreserved))
1146 );
1147 }
1148 }
1149 bounty.bond = new_bond;
1150 Bounties::<T, I>::insert(bounty_id, &bounty);
1151
1152 Self::deposit_event(Event::<T, I>::DepositPoked {
1153 bounty_id,
1154 proposer: bounty.proposer,
1155 old_deposit: old_bond,
1156 new_deposit: new_bond,
1157 });
1158
1159 Ok(true)
1160 }
1161}
1162
1163impl<T: Config<I>, I: 'static> pallet_treasury::SpendFunds<T, I> for Pallet<T, I> {
1164 fn spend_funds(
1165 budget_remaining: &mut BalanceOf<T, I>,
1166 imbalance: &mut PositiveImbalanceOf<T, I>,
1167 total_weight: &mut Weight,
1168 missed_any: &mut bool,
1169 ) {
1170 let bounties_len = BountyApprovals::<T, I>::mutate(|v| {
1171 let bounties_approval_len = v.len() as u32;
1172 v.retain(|&index| {
1173 Bounties::<T, I>::mutate(index, |bounty| {
1174 if let Some(bounty) = bounty {
1176 if bounty.value <= *budget_remaining {
1177 *budget_remaining -= bounty.value;
1178
1179 if let BountyStatus::ApprovedWithCurator { curator } = &bounty.status {
1181 bounty.status =
1182 BountyStatus::CuratorProposed { curator: curator.clone() };
1183 } else {
1184 bounty.status = BountyStatus::Funded;
1185 }
1186
1187 let err_amount = T::Currency::unreserve(&bounty.proposer, bounty.bond);
1189 debug_assert!(err_amount.is_zero());
1190
1191 imbalance.subsume(T::Currency::deposit_creating(
1193 &Self::bounty_account_id(index),
1194 bounty.value,
1195 ));
1196
1197 Self::deposit_event(Event::<T, I>::BountyBecameActive { index });
1198 false
1199 } else {
1200 *missed_any = true;
1201 true
1202 }
1203 } else {
1204 false
1205 }
1206 })
1207 });
1208 bounties_approval_len
1209 });
1210
1211 *total_weight += <T as pallet::Config<I>>::WeightInfo::spend_funds(bounties_len);
1212 }
1213}
1214
1215impl<Balance: Zero> ChildBountyManager<Balance> for () {
1217 fn child_bounties_count(_bounty_id: BountyIndex) -> BountyIndex {
1218 Default::default()
1219 }
1220
1221 fn children_curator_fees(_bounty_id: BountyIndex) -> Balance {
1222 Zero::zero()
1223 }
1224
1225 fn bounty_removed(_bounty_id: BountyIndex) {}
1226}