referrerpolicy=no-referrer-when-downgrade

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//!
62//! 2. Award a bounty to a beneficiary.
63#![doc = docify::embed!("src/tests.rs", award_bounty_works)]
64//!
65//! ## Pallet API
66//!
67//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
68//! including its configuration trait, dispatchables, storage items, events and errors.
69
70#![cfg_attr(not(feature = "std"), no_std)]
71
72mod benchmarking;
73mod mock;
74mod tests;
75pub mod weights;
76#[cfg(feature = "runtime-benchmarks")]
77pub use benchmarking::ArgumentsFactory;
78pub use pallet::*;
79pub use weights::WeightInfo;
80
81extern crate alloc;
82use alloc::{boxed::Box, collections::btree_map::BTreeMap};
83use frame_support::{
84	dispatch::{DispatchResult, DispatchResultWithPostInfo},
85	dispatch_context::with_context,
86	pallet_prelude::*,
87	traits::{
88		tokens::{
89			Balance, ConversionFromAssetBalance, ConversionToAssetBalance, PayWithSource,
90			PaymentStatus,
91		},
92		Consideration, EnsureOrigin, Get, QueryPreimage, StorePreimage,
93	},
94	PalletId,
95};
96use frame_system::pallet_prelude::{
97	ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
98};
99use scale_info::TypeInfo;
100use sp_runtime::{
101	traits::{AccountIdConversion, BadOrigin, Convert, Saturating, StaticLookup, TryConvert, Zero},
102	Debug, Permill,
103};
104
105/// Lookup type for beneficiary addresses.
106pub type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source;
107/// An index of a bounty. Just a `u32`.
108pub type BountyIndex = u32;
109/// Lookup type for account addresses.
110pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
111/// The payment identifier type used by the [`Config::Paymaster`].
112pub type PaymentIdOf<T, I = ()> = <<T as crate::Config<I>>::Paymaster as PayWithSource>::Id;
113/// Convenience alias for `Bounty`.
114pub type BountyOf<T, I> = Bounty<
115	<T as frame_system::Config>::AccountId,
116	<T as Config<I>>::Balance,
117	<T as Config<I>>::AssetKind,
118	<T as frame_system::Config>::Hash,
119	PaymentIdOf<T, I>,
120	<T as Config<I>>::Beneficiary,
121>;
122/// Convenience alias for `ChildBounty`.
123pub type ChildBountyOf<T, I> = ChildBounty<
124	<T as frame_system::Config>::AccountId,
125	<T as Config<I>>::Balance,
126	<T as frame_system::Config>::Hash,
127	PaymentIdOf<T, I>,
128	<T as Config<I>>::Beneficiary,
129>;
130
131/// A funded bounty.
132#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
133pub struct Bounty<AccountId, Balance, AssetKind, Hash, PaymentId, Beneficiary> {
134	/// The kind of asset this bounty is rewarded in.
135	pub asset_kind: AssetKind,
136	/// The amount that should be paid if the bounty is rewarded, including
137	/// beneficiary payout and possible child bounties.
138	///
139	/// The asset class determined by `asset_kind`.
140	pub value: Balance,
141	/// The metadata concerning the bounty.
142	///
143	/// The `Hash` refers to the preimage of the `Preimages` provider which can be a JSON
144	/// dump or IPFS hash of a JSON file.
145	pub metadata: Hash,
146	/// The status of this bounty.
147	pub status: BountyStatus<AccountId, PaymentId, Beneficiary>,
148}
149
150/// A funded child-bounty.
151#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
152pub struct ChildBounty<AccountId, Balance, Hash, PaymentId, Beneficiary> {
153	/// The parent bounty index of this child-bounty.
154	pub parent_bounty: BountyIndex,
155	/// The amount that should be paid if the child-bounty is rewarded.
156	///
157	/// The asset class determined by the parent bounty `asset_kind`.
158	pub value: Balance,
159	/// The metadata concerning the child-bounty.
160	///
161	/// The `Hash` refers to the preimage of the `Preimages` provider which can be a JSON
162	/// dump or IPFS hash of a JSON file.
163	pub metadata: Hash,
164	/// The status of this child-bounty.
165	pub status: BountyStatus<AccountId, PaymentId, Beneficiary>,
166}
167
168/// The status of a child-/bounty proposal.
169#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
170pub enum BountyStatus<AccountId, PaymentId, Beneficiary> {
171	/// The child-/bounty funding has been attempted and is waiting to confirm the funds
172	/// allocation.
173	///
174	/// Call `check_status` to confirm whether the funding payment succeeded. If successful, the
175	/// child-/bounty transitions to [`BountyStatus::Funded`]. Otherwise, use `retry_payment` to
176	/// reinitiate the funding payment.
177	FundingAttempted {
178		/// The proposed curator of this child-/bounty.
179		curator: AccountId,
180		/// The funding payment status from the source (e.g. Treasury, parent bounty) to
181		/// the child-/bounty account/location.
182		payment_status: PaymentState<PaymentId>,
183	},
184	/// The child-/bounty is funded and waiting for curator to accept role.
185	Funded {
186		/// The proposed curator of this child-/bounty.
187		curator: AccountId,
188	},
189	/// The child-/bounty previously assigned curator has been unassigned.
190	///
191	/// It remains funded and is waiting for a curator proposal.
192	CuratorUnassigned,
193	/// The child-/bounty is active and waiting to be awarded.
194	///
195	/// During the `Active` state, the curator can call `fund_child_bounty` to create multiple
196	/// child bounties.
197	Active {
198		/// The curator of this child-/bounty.
199		curator: AccountId,
200	},
201	/// The child-/bounty is closed, and the funds are being refunded to the original source (e.g.,
202	/// Treasury). Once `check_status` confirms the payment succeeded, the child-/bounty is
203	/// finalized and removed from storage. Otherwise, use `retry_payment` to reinitiate the refund
204	/// payment.
205	RefundAttempted {
206		/// The curator of this child-/bounty.
207		///
208		/// If `None`, it means the child-/bounty curator was unassigned.
209		curator: Option<AccountId>,
210		/// The refund payment status from the child-/bounty account/location to the source (e.g.
211		/// Treasury, parent bounty).
212		payment_status: PaymentState<PaymentId>,
213	},
214	/// The child-/bounty payout to a beneficiary has been attempted.
215	///
216	/// Call `check_status` to confirm whether the payout payment succeeded. If successful, the
217	/// child-/bounty is finalized and removed from storage. Otherwise, use `retry_payment` to
218	/// reinitiate the payout payment.
219	PayoutAttempted {
220		/// The curator of this child-/bounty.
221		curator: AccountId,
222		/// The beneficiary stash account/location.
223		beneficiary: Beneficiary,
224		/// The payout payment status from the child-/bounty account/location to the beneficiary.
225		payment_status: PaymentState<PaymentId>,
226	},
227}
228
229/// The state of a single payment.
230///
231/// When a payment is initiated via `Paymaster::pay`, it begins in the `Pending` state. The
232/// `check_status` call updates the payment state and advances the child-/bounty status. The
233/// `retry_payment` call can be used to reattempt payments in either `Pending` or `Failed` states.
234#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo)]
235pub enum PaymentState<Id> {
236	/// Pending claim.
237	Pending,
238	/// Payment attempted with a payment identifier.
239	Attempted { id: Id },
240	/// Payment failed.
241	Failed,
242	/// Payment succeeded.
243	Succeeded,
244}
245impl<Id: Clone> PaymentState<Id> {
246	/// Used to check if payment can be retried.
247	pub fn is_pending_or_failed(&self) -> bool {
248		matches!(self, PaymentState::Pending | PaymentState::Failed)
249	}
250
251	/// If a payment has been initiated, returns its identifier, which is used to check its
252	/// status.
253	pub fn get_attempt_id(&self) -> Option<Id> {
254		match self {
255			PaymentState::Attempted { id } => Some(id.clone()),
256			_ => None,
257		}
258	}
259}
260
261#[frame_support::pallet]
262pub mod pallet {
263	use super::*;
264
265	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
266
267	#[pallet::pallet]
268	#[pallet::storage_version(STORAGE_VERSION)]
269	pub struct Pallet<T, I = ()>(_);
270
271	#[pallet::config]
272	pub trait Config<I: 'static = ()>: frame_system::Config {
273		/// The type in which the assets are measured.
274		type Balance: Balance;
275
276		/// Origin from which bounties rejections must come.
277		type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
278
279		/// The origin required for funding the bounty. The `Success` value is the maximum amount in
280		/// a native asset that this origin is allowed to spend at a time.
281		type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::Balance>;
282
283		/// Type parameter representing the asset kinds used to fund, refund and spend from
284		/// bounties.
285		type AssetKind: Parameter + MaxEncodedLen;
286
287		/// Type parameter used to identify the beneficiaries eligible to receive payments.
288		type Beneficiary: Parameter + MaxEncodedLen;
289
290		/// Converting trait to take a source type and convert to [`Self::Beneficiary`].
291		type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
292
293		/// Minimum value for a bounty.
294		#[pallet::constant]
295		type BountyValueMinimum: Get<Self::Balance>;
296
297		/// Minimum value for a child-bounty.
298		#[pallet::constant]
299		type ChildBountyValueMinimum: Get<Self::Balance>;
300
301		/// Maximum number of child bounties that can be added to a parent bounty.
302		#[pallet::constant]
303		type MaxActiveChildBountyCount: Get<u32>;
304
305		/// Weight information for extrinsics in this pallet.
306		type WeightInfo: WeightInfo;
307
308		/// Converts an `AssetKind` into the funding source account/location.
309		///
310		/// Used when initiating funding and refund payments to and from a bounty.
311		type FundingSource: TryConvert<
312			Self::AssetKind,
313			<<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
314		>;
315
316		/// Converts a bounty index and `AssetKind` into its funding source account/location.
317		///
318		/// Used when initiating the funding, refund, and payout payments to and from a bounty.
319		type BountySource: TryConvert<
320			(BountyIndex, Self::AssetKind),
321			<<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
322		>;
323
324		/// Converts a parent bounty index, child bounty index, and `AssetKind` into the
325		/// child-bounty account/location.
326		///
327		/// Used when initiating the funding, refund, and payout payments to and from a
328		/// child-bounty.
329		type ChildBountySource: TryConvert<
330			(BountyIndex, BountyIndex, Self::AssetKind),
331			<<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
332		>;
333
334		/// Type for processing payments of [`Self::AssetKind`] from a `Source` in favor of
335		/// [`Self::Beneficiary`].
336		type Paymaster: PayWithSource<
337			Balance = Self::Balance,
338			Source = Self::Beneficiary,
339			Beneficiary = Self::Beneficiary,
340			AssetKind = Self::AssetKind,
341		>;
342
343		/// Type for converting the balance of an [`Self::AssetKind`] to the balance of the native
344		/// asset, solely for the purpose of asserting the result against the maximum allowed spend
345		/// amount of the [`Self::SpendOrigin`].
346		///
347		/// The conversion from the native asset balance to the balance of an [`Self::AssetKind`] is
348		/// used in benchmarks to convert [`Self::BountyValueMinimum`] to the asset kind amount.
349		type BalanceConverter: ConversionFromAssetBalance<Self::Balance, Self::AssetKind, Self::Balance>
350			+ ConversionToAssetBalance<Self::Balance, Self::AssetKind, Self::Balance>;
351
352		/// The preimage provider used for child-/bounty metadata.
353		type Preimages: QueryPreimage<H = Self::Hashing> + StorePreimage;
354
355		/// Means of associating a cost with committing to the curator role, which is incurred by
356		/// the child-/bounty curator.
357		///
358		/// The footprint accounts for the child-/bounty value converted to the native balance
359		/// type (using [`Self::BalanceConverter`]). The native balance type corresponds to the
360		/// `Success` type returned by [`Self::SpendOrigin`], which represents the maximum
361		/// spendable amount. The bounty amount must be converted with [`Self::BalanceConverter`]
362		/// before comparison against this maximum. The cost taken from the curator `AccountId`
363		/// may vary based on this converted balance.
364		type Consideration: Consideration<Self::AccountId, Self::Balance>;
365
366		/// Helper type for benchmarks.
367		#[cfg(feature = "runtime-benchmarks")]
368		type BenchmarkHelper: benchmarking::ArgumentsFactory<
369			Self::AssetKind,
370			Self::Beneficiary,
371			Self::Balance,
372		>;
373	}
374
375	#[pallet::error]
376	pub enum Error<T, I = ()> {
377		/// No child-/bounty at that index.
378		InvalidIndex,
379		/// The reason given is just too big.
380		ReasonTooBig,
381		/// Invalid child-/bounty value.
382		InvalidValue,
383		/// The balance of the asset kind is not convertible to the balance of the native asset for
384		/// asserting the origin permissions.
385		FailedToConvertBalance,
386		/// The child-/bounty status is unexpected.
387		UnexpectedStatus,
388		/// Require child-/bounty curator.
389		RequireCurator,
390		/// The spend origin is valid but the amount it is allowed to spend is lower than the
391		/// requested amount.
392		InsufficientPermission,
393		/// There was issue with funding the child-/bounty.
394		FundingError,
395		/// There was issue with refunding the child-/bounty.
396		RefundError,
397		// There was issue paying out the child-/bounty.
398		PayoutError,
399		/// Child-/bounty funding has not concluded yet.
400		FundingInconclusive,
401		/// Child-/bounty refund has not concluded yet.
402		RefundInconclusive,
403		/// Child-/bounty payout has not concluded yet.
404		PayoutInconclusive,
405		/// The child-/bounty or funding source account could not be derived from the indexes and
406		/// asset kind.
407		FailedToConvertSource,
408		/// The parent bounty cannot be closed because it has active child bounties.
409		HasActiveChildBounty,
410		/// Number of child bounties exceeds limit `MaxActiveChildBountyCount`.
411		TooManyChildBounties,
412		/// The parent bounty value is not enough to add new child-bounty.
413		InsufficientBountyValue,
414		/// The preimage does not exist.
415		PreimageNotExist,
416	}
417
418	#[pallet::event]
419	#[pallet::generate_deposit(pub(super) fn deposit_event)]
420	pub enum Event<T: Config<I>, I: 'static = ()> {
421		/// A new bounty was created and funding has been initiated.
422		BountyCreated { index: BountyIndex },
423		/// A new child-bounty was created and funding has been initiated.
424		ChildBountyCreated { index: BountyIndex, child_index: BountyIndex },
425		/// The curator accepted role and child-/bounty became active.
426		BountyBecameActive {
427			index: BountyIndex,
428			child_index: Option<BountyIndex>,
429			curator: T::AccountId,
430		},
431		/// A child-/bounty was awarded to a beneficiary.
432		BountyAwarded {
433			index: BountyIndex,
434			child_index: Option<BountyIndex>,
435			beneficiary: T::Beneficiary,
436		},
437		/// Payout payment to the beneficiary has concluded successfully.
438		BountyPayoutProcessed {
439			index: BountyIndex,
440			child_index: Option<BountyIndex>,
441			asset_kind: T::AssetKind,
442			value: T::Balance,
443			beneficiary: T::Beneficiary,
444		},
445		/// Funding payment has concluded successfully.
446		BountyFundingProcessed { index: BountyIndex, child_index: Option<BountyIndex> },
447		/// Refund payment has concluded successfully.
448		BountyRefundProcessed { index: BountyIndex, child_index: Option<BountyIndex> },
449		/// A child-/bounty was cancelled.
450		BountyCanceled { index: BountyIndex, child_index: Option<BountyIndex> },
451		/// A child-/bounty curator was unassigned.
452		CuratorUnassigned { index: BountyIndex, child_index: Option<BountyIndex> },
453		/// A child-/bounty curator was proposed.
454		CuratorProposed {
455			index: BountyIndex,
456			child_index: Option<BountyIndex>,
457			curator: T::AccountId,
458		},
459		/// A payment failed and can be retried.
460		PaymentFailed {
461			index: BountyIndex,
462			child_index: Option<BountyIndex>,
463			payment_id: PaymentIdOf<T, I>,
464		},
465		/// A payment happened and can be checked.
466		Paid { index: BountyIndex, child_index: Option<BountyIndex>, payment_id: PaymentIdOf<T, I> },
467	}
468
469	/// A reason for this pallet placing a hold on funds.
470	#[pallet::composite_enum]
471	pub enum HoldReason<I: 'static = ()> {
472		/// The funds are held as deposit for the curator commitment to a bounty.
473		#[codec(index = 0)]
474		CuratorDeposit,
475	}
476
477	/// Number of bounty proposals that have been made.
478	#[pallet::storage]
479	pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
480
481	/// Bounties that have been made.
482	#[pallet::storage]
483	pub type Bounties<T: Config<I>, I: 'static = ()> =
484		StorageMap<_, Twox64Concat, BountyIndex, BountyOf<T, I>>;
485
486	/// Child bounties that have been added.
487	///
488	/// Indexed by `(parent_bounty_id, child_bounty_id)`.
489	#[pallet::storage]
490	pub type ChildBounties<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
491		_,
492		Twox64Concat,
493		BountyIndex,
494		Twox64Concat,
495		BountyIndex,
496		ChildBountyOf<T, I>,
497	>;
498
499	/// Number of active child bounties per parent bounty.
500	///
501	/// Indexed by `parent_bounty_id`.
502	#[pallet::storage]
503	pub type ChildBountiesPerParent<T: Config<I>, I: 'static = ()> =
504		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
505
506	/// Number of total child bounties per parent bounty, including completed bounties.
507	///
508	/// Indexed by `parent_bounty_id`.
509	#[pallet::storage]
510	pub type TotalChildBountiesPerParent<T: Config<I>, I: 'static = ()> =
511		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
512
513	/// The cumulative child-bounty value for each parent bounty. To be subtracted from the parent
514	/// bounty payout when awarding bounty.
515	///
516	/// Indexed by `parent_bounty_id`.
517	#[pallet::storage]
518	pub type ChildBountiesValuePerParent<T: Config<I>, I: 'static = ()> =
519		StorageMap<_, Twox64Concat, BountyIndex, T::Balance, ValueQuery>;
520
521	/// The consideration cost incurred by the child-/bounty curator for committing to the role.
522	///
523	/// Determined by [`pallet::Config::Consideration`]. It is created when the curator accepts the
524	/// role, and is either burned if the curator misbehaves or consumed upon successful
525	/// completion of the child-/bounty.
526	///
527	/// Note: If the parent curator is also assigned to the child-bounty,  
528	/// the consideration cost is charged only once — when the curator  
529	/// accepts the role for the parent bounty.
530	///
531	/// Indexed by `(parent_bounty_id, child_bounty_id)`.
532	#[pallet::storage]
533	pub type CuratorDeposit<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
534		_,
535		Twox64Concat,
536		BountyIndex,
537		Twox64Concat,
538		Option<BountyIndex>,
539		T::Consideration,
540	>;
541
542	/// Temporarily tracks spending limits within the current context to prevent overspending.
543	#[derive(Default)]
544	pub struct SpendContext<Balance> {
545		pub spend_in_context: BTreeMap<Balance, Balance>,
546	}
547
548	#[pallet::call]
549	impl<T: Config<I>, I: 'static> Pallet<T, I> {
550		/// Fund a new bounty with a proposed curator, initiating the payment from the
551		/// funding source to the bounty account/location.
552		///
553		/// ## Dispatch Origin
554		///
555		/// Must be [`Config::SpendOrigin`] with the `Success` value being at least
556		/// the bounty value converted to native balance using [`Config::BalanceConverter`].
557		/// The converted native amount is validated against the maximum spendable amount
558		/// returned by [`Config::SpendOrigin`].
559		///
560		/// ## Details
561		///
562		/// - The `SpendOrigin` must have sufficient permissions to fund the bounty.
563		/// - The bounty `value` (in asset balance) is converted to native balance for validation.
564		/// - In case of a funding failure, the bounty status must be updated with the
565		///   `check_status` call before retrying with `retry_payment` call.
566		///
567		/// ### Parameters
568		/// - `asset_kind`: An indicator of the specific asset class to be funded.
569		/// - `value`: The total payment amount of this bounty.
570		/// - `curator`: Address of bounty curator.
571		/// - `metadata`: The hash of an on-chain stored preimage with bounty metadata.
572		///
573		/// ## Events
574		///
575		/// Emits [`Event::BountyCreated`] and [`Event::Paid`] if successful.
576		#[pallet::call_index(0)]
577		#[pallet::weight(<T as Config<I>>::WeightInfo::fund_bounty())]
578		pub fn fund_bounty(
579			origin: OriginFor<T>,
580			asset_kind: Box<T::AssetKind>,
581			#[pallet::compact] value: T::Balance,
582			curator: AccountIdLookupOf<T>,
583			metadata: T::Hash,
584		) -> DispatchResult {
585			let max_amount = T::SpendOrigin::ensure_origin(origin)?;
586			let curator = T::Lookup::lookup(curator)?;
587			ensure!(T::Preimages::len(&metadata).is_some(), Error::<T, I>::PreimageNotExist);
588
589			let native_amount = T::BalanceConverter::from_asset_balance(value, *asset_kind.clone())
590				.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
591			ensure!(native_amount >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
592			ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
593
594			with_context::<SpendContext<T::Balance>, _>(|v| {
595				let context = v.or_default();
596				let funding = context.spend_in_context.entry(max_amount).or_default();
597
598				if funding.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
599					Err(Error::<T, I>::InsufficientPermission)
600				} else {
601					*funding = funding.saturating_add(native_amount);
602					Ok(())
603				}
604			})
605			.unwrap_or(Ok(()))?;
606
607			let index = BountyCount::<T, I>::get();
608			let payment_status =
609				Self::do_process_funding_payment(index, None, *asset_kind.clone(), value, None)?;
610
611			let bounty = BountyOf::<T, I> {
612				asset_kind: *asset_kind,
613				value,
614				metadata,
615				status: BountyStatus::FundingAttempted { curator, payment_status },
616			};
617			Bounties::<T, I>::insert(index, &bounty);
618			T::Preimages::request(&metadata);
619			BountyCount::<T, I>::put(index + 1);
620
621			Self::deposit_event(Event::<T, I>::BountyCreated { index });
622
623			Ok(())
624		}
625
626		/// Fund a new child-bounty with a proposed curator, initiating the payment from the parent
627		/// bounty to the child-bounty account/location.
628		///
629		/// ## Dispatch Origin
630		///
631		/// Must be signed by the parent curator.
632		///
633		/// ## Details
634		///
635		/// - If `curator` is not provided, the child-bounty will default to using the parent
636		///   curator, allowing the parent curator to immediately call `check_status` and
637		///   `award_bounty` to payout the child-bounty.
638		/// - In case of a funding failure, the child-/bounty status must be updated with the
639		///   `check_status` call before retrying with `retry_payment` call.
640		///
641		/// ### Parameters
642		/// - `parent_bounty_id`: Index of parent bounty for which child-bounty is being added.
643		/// - `value`: The payment amount of this child-bounty.
644		/// - `metadata`: The hash of an on-chain stored preimage with child-bounty metadata.
645		/// - `curator`: Address of child-bounty curator.
646		///
647		/// ## Events
648		///
649		/// Emits [`Event::ChildBountyCreated`] and [`Event::Paid`] if successful.
650		#[pallet::call_index(1)]
651		#[pallet::weight(<T as Config<I>>::WeightInfo::fund_child_bounty())]
652		pub fn fund_child_bounty(
653			origin: OriginFor<T>,
654			#[pallet::compact] parent_bounty_id: BountyIndex,
655			#[pallet::compact] value: T::Balance,
656			metadata: T::Hash,
657			curator: Option<AccountIdLookupOf<T>>,
658		) -> DispatchResult {
659			let signer = ensure_signed(origin)?;
660			ensure!(T::Preimages::len(&metadata).is_some(), Error::<T, I>::PreimageNotExist);
661
662			let (asset_kind, parent_value, _, _, parent_curator) =
663				Self::get_bounty_details(parent_bounty_id, None)
664					.map_err(|_| Error::<T, I>::InvalidIndex)?;
665			let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind.clone())
666				.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
667
668			ensure!(
669				native_amount >= T::ChildBountyValueMinimum::get(),
670				Error::<T, I>::InvalidValue
671			);
672			ensure!(
673				ChildBountiesPerParent::<T, I>::get(parent_bounty_id) <
674					T::MaxActiveChildBountyCount::get(),
675				Error::<T, I>::TooManyChildBounties,
676			);
677
678			// Parent bounty must be `Active` with a curator assigned.
679			let parent_curator = parent_curator.ok_or(Error::<T, I>::UnexpectedStatus)?;
680			let final_curator = match curator {
681				Some(curator) => T::Lookup::lookup(curator)?,
682				None => parent_curator.clone(),
683			};
684			ensure!(signer == parent_curator, Error::<T, I>::RequireCurator);
685
686			// Check value
687			let child_bounties_value = ChildBountiesValuePerParent::<T, I>::get(parent_bounty_id);
688			let remaining_parent_value = parent_value.saturating_sub(child_bounties_value);
689			ensure!(remaining_parent_value >= value, Error::<T, I>::InsufficientBountyValue);
690
691			// Get child-bounty ID.
692			let child_bounty_id = TotalChildBountiesPerParent::<T, I>::get(parent_bounty_id);
693
694			// Initiate funding payment
695			let payment_status = Self::do_process_funding_payment(
696				parent_bounty_id,
697				Some(child_bounty_id),
698				asset_kind,
699				value,
700				None,
701			)?;
702
703			let child_bounty = ChildBounty {
704				parent_bounty: parent_bounty_id,
705				value,
706				metadata,
707				status: BountyStatus::FundingAttempted {
708					curator: final_curator,
709					payment_status: payment_status.clone(),
710				},
711			};
712			ChildBounties::<T, I>::insert(parent_bounty_id, child_bounty_id, child_bounty);
713			T::Preimages::request(&metadata);
714
715			// Add child-bounty value to the cumulative value sum. To be
716			// subtracted from the parent bounty payout when awarding
717			// bounty.
718			ChildBountiesValuePerParent::<T, I>::mutate(parent_bounty_id, |children_value| {
719				*children_value = children_value.saturating_add(value)
720			});
721
722			// Increment the active child-bounty count.
723			ChildBountiesPerParent::<T, I>::mutate(parent_bounty_id, |count| {
724				count.saturating_inc()
725			});
726			TotalChildBountiesPerParent::<T, I>::insert(
727				parent_bounty_id,
728				child_bounty_id.saturating_add(1),
729			);
730
731			Self::deposit_event(Event::<T, I>::ChildBountyCreated {
732				index: parent_bounty_id,
733				child_index: child_bounty_id,
734			});
735
736			Ok(())
737		}
738
739		/// Propose a new curator for a child-/bounty after the previous was unassigned.
740		///
741		/// ## Dispatch Origin
742		///
743		/// Must be signed by `T::SpendOrigin` for a bounty, or by the parent bounty curator
744		/// for a child-bounty.
745		///
746		/// ## Details
747		///
748		/// - The child-/bounty must be in the `CuratorUnassigned` state.
749		/// - For a bounty, the `SpendOrigin` must have sufficient permissions to propose the
750		///   curator.
751		///
752		/// ### Parameters
753		/// - `parent_bounty_id`: Index of bounty.
754		/// - `child_bounty_id`: Index of child-bounty.
755		/// - `curator`: Account to be proposed as the curator.
756		///
757		/// ## Events
758		///
759		/// Emits [`Event::CuratorProposed`] if successful.
760		#[pallet::call_index(2)]
761		#[pallet::weight(match child_bounty_id {
762			None => <T as Config<I>>::WeightInfo::propose_curator_parent_bounty(),
763			Some(_) => <T as Config<I>>::WeightInfo::propose_curator_child_bounty(),
764		})]
765		pub fn propose_curator(
766			origin: OriginFor<T>,
767			#[pallet::compact] parent_bounty_id: BountyIndex,
768			child_bounty_id: Option<BountyIndex>,
769			curator: AccountIdLookupOf<T>,
770		) -> DispatchResult {
771			let maybe_sender = ensure_signed(origin.clone())
772				.map(Some)
773				.or_else(|_| T::SpendOrigin::ensure_origin(origin.clone()).map(|_| None))?;
774			let curator = T::Lookup::lookup(curator)?;
775
776			let (asset_kind, value, _, status, parent_curator) =
777				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
778			ensure!(status == BountyStatus::CuratorUnassigned, Error::<T, I>::UnexpectedStatus);
779
780			match child_bounty_id {
781				// Only `SpendOrigin` can propose curator for bounty
782				None => {
783					ensure!(maybe_sender.is_none(), BadOrigin);
784					let max_amount = T::SpendOrigin::ensure_origin(origin)?;
785					let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind)
786						.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
787					ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
788				},
789				// Only parent curator can propose curator for child-bounty
790				Some(_) => {
791					let parent_curator = parent_curator.ok_or(Error::<T, I>::UnexpectedStatus)?;
792					let sender = maybe_sender.ok_or(BadOrigin)?;
793					ensure!(sender == parent_curator, BadOrigin);
794				},
795			};
796
797			let new_status = BountyStatus::Funded { curator: curator.clone() };
798			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
799
800			Self::deposit_event(Event::<T, I>::CuratorProposed {
801				index: parent_bounty_id,
802				child_index: child_bounty_id,
803				curator,
804			});
805
806			Ok(())
807		}
808
809		/// Accept the curator role for a child-/bounty.
810		///
811		/// ## Dispatch Origin
812		///
813		/// Must be signed by the proposed curator.
814		///
815		/// ## Details
816		///
817		/// - The child-/bounty must be in the `Funded` state.
818		/// - The curator must accept the role by calling this function.
819		/// - A deposit will be reserved from the curator and refunded upon successful payout.
820		///
821		/// ### Parameters
822		/// - `parent_bounty_id`: Index of parent bounty.
823		/// - `child_bounty_id`: Index of child-bounty.
824		///
825		/// ## Events
826		///
827		/// Emits [`Event::BountyBecameActive`] if successful.
828		#[pallet::call_index(3)]
829		#[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
830		pub fn accept_curator(
831			origin: OriginFor<T>,
832			#[pallet::compact] parent_bounty_id: BountyIndex,
833			child_bounty_id: Option<BountyIndex>,
834		) -> DispatchResult {
835			let signer = ensure_signed(origin)?;
836
837			let (asset_kind, value, _, status, _) =
838				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
839
840			let BountyStatus::Funded { ref curator } = status else {
841				return Err(Error::<T, I>::UnexpectedStatus.into())
842			};
843			ensure!(signer == *curator, Error::<T, I>::RequireCurator);
844
845			let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind)
846				.map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
847			let curator_deposit = T::Consideration::new(&curator, native_amount)?;
848			CuratorDeposit::<T, I>::insert(parent_bounty_id, child_bounty_id, curator_deposit);
849
850			let new_status = BountyStatus::Active { curator: curator.clone() };
851			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
852
853			Self::deposit_event(Event::<T, I>::BountyBecameActive {
854				index: parent_bounty_id,
855				child_index: child_bounty_id,
856				curator: signer,
857			});
858
859			Ok(())
860		}
861
862		/// Unassign curator from a child-/bounty.
863		///
864		/// ## Dispatch Origin
865		///
866		/// This function can only be called by the `RejectOrigin` or the child-/bounty curator.
867		///
868		/// ## Details
869		///
870		/// - If this function is called by the `RejectOrigin`, or by the parent curator in the case
871		///   of a child bounty, we assume that the curator is malicious or inactive. As a result,
872		///   we will slash the curator when possible.
873		/// - If the origin is the child-/bounty curator, we take this as a sign they are unable to
874		///   do their job and they willingly give up. We could slash them, but for now we allow
875		///   them to recover their deposit and exit without issue. (We may want to change this if
876		///   it is abused).
877		/// - If successful, the child-/bounty status is updated to `CuratorUnassigned`. To
878		///   reactivate the bounty, a new curator must be proposed and must accept the role.
879		///
880		/// ### Parameters
881		/// - `parent_bounty_id`: Index of parent bounty.
882		/// - `child_bounty_id`: Index of child-bounty.
883		///
884		/// ## Events
885		///
886		/// Emits [`Event::CuratorUnassigned`] if successful.
887		#[pallet::call_index(4)]
888		#[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
889		pub fn unassign_curator(
890			origin: OriginFor<T>,
891			#[pallet::compact] parent_bounty_id: BountyIndex,
892			child_bounty_id: Option<BountyIndex>,
893		) -> DispatchResult {
894			let maybe_sender = ensure_signed(origin.clone())
895				.map(Some)
896				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
897
898			let (_, _, _, status, parent_curator) =
899				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
900
901			match status {
902				BountyStatus::Funded { ref curator } => {
903					// A bounty curator has been proposed, but not accepted yet.
904					// Either `RejectOrigin`, parent bounty curator or the proposed
905					// curator can unassign the child-/bounty curator.
906					ensure!(
907						maybe_sender.map_or(true, |sender| {
908							sender == *curator ||
909								parent_curator
910									.map_or(false, |parent_curator| sender == parent_curator)
911						}),
912						BadOrigin
913					);
914				},
915				BountyStatus::Active { ref curator, .. } => {
916					let maybe_curator_deposit =
917						CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id);
918					// The child-/bounty is active.
919					match maybe_sender {
920						// If the `RejectOrigin` is calling this function, burn the curator deposit.
921						None => {
922							if let Some(curator_deposit) = maybe_curator_deposit {
923								T::Consideration::burn(curator_deposit, curator);
924							}
925							// Continue to change bounty status below...
926						},
927						Some(sender) if sender == *curator => {
928							if let Some(curator_deposit) = maybe_curator_deposit {
929								// This is the curator, willingly giving up their role. Free their
930								// deposit.
931								T::Consideration::drop(curator_deposit, curator)?;
932							}
933							// Continue to change bounty status below...
934						},
935						Some(sender) => {
936							if let Some(parent_curator) = parent_curator {
937								// If the parent curator is unassigning a child curator, that is not
938								// itself, burn the child curator deposit.
939								if sender == parent_curator && *curator != parent_curator {
940									if let Some(curator_deposit) = maybe_curator_deposit {
941										T::Consideration::burn(curator_deposit, curator);
942									}
943								} else {
944									return Err(BadOrigin.into());
945								}
946							}
947						},
948					}
949				},
950				_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
951			};
952
953			let new_status = BountyStatus::CuratorUnassigned;
954			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
955
956			Self::deposit_event(Event::<T, I>::CuratorUnassigned {
957				index: parent_bounty_id,
958				child_index: child_bounty_id,
959			});
960
961			Ok(())
962		}
963
964		/// Awards the child-/bounty to a beneficiary account/location,
965		/// initiating the payout payments to both the beneficiary and the curator.
966		///
967		/// ## Dispatch Origin
968		///
969		/// This function can only be called by the `RejectOrigin` or the child-/bounty curator.
970		///
971		/// ## Details
972		///
973		/// - The child-/bounty must be in the `Active` state.
974		/// - if awarding a parent bounty it must not have active or funded child bounties.
975		/// - Initiates payout payment from the child-/bounty to the beneficiary account/location.
976		/// - If successful the child-/bounty status is updated to `PayoutAttempted`.
977		/// - In case of a payout failure, the child-/bounty status must be updated with
978		/// `check_status` call before retrying with `retry_payment` call.
979		///
980		/// ### Parameters
981		/// - `parent_bounty_id`: Index of parent bounty.
982		/// - `child_bounty_id`: Index of child-bounty.
983		/// - `beneficiary`: Account/location to be awarded the child-/bounty.
984		///
985		/// ## Events
986		///
987		/// Emits [`Event::BountyAwarded`] and [`Event::Paid`] if successful.
988		#[pallet::call_index(5)]
989		#[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
990		pub fn award_bounty(
991			origin: OriginFor<T>,
992			#[pallet::compact] parent_bounty_id: BountyIndex,
993			child_bounty_id: Option<BountyIndex>,
994			beneficiary: BeneficiaryLookupOf<T, I>,
995		) -> DispatchResult {
996			let signer = ensure_signed(origin)?;
997			let beneficiary = T::BeneficiaryLookup::lookup(beneficiary)?;
998
999			let (asset_kind, value, _, status, _) =
1000				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1001
1002			if child_bounty_id.is_none() {
1003				ensure!(
1004					ChildBountiesPerParent::<T, I>::get(parent_bounty_id) == 0,
1005					Error::<T, I>::HasActiveChildBounty
1006				);
1007			}
1008
1009			let BountyStatus::Active { ref curator } = status else {
1010				return Err(Error::<T, I>::UnexpectedStatus.into())
1011			};
1012			ensure!(signer == *curator, Error::<T, I>::RequireCurator);
1013
1014			let beneficiary_payment_status = Self::do_process_payout_payment(
1015				parent_bounty_id,
1016				child_bounty_id,
1017				asset_kind,
1018				value,
1019				beneficiary.clone(),
1020				None,
1021			)?;
1022
1023			let new_status = BountyStatus::PayoutAttempted {
1024				curator: curator.clone(),
1025				beneficiary: beneficiary.clone(),
1026				payment_status: beneficiary_payment_status.clone(),
1027			};
1028			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1029
1030			Self::deposit_event(Event::<T, I>::BountyAwarded {
1031				index: parent_bounty_id,
1032				child_index: child_bounty_id,
1033				beneficiary,
1034			});
1035
1036			Ok(())
1037		}
1038
1039		/// Cancel an active child-/bounty. A payment to send all the funds to the funding source is
1040		/// initialized.
1041		///
1042		/// ## Dispatch Origin
1043		///
1044		/// This function can only be called by the `RejectOrigin` or the parent bounty curator.
1045		///
1046		/// ## Details
1047		///
1048		/// - If the child-/bounty is in the `Funded` state, a refund payment is initiated.
1049		/// - If the child-/bounty is in the `Active` state, a refund payment is initiated and the
1050		///   child-/bounty status is updated with the curator account/location.
1051		/// - If the child-/bounty is in the funding or payout phase, it cannot be canceled.
1052		/// - In case of a refund failure, the child-/bounty status must be updated with the
1053		/// `check_status` call before retrying with `retry_payment` call.
1054		///
1055		/// ### Parameters
1056		/// - `parent_bounty_id`: Index of parent bounty.
1057		/// - `child_bounty_id`: Index of child-bounty.
1058		///
1059		/// ## Events
1060		///
1061		/// Emits [`Event::BountyCanceled`] and [`Event::Paid`] if successful.
1062		#[pallet::call_index(6)]
1063		#[pallet::weight(match child_bounty_id {
1064			None => <T as Config<I>>::WeightInfo::close_parent_bounty(),
1065			Some(_) => <T as Config<I>>::WeightInfo::close_child_bounty(),
1066		})]
1067		pub fn close_bounty(
1068			origin: OriginFor<T>,
1069			#[pallet::compact] parent_bounty_id: BountyIndex,
1070			child_bounty_id: Option<BountyIndex>,
1071		) -> DispatchResult {
1072			let maybe_sender = ensure_signed(origin.clone())
1073				.map(Some)
1074				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
1075
1076			let (asset_kind, value, _, status, parent_curator) =
1077				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1078
1079			let maybe_curator = match status {
1080				BountyStatus::Funded { curator } | BountyStatus::Active { curator, .. } =>
1081					Some(curator),
1082				BountyStatus::CuratorUnassigned => None,
1083				_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1084			};
1085
1086			match child_bounty_id {
1087				None => {
1088					// Parent bounty can only be closed if it has no active child bounties.
1089					ensure!(
1090						ChildBountiesPerParent::<T, I>::get(parent_bounty_id) == 0,
1091						Error::<T, I>::HasActiveChildBounty
1092					);
1093					// Bounty can be closed by `RejectOrigin` or the curator.
1094					if let Some(sender) = maybe_sender.as_ref() {
1095						let is_curator =
1096							maybe_curator.as_ref().map_or(false, |curator| curator == sender);
1097						ensure!(is_curator, BadOrigin);
1098					}
1099				},
1100				Some(_) => {
1101					// Child-bounty can be closed by `RejectOrigin`, the curator or parent curator.
1102					if let Some(sender) = maybe_sender.as_ref() {
1103						let is_curator =
1104							maybe_curator.as_ref().map_or(false, |curator| curator == sender);
1105						let is_parent_curator = parent_curator
1106							.as_ref()
1107							.map_or(false, |parent_curator| parent_curator == sender);
1108						ensure!(is_curator || is_parent_curator, BadOrigin);
1109					}
1110				},
1111			};
1112
1113			let payment_status = Self::do_process_refund_payment(
1114				parent_bounty_id,
1115				child_bounty_id,
1116				asset_kind,
1117				value,
1118				None,
1119			)?;
1120			let new_status = BountyStatus::RefundAttempted {
1121				payment_status: payment_status.clone(),
1122				curator: maybe_curator.clone(),
1123			};
1124			Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1125
1126			Self::deposit_event(Event::<T, I>::BountyCanceled {
1127				index: parent_bounty_id,
1128				child_index: child_bounty_id,
1129			});
1130
1131			Ok(())
1132		}
1133
1134		/// Check and update the payment status of a child-/bounty.
1135		///
1136		/// ## Dispatch Origin
1137		///
1138		/// Must be signed.
1139		///
1140		/// ## Details
1141		///
1142		/// - If the child-/bounty status is `FundingAttempted`, it checks if the funding payment
1143		///   has succeeded. If successful, the bounty status becomes `Funded`.
1144		/// - If the child-/bounty status is `RefundAttempted`, it checks if the refund payment has
1145		///   succeeded. If successful, the child-/bounty is removed from storage.
1146		/// - If the child-/bounty status is `PayoutAttempted`, it checks if the payout payment has
1147		///   succeeded. If successful, the child-/bounty is removed from storage.
1148		///
1149		/// ### Parameters
1150		/// - `parent_bounty_id`: Index of parent bounty.
1151		/// - `child_bounty_id`: Index of child-bounty.
1152		///
1153		/// ## Events
1154		///
1155		/// Emits [`Event::BountyBecameActive`] if the child/bounty status transitions to `Active`.
1156		/// Emits [`Event::BountyRefundProcessed`] if the refund payment has succeed.
1157		/// Emits [`Event::BountyPayoutProcessed`] if the payout payment has succeed.
1158		/// Emits [`Event::PaymentFailed`] if the funding, refund our payment payment has failed.
1159		#[pallet::call_index(7)]
1160		#[pallet::weight(<T as Config<I>>::WeightInfo::check_status_funding().max(
1161			<T as Config<I>>::WeightInfo::check_status_refund(),
1162		).max(<T as Config<I>>::WeightInfo::check_status_payout()))]
1163		pub fn check_status(
1164			origin: OriginFor<T>,
1165			#[pallet::compact] parent_bounty_id: BountyIndex,
1166			child_bounty_id: Option<BountyIndex>,
1167		) -> DispatchResultWithPostInfo {
1168			use BountyStatus::*;
1169
1170			ensure_signed(origin)?;
1171			let (asset_kind, value, metadata, status, parent_curator) =
1172				Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1173
1174			let (new_status, weight) = match status {
1175				FundingAttempted { ref payment_status, curator } => {
1176					let new_payment_status = Self::do_check_funding_payment_status(
1177						parent_bounty_id,
1178						child_bounty_id,
1179						payment_status.clone(),
1180					)?;
1181
1182					let new_status = match new_payment_status {
1183						PaymentState::Succeeded => match (child_bounty_id, parent_curator) {
1184							(Some(_), Some(parent_curator)) if curator == parent_curator =>
1185								BountyStatus::Active { curator },
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			PaymentStatus::Failure => {
1642				Self::deposit_event(Event::<T, I>::PaymentFailed {
1643					index: parent_bounty_id,
1644					child_index: child_bounty_id,
1645					payment_id,
1646				});
1647				return Ok(PaymentState::Failed)
1648			},
1649		}
1650	}
1651
1652	/// Initializes payment from the child-/bounty account/location to the funding source (i.e.
1653	/// treasury pot, parent bounty).
1654	fn do_process_refund_payment(
1655		parent_bounty_id: BountyIndex,
1656		child_bounty_id: Option<BountyIndex>,
1657		asset_kind: T::AssetKind,
1658		value: T::Balance,
1659		payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1660	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1661		if let Some(payment_status) = payment_status {
1662			ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1663		}
1664
1665		let (source, beneficiary) = match child_bounty_id {
1666			None => (
1667				Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1668				Self::funding_source_account(asset_kind.clone())?,
1669			),
1670			Some(child_bounty_id) => (
1671				Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?,
1672				Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1673			),
1674		};
1675
1676		let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, value)
1677			.map_err(|_| Error::<T, I>::RefundError)?;
1678
1679		Self::deposit_event(Event::<T, I>::Paid {
1680			index: parent_bounty_id,
1681			child_index: child_bounty_id,
1682			payment_id: id,
1683		});
1684
1685		Ok(PaymentState::Attempted { id })
1686	}
1687
1688	/// Queries the status of the refund payment from the child-/bounty account/location to the
1689	/// funding source and returns a new payment status.
1690	fn do_check_refund_payment_status(
1691		parent_bounty_id: BountyIndex,
1692		child_bounty_id: Option<BountyIndex>,
1693		payment_status: PaymentState<PaymentIdOf<T, I>>,
1694	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1695		let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1696
1697		match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1698			PaymentStatus::Success => {
1699				Self::deposit_event(Event::<T, I>::BountyRefundProcessed {
1700					index: parent_bounty_id,
1701					child_index: child_bounty_id,
1702				});
1703				Ok(PaymentState::Succeeded)
1704			},
1705			PaymentStatus::InProgress | PaymentStatus::Unknown =>
1706			// nothing new to report
1707				Err(Error::<T, I>::RefundInconclusive.into()),
1708			PaymentStatus::Failure => {
1709				// assume payment has failed, allow user to retry
1710				Self::deposit_event(Event::<T, I>::PaymentFailed {
1711					index: parent_bounty_id,
1712					child_index: child_bounty_id,
1713					payment_id,
1714				});
1715				Ok(PaymentState::Failed)
1716			},
1717		}
1718	}
1719
1720	/// Initializes payment from the child-/bounty to the beneficiary account/location.
1721	fn do_process_payout_payment(
1722		parent_bounty_id: BountyIndex,
1723		child_bounty_id: Option<BountyIndex>,
1724		asset_kind: T::AssetKind,
1725		value: T::Balance,
1726		beneficiary: T::Beneficiary,
1727		payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1728	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1729		if let Some(payment_status) = payment_status {
1730			ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1731		}
1732
1733		let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1734
1735		let source = match child_bounty_id {
1736			None => Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1737			Some(child_bounty_id) =>
1738				Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?,
1739		};
1740
1741		let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, payout)
1742			.map_err(|_| Error::<T, I>::PayoutError)?;
1743
1744		Self::deposit_event(Event::<T, I>::Paid {
1745			index: parent_bounty_id,
1746			child_index: child_bounty_id,
1747			payment_id: id,
1748		});
1749
1750		Ok(PaymentState::Attempted { id })
1751	}
1752
1753	/// Queries the status of the payment from the child-/bounty to the beneficiary account/location
1754	/// and returns a new payment status.
1755	fn do_check_payout_payment_status(
1756		parent_bounty_id: BountyIndex,
1757		child_bounty_id: Option<BountyIndex>,
1758		asset_kind: T::AssetKind,
1759		value: T::Balance,
1760		beneficiary: T::Beneficiary,
1761		payment_status: PaymentState<PaymentIdOf<T, I>>,
1762	) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1763		let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1764
1765		match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1766			PaymentStatus::Success => {
1767				let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1768
1769				Self::deposit_event(Event::<T, I>::BountyPayoutProcessed {
1770					index: parent_bounty_id,
1771					child_index: child_bounty_id,
1772					asset_kind: asset_kind.clone(),
1773					value: payout,
1774					beneficiary,
1775				});
1776
1777				Ok(PaymentState::Succeeded)
1778			},
1779			PaymentStatus::InProgress | PaymentStatus::Unknown =>
1780			// nothing new to report
1781				Err(Error::<T, I>::PayoutInconclusive.into()),
1782			PaymentStatus::Failure => {
1783				// assume payment has failed, allow user to retry
1784				Self::deposit_event(Event::<T, I>::PaymentFailed {
1785					index: parent_bounty_id,
1786					child_index: child_bounty_id,
1787					payment_id,
1788				});
1789				Ok(PaymentState::Failed)
1790			},
1791		}
1792	}
1793}
1794
1795/// Type implementing curator deposit as a percentage of the child-/bounty value.
1796///
1797/// It implements `Convert` trait and can be used with types like `HoldConsideration` implementing
1798/// `Consideration` trait.
1799pub struct CuratorDepositAmount<Mult, Min, Max, Balance>(PhantomData<(Mult, Min, Max, Balance)>);
1800impl<Mult, Min, Max, Balance> Convert<Balance, Balance>
1801	for CuratorDepositAmount<Mult, Min, Max, Balance>
1802where
1803	Balance: frame_support::traits::tokens::Balance,
1804	Min: Get<Option<Balance>>,
1805	Max: Get<Option<Balance>>,
1806	Mult: Get<Permill>,
1807{
1808	fn convert(value: Balance) -> Balance {
1809		let mut deposit = Mult::get().mul_floor(value);
1810
1811		if let Some(min) = Min::get() {
1812			if deposit < min {
1813				deposit = min;
1814			}
1815		}
1816
1817		if let Some(max) = Max::get() {
1818			if deposit > max {
1819				deposit = max;
1820			}
1821		}
1822
1823		deposit
1824	}
1825}
1826
1827/// Derives the funding `AccountId` from the `PalletId` and converts it into the
1828/// bounty `Beneficiary`, used as the source of bounty funds.
1829///
1830/// Used when the [`PalletId`] itself owns the funds (i.e. pallet-treasury id).
1831/// # Type Parameters
1832/// - `Id`: The pallet ID getter
1833/// - `T`: The pallet configuration
1834/// - `C`: Converter from `T::AccountId` to `T::Beneficiary`. Use `Identity` when types are the
1835///   same.
1836/// - `I`: Instance parameter (default: `()`)
1837pub struct PalletIdAsFundingSource<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1838impl<Id, T, C, I> TryConvert<T::AssetKind, T::Beneficiary> for PalletIdAsFundingSource<Id, T, C, I>
1839where
1840	Id: Get<PalletId>,
1841	T: crate::Config<I>,
1842	C: Convert<T::AccountId, T::Beneficiary>,
1843{
1844	fn try_convert(_asset_kind: T::AssetKind) -> Result<T::Beneficiary, T::AssetKind> {
1845		let account: T::AccountId = Id::get().into_account_truncating();
1846		Ok(C::convert(account))
1847	}
1848}
1849
1850/// Derives a bounty `AccountId` from the `PalletId` and the `BountyIndex`,
1851/// then converts it into the corresponding bounty `Beneficiary`.
1852///
1853/// Used when the [`PalletId`] itself owns the funds (i.e. pallet-treasury id).
1854/// # Type Parameters
1855/// - `Id`: The pallet ID getter
1856/// - `T`: The pallet configuration
1857/// - `C`: Converter from `T::AccountId` to `T::Beneficiary`. Use `Identity` when types are the
1858///   same.
1859/// - `I`: Instance parameter (default: `()`)
1860pub struct BountySourceFromPalletId<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1861impl<Id, T, C, I> TryConvert<(BountyIndex, T::AssetKind), T::Beneficiary>
1862	for BountySourceFromPalletId<Id, T, C, I>
1863where
1864	Id: Get<PalletId>,
1865	T: crate::Config<I>,
1866	C: Convert<T::AccountId, T::Beneficiary>,
1867{
1868	fn try_convert(
1869		(parent_bounty_id, _asset_kind): (BountyIndex, T::AssetKind),
1870	) -> Result<T::Beneficiary, (BountyIndex, T::AssetKind)> {
1871		let account: T::AccountId = Id::get().into_sub_account_truncating(("bt", parent_bounty_id));
1872		Ok(C::convert(account))
1873	}
1874}
1875
1876/// Derives a child-bounty `AccountId` from the `PalletId`, the parent index,
1877/// and the child index, then converts it into the child-bounty `Beneficiary`.
1878///
1879/// Used when the [`PalletId`] itself owns the funds (i.e. pallet-treasury id).
1880/// # Type Parameters
1881/// - `Id`: The pallet ID getter
1882/// - `T`: The pallet configuration
1883/// - `C`: Converter from `T::AccountId` to `T::Beneficiary`. Use `Identity` when types are the
1884///   same.
1885/// - `I`: Instance parameter (default: `()`)
1886pub struct ChildBountySourceFromPalletId<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1887impl<Id, T, C, I> TryConvert<(BountyIndex, BountyIndex, T::AssetKind), T::Beneficiary>
1888	for ChildBountySourceFromPalletId<Id, T, C, I>
1889where
1890	Id: Get<PalletId>,
1891	T: crate::Config<I>,
1892	C: Convert<T::AccountId, T::Beneficiary>,
1893{
1894	fn try_convert(
1895		(parent_bounty_id, child_bounty_id, _asset_kind): (BountyIndex, BountyIndex, T::AssetKind),
1896	) -> Result<T::Beneficiary, (BountyIndex, BountyIndex, T::AssetKind)> {
1897		// The prefix is changed to have different AccountId when the index of
1898		// parent and child is same.
1899		let account: T::AccountId =
1900			Id::get().into_sub_account_truncating(("cb", parent_bounty_id, child_bounty_id));
1901		Ok(C::convert(account))
1902	}
1903}