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