Skip to main content

pallet_multi_asset_bounties/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! > Made with *Substrate*, for *Polkadot*.
19//!
20//! [![github]](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/multi-asset-bounties) -
21//! [![polkadot]](https://polkadot.com)
22//!
23//! [polkadot]: https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white
24//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
25//!
26//!
27//! # Multi Asset Bounties Pallet ( `pallet-multi-asset-bounties` )
28//!
29//! ## Bounty
30//!
31//! A bounty is a reward for completing a specified body of work or achieving a defined set of
32//! objectives. The work must be completed for a predefined amount to be paid out. A curator is
33//! assigned when the bounty is funded, and is responsible for awarding the bounty once the
34//! objectives are met. To support parallel execution and better governance, a bounty can be split
35//! into multiple child bounties. Each child bounty represents a smaller task derived from the
36//! parent bounty. The parent bounty curator may assign a separate curator to each child bounty at
37//! creation time. The curator may be unassigned, resulting in a new curator election. A bounty may
38//! be cancelled at any time—unless a payment has already been attempted and is awaiting status
39//! confirmation.
40//!
41//! > NOTE: A parent bounty cannot be closed if it has any active child bounties associated with it.
42//!
43//! ### Terminology
44//!
45//! - **Bounty:** A reward for a predefined body of work upon completion. A bounty defines the total
46//!   reward and can be subdivided into multiple child bounties. When referenced in the context of
47//!   child bounties, it is referred to as *parent bounty*.
48//! - **Curator:** An account managing the bounty and assigning a payout address.
49//! - **Child Bounty:** A subtask or milestone funded by a parent bounty. It may carry its own
50//!   curator, and reward similar to the parent bounty.
51//! - **Curator deposit:** The payment in native asset from a candidate willing to curate a funded
52//!   bounty. The deposit is returned when/if the bounty is completed.
53//! - **Bounty value:** The total amount in a given asset kind that should be paid to the
54//!   Beneficiary if the bounty is rewarded.
55//! - **Beneficiary:** The account/location to which the total or part of the bounty is assigned to.
56//!
57//! ### Example
58//!
59//! 1. Fund a bounty approved by spend origin of some asset kind with a proposed curator.
60#![doc = docify::embed!("src/tests.rs", fund_bounty_works)]
61//! 2. Award a bounty to a beneficiary.
62#![doc = docify::embed!("src/tests.rs", award_bounty_works)]
63//! ## Pallet API
64//!
65//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
66//! including its configuration trait, dispatchables, storage items, events and errors.
67
68#![cfg_attr(not(feature = "std"), no_std)]
69
70mod benchmarking;
71mod mock;
72mod tests;
73pub mod weights;
74#[cfg(feature = "runtime-benchmarks")]
75pub use benchmarking::ArgumentsFactory;
76pub use pallet::*;
77pub use weights::WeightInfo;
78
79extern crate alloc;
80use alloc::{boxed::Box, collections::btree_map::BTreeMap};
81use frame_support::{
82	dispatch::{DispatchResult, DispatchResultWithPostInfo},
83	dispatch_context::with_context,
84	pallet_prelude::*,
85	traits::{
86		tokens::{
87			Balance, ConversionFromAssetBalance, ConversionToAssetBalance, PayWithSource,
88			PaymentStatus,
89		},
90		Consideration, EnsureOrigin, Get, QueryPreimage, StorePreimage,
91	},
92	PalletId,
93};
94use frame_system::pallet_prelude::{
95	ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
96};
97use scale_info::TypeInfo;
98use sp_runtime::{
99	traits::{AccountIdConversion, BadOrigin, Convert, Saturating, StaticLookup, TryConvert, Zero},
100	Debug, Permill,
101};
102
103/// Lookup type for beneficiary addresses.
104pub type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source;
105/// An index of a bounty. Just a `u32`.
106pub type BountyIndex = u32;
107/// Lookup type for account addresses.
108pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
109/// The payment identifier type used by the [`Config::Paymaster`].
110pub type PaymentIdOf<T, I = ()> = <<T as crate::Config<I>>::Paymaster as PayWithSource>::Id;
111/// Convenience alias for `Bounty`.
112pub type BountyOf<T, I> = Bounty<
113	<T as frame_system::Config>::AccountId,
114	<T as Config<I>>::Balance,
115	<T as Config<I>>::AssetKind,
116	<T as frame_system::Config>::Hash,
117	PaymentIdOf<T, I>,
118	<T as Config<I>>::Beneficiary,
119>;
120/// Convenience alias for `ChildBounty`.
121pub type ChildBountyOf<T, I> = ChildBounty<
122	<T as frame_system::Config>::AccountId,
123	<T as Config<I>>::Balance,
124	<T as frame_system::Config>::Hash,
125	PaymentIdOf<T, I>,
126	<T as Config<I>>::Beneficiary,
127>;
128
129/// A funded bounty.
130#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
131pub struct Bounty<AccountId, Balance, AssetKind, Hash, PaymentId, Beneficiary> {
132	/// The kind of asset this bounty is rewarded in.
133	pub asset_kind: AssetKind,
134	/// The amount that should be paid if the bounty is rewarded, including
135	/// beneficiary payout and possible child bounties.
136	///
137	/// The asset class determined by `asset_kind`.
138	pub value: Balance,
139	/// The metadata concerning the bounty.
140	///
141	/// The `Hash` refers to the preimage of the `Preimages` provider which can be a JSON
142	/// dump or IPFS hash of a JSON file.
143	pub metadata: Hash,
144	/// The status of this bounty.
145	pub status: BountyStatus<AccountId, PaymentId, Beneficiary>,
146}
147
148/// A funded child-bounty.
149#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
150pub struct ChildBounty<AccountId, Balance, Hash, PaymentId, Beneficiary> {
151	/// The parent bounty index of this child-bounty.
152	pub parent_bounty: BountyIndex,
153	/// The amount that should be paid if the child-bounty is rewarded.
154	///
155	/// The asset class determined by the parent bounty `asset_kind`.
156	pub value: Balance,
157	/// The metadata concerning the child-bounty.
158	///
159	/// The `Hash` refers to the preimage of the `Preimages` provider which can be a JSON
160	/// dump or IPFS hash of a JSON file.
161	pub metadata: Hash,
162	/// The status of this child-bounty.
163	pub status: BountyStatus<AccountId, PaymentId, Beneficiary>,
164}
165
166/// The status of a child-/bounty proposal.
167#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
168pub enum BountyStatus<AccountId, PaymentId, Beneficiary> {
169	/// The child-/bounty funding has been attempted and is waiting to confirm the funds
170	/// allocation.
171	///
172	/// Call `check_status` to confirm whether the funding payment succeeded. If successful, the
173	/// child-/bounty transitions to [`BountyStatus::Funded`]. Otherwise, use `retry_payment` to
174	/// reinitiate the funding payment.
175	FundingAttempted {
176		/// The proposed curator of this child-/bounty.
177		curator: AccountId,
178		/// The funding payment status from the source (e.g. Treasury, parent bounty) to
179		/// the child-/bounty account/location.
180		payment_status: PaymentState<PaymentId>,
181	},
182	/// The child-/bounty is funded and waiting for curator to accept role.
183	Funded {
184		/// The proposed curator of this child-/bounty.
185		curator: AccountId,
186	},
187	/// The child-/bounty previously assigned curator has been unassigned.
188	///
189	/// It remains funded and is waiting for a curator proposal.
190	CuratorUnassigned,
191	/// The child-/bounty is active and waiting to be awarded.
192	///
193	/// During the `Active` state, the curator can call `fund_child_bounty` to create multiple
194	/// child bounties.
195	Active {
196		/// The curator of this child-/bounty.
197		curator: AccountId,
198	},
199	/// The child-/bounty is closed, and the funds are being refunded to the original source (e.g.,
200	/// Treasury). Once `check_status` confirms the payment succeeded, the child-/bounty is
201	/// finalized and removed from storage. Otherwise, use `retry_payment` to reinitiate the refund
202	/// payment.
203	RefundAttempted {
204		/// The curator of this child-/bounty.
205		///
206		/// If `None`, it means the child-/bounty curator was unassigned.
207		curator: Option<AccountId>,
208		/// The refund payment status from the child-/bounty account/location to the source (e.g.
209		/// Treasury, parent bounty).
210		payment_status: PaymentState<PaymentId>,
211	},
212	/// The child-/bounty payout to a beneficiary has been attempted.
213	///
214	/// Call `check_status` to confirm whether the payout payment succeeded. If successful, the
215	/// child-/bounty is finalized and removed from storage. Otherwise, use `retry_payment` to
216	/// reinitiate the payout payment.
217	PayoutAttempted {
218		/// The curator of this child-/bounty.
219		curator: AccountId,
220		/// The beneficiary stash account/location.
221		beneficiary: Beneficiary,
222		/// The payout payment status from the child-/bounty account/location to the beneficiary.
223		payment_status: PaymentState<PaymentId>,
224	},
225}
226
227/// The state of a single payment.
228///
229/// When a payment is initiated via `Paymaster::pay`, it begins in the `Pending` state. The
230/// `check_status` call updates the payment state and advances the child-/bounty status. The
231/// `retry_payment` call can be used to reattempt payments in either `Pending` or `Failed` states.
232#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo)]
233pub enum PaymentState<Id> {
234	/// Pending claim.
235	Pending,
236	/// Payment attempted with a payment identifier.
237	Attempted { id: Id },
238	/// Payment failed.
239	Failed,
240	/// Payment succeeded.
241	Succeeded,
242}
243impl<Id: Clone> PaymentState<Id> {
244	/// Used to check if payment can be retried.
245	pub fn is_pending_or_failed(&self) -> bool {
246		matches!(self, PaymentState::Pending | PaymentState::Failed)
247	}
248
249	/// If a payment has been initiated, returns its identifier, which is used to check its
250	/// status.
251	pub fn get_attempt_id(&self) -> Option<Id> {
252		match self {
253			PaymentState::Attempted { id } => Some(id.clone()),
254			_ => None,
255		}
256	}
257}
258
259#[frame_support::pallet]
260pub mod pallet {
261	use super::*;
262
263	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
264
265	#[pallet::pallet]
266	#[pallet::storage_version(STORAGE_VERSION)]
267	pub struct Pallet<T, I = ()>(_);
268
269	#[pallet::config]
270	pub trait Config<I: 'static = ()>: frame_system::Config {
271		/// The type in which the assets are measured.
272		type Balance: Balance;
273
274		/// Origin from which bounties rejections must come.
275		type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
276
277		/// The origin required for funding the bounty. The `Success` value is the maximum amount in
278		/// a native asset that this origin is allowed to spend at a time.
279		type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::Balance>;
280
281		/// Type parameter representing the asset kinds used to fund, refund and spend from
282		/// bounties.
283		type AssetKind: Parameter + MaxEncodedLen;
284
285		/// Type parameter used to identify the beneficiaries eligible to receive payments.
286		type Beneficiary: Parameter + MaxEncodedLen;
287
288		/// Converting trait to take a source type and convert to [`Self::Beneficiary`].
289		type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
290
291		/// Minimum value for a bounty.
292		#[pallet::constant]
293		type BountyValueMinimum: Get<Self::Balance>;
294
295		/// Minimum value for a child-bounty.
296		#[pallet::constant]
297		type ChildBountyValueMinimum: Get<Self::Balance>;
298
299		/// Maximum number of child bounties that can be added to a parent bounty.
300		#[pallet::constant]
301		type MaxActiveChildBountyCount: Get<u32>;
302
303		/// Weight information for extrinsics in this pallet.
304		type WeightInfo: WeightInfo;
305
306		/// Converts an `AssetKind` into the funding source account/location.
307		///
308		/// Used when initiating funding and refund payments to and from a bounty.
309		type FundingSource: TryConvert<
310			Self::AssetKind,
311			<<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
312		>;
313
314		/// Converts a bounty index and `AssetKind` into its funding source account/location.
315		///
316		/// Used when initiating the funding, refund, and payout payments to and from a bounty.
317		type BountySource: TryConvert<
318			(BountyIndex, Self::AssetKind),
319			<<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
320		>;
321
322		/// Converts a parent bounty index, child bounty index, and `AssetKind` into the
323		/// child-bounty account/location.
324		///
325		/// Used when initiating the funding, refund, and payout payments to and from a
326		/// child-bounty.
327		type ChildBountySource: TryConvert<
328			(BountyIndex, BountyIndex, Self::AssetKind),
329			<<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
330		>;
331
332		/// Type for processing payments of [`Self::AssetKind`] from a `Source` in favor of
333		/// [`Self::Beneficiary`].
334		type Paymaster: PayWithSource<
335			Balance = Self::Balance,
336			Source = Self::Beneficiary,
337			Beneficiary = Self::Beneficiary,
338			AssetKind = Self::AssetKind,
339		>;
340
341		/// Type for converting the balance of an [`Self::AssetKind`] to the balance of the native
342		/// asset, solely for the purpose of asserting the result against the maximum allowed spend
343		/// amount of the [`Self::SpendOrigin`].
344		///
345		/// The conversion from the native asset balance to the balance of an [`Self::AssetKind`] is
346		/// used in benchmarks to convert [`Self::BountyValueMinimum`] to the asset kind amount.
347		type BalanceConverter: ConversionFromAssetBalance<Self::Balance, Self::AssetKind, Self::Balance>
348			+ ConversionToAssetBalance<Self::Balance, Self::AssetKind, Self::Balance>;
349
350		/// The preimage provider used for child-/bounty metadata.
351		type Preimages: QueryPreimage<H = Self::Hashing> + StorePreimage;
352
353		/// Means of associating a cost with committing to the curator role, which is incurred by
354		/// the child-/bounty curator.
355		///
356		/// The footprint accounts for the child-/bounty value converted to the native balance
357		/// type (using [`Self::BalanceConverter`]). The native balance type corresponds to the
358		/// `Success` type returned by [`Self::SpendOrigin`], which represents the maximum
359		/// spendable amount. The bounty amount must be converted with [`Self::BalanceConverter`]
360		/// before comparison against this maximum. The cost taken from the curator `AccountId`
361		/// may vary based on this converted balance.
362		type Consideration: Consideration<Self::AccountId, Self::Balance>;
363
364		/// Helper type for benchmarks.
365		#[cfg(feature = "runtime-benchmarks")]
366		type BenchmarkHelper: benchmarking::ArgumentsFactory<
367			Self::AssetKind,
368			Self::Beneficiary,
369			Self::Balance,
370		>;
371	}
372
373	#[pallet::error]
374	pub enum Error<T, I = ()> {
375		/// No child-/bounty at that index.
376		InvalidIndex,
377		/// The reason given is just too big.
378		ReasonTooBig,
379		/// Invalid child-/bounty value.
380		InvalidValue,
381		/// The balance of the asset kind is not convertible to the balance of the native asset for
382		/// asserting the origin permissions.
383		FailedToConvertBalance,
384		/// The child-/bounty status is unexpected.
385		UnexpectedStatus,
386		/// Require child-/bounty curator.
387		RequireCurator,
388		/// The spend origin is valid but the amount it is allowed to spend is lower than the
389		/// requested amount.
390		InsufficientPermission,
391		/// There was issue with funding the child-/bounty.
392		FundingError,
393		/// There was issue with refunding the child-/bounty.
394		RefundError,
395		// There was issue paying out the child-/bounty.
396		PayoutError,
397		/// Child-/bounty funding has not concluded yet.
398		FundingInconclusive,
399		/// Child-/bounty refund has not concluded yet.
400		RefundInconclusive,
401		/// Child-/bounty payout has not concluded yet.
402		PayoutInconclusive,
403		/// The child-/bounty or funding source account could not be derived from the indexes and
404		/// asset kind.
405		FailedToConvertSource,
406		/// The parent bounty cannot be closed because it has active child bounties.
407		HasActiveChildBounty,
408		/// Number of child bounties exceeds limit `MaxActiveChildBountyCount`.
409		TooManyChildBounties,
410		/// The parent bounty value is not enough to add new child-bounty.
411		InsufficientBountyValue,
412		/// The preimage does not exist.
413		PreimageNotExist,
414	}
415
416	#[pallet::event]
417	#[pallet::generate_deposit(pub(super) fn deposit_event)]
418	pub enum Event<T: Config<I>, I: 'static = ()> {
419		/// A new bounty was created and funding has been initiated.
420		BountyCreated { index: BountyIndex },
421		/// A new child-bounty was created and funding has been initiated.
422		ChildBountyCreated { index: BountyIndex, child_index: BountyIndex },
423		/// The curator accepted role and child-/bounty became active.
424		BountyBecameActive {
425			index: BountyIndex,
426			child_index: Option<BountyIndex>,
427			curator: T::AccountId,
428		},
429		/// A child-/bounty was awarded to a beneficiary.
430		BountyAwarded {
431			index: BountyIndex,
432			child_index: Option<BountyIndex>,
433			beneficiary: T::Beneficiary,
434		},
435		/// Payout payment to the beneficiary has concluded successfully.
436		BountyPayoutProcessed {
437			index: BountyIndex,
438			child_index: Option<BountyIndex>,
439			asset_kind: T::AssetKind,
440			value: T::Balance,
441			beneficiary: T::Beneficiary,
442		},
443		/// Funding payment has concluded successfully.
444		BountyFundingProcessed { index: BountyIndex, child_index: Option<BountyIndex> },
445		/// Refund payment has concluded successfully.
446		BountyRefundProcessed { index: BountyIndex, child_index: Option<BountyIndex> },
447		/// A child-/bounty was cancelled.
448		BountyCanceled { index: BountyIndex, child_index: Option<BountyIndex> },
449		/// A child-/bounty curator was unassigned.
450		CuratorUnassigned { index: BountyIndex, child_index: Option<BountyIndex> },
451		/// A child-/bounty curator was proposed.
452		CuratorProposed {
453			index: BountyIndex,
454			child_index: Option<BountyIndex>,
455			curator: T::AccountId,
456		},
457		/// A payment failed and can be retried.
458		PaymentFailed {
459			index: BountyIndex,
460			child_index: Option<BountyIndex>,
461			payment_id: PaymentIdOf<T, I>,
462		},
463		/// A payment happened and can be checked.
464		Paid { index: BountyIndex, child_index: Option<BountyIndex>, payment_id: PaymentIdOf<T, I> },
465	}
466
467	/// A reason for this pallet placing a hold on funds.
468	#[pallet::composite_enum]
469	pub enum HoldReason<I: 'static = ()> {
470		/// The funds are held as deposit for the curator commitment to a bounty.
471		#[codec(index = 0)]
472		CuratorDeposit,
473	}
474
475	/// Number of bounty proposals that have been made.
476	#[pallet::storage]
477	pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
478
479	/// Bounties that have been made.
480	#[pallet::storage]
481	pub type Bounties<T: Config<I>, I: 'static = ()> =
482		StorageMap<_, Twox64Concat, BountyIndex, BountyOf<T, I>>;
483
484	/// Child bounties that have been added.
485	///
486	/// Indexed by `(parent_bounty_id, child_bounty_id)`.
487	#[pallet::storage]
488	pub type ChildBounties<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
489		_,
490		Twox64Concat,
491		BountyIndex,
492		Twox64Concat,
493		BountyIndex,
494		ChildBountyOf<T, I>,
495	>;
496
497	/// Number of active child bounties per parent bounty.
498	///
499	/// Indexed by `parent_bounty_id`.
500	#[pallet::storage]
501	pub type ChildBountiesPerParent<T: Config<I>, I: 'static = ()> =
502		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
503
504	/// Number of total child bounties per parent bounty, including completed bounties.
505	///
506	/// Indexed by `parent_bounty_id`.
507	#[pallet::storage]
508	pub type TotalChildBountiesPerParent<T: Config<I>, I: 'static = ()> =
509		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
510
511	/// The cumulative child-bounty value for each parent bounty. To be subtracted from the parent
512	/// bounty payout when awarding bounty.
513	///
514	/// Indexed by `parent_bounty_id`.
515	#[pallet::storage]
516	pub type ChildBountiesValuePerParent<T: Config<I>, I: 'static = ()> =
517		StorageMap<_, Twox64Concat, BountyIndex, T::Balance, ValueQuery>;
518
519	/// The consideration cost incurred by the child-/bounty curator for committing to the role.
520	///
521	/// Determined by [`pallet::Config::Consideration`]. It is created when the curator accepts the
522	/// role, and is either burned if the curator misbehaves or consumed upon successful
523	/// completion of the child-/bounty.
524	///
525	/// Note: If the parent curator is also assigned to the child-bounty,  
526	/// the consideration cost is charged only once — when the curator  
527	/// accepts the role for the parent bounty.
528	///
529	/// Indexed by `(parent_bounty_id, child_bounty_id)`.
530	#[pallet::storage]
531	pub type CuratorDeposit<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
532		_,
533		Twox64Concat,
534		BountyIndex,
535		Twox64Concat,
536		Option<BountyIndex>,
537		T::Consideration,
538	>;
539
540	/// Temporarily tracks spending limits within the current context to prevent overspending.
541	#[derive(Default)]
542	pub struct SpendContext<Balance> {
543		pub spend_in_context: BTreeMap<Balance, Balance>,
544	}
545
546	#[pallet::call]
547	impl<T: Config<I>, I: 'static> Pallet<T, I> {
548		/// Fund a new bounty with a proposed curator, initiating the payment from the
549		/// funding source to the bounty account/location.
550		///
551		/// ## Dispatch Origin
552		///
553		/// Must be [`Config::SpendOrigin`] with the `Success` value being at least
554		/// the bounty value converted to native balance using [`Config::BalanceConverter`].
555		/// The converted native amount is validated against the maximum spendable amount
556		/// returned by [`Config::SpendOrigin`].
557		///
558		/// ## Details
559		///
560		/// - The `SpendOrigin` must have sufficient permissions to fund the bounty.
561		/// - The bounty `value` (in asset balance) is converted to native balance for validation.
562		/// - In case of a funding failure, the bounty status must be updated with the
563		///   `check_status` call before retrying with `retry_payment` call.
564		///
565		/// ### Parameters
566		/// - `asset_kind`: An indicator of the specific asset class to be funded.
567		/// - `value`: The total payment amount of this bounty.
568		/// - `curator`: Address of bounty curator.
569		/// - `metadata`: The hash of an on-chain stored preimage with bounty metadata.
570		///
571		/// ## Events
572		///
573		/// Emits [`Event::BountyCreated`] and [`Event::Paid`] if successful.
574		#[pallet::call_index(0)]
575		#[pallet::weight(<T as Config<I>>::WeightInfo::fund_bounty())]
576		pub fn fund_bounty(
577			origin: OriginFor<T>,
578			asset_kind: Box<T::AssetKind>,
579			#[pallet::compact] value: T::Balance,
580			curator: AccountIdLookupOf<T>,
581			metadata: T::Hash,
582		) -> DispatchResult {
583			let max_amount = T::SpendOrigin::ensure_origin(origin)?;
584			let curator = T::Lookup::lookup(curator)?;
585			ensure!(T::Preimages::len(&metadata).is_some(), Error::<T, I>::PreimageNotExist);
586
587			let native_amount = T::BalanceConverter::from_asset_balance(value, *asset_kind.clone())
588				.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
589			ensure!(native_amount >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
590			ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
591
592			with_context::<SpendContext<T::Balance>, _>(|v| {
593				let context = v.or_default();
594				let funding = context.spend_in_context.entry(max_amount).or_default();
595
596				if funding.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
597					Err(Error::<T, I>::InsufficientPermission)
598				} else {
599					*funding = funding.saturating_add(native_amount);
600					Ok(())
601				}
602			})
603			.unwrap_or(Ok(()))?;
604
605			let index = BountyCount::<T, I>::get();
606			let payment_status =
607				Self::do_process_funding_payment(index, None, *asset_kind.clone(), value, None)?;
608
609			let bounty = BountyOf::<T, I> {
610				asset_kind: *asset_kind,
611				value,
612				metadata,
613				status: BountyStatus::FundingAttempted { curator, payment_status },
614			};
615			Bounties::<T, I>::insert(index, &bounty);
616			T::Preimages::request(&metadata);
617			BountyCount::<T, I>::put(index + 1);
618
619			Self::deposit_event(Event::<T, I>::BountyCreated { index });
620
621			Ok(())
622		}
623
624		/// Fund a new child-bounty with a proposed curator, initiating the payment from the parent
625		/// bounty to the child-bounty account/location.
626		///
627		/// ## Dispatch Origin
628		///
629		/// Must be signed by the parent curator.
630		///
631		/// ## Details
632		///
633		/// - If `curator` is not provided, the child-bounty will default to using the parent
634		///   curator, allowing the parent curator to immediately call `check_status` and
635		///   `award_bounty` to payout the child-bounty.
636		/// - In case of a funding failure, the child-/bounty status must be updated with the
637		///   `check_status` call before retrying with `retry_payment` call.
638		///
639		/// ### Parameters
640		/// - `parent_bounty_id`: Index of parent bounty for which child-bounty is being added.
641		/// - `value`: The payment amount of this child-bounty.
642		/// - `metadata`: The hash of an on-chain stored preimage with child-bounty metadata.
643		/// - `curator`: Address of child-bounty curator.
644		///
645		/// ## Events
646		///
647		/// Emits [`Event::ChildBountyCreated`] and [`Event::Paid`] if successful.
648		#[pallet::call_index(1)]
649		#[pallet::weight(<T as Config<I>>::WeightInfo::fund_child_bounty())]
650		pub fn fund_child_bounty(
651			origin: OriginFor<T>,
652			#[pallet::compact] parent_bounty_id: BountyIndex,
653			#[pallet::compact] value: T::Balance,
654			metadata: T::Hash,
655			curator: Option<AccountIdLookupOf<T>>,
656		) -> DispatchResult {
657			let signer = ensure_signed(origin)?;
658			ensure!(T::Preimages::len(&metadata).is_some(), Error::<T, I>::PreimageNotExist);
659
660			let (asset_kind, parent_value, _, _, parent_curator) =
661				Self::get_bounty_details(parent_bounty_id, None)
662					.map_err(|_| Error::<T, I>::InvalidIndex)?;
663			let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind.clone())
664				.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
665
666			ensure!(
667				native_amount >= T::ChildBountyValueMinimum::get(),
668				Error::<T, I>::InvalidValue
669			);
670			ensure!(
671				ChildBountiesPerParent::<T, I>::get(parent_bounty_id) <
672					T::MaxActiveChildBountyCount::get(),
673				Error::<T, I>::TooManyChildBounties,
674			);
675
676			// Parent bounty must be `Active` with a curator assigned.
677			let parent_curator = parent_curator.ok_or(Error::<T, I>::UnexpectedStatus)?;
678			let final_curator = match curator {
679				Some(curator) => T::Lookup::lookup(curator)?,
680				None => parent_curator.clone(),
681			};
682			ensure!(signer == parent_curator, Error::<T, I>::RequireCurator);
683
684			// Check value
685			let child_bounties_value = ChildBountiesValuePerParent::<T, I>::get(parent_bounty_id);
686			let remaining_parent_value = parent_value.saturating_sub(child_bounties_value);
687			ensure!(remaining_parent_value >= value, Error::<T, I>::InsufficientBountyValue);
688
689			// Get child-bounty ID.
690			let child_bounty_id = TotalChildBountiesPerParent::<T, I>::get(parent_bounty_id);
691
692			// Initiate funding payment
693			let payment_status = Self::do_process_funding_payment(
694				parent_bounty_id,
695				Some(child_bounty_id),
696				asset_kind,
697				value,
698				None,
699			)?;
700
701			let child_bounty = ChildBounty {
702				parent_bounty: parent_bounty_id,
703				value,
704				metadata,
705				status: BountyStatus::FundingAttempted {
706					curator: final_curator,
707					payment_status: payment_status.clone(),
708				},
709			};
710			ChildBounties::<T, I>::insert(parent_bounty_id, child_bounty_id, child_bounty);
711			T::Preimages::request(&metadata);
712
713			// Add child-bounty value to the cumulative value sum. To be
714			// subtracted from the parent bounty payout when awarding
715			// bounty.
716			ChildBountiesValuePerParent::<T, I>::mutate(parent_bounty_id, |children_value| {
717				*children_value = children_value.saturating_add(value)
718			});
719
720			// Increment the active child-bounty count.
721			ChildBountiesPerParent::<T, I>::mutate(parent_bounty_id, |count| {
722				count.saturating_inc()
723			});
724			TotalChildBountiesPerParent::<T, I>::insert(
725				parent_bounty_id,
726				child_bounty_id.saturating_add(1),
727			);
728
729			Self::deposit_event(Event::<T, I>::ChildBountyCreated {
730				index: parent_bounty_id,
731				child_index: child_bounty_id,
732			});
733
734			Ok(())
735		}
736
737		/// Propose a new curator for a child-/bounty after the previous was unassigned.
738		///
739		/// ## Dispatch Origin
740		///
741		/// Must be signed by `T::SpendOrigin` for a bounty, or by the parent bounty curator
742		/// for a child-bounty.
743		///
744		/// ## Details
745		///
746		/// - The child-/bounty must be in the `CuratorUnassigned` state.
747		/// - For a bounty, the `SpendOrigin` must have sufficient permissions to propose the
748		///   curator.
749		///
750		/// ### Parameters
751		/// - `parent_bounty_id`: Index of bounty.
752		/// - `child_bounty_id`: Index of child-bounty.
753		/// - `curator`: Account to be proposed as the curator.
754		///
755		/// ## Events
756		///
757		/// Emits [`Event::CuratorProposed`] if successful.
758		#[pallet::call_index(2)]
759		#[pallet::weight(match child_bounty_id {
760			None => <T as Config<I>>::WeightInfo::propose_curator_parent_bounty(),
761			Some(_) => <T as Config<I>>::WeightInfo::propose_curator_child_bounty(),
762		})]
763		pub fn propose_curator(
764			origin: OriginFor<T>,
765			#[pallet::compact] parent_bounty_id: BountyIndex,
766			child_bounty_id: Option<BountyIndex>,
767			curator: AccountIdLookupOf<T>,
768		) -> DispatchResult {
769			let maybe_sender = ensure_signed(origin.clone())
770				.map(Some)
771				.or_else(|_| T::SpendOrigin::ensure_origin(origin.clone()).map(|_| None))?;
772			let curator = T::Lookup::lookup(curator)?;
773
774			let (asset_kind, value, _, status, parent_curator) =
775				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
776			ensure!(status == BountyStatus::CuratorUnassigned, Error::<T, I>::UnexpectedStatus);
777
778			match child_bounty_id {
779				// Only `SpendOrigin` can propose curator for bounty
780				None => {
781					ensure!(maybe_sender.is_none(), BadOrigin);
782					let max_amount = T::SpendOrigin::ensure_origin(origin)?;
783					let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind)
784						.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
785					ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
786				},
787				// Only parent curator can propose curator for child-bounty
788				Some(_) => {
789					let parent_curator = parent_curator.ok_or(Error::<T, I>::UnexpectedStatus)?;
790					let sender = maybe_sender.ok_or(BadOrigin)?;
791					ensure!(sender == parent_curator, BadOrigin);
792				},
793			};
794
795			let new_status = BountyStatus::Funded { curator: curator.clone() };
796			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
797
798			Self::deposit_event(Event::<T, I>::CuratorProposed {
799				index: parent_bounty_id,
800				child_index: child_bounty_id,
801				curator,
802			});
803
804			Ok(())
805		}
806
807		/// Accept the curator role for a child-/bounty.
808		///
809		/// ## Dispatch Origin
810		///
811		/// Must be signed by the proposed curator.
812		///
813		/// ## Details
814		///
815		/// - The child-/bounty must be in the `Funded` state.
816		/// - The curator must accept the role by calling this function.
817		/// - A deposit will be reserved from the curator and refunded upon successful payout.
818		///
819		/// ### Parameters
820		/// - `parent_bounty_id`: Index of parent bounty.
821		/// - `child_bounty_id`: Index of child-bounty.
822		///
823		/// ## Events
824		///
825		/// Emits [`Event::BountyBecameActive`] if successful.
826		#[pallet::call_index(3)]
827		#[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
828		pub fn accept_curator(
829			origin: OriginFor<T>,
830			#[pallet::compact] parent_bounty_id: BountyIndex,
831			child_bounty_id: Option<BountyIndex>,
832		) -> DispatchResult {
833			let signer = ensure_signed(origin)?;
834
835			let (asset_kind, value, _, status, _) =
836				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
837
838			let BountyStatus::Funded { ref curator } = status else {
839				return Err(Error::<T, I>::UnexpectedStatus.into());
840			};
841			ensure!(signer == *curator, Error::<T, I>::RequireCurator);
842
843			let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind)
844				.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
845			let curator_deposit = T::Consideration::new(&curator, native_amount)?;
846			CuratorDeposit::<T, I>::insert(parent_bounty_id, child_bounty_id, curator_deposit);
847
848			let new_status = BountyStatus::Active { curator: curator.clone() };
849			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
850
851			Self::deposit_event(Event::<T, I>::BountyBecameActive {
852				index: parent_bounty_id,
853				child_index: child_bounty_id,
854				curator: signer,
855			});
856
857			Ok(())
858		}
859
860		/// Unassign curator from a child-/bounty.
861		///
862		/// ## Dispatch Origin
863		///
864		/// This function can only be called by the `RejectOrigin` or the child-/bounty curator.
865		///
866		/// ## Details
867		///
868		/// - If this function is called by the `RejectOrigin`, or by the parent curator in the case
869		///   of a child bounty, we assume that the curator is malicious or inactive. As a result,
870		///   we will slash the curator when possible.
871		/// - If the origin is the child-/bounty curator, we take this as a sign they are unable to
872		///   do their job and they willingly give up. We could slash them, but for now we allow
873		///   them to recover their deposit and exit without issue. (We may want to change this if
874		///   it is abused).
875		/// - If successful, the child-/bounty status is updated to `CuratorUnassigned`. To
876		///   reactivate the bounty, a new curator must be proposed and must accept the role.
877		///
878		/// ### Parameters
879		/// - `parent_bounty_id`: Index of parent bounty.
880		/// - `child_bounty_id`: Index of child-bounty.
881		///
882		/// ## Events
883		///
884		/// Emits [`Event::CuratorUnassigned`] if successful.
885		#[pallet::call_index(4)]
886		#[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
887		pub fn unassign_curator(
888			origin: OriginFor<T>,
889			#[pallet::compact] parent_bounty_id: BountyIndex,
890			child_bounty_id: Option<BountyIndex>,
891		) -> DispatchResult {
892			let maybe_sender = ensure_signed(origin.clone())
893				.map(Some)
894				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
895
896			let (_, _, _, status, parent_curator) =
897				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
898
899			match status {
900				BountyStatus::Funded { ref curator } => {
901					// A bounty curator has been proposed, but not accepted yet.
902					// Either `RejectOrigin`, parent bounty curator or the proposed
903					// curator can unassign the child-/bounty curator.
904					ensure!(
905						maybe_sender.map_or(true, |sender| {
906							sender == *curator ||
907								parent_curator
908									.map_or(false, |parent_curator| sender == parent_curator)
909						}),
910						BadOrigin
911					);
912				},
913				BountyStatus::Active { ref curator, .. } => {
914					let maybe_curator_deposit =
915						CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id);
916					// The child-/bounty is active.
917					match maybe_sender {
918						// If the `RejectOrigin` is calling this function, burn the curator deposit.
919						None => {
920							if let Some(curator_deposit) = maybe_curator_deposit {
921								T::Consideration::burn(curator_deposit, curator);
922							}
923							// Continue to change bounty status below...
924						},
925						Some(sender) if sender == *curator => {
926							if let Some(curator_deposit) = maybe_curator_deposit {
927								// This is the curator, willingly giving up their role. Free their
928								// deposit.
929								T::Consideration::drop(curator_deposit, curator)?;
930							}
931							// Continue to change bounty status below...
932						},
933						Some(sender) => {
934							if let Some(parent_curator) = parent_curator {
935								// If the parent curator is unassigning a child curator, that is not
936								// itself, burn the child curator deposit.
937								if sender == parent_curator && *curator != parent_curator {
938									if let Some(curator_deposit) = maybe_curator_deposit {
939										T::Consideration::burn(curator_deposit, curator);
940									}
941								} else {
942									return Err(BadOrigin.into());
943								}
944							}
945						},
946					}
947				},
948				_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
949			};
950
951			let new_status = BountyStatus::CuratorUnassigned;
952			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
953
954			Self::deposit_event(Event::<T, I>::CuratorUnassigned {
955				index: parent_bounty_id,
956				child_index: child_bounty_id,
957			});
958
959			Ok(())
960		}
961
962		/// Awards the child-/bounty to a beneficiary account/location,
963		/// initiating the payout payments to both the beneficiary and the curator.
964		///
965		/// ## Dispatch Origin
966		///
967		/// This function can only be called by the `RejectOrigin` or the child-/bounty curator.
968		///
969		/// ## Details
970		///
971		/// - The child-/bounty must be in the `Active` state.
972		/// - if awarding a parent bounty it must not have active or funded child bounties.
973		/// - Initiates payout payment from the child-/bounty to the beneficiary account/location.
974		/// - If successful the child-/bounty status is updated to `PayoutAttempted`.
975		/// - In case of a payout failure, the child-/bounty status must be updated with
976		/// `check_status` call before retrying with `retry_payment` call.
977		///
978		/// ### Parameters
979		/// - `parent_bounty_id`: Index of parent bounty.
980		/// - `child_bounty_id`: Index of child-bounty.
981		/// - `beneficiary`: Account/location to be awarded the child-/bounty.
982		///
983		/// ## Events
984		///
985		/// Emits [`Event::BountyAwarded`] and [`Event::Paid`] if successful.
986		#[pallet::call_index(5)]
987		#[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
988		pub fn award_bounty(
989			origin: OriginFor<T>,
990			#[pallet::compact] parent_bounty_id: BountyIndex,
991			child_bounty_id: Option<BountyIndex>,
992			beneficiary: BeneficiaryLookupOf<T, I>,
993		) -> DispatchResult {
994			let signer = ensure_signed(origin)?;
995			let beneficiary = T::BeneficiaryLookup::lookup(beneficiary)?;
996
997			let (asset_kind, value, _, status, _) =
998				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
999
1000			if child_bounty_id.is_none() {
1001				ensure!(
1002					ChildBountiesPerParent::<T, I>::get(parent_bounty_id) == 0,
1003					Error::<T, I>::HasActiveChildBounty
1004				);
1005			}
1006
1007			let BountyStatus::Active { ref curator } = status else {
1008				return Err(Error::<T, I>::UnexpectedStatus.into());
1009			};
1010			ensure!(signer == *curator, Error::<T, I>::RequireCurator);
1011
1012			let beneficiary_payment_status = Self::do_process_payout_payment(
1013				parent_bounty_id,
1014				child_bounty_id,
1015				asset_kind,
1016				value,
1017				beneficiary.clone(),
1018				None,
1019			)?;
1020
1021			let new_status = BountyStatus::PayoutAttempted {
1022				curator: curator.clone(),
1023				beneficiary: beneficiary.clone(),
1024				payment_status: beneficiary_payment_status.clone(),
1025			};
1026			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1027
1028			Self::deposit_event(Event::<T, I>::BountyAwarded {
1029				index: parent_bounty_id,
1030				child_index: child_bounty_id,
1031				beneficiary,
1032			});
1033
1034			Ok(())
1035		}
1036
1037		/// Cancel an active child-/bounty. A payment to send all the funds to the funding source is
1038		/// initialized.
1039		///
1040		/// ## Dispatch Origin
1041		///
1042		/// This function can only be called by the `RejectOrigin` or the parent bounty curator.
1043		///
1044		/// ## Details
1045		///
1046		/// - If the child-/bounty is in the `Funded` state, a refund payment is initiated.
1047		/// - If the child-/bounty is in the `Active` state, a refund payment is initiated and the
1048		///   child-/bounty status is updated with the curator account/location.
1049		/// - If the child-/bounty is in the funding or payout phase, it cannot be canceled.
1050		/// - In case of a refund failure, the child-/bounty status must be updated with the
1051		/// `check_status` call before retrying with `retry_payment` call.
1052		///
1053		/// ### Parameters
1054		/// - `parent_bounty_id`: Index of parent bounty.
1055		/// - `child_bounty_id`: Index of child-bounty.
1056		///
1057		/// ## Events
1058		///
1059		/// Emits [`Event::BountyCanceled`] and [`Event::Paid`] if successful.
1060		#[pallet::call_index(6)]
1061		#[pallet::weight(match child_bounty_id {
1062			None => <T as Config<I>>::WeightInfo::close_parent_bounty(),
1063			Some(_) => <T as Config<I>>::WeightInfo::close_child_bounty(),
1064		})]
1065		pub fn close_bounty(
1066			origin: OriginFor<T>,
1067			#[pallet::compact] parent_bounty_id: BountyIndex,
1068			child_bounty_id: Option<BountyIndex>,
1069		) -> DispatchResult {
1070			let maybe_sender = ensure_signed(origin.clone())
1071				.map(Some)
1072				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
1073
1074			let (asset_kind, value, _, status, parent_curator) =
1075				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1076
1077			let maybe_curator = match status {
1078				BountyStatus::Funded { curator } | BountyStatus::Active { curator, .. } => {
1079					Some(curator)
1080				},
1081				BountyStatus::CuratorUnassigned => None,
1082				_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1083			};
1084
1085			match child_bounty_id {
1086				None => {
1087					// Parent bounty can only be closed if it has no active child bounties.
1088					ensure!(
1089						ChildBountiesPerParent::<T, I>::get(parent_bounty_id) == 0,
1090						Error::<T, I>::HasActiveChildBounty
1091					);
1092					// Bounty can be closed by `RejectOrigin` or the curator.
1093					if let Some(sender) = maybe_sender.as_ref() {
1094						let is_curator =
1095							maybe_curator.as_ref().map_or(false, |curator| curator == sender);
1096						ensure!(is_curator, BadOrigin);
1097					}
1098				},
1099				Some(_) => {
1100					// Child-bounty can be closed by `RejectOrigin`, the curator or parent curator.
1101					if let Some(sender) = maybe_sender.as_ref() {
1102						let is_curator =
1103							maybe_curator.as_ref().map_or(false, |curator| curator == sender);
1104						let is_parent_curator = parent_curator
1105							.as_ref()
1106							.map_or(false, |parent_curator| parent_curator == sender);
1107						ensure!(is_curator || is_parent_curator, BadOrigin);
1108					}
1109				},
1110			};
1111
1112			let payment_status = Self::do_process_refund_payment(
1113				parent_bounty_id,
1114				child_bounty_id,
1115				asset_kind,
1116				value,
1117				None,
1118			)?;
1119			let new_status = BountyStatus::RefundAttempted {
1120				payment_status: payment_status.clone(),
1121				curator: maybe_curator.clone(),
1122			};
1123			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1124
1125			Self::deposit_event(Event::<T, I>::BountyCanceled {
1126				index: parent_bounty_id,
1127				child_index: child_bounty_id,
1128			});
1129
1130			Ok(())
1131		}
1132
1133		/// Check and update the payment status of a child-/bounty.
1134		///
1135		/// ## Dispatch Origin
1136		///
1137		/// Must be signed.
1138		///
1139		/// ## Details
1140		///
1141		/// - If the child-/bounty status is `FundingAttempted`, it checks if the funding payment
1142		///   has succeeded. If successful, the bounty status becomes `Funded`.
1143		/// - If the child-/bounty status is `RefundAttempted`, it checks if the refund payment has
1144		///   succeeded. If successful, the child-/bounty is removed from storage.
1145		/// - If the child-/bounty status is `PayoutAttempted`, it checks if the payout payment has
1146		///   succeeded. If successful, the child-/bounty is removed from storage.
1147		///
1148		/// ### Parameters
1149		/// - `parent_bounty_id`: Index of parent bounty.
1150		/// - `child_bounty_id`: Index of child-bounty.
1151		///
1152		/// ## Events
1153		///
1154		/// Emits [`Event::BountyBecameActive`] if the child/bounty status transitions to `Active`.
1155		/// Emits [`Event::BountyRefundProcessed`] if the refund payment has succeed.
1156		/// Emits [`Event::BountyPayoutProcessed`] if the payout payment has succeed.
1157		/// Emits [`Event::PaymentFailed`] if the funding, refund our payment payment has failed.
1158		#[pallet::call_index(7)]
1159		#[pallet::weight(<T as Config<I>>::WeightInfo::check_status_funding().max(
1160			<T as Config<I>>::WeightInfo::check_status_refund(),
1161		).max(<T as Config<I>>::WeightInfo::check_status_payout()))]
1162		pub fn check_status(
1163			origin: OriginFor<T>,
1164			#[pallet::compact] parent_bounty_id: BountyIndex,
1165			child_bounty_id: Option<BountyIndex>,
1166		) -> DispatchResultWithPostInfo {
1167			use BountyStatus::*;
1168
1169			ensure_signed(origin)?;
1170			let (asset_kind, value, metadata, status, parent_curator) =
1171				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1172
1173			let (new_status, weight) = match status {
1174				FundingAttempted { ref payment_status, curator } => {
1175					let new_payment_status = Self::do_check_funding_payment_status(
1176						parent_bounty_id,
1177						child_bounty_id,
1178						payment_status.clone(),
1179					)?;
1180
1181					let new_status = match new_payment_status {
1182						PaymentState::Succeeded => match (child_bounty_id, parent_curator) {
1183							(Some(_), Some(parent_curator)) if curator == parent_curator => {
1184								BountyStatus::Active { curator }
1185							},
1186							_ => BountyStatus::Funded { curator },
1187						},
1188						PaymentState::Pending |
1189						PaymentState::Failed |
1190						PaymentState::Attempted { .. } => BountyStatus::FundingAttempted {
1191							payment_status: new_payment_status,
1192							curator,
1193						},
1194					};
1195
1196					let weight = <T as Config<I>>::WeightInfo::check_status_funding();
1197
1198					(new_status, weight)
1199				},
1200				RefundAttempted { ref payment_status, ref curator } => {
1201					let new_payment_status = Self::do_check_refund_payment_status(
1202						parent_bounty_id,
1203						child_bounty_id,
1204						payment_status.clone(),
1205					)?;
1206
1207					let new_status = match new_payment_status {
1208						PaymentState::Succeeded => {
1209							if let Some(curator) = curator {
1210								// Drop the curator deposit when payment succeeds
1211								// If the parent curator is also the child curator, there
1212								// is no deposit
1213								if let Some(curator_deposit) =
1214									CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id)
1215								{
1216									T::Consideration::drop(curator_deposit, curator)?;
1217								}
1218							}
1219							if let Some(_) = child_bounty_id {
1220								// Revert the value back to parent bounty
1221								ChildBountiesValuePerParent::<T, I>::mutate(
1222									parent_bounty_id,
1223									|total_value| *total_value = total_value.saturating_sub(value),
1224								);
1225							}
1226							// refund succeeded, cleanup the bounty
1227							Self::remove_bounty(parent_bounty_id, child_bounty_id, metadata);
1228							return Ok(Pays::No.into());
1229						},
1230						PaymentState::Pending |
1231						PaymentState::Failed |
1232						PaymentState::Attempted { .. } => BountyStatus::RefundAttempted {
1233							payment_status: new_payment_status,
1234							curator: curator.clone(),
1235						},
1236					};
1237
1238					let weight = <T as Config<I>>::WeightInfo::check_status_refund();
1239
1240					(new_status, weight)
1241				},
1242				PayoutAttempted { ref curator, ref beneficiary, ref payment_status } => {
1243					let new_payment_status = Self::do_check_payout_payment_status(
1244						parent_bounty_id,
1245						child_bounty_id,
1246						asset_kind,
1247						value,
1248						beneficiary.clone(),
1249						payment_status.clone(),
1250					)?;
1251
1252					let new_status = match new_payment_status {
1253						PaymentState::Succeeded => {
1254							if let Some(curator_deposit) =
1255								CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id)
1256							{
1257								// Drop the curator deposit when both payments succeed
1258								// If the child curator is the parent curator, the
1259								// deposit is 0
1260								T::Consideration::drop(curator_deposit, curator)?;
1261							}
1262							// payout succeeded, cleanup the bounty
1263							Self::remove_bounty(parent_bounty_id, child_bounty_id, metadata);
1264							return Ok(Pays::No.into());
1265						},
1266						PaymentState::Pending |
1267						PaymentState::Failed |
1268						PaymentState::Attempted { .. } => BountyStatus::PayoutAttempted {
1269							curator: curator.clone(),
1270							beneficiary: beneficiary.clone(),
1271							payment_status: new_payment_status.clone(),
1272						},
1273					};
1274
1275					let weight = <T as Config<I>>::WeightInfo::check_status_payout();
1276
1277					(new_status, weight)
1278				},
1279				_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1280			};
1281
1282			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1283
1284			Ok(Some(weight).into())
1285		}
1286
1287		/// Retry the funding, refund or payout payments.
1288		///
1289		/// ## Dispatch Origin
1290		///
1291		/// Must be signed.
1292		///
1293		/// ## Details
1294		///
1295		/// - If the child-/bounty status is `FundingAttempted`, it retries the funding payment from
1296		///   funding source the child-/bounty account/location.
1297		/// - If the child-/bounty status is `RefundAttempted`, it retries the refund payment from
1298		///   the child-/bounty account/location to the funding source.
1299		/// - If the child-/bounty status is `PayoutAttempted`, it retries the payout payment from
1300		///   the child-/bounty account/location to the beneficiary account/location.
1301		///
1302		/// ### Parameters
1303		/// - `parent_bounty_id`: Index of parent bounty.
1304		/// - `child_bounty_id`: Index of child-bounty.
1305		///
1306		/// ## Events
1307		///
1308		/// Emits [`Event::Paid`] if the funding, refund or payout payment has initiated.
1309		#[pallet::call_index(8)]
1310		#[pallet::weight(<T as Config<I>>::WeightInfo::retry_payment_funding().max(
1311			<T as Config<I>>::WeightInfo::retry_payment_refund(),
1312		).max(<T as Config<I>>::WeightInfo::retry_payment_payout()))]
1313		pub fn retry_payment(
1314			origin: OriginFor<T>,
1315			#[pallet::compact] parent_bounty_id: BountyIndex,
1316			child_bounty_id: Option<BountyIndex>,
1317		) -> DispatchResultWithPostInfo {
1318			use BountyStatus::*;
1319
1320			ensure_signed(origin)?;
1321			let (asset_kind, value, _, status, _) =
1322				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1323
1324			let (new_status, weight) = match status {
1325				FundingAttempted { ref payment_status, ref curator } => {
1326					let new_payment_status = Self::do_process_funding_payment(
1327						parent_bounty_id,
1328						child_bounty_id,
1329						asset_kind,
1330						value,
1331						Some(payment_status.clone()),
1332					)?;
1333
1334					(
1335						FundingAttempted {
1336							payment_status: new_payment_status,
1337							curator: curator.clone(),
1338						},
1339						<T as Config<I>>::WeightInfo::retry_payment_funding(),
1340					)
1341				},
1342				RefundAttempted { ref curator, ref payment_status } => {
1343					let new_payment_status = Self::do_process_refund_payment(
1344						parent_bounty_id,
1345						child_bounty_id,
1346						asset_kind,
1347						value,
1348						Some(payment_status.clone()),
1349					)?;
1350					(
1351						RefundAttempted {
1352							curator: curator.clone(),
1353							payment_status: new_payment_status,
1354						},
1355						<T as Config<I>>::WeightInfo::retry_payment_refund(),
1356					)
1357				},
1358				PayoutAttempted { ref curator, ref beneficiary, ref payment_status } => {
1359					let new_payment_status = Self::do_process_payout_payment(
1360						parent_bounty_id,
1361						child_bounty_id,
1362						asset_kind,
1363						value,
1364						beneficiary.clone(),
1365						Some(payment_status.clone()),
1366					)?;
1367					(
1368						PayoutAttempted {
1369							curator: curator.clone(),
1370							beneficiary: beneficiary.clone(),
1371							payment_status: new_payment_status,
1372						},
1373						<T as Config<I>>::WeightInfo::retry_payment_payout(),
1374					)
1375				},
1376				_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1377			};
1378
1379			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1380
1381			Ok(Some(weight).into())
1382		}
1383	}
1384
1385	#[pallet::hooks]
1386	impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
1387		#[cfg(feature = "try-runtime")]
1388		fn try_state(_n: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1389			Self::do_try_state()
1390		}
1391	}
1392}
1393
1394#[cfg(any(feature = "try-runtime", test))]
1395impl<T: Config<I>, I: 'static> Pallet<T, I> {
1396	/// Ensure the correctness of the state of this pallet.
1397	///
1398	/// This should be valid before or after each state transition of this pallet.
1399	pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1400		Self::try_state_bounties_count()?;
1401
1402		for parent_bounty_id in Bounties::<T, I>::iter_keys() {
1403			Self::try_state_child_bounties_count(parent_bounty_id)?;
1404		}
1405
1406		Ok(())
1407	}
1408
1409	/// # Bounty Invariants
1410	///
1411	/// * `BountyCount` should be greater or equals to the length of the number of items in
1412	///   `Bounties`.
1413	fn try_state_bounties_count() -> Result<(), sp_runtime::TryRuntimeError> {
1414		let bounties_length = Bounties::<T, I>::iter().count() as u32;
1415
1416		ensure!(
1417			<BountyCount<T, I>>::get() >= bounties_length,
1418			"`BountyCount` must be grater or equals the number of `Bounties` in storage"
1419		);
1420
1421		Ok(())
1422	}
1423
1424	/// # Child-Bounty Invariants for a given parent bounty
1425	///
1426	/// * `ChildBountyCount` should be greater or equals to the length of the number of items in
1427	///   `ChildBounties`.
1428	fn try_state_child_bounties_count(
1429		parent_bounty_id: BountyIndex,
1430	) -> Result<(), sp_runtime::TryRuntimeError> {
1431		let child_bounties_length =
1432			ChildBounties::<T, I>::iter_prefix(parent_bounty_id).count() as u32;
1433
1434		ensure!(
1435			<ChildBountiesPerParent<T, I>>::get(parent_bounty_id) >= child_bounties_length,
1436			"`ChildBountiesPerParent` must be grater or equals the number of `ChildBounties` in storage"
1437		);
1438
1439		Ok(())
1440	}
1441}
1442
1443impl<T: Config<I>, I: 'static> Pallet<T, I> {
1444	/// The account/location of the funding source.
1445	pub fn funding_source_account(
1446		asset_kind: T::AssetKind,
1447	) -> Result<T::Beneficiary, DispatchError> {
1448		T::FundingSource::try_convert(asset_kind)
1449			.map_err(|_| Error::<T, I>::FailedToConvertSource.into())
1450	}
1451
1452	/// The account/location of a bounty.
1453	pub fn bounty_account(
1454		bounty_id: BountyIndex,
1455		asset_kind: T::AssetKind,
1456	) -> Result<T::Beneficiary, DispatchError> {
1457		T::BountySource::try_convert((bounty_id, asset_kind))
1458			.map_err(|_| Error::<T, I>::FailedToConvertSource.into())
1459	}
1460
1461	/// The account/location of a child-bounty.
1462	pub fn child_bounty_account(
1463		parent_bounty_id: BountyIndex,
1464		child_bounty_id: BountyIndex,
1465		asset_kind: T::AssetKind,
1466	) -> Result<T::Beneficiary, DispatchError> {
1467		T::ChildBountySource::try_convert((parent_bounty_id, child_bounty_id, asset_kind))
1468			.map_err(|_| Error::<T, I>::FailedToConvertSource.into())
1469	}
1470
1471	/// Returns the asset kind, value, status and parent curator (if parent bounty
1472	/// active) of a child-/bounty.
1473	///
1474	/// The asset kind derives from the parent bounty.
1475	pub fn get_bounty_details(
1476		parent_bounty_id: BountyIndex,
1477		child_bounty_id: Option<BountyIndex>,
1478	) -> Result<
1479		(
1480			T::AssetKind,
1481			T::Balance,
1482			T::Hash,
1483			BountyStatus<T::AccountId, PaymentIdOf<T, I>, T::Beneficiary>,
1484			Option<T::AccountId>,
1485		),
1486		DispatchError,
1487	> {
1488		let parent_bounty =
1489			Bounties::<T, I>::get(parent_bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1490
1491		// Ensures child-bounty uses parent curator only when parent bounty is active.
1492		let parent_curator = if let BountyStatus::Active { curator } = &parent_bounty.status {
1493			Some(curator.clone())
1494		} else {
1495			None
1496		};
1497
1498		match child_bounty_id {
1499			None => Ok((
1500				parent_bounty.asset_kind,
1501				parent_bounty.value,
1502				parent_bounty.metadata,
1503				parent_bounty.status,
1504				parent_curator,
1505			)),
1506			Some(child_bounty_id) => {
1507				let child_bounty = ChildBounties::<T, I>::get(parent_bounty_id, child_bounty_id)
1508					.ok_or(Error::<T, I>::InvalidIndex)?;
1509				Ok((
1510					parent_bounty.asset_kind,
1511					child_bounty.value,
1512					child_bounty.metadata,
1513					child_bounty.status,
1514					parent_curator,
1515				))
1516			},
1517		}
1518	}
1519
1520	/// Updates the status of a child-/bounty.
1521	pub fn update_bounty_status(
1522		parent_bounty_id: BountyIndex,
1523		child_bounty_id: Option<BountyIndex>,
1524		new_status: BountyStatus<T::AccountId, PaymentIdOf<T, I>, T::Beneficiary>,
1525	) -> Result<(), DispatchError> {
1526		match child_bounty_id {
1527			None => {
1528				let mut bounty =
1529					Bounties::<T, I>::get(parent_bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1530				bounty.status = new_status;
1531				Bounties::<T, I>::insert(parent_bounty_id, bounty);
1532			},
1533			Some(child_bounty_id) => {
1534				let mut bounty = ChildBounties::<T, I>::get(parent_bounty_id, child_bounty_id)
1535					.ok_or(Error::<T, I>::InvalidIndex)?;
1536				bounty.status = new_status;
1537				ChildBounties::<T, I>::insert(parent_bounty_id, child_bounty_id, bounty);
1538			},
1539		}
1540
1541		Ok(())
1542	}
1543
1544	/// Calculates amount the beneficiary receives during child-/bounty payout.
1545	fn calculate_payout(
1546		parent_bounty_id: BountyIndex,
1547		child_bounty_id: Option<BountyIndex>,
1548		value: T::Balance,
1549	) -> T::Balance {
1550		match child_bounty_id {
1551			None => {
1552				// Get total child bounties value, and subtract it from the parent
1553				// value.
1554				let children_value = ChildBountiesValuePerParent::<T, I>::take(parent_bounty_id);
1555				debug_assert!(children_value <= value);
1556				let payout = value.saturating_sub(children_value);
1557				payout
1558			},
1559			Some(_) => value,
1560		}
1561	}
1562
1563	/// Cleanup a child-/bounty from the storage.
1564	fn remove_bounty(
1565		parent_bounty_id: BountyIndex,
1566		child_bounty_id: Option<BountyIndex>,
1567		metadata: T::Hash,
1568	) {
1569		match child_bounty_id {
1570			None => {
1571				Bounties::<T, I>::remove(parent_bounty_id);
1572				ChildBountiesPerParent::<T, I>::remove(parent_bounty_id);
1573				TotalChildBountiesPerParent::<T, I>::remove(parent_bounty_id);
1574				debug_assert!(ChildBountiesValuePerParent::<T, I>::get(parent_bounty_id).is_zero());
1575			},
1576			Some(child_bounty_id) => {
1577				ChildBounties::<T, I>::remove(parent_bounty_id, child_bounty_id);
1578				ChildBountiesPerParent::<T, I>::mutate(parent_bounty_id, |count| {
1579					count.saturating_dec()
1580				});
1581			},
1582		}
1583
1584		T::Preimages::unrequest(&metadata);
1585	}
1586
1587	/// Initiates payment from the funding source to the child-/bounty account/location.
1588	fn do_process_funding_payment(
1589		parent_bounty_id: BountyIndex,
1590		child_bounty_id: Option<BountyIndex>,
1591		asset_kind: T::AssetKind,
1592		value: T::Balance,
1593		maybe_payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1594	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1595		if let Some(payment_status) = maybe_payment_status {
1596			ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1597		}
1598
1599		let (source, beneficiary) = match child_bounty_id {
1600			None => (
1601				Self::funding_source_account(asset_kind.clone())?,
1602				Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1603			),
1604			Some(child_bounty_id) => (
1605				Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1606				Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?,
1607			),
1608		};
1609
1610		let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, value)
1611			.map_err(|_| Error::<T, I>::FundingError)?;
1612
1613		Self::deposit_event(Event::<T, I>::Paid {
1614			index: parent_bounty_id,
1615			child_index: child_bounty_id,
1616			payment_id: id,
1617		});
1618
1619		Ok(PaymentState::Attempted { id })
1620	}
1621
1622	/// Queries the status of the payment from the funding source to the child-/bounty
1623	/// account/location and returns a new payment status.
1624	fn do_check_funding_payment_status(
1625		parent_bounty_id: BountyIndex,
1626		child_bounty_id: Option<BountyIndex>,
1627		payment_status: PaymentState<PaymentIdOf<T, I>>,
1628	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1629		let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1630
1631		match <T as Config<I>>::Paymaster::check_payment(payment_id) {
1632			PaymentStatus::Success => {
1633				Self::deposit_event(Event::<T, I>::BountyFundingProcessed {
1634					index: parent_bounty_id,
1635					child_index: child_bounty_id,
1636				});
1637				Ok(PaymentState::Succeeded)
1638			},
1639			PaymentStatus::InProgress | PaymentStatus::Unknown => {
1640				return Err(Error::<T, I>::FundingInconclusive.into())
1641			},
1642			PaymentStatus::Failure => {
1643				Self::deposit_event(Event::<T, I>::PaymentFailed {
1644					index: parent_bounty_id,
1645					child_index: child_bounty_id,
1646					payment_id,
1647				});
1648				return Ok(PaymentState::Failed);
1649			},
1650		}
1651	}
1652
1653	/// Initializes payment from the child-/bounty account/location to the funding source (i.e.
1654	/// treasury pot, parent bounty).
1655	fn do_process_refund_payment(
1656		parent_bounty_id: BountyIndex,
1657		child_bounty_id: Option<BountyIndex>,
1658		asset_kind: T::AssetKind,
1659		value: T::Balance,
1660		payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1661	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1662		if let Some(payment_status) = payment_status {
1663			ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1664		}
1665
1666		let (source, beneficiary) = match child_bounty_id {
1667			None => (
1668				Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1669				Self::funding_source_account(asset_kind.clone())?,
1670			),
1671			Some(child_bounty_id) => (
1672				Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?,
1673				Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1674			),
1675		};
1676
1677		let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, value)
1678			.map_err(|_| Error::<T, I>::RefundError)?;
1679
1680		Self::deposit_event(Event::<T, I>::Paid {
1681			index: parent_bounty_id,
1682			child_index: child_bounty_id,
1683			payment_id: id,
1684		});
1685
1686		Ok(PaymentState::Attempted { id })
1687	}
1688
1689	/// Queries the status of the refund payment from the child-/bounty account/location to the
1690	/// funding source and returns a new payment status.
1691	fn do_check_refund_payment_status(
1692		parent_bounty_id: BountyIndex,
1693		child_bounty_id: Option<BountyIndex>,
1694		payment_status: PaymentState<PaymentIdOf<T, I>>,
1695	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1696		let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1697
1698		match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1699			PaymentStatus::Success => {
1700				Self::deposit_event(Event::<T, I>::BountyRefundProcessed {
1701					index: parent_bounty_id,
1702					child_index: child_bounty_id,
1703				});
1704				Ok(PaymentState::Succeeded)
1705			},
1706			PaymentStatus::InProgress | PaymentStatus::Unknown =>
1707			// nothing new to report
1708			{
1709				Err(Error::<T, I>::RefundInconclusive.into())
1710			},
1711			PaymentStatus::Failure => {
1712				// assume payment has failed, allow user to retry
1713				Self::deposit_event(Event::<T, I>::PaymentFailed {
1714					index: parent_bounty_id,
1715					child_index: child_bounty_id,
1716					payment_id,
1717				});
1718				Ok(PaymentState::Failed)
1719			},
1720		}
1721	}
1722
1723	/// Initializes payment from the child-/bounty to the beneficiary account/location.
1724	fn do_process_payout_payment(
1725		parent_bounty_id: BountyIndex,
1726		child_bounty_id: Option<BountyIndex>,
1727		asset_kind: T::AssetKind,
1728		value: T::Balance,
1729		beneficiary: T::Beneficiary,
1730		payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1731	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1732		if let Some(payment_status) = payment_status {
1733			ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1734		}
1735
1736		let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1737
1738		let source = match child_bounty_id {
1739			None => Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1740			Some(child_bounty_id) => {
1741				Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?
1742			},
1743		};
1744
1745		let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, payout)
1746			.map_err(|_| Error::<T, I>::PayoutError)?;
1747
1748		Self::deposit_event(Event::<T, I>::Paid {
1749			index: parent_bounty_id,
1750			child_index: child_bounty_id,
1751			payment_id: id,
1752		});
1753
1754		Ok(PaymentState::Attempted { id })
1755	}
1756
1757	/// Queries the status of the payment from the child-/bounty to the beneficiary account/location
1758	/// and returns a new payment status.
1759	fn do_check_payout_payment_status(
1760		parent_bounty_id: BountyIndex,
1761		child_bounty_id: Option<BountyIndex>,
1762		asset_kind: T::AssetKind,
1763		value: T::Balance,
1764		beneficiary: T::Beneficiary,
1765		payment_status: PaymentState<PaymentIdOf<T, I>>,
1766	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1767		let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1768
1769		match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1770			PaymentStatus::Success => {
1771				let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1772
1773				Self::deposit_event(Event::<T, I>::BountyPayoutProcessed {
1774					index: parent_bounty_id,
1775					child_index: child_bounty_id,
1776					asset_kind: asset_kind.clone(),
1777					value: payout,
1778					beneficiary,
1779				});
1780
1781				Ok(PaymentState::Succeeded)
1782			},
1783			PaymentStatus::InProgress | PaymentStatus::Unknown =>
1784			// nothing new to report
1785			{
1786				Err(Error::<T, I>::PayoutInconclusive.into())
1787			},
1788			PaymentStatus::Failure => {
1789				// assume payment has failed, allow user to retry
1790				Self::deposit_event(Event::<T, I>::PaymentFailed {
1791					index: parent_bounty_id,
1792					child_index: child_bounty_id,
1793					payment_id,
1794				});
1795				Ok(PaymentState::Failed)
1796			},
1797		}
1798	}
1799}
1800
1801/// Type implementing curator deposit as a percentage of the child-/bounty value.
1802///
1803/// It implements `Convert` trait and can be used with types like `HoldConsideration` implementing
1804/// `Consideration` trait.
1805pub struct CuratorDepositAmount<Mult, Min, Max, Balance>(PhantomData<(Mult, Min, Max, Balance)>);
1806impl<Mult, Min, Max, Balance> Convert<Balance, Balance>
1807	for CuratorDepositAmount<Mult, Min, Max, Balance>
1808where
1809	Balance: frame_support::traits::tokens::Balance,
1810	Min: Get<Option<Balance>>,
1811	Max: Get<Option<Balance>>,
1812	Mult: Get<Permill>,
1813{
1814	fn convert(value: Balance) -> Balance {
1815		let mut deposit = Mult::get().mul_floor(value);
1816
1817		if let Some(min) = Min::get() {
1818			if deposit < min {
1819				deposit = min;
1820			}
1821		}
1822
1823		if let Some(max) = Max::get() {
1824			if deposit > max {
1825				deposit = max;
1826			}
1827		}
1828
1829		deposit
1830	}
1831}
1832
1833/// Derives the funding `AccountId` from the `PalletId` and converts it into the
1834/// bounty `Beneficiary`, used as the source of bounty funds.
1835///
1836/// Used when the [`PalletId`] itself owns the funds (i.e. pallet-treasury id).
1837/// # Type Parameters
1838/// - `Id`: The pallet ID getter
1839/// - `T`: The pallet configuration
1840/// - `C`: Converter from `T::AccountId` to `T::Beneficiary`. Use `Identity` when types are the
1841///   same.
1842/// - `I`: Instance parameter (default: `()`)
1843pub struct PalletIdAsFundingSource<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1844impl<Id, T, C, I> TryConvert<T::AssetKind, T::Beneficiary> for PalletIdAsFundingSource<Id, T, C, I>
1845where
1846	Id: Get<PalletId>,
1847	T: crate::Config<I>,
1848	C: Convert<T::AccountId, T::Beneficiary>,
1849{
1850	fn try_convert(_asset_kind: T::AssetKind) -> Result<T::Beneficiary, T::AssetKind> {
1851		let account: T::AccountId = Id::get().into_account_truncating();
1852		Ok(C::convert(account))
1853	}
1854}
1855
1856/// Derives a bounty `AccountId` from the `PalletId` and the `BountyIndex`,
1857/// then converts it into the corresponding bounty `Beneficiary`.
1858///
1859/// Used when the [`PalletId`] itself owns the funds (i.e. pallet-treasury id).
1860/// # Type Parameters
1861/// - `Id`: The pallet ID getter
1862/// - `T`: The pallet configuration
1863/// - `C`: Converter from `T::AccountId` to `T::Beneficiary`. Use `Identity` when types are the
1864///   same.
1865/// - `I`: Instance parameter (default: `()`)
1866pub struct BountySourceFromPalletId<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1867impl<Id, T, C, I> TryConvert<(BountyIndex, T::AssetKind), T::Beneficiary>
1868	for BountySourceFromPalletId<Id, T, C, I>
1869where
1870	Id: Get<PalletId>,
1871	T: crate::Config<I>,
1872	C: Convert<T::AccountId, T::Beneficiary>,
1873{
1874	fn try_convert(
1875		(parent_bounty_id, _asset_kind): (BountyIndex, T::AssetKind),
1876	) -> Result<T::Beneficiary, (BountyIndex, T::AssetKind)> {
1877		let account: T::AccountId = Id::get().into_sub_account_truncating(("bt", parent_bounty_id));
1878		Ok(C::convert(account))
1879	}
1880}
1881
1882/// Derives a child-bounty `AccountId` from the `PalletId`, the parent index,
1883/// and the child index, then converts it into the child-bounty `Beneficiary`.
1884///
1885/// Used when the [`PalletId`] itself owns the funds (i.e. pallet-treasury id).
1886/// # Type Parameters
1887/// - `Id`: The pallet ID getter
1888/// - `T`: The pallet configuration
1889/// - `C`: Converter from `T::AccountId` to `T::Beneficiary`. Use `Identity` when types are the
1890///   same.
1891/// - `I`: Instance parameter (default: `()`)
1892pub struct ChildBountySourceFromPalletId<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1893impl<Id, T, C, I> TryConvert<(BountyIndex, BountyIndex, T::AssetKind), T::Beneficiary>
1894	for ChildBountySourceFromPalletId<Id, T, C, I>
1895where
1896	Id: Get<PalletId>,
1897	T: crate::Config<I>,
1898	C: Convert<T::AccountId, T::Beneficiary>,
1899{
1900	fn try_convert(
1901		(parent_bounty_id, child_bounty_id, _asset_kind): (BountyIndex, BountyIndex, T::AssetKind),
1902	) -> Result<T::Beneficiary, (BountyIndex, BountyIndex, T::AssetKind)> {
1903		// The prefix is changed to have different AccountId when the index of
1904		// parent and child is same.
1905		let account: T::AccountId =
1906			Id::get().into_sub_account_truncating(("cb", parent_bounty_id, child_bounty_id));
1907		Ok(C::convert(account))
1908	}
1909}