Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Commit f3e7230

Browse files
committed
implement bounties for treasury
1 parent 1f31ad9 commit f3e7230

File tree

1 file changed

+236
-3
lines changed

1 file changed

+236
-3
lines changed

frame/treasury/src/lib.rs

Lines changed: 236 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
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.
@@ -63,6 +67,9 @@
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
@@ -81,6 +88,9 @@
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};
92102
use sp_std::prelude::*;
93103
use frame_support::{decl_module, decl_storage, decl_event, ensure, print, decl_error, Parameter};
94104
use frame_support::traits::{
95-
Currency, Get, Imbalance, OnUnbalanced, ExistenceRequirement::KeepAlive,
105+
Currency, Get, Imbalance, OnUnbalanced, ExistenceRequirement::{KeepAlive, AllowDeath},
96106
ReservableCurrency, WithdrawReason
97107
};
98108
use 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.
114124
const MODULE_ID: ModuleId = ModuleId(*b"py/trsry");
115125

126+
/// Maximum acceptable reason length.
127+
const MAX_SENSIBLE_REASON_LENGTH: usize = 16384;
128+
116129
pub 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+
201249
decl_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

Comments
 (0)