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