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