referrerpolicy=no-referrer-when-downgrade

pallet_child_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//! # Child Bounties Pallet ( `pallet-child-bounties` )
19//!
20//! ## Child Bounty
21//!
22//! > NOTE: This pallet is tightly coupled with `pallet-treasury` and `pallet-bounties`.
23//!
24//! With child bounties, a large bounty proposal can be divided into smaller chunks,
25//! for parallel execution, and for efficient governance and tracking of spent funds.
26//! A child bounty is a smaller piece of work, extracted from a parent bounty.
27//! A curator is assigned after the child bounty is created by the parent bounty curator,
28//! to be delegated with the responsibility of assigning a payout address once the specified
29//! set of tasks is completed.
30//!
31//! ## Interface
32//!
33//! ### Dispatchable Functions
34//!
35//! Child Bounty protocol:
36//! - `add_child_bounty` - Add a child bounty for a parent bounty to for dividing the work in
37//!   smaller tasks.
38//! - `propose_curator` - Assign an account to a child bounty as candidate curator.
39//! - `accept_curator` - Accept a child bounty assignment from the parent bounty curator, setting a
40//!   curator deposit.
41//! - `award_child_bounty` - Close and pay out the specified amount for the completed work.
42//! - `claim_child_bounty` - Claim a specific child bounty amount from the payout address.
43//! - `unassign_curator` - Unassign an accepted curator from a specific child bounty.
44//! - `close_child_bounty` - Cancel the child bounty for a specific treasury amount and close the
45//!   bounty.
46
47// Most of the business logic in this pallet has been
48// originally contributed by "https://github.com/shamb0",
49// as part of the PR - https://github.com/paritytech/substrate/pull/7965.
50// The code has been moved here and then refactored in order to
51// extract child bounties as a separate pallet.
52
53#![cfg_attr(not(feature = "std"), no_std)]
54
55mod benchmarking;
56pub mod migration;
57mod tests;
58pub mod weights;
59
60extern crate alloc;
61
62/// The log target for this pallet.
63const LOG_TARGET: &str = "runtime::child-bounties";
64
65use alloc::vec::Vec;
66
67use frame_support::traits::{
68	Currency,
69	ExistenceRequirement::{AllowDeath, KeepAlive},
70	Get, OnUnbalanced, ReservableCurrency, WithdrawReasons,
71};
72
73use sp_runtime::{
74	traits::{
75		AccountIdConversion, BadOrigin, BlockNumberProvider, CheckedSub, Saturating, StaticLookup,
76		Zero,
77	},
78	Debug, DispatchResult,
79};
80
81use frame_support::pallet_prelude::*;
82use frame_system::pallet_prelude::{
83	ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
84};
85use pallet_bounties::BountyStatus;
86use scale_info::TypeInfo;
87pub use weights::WeightInfo;
88
89pub use pallet::*;
90
91pub type BalanceOf<T> = pallet_treasury::BalanceOf<T>;
92pub type BountiesError<T> = pallet_bounties::Error<T>;
93pub type BountyIndex = pallet_bounties::BountyIndex;
94pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
95pub type BlockNumberFor<T> =
96	<<T as pallet_treasury::Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
97
98/// A child bounty proposal.
99#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
100pub struct ChildBounty<AccountId, Balance, BlockNumber> {
101	/// The parent of this child-bounty.
102	pub parent_bounty: BountyIndex,
103	/// The (total) amount that should be paid if this child-bounty is rewarded.
104	pub value: Balance,
105	/// The child bounty curator fee.
106	pub fee: Balance,
107	/// The deposit of child-bounty curator.
108	pub curator_deposit: Balance,
109	/// The status of this child-bounty.
110	pub status: ChildBountyStatus<AccountId, BlockNumber>,
111}
112
113/// The status of a child-bounty.
114#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
115pub enum ChildBountyStatus<AccountId, BlockNumber> {
116	/// The child-bounty is added and waiting for curator assignment.
117	Added,
118	/// A curator has been proposed by the parent bounty curator. Waiting for
119	/// acceptance from the child-bounty curator.
120	CuratorProposed {
121		/// The assigned child-bounty curator of this bounty.
122		curator: AccountId,
123	},
124	/// The child-bounty is active and waiting to be awarded.
125	Active {
126		/// The curator of this child-bounty.
127		curator: AccountId,
128	},
129	/// The child-bounty is awarded and waiting to released after a delay.
130	PendingPayout {
131		/// The curator of this child-bounty.
132		curator: AccountId,
133		/// The beneficiary of the child-bounty.
134		beneficiary: AccountId,
135		/// When the child-bounty can be claimed.
136		unlock_at: BlockNumber,
137	},
138}
139
140#[frame_support::pallet]
141pub mod pallet {
142
143	use super::*;
144
145	/// The in-code storage version.
146	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
147
148	#[pallet::pallet]
149	#[pallet::storage_version(STORAGE_VERSION)]
150	pub struct Pallet<T>(_);
151
152	#[pallet::config]
153	pub trait Config:
154		frame_system::Config + pallet_treasury::Config + pallet_bounties::Config
155	{
156		/// Maximum number of child bounties that can be added to a parent bounty.
157		#[pallet::constant]
158		type MaxActiveChildBountyCount: Get<u32>;
159
160		/// Minimum value for a child-bounty.
161		#[pallet::constant]
162		type ChildBountyValueMinimum: Get<BalanceOf<Self>>;
163
164		/// The overarching event type.
165		#[allow(deprecated)]
166		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
167
168		/// Weight information for extrinsics in this pallet.
169		type WeightInfo: WeightInfo;
170	}
171
172	#[pallet::error]
173	pub enum Error<T> {
174		/// The parent bounty is not in active state.
175		ParentBountyNotActive,
176		/// The bounty balance is not enough to add new child-bounty.
177		InsufficientBountyBalance,
178		/// Number of child bounties exceeds limit `MaxActiveChildBountyCount`.
179		TooManyChildBounties,
180	}
181
182	#[pallet::event]
183	#[pallet::generate_deposit(pub(super) fn deposit_event)]
184	pub enum Event<T: Config> {
185		/// A child-bounty is added.
186		Added { index: BountyIndex, child_index: BountyIndex },
187		/// A child-bounty is awarded to a beneficiary.
188		Awarded { index: BountyIndex, child_index: BountyIndex, beneficiary: T::AccountId },
189		/// A child-bounty is claimed by beneficiary.
190		Claimed {
191			index: BountyIndex,
192			child_index: BountyIndex,
193			payout: BalanceOf<T>,
194			beneficiary: T::AccountId,
195		},
196		/// A child-bounty is cancelled.
197		Canceled { index: BountyIndex, child_index: BountyIndex },
198	}
199
200	/// Number of active child bounties per parent bounty.
201	/// Map of parent bounty index to number of child bounties.
202	#[pallet::storage]
203	pub type ParentChildBounties<T: Config> =
204		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
205
206	/// Number of total child bounties per parent bounty, including completed bounties.
207	#[pallet::storage]
208	pub type ParentTotalChildBounties<T: Config> =
209		StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
210
211	/// Child bounties that have been added.
212	#[pallet::storage]
213	pub type ChildBounties<T: Config> = StorageDoubleMap<
214		_,
215		Twox64Concat,
216		BountyIndex,
217		Twox64Concat,
218		BountyIndex,
219		ChildBounty<T::AccountId, BalanceOf<T>, BlockNumberFor<T>>,
220	>;
221
222	/// The description of each child-bounty. Indexed by `(parent_id, child_id)`.
223	///
224	/// This item replaces the `ChildBountyDescriptions` storage item from the V0 storage version.
225	#[pallet::storage]
226	pub type ChildBountyDescriptionsV1<T: Config> = StorageDoubleMap<
227		_,
228		Twox64Concat,
229		BountyIndex,
230		Twox64Concat,
231		BountyIndex,
232		BoundedVec<u8, T::MaximumReasonLength>,
233	>;
234
235	/// The mapping of the child bounty ids from storage version `V0` to the new `V1` version.
236	///
237	/// The `V0` ids were based on a single total child-bounty count. The `V1` version ids are
238	/// based on the child bounty count per parent bounty [`ParentTotalChildBounties`].
239	/// The item intended solely for client convenience and not used in the pallet's core logic.
240	#[pallet::storage]
241	pub type V0ToV1ChildBountyIds<T: Config> =
242		StorageMap<_, Twox64Concat, BountyIndex, (BountyIndex, BountyIndex)>;
243
244	/// The cumulative child-bounty curator fee for each parent bounty.
245	#[pallet::storage]
246	pub type ChildrenCuratorFees<T: Config> =
247		StorageMap<_, Twox64Concat, BountyIndex, BalanceOf<T>, ValueQuery>;
248
249	#[pallet::call]
250	impl<T: Config> Pallet<T> {
251		/// Add a new child-bounty.
252		///
253		/// The dispatch origin for this call must be the curator of parent
254		/// bounty and the parent bounty must be in "active" state.
255		///
256		/// Child-bounty gets added successfully & fund gets transferred from
257		/// parent bounty to child-bounty account, if parent bounty has enough
258		/// funds, else the call fails.
259		///
260		/// Upper bound to maximum number of active  child bounties that can be
261		/// added are managed via runtime trait config
262		/// [`Config::MaxActiveChildBountyCount`].
263		///
264		/// If the call is success, the status of child-bounty is updated to
265		/// "Added".
266		///
267		/// - `parent_bounty_id`: Index of parent bounty for which child-bounty is being added.
268		/// - `value`: Value for executing the proposal.
269		/// - `description`: Text description for the child-bounty.
270		#[pallet::call_index(0)]
271		#[pallet::weight(<T as Config>::WeightInfo::add_child_bounty(description.len() as u32))]
272		pub fn add_child_bounty(
273			origin: OriginFor<T>,
274			#[pallet::compact] parent_bounty_id: BountyIndex,
275			#[pallet::compact] value: BalanceOf<T>,
276			description: Vec<u8>,
277		) -> DispatchResult {
278			let signer = ensure_signed(origin)?;
279
280			// Verify the arguments.
281			let bounded_description =
282				description.try_into().map_err(|_| BountiesError::<T>::ReasonTooBig)?;
283			ensure!(value >= T::ChildBountyValueMinimum::get(), BountiesError::<T>::InvalidValue);
284			ensure!(
285				ParentChildBounties::<T>::get(parent_bounty_id) <
286					T::MaxActiveChildBountyCount::get(),
287				Error::<T>::TooManyChildBounties,
288			);
289
290			let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
291			ensure!(signer == curator, BountiesError::<T>::RequireCurator);
292
293			// Read parent bounty account info.
294			let parent_bounty_account =
295				pallet_bounties::Pallet::<T>::bounty_account_id(parent_bounty_id);
296
297			// Ensure parent bounty has enough balance after adding child-bounty.
298			let bounty_balance = T::Currency::free_balance(&parent_bounty_account);
299			let new_bounty_balance = bounty_balance
300				.checked_sub(&value)
301				.ok_or(Error::<T>::InsufficientBountyBalance)?;
302			T::Currency::ensure_can_withdraw(
303				&parent_bounty_account,
304				value,
305				WithdrawReasons::TRANSFER,
306				new_bounty_balance,
307			)?;
308
309			// Get child-bounty ID.
310			let child_bounty_id = ParentTotalChildBounties::<T>::get(parent_bounty_id);
311			let child_bounty_account =
312				Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
313
314			// Transfer funds from parent bounty to child-bounty.
315			T::Currency::transfer(&parent_bounty_account, &child_bounty_account, value, KeepAlive)?;
316
317			// Increment the active child-bounty count.
318			ParentChildBounties::<T>::mutate(parent_bounty_id, |count| count.saturating_inc());
319			ParentTotalChildBounties::<T>::insert(
320				parent_bounty_id,
321				child_bounty_id.saturating_add(1),
322			);
323
324			// Create child-bounty instance.
325			Self::create_child_bounty(
326				parent_bounty_id,
327				child_bounty_id,
328				value,
329				bounded_description,
330			);
331			Ok(())
332		}
333
334		/// Propose curator for funded child-bounty.
335		///
336		/// The dispatch origin for this call must be curator of parent bounty.
337		///
338		/// Parent bounty must be in active state, for this child-bounty call to
339		/// work.
340		///
341		/// Child-bounty must be in "Added" state, for processing the call. And
342		/// state of child-bounty is moved to "CuratorProposed" on successful
343		/// call completion.
344		///
345		/// - `parent_bounty_id`: Index of parent bounty.
346		/// - `child_bounty_id`: Index of child bounty.
347		/// - `curator`: Address of child-bounty curator.
348		/// - `fee`: payment fee to child-bounty curator for execution.
349		#[pallet::call_index(1)]
350		#[pallet::weight(<T as Config>::WeightInfo::propose_curator())]
351		pub fn propose_curator(
352			origin: OriginFor<T>,
353			#[pallet::compact] parent_bounty_id: BountyIndex,
354			#[pallet::compact] child_bounty_id: BountyIndex,
355			curator: AccountIdLookupOf<T>,
356			#[pallet::compact] fee: BalanceOf<T>,
357		) -> DispatchResult {
358			let signer = ensure_signed(origin)?;
359			let child_bounty_curator = T::Lookup::lookup(curator)?;
360
361			let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
362			ensure!(signer == curator, BountiesError::<T>::RequireCurator);
363
364			// Mutate the child-bounty instance.
365			ChildBounties::<T>::try_mutate_exists(
366				parent_bounty_id,
367				child_bounty_id,
368				|maybe_child_bounty| -> DispatchResult {
369					let child_bounty =
370						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
371
372					// Ensure child-bounty is in expected state.
373					ensure!(
374						child_bounty.status == ChildBountyStatus::Added,
375						BountiesError::<T>::UnexpectedStatus,
376					);
377
378					// Ensure child-bounty curator fee is less than child-bounty value.
379					ensure!(fee < child_bounty.value, BountiesError::<T>::InvalidFee);
380
381					// Add child-bounty curator fee to the cumulative sum. To be
382					// subtracted from the parent bounty curator when claiming
383					// bounty.
384					ChildrenCuratorFees::<T>::mutate(parent_bounty_id, |value| {
385						*value = value.saturating_add(fee)
386					});
387
388					// Update the child-bounty curator fee.
389					child_bounty.fee = fee;
390
391					// Update the child-bounty state.
392					child_bounty.status =
393						ChildBountyStatus::CuratorProposed { curator: child_bounty_curator };
394
395					Ok(())
396				},
397			)
398		}
399
400		/// Accept the curator role for the child-bounty.
401		///
402		/// The dispatch origin for this call must be the curator of this
403		/// child-bounty.
404		///
405		/// A deposit will be reserved from the curator and refund upon
406		/// successful payout or cancellation.
407		///
408		/// Fee for curator is deducted from curator fee of parent bounty.
409		///
410		/// Parent bounty must be in active state, for this child-bounty call to
411		/// work.
412		///
413		/// Child-bounty must be in "CuratorProposed" state, for processing the
414		/// call. And state of child-bounty is moved to "Active" on successful
415		/// call completion.
416		///
417		/// - `parent_bounty_id`: Index of parent bounty.
418		/// - `child_bounty_id`: Index of child bounty.
419		#[pallet::call_index(2)]
420		#[pallet::weight(<T as Config>::WeightInfo::accept_curator())]
421		pub fn accept_curator(
422			origin: OriginFor<T>,
423			#[pallet::compact] parent_bounty_id: BountyIndex,
424			#[pallet::compact] child_bounty_id: BountyIndex,
425		) -> DispatchResult {
426			let signer = ensure_signed(origin)?;
427
428			let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
429			// Mutate child-bounty.
430			ChildBounties::<T>::try_mutate_exists(
431				parent_bounty_id,
432				child_bounty_id,
433				|maybe_child_bounty| -> DispatchResult {
434					let child_bounty =
435						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
436
437					// Ensure child-bounty is in expected state.
438					if let ChildBountyStatus::CuratorProposed { ref curator } = child_bounty.status
439					{
440						ensure!(signer == *curator, BountiesError::<T>::RequireCurator);
441
442						// Reserve child-bounty curator deposit.
443						let deposit = Self::calculate_curator_deposit(
444							&parent_curator,
445							curator,
446							&child_bounty.fee,
447						);
448
449						T::Currency::reserve(curator, deposit)?;
450						child_bounty.curator_deposit = deposit;
451
452						child_bounty.status =
453							ChildBountyStatus::Active { curator: curator.clone() };
454						Ok(())
455					} else {
456						Err(BountiesError::<T>::UnexpectedStatus.into())
457					}
458				},
459			)
460		}
461
462		/// Unassign curator from a child-bounty.
463		///
464		/// The dispatch origin for this call can be either `RejectOrigin`, or
465		/// the curator of the parent bounty, or any signed origin.
466		///
467		/// For the origin other than T::RejectOrigin and the child-bounty
468		/// curator, parent bounty must be in active state, for this call to
469		/// work. We allow child-bounty curator and T::RejectOrigin to execute
470		/// this call irrespective of the parent bounty state.
471		///
472		/// If this function is called by the `RejectOrigin` or the
473		/// parent bounty curator, we assume that the child-bounty curator is
474		/// malicious or inactive. As a result, child-bounty curator deposit is
475		/// slashed.
476		///
477		/// If the origin is the child-bounty curator, we take this as a sign
478		/// that they are unable to do their job, and are willingly giving up.
479		/// We could slash the deposit, but for now we allow them to unreserve
480		/// their deposit and exit without issue. (We may want to change this if
481		/// it is abused.)
482		///
483		/// Finally, the origin can be anyone iff the child-bounty curator is
484		/// "inactive". Expiry update due of parent bounty is used to estimate
485		/// inactive state of child-bounty curator.
486		///
487		/// This allows anyone in the community to call out that a child-bounty
488		/// curator is not doing their due diligence, and we should pick a new
489		/// one. In this case the child-bounty curator deposit is slashed.
490		///
491		/// State of child-bounty is moved to Added state on successful call
492		/// completion.
493		///
494		/// - `parent_bounty_id`: Index of parent bounty.
495		/// - `child_bounty_id`: Index of child bounty.
496		#[pallet::call_index(3)]
497		#[pallet::weight(<T as Config>::WeightInfo::unassign_curator())]
498		pub fn unassign_curator(
499			origin: OriginFor<T>,
500			#[pallet::compact] parent_bounty_id: BountyIndex,
501			#[pallet::compact] child_bounty_id: BountyIndex,
502		) -> DispatchResult {
503			let maybe_sender = ensure_signed(origin.clone())
504				.map(Some)
505				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
506
507			ChildBounties::<T>::try_mutate_exists(
508				parent_bounty_id,
509				child_bounty_id,
510				|maybe_child_bounty| -> DispatchResult {
511					let child_bounty =
512						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
513
514					let slash_curator =
515						|curator: &T::AccountId, curator_deposit: &mut BalanceOf<T>| {
516							let imbalance =
517								T::Currency::slash_reserved(curator, *curator_deposit).0;
518							T::OnSlash::on_unbalanced(imbalance);
519							*curator_deposit = Zero::zero();
520						};
521
522					match child_bounty.status {
523						ChildBountyStatus::Added => {
524							// No curator to unassign at this point.
525							return Err(BountiesError::<T>::UnexpectedStatus.into());
526						},
527						ChildBountyStatus::CuratorProposed { ref curator } => {
528							// A child-bounty curator has been proposed, but not accepted yet.
529							// Either `RejectOrigin`, parent bounty curator or the proposed
530							// child-bounty curator can unassign the child-bounty curator.
531							ensure!(
532								maybe_sender.map_or(true, |sender| {
533									sender == *curator ||
534										Self::ensure_bounty_active(parent_bounty_id)
535											.map_or(false, |(parent_curator, _)| {
536												sender == parent_curator
537											})
538								}),
539								BadOrigin
540							);
541							// Continue to change bounty status below.
542						},
543						ChildBountyStatus::Active { ref curator } => {
544							// The child-bounty is active.
545							match maybe_sender {
546								// If the `RejectOrigin` is calling this function, slash the curator
547								// deposit.
548								None => {
549									slash_curator(curator, &mut child_bounty.curator_deposit);
550									// Continue to change child-bounty status below.
551								},
552								Some(sender) if sender == *curator => {
553									// This is the child-bounty curator, willingly giving up their
554									// role. Give back their deposit.
555									T::Currency::unreserve(curator, child_bounty.curator_deposit);
556									// Reset curator deposit.
557									child_bounty.curator_deposit = Zero::zero();
558									// Continue to change bounty status below.
559								},
560								Some(sender) => {
561									let (parent_curator, update_due) =
562										Self::ensure_bounty_active(parent_bounty_id)?;
563									if sender == parent_curator ||
564										update_due < Self::treasury_block_number()
565									{
566										// Slash the child-bounty curator if
567										// + the call is made by the parent bounty curator.
568										// + or the curator is inactive.
569										slash_curator(curator, &mut child_bounty.curator_deposit);
570									// Continue to change bounty status below.
571									} else {
572										// Curator has more time to give an update.
573										return Err(BountiesError::<T>::Premature.into());
574									}
575								},
576							}
577						},
578						ChildBountyStatus::PendingPayout { ref curator, .. } => {
579							let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
580							ensure!(
581								maybe_sender.map_or(true, |sender| parent_curator == sender),
582								BadOrigin,
583							);
584							slash_curator(curator, &mut child_bounty.curator_deposit);
585							// Continue to change child-bounty status below.
586						},
587					};
588					// Move the child-bounty state to Added.
589					child_bounty.status = ChildBountyStatus::Added;
590					Ok(())
591				},
592			)
593		}
594
595		/// Award child-bounty to a beneficiary.
596		///
597		/// The beneficiary will be able to claim the funds after a delay.
598		///
599		/// The dispatch origin for this call must be the parent curator or
600		/// curator of this child-bounty.
601		///
602		/// Parent bounty must be in active state, for this child-bounty call to
603		/// work.
604		///
605		/// Child-bounty must be in active state, for processing the call. And
606		/// state of child-bounty is moved to "PendingPayout" on successful call
607		/// completion.
608		///
609		/// - `parent_bounty_id`: Index of parent bounty.
610		/// - `child_bounty_id`: Index of child bounty.
611		/// - `beneficiary`: Beneficiary account.
612		#[pallet::call_index(4)]
613		#[pallet::weight(<T as Config>::WeightInfo::award_child_bounty())]
614		pub fn award_child_bounty(
615			origin: OriginFor<T>,
616			#[pallet::compact] parent_bounty_id: BountyIndex,
617			#[pallet::compact] child_bounty_id: BountyIndex,
618			beneficiary: AccountIdLookupOf<T>,
619		) -> DispatchResult {
620			let signer = ensure_signed(origin)?;
621			let beneficiary = T::Lookup::lookup(beneficiary)?;
622
623			// Ensure parent bounty exists, and is active.
624			let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
625
626			ChildBounties::<T>::try_mutate_exists(
627				parent_bounty_id,
628				child_bounty_id,
629				|maybe_child_bounty| -> DispatchResult {
630					let child_bounty =
631						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
632
633					// Ensure child-bounty is in active state.
634					if let ChildBountyStatus::Active { ref curator } = child_bounty.status {
635						ensure!(
636							signer == *curator || signer == parent_curator,
637							BountiesError::<T>::RequireCurator,
638						);
639						// Move the child-bounty state to pending payout.
640						child_bounty.status = ChildBountyStatus::PendingPayout {
641							curator: signer,
642							beneficiary: beneficiary.clone(),
643							unlock_at: Self::treasury_block_number() +
644								T::BountyDepositPayoutDelay::get(),
645						};
646						Ok(())
647					} else {
648						Err(BountiesError::<T>::UnexpectedStatus.into())
649					}
650				},
651			)?;
652
653			// Trigger the event Awarded.
654			Self::deposit_event(Event::<T>::Awarded {
655				index: parent_bounty_id,
656				child_index: child_bounty_id,
657				beneficiary,
658			});
659
660			Ok(())
661		}
662
663		/// Claim the payout from an awarded child-bounty after payout delay.
664		///
665		/// The dispatch origin for this call may be any signed origin.
666		///
667		/// Call works independent of parent bounty state, No need for parent
668		/// bounty to be in active state.
669		///
670		/// The Beneficiary is paid out with agreed bounty value. Curator fee is
671		/// paid & curator deposit is unreserved.
672		///
673		/// Child-bounty must be in "PendingPayout" state, for processing the
674		/// call. And instance of child-bounty is removed from the state on
675		/// successful call completion.
676		///
677		/// - `parent_bounty_id`: Index of parent bounty.
678		/// - `child_bounty_id`: Index of child bounty.
679		#[pallet::call_index(5)]
680		#[pallet::weight(<T as Config>::WeightInfo::claim_child_bounty())]
681		pub fn claim_child_bounty(
682			origin: OriginFor<T>,
683			#[pallet::compact] parent_bounty_id: BountyIndex,
684			#[pallet::compact] child_bounty_id: BountyIndex,
685		) -> DispatchResult {
686			ensure_signed(origin)?;
687
688			// Ensure child-bounty is in expected state.
689			ChildBounties::<T>::try_mutate_exists(
690				parent_bounty_id,
691				child_bounty_id,
692				|maybe_child_bounty| -> DispatchResult {
693					let child_bounty =
694						maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
695
696					if let ChildBountyStatus::PendingPayout {
697						ref curator,
698						ref beneficiary,
699						ref unlock_at,
700					} = child_bounty.status
701					{
702						// Ensure block number is elapsed for processing the
703						// claim.
704						ensure!(
705							Self::treasury_block_number() >= *unlock_at,
706							BountiesError::<T>::Premature,
707						);
708
709						// Make curator fee payment.
710						let child_bounty_account =
711							Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
712						let balance = T::Currency::free_balance(&child_bounty_account);
713						let curator_fee = child_bounty.fee.min(balance);
714						let payout = balance.saturating_sub(curator_fee);
715
716						// Unreserve the curator deposit. Should not fail
717						// because the deposit is always reserved when curator is
718						// assigned.
719						let _ = T::Currency::unreserve(curator, child_bounty.curator_deposit);
720
721						// Make payout to child-bounty curator.
722						// Should not fail because curator fee is always less than bounty value.
723						let fee_transfer_result = T::Currency::transfer(
724							&child_bounty_account,
725							curator,
726							curator_fee,
727							AllowDeath,
728						);
729						debug_assert!(fee_transfer_result.is_ok());
730
731						// Make payout to beneficiary.
732						// Should not fail.
733						let payout_transfer_result = T::Currency::transfer(
734							&child_bounty_account,
735							beneficiary,
736							payout,
737							AllowDeath,
738						);
739						debug_assert!(payout_transfer_result.is_ok());
740
741						// Trigger the Claimed event.
742						Self::deposit_event(Event::<T>::Claimed {
743							index: parent_bounty_id,
744							child_index: child_bounty_id,
745							payout,
746							beneficiary: beneficiary.clone(),
747						});
748
749						// Update the active child-bounty tracking count.
750						ParentChildBounties::<T>::mutate(parent_bounty_id, |count| {
751							count.saturating_dec()
752						});
753
754						// Remove the child-bounty description.
755						ChildBountyDescriptionsV1::<T>::remove(parent_bounty_id, child_bounty_id);
756
757						// Remove the child-bounty instance from the state.
758						*maybe_child_bounty = None;
759
760						Ok(())
761					} else {
762						Err(BountiesError::<T>::UnexpectedStatus.into())
763					}
764				},
765			)
766		}
767
768		/// Cancel a proposed or active child-bounty. Child-bounty account funds
769		/// are transferred to parent bounty account. The child-bounty curator
770		/// deposit may be unreserved if possible.
771		///
772		/// The dispatch origin for this call must be either parent curator or
773		/// `T::RejectOrigin`.
774		///
775		/// If the state of child-bounty is `Active`, curator deposit is
776		/// unreserved.
777		///
778		/// If the state of child-bounty is `PendingPayout`, call fails &
779		/// returns `PendingPayout` error.
780		///
781		/// For the origin other than T::RejectOrigin, parent bounty must be in
782		/// active state, for this child-bounty call to work. For origin
783		/// T::RejectOrigin execution is forced.
784		///
785		/// Instance of child-bounty is removed from the state on successful
786		/// call completion.
787		///
788		/// - `parent_bounty_id`: Index of parent bounty.
789		/// - `child_bounty_id`: Index of child bounty.
790		#[pallet::call_index(6)]
791		#[pallet::weight(<T as Config>::WeightInfo::close_child_bounty_added()
792			.max(<T as Config>::WeightInfo::close_child_bounty_active()))]
793		pub fn close_child_bounty(
794			origin: OriginFor<T>,
795			#[pallet::compact] parent_bounty_id: BountyIndex,
796			#[pallet::compact] child_bounty_id: BountyIndex,
797		) -> DispatchResult {
798			let maybe_sender = ensure_signed(origin.clone())
799				.map(Some)
800				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
801
802			// Ensure parent bounty exist, get parent curator.
803			let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?;
804
805			ensure!(maybe_sender.map_or(true, |sender| parent_curator == sender), BadOrigin);
806
807			Self::impl_close_child_bounty(parent_bounty_id, child_bounty_id)?;
808			Ok(())
809		}
810	}
811
812	#[pallet::hooks]
813	impl<T: Config> Hooks<SystemBlockNumberFor<T>> for Pallet<T> {
814		fn integrity_test() {
815			let parent_bounty_id: BountyIndex = 1;
816			let child_bounty_id: BountyIndex = 2;
817			let _: T::AccountId = T::PalletId::get()
818				.try_into_sub_account(("cb", parent_bounty_id, child_bounty_id))
819				.expect(
820					"The `AccountId` type must be large enough to fit the child bounty account ID.",
821				);
822		}
823	}
824}
825
826impl<T: Config> Pallet<T> {
827	/// Get the block number used in the treasury pallet.
828	///
829	/// It may be configured to use the relay chain block number on a parachain.
830	pub fn treasury_block_number() -> BlockNumberFor<T> {
831		<T as pallet_treasury::Config>::BlockNumberProvider::current_block_number()
832	}
833
834	// This function will calculate the deposit of a curator.
835	fn calculate_curator_deposit(
836		parent_curator: &T::AccountId,
837		child_curator: &T::AccountId,
838		bounty_fee: &BalanceOf<T>,
839	) -> BalanceOf<T> {
840		if parent_curator == child_curator {
841			return Zero::zero();
842		}
843
844		// We just use the same logic from the parent bounties pallet.
845		pallet_bounties::Pallet::<T>::calculate_curator_deposit(bounty_fee)
846	}
847
848	/// The account ID of a child-bounty account.
849	pub fn child_bounty_account_id(
850		parent_bounty_id: BountyIndex,
851		child_bounty_id: BountyIndex,
852	) -> T::AccountId {
853		// This function is taken from the parent (bounties) pallet, but the
854		// prefix is changed to have different AccountId when the index of
855		// parent and child is same.
856		T::PalletId::get().into_sub_account_truncating(("cb", parent_bounty_id, child_bounty_id))
857	}
858
859	fn create_child_bounty(
860		parent_bounty_id: BountyIndex,
861		child_bounty_id: BountyIndex,
862		child_bounty_value: BalanceOf<T>,
863		description: BoundedVec<u8, T::MaximumReasonLength>,
864	) {
865		let child_bounty = ChildBounty {
866			parent_bounty: parent_bounty_id,
867			value: child_bounty_value,
868			fee: 0u32.into(),
869			curator_deposit: 0u32.into(),
870			status: ChildBountyStatus::Added,
871		};
872		ChildBounties::<T>::insert(parent_bounty_id, child_bounty_id, &child_bounty);
873		ChildBountyDescriptionsV1::<T>::insert(parent_bounty_id, child_bounty_id, description);
874		Self::deposit_event(Event::Added { index: parent_bounty_id, child_index: child_bounty_id });
875	}
876
877	fn ensure_bounty_active(
878		bounty_id: BountyIndex,
879	) -> Result<(T::AccountId, BlockNumberFor<T>), DispatchError> {
880		let parent_bounty = pallet_bounties::Bounties::<T>::get(bounty_id)
881			.ok_or(BountiesError::<T>::InvalidIndex)?;
882		if let BountyStatus::Active { curator, update_due } = parent_bounty.get_status() {
883			Ok((curator, update_due))
884		} else {
885			Err(Error::<T>::ParentBountyNotActive.into())
886		}
887	}
888
889	fn impl_close_child_bounty(
890		parent_bounty_id: BountyIndex,
891		child_bounty_id: BountyIndex,
892	) -> DispatchResult {
893		ChildBounties::<T>::try_mutate_exists(
894			parent_bounty_id,
895			child_bounty_id,
896			|maybe_child_bounty| -> DispatchResult {
897				let child_bounty =
898					maybe_child_bounty.as_mut().ok_or(BountiesError::<T>::InvalidIndex)?;
899
900				match &child_bounty.status {
901					ChildBountyStatus::Added | ChildBountyStatus::CuratorProposed { .. } => {
902						// Nothing extra to do besides the removal of the child-bounty below.
903					},
904					ChildBountyStatus::Active { curator } => {
905						// Cancelled by parent curator or RejectOrigin,
906						// refund deposit of the working child-bounty curator.
907						let _ = T::Currency::unreserve(curator, child_bounty.curator_deposit);
908						// Then execute removal of the child-bounty below.
909					},
910					ChildBountyStatus::PendingPayout { .. } => {
911						// Child-bounty is already in pending payout. If parent
912						// curator or RejectOrigin wants to close this
913						// child-bounty, it should mean the child-bounty curator
914						// was acting maliciously. So first unassign the
915						// child-bounty curator, slashing their deposit.
916						return Err(BountiesError::<T>::PendingPayout.into());
917					},
918				}
919
920				// Revert the curator fee back to parent bounty curator &
921				// reduce the active child-bounty count.
922				ChildrenCuratorFees::<T>::mutate(parent_bounty_id, |value| {
923					*value = value.saturating_sub(child_bounty.fee)
924				});
925				ParentChildBounties::<T>::mutate(parent_bounty_id, |count| {
926					*count = count.saturating_sub(1)
927				});
928
929				// Transfer fund from child-bounty to parent bounty.
930				let parent_bounty_account =
931					pallet_bounties::Pallet::<T>::bounty_account_id(parent_bounty_id);
932				let child_bounty_account =
933					Self::child_bounty_account_id(parent_bounty_id, child_bounty_id);
934				let balance = T::Currency::free_balance(&child_bounty_account);
935				let transfer_result = T::Currency::transfer(
936					&child_bounty_account,
937					&parent_bounty_account,
938					balance,
939					AllowDeath,
940				); // Should not fail; child bounty account gets this balance during creation.
941				debug_assert!(transfer_result.is_ok());
942
943				// Remove the child-bounty description.
944				ChildBountyDescriptionsV1::<T>::remove(parent_bounty_id, child_bounty_id);
945
946				*maybe_child_bounty = None;
947
948				Self::deposit_event(Event::<T>::Canceled {
949					index: parent_bounty_id,
950					child_index: child_bounty_id,
951				});
952				Ok(())
953			},
954		)
955	}
956}
957
958/// Implement ChildBountyManager to connect with the bounties pallet. This is
959/// where we pass the active child bounties and child curator fees to the parent
960/// bounty.
961///
962/// Function `children_curator_fees` not only returns the fee but also removes cumulative curator
963/// fees during call.
964impl<T: Config> pallet_bounties::ChildBountyManager<BalanceOf<T>> for Pallet<T> {
965	/// Returns number of active child bounties for `bounty_id`
966	fn child_bounties_count(
967		bounty_id: pallet_bounties::BountyIndex,
968	) -> pallet_bounties::BountyIndex {
969		ParentChildBounties::<T>::get(bounty_id)
970	}
971
972	/// Returns cumulative child bounty curator fees for `bounty_id` also removing the associated
973	/// storage item. This function is assumed to be called when parent bounty is claimed.
974	fn children_curator_fees(bounty_id: pallet_bounties::BountyIndex) -> BalanceOf<T> {
975		// This is asked for when the parent bounty is being claimed. No use of
976		// keeping it in state after that. Hence removing.
977		let children_fee_total = ChildrenCuratorFees::<T>::get(bounty_id);
978		ChildrenCuratorFees::<T>::remove(bounty_id);
979		children_fee_total
980	}
981
982	/// Clean up the storage on a parent bounty removal.
983	fn bounty_removed(bounty_id: BountyIndex) {
984		debug_assert!(ParentChildBounties::<T>::get(bounty_id).is_zero());
985		debug_assert!(ChildrenCuratorFees::<T>::get(bounty_id).is_zero());
986		debug_assert!(ChildBounties::<T>::iter_key_prefix(bounty_id).count().is_zero());
987		debug_assert!(ChildBountyDescriptionsV1::<T>::iter_key_prefix(bounty_id).count().is_zero());
988		ParentChildBounties::<T>::remove(bounty_id);
989		ParentTotalChildBounties::<T>::remove(bounty_id);
990	}
991}