Skip to content
Snippets Groups Projects
lib.rs 35 KiB
Newer Older
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! # Child Bounties Pallet ( `pallet-child-bounties` )
//!
//! ## Child Bounty
//!
//! > NOTE: This pallet is tightly coupled with `pallet-treasury` and `pallet-bounties`.
//!
//! With child bounties, a large bounty proposal can be divided into smaller chunks,
//! for parallel execution, and for efficient governance and tracking of spent funds.
//! A child bounty is a smaller piece of work, extracted from a parent bounty.
//! A curator is assigned after the child bounty is created by the parent bounty curator,
//! to be delegated with the responsibility of assigning a payout address once the specified
//! set of tasks is completed.
//!
//! ## Interface
//!
//! ### Dispatchable Functions
//!
//! Child Bounty protocol:
//! - `add_child_bounty` - Add a child bounty for a parent bounty to for dividing the work in
//!   smaller tasks.
//! - `propose_curator` - Assign an account to a child bounty as candidate curator.
//! - `accept_curator` - Accept a child bounty assignment from the parent bounty curator, setting a
//!   curator deposit.
//! - `award_child_bounty` - Close and pay out the specified amount for the completed work.
//! - `claim_child_bounty` - Claim a specific child bounty amount from the payout address.
//! - `unassign_curator` - Unassign an accepted curator from a specific child bounty.
//! - `close_child_bounty` - Cancel the child bounty for a specific treasury amount and close the
//!   bounty.

// Most of the business logic in this pallet has been
// originally contributed by "https://github.com/shamb0",
// as part of the PR - https://github.com/paritytech/substrate/pull/7965.
// The code has been moved here and then refactored in order to
// extract child bounties as a separate pallet.

#![cfg_attr(not(feature = "std"), no_std)]

mod benchmarking;
mod tests;
pub mod weights;

extern crate alloc;

/// The log target for this pallet.
const LOG_TARGET: &str = "runtime::child-bounties";

use alloc::vec::Vec;

use frame_support::traits::{
	Currency,
	ExistenceRequirement::{AllowDeath, KeepAlive},
	Get, OnUnbalanced, ReservableCurrency, WithdrawReasons,
};

use sp_runtime::{
	traits::{
		AccountIdConversion, BadOrigin, BlockNumberProvider, CheckedSub, Saturating, StaticLookup,
		Zero,
	},
	DispatchResult, RuntimeDebug,
};

use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::{
	ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
};
use pallet_bounties::BountyStatus;
use scale_info::TypeInfo;
pub use weights::WeightInfo;

pub use pallet::*;

pub type BalanceOf<T> = pallet_treasury::BalanceOf<T>;
pub type BountiesError<T> = pallet_bounties::Error<T>;
pub type BountyIndex = pallet_bounties::BountyIndex;
pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
pub type BlockNumberFor<T> =
	<<T as pallet_treasury::Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;

/// A child bounty proposal.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ChildBounty<AccountId, Balance, BlockNumber> {
	/// The parent of this child-bounty.
	pub parent_bounty: BountyIndex,
	/// The (total) amount that should be paid if this child-bounty is rewarded.
	pub value: Balance,
	/// The child bounty curator fee.
	pub fee: Balance,
	/// The deposit of child-bounty curator.
	pub curator_deposit: Balance,
	/// The status of this child-bounty.
	pub status: ChildBountyStatus<AccountId, BlockNumber>,
}

/// The status of a child-bounty.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum ChildBountyStatus<AccountId, BlockNumber> {
	/// The child-bounty is added and waiting for curator assignment.
	Added,
	/// A curator has been proposed by the parent bounty curator. Waiting for
	/// acceptance from the child-bounty curator.
	CuratorProposed {
		/// The assigned child-bounty curator of this bounty.
		curator: AccountId,
	},
	/// The child-bounty is active and waiting to be awarded.
	Active {
		/// The curator of this child-bounty.
		curator: AccountId,
	},
	/// The child-bounty is awarded and waiting to released after a delay.
	PendingPayout {
		/// The curator of this child-bounty.
		curator: AccountId,
		/// The beneficiary of the child-bounty.
		beneficiary: AccountId,
		/// When the child-bounty can be claimed.
		unlock_at: BlockNumber,
	},
}

#[frame_support::pallet]
pub mod pallet {

	use super::*;

	/// The in-code storage version.
	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);

	#[pallet::pallet]
	#[pallet::storage_version(STORAGE_VERSION)]
	pub struct Pallet<T>(_);

	#[pallet::config]
	pub trait Config:
		frame_system::Config + pallet_treasury::Config + pallet_bounties::Config
	{
		/// Maximum number of child bounties that can be added to a parent bounty.
		#[pallet::constant]
		type MaxActiveChildBountyCount: Get<u32>;

		/// Minimum value for a child-bounty.
		#[pallet::constant]
		type ChildBountyValueMinimum: Get<BalanceOf<Self>>;

		/// The overarching event type.
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

		/// Weight information for extrinsics in this pallet.
		type WeightInfo: WeightInfo;
	}

	#[pallet::error]
	pub enum Error<T> {
		/// The parent bounty is not in active state.
		ParentBountyNotActive,
		/// The bounty balance is not enough to add new child-bounty.
		InsufficientBountyBalance,
		/// Number of child bounties exceeds limit `MaxActiveChildBountyCount`.
		TooManyChildBounties,
	}

	#[pallet::event]
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
	pub enum Event<T: Config> {
		/// A child-bounty is added.
		Added { index: BountyIndex, child_index: BountyIndex },
		/// A child-bounty is awarded to a beneficiary.
		Awarded { index: BountyIndex, child_index: BountyIndex, beneficiary: T::AccountId },
		/// A child-bounty is claimed by beneficiary.
		Claimed {
			index: BountyIndex,
			child_index: BountyIndex,
			payout: BalanceOf<T>,
			beneficiary: T::AccountId,
		},
		/// A child-bounty is cancelled.
		Canceled { index: BountyIndex, child_index: BountyIndex },
	}

	/// DEPRECATED: Replaced with `ParentTotalChildBounties` storage item keeping dedicated counts
	/// for each parent bounty. Number of total child bounties. Will be removed in May 2025.
	#[pallet::storage]
	pub type ChildBountyCount<T: Config> = StorageValue<_, BountyIndex, ValueQuery>;

	/// Number of active child bounties per parent bounty.
	/// Map of parent bounty index to number of child bounties.
	#[pallet::storage]
	pub type ParentChildBounties<T: Config> =
		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;

	/// Number of total child bounties per parent bounty, including completed bounties.
	#[pallet::storage]
	pub type ParentTotalChildBounties<T: Config> =
		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;

	/// Child bounties that have been added.
	#[pallet::storage]
	pub type ChildBounties<T: Config> = StorageDoubleMap<
		_,
		Twox64Concat,
		BountyIndex,
		Twox64Concat,
		BountyIndex,
		ChildBounty<T::AccountId, BalanceOf<T>, BlockNumberFor<T>>,
	/// The description of each child-bounty. Indexed by `(parent_id, child_id)`.
	///
	/// This item replaces the `ChildBountyDescriptions` storage item from the V0 storage version.
	#[pallet::storage]
	pub type ChildBountyDescriptionsV1<T: Config> = StorageDoubleMap<
		_,
		Twox64Concat,
		BountyIndex,
		Twox64Concat,
		BountyIndex,
		BoundedVec<u8, T::MaximumReasonLength>,
	>;

	/// The mapping of the child bounty ids from storage version `V0` to the new `V1` version.
	///
	/// The `V0` ids based on total child bounty count [`ChildBountyCount`]`. The `V1` version ids
	/// based on the child bounty count per parent bounty [`ParentTotalChildBounties`].
	/// The item intended solely for client convenience and not used in the pallet's core logic.
	#[pallet::storage]
	pub type V0ToV1ChildBountyIds<T: Config> =
		StorageMap<_, Twox64Concat, BountyIndex, (BountyIndex, BountyIndex)>;

	/// The cumulative child-bounty curator fee for each parent bounty.
	#[pallet::storage]
	pub type ChildrenCuratorFees<T: Config> =
		StorageMap<_, Twox64Concat, BountyIndex, BalanceOf<T>, ValueQuery>;

	#[pallet::call]
	impl<T: Config> Pallet<T> {
		/// Add a new child-bounty.
		///
		/// The dispatch origin for this call must be the curator of parent
		/// bounty and the parent bounty must be in "active" state.
		///
		/// Child-bounty gets added successfully & fund gets transferred from
		/// parent bounty to child-bounty account, if parent bounty has enough
		/// funds, else the call fails.
		///
		/// Upper bound to maximum number of active  child bounties that can be
		/// added are managed via runtime trait config
		/// [`Config::MaxActiveChildBountyCount`].
		///
		/// If the call is success, the status of child-bounty is updated to
		/// "Added".
		///
		/// - `parent_bounty_id`: Index of parent bounty for which child-bounty is being added.
		/// - `value`: Value for executing the proposal.
		/// - `description`: Text description for the child-bounty.
		#[pallet::call_index(0)]
		#[pallet::weight(<T as Config>::WeightInfo::add_child_bounty(description.len() as u32))]
		pub fn add_child_bounty(
			origin: OriginFor<T>,
			#[pallet::compact] parent_bounty_id: BountyIndex,
			#[pallet::compact] value: BalanceOf<T>,
			description: Vec<u8>,
		) -> DispatchResult {
			let signer = ensure_signed(origin)?;

			// Verify the arguments.
			let bounded_description =
				description.try_into().map_err(|_| BountiesError::<T>::ReasonTooBig)?;
			ensure!(value >= T::ChildBountyValueMinimum::get(), BountiesError::<T>::InvalidValue);
			ensure!(
				ParentChildBounties::<T>::get(parent_bounty_id) <=
					T::MaxActiveChildBountyCount::get() as u32,
				Error::<T>::TooManyChildBounties,
			);

			let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
			ensure!(signer == curator, BountiesError::<T>::RequireCurator);

			// Read parent bounty account info.
			let parent_bounty_account =
				pallet_bounties::Pallet::<T>::bounty_account_id(parent_bounty_id);

			// Ensure parent bounty has enough balance after adding child-bounty.
			let bounty_balance = T::Currency::free_balance(&parent_bounty_account);
			let new_bounty_balance = bounty_balance
				.checked_sub(&value)
				.ok_or(Error::<T>::InsufficientBountyBalance)?;
			T::Currency::ensure_can_withdraw(
				&parent_bounty_account,
				value,
				WithdrawReasons::TRANSFER,
				new_bounty_balance,
			)?;

			// Get child-bounty ID.
			let child_bounty_id = ParentTotalChildBounties::<T>::get(parent_bounty_id);
			let child_bounty_account =
				Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);

			// Transfer funds from parent bounty to child-bounty.
			T::Currency::transfer(&parent_bounty_account, &child_bounty_account, value, KeepAlive)?;

			// Increment the active child-bounty count.
			ParentChildBounties::<T>::mutate(parent_bounty_id, |count| count.saturating_inc());
			ParentTotalChildBounties::<T>::insert(
				parent_bounty_id,
				child_bounty_id.saturating_add(1),
			);

			// Create child-bounty instance.
			Self::create_child_bounty(
				parent_bounty_id,
				child_bounty_id,
				value,
				bounded_description,
			);
			Ok(())
		}

		/// Propose curator for funded child-bounty.
		///
		/// The dispatch origin for this call must be curator of parent bounty.
		///
		/// Parent bounty must be in active state, for this child-bounty call to
		/// work.
		///
		/// Child-bounty must be in "Added" state, for processing the call. And
		/// state of child-bounty is moved to "CuratorProposed" on successful
		/// call completion.
		///
		/// - `parent_bounty_id`: Index of parent bounty.
		/// - `child_bounty_id`: Index of child bounty.
		/// - `curator`: Address of child-bounty curator.
		/// - `fee`: payment fee to child-bounty curator for execution.
		#[pallet::call_index(1)]
		#[pallet::weight(<T as Config>::WeightInfo::propose_curator())]
		pub fn propose_curator(
			origin: OriginFor<T>,
			#[pallet::compact] parent_bounty_id: BountyIndex,
			#[pallet::compact] child_bounty_id: BountyIndex,
			#[pallet::compact] fee: BalanceOf<T>,
		) -> DispatchResult {
			let signer = ensure_signed(origin)?;
			let child_bounty_curator = T::Lookup::lookup(curator)?;

			let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
			ensure!(signer == curator, BountiesError::<T>::RequireCurator);

			// Mutate the child-bounty instance.
			ChildBounties::<T>::try_mutate_exists(
				parent_bounty_id,
				child_bounty_id,
				|maybe_child_bounty| -> DispatchResult {
						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;

					// Ensure child-bounty is in expected state.
					ensure!(
						child_bounty.status == ChildBountyStatus::Added,
						BountiesError::<T>::UnexpectedStatus,
					);

					// Ensure child-bounty curator fee is less than child-bounty value.
					ensure!(fee < child_bounty.value, BountiesError::<T>::InvalidFee);

					// Add child-bounty curator fee to the cumulative sum. To be
					// subtracted from the parent bounty curator when claiming
					// bounty.
					ChildrenCuratorFees::<T>::mutate(parent_bounty_id, |value| {
						*value = value.saturating_add(fee)
					});

					// Update the child-bounty curator fee.
					child_bounty.fee = fee;

					// Update the child-bounty state.
					child_bounty.status =
						ChildBountyStatus::CuratorProposed { curator: child_bounty_curator };

					Ok(())
				},
			)
		}

		/// Accept the curator role for the child-bounty.
		///
		/// The dispatch origin for this call must be the curator of this
		/// child-bounty.
		///
		/// A deposit will be reserved from the curator and refund upon
		/// successful payout or cancellation.
		///
		/// Fee for curator is deducted from curator fee of parent bounty.
		///
		/// Parent bounty must be in active state, for this child-bounty call to
		/// work.
		///
		/// Child-bounty must be in "CuratorProposed" state, for processing the
		/// call. And state of child-bounty is moved to "Active" on successful
		/// call completion.
		///
		/// - `parent_bounty_id`: Index of parent bounty.
		/// - `child_bounty_id`: Index of child bounty.
		#[pallet::call_index(2)]
		#[pallet::weight(<T as Config>::WeightInfo::accept_curator())]
		pub fn accept_curator(
			origin: OriginFor<T>,
			#[pallet::compact] parent_bounty_id: BountyIndex,
			#[pallet::compact] child_bounty_id: BountyIndex,
		) -> DispatchResult {
			let signer = ensure_signed(origin)?;

			let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
			// Mutate child-bounty.
			ChildBounties::<T>::try_mutate_exists(
				parent_bounty_id,
				child_bounty_id,
				|maybe_child_bounty| -> DispatchResult {
						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;

					// Ensure child-bounty is in expected state.
					if let ChildBountyStatus::CuratorProposed { ref curator } = child_bounty.status
					{
						ensure!(signer == *curator, BountiesError::<T>::RequireCurator);

						// Reserve child-bounty curator deposit.
						let deposit = Self::calculate_curator_deposit(
							&parent_curator,
							curator,
							&child_bounty.fee,
						);

						T::Currency::reserve(curator, deposit)?;
						child_bounty.curator_deposit = deposit;

						child_bounty.status =
							ChildBountyStatus::Active { curator: curator.clone() };
						Ok(())
					} else {
						Err(BountiesError::<T>::UnexpectedStatus.into())
					}
				},
			)
		}

		/// Unassign curator from a child-bounty.
		///
		/// The dispatch origin for this call can be either `RejectOrigin`, or
		/// the curator of the parent bounty, or any signed origin.
		///
		/// For the origin other than T::RejectOrigin and the child-bounty
		/// curator, parent bounty must be in active state, for this call to
		/// work. We allow child-bounty curator and T::RejectOrigin to execute
		/// this call irrespective of the parent bounty state.
		///
		/// If this function is called by the `RejectOrigin` or the
		/// parent bounty curator, we assume that the child-bounty curator is
		/// malicious or inactive. As a result, child-bounty curator deposit is
		/// slashed.
		///
		/// If the origin is the child-bounty curator, we take this as a sign
		/// that they are unable to do their job, and are willingly giving up.
		/// We could slash the deposit, but for now we allow them to unreserve
		/// their deposit and exit without issue. (We may want to change this if
		/// it is abused.)
		///
		/// Finally, the origin can be anyone iff the child-bounty curator is
		/// "inactive". Expiry update due of parent bounty is used to estimate
		/// inactive state of child-bounty curator.
		///
		/// This allows anyone in the community to call out that a child-bounty
		/// curator is not doing their due diligence, and we should pick a new
		/// one. In this case the child-bounty curator deposit is slashed.
		///
		/// State of child-bounty is moved to Added state on successful call
		/// completion.
		///
		/// - `parent_bounty_id`: Index of parent bounty.
		/// - `child_bounty_id`: Index of child bounty.
		#[pallet::call_index(3)]
		#[pallet::weight(<T as Config>::WeightInfo::unassign_curator())]
		pub fn unassign_curator(
			origin: OriginFor<T>,
			#[pallet::compact] parent_bounty_id: BountyIndex,
			#[pallet::compact] child_bounty_id: BountyIndex,
		) -> DispatchResult {
			let maybe_sender = ensure_signed(origin.clone())
				.map(Some)
				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;

			ChildBounties::<T>::try_mutate_exists(
				parent_bounty_id,
				child_bounty_id,
				|maybe_child_bounty| -> DispatchResult {
						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;

					let slash_curator =
						|curator: &T::AccountId, curator_deposit: &mut BalanceOf<T>| {
							let imbalance =
								T::Currency::slash_reserved(curator, *curator_deposit).0;
							T::OnSlash::on_unbalanced(imbalance);
							*curator_deposit = Zero::zero();
						};

					match child_bounty.status {
						ChildBountyStatus::Added => {
							// No curator to unassign at this point.
							return Err(BountiesError::<T>::UnexpectedStatus.into())
						},
						ChildBountyStatus::CuratorProposed { ref curator } => {
							// A child-bounty curator has been proposed, but not accepted yet.
							// Either `RejectOrigin`, parent bounty curator or the proposed
							// child-bounty curator can unassign the child-bounty curator.
							ensure!(
								maybe_sender.map_or(true, |sender| {
									sender == *curator ||
										Self::ensure_bounty_active(parent_bounty_id)
											.map_or(false, |(parent_curator, _)| {
												sender == parent_curator
											})
								}),
								BadOrigin
							);
							// Continue to change bounty status below.
						},
						ChildBountyStatus::Active { ref curator } => {
							// The child-bounty is active.
							match maybe_sender {
								// If the `RejectOrigin` is calling this function, slash the curator
								// deposit.
								None => {
									slash_curator(curator, &mut child_bounty.curator_deposit);
									// Continue to change child-bounty status below.
								},
								Some(sender) if sender == *curator => {
									// This is the child-bounty curator, willingly giving up their
									// role. Give back their deposit.
									T::Currency::unreserve(curator, child_bounty.curator_deposit);
									// Reset curator deposit.
									child_bounty.curator_deposit = Zero::zero();
									// Continue to change bounty status below.
								},
								Some(sender) => {
									let (parent_curator, update_due) =
										Self::ensure_bounty_active(parent_bounty_id)?;
									if sender == parent_curator ||
										update_due < Self::treasury_block_number()
									{
										// Slash the child-bounty curator if
										// + the call is made by the parent bounty curator.
										// + or the curator is inactive.
										slash_curator(curator, &mut child_bounty.curator_deposit);
									// Continue to change bounty status below.
									} else {
										// Curator has more time to give an update.
										return Err(BountiesError::<T>::Premature.into())
									}
								},
							}
						},
						ChildBountyStatus::PendingPayout { ref curator, .. } => {
							let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
							ensure!(
								maybe_sender.map_or(true, |sender| parent_curator == sender),
								BadOrigin,
							);
							slash_curator(curator, &mut child_bounty.curator_deposit);
							// Continue to change child-bounty status below.
						},
					};
					// Move the child-bounty state to Added.
					child_bounty.status = ChildBountyStatus::Added;
					Ok(())
				},
			)
		}

		/// Award child-bounty to a beneficiary.
		///
		/// The beneficiary will be able to claim the funds after a delay.
		///
		/// The dispatch origin for this call must be the parent curator or
		/// curator of this child-bounty.
		///
		/// Parent bounty must be in active state, for this child-bounty call to
		/// work.
		///
		/// Child-bounty must be in active state, for processing the call. And
		/// state of child-bounty is moved to "PendingPayout" on successful call
		/// completion.
		///
		/// - `parent_bounty_id`: Index of parent bounty.
		/// - `child_bounty_id`: Index of child bounty.
		/// - `beneficiary`: Beneficiary account.
		#[pallet::call_index(4)]
		#[pallet::weight(<T as Config>::WeightInfo::award_child_bounty())]
		pub fn award_child_bounty(
			origin: OriginFor<T>,
			#[pallet::compact] parent_bounty_id: BountyIndex,
			#[pallet::compact] child_bounty_id: BountyIndex,
		) -> DispatchResult {
			let signer = ensure_signed(origin)?;
			let beneficiary = T::Lookup::lookup(beneficiary)?;

			// Ensure parent bounty exists, and is active.
			let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;

			ChildBounties::<T>::try_mutate_exists(
				parent_bounty_id,
				child_bounty_id,
				|maybe_child_bounty| -> DispatchResult {
						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;

					// Ensure child-bounty is in active state.
					if let ChildBountyStatus::Active { ref curator } = child_bounty.status {
						ensure!(
							signer == *curator || signer == parent_curator,
							BountiesError::<T>::RequireCurator,
						);
						// Move the child-bounty state to pending payout.
						child_bounty.status = ChildBountyStatus::PendingPayout {
							curator: signer,
							beneficiary: beneficiary.clone(),
							unlock_at: Self::treasury_block_number() +
								T::BountyDepositPayoutDelay::get(),
						};
						Ok(())
					} else {
						Err(BountiesError::<T>::UnexpectedStatus.into())
					}
				},
			)?;

			// Trigger the event Awarded.
			Self::deposit_event(Event::<T>::Awarded {
				index: parent_bounty_id,
				child_index: child_bounty_id,
				beneficiary,
			});

			Ok(())
		}

		/// Claim the payout from an awarded child-bounty after payout delay.
		///
		/// The dispatch origin for this call may be any signed origin.
		///
		/// Call works independent of parent bounty state, No need for parent
		/// bounty to be in active state.
		///
		/// The Beneficiary is paid out with agreed bounty value. Curator fee is
		/// paid & curator deposit is unreserved.
		///
		/// Child-bounty must be in "PendingPayout" state, for processing the
		/// call. And instance of child-bounty is removed from the state on
		/// successful call completion.
		///
		/// - `parent_bounty_id`: Index of parent bounty.
		/// - `child_bounty_id`: Index of child bounty.
		#[pallet::call_index(5)]
		#[pallet::weight(<T as Config>::WeightInfo::claim_child_bounty())]
		pub fn claim_child_bounty(
			origin: OriginFor<T>,
			#[pallet::compact] parent_bounty_id: BountyIndex,
			#[pallet::compact] child_bounty_id: BountyIndex,
		) -> DispatchResult {
			let _ = ensure_signed(origin)?;

			// Ensure child-bounty is in expected state.
			ChildBounties::<T>::try_mutate_exists(
				parent_bounty_id,
				child_bounty_id,
				|maybe_child_bounty| -> DispatchResult {
					let child_bounty =
						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;

					if let ChildBountyStatus::PendingPayout {
						ref curator,
						ref beneficiary,
						ref unlock_at,
					} = child_bounty.status
					{
						// Ensure block number is elapsed for processing the
						// claim.
						ensure!(
							Self::treasury_block_number() >= *unlock_at,
							BountiesError::<T>::Premature,
						);

						// Make curator fee payment.
						let child_bounty_account =
							Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
						let balance = T::Currency::free_balance(&child_bounty_account);
						let curator_fee = child_bounty.fee.min(balance);
						let payout = balance.saturating_sub(curator_fee);

						// Unreserve the curator deposit. Should not fail
						// because the deposit is always reserved when curator is
						// assigned.
						let _ = T::Currency::unreserve(curator, child_bounty.curator_deposit);

						// Make payout to child-bounty curator.
						// Should not fail because curator fee is always less than bounty value.
						let fee_transfer_result = T::Currency::transfer(
							&child_bounty_account,
							curator_fee,
							AllowDeath,
						);
						debug_assert!(fee_transfer_result.is_ok());

						// Make payout to beneficiary.
						// Should not fail.
						let payout_transfer_result = T::Currency::transfer(
							&child_bounty_account,
							beneficiary,
							payout,
							AllowDeath,
						);
						debug_assert!(payout_transfer_result.is_ok());

						// Trigger the Claimed event.
						Self::deposit_event(Event::<T>::Claimed {
							index: parent_bounty_id,
							child_index: child_bounty_id,
							payout,
							beneficiary: beneficiary.clone(),
						});

						// Update the active child-bounty tracking count.
						ParentChildBounties::<T>::mutate(parent_bounty_id, |count| {
							count.saturating_dec()
						});

						// Remove the child-bounty description.
						ChildBountyDescriptionsV1::<T>::remove(parent_bounty_id, child_bounty_id);

						// Remove the child-bounty instance from the state.
						*maybe_child_bounty = None;

						Ok(())
					} else {
						Err(BountiesError::<T>::UnexpectedStatus.into())
					}
				},
			)
		}

		/// Cancel a proposed or active child-bounty. Child-bounty account funds
		/// are transferred to parent bounty account. The child-bounty curator
		/// deposit may be unreserved if possible.
		///
		/// The dispatch origin for this call must be either parent curator or
		/// `T::RejectOrigin`.
		///
		/// If the state of child-bounty is `Active`, curator deposit is
		/// unreserved.
		///
		/// If the state of child-bounty is `PendingPayout`, call fails &
		/// returns `PendingPayout` error.
		///
		/// For the origin other than T::RejectOrigin, parent bounty must be in
		/// active state, for this child-bounty call to work. For origin
		/// T::RejectOrigin execution is forced.
		///
		/// Instance of child-bounty is removed from the state on successful
		/// call completion.
		///
		/// - `parent_bounty_id`: Index of parent bounty.
		/// - `child_bounty_id`: Index of child bounty.
		#[pallet::call_index(6)]
		#[pallet::weight(<T as Config>::WeightInfo::close_child_bounty_added()
			.max(<T as Config>::WeightInfo::close_child_bounty_active()))]
		pub fn close_child_bounty(
			origin: OriginFor<T>,
			#[pallet::compact] parent_bounty_id: BountyIndex,
			#[pallet::compact] child_bounty_id: BountyIndex,
		) -> DispatchResult {
			let maybe_sender = ensure_signed(origin.clone())
				.map(Some)
				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;

			// Ensure parent bounty exist, get parent curator.
			let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;

			ensure!(maybe_sender.map_or(true, |sender| parent_curator == sender), BadOrigin);

			Self::impl_close_child_bounty(parent_bounty_id, child_bounty_id)?;
			Ok(())
		}
	}
	impl<T: Config> Hooks<SystemBlockNumberFor<T>> for Pallet<T> {
		fn integrity_test() {
			let parent_bounty_id: BountyIndex = 1;
			let child_bounty_id: BountyIndex = 2;
			let _: T::AccountId = T::PalletId::get()
				.try_into_sub_account(("cb", parent_bounty_id, child_bounty_id))
				.expect(
					"The `AccountId` type must be large enough to fit the child bounty account ID.",
				);
		}
	}
}

impl<T: Config> Pallet<T> {
	/// Get the block number used in the treasury pallet.
	///
	/// It may be configured to use the relay chain block number on a parachain.
	pub fn treasury_block_number() -> BlockNumberFor<T> {
		<T as pallet_treasury::Config>::BlockNumberProvider::current_block_number()
	}

	// This function will calculate the deposit of a curator.
	fn calculate_curator_deposit(
		parent_curator: &T::AccountId,
		child_curator: &T::AccountId,
		bounty_fee: &BalanceOf<T>,
	) -> BalanceOf<T> {
		if parent_curator == child_curator {
			return Zero::zero()
		}

		// We just use the same logic from the parent bounties pallet.
		pallet_bounties::Pallet::<T>::calculate_curator_deposit(bounty_fee)
	}

	/// The account ID of a child-bounty account.
	pub fn child_bounty_account_id(
		parent_bounty_id: BountyIndex,
		child_bounty_id: BountyIndex,
	) -> T::AccountId {
		// This function is taken from the parent (bounties) pallet, but the
		// prefix is changed to have different AccountId when the index of
		// parent and child is same.
		T::PalletId::get().into_sub_account_truncating(("cb", parent_bounty_id, child_bounty_id))
	}

	fn create_child_bounty(
		parent_bounty_id: BountyIndex,
		child_bounty_id: BountyIndex,
		child_bounty_value: BalanceOf<T>,
		description: BoundedVec<u8, T::MaximumReasonLength>,
	) {
		let child_bounty = ChildBounty {
			parent_bounty: parent_bounty_id,
			value: child_bounty_value,
			fee: 0u32.into(),
			curator_deposit: 0u32.into(),
			status: ChildBountyStatus::Added,
		};
		ChildBounties::<T>::insert(parent_bounty_id, child_bounty_id, &child_bounty);
		ChildBountyDescriptionsV1::<T>::insert(parent_bounty_id, child_bounty_id, description);
		Self::deposit_event(Event::Added { index: parent_bounty_id, child_index: child_bounty_id });
	}

	fn ensure_bounty_active(
		bounty_id: BountyIndex,
	) -> Result<(T::AccountId, BlockNumberFor<T>), DispatchError> {
		let parent_bounty = pallet_bounties::Bounties::<T>::get(bounty_id)
			.ok_or(BountiesError::<T>::InvalidIndex)?;
		if let BountyStatus::Active { curator, update_due } = parent_bounty.get_status() {
			Ok((curator, update_due))
		} else {
			Err(Error::<T>::ParentBountyNotActive.into())
		}
	}

	fn impl_close_child_bounty(
		parent_bounty_id: BountyIndex,
		child_bounty_id: BountyIndex,
	) -> DispatchResult {
		ChildBounties::<T>::try_mutate_exists(
			parent_bounty_id,
			child_bounty_id,
			|maybe_child_bounty| -> DispatchResult {
				let child_bounty =
					maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;

				match &child_bounty.status {
					ChildBountyStatus::Added | ChildBountyStatus::CuratorProposed { .. } => {
						// Nothing extra to do besides the removal of the child-bounty below.
					},
					ChildBountyStatus::Active { curator } => {
						// Cancelled by parent curator or RejectOrigin,
						// refund deposit of the working child-bounty curator.
						let _ = T::Currency::unreserve(curator, child_bounty.curator_deposit);
						// Then execute removal of the child-bounty below.
					},
					ChildBountyStatus::PendingPayout { .. } => {
						// Child-bounty is already in pending payout. If parent
						// curator or RejectOrigin wants to close this
						// child-bounty, it should mean the child-bounty curator
						// was acting maliciously. So first unassign the
						// child-bounty curator, slashing their deposit.
						return Err(BountiesError::<T>::PendingPayout.into())
					},
				}

				// Revert the curator fee back to parent bounty curator &
				// reduce the active child-bounty count.
				ChildrenCuratorFees::<T>::mutate(parent_bounty_id, |value| {
					*value = value.saturating_sub(child_bounty.fee)
				});
				ParentChildBounties::<T>::mutate(parent_bounty_id, |count| {
					*count = count.saturating_sub(1)
				});

				// Transfer fund from child-bounty to parent bounty.
				let parent_bounty_account =
					pallet_bounties::Pallet::<T>::bounty_account_id(parent_bounty_id);
				let child_bounty_account =
					Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
				let balance = T::Currency::free_balance(&child_bounty_account);
				let transfer_result = T::Currency::transfer(
					&child_bounty_account,
					&parent_bounty_account,
					balance,
					AllowDeath,
				); // Should not fail; child bounty account gets this balance during creation.
				debug_assert!(transfer_result.is_ok());

				// Remove the child-bounty description.
				ChildBountyDescriptionsV1::<T>::remove(parent_bounty_id, child_bounty_id);

				*maybe_child_bounty = None;

				Self::deposit_event(Event::<T>::Canceled {
					index: parent_bounty_id,
					child_index: child_bounty_id,
				});
				Ok(())
			},
		)
	}
}

/// Implement ChildBountyManager to connect with the bounties pallet. This is
/// where we pass the active child bounties and child curator fees to the parent
/// bounty.
///
/// Function `children_curator_fees` not only returns the fee but also removes cumulative curator
/// fees during call.
impl<T: Config> pallet_bounties::ChildBountyManager<BalanceOf<T>> for Pallet<T> {
	/// Returns number of active child bounties for `bounty_id`
	fn child_bounties_count(
		bounty_id: pallet_bounties::BountyIndex,
	) -> pallet_bounties::BountyIndex {
		ParentChildBounties::<T>::get(bounty_id)
	/// Returns cumulative child bounty curator fees for `bounty_id` also removing the associated
	/// storage item. This function is assumed to be called when parent bounty is claimed.
	fn children_curator_fees(bounty_id: pallet_bounties::BountyIndex) -> BalanceOf<T> {
		// This is asked for when the parent bounty is being claimed. No use of
		// keeping it in state after that. Hence removing.
		let children_fee_total = ChildrenCuratorFees::<T>::get(bounty_id);
		ChildrenCuratorFees::<T>::remove(bounty_id);
		children_fee_total
	}

	/// Clean up the storage on a parent bounty removal.
	fn bounty_removed(bounty_id: BountyIndex) {
		debug_assert!(ParentChildBounties::<T>::get(bounty_id).is_zero());
		debug_assert!(ChildrenCuratorFees::<T>::get(bounty_id).is_zero());
		debug_assert!(ChildBounties::<T>::iter_key_prefix(bounty_id).count().is_zero());
		debug_assert!(ChildBountyDescriptionsV1::<T>::iter_key_prefix(bounty_id).count().is_zero());
		ParentChildBounties::<T>::remove(bounty_id);
		ParentTotalChildBounties::<T>::remove(bounty_id);
	}