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