referrerpolicy=no-referrer-when-downgrade

pallet_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//! # Bounties Module ( pallet-bounties )
19//!
20//! ## Bounty
21//!
22//! > NOTE: This pallet is tightly coupled with pallet-treasury.
23//!
24//! A Bounty Spending is a reward for a specified body of work - or specified set of objectives -
25//! that needs to be executed for a predefined Treasury amount to be paid out. A curator is assigned
26//! after the bounty is approved and funded by Council, to be delegated with the responsibility of
27//! assigning a payout address once the specified set of objectives is completed.
28//!
29//! After the Council has activated a bounty, it delegates the work that requires expertise to a
30//! curator in exchange of a deposit. Once the curator accepts the bounty, they get to close the
31//! active bounty. Closing the active bounty enacts a delayed payout to the payout address, the
32//! curator fee and the return of the curator deposit. The delay allows for intervention through
33//! regular democracy. The Council gets to unassign the curator, resulting in a new curator
34//! election. The Council also gets to cancel the bounty if deemed necessary before assigning a
35//! curator or once the bounty is active or payout is pending, resulting in the slash of the
36//! curator's deposit.
37//!
38//! This pallet may opt into using a [`ChildBountyManager`] that enables bounties to be split into
39//! sub-bounties, as children of an established bounty (called the parent in the context of it's
40//! children).
41//!
42//! > NOTE: The parent bounty cannot be closed if it has a non-zero number of it has active child
43//! > bounties associated with it.
44//!
45//! ### Terminology
46//!
47//! Bounty:
48//!
49//! - **Bounty spending proposal:** A proposal to reward a predefined body of work upon completion
50//!   by the Treasury.
51//! - **Proposer:** An account proposing a bounty spending.
52//! - **Curator:** An account managing the bounty and assigning a payout address receiving the
53//!   reward for the completion of work.
54//! - **Deposit:** The amount held on deposit for placing a bounty proposal plus the amount held on
55//!   deposit per byte within the bounty description.
56//! - **Curator deposit:** The payment from a candidate willing to curate an approved bounty. The
57//!   deposit is returned when/if the bounty is completed.
58//! - **Bounty value:** The total amount that should be paid to the Payout Address if the bounty is
59//!   rewarded.
60//! - **Payout address:** The account to which the total or part of the bounty is assigned to.
61//! - **Payout Delay:** The delay period for which a bounty beneficiary needs to wait before
62//!   claiming.
63//! - **Curator fee:** The reserved upfront payment for a curator for work related to the bounty.
64//!
65//! ## Interface
66//!
67//! ### Dispatchable Functions
68//!
69//! Bounty protocol:
70//!
71//! - `propose_bounty` - Propose a specific treasury amount to be earmarked for a predefined set of
72//!   tasks and stake the required deposit.
73//! - `approve_bounty` - Accept a specific treasury amount to be earmarked for a predefined body of
74//!   work.
75//! - `propose_curator` - Assign an account to a bounty as candidate curator.
76//! - `approve_bounty_with_curator` - Accept a specific treasury amount for a predefined body of
77//!   work with assigned candidate curator account.
78//! - `accept_curator` - Accept a bounty assignment from the Council, setting a curator deposit.
79//! - `extend_bounty_expiry` - Extend the expiry block number of the bounty and stay active.
80//! - `award_bounty` - Close and pay out the specified amount for the completed work.
81//! - `claim_bounty` - Claim a specific bounty amount from the Payout Address.
82//! - `unassign_curator` - Unassign an accepted curator from a specific earmark.
83//! - `close_bounty` - Cancel the earmark for a specific treasury amount and close the bounty.
84
85#![recursion_limit = "512"]
86#![cfg_attr(not(feature = "std"), no_std)]
87
88#[cfg(feature = "runtime-benchmarks")]
89mod benchmarking;
90pub mod migrations;
91mod tests;
92pub mod weights;
93
94extern crate alloc;
95
96use alloc::vec::Vec;
97
98use frame_support::traits::{
99	fungible::Mutate as FungibleMutate,
100	fungibles::{
101		Create as FungiblesCreate, Inspect as FungiblesInspect, Mutate as FungiblesMutate,
102	},
103	tokens::{Fortitude, Preservation},
104	Currency,
105	ExistenceRequirement::AllowDeath,
106	Get, Imbalance, OnUnbalanced, ReservableCurrency,
107};
108
109use sp_runtime::{
110	traits::{AccountIdConversion, BadOrigin, BlockNumberProvider, Saturating, StaticLookup, Zero},
111	Debug, DispatchResult, Permill,
112};
113
114use frame_support::{
115	dispatch::DispatchResultWithPostInfo, pallet_prelude::*, traits::EnsureOrigin,
116};
117use frame_system::pallet_prelude::{
118	ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
119};
120use scale_info::TypeInfo;
121pub use weights::WeightInfo;
122
123pub use pallet::*;
124
125type BalanceOf<T, I = ()> = pallet_treasury::BalanceOf<T, I>;
126
127type PositiveImbalanceOf<T, I = ()> = pallet_treasury::PositiveImbalanceOf<T, I>;
128
129/// An index of a bounty. Just a `u32`.
130pub type BountyIndex = u32;
131
132type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
133
134type BlockNumberFor<T, I = ()> =
135	<<T as pallet_treasury::Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
136
137/// A bounty proposal.
138#[derive(
139	Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
140)]
141pub struct Bounty<AccountId, Balance, BlockNumber> {
142	/// The account proposing it.
143	pub proposer: AccountId,
144	/// The (total) amount that should be paid if the bounty is rewarded.
145	pub value: Balance,
146	/// The curator fee. Included in value.
147	pub fee: Balance,
148	/// The deposit of curator.
149	pub curator_deposit: Balance,
150	/// The amount held on deposit (reserved) for making this proposal.
151	bond: Balance,
152	/// The status of this bounty.
153	status: BountyStatus<AccountId, BlockNumber>,
154}
155
156impl<AccountId: PartialEq + Clone + Ord, Balance, BlockNumber: Clone>
157	Bounty<AccountId, Balance, BlockNumber>
158{
159	/// Getter for bounty status, to be used for child bounties.
160	pub fn get_status(&self) -> BountyStatus<AccountId, BlockNumber> {
161		self.status.clone()
162	}
163}
164
165/// The status of a bounty proposal.
166#[derive(
167	Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
168)]
169pub enum BountyStatus<AccountId, BlockNumber> {
170	/// The bounty is proposed and waiting for approval.
171	Proposed,
172	/// The bounty is approved and waiting to become active at next spend period.
173	Approved,
174	/// The bounty is funded and waiting for curator assignment.
175	Funded,
176	/// A curator has been proposed. Waiting for acceptance from the curator.
177	CuratorProposed {
178		/// The assigned curator of this bounty.
179		curator: AccountId,
180	},
181	/// The bounty is active and waiting to be awarded.
182	Active {
183		/// The curator of this bounty.
184		curator: AccountId,
185		/// An update from the curator is due by this block, else they are considered inactive.
186		update_due: BlockNumber,
187	},
188	/// The bounty is awarded and waiting to released after a delay.
189	PendingPayout {
190		/// The curator of this bounty.
191		curator: AccountId,
192		/// The beneficiary of the bounty.
193		beneficiary: AccountId,
194		/// When the bounty can be claimed.
195		unlock_at: BlockNumber,
196	},
197	/// The bounty is approved with curator assigned.
198	ApprovedWithCurator {
199		/// The assigned curator of this bounty.
200		curator: AccountId,
201	},
202}
203
204/// The child bounty manager.
205pub trait ChildBountyManager<Balance> {
206	/// Get the active child bounties for a parent bounty.
207	fn child_bounties_count(bounty_id: BountyIndex) -> BountyIndex;
208
209	/// Take total curator fees of children-bounty curators.
210	fn children_curator_fees(bounty_id: BountyIndex) -> Balance;
211
212	/// Hook called when a parent bounty is removed.
213	fn bounty_removed(bounty_id: BountyIndex);
214}
215
216/// Transfer all assets held by an account (e.g. a stale bounty sub-account) back to another.
217pub trait TransferAllAssets<AccountId> {
218	/// Transfer all assets from one account to another, possibly reaping `from`.
219	///
220	/// Returns `true` if any balance was actually transferred, `false` if the account was already
221	/// empty.
222	fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> Result<bool, DispatchError>;
223
224	/// Mint benchmark amounts of each relevant asset into `from`.
225	///
226	/// Called during benchmarking so that `force_transfer_all_assets` exercises real transfers
227	/// instead of being a no-op. The default implementation is a no-op.
228	#[cfg(feature = "runtime-benchmarks")]
229	fn ensure_successful(_from: &AccountId) {}
230}
231
232impl<AccountId> TransferAllAssets<AccountId> for () {
233	fn force_transfer_all_assets(_: &AccountId, _: &AccountId) -> Result<bool, DispatchError> {
234		Ok(false)
235	}
236}
237
238/// Transfer the entire balance of a single `fungible::Mutate` currency from one account to
239/// another.
240///
241/// Suitable for runtimes that expose exactly one relevant currency (e.g. native-only runtimes
242/// without multi-asset support). For runtimes with multi-asset support, prefer
243/// [`TransferAllFungibles`] with all relevant asset IDs in `RelevantAssets`.
244pub struct TransferFungible<AccountId, Currency>(core::marker::PhantomData<(AccountId, Currency)>);
245impl<AccountId, C> TransferAllAssets<AccountId> for TransferFungible<AccountId, C>
246where
247	C: FungibleMutate<AccountId>,
248	AccountId: Eq,
249{
250	fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> Result<bool, DispatchError> {
251		let balance = C::reducible_balance(from, Preservation::Expendable, Fortitude::Polite);
252		if balance.is_zero() {
253			return Ok(false);
254		}
255		C::transfer(from, to, balance, Preservation::Expendable)?;
256		Ok(true)
257	}
258
259	#[cfg(feature = "runtime-benchmarks")]
260	fn ensure_successful(from: &AccountId) {
261		let _ = C::mint_into(from, 1_000_000u32.into());
262	}
263}
264
265/// Transfer all `RelevantAssets` of the `Fungibles` from one account to another.
266///
267/// The native asset should be the first in the list of `RelevantAssets`, otherwise the transfers
268/// of the other maybe fails.
269pub struct TransferAllFungibles<AccountId, Fungibles, RelevantAssets>(
270	core::marker::PhantomData<(AccountId, Fungibles, RelevantAssets)>,
271);
272impl<AccountId, Fungibles, RelevantAssets> TransferAllAssets<AccountId>
273	for TransferAllFungibles<AccountId, Fungibles, RelevantAssets>
274where
275	Fungibles: FungiblesMutate<AccountId> + FungiblesCreate<AccountId>,
276	RelevantAssets: Get<Vec<<Fungibles as FungiblesInspect<AccountId>>::AssetId>>,
277	AccountId: Eq + Clone,
278{
279	fn force_transfer_all_assets(from: &AccountId, to: &AccountId) -> Result<bool, DispatchError> {
280		// We iterate through all assets twice in case that the Native asset was not last in the
281		// list and ED remained because of an insufficient asset at the end of the list.
282		let assets_twice =
283			RelevantAssets::get().into_iter().chain(RelevantAssets::get().into_iter());
284
285		let mut transferred_any = false;
286		for id in assets_twice {
287			let balance = Fungibles::reducible_balance(
288				id.clone(),
289				from,
290				Preservation::Expendable,
291				Fortitude::Polite,
292			);
293			if balance.is_zero() {
294				continue;
295			}
296
297			// Ignore errors since this can only fail if the receiver does not exist.
298			if Fungibles::transfer(id, from, to, balance, Preservation::Expendable).is_ok() {
299				transferred_any = true;
300			}
301		}
302		Ok(transferred_any)
303	}
304
305	#[cfg(feature = "runtime-benchmarks")]
306	fn ensure_successful(from: &AccountId) {
307		for id in RelevantAssets::get() {
308			if !Fungibles::asset_exists(id.clone()) {
309				// For native assets, Create is a no-op; for fungible assets this ensures the
310				// asset exists before minting so the benchmark exercises real transfers.
311				Fungibles::create(id.clone(), from.clone(), true, 1u32.into())
312					.expect("asset creation should succeed in benchmarks");
313			}
314			let _ = Fungibles::mint_into(id, from, 1_000u32.into());
315		}
316	}
317}
318
319#[frame_support::pallet]
320pub mod pallet {
321	use super::*;
322
323	const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
324
325	#[pallet::pallet]
326	#[pallet::storage_version(STORAGE_VERSION)]
327	pub struct Pallet<T, I = ()>(_);
328
329	#[pallet::config]
330	pub trait Config<I: 'static = ()>: frame_system::Config + pallet_treasury::Config<I> {
331		/// The amount held on deposit for placing a bounty proposal.
332		#[pallet::constant]
333		type BountyDepositBase: Get<BalanceOf<Self, I>>;
334
335		/// The delay period for which a bounty beneficiary need to wait before claim the payout.
336		#[pallet::constant]
337		type BountyDepositPayoutDelay: Get<BlockNumberFor<Self, I>>;
338
339		/// The time limit for a curator to act before a bounty expires.
340		///
341		/// The period that starts when a curator is approved, during which they must execute or
342		/// update the bounty via `extend_bounty_expiry`. If missed, the bounty expires, and the
343		/// curator may be slashed. If `BlockNumberFor::MAX`, bounties stay active indefinitely,
344		/// removing the need for `extend_bounty_expiry`.
345		#[pallet::constant]
346		type BountyUpdatePeriod: Get<BlockNumberFor<Self, I>>;
347
348		/// The curator deposit is calculated as a percentage of the curator fee.
349		///
350		/// This deposit has optional upper and lower bounds with `CuratorDepositMax` and
351		/// `CuratorDepositMin`.
352		#[pallet::constant]
353		type CuratorDepositMultiplier: Get<Permill>;
354
355		/// Maximum amount of funds that should be placed in a deposit for making a proposal.
356		#[pallet::constant]
357		type CuratorDepositMax: Get<Option<BalanceOf<Self, I>>>;
358
359		/// Minimum amount of funds that should be placed in a deposit for making a proposal.
360		#[pallet::constant]
361		type CuratorDepositMin: Get<Option<BalanceOf<Self, I>>>;
362
363		/// Minimum value for a bounty.
364		#[pallet::constant]
365		type BountyValueMinimum: Get<BalanceOf<Self, I>>;
366
367		/// The amount held on deposit per byte within the tip report reason or bounty description.
368		#[pallet::constant]
369		type DataDepositPerByte: Get<BalanceOf<Self, I>>;
370
371		/// The overarching event type.
372		#[allow(deprecated)]
373		type RuntimeEvent: From<Event<Self, I>>
374			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
375
376		/// Maximum acceptable reason length.
377		///
378		/// Benchmarks depend on this value, be sure to update weights file when changing this value
379		#[pallet::constant]
380		type MaximumReasonLength: Get<u32>;
381
382		/// Weight information for extrinsics in this pallet.
383		type WeightInfo: WeightInfo;
384
385		/// The child bounty manager.
386		type ChildBountyManager: ChildBountyManager<BalanceOf<Self, I>>;
387
388		/// Handler for the unbalanced decrease when slashing for a rejected bounty.
389		type OnSlash: OnUnbalanced<pallet_treasury::NegativeImbalanceOf<Self, I>>;
390
391		/// Means to transfer all assets from one account to another.
392		///
393		/// This is only used for bounty closure to ensure that all assets are returned to the
394		/// treasury.
395		type TransferAllAssets: TransferAllAssets<Self::AccountId>;
396	}
397
398	#[pallet::error]
399	pub enum Error<T, I = ()> {
400		/// Proposer's balance is too low.
401		InsufficientProposersBalance,
402		/// No proposal or bounty at that index.
403		InvalidIndex,
404		/// The reason given is just too big.
405		ReasonTooBig,
406		/// The bounty status is unexpected.
407		UnexpectedStatus,
408		/// Require bounty curator.
409		RequireCurator,
410		/// Invalid bounty value.
411		InvalidValue,
412		/// Invalid bounty fee.
413		InvalidFee,
414		/// A bounty payout is pending.
415		/// To cancel the bounty, you must unassign and slash the curator.
416		PendingPayout,
417		/// The bounties cannot be claimed/closed because it's still in the countdown period.
418		Premature,
419		/// The bounty cannot be closed because it has active child bounties.
420		HasActiveChildBounty,
421		/// Too many approvals are already queued.
422		TooManyQueued,
423		/// User is not the proposer of the bounty.
424		NotProposer,
425		/// The bounty is still active and its account cannot be reclaimed.
426		BountyStillActive,
427	}
428
429	#[pallet::event]
430	#[pallet::generate_deposit(pub(super) fn deposit_event)]
431	pub enum Event<T: Config<I>, I: 'static = ()> {
432		/// New bounty proposal.
433		BountyProposed { index: BountyIndex },
434		/// A bounty proposal was rejected; funds were slashed.
435		BountyRejected { index: BountyIndex, bond: BalanceOf<T, I> },
436		/// A bounty proposal is funded and became active.
437		BountyBecameActive { index: BountyIndex },
438		/// A bounty is awarded to a beneficiary.
439		BountyAwarded { index: BountyIndex, beneficiary: T::AccountId },
440		/// A bounty is claimed by beneficiary.
441		BountyClaimed { index: BountyIndex, payout: BalanceOf<T, I>, beneficiary: T::AccountId },
442		/// A bounty is cancelled.
443		BountyCanceled { index: BountyIndex },
444		/// A bounty expiry is extended.
445		BountyExtended { index: BountyIndex },
446		/// A bounty is approved.
447		BountyApproved { index: BountyIndex },
448		/// A bounty curator is proposed.
449		CuratorProposed { bounty_id: BountyIndex, curator: T::AccountId },
450		/// A bounty curator is unassigned.
451		CuratorUnassigned { bounty_id: BountyIndex },
452		/// A bounty curator is accepted.
453		CuratorAccepted { bounty_id: BountyIndex, curator: T::AccountId },
454		/// A bounty deposit has been poked.
455		DepositPoked {
456			bounty_id: BountyIndex,
457			proposer: T::AccountId,
458			old_deposit: BalanceOf<T, I>,
459			new_deposit: BalanceOf<T, I>,
460		},
461		/// Stranded funds left in a closed bounty's account were reclaimed to the treasury.
462		BountyFundsReclaimed { bounty_id: BountyIndex },
463	}
464
465	/// Number of bounty proposals that have been made.
466	#[pallet::storage]
467	pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, BountyIndex, ValueQuery>;
468
469	/// Bounties that have been made.
470	#[pallet::storage]
471	pub type Bounties<T: Config<I>, I: 'static = ()> = StorageMap<
472		_,
473		Twox64Concat,
474		BountyIndex,
475		Bounty<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T, I>>,
476	>;
477
478	/// The description of each bounty.
479	#[pallet::storage]
480	pub type BountyDescriptions<T: Config<I>, I: 'static = ()> =
481		StorageMap<_, Twox64Concat, BountyIndex, BoundedVec<u8, T::MaximumReasonLength>>;
482
483	/// Bounty indices that have been approved but not yet funded.
484	#[pallet::storage]
485	#[allow(deprecated)]
486	pub type BountyApprovals<T: Config<I>, I: 'static = ()> =
487		StorageValue<_, BoundedVec<BountyIndex, T::MaxApprovals>, ValueQuery>;
488
489	#[pallet::call]
490	impl<T: Config<I>, I: 'static> Pallet<T, I> {
491		/// Propose a new bounty.
492		///
493		/// The dispatch origin for this call must be _Signed_.
494		///
495		/// Payment: `TipReportDepositBase` will be reserved from the origin account, as well as
496		/// `DataDepositPerByte` for each byte in `reason`. It will be unreserved upon approval,
497		/// or slashed when rejected.
498		///
499		/// - `curator`: The curator account whom will manage this bounty.
500		/// - `fee`: The curator fee.
501		/// - `value`: The total payment amount of this bounty, curator fee included.
502		/// - `description`: The description of this bounty.
503		#[pallet::call_index(0)]
504		#[pallet::weight(<T as Config<I>>::WeightInfo::propose_bounty(description.len() as u32))]
505		pub fn propose_bounty(
506			origin: OriginFor<T>,
507			#[pallet::compact] value: BalanceOf<T, I>,
508			description: Vec<u8>,
509		) -> DispatchResult {
510			let proposer = ensure_signed(origin)?;
511			Self::create_bounty(proposer, description, value)?;
512			Ok(())
513		}
514
515		/// Approve a bounty proposal. At a later time, the bounty will be funded and become active
516		/// and the original deposit will be returned.
517		///
518		/// May only be called from `T::SpendOrigin`.
519		///
520		/// ## Complexity
521		/// - O(1).
522		#[pallet::call_index(1)]
523		#[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty())]
524		pub fn approve_bounty(
525			origin: OriginFor<T>,
526			#[pallet::compact] bounty_id: BountyIndex,
527		) -> DispatchResult {
528			let max_amount = T::SpendOrigin::ensure_origin(origin)?;
529			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
530				let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
531				ensure!(
532					bounty.value <= max_amount,
533					pallet_treasury::Error::<T, I>::InsufficientPermission
534				);
535				ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
536
537				bounty.status = BountyStatus::Approved;
538
539				BountyApprovals::<T, I>::try_append(bounty_id)
540					.map_err(|()| Error::<T, I>::TooManyQueued)?;
541
542				Ok(())
543			})?;
544
545			Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
546			Ok(())
547		}
548
549		/// Propose a curator to a funded bounty.
550		///
551		/// May only be called from `T::SpendOrigin`.
552		///
553		/// ## Complexity
554		/// - O(1).
555		#[pallet::call_index(2)]
556		#[pallet::weight(<T as Config<I>>::WeightInfo::propose_curator())]
557		pub fn propose_curator(
558			origin: OriginFor<T>,
559			#[pallet::compact] bounty_id: BountyIndex,
560			curator: AccountIdLookupOf<T>,
561			#[pallet::compact] fee: BalanceOf<T, I>,
562		) -> DispatchResult {
563			let max_amount = T::SpendOrigin::ensure_origin(origin)?;
564
565			let curator = T::Lookup::lookup(curator)?;
566			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
567				let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
568				ensure!(
569					bounty.value <= max_amount,
570					pallet_treasury::Error::<T, I>::InsufficientPermission
571				);
572				match bounty.status {
573					BountyStatus::Funded => {},
574					_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
575				};
576
577				ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
578
579				bounty.status = BountyStatus::CuratorProposed { curator: curator.clone() };
580				bounty.fee = fee;
581
582				Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
583
584				Ok(())
585			})?;
586			Ok(())
587		}
588
589		/// Unassign curator from a bounty.
590		///
591		/// This function can only be called by the `RejectOrigin` a signed origin.
592		///
593		/// If this function is called by the `RejectOrigin`, we assume that the curator is
594		/// malicious or inactive. As a result, we will slash the curator when possible.
595		///
596		/// If the origin is the curator, we take this as a sign they are unable to do their job and
597		/// they willingly give up. We could slash them, but for now we allow them to recover their
598		/// deposit and exit without issue. (We may want to change this if it is abused.)
599		///
600		/// Finally, the origin can be anyone if and only if the curator is "inactive". This allows
601		/// anyone in the community to call out that a curator is not doing their due diligence, and
602		/// we should pick a new curator. In this case the curator should also be slashed.
603		///
604		/// ## Complexity
605		/// - O(1).
606		#[pallet::call_index(3)]
607		#[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
608		pub fn unassign_curator(
609			origin: OriginFor<T>,
610			#[pallet::compact] bounty_id: BountyIndex,
611		) -> DispatchResult {
612			let maybe_sender = ensure_signed(origin.clone())
613				.map(Some)
614				.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
615
616			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
617				let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
618
619				let slash_curator =
620					|curator: &T::AccountId, curator_deposit: &mut BalanceOf<T, I>| {
621						let imbalance = T::Currency::slash_reserved(curator, *curator_deposit).0;
622						T::OnSlash::on_unbalanced(imbalance);
623						*curator_deposit = Zero::zero();
624					};
625
626				match bounty.status {
627					BountyStatus::Proposed | BountyStatus::Approved | BountyStatus::Funded => {
628						// No curator to unassign at this point.
629						return Err(Error::<T, I>::UnexpectedStatus.into());
630					},
631					BountyStatus::ApprovedWithCurator { ref curator } => {
632						// Bounty not yet funded, but bounty was approved with curator.
633						// `RejectOrigin` or curator himself can unassign from this bounty.
634						ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
635						// This state can only be while the bounty is not yet funded so we return
636						// bounty to the `Approved` state without curator
637						bounty.status = BountyStatus::Approved;
638						return Ok(());
639					},
640					BountyStatus::CuratorProposed { ref curator } => {
641						// A curator has been proposed, but not accepted yet.
642						// Either `RejectOrigin` or the proposed curator can unassign the curator.
643						ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
644					},
645					BountyStatus::Active { ref curator, ref update_due } => {
646						// The bounty is active.
647						match maybe_sender {
648							// If the `RejectOrigin` is calling this function, slash the curator.
649							None => {
650								slash_curator(curator, &mut bounty.curator_deposit);
651								// Continue to change bounty status below...
652							},
653							Some(sender) => {
654								// If the sender is not the curator, and the curator is inactive,
655								// slash the curator.
656								if sender != *curator {
657									let block_number = Self::treasury_block_number();
658									if *update_due < block_number {
659										slash_curator(curator, &mut bounty.curator_deposit);
660									// Continue to change bounty status below...
661									} else {
662										// Curator has more time to give an update.
663										return Err(Error::<T, I>::Premature.into());
664									}
665								} else {
666									// Else this is the curator, willingly giving up their role.
667									// Give back their deposit.
668									let err_amount =
669										T::Currency::unreserve(curator, bounty.curator_deposit);
670									debug_assert!(err_amount.is_zero());
671									bounty.curator_deposit = Zero::zero();
672									// Continue to change bounty status below...
673								}
674							},
675						}
676					},
677					BountyStatus::PendingPayout { ref curator, .. } => {
678						// The bounty is pending payout, so only council can unassign a curator.
679						// By doing so, they are claiming the curator is acting maliciously, so
680						// we slash the curator.
681						ensure!(maybe_sender.is_none(), BadOrigin);
682						slash_curator(curator, &mut bounty.curator_deposit);
683						// Continue to change bounty status below...
684					},
685				};
686
687				bounty.status = BountyStatus::Funded;
688				Ok(())
689			})?;
690
691			Self::deposit_event(Event::<T, I>::CuratorUnassigned { bounty_id });
692			Ok(())
693		}
694
695		/// Accept the curator role for a bounty.
696		/// A deposit will be reserved from curator and refund upon successful payout.
697		///
698		/// May only be called from the curator.
699		///
700		/// ## Complexity
701		/// - O(1).
702		#[pallet::call_index(4)]
703		#[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
704		pub fn accept_curator(
705			origin: OriginFor<T>,
706			#[pallet::compact] bounty_id: BountyIndex,
707		) -> DispatchResult {
708			let signer = ensure_signed(origin)?;
709
710			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
711				let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
712
713				match bounty.status {
714					BountyStatus::CuratorProposed { ref curator } => {
715						ensure!(signer == *curator, Error::<T, I>::RequireCurator);
716
717						let deposit = Self::calculate_curator_deposit(&bounty.fee);
718						T::Currency::reserve(curator, deposit)?;
719						bounty.curator_deposit = deposit;
720
721						let update_due = Self::treasury_block_number()
722							.saturating_add(T::BountyUpdatePeriod::get());
723						bounty.status =
724							BountyStatus::Active { curator: curator.clone(), update_due };
725
726						Self::deposit_event(Event::<T, I>::CuratorAccepted {
727							bounty_id,
728							curator: signer,
729						});
730						Ok(())
731					},
732					_ => Err(Error::<T, I>::UnexpectedStatus.into()),
733				}
734			})?;
735			Ok(())
736		}
737
738		/// Award bounty to a beneficiary account. The beneficiary will be able to claim the funds
739		/// after a delay.
740		///
741		/// The dispatch origin for this call must be the curator of this bounty.
742		///
743		/// - `bounty_id`: Bounty ID to award.
744		/// - `beneficiary`: The beneficiary account whom will receive the payout.
745		///
746		/// ## Complexity
747		/// - O(1).
748		#[pallet::call_index(5)]
749		#[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
750		pub fn award_bounty(
751			origin: OriginFor<T>,
752			#[pallet::compact] bounty_id: BountyIndex,
753			beneficiary: AccountIdLookupOf<T>,
754		) -> DispatchResult {
755			let signer = ensure_signed(origin)?;
756			let beneficiary = T::Lookup::lookup(beneficiary)?;
757
758			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
759				let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
760
761				// Ensure no active child bounties before processing the call.
762				ensure!(
763					T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
764					Error::<T, I>::HasActiveChildBounty
765				);
766
767				match &bounty.status {
768					BountyStatus::Active { curator, .. } => {
769						ensure!(signer == *curator, Error::<T, I>::RequireCurator);
770					},
771					_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
772				}
773				bounty.status = BountyStatus::PendingPayout {
774					curator: signer,
775					beneficiary: beneficiary.clone(),
776					unlock_at: Self::treasury_block_number() + T::BountyDepositPayoutDelay::get(),
777				};
778
779				Ok(())
780			})?;
781
782			Self::deposit_event(Event::<T, I>::BountyAwarded { index: bounty_id, beneficiary });
783			Ok(())
784		}
785
786		/// Claim the payout from an awarded bounty after payout delay.
787		///
788		/// The dispatch origin for this call must be the beneficiary of this bounty.
789		///
790		/// - `bounty_id`: Bounty ID to claim.
791		///
792		/// ## Complexity
793		/// - O(1).
794		#[pallet::call_index(6)]
795		#[pallet::weight(<T as Config<I>>::WeightInfo::claim_bounty())]
796		pub fn claim_bounty(
797			origin: OriginFor<T>,
798			#[pallet::compact] bounty_id: BountyIndex,
799		) -> DispatchResult {
800			ensure_signed(origin)?; // anyone can trigger claim
801
802			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
803				let bounty = maybe_bounty.take().ok_or(Error::<T, I>::InvalidIndex)?;
804				if let BountyStatus::PendingPayout { curator, beneficiary, unlock_at } =
805					bounty.status
806				{
807					ensure!(Self::treasury_block_number() >= unlock_at, Error::<T, I>::Premature);
808					let bounty_account = Self::bounty_account_id(bounty_id);
809					let balance = T::Currency::free_balance(&bounty_account);
810					let fee = bounty.fee.min(balance); // just to be safe
811					let payout = balance.saturating_sub(fee);
812					let err_amount = T::Currency::unreserve(&curator, bounty.curator_deposit);
813					debug_assert!(err_amount.is_zero());
814
815					// Get total child bounties curator fees, and subtract it from the parent
816					// curator fee (the fee in present referenced bounty, `self`).
817					let children_fee = T::ChildBountyManager::children_curator_fees(bounty_id);
818					debug_assert!(children_fee <= fee);
819
820					let final_fee = fee.saturating_sub(children_fee);
821					let res =
822						T::Currency::transfer(&bounty_account, &curator, final_fee, AllowDeath); // should not fail
823					debug_assert!(res.is_ok());
824					let res =
825						T::Currency::transfer(&bounty_account, &beneficiary, payout, AllowDeath); // should not fail
826					debug_assert!(res.is_ok());
827
828					*maybe_bounty = None;
829
830					BountyDescriptions::<T, I>::remove(bounty_id);
831					T::ChildBountyManager::bounty_removed(bounty_id);
832
833					Self::deposit_event(Event::<T, I>::BountyClaimed {
834						index: bounty_id,
835						payout,
836						beneficiary,
837					});
838					Ok(())
839				} else {
840					Err(Error::<T, I>::UnexpectedStatus.into())
841				}
842			})?;
843			Ok(())
844		}
845
846		/// Cancel a proposed or active bounty. All the funds will be sent to treasury and
847		/// the curator deposit will be unreserved if possible.
848		///
849		/// Only `T::RejectOrigin` is able to cancel a bounty.
850		///
851		/// - `bounty_id`: Bounty ID to cancel.
852		///
853		/// ## Complexity
854		/// - O(1).
855		#[pallet::call_index(7)]
856		#[pallet::weight(<T as Config<I>>::WeightInfo::close_bounty_proposed()
857			.max(<T as Config<I>>::WeightInfo::close_bounty_active()))]
858		pub fn close_bounty(
859			origin: OriginFor<T>,
860			#[pallet::compact] bounty_id: BountyIndex,
861		) -> DispatchResultWithPostInfo {
862			T::RejectOrigin::ensure_origin(origin)?;
863
864			Bounties::<T, I>::try_mutate_exists(
865				bounty_id,
866				|maybe_bounty| -> DispatchResultWithPostInfo {
867					let bounty = maybe_bounty.as_ref().ok_or(Error::<T, I>::InvalidIndex)?;
868
869					// Ensure no active child bounties before processing the call.
870					ensure!(
871						T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
872						Error::<T, I>::HasActiveChildBounty
873					);
874
875					match &bounty.status {
876						BountyStatus::Proposed => {
877							// The reject origin would like to cancel a proposed bounty.
878							BountyDescriptions::<T, I>::remove(bounty_id);
879							let value = bounty.bond;
880							let imbalance = T::Currency::slash_reserved(&bounty.proposer, value).0;
881							T::OnSlash::on_unbalanced(imbalance);
882							*maybe_bounty = None;
883
884							Self::deposit_event(Event::<T, I>::BountyRejected {
885								index: bounty_id,
886								bond: value,
887							});
888							// Return early, nothing else to do.
889							return Ok(
890								Some(<T as Config<I>>::WeightInfo::close_bounty_proposed()).into()
891							);
892						},
893						BountyStatus::Approved | BountyStatus::ApprovedWithCurator { .. } => {
894							// For weight reasons, we don't allow a council to cancel in this phase.
895							// We ask for them to wait until it is funded before they can cancel.
896							return Err(Error::<T, I>::UnexpectedStatus.into());
897						},
898						BountyStatus::Funded | BountyStatus::CuratorProposed { .. } => {
899							// Nothing extra to do besides the removal of the bounty below.
900						},
901						BountyStatus::Active { curator, .. } => {
902							// Cancelled by council, refund deposit of the working curator.
903							let err_amount =
904								T::Currency::unreserve(curator, bounty.curator_deposit);
905							debug_assert!(err_amount.is_zero());
906							// Then execute removal of the bounty below.
907						},
908						BountyStatus::PendingPayout { .. } => {
909							// Bounty is already pending payout. If council wants to cancel
910							// this bounty, it should mean the curator was acting maliciously.
911							// So the council should first unassign the curator, slashing their
912							// deposit.
913							return Err(Error::<T, I>::PendingPayout.into());
914						},
915					}
916
917					let bounty_account = Self::bounty_account_id(bounty_id);
918
919					BountyDescriptions::<T, I>::remove(bounty_id);
920
921					T::TransferAllAssets::force_transfer_all_assets(
922						&bounty_account,
923						&Self::account_id(),
924					)?;
925
926					*maybe_bounty = None;
927					T::ChildBountyManager::bounty_removed(bounty_id);
928
929					Self::deposit_event(Event::<T, I>::BountyCanceled { index: bounty_id });
930					Ok(Some(<T as Config<I>>::WeightInfo::close_bounty_active()).into())
931				},
932			)
933		}
934
935		/// Extend the expiry time of an active bounty.
936		///
937		/// The dispatch origin for this call must be the curator of this bounty.
938		///
939		/// - `bounty_id`: Bounty ID to extend.
940		/// - `remark`: additional information.
941		///
942		/// ## Complexity
943		/// - O(1).
944		#[pallet::call_index(8)]
945		#[pallet::weight(<T as Config<I>>::WeightInfo::extend_bounty_expiry())]
946		pub fn extend_bounty_expiry(
947			origin: OriginFor<T>,
948			#[pallet::compact] bounty_id: BountyIndex,
949			_remark: Vec<u8>,
950		) -> DispatchResult {
951			let signer = ensure_signed(origin)?;
952
953			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
954				let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
955
956				match bounty.status {
957					BountyStatus::Active { ref curator, ref mut update_due } => {
958						ensure!(*curator == signer, Error::<T, I>::RequireCurator);
959						*update_due = Self::treasury_block_number()
960							.saturating_add(T::BountyUpdatePeriod::get())
961							.max(*update_due);
962					},
963					_ => return Err(Error::<T, I>::UnexpectedStatus.into()),
964				}
965
966				Ok(())
967			})?;
968
969			Self::deposit_event(Event::<T, I>::BountyExtended { index: bounty_id });
970			Ok(())
971		}
972
973		/// Approve bountry and propose a curator simultaneously.
974		/// This call is a shortcut to calling `approve_bounty` and `propose_curator` separately.
975		///
976		/// May only be called from `T::SpendOrigin`.
977		///
978		/// - `bounty_id`: Bounty ID to approve.
979		/// - `curator`: The curator account whom will manage this bounty.
980		/// - `fee`: The curator fee.
981		///
982		/// ## Complexity
983		/// - O(1).
984		#[pallet::call_index(9)]
985		#[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty_with_curator())]
986		pub fn approve_bounty_with_curator(
987			origin: OriginFor<T>,
988			#[pallet::compact] bounty_id: BountyIndex,
989			curator: AccountIdLookupOf<T>,
990			#[pallet::compact] fee: BalanceOf<T, I>,
991		) -> DispatchResult {
992			let max_amount = T::SpendOrigin::ensure_origin(origin)?;
993			let curator = T::Lookup::lookup(curator)?;
994			Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
995				// approve bounty
996				let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
997				ensure!(
998					bounty.value <= max_amount,
999					pallet_treasury::Error::<T, I>::InsufficientPermission
1000				);
1001				ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
1002				ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);
1003
1004				BountyApprovals::<T, I>::try_append(bounty_id)
1005					.map_err(|()| Error::<T, I>::TooManyQueued)?;
1006
1007				bounty.status = BountyStatus::ApprovedWithCurator { curator: curator.clone() };
1008				bounty.fee = fee;
1009
1010				Ok(())
1011			})?;
1012
1013			Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
1014			Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });
1015
1016			Ok(())
1017		}
1018
1019		/// Poke the deposit reserved for creating a bounty proposal.
1020		///
1021		/// This can be used by accounts to update their reserved amount.
1022		///
1023		/// The dispatch origin for this call must be _Signed_.
1024		///
1025		/// Parameters:
1026		/// - `bounty_id`: The bounty id for which to adjust the deposit.
1027		///
1028		/// If the deposit is updated, the difference will be reserved/unreserved from the
1029		/// proposer's account.
1030		///
1031		/// The transaction is made free if the deposit is updated and paid otherwise.
1032		///
1033		/// Emits `DepositPoked` if the deposit is updated.
1034		#[pallet::call_index(10)]
1035		#[pallet::weight(<T as Config<I>>::WeightInfo::poke_deposit())]
1036		pub fn poke_deposit(
1037			origin: OriginFor<T>,
1038			#[pallet::compact] bounty_id: BountyIndex,
1039		) -> DispatchResultWithPostInfo {
1040			ensure_signed(origin)?;
1041
1042			let deposit_updated = Self::poke_bounty_deposit(bounty_id)?;
1043
1044			Ok(if deposit_updated { Pays::No } else { Pays::Yes }.into())
1045		}
1046
1047		/// Reclaim funds stranded in a closed bounty's account back to the treasury.
1048		///
1049		/// Permissionless. Moves all remaining assets from a closed bounty's account back to the
1050		/// treasury in a single call. Which assets are swept depends on the `TransferAllAssets`
1051		/// configuration.
1052		///
1053		/// The call is free if funds were reclaimed and paid otherwise, so no-op calls cannot be
1054		/// used to grief the network. Emits `BountyFundsReclaimed` on success.
1055		///
1056		/// ## Complexity
1057		/// - O(A) where A is the number of relevant assets configured in `TransferAllAssets`.
1058		#[pallet::call_index(11)]
1059		#[pallet::weight(<T as Config<I>>::WeightInfo::reclaim_bounty_funds())]
1060		pub fn reclaim_bounty_funds(
1061			origin: OriginFor<T>,
1062			#[pallet::compact] bounty_id: BountyIndex,
1063		) -> DispatchResultWithPostInfo {
1064			ensure_signed(origin)?;
1065
1066			// A live bounty still manages its account, so leave it untouched.
1067			ensure!(!Bounties::<T, I>::contains_key(bounty_id), Error::<T, I>::BountyStillActive);
1068
1069			debug_assert!(
1070				T::ChildBountyManager::child_bounties_count(bounty_id) == 0,
1071				"child bounties should not exist for a closed bounty"
1072			);
1073
1074			let bounty_account = Self::bounty_account_id(bounty_id);
1075			let treasury_account = Self::account_id();
1076
1077			let transferred = T::TransferAllAssets::force_transfer_all_assets(
1078				&bounty_account,
1079				&treasury_account,
1080			)?;
1081
1082			// Free only if something moved, otherwise paid to prevent griefing.
1083			if !transferred {
1084				return Ok(Pays::Yes.into());
1085			}
1086
1087			Self::deposit_event(Event::<T, I>::BountyFundsReclaimed { bounty_id });
1088
1089			Ok(Pays::No.into())
1090		}
1091	}
1092
1093	#[pallet::hooks]
1094	impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
1095		#[cfg(feature = "try-runtime")]
1096		fn try_state(_n: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1097			Self::do_try_state()
1098		}
1099	}
1100}
1101
1102#[cfg(any(feature = "try-runtime", test))]
1103impl<T: Config<I>, I: 'static> Pallet<T, I> {
1104	/// Ensure the correctness of the state of this pallet.
1105	///
1106	/// This should be valid before or after each state transition of this pallet.
1107	pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1108		Self::try_state_bounties_count()?;
1109
1110		Ok(())
1111	}
1112
1113	/// # Invariants
1114	///
1115	/// * `BountyCount` should be greater or equals to the length of the number of items in
1116	///   `Bounties`.
1117	/// * `BountyCount` should be greater or equals to the length of the number of items in
1118	///   `BountyDescriptions`.
1119	/// * Number of items in `Bounties` should be the same as `BountyDescriptions` length.
1120	fn try_state_bounties_count() -> Result<(), sp_runtime::TryRuntimeError> {
1121		let bounties_length = Bounties::<T, I>::iter().count() as u32;
1122
1123		ensure!(
1124			<BountyCount<T, I>>::get() >= bounties_length,
1125			"`BountyCount` must be grater or equals the number of `Bounties` in storage"
1126		);
1127
1128		let bounties_description_length = BountyDescriptions::<T, I>::iter().count() as u32;
1129		ensure!(
1130			<BountyCount<T, I>>::get() >= bounties_description_length,
1131			"`BountyCount` must be grater or equals the number of `BountiesDescriptions` in storage."
1132		);
1133
1134		ensure!(
1135				bounties_length == bounties_description_length,
1136				"Number of `Bounties` in storage must be the same as the Number of `BountiesDescription` in storage."
1137		);
1138		Ok(())
1139	}
1140}
1141
1142impl<T: Config<I>, I: 'static> Pallet<T, I> {
1143	/// Get the block number used in the treasury pallet.
1144	///
1145	/// It may be configured to use the relay chain block number on a parachain.
1146	pub fn treasury_block_number() -> BlockNumberFor<T, I> {
1147		<T as pallet_treasury::Config<I>>::BlockNumberProvider::current_block_number()
1148	}
1149
1150	/// Calculate the deposit required for a curator.
1151	pub fn calculate_curator_deposit(fee: &BalanceOf<T, I>) -> BalanceOf<T, I> {
1152		let mut deposit = T::CuratorDepositMultiplier::get() * *fee;
1153
1154		if let Some(max_deposit) = T::CuratorDepositMax::get() {
1155			deposit = deposit.min(max_deposit)
1156		}
1157
1158		if let Some(min_deposit) = T::CuratorDepositMin::get() {
1159			deposit = deposit.max(min_deposit)
1160		}
1161
1162		deposit
1163	}
1164
1165	/// The account ID of the treasury pot.
1166	///
1167	/// This actually does computation. If you need to keep using it, then make sure you cache the
1168	/// value and only call this once.
1169	pub fn account_id() -> T::AccountId {
1170		T::PalletId::get().into_account_truncating()
1171	}
1172
1173	/// The account ID of a bounty account
1174	pub fn bounty_account_id(id: BountyIndex) -> T::AccountId {
1175		// only use two byte prefix to support 16 byte account id (used by test)
1176		// "modl" ++ "py/trsry" ++ "bt" is 14 bytes, and two bytes remaining for bounty index
1177		T::PalletId::get().into_sub_account_truncating(("bt", id))
1178	}
1179
1180	fn create_bounty(
1181		proposer: T::AccountId,
1182		description: Vec<u8>,
1183		value: BalanceOf<T, I>,
1184	) -> DispatchResult {
1185		let bounded_description: BoundedVec<_, _> =
1186			description.try_into().map_err(|_| Error::<T, I>::ReasonTooBig)?;
1187		ensure!(value >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
1188
1189		let index = BountyCount::<T, I>::get();
1190
1191		// reserve deposit for new bounty
1192		let bond = Self::calculate_bounty_deposit(&bounded_description);
1193		T::Currency::reserve(&proposer, bond)
1194			.map_err(|_| Error::<T, I>::InsufficientProposersBalance)?;
1195
1196		BountyCount::<T, I>::put(index + 1);
1197
1198		let bounty = Bounty {
1199			proposer,
1200			value,
1201			fee: 0u32.into(),
1202			curator_deposit: 0u32.into(),
1203			bond,
1204			status: BountyStatus::Proposed,
1205		};
1206
1207		Bounties::<T, I>::insert(index, &bounty);
1208		BountyDescriptions::<T, I>::insert(index, bounded_description);
1209
1210		Self::deposit_event(Event::<T, I>::BountyProposed { index });
1211
1212		Ok(())
1213	}
1214
1215	/// Helper function to calculate the bounty storage deposit.
1216	fn calculate_bounty_deposit(
1217		description: &BoundedVec<u8, T::MaximumReasonLength>,
1218	) -> BalanceOf<T, I> {
1219		T::BountyDepositBase::get().saturating_add(
1220			T::DataDepositPerByte::get().saturating_mul((description.len() as u32).into()),
1221		)
1222	}
1223
1224	/// Helper function to poke the deposit reserved for proposing a bounty.
1225	///
1226	/// Returns true if the deposit was updated and false otherwise.
1227	fn poke_bounty_deposit(bounty_id: BountyIndex) -> Result<bool, DispatchError> {
1228		let mut bounty = Bounties::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1229		let bounty_description =
1230			BountyDescriptions::<T, I>::get(bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1231		// ensure that the bounty status is proposed.
1232		ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
1233
1234		let new_bond = Self::calculate_bounty_deposit(&bounty_description);
1235		let old_bond = bounty.bond;
1236		if new_bond == old_bond {
1237			return Ok(false);
1238		}
1239		if new_bond > old_bond {
1240			let extra = new_bond.saturating_sub(old_bond);
1241			T::Currency::reserve(&bounty.proposer, extra)?;
1242		} else {
1243			let excess = old_bond.saturating_sub(new_bond);
1244			let remaining_unreserved = T::Currency::unreserve(&bounty.proposer, excess);
1245			if !remaining_unreserved.is_zero() {
1246				defensive!(
1247					"Failed to unreserve full amount. (Requested, Actual)",
1248					(excess, excess.saturating_sub(remaining_unreserved))
1249				);
1250			}
1251		}
1252		bounty.bond = new_bond;
1253		Bounties::<T, I>::insert(bounty_id, &bounty);
1254
1255		Self::deposit_event(Event::<T, I>::DepositPoked {
1256			bounty_id,
1257			proposer: bounty.proposer,
1258			old_deposit: old_bond,
1259			new_deposit: new_bond,
1260		});
1261
1262		Ok(true)
1263	}
1264}
1265
1266impl<T: Config<I>, I: 'static> pallet_treasury::SpendFunds<T, I> for Pallet<T, I> {
1267	fn spend_funds(
1268		budget_remaining: &mut BalanceOf<T, I>,
1269		imbalance: &mut PositiveImbalanceOf<T, I>,
1270		total_weight: &mut Weight,
1271		missed_any: &mut bool,
1272	) {
1273		let bounties_len = BountyApprovals::<T, I>::mutate(|v| {
1274			let bounties_approval_len = v.len() as u32;
1275			v.retain(|&index| {
1276				Bounties::<T, I>::mutate(index, |bounty| {
1277					// Should always be true, but shouldn't panic if false or we're screwed.
1278					if let Some(bounty) = bounty {
1279						if bounty.value <= *budget_remaining {
1280							*budget_remaining -= bounty.value;
1281
1282							// jump through the funded phase if we're already approved with curator
1283							if let BountyStatus::ApprovedWithCurator { curator } = &bounty.status {
1284								bounty.status =
1285									BountyStatus::CuratorProposed { curator: curator.clone() };
1286							} else {
1287								bounty.status = BountyStatus::Funded;
1288							}
1289
1290							// return their deposit.
1291							let err_amount = T::Currency::unreserve(&bounty.proposer, bounty.bond);
1292							debug_assert!(err_amount.is_zero());
1293
1294							// fund the bounty account
1295							imbalance.subsume(T::Currency::deposit_creating(
1296								&Self::bounty_account_id(index),
1297								bounty.value,
1298							));
1299
1300							Self::deposit_event(Event::<T, I>::BountyBecameActive { index });
1301							false
1302						} else {
1303							*missed_any = true;
1304							true
1305						}
1306					} else {
1307						false
1308					}
1309				})
1310			});
1311			bounties_approval_len
1312		});
1313
1314		*total_weight += <T as pallet::Config<I>>::WeightInfo::spend_funds(bounties_len);
1315	}
1316}
1317
1318// Default impl for when ChildBounties is not being used in the runtime.
1319impl<Balance: Zero> ChildBountyManager<Balance> for () {
1320	fn child_bounties_count(_bounty_id: BountyIndex) -> BountyIndex {
1321		Default::default()
1322	}
1323
1324	fn children_curator_fees(_bounty_id: BountyIndex) -> Balance {
1325		Zero::zero()
1326	}
1327
1328	fn bounty_removed(_bounty_id: BountyIndex) {}
1329}