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 fungible::Mutate as FungibleMutate,
100 fungibles::{
101 Create as FungiblesCreate, Inspect as FungiblesInspect, Mutate as FungiblesMutate,
102 },
103 tokens::{Fortitude, Preservation},
104 Currency,
105 ExistenceRequirement::AllowDeath,
106 Get, Imbalance, OnUnbalanced, ReservableCurrency,
107};
108
109use sp_runtime::{
110 traits::{AccountIdConversion, BadOrigin, BlockNumberProvider, Saturating, StaticLookup, Zero},
111 Debug, DispatchResult, Permill,
112};
113
114use frame_support::{
115 dispatch::DispatchResultWithPostInfo, pallet_prelude::*, traits::EnsureOrigin,
116};
117use frame_system::pallet_prelude::{
118 ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
119};
120use scale_info::TypeInfo;
121pub use weights::WeightInfo;
122
123pub use pallet::*;
124
125type BalanceOf<T, I = ()> = pallet_treasury::BalanceOf<T, I>;
126
127type PositiveImbalanceOf<T, I = ()> = pallet_treasury::PositiveImbalanceOf<T, I>;
128
129pub type BountyIndex = u32;
131
132type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
133
134type BlockNumberFor<T, I = ()> =
135 <<T as pallet_treasury::Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
136
137#[derive(
139 Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, 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, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
168)]
169pub enum BountyStatus<AccountId, BlockNumber> {
170 Proposed,
172 Approved,
174 Funded,
176 CuratorProposed {
178 curator: AccountId,
180 },
181 Active {
183 curator: AccountId,
185 update_due: BlockNumber,
187 },
188 PendingPayout {
190 curator: AccountId,
192 beneficiary: AccountId,
194 unlock_at: BlockNumber,
196 },
197 ApprovedWithCurator {
199 curator: AccountId,
201 },
202}
203
204pub trait ChildBountyManager<Balance> {
206 fn child_bounties_count(bounty_id: BountyIndex) -> BountyIndex;
208
209 fn children_curator_fees(bounty_id: BountyIndex) -> Balance;
211
212 fn bounty_removed(bounty_id: BountyIndex);
214}
215
216pub trait TransferAllAssets<AccountId> {
218 fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> Result<bool, DispatchError>;
223
224 #[cfg(feature = "runtime-benchmarks")]
229 fn ensure_successful(_from: &AccountId) {}
230}
231
232impl<AccountId> TransferAllAssets<AccountId> for () {
233 fn force_transfer_all_assets(_: &AccountId, _: &AccountId) -> Result<bool, DispatchError> {
234 Ok(false)
235 }
236}
237
238pub struct TransferFungible<AccountId, Currency>(core::marker::PhantomData<(AccountId, Currency)>);
245impl<AccountId, C> TransferAllAssets<AccountId> for TransferFungible<AccountId, C>
246where
247 C: FungibleMutate<AccountId>,
248 AccountId: Eq,
249{
250 fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> Result<bool, DispatchError> {
251 let balance = C::reducible_balance(from, Preservation::Expendable, Fortitude::Polite);
252 if balance.is_zero() {
253 return Ok(false);
254 }
255 C::transfer(from, to, balance, Preservation::Expendable)?;
256 Ok(true)
257 }
258
259 #[cfg(feature = "runtime-benchmarks")]
260 fn ensure_successful(from: &AccountId) {
261 let _ = C::mint_into(from, 1_000_000u32.into());
262 }
263}
264
265pub struct TransferAllFungibles<AccountId, Fungibles, RelevantAssets>(
270 core::marker::PhantomData<(AccountId, Fungibles, RelevantAssets)>,
271);
272impl<AccountId, Fungibles, RelevantAssets> TransferAllAssets<AccountId>
273 for TransferAllFungibles<AccountId, Fungibles, RelevantAssets>
274where
275 Fungibles: FungiblesMutate<AccountId> + FungiblesCreate<AccountId>,
276 RelevantAssets: Get<Vec<<Fungibles as FungiblesInspect<AccountId>>::AssetId>>,
277 AccountId: Eq + Clone,
278{
279 fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> Result<bool, DispatchError> {
280 let assets_twice =
283 RelevantAssets::get().into_iter().chain(RelevantAssets::get().into_iter());
284
285 let mut transferred_any = false;
286 for id in assets_twice {
287 let balance = Fungibles::reducible_balance(
288 id.clone(),
289 from,
290 Preservation::Expendable,
291 Fortitude::Polite,
292 );
293 if balance.is_zero() {
294 continue;
295 }
296
297 if Fungibles::transfer(id, from, to, balance, Preservation::Expendable).is_ok() {
299 transferred_any = true;
300 }
301 }
302 Ok(transferred_any)
303 }
304
305 #[cfg(feature = "runtime-benchmarks")]
306 fn ensure_successful(from: &AccountId) {
307 for id in RelevantAssets::get() {
308 if !Fungibles::asset_exists(id.clone()) {
309 Fungibles::create(id.clone(), from.clone(), true, 1u32.into())
312 .expect("asset creation should succeed in benchmarks");
313 }
314 let _ = Fungibles::mint_into(id, from, 1_000u32.into());
315 }
316 }
317}
318
319#[frame_support::pallet]
320pub mod pallet {
321 use super::*;
322
323 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
324
325 #[pallet::pallet]
326 #[pallet::storage_version(STORAGE_VERSION)]
327 pub struct Pallet<T, I = ()>(_);
328
329 #[pallet::config]
330 pub trait Config<I: 'static = ()>: frame_system::Config + pallet_treasury::Config<I> {
331 #[pallet::constant]
333 type BountyDepositBase: Get<BalanceOf<Self, I>>;
334
335 #[pallet::constant]
337 type BountyDepositPayoutDelay: Get<BlockNumberFor<Self, I>>;
338
339 #[pallet::constant]
346 type BountyUpdatePeriod: Get<BlockNumberFor<Self, I>>;
347
348 #[pallet::constant]
353 type CuratorDepositMultiplier: Get<Permill>;
354
355 #[pallet::constant]
357 type CuratorDepositMax: Get<Option<BalanceOf<Self, I>>>;
358
359 #[pallet::constant]
361 type CuratorDepositMin: Get<Option<BalanceOf<Self, I>>>;
362
363 #[pallet::constant]
365 type BountyValueMinimum: Get<BalanceOf<Self, I>>;
366
367 #[pallet::constant]
369 type DataDepositPerByte: Get<BalanceOf<Self, I>>;
370
371 #[allow(deprecated)]
373 type RuntimeEvent: From<Event<Self, I>>
374 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
375
376 #[pallet::constant]
380 type MaximumReasonLength: Get<u32>;
381
382 type WeightInfo: WeightInfo;
384
385 type ChildBountyManager: ChildBountyManager<BalanceOf<Self, I>>;
387
388 type OnSlash: OnUnbalanced<pallet_treasury::NegativeImbalanceOf<Self, I>>;
390
391 type TransferAllAssets: TransferAllAssets<Self::AccountId>;
396 }
397
398 #[pallet::error]
399 pub enum Error<T, I = ()> {
400 InsufficientProposersBalance,
402 InvalidIndex,
404 ReasonTooBig,
406 UnexpectedStatus,
408 RequireCurator,
410 InvalidValue,
412 InvalidFee,
414 PendingPayout,
417 Premature,
419 HasActiveChildBounty,
421 TooManyQueued,
423 NotProposer,
425 BountyStillActive,
427 }
428
429 #[pallet::event]
430 #[pallet::generate_deposit(pub(super) fn deposit_event)]
431 pub enum Event<T: Config<I>, I: 'static = ()> {
432 BountyProposed { index: BountyIndex },
434 BountyRejected { index: BountyIndex, bond: BalanceOf<T, I> },
436 BountyBecameActive { index: BountyIndex },
438 BountyAwarded { index: BountyIndex, beneficiary: T::AccountId },
440 BountyClaimed { index: BountyIndex, payout: BalanceOf<T, I>, beneficiary: T::AccountId },
442 BountyCanceled { index: BountyIndex },
444 BountyExtended { index: BountyIndex },
446 BountyApproved { index: BountyIndex },
448 CuratorProposed { bounty_id: BountyIndex, curator: T::AccountId },
450 CuratorUnassigned { bounty_id: BountyIndex },
452 CuratorAccepted { bounty_id: BountyIndex, curator: T::AccountId },
454 DepositPoked {
456 bounty_id: BountyIndex,
457 proposer: T::AccountId,
458 old_deposit: BalanceOf<T, I>,
459 new_deposit: BalanceOf<T, I>,
460 },
461 BountyFundsReclaimed { bounty_id: BountyIndex },
463 }
464
465 #[pallet::storage]
467 pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, BountyIndex, ValueQuery>;
468
469 #[pallet::storage]
471 pub type Bounties<T: Config<I>, I: 'static = ()> = StorageMap<
472 _,
473 Twox64Concat,
474 BountyIndex,
475 Bounty<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T, I>>,
476 >;
477
478 #[pallet::storage]
480 pub type BountyDescriptions<T: Config<I>, I: 'static = ()> =
481 StorageMap<_, Twox64Concat, BountyIndex, BoundedVec<u8, T::MaximumReasonLength>>;
482
483 #[pallet::storage]
485 #[allow(deprecated)]
486 pub type BountyApprovals<T: Config<I>, I: 'static = ()> =
487 StorageValue<_, BoundedVec<BountyIndex, T::MaxApprovals>, ValueQuery>;
488
489 #[pallet::call]
490 impl<T: Config<I>, I: 'static> Pallet<T, I> {
491 #[pallet::call_index(0)]
504 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_bounty(description.len() as u32))]
505 pub fn propose_bounty(
506 origin: OriginFor<T>,
507 #[pallet::compact] value: BalanceOf<T, I>,
508 description: Vec<u8>,
509 ) -> DispatchResult {
510 let proposer = ensure_signed(origin)?;
511 Self::create_bounty(proposer, description, value)?;
512 Ok(())
513 }
514
515 #[pallet::call_index(1)]
523 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty())]
524 pub fn approve_bounty(
525 origin: OriginFor<T>,
526 #[pallet::compact] bounty_id: BountyIndex,
527 ) -> DispatchResult {
528 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
529 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
530 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
531 ensure!(
532 bounty.value <= max_amount,
533 pallet_treasury::Error::<T, I>::InsufficientPermission
534 );
535 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
536
537 bounty.status = BountyStatus::Approved;
538
539 BountyApprovals::<T, I>::try_append(bounty_id)
540 .map_err(|()| Error::<T, I>::TooManyQueued)?;
541
542 Ok(())
543 })?;
544
545 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
546 Ok(())
547 }
548
549 #[pallet::call_index(2)]
556 #[pallet::weight(<T as Config<I>>::WeightInfo::propose_curator())]
557 pub fn propose_curator(
558 origin: OriginFor<T>,
559 #[pallet::compact] bounty_id: BountyIndex,
560 curator: AccountIdLookupOf<T>,
561 #[pallet::compact] fee: BalanceOf<T, I>,
562 ) -> DispatchResult {
563 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
564
565 let curator = T::Lookup::lookup(curator)?;
566 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
567 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
568 ensure!(
569 bounty.value <= max_amount,
570 pallet_treasury::Error::<T, I>::InsufficientPermission
571 );
572 match bounty.status {
573 BountyStatus::Funded => {},
574 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
575 };
576
577 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
578
579 bounty.status = BountyStatus::CuratorProposed { curator: curator.clone() };
580 bounty.fee = fee;
581
582 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
583
584 Ok(())
585 })?;
586 Ok(())
587 }
588
589 #[pallet::call_index(3)]
607 #[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
608 pub fn unassign_curator(
609 origin: OriginFor<T>,
610 #[pallet::compact] bounty_id: BountyIndex,
611 ) -> DispatchResult {
612 let maybe_sender = ensure_signed(origin.clone())
613 .map(Some)
614 .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
615
616 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
617 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
618
619 let slash_curator =
620 |curator: &T::AccountId, curator_deposit: &mut BalanceOf<T, I>| {
621 let imbalance = T::Currency::slash_reserved(curator, *curator_deposit).0;
622 T::OnSlash::on_unbalanced(imbalance);
623 *curator_deposit = Zero::zero();
624 };
625
626 match bounty.status {
627 BountyStatus::Proposed | BountyStatus::Approved | BountyStatus::Funded => {
628 return Err(Error::<T, I>::UnexpectedStatus.into());
630 },
631 BountyStatus::ApprovedWithCurator { ref curator } => {
632 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
635 bounty.status = BountyStatus::Approved;
638 return Ok(());
639 },
640 BountyStatus::CuratorProposed { ref curator } => {
641 ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
644 },
645 BountyStatus::Active { ref curator, ref update_due } => {
646 match maybe_sender {
648 None => {
650 slash_curator(curator, &mut bounty.curator_deposit);
651 },
653 Some(sender) => {
654 if sender != *curator {
657 let block_number = Self::treasury_block_number();
658 if *update_due < block_number {
659 slash_curator(curator, &mut bounty.curator_deposit);
660 } else {
662 return Err(Error::<T, I>::Premature.into());
664 }
665 } else {
666 let err_amount =
669 T::Currency::unreserve(curator, bounty.curator_deposit);
670 debug_assert!(err_amount.is_zero());
671 bounty.curator_deposit = Zero::zero();
672 }
674 },
675 }
676 },
677 BountyStatus::PendingPayout { ref curator, .. } => {
678 ensure!(maybe_sender.is_none(), BadOrigin);
682 slash_curator(curator, &mut bounty.curator_deposit);
683 },
685 };
686
687 bounty.status = BountyStatus::Funded;
688 Ok(())
689 })?;
690
691 Self::deposit_event(Event::<T, I>::CuratorUnassigned { bounty_id });
692 Ok(())
693 }
694
695 #[pallet::call_index(4)]
703 #[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
704 pub fn accept_curator(
705 origin: OriginFor<T>,
706 #[pallet::compact] bounty_id: BountyIndex,
707 ) -> DispatchResult {
708 let signer = ensure_signed(origin)?;
709
710 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
711 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
712
713 match bounty.status {
714 BountyStatus::CuratorProposed { ref curator } => {
715 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
716
717 let deposit = Self::calculate_curator_deposit(&bounty.fee);
718 T::Currency::reserve(curator, deposit)?;
719 bounty.curator_deposit = deposit;
720
721 let update_due = Self::treasury_block_number()
722 .saturating_add(T::BountyUpdatePeriod::get());
723 bounty.status =
724 BountyStatus::Active { curator: curator.clone(), update_due };
725
726 Self::deposit_event(Event::<T, I>::CuratorAccepted {
727 bounty_id,
728 curator: signer,
729 });
730 Ok(())
731 },
732 _ => Err(Error::<T, I>::UnexpectedStatus.into()),
733 }
734 })?;
735 Ok(())
736 }
737
738 #[pallet::call_index(5)]
749 #[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
750 pub fn award_bounty(
751 origin: OriginFor<T>,
752 #[pallet::compact] bounty_id: BountyIndex,
753 beneficiary: AccountIdLookupOf<T>,
754 ) -> DispatchResult {
755 let signer = ensure_signed(origin)?;
756 let beneficiary = T::Lookup::lookup(beneficiary)?;
757
758 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
759 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
760
761 ensure!(
763 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
764 Error::<T, I>::HasActiveChildBounty
765 );
766
767 match &bounty.status {
768 BountyStatus::Active { curator, .. } => {
769 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
770 },
771 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
772 }
773 bounty.status = BountyStatus::PendingPayout {
774 curator: signer,
775 beneficiary: beneficiary.clone(),
776 unlock_at: Self::treasury_block_number() + T::BountyDepositPayoutDelay::get(),
777 };
778
779 Ok(())
780 })?;
781
782 Self::deposit_event(Event::<T, I>::BountyAwarded { index: bounty_id, beneficiary });
783 Ok(())
784 }
785
786 #[pallet::call_index(6)]
795 #[pallet::weight(<T as Config<I>>::WeightInfo::claim_bounty())]
796 pub fn claim_bounty(
797 origin: OriginFor<T>,
798 #[pallet::compact] bounty_id: BountyIndex,
799 ) -> DispatchResult {
800 ensure_signed(origin)?; Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
803 let bounty = maybe_bounty.take().ok_or(Error::<T, I>::InvalidIndex)?;
804 if let BountyStatus::PendingPayout { curator, beneficiary, unlock_at } =
805 bounty.status
806 {
807 ensure!(Self::treasury_block_number() >= unlock_at, Error::<T, I>::Premature);
808 let bounty_account = Self::bounty_account_id(bounty_id);
809 let balance = T::Currency::free_balance(&bounty_account);
810 let fee = bounty.fee.min(balance); let payout = balance.saturating_sub(fee);
812 let err_amount = T::Currency::unreserve(&curator, bounty.curator_deposit);
813 debug_assert!(err_amount.is_zero());
814
815 let children_fee = T::ChildBountyManager::children_curator_fees(bounty_id);
818 debug_assert!(children_fee <= fee);
819
820 let final_fee = fee.saturating_sub(children_fee);
821 let res =
822 T::Currency::transfer(&bounty_account, &curator, final_fee, AllowDeath); debug_assert!(res.is_ok());
824 let res =
825 T::Currency::transfer(&bounty_account, &beneficiary, payout, AllowDeath); debug_assert!(res.is_ok());
827
828 *maybe_bounty = None;
829
830 BountyDescriptions::<T, I>::remove(bounty_id);
831 T::ChildBountyManager::bounty_removed(bounty_id);
832
833 Self::deposit_event(Event::<T, I>::BountyClaimed {
834 index: bounty_id,
835 payout,
836 beneficiary,
837 });
838 Ok(())
839 } else {
840 Err(Error::<T, I>::UnexpectedStatus.into())
841 }
842 })?;
843 Ok(())
844 }
845
846 #[pallet::call_index(7)]
856 #[pallet::weight(<T as Config<I>>::WeightInfo::close_bounty_proposed()
857 .max(<T as Config<I>>::WeightInfo::close_bounty_active()))]
858 pub fn close_bounty(
859 origin: OriginFor<T>,
860 #[pallet::compact] bounty_id: BountyIndex,
861 ) -> DispatchResultWithPostInfo {
862 T::RejectOrigin::ensure_origin(origin)?;
863
864 Bounties::<T, I>::try_mutate_exists(
865 bounty_id,
866 |maybe_bounty| -> DispatchResultWithPostInfo {
867 let bounty = maybe_bounty.as_ref().ok_or(Error::<T, I>::InvalidIndex)?;
868
869 ensure!(
871 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
872 Error::<T, I>::HasActiveChildBounty
873 );
874
875 match &bounty.status {
876 BountyStatus::Proposed => {
877 BountyDescriptions::<T, I>::remove(bounty_id);
879 let value = bounty.bond;
880 let imbalance = T::Currency::slash_reserved(&bounty.proposer, value).0;
881 T::OnSlash::on_unbalanced(imbalance);
882 *maybe_bounty = None;
883
884 Self::deposit_event(Event::<T, I>::BountyRejected {
885 index: bounty_id,
886 bond: value,
887 });
888 return Ok(
890 Some(<T as Config<I>>::WeightInfo::close_bounty_proposed()).into()
891 );
892 },
893 BountyStatus::Approved | BountyStatus::ApprovedWithCurator { .. } => {
894 return Err(Error::<T, I>::UnexpectedStatus.into());
897 },
898 BountyStatus::Funded | BountyStatus::CuratorProposed { .. } => {
899 },
901 BountyStatus::Active { curator, .. } => {
902 let err_amount =
904 T::Currency::unreserve(curator, bounty.curator_deposit);
905 debug_assert!(err_amount.is_zero());
906 },
908 BountyStatus::PendingPayout { .. } => {
909 return Err(Error::<T, I>::PendingPayout.into());
914 },
915 }
916
917 let bounty_account = Self::bounty_account_id(bounty_id);
918
919 BountyDescriptions::<T, I>::remove(bounty_id);
920
921 T::TransferAllAssets::force_transfer_all_assets(
922 &bounty_account,
923 &Self::account_id(),
924 )?;
925
926 *maybe_bounty = None;
927 T::ChildBountyManager::bounty_removed(bounty_id);
928
929 Self::deposit_event(Event::<T, I>::BountyCanceled { index: bounty_id });
930 Ok(Some(<T as Config<I>>::WeightInfo::close_bounty_active()).into())
931 },
932 )
933 }
934
935 #[pallet::call_index(8)]
945 #[pallet::weight(<T as Config<I>>::WeightInfo::extend_bounty_expiry())]
946 pub fn extend_bounty_expiry(
947 origin: OriginFor<T>,
948 #[pallet::compact] bounty_id: BountyIndex,
949 _remark: Vec<u8>,
950 ) -> DispatchResult {
951 let signer = ensure_signed(origin)?;
952
953 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
954 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
955
956 match bounty.status {
957 BountyStatus::Active { ref curator, ref mut update_due } => {
958 ensure!(*curator == signer, Error::<T, I>::RequireCurator);
959 *update_due = Self::treasury_block_number()
960 .saturating_add(T::BountyUpdatePeriod::get())
961 .max(*update_due);
962 },
963 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
964 }
965
966 Ok(())
967 })?;
968
969 Self::deposit_event(Event::<T, I>::BountyExtended { index: bounty_id });
970 Ok(())
971 }
972
973 #[pallet::call_index(9)]
985 #[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty_with_curator())]
986 pub fn approve_bounty_with_curator(
987 origin: OriginFor<T>,
988 #[pallet::compact] bounty_id: BountyIndex,
989 curator: AccountIdLookupOf<T>,
990 #[pallet::compact] fee: BalanceOf<T, I>,
991 ) -> DispatchResult {
992 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
993 let curator = T::Lookup::lookup(curator)?;
994 Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
995 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
997 ensure!(
998 bounty.value <= max_amount,
999 pallet_treasury::Error::<T, I>::InsufficientPermission
1000 );
1001 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
1002 ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
1003
1004 BountyApprovals::<T, I>::try_append(bounty_id)
1005 .map_err(|()| Error::<T, I>::TooManyQueued)?;
1006
1007 bounty.status = BountyStatus::ApprovedWithCurator { curator: curator.clone() };
1008 bounty.fee = fee;
1009
1010 Ok(())
1011 })?;
1012
1013 Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
1014 Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
1015
1016 Ok(())
1017 }
1018
1019 #[pallet::call_index(10)]
1035 #[pallet::weight(<T as Config<I>>::WeightInfo::poke_deposit())]
1036 pub fn poke_deposit(
1037 origin: OriginFor<T>,
1038 #[pallet::compact] bounty_id: BountyIndex,
1039 ) -> DispatchResultWithPostInfo {
1040 ensure_signed(origin)?;
1041
1042 let deposit_updated = Self::poke_bounty_deposit(bounty_id)?;
1043
1044 Ok(if deposit_updated { Pays::No } else { Pays::Yes }.into())
1045 }
1046
1047 #[pallet::call_index(11)]
1059 #[pallet::weight(<T as Config<I>>::WeightInfo::reclaim_bounty_funds())]
1060 pub fn reclaim_bounty_funds(
1061 origin: OriginFor<T>,
1062 #[pallet::compact] bounty_id: BountyIndex,
1063 ) -> DispatchResultWithPostInfo {
1064 ensure_signed(origin)?;
1065
1066 ensure!(!Bounties::<T, I>::contains_key(bounty_id), Error::<T, I>::BountyStillActive);
1068
1069 debug_assert!(
1070 T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
1071 "child bounties should not exist for a closed bounty"
1072 );
1073
1074 let bounty_account = Self::bounty_account_id(bounty_id);
1075 let treasury_account = Self::account_id();
1076
1077 let transferred = T::TransferAllAssets::force_transfer_all_assets(
1078 &bounty_account,
1079 &treasury_account,
1080 )?;
1081
1082 if !transferred {
1084 return Ok(Pays::Yes.into());
1085 }
1086
1087 Self::deposit_event(Event::<T, I>::BountyFundsReclaimed { bounty_id });
1088
1089 Ok(Pays::No.into())
1090 }
1091 }
1092
1093 #[pallet::hooks]
1094 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
1095 #[cfg(feature = "try-runtime")]
1096 fn try_state(_n: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1097 Self::do_try_state()
1098 }
1099 }
1100}
1101
1102#[cfg(any(feature = "try-runtime", test))]
1103impl<T: Config<I>, I: 'static> Pallet<T, I> {
1104 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1108 Self::try_state_bounties_count()?;
1109
1110 Ok(())
1111 }
1112
1113 fn try_state_bounties_count() -> Result<(), sp_runtime::TryRuntimeError> {
1121 let bounties_length = Bounties::<T, I>::iter().count() as u32;
1122
1123 ensure!(
1124 <BountyCount<T, I>>::get() >= bounties_length,
1125 "`BountyCount` must be grater or equals the number of `Bounties` in storage"
1126 );
1127
1128 let bounties_description_length = BountyDescriptions::<T, I>::iter().count() as u32;
1129 ensure!(
1130 <BountyCount<T, I>>::get() >= bounties_description_length,
1131 "`BountyCount` must be grater or equals the number of `BountiesDescriptions` in storage."
1132 );
1133
1134 ensure!(
1135 bounties_length == bounties_description_length,
1136 "Number of `Bounties` in storage must be the same as the Number of `BountiesDescription` in storage."
1137 );
1138 Ok(())
1139 }
1140}
1141
1142impl<T: Config<I>, I: 'static> Pallet<T, I> {
1143 pub fn treasury_block_number() -> BlockNumberFor<T, I> {
1147 <T as pallet_treasury::Config<I>>::BlockNumberProvider::current_block_number()
1148 }
1149
1150 pub fn calculate_curator_deposit(fee: &BalanceOf<T, I>) -> BalanceOf<T, I> {
1152 let mut deposit = T::CuratorDepositMultiplier::get() * *fee;
1153
1154 if let Some(max_deposit) = T::CuratorDepositMax::get() {
1155 deposit = deposit.min(max_deposit)
1156 }
1157
1158 if let Some(min_deposit) = T::CuratorDepositMin::get() {
1159 deposit = deposit.max(min_deposit)
1160 }
1161
1162 deposit
1163 }
1164
1165 pub fn account_id() -> T::AccountId {
1170 T::PalletId::get().into_account_truncating()
1171 }
1172
1173 pub fn bounty_account_id(id: BountyIndex) -> T::AccountId {
1175 T::PalletId::get().into_sub_account_truncating(("bt", id))
1178 }
1179
1180 fn create_bounty(
1181 proposer: T::AccountId,
1182 description: Vec<u8>,
1183 value: BalanceOf<T, I>,
1184 ) -> DispatchResult {
1185 let bounded_description: BoundedVec<_, _> =
1186 description.try_into().map_err(|_| Error::<T, I>::ReasonTooBig)?;
1187 ensure!(value >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
1188
1189 let index = BountyCount::<T, I>::get();
1190
1191 let bond = Self::calculate_bounty_deposit(&bounded_description);
1193 T::Currency::reserve(&proposer, bond)
1194 .map_err(|_| Error::<T, I>::InsufficientProposersBalance)?;
1195
1196 BountyCount::<T, I>::put(index + 1);
1197
1198 let bounty = Bounty {
1199 proposer,
1200 value,
1201 fee: 0u32.into(),
1202 curator_deposit: 0u32.into(),
1203 bond,
1204 status: BountyStatus::Proposed,
1205 };
1206
1207 Bounties::<T, I>::insert(index, &bounty);
1208 BountyDescriptions::<T, I>::insert(index, bounded_description);
1209
1210 Self::deposit_event(Event::<T, I>::BountyProposed { index });
1211
1212 Ok(())
1213 }
1214
1215 fn calculate_bounty_deposit(
1217 description: &BoundedVec<u8, T::MaximumReasonLength>,
1218 ) -> BalanceOf<T, I> {
1219 T::BountyDepositBase::get().saturating_add(
1220 T::DataDepositPerByte::get().saturating_mul((description.len() as u32).into()),
1221 )
1222 }
1223
1224 fn poke_bounty_deposit(bounty_id: BountyIndex) -> Result<bool, DispatchError> {
1228 let mut bounty = Bounties::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1229 let bounty_description =
1230 BountyDescriptions::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1231 ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
1233
1234 let new_bond = Self::calculate_bounty_deposit(&bounty_description);
1235 let old_bond = bounty.bond;
1236 if new_bond == old_bond {
1237 return Ok(false);
1238 }
1239 if new_bond > old_bond {
1240 let extra = new_bond.saturating_sub(old_bond);
1241 T::Currency::reserve(&bounty.proposer, extra)?;
1242 } else {
1243 let excess = old_bond.saturating_sub(new_bond);
1244 let remaining_unreserved = T::Currency::unreserve(&bounty.proposer, excess);
1245 if !remaining_unreserved.is_zero() {
1246 defensive!(
1247 "Failed to unreserve full amount. (Requested, Actual)",
1248 (excess, excess.saturating_sub(remaining_unreserved))
1249 );
1250 }
1251 }
1252 bounty.bond = new_bond;
1253 Bounties::<T, I>::insert(bounty_id, &bounty);
1254
1255 Self::deposit_event(Event::<T, I>::DepositPoked {
1256 bounty_id,
1257 proposer: bounty.proposer,
1258 old_deposit: old_bond,
1259 new_deposit: new_bond,
1260 });
1261
1262 Ok(true)
1263 }
1264}
1265
1266impl<T: Config<I>, I: 'static> pallet_treasury::SpendFunds<T, I> for Pallet<T, I> {
1267 fn spend_funds(
1268 budget_remaining: &mut BalanceOf<T, I>,
1269 imbalance: &mut PositiveImbalanceOf<T, I>,
1270 total_weight: &mut Weight,
1271 missed_any: &mut bool,
1272 ) {
1273 let bounties_len = BountyApprovals::<T, I>::mutate(|v| {
1274 let bounties_approval_len = v.len() as u32;
1275 v.retain(|&index| {
1276 Bounties::<T, I>::mutate(index, |bounty| {
1277 if let Some(bounty) = bounty {
1279 if bounty.value <= *budget_remaining {
1280 *budget_remaining -= bounty.value;
1281
1282 if let BountyStatus::ApprovedWithCurator { curator } = &bounty.status {
1284 bounty.status =
1285 BountyStatus::CuratorProposed { curator: curator.clone() };
1286 } else {
1287 bounty.status = BountyStatus::Funded;
1288 }
1289
1290 let err_amount = T::Currency::unreserve(&bounty.proposer, bounty.bond);
1292 debug_assert!(err_amount.is_zero());
1293
1294 imbalance.subsume(T::Currency::deposit_creating(
1296 &Self::bounty_account_id(index),
1297 bounty.value,
1298 ));
1299
1300 Self::deposit_event(Event::<T, I>::BountyBecameActive { index });
1301 false
1302 } else {
1303 *missed_any = true;
1304 true
1305 }
1306 } else {
1307 false
1308 }
1309 })
1310 });
1311 bounties_approval_len
1312 });
1313
1314 *total_weight += <T as pallet::Config<I>>::WeightInfo::spend_funds(bounties_len);
1315 }
1316}
1317
1318impl<Balance: Zero> ChildBountyManager<Balance> for () {
1320 fn child_bounties_count(_bounty_id: BountyIndex) -> BountyIndex {
1321 Default::default()
1322 }
1323
1324 fn children_curator_fees(_bounty_id: BountyIndex) -> Balance {
1325 Zero::zero()
1326 }
1327
1328 fn bounty_removed(_bounty_id: BountyIndex) {}
1329}