4343//! countdown period, the median of all declared tips is paid to the reported beneficiary, along 
4444//! with any finders fee, in case of a public (and bonded) original report. 
4545//! 
46+ //! ### Bounty 
47+ //! 
48+ //! TODO 
49+ //! 
4650//! ### Terminology 
4751//! 
4852//! - **Proposal:** A suggestion to allocate funds from the pot to a beneficiary. 
6367//! - **Finders Fee:** Some proportion of the tip amount that is paid to the reporter of the tip, 
6468//!   rather than the main beneficiary. 
6569//! 
70+ //! Bounty: 
71+ //! - TODO 
72+ //! 
6673//! ## Interface 
6774//! 
6875//! ### Dispatchable Functions 
8188//! - `tip` - Declare or redeclare an amount to tip for a particular reason. 
8289//! - `close_tip` - Close and pay out a tip. 
8390//! 
91+ //! Bounty protocol: 
92+ //! - TODO 
93+ //! 
8494//! ## GenesisConfig 
8595//! 
8696//! The Treasury module depends on the [`GenesisConfig`](./struct.GenesisConfig.html). 
@@ -92,7 +102,7 @@ use serde::{Serialize, Deserialize};
92102use  sp_std:: prelude:: * ; 
93103use  frame_support:: { decl_module,  decl_storage,  decl_event,  ensure,  print,  decl_error,  Parameter } ; 
94104use  frame_support:: traits:: { 
95- 	Currency ,  Get ,  Imbalance ,  OnUnbalanced ,  ExistenceRequirement :: KeepAlive , 
105+ 	Currency ,  Get ,  Imbalance ,  OnUnbalanced ,  ExistenceRequirement :: { KeepAlive ,   AllowDeath } , 
96106	ReservableCurrency ,  WithdrawReason 
97107} ; 
98108use  sp_runtime:: { Permill ,  ModuleId ,  Percent ,  RuntimeDebug ,  traits:: { 
@@ -113,6 +123,9 @@ type NegativeImbalanceOf<T> = <<T as Trait>::Currency as Currency<<T as frame_sy
113123/// The treasury's module id, used for deriving its sovereign account ID. 
114124const  MODULE_ID :  ModuleId  = ModuleId ( * b"py/trsry" ) ; 
115125
126+ /// Maximum acceptable reason length. 
127+ const  MAX_SENSIBLE_REASON_LENGTH :  usize  = 16384 ; 
128+ 
116129pub  trait  Trait :  frame_system:: Trait  { 
117130	/// The staking balance. 
118131 	type  Currency :  Currency < Self :: AccountId >  + ReservableCurrency < Self :: AccountId > ; 
@@ -156,6 +169,15 @@ pub trait Trait: frame_system::Trait {
156169
157170	/// Percentage of spare funds (if any) that are burnt per spend period. 
158171 	type  Burn :  Get < Permill > ; 
172+ 
173+ 	/// The amount held on deposit for placing a bounty proposal. 
174+  	type  BountyDepositBase :  Get < BalanceOf < Self > > ; 
175+ 
176+ 	/// The amount held on deposit per byte within bounty description. 
177+  	type  BountyDepositPerByte :  Get < BalanceOf < Self > > ; 
178+ 
179+ 	/// The delay period for which a bounty beneficiary need to wait before claim the payout. 
180+  	type  BountyDepositPayoutDelay :  Get < Self :: BlockNumber > ; 
159181} 
160182
161183/// An index of a proposal. Just a `u32`. 
@@ -198,6 +220,32 @@ pub struct OpenTip<
198220 	tips :  Vec < ( AccountId ,  Balance ) > , 
199221} 
200222
223+ /// An index of a bounty. Just a `u32`. 
224+ pub  type  BountyIndex  = u32 ; 
225+ 
226+ /// A bounty proposal. 
227+ #[ derive( Encode ,  Decode ,  Clone ,  PartialEq ,  Eq ,  RuntimeDebug ) ]  
228+ pub  struct  Bounty < AccountId ,  Balance >  { 
229+ 	/// The account proposing it. 
230+  	proposer :  AccountId , 
231+ 	/// The account manages this bounty. 
232+  	curator :  AccountId , 
233+ 	/// The (total) amount that should be paid if the bounty is rewarded. 
234+  	value :  Balance , 
235+ 	/// The amount held on deposit (reserved) for making this proposal. 
236+  	bond :  Balance , 
237+ 	/// The description of this bounty. 
238+  	description :  Vec < u8 > , 
239+ } 
240+ 
241+ #[ derive( Encode ,  Decode ,  Clone ,  PartialEq ,  Eq ,  RuntimeDebug ) ]  
242+ pub  enum  BountyStatus  { 
243+ 	Proposed , 
244+ 	Approved , 
245+ 	Active , 
246+ 	PendingPayout , 
247+ } 
248+ 
201249decl_storage !  { 
202250	trait  Store  for  Module <T :  Trait > as  Treasury  { 
203251		/// Number of proposals that have been made. 
@@ -221,6 +269,25 @@ decl_storage! {
221269		/// Simple preimage lookup from the reason's hash to the original data. Again, has an 
222270 		/// insecure enumerable hash since the key is guaranteed to be the result of a secure hash. 
223271 		pub  Reasons  get( fn  reasons) :  map hasher( identity)  T :: Hash  => Option <Vec <u8 >>; 
272+ 
273+ 		/// Number of bounty proposals that have been made. 
274+  		pub  BountyCount  get( fn  bounty_count) :  BountyIndex ; 
275+ 
276+ 		/// Bounties that have been made. 
277+  		pub  Bounties  get( fn  bounties) : 
278+ 			map hasher( twox_64_concat)  BountyIndex 
279+ 			=> Option <Bounty <T :: AccountId ,  BalanceOf <T >>>; 
280+ 
281+ 		/// The status of each bounty. 
282+  		pub  BountyStatuses  get( fn  bounty_statuses) : 
283+ 			map hasher( twox_64_concat)  BountyIndex  => Option <BountyStatus >; 
284+ 
285+ 		/// The bounty beneficiary and the block the fund can be claimed 
286+  		pub  BountyBeneficiary  get( fn  bounty_beneficiary) : 
287+ 			map hasher( twox_64_concat)  BountyIndex  => Option <( T :: AccountId ,  T :: BlockNumber ) >; 
288+ 
289+ 		/// Bounty indices that have been approved but not yet funded. 
290+  		pub  BountyApprovals  get( fn  bounty_approvals) :  Vec <BountyIndex >; 
224291	} 
225292	add_extra_genesis { 
226293		build( |_config| { 
@@ -262,6 +329,16 @@ decl_event!(
262329 		TipClosed ( Hash ,  AccountId ,  Balance ) , 
263330		/// A tip suggestion has been retracted. 
264331 		TipRetracted ( Hash ) , 
332+ 		/// New bounty proposal. 
333+  		BountyProposed ( BountyIndex ) , 
334+ 		/// A bounty proposal was rejected; funds were slashed. 
335+  		BountyRejected ( BountyIndex ,  Balance ) , 
336+ 		/// A bounty proposal is funded and become active. 
337+  		BountyBecomeActive ( BountyIndex ) , 
338+ 		/// A bounty is awarded to a beneficiary. 
339+  		BountyAwarded ( BountyIndex ,  AccountId ) , 
340+ 		/// A bounty is claimed by beneficiary. 
341+  		BountyClaimed ( BountyIndex ,  Balance ,  AccountId ) , 
265342	} 
266343) ; 
267344
@@ -284,6 +361,10 @@ decl_error! {
284361 		StillOpen , 
285362		/// The tip cannot be claimed/closed because it's still in the countdown period. 
286363 		Premature , 
364+ 		/// The bounty status is unexpected. 
365+  		UnexpectedStatus , 
366+ 		/// Require bounty curator. 
367+  		RequireCurator , 
287368	} 
288369} 
289370
@@ -314,6 +395,15 @@ decl_module! {
314395		/// The amount held on deposit per byte within the tip report reason. 
315396 		const  TipReportDepositPerByte :  BalanceOf <T > = T :: TipReportDepositPerByte :: get( ) ; 
316397
398+ 		/// The amount held on deposit for placing a bounty proposal. 
399+  		const  BountyDepositBase :  BalanceOf <T > = T :: BountyDepositBase :: get( ) ; 
400+ 
401+ 		/// The amount held on deposit per byte within bounty description. 
402+  		const  BountyDepositPerByte :  BalanceOf <T > = T :: BountyDepositPerByte :: get( ) ; 
403+ 
404+ 		/// The delay period for which a bounty beneficiary need to wait before claim the payout. 
405+  		const  BountyDepositPayoutDelay :  T :: BlockNumber  = T :: BountyDepositPayoutDelay :: get( ) ; 
406+ 
317407		type  Error  = Error <T >; 
318408
319409		fn  deposit_event( )  = default ; 
@@ -409,7 +499,6 @@ decl_module! {
409499		fn  report_awesome( origin,  reason:  Vec <u8 >,  who:  T :: AccountId )  { 
410500			let  finder = ensure_signed( origin) ?; 
411501
412- 			const  MAX_SENSIBLE_REASON_LENGTH :  usize  = 16384 ; 
413502			ensure!( reason. len( )  <= MAX_SENSIBLE_REASON_LENGTH ,  Error :: <T >:: ReasonTooBig ) ; 
414503
415504			let  reason_hash = T :: Hashing :: hash( & reason[ ..] ) ; 
@@ -552,6 +641,118 @@ decl_module! {
552641			Self :: payout_tip( hash,  tip) ; 
553642		} 
554643
644+ 		#[ weight = SimpleDispatchInfo :: FixedNormal ( 150_000_000 ) ] 
645+ 		fn  propose_bounty( 
646+ 			origin, 
647+ 			curator:  <T :: Lookup  as  StaticLookup >:: Source , 
648+ 			#[ compact]  value:  BalanceOf <T >, 
649+ 			description:  Vec <u8 >, 
650+ 		)  { 
651+ 			let  proposer = ensure_signed( origin) ?; 
652+ 			let  curator = T :: Lookup :: lookup( curator) ?; 
653+ 
654+ 			ensure!( description. len( )  <= MAX_SENSIBLE_REASON_LENGTH ,  Error :: <T >:: ReasonTooBig ) ; 
655+ 
656+ 			let  bond = T :: BountyDepositBase :: get( ) 
657+ 				+ T :: BountyDepositPerByte :: get( )  *  ( description. len( )  as  u32 ) . into( ) ; 
658+ 			T :: Currency :: reserve( & proposer,  bond) 
659+ 				. map_err( |_| Error :: <T >:: InsufficientProposersBalance ) ?; 
660+ 
661+ 			let  index = Self :: bounty_count( ) ; 
662+ 			BountyCount :: put( index + 1 ) ; 
663+ 
664+ 			let  bounty = Bounty  { 
665+ 				proposer,  curator,  value,  bond,  description
666+ 			} ; 
667+ 
668+ 			Bounties :: <T >:: insert( index,  & bounty) ; 
669+ 			BountyStatuses :: insert( index,  BountyStatus :: Proposed ) ; 
670+ 
671+ 			Self :: deposit_event( RawEvent :: BountyProposed ( index) ) ; 
672+ 		} 
673+ 
674+ 		/// Reject a bounty proposal. The original deposit will be slashed. 
675+  		/// 
676+  		/// # <weight> 
677+  		/// - O(1). 
678+  		/// - Limited storage reads. 
679+  		/// - Two DB clear. 
680+  		/// # </weight> 
681+  		#[ weight = SimpleDispatchInfo :: FixedOperational ( 100_000_000 ) ] 
682+ 		fn  reject_bounty( origin,  #[ compact]  bounty_id:  BountyIndex )  { 
683+ 			T :: RejectOrigin :: try_origin( origin) 
684+ 				. map( |_| ( ) ) 
685+ 				. or_else( ensure_root) ?; 
686+ 
687+ 			ensure!( Self :: bounty_statuses( bounty_id)  == Some ( BountyStatus :: Proposed ) ,  Error :: <T >:: UnexpectedStatus ) ; 
688+ 			let  bounty = <Bounties <T >>:: take( & bounty_id) . ok_or( Error :: <T >:: InvalidProposalIndex ) ?; 
689+ 
690+ 			BountyStatuses :: remove( bounty_id) ; 
691+ 
692+ 			let  value = bounty. bond; 
693+ 			let  imbalance = T :: Currency :: slash_reserved( & bounty. proposer,  value) . 0 ; 
694+ 			T :: ProposalRejection :: on_unbalanced( imbalance) ; 
695+ 
696+ 			Self :: deposit_event( Event :: <T >:: BountyRejected ( bounty_id,  value) ) ; 
697+ 		} 
698+ 
699+ 		/// Approve a bounty proposal. At a later time, the bounty will be funded and become active 
700+  		/// and the original deposit will be returned. 
701+  		/// 
702+  		/// # <weight> 
703+  		/// - O(1). 
704+  		/// - Limited storage reads. 
705+  		/// - One DB change. 
706+  		/// # </weight> 
707+  		#[ weight = SimpleDispatchInfo :: FixedOperational ( 100_000_000 ) ] 
708+ 		fn  approve_bounty( origin,  #[ compact]  bounty_id:  ProposalIndex )  { 
709+ 			T :: ApproveOrigin :: try_origin( origin) 
710+ 				. map( |_| ( ) ) 
711+ 				. or_else( ensure_root) ?; 
712+ 
713+ 			ensure!( <Bounties <T >>:: contains_key( bounty_id) ,  Error :: <T >:: InvalidProposalIndex ) ; 
714+ 			ensure!( Self :: bounty_statuses( bounty_id)  == Some ( BountyStatus :: Proposed ) ,  Error :: <T >:: UnexpectedStatus ) ; 
715+ 			BountyStatuses :: insert( bounty_id,  BountyStatus :: Approved ) ; 
716+ 		} 
717+ 
718+ 		#[ weight = SimpleDispatchInfo :: FixedOperational ( 100_000_000 ) ] 
719+ 		fn  award_bounty( origin,  #[ compact]  bounty_id:  ProposalIndex ,  beneficiary:  <T :: Lookup  as  StaticLookup >:: Source )  { 
720+ 			let  curator = ensure_signed( origin) ?; 
721+ 			let  beneficiary = T :: Lookup :: lookup( beneficiary) ?; 
722+ 
723+ 			ensure!( Self :: bounty_statuses( bounty_id)  == Some ( BountyStatus :: Active ) ,  Error :: <T >:: UnexpectedStatus ) ; 
724+ 
725+ 			let  bounty = Self :: bounties( bounty_id) . ok_or( Error :: <T >:: InvalidProposalIndex ) ?; 
726+ 			ensure!( bounty. curator == curator,  Error :: <T >:: RequireCurator ) ; 
727+ 
728+ 			BountyStatuses :: insert( bounty_id,  BountyStatus :: PendingPayout ) ; 
729+ 			BountyBeneficiary :: <T >:: insert( bounty_id,  ( & beneficiary,  system:: Module :: <T >:: block_number( )  + T :: BountyDepositPayoutDelay :: get( ) ) ) ; 
730+ 
731+ 			Bounties :: <T >:: remove( bounty_id) ;  // no longer needed 
732+ 
733+ 			Self :: deposit_event( Event :: <T >:: BountyAwarded ( bounty_id,  beneficiary) ) ; 
734+ 		} 
735+ 
736+ 		#[ weight = SimpleDispatchInfo :: FixedOperational ( 100_000_000 ) ] 
737+ 		fn  claim_bounty( origin,  #[ compact]  bounty_id:  ProposalIndex )  { 
738+ 			let  _ = ensure_signed( origin) ?; 
739+ 
740+ 			ensure!( Self :: bounty_statuses( bounty_id)  == Some ( BountyStatus :: PendingPayout ) ,  Error :: <T >:: UnexpectedStatus ) ; 
741+ 			let  ( beneficiary,  released)  = Self :: bounty_beneficiary( bounty_id) 
742+ 				. ok_or( Error :: <T >:: InvalidProposalIndex ) ?;  // this should not fail 
743+ 
744+ 			ensure!( system:: Module :: <T >:: block_number( )  >= released,  Error :: <T >:: Premature ) ; 
745+ 
746+ 			let  bounty_account = Self :: bounty_account_id( bounty_id) ; 
747+ 			let  balance = T :: Currency :: free_balance( & bounty_account) ; 
748+ 			let  _ = T :: Currency :: transfer( & bounty_account,  & beneficiary,  balance,  AllowDeath ) ;  // should not fail 
749+ 
750+ 			BountyStatuses :: remove( bounty_id) ; 
751+ 			BountyBeneficiary :: <T >:: remove( bounty_id) ; 
752+ 
753+ 			Self :: deposit_event( Event :: <T >:: BountyClaimed ( bounty_id,  balance,  beneficiary) ) ; 
754+ 		} 
755+ 
555756		fn  on_initialize( n:  T :: BlockNumber )  -> Weight  { 
556757			// Check to see if we should spend some funds! 
557758			if  ( n % T :: SpendPeriod :: get( ) ) . is_zero( )  { 
@@ -574,6 +775,11 @@ impl<T: Trait> Module<T> {
574775		MODULE_ID . into_account ( ) 
575776	} 
576777
778+ 	/// The account ID of a bounty account 
779+  	pub  fn  bounty_account_id ( id :  BountyIndex )  -> T :: AccountId  { 
780+ 		MODULE_ID . into_sub_account ( ( "bounty" ,  id) ) 
781+ 	} 
782+ 
577783	/// The needed bond for a proposal whose spend is `value`. 
578784 	fn  calculate_bond ( value :  BalanceOf < T > )  -> BalanceOf < T >  { 
579785		T :: ProposalBondMinimum :: get ( ) . max ( T :: ProposalBond :: get ( )  *  value) 
@@ -654,6 +860,7 @@ impl<T: Trait> Module<T> {
654860	fn  spend_funds ( )  { 
655861		let  mut  budget_remaining = Self :: pot ( ) ; 
656862		Self :: deposit_event ( RawEvent :: Spending ( budget_remaining) ) ; 
863+ 		let  account_id = Self :: account_id ( ) ; 
657864
658865		let  mut  missed_any = false ; 
659866		let  mut  imbalance = <PositiveImbalanceOf < T > >:: zero ( ) ; 
@@ -683,6 +890,32 @@ impl<T: Trait> Module<T> {
683890			} ) ; 
684891		} ) ; 
685892
893+ 		BountyApprovals :: mutate ( |v| { 
894+ 			v. retain ( |& index| { 
895+ 				// Should always be true, but shouldn't panic if false or we're screwed. 
896+ 				if  let  Some ( bounty)  = Self :: bounties ( index)  { 
897+ 					if  bounty. value  <= budget_remaining { 
898+ 						budget_remaining -= bounty. value ; 
899+ 						BountyStatuses :: insert ( index,  BountyStatus :: Active ) ; 
900+ 
901+ 						// return their deposit. 
902+ 						let  _ = T :: Currency :: unreserve ( & bounty. proposer ,  bounty. bond ) ; 
903+ 
904+ 						// fund the bounty account 
905+ 						imbalance. subsume ( T :: Currency :: deposit_creating ( & Self :: bounty_account_id ( index) ,  bounty. value ) ) ; 
906+ 
907+ 						Self :: deposit_event ( RawEvent :: BountyBecomeActive ( index) ) ; 
908+ 						false 
909+ 					}  else  { 
910+ 						missed_any = true ; 
911+ 						true 
912+ 					} 
913+ 				}  else  { 
914+ 					false 
915+ 				} 
916+ 			} ) ; 
917+ 		} ) ; 
918+ 
686919		if  !missed_any { 
687920			// burn some proportion of the remaining budget if we run a surplus. 
688921			let  burn = ( T :: Burn :: get ( )  *  budget_remaining) . min ( budget_remaining) ; 
@@ -696,7 +929,7 @@ impl<T: Trait> Module<T> {
696929		// Thus we can't spend more than account free balance minus ED; 
697930		// Thus account is kept alive; qed; 
698931		if  let  Err ( problem)  = T :: Currency :: settle ( 
699- 			& Self :: account_id ( ) , 
932+ 			& account_id, 
700933			imbalance, 
701934			WithdrawReason :: Transfer . into ( ) , 
702935			KeepAlive 
0 commit comments