Issue 7143 | Refactor Treasury Pallet into Bounties, Tips, and Proposals (#7536)

* wk2046 | D5 | treasury refactor to bounties and tips

* wk2046 | D5 | treasury refactor to bounties and tips | p2

* fix test compilation, ignoring events

* initialize treasury in genesis

* wk2046 | D7 | treasury refactor | fix bounties test build issues

* wk2047 | D1 | treasury refactor | tips pallet bringup

* wk2047 | D2 | treasury refactor | bounties  pallet | unit test bringup

* wk2047 | D2 | treasury refactor | bounties  pallet | unit test bringup | p2

* wk2047 | D2 | treasury refactor | pallet-tips| test_last_reward_migration | test failure - analysis

* wk2047 | D3 | treasury refactor | pallet-tips| test_last_reward_migration | test failure - fix

* wk2047 | D3 | treasury refactor | pallet-bounties | on_initialize() fix

* wk2047 | D3 | treasury refactor | pallet-bounties | on_initialize() fix | p2

* wk2047 | D4 | treasury refactor | pallet-bounties + pallet-treasury | spend_fund runtime hooks

* wk2047 | D4 | treasury refactor | pallet-bounties + pallet-treasury | spend_fund runtime hooks | p2

* wk2047 | D4 | treasury refactor | pallet-bounties + pallet-treasury | spend_fund runtime hooks | p3

* wk2047 | D5 | treasury refactor | pallet-bounties + pallet-treasury | spend_fund runtime hooks | p4

* wk2047 | D6 | treasury refactor | review comments fix

* some fixes

* fix bounties instantiable

* remove instantiable from tips and bounties

* fix compile for benchmarks

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_treasury --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/treasury/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Update lib.rs

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_tips --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/tips/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_bounties --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/bounties/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* add back `on_initialize_bounties`

* patch up bounties benchmarks

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_bounties --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/bounties/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Remove development TODO from public doc comment (#7500)

* refactor subtrait/elevated trait as not needed (#7497)

* Fix comments of indices pallet events (#7511)

Arguments for IndexAssigned and IndexFrozen were inverted in comments.

* Skip slot lenience on first block in BABE (#7515)

The genesis header doesn't have the BABE pre-digest and we insert `0` as
slot number. The slot lenience calculation will return the maximum in
this situation. Besides returning the maximum which is not bad at all,
it also prints some a debug message that can be confusing in the first
moment. To prevent printing this debug message, we now just return early
when we see that the parent block is the genesis block.

* slots: incrementally backoff claiming slots if finality lags behind (#7186)

* babe: backoff authoring blocks when finality lags

* babe: move backoff authoring params to default constructor

* babe: deduplicate the test a bit

* babe: set backoff constants in service

* babe: use better names for backoff authoring block parameters

* babe: remove last unwrap

* babe: slight style tweak

* babe: fix comment

* slots: move backoff block authorship logic to SimpleSlotWorker

* aura: append SlotInfo in on_slot

* slots: use the correct types for parameters

* slots: fix review comments

* aura: add missing backoff authoring blocks parameters

* slots: add comments for default values

* slots: add additional checks in test

* slots: update implementation for new master

* slots: revert the change to SlotInfo

* Fix review comments

* slots: rework unit tests for backing off claiming slots

* slots: add test for asymptotic behaviour for slot claims

* slots: address review comments

* slots: add test for max_interval

* slots: add assertion for intervals between between claimed slots

* slots: remove rustfmt directive

* slots: another attempt at explaining authoring_rate

* slots: up unfinalized_slack to 50 by default

* slots: add tests for time to reach max_interval

* slots: fix typo in comments

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* slots: additional tweaks to comments and info calls

* slots: rename to BackoffAuthoringOnFinalizedHeadLagging

* slots: make the backing off strategy generic

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* slots: implement backoff trait for () for simplicity

* slots: move logging inside backing off function to make it more specific

* aura: add missing function parameter

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Export app-crypto specific keystore functions (#7489)

* Export app-crypto specific keystore functions

* Also add back the insert function

* Switch KeystoreContainer to an enum

* Only export the bare minimal for LocalKeystore and fix service compile

* fix: should return Arc

* Add docs stating that functions only available in local keystore

* Remove insert and generate functions

* fix: generate function should be available in test

* Add keypair function to trait

* Revert "Add keypair function to trait"

This reverts commit ad921b09ca73d3c09298e3a51b562ef8e0067781.

* Add note for local_keystore function in service

* Update doc for the --chain flag (#7520)

* contracts: Add missing instruction to the `Schedule` (#7527)

* Don't log with colors when we are writing to a tty (#7525)

* Don't log with colors when we are writing to a tty

This fixes a regression that was introduced by the switch to tracing.
Before we killed all colors before writing to a tty, this pr brings the
behaviour back.

* Remove accidentally added crate

* Review feedback

* More feedback

* Update client/cli/src/logging.rs

Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>

* Update client/cli/src/logging.rs

Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>

Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>

* MemoryId -> u32 (#7534)

* Enable local addresses in DHT when chain type == `Local` | `Development` (#7538)

* Enable local addresses in DHT when chain type == `Local` | `Development`

This pr changes when to add local addresses to DHT. Instead of only
checking if `--discover-local` and `--dev` are present, we now also
check if the chain type is `Local` or `Development`.

* Update the docs!

* Update tiny-bip39 to v0.8 (#7539)

It would improve secret zeroization due to
https://github.com/maciejhirsz/tiny-bip39/pull/22, and would also remove
one of the points where we depend on `failure` crate, which is
deprecated (see https://github.com/rust-lang-nursery/failure/pull/347)

* make LocalCallExecutor public (#7528)

* Fix some weirdness in `offchain_worker` (#7541)

We call `offchain_worker` with the state of the imported block and pass
the header of this block. However in the runtime we call all
`offchain_worker` functions with the number of the parent block. Besides
that we also pass all digests and not only the pre runtime digests. In
the context where the offchain worker is executed we have all digests, so
there is no real reason to only pass pre runtime digests. Another fix is
that we also insert the hash of the current header into the block hash map.

* Use inbound peerslot slots when a substream is received, rather than a connection (#7464)

* Use inbound peerslot slots when a substream is received, rather than a connection

* Refactor PeerState

* Some bugfixes

* Fix warnings so that CI runs, gmlrlblbl

* Bugfixes

* Update docs

* Apply suggestions from code review

Co-authored-by: Roman Borschel <romanb@users.noreply.github.com>

* Clean up Banned state

* Refactor connections state

* Fix possibility of Enabled with no Opening or Open connection

* Line width

* Add some debug_asserts! and fix TODO

* Refactor legacy handler

* Rewrite group.rs entirely [part 1]

* Rewrite group.rs entirely [part 2]

* Remove faulty assertion

Because of the asynchronous nature of the behaviour <-> handler communications, it is possible to receive notifications while in the Closing state

* Don't poll the legacy substream is not Open

* Tolerate when not all substreams are accepted

* Remove TODOs

* Dummy commit to make CI log interesting things

* Try race condition fix

* Revert "Try race condition fix"

This reverts commit 0675c659d06195c30f8c5bc13e2d88141d57a3ba.

* Correctly rebuild pending_opening

* Minor tweaks

* Printlns for CI debugging

* Revert "Printlns for CI debugging"

This reverts commit e7852a231f4fc418898767aaa27c9a4358e12e8b.

* Revert "Dummy commit to make CI log interesting things"

This reverts commit 259ddd74088e53e7c6a9b0a62a8d1573a0063ce3.

* mv group.rs ../handler.rs

* Apply suggestions from code review

Co-authored-by: Max Inden <mail@max-inden.de>

* Banned => Backoff

* Mention the actual PeerStates

* OpenDesired -> OpenDesiredByRemote

* OpeningThenClosing

* Add doc links to PeerState

* Simplify increment logic

* One more debug_assert

* debug_assert!

* OpenDesiredByRemote

* Update client/network/src/protocol/generic_proto/behaviour.rs

Co-authored-by: Max Inden <mail@max-inden.de>

Co-authored-by: Roman Borschel <romanb@users.noreply.github.com>
Co-authored-by: Max Inden <mail@max-inden.de>

* *: Update to libp2p v0.30.0 (#7508)

* *: Update to libp2p v0.30.0

* Cargo.lock: Update

* *: Update to libp2p v0.30.1

* make ClientConfig public (#7544)

* sc-basic-authorship: remove useless dependencies (#7550)

Signed-off-by: koushiro <koushiro.cqx@gmail.com>

* Add slashing events to elections-phragmen. (#7543)

* Add slashing events to elections-phragmen.

* Fix build

* Apply suggestions from code review

* Update frame/elections-phragmen/src/lib.rs

* Update frame/elections-phragmen/src/lib.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Remove necessity to pass ConsensusEngineId when registering notifications protocol (#7549)

* Remove necessity to pass ConsensusEngineId when registering notifications protocol

* Line width

* Fix tests protocol name

* Other renames

* Doc update

* Change issue in TODO

* sc-cli: replace bip39 with tiny-bip39 (#7551)

Signed-off-by: koushiro <koushiro.cqx@gmail.com>

* Add extra docs to on_initialize (#7552)

* Add some extra on_initialize docs.

* Address review comments.

* More Extensible Multiaddress Format (#7380)

* More extensible multiaddress format

* update name

* Don't depend on indices to define multiaddress type

* Use MultiAddress in Node Template too!

* reduce traits, fix build

* support multiple `StaticLookup`

* bump tx version

* feedback

* Fix weight template to remove ugliness in rust doc (#7565)

fixed weight template

* Cargo.lock: Run cargo update (#7553)

* Cargo.lock: Run cargo update

* Cargo.lock: Downgrade cc to v1.0.62

* Cargo.lock: Revert wasm-* updates

* .github: Add dependabot config and thus enable dependabot (#7509)

* .github: Add dependabot config and thus enable dependabot

* Update .github/dependabot.yml

Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>

Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>

* Thread-local parameter_types for testing. (#7542)

* Thread-local parameter_types for testing.

* Better docs.

* Some minors

* Merge'em

* Update frame/support/src/lib.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Align more to basti's trick

* Update frame/support/src/lib.rs

* Update frame/support/src/lib.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
Co-authored-by: Bastian Köcher <git@kchr.de>

* Bump wasm-bindgen-test from 0.3.12 to 0.3.17 (#7567)

* Bump wasm-bindgen-test from 0.3.12 to 0.3.17

Bumps [wasm-bindgen-test](https://github.com/rustwasm/wasm-bindgen) from 0.3.12 to 0.3.17.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

Signed-off-by: dependabot[bot] <support@github.com>

* Update wasm-bindgen pin to 0.2.68

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>

* wk2047 | D6 | treasury refactor | review comments fix | p2

* wk2048 | D1 | treasury refactor | review comments fix | p3

* Update bin/node/runtime/src/lib.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Update bin/node/runtime/src/lib.rs

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* wk2048 | D3 | treasury refactor | review comments fix | p4

* wk2048 | D3 | treasury refactor | review comments fix | p5

* wk2048 | D4 | treasury refactor | review comments fix |  removal of deadcode | p6

* remove broken link

* wk2048 | D5 | treasury refactor | review comments fix | bountise doc string | p7

* wk2048 | D5 | treasury refactor | review comments fix | p8

* docs and formatting

* Update frame/tips/src/benchmarking.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* formatting nits

* Trait -> Config

* trait -> config in benchmarks

* clean up weight docs

* Trait -> Config in Runtime

* fix test build

* try to fix polkadot build check

* fix traits

* Update lib.rs

* Apply suggestions from code review

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Apply suggestions from code review

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* fix trait location

* nits

* uncomment on_initialize for bounties benchmarks

* update weights

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
Co-authored-by: Parity Benchmarking Bot <admin@parity.io>
Co-authored-by: Caio <c410.f3r@gmail.com>
Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Antoine Le Calvez <alecalve@users.noreply.github.com>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
Co-authored-by: Wei Tang <wei@that.world>
Co-authored-by: Sergei Shulepov <sergei@parity.io>
Co-authored-by: Alexander Theißen <alex.theissen@me.com>
Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>
Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>
Co-authored-by: Kirill Pimenov <kirill@parity.io>
Co-authored-by: Andrew Plaza <aplaza@liquidthink.net>
Co-authored-by: Roman Borschel <romanb@users.noreply.github.com>
Co-authored-by: Max Inden <mail@max-inden.de>
Co-authored-by: Qinxuan Chen <koushiro.cqx@gmail.com>
Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: Tomasz Drwięga <tomusdrw@users.noreply.github.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Addie Wagenknecht <addie@nortd.com>
This commit is contained in:
RK
2020-12-16 16:02:16 +05:30
committed by GitHub
parent 581e723a11
commit 8e64a2def9
22 changed files with 3795 additions and 2509 deletions
+757
View File
@@ -0,0 +1,757 @@
// This file is part of Substrate.
// Copyright (C) 2017-2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Bounties Module ( pallet-bounties )
//!
//! ## Bounty
//!
//! > NOTE: This pallet is tightly coupled with pallet-treasury.
//!
//! A Bounty Spending is a reward for a specified body of work - or specified set of objectives -
//! that needs to be executed for a predefined Treasury amount to be paid out. A curator is assigned
//! after the bounty is approved and funded by Council, to be delegated with the responsibility of
//! assigning a payout address once the specified set of objectives is completed.
//!
//! After the Council has activated a bounty, it delegates the work that requires expertise to a
//! curator in exchange of a deposit. Once the curator accepts the bounty, they get to close the
//! active bounty. Closing the active bounty enacts a delayed payout to the payout address, the
//! curator fee and the return of the curator deposit. The delay allows for intervention through
//! regular democracy. The Council gets to unassign the curator, resulting in a new curator
//! election. The Council also gets to cancel the bounty if deemed necessary before assigning a
//! curator or once the bounty is active or payout is pending, resulting in the slash of the
//! curator's deposit.
//!
//!
//! ### Terminology
//!
//! Bounty:
//! - **Bounty spending proposal:** A proposal to reward a predefined body of work upon completion
//! by the Treasury.
//! - **Proposer:** An account proposing a bounty spending.
//! - **Curator:** An account managing the bounty and assigning a payout address receiving the
//! reward for the completion of work.
//! - **Deposit:** The amount held on deposit for placing a bounty proposal plus the amount held on
//! deposit per byte within the bounty description.
//! - **Curator deposit:** The payment from a candidate willing to curate an approved bounty. The
//! deposit is returned when/if the bounty is completed.
//! - **Bounty value:** The total amount that should be paid to the Payout Address if the bounty is
//! rewarded.
//! - **Payout address:** The account to which the total or part of the bounty is assigned to.
//! - **Payout Delay:** The delay period for which a bounty beneficiary needs to wait before
//! claiming.
//! - **Curator fee:** The reserved upfront payment for a curator for work related to the bounty.
//!
//! ## Interface
//!
//! ### Dispatchable Functions
//!
//! Bounty protocol:
//! - `propose_bounty` - Propose a specific treasury amount to be earmarked for a predefined set of
//! tasks and stake the required deposit.
//! - `approve_bounty` - Accept a specific treasury amount to be earmarked for a predefined body of
//! work.
//! - `propose_curator` - Assign an account to a bounty as candidate curator.
//! - `accept_curator` - Accept a bounty assignment from the Council, setting a curator deposit.
//! - `extend_bounty_expiry` - Extend the expiry block number of the bounty and stay active.
//! - `award_bounty` - Close and pay out the specified amount for the completed work.
//! - `claim_bounty` - Claim a specific bounty amount from the Payout Address.
//! - `unassign_curator` - Unassign an accepted curator from a specific earmark.
//! - `close_bounty` - Cancel the earmark for a specific treasury amount and close the bounty.
#![cfg_attr(not(feature = "std"), no_std)]
mod tests;
mod benchmarking;
pub mod weights;
use sp_std::prelude::*;
use frame_support::{decl_module, decl_storage, decl_event, ensure, decl_error};
use frame_support::traits::{
Currency, Get, Imbalance, OnUnbalanced, ExistenceRequirement::{AllowDeath},
ReservableCurrency};
use sp_runtime::{Permill, RuntimeDebug, DispatchResult, traits::{
Zero, StaticLookup, AccountIdConversion, Saturating, BadOrigin
}};
use frame_support::dispatch::DispatchResultWithPostInfo;
use frame_support::traits::{EnsureOrigin};
use frame_support::weights::{Weight};
use codec::{Encode, Decode};
use frame_system::{self as system, ensure_signed};
pub use weights::WeightInfo;
type BalanceOf<T> = pallet_treasury::BalanceOf<T>;
type PositiveImbalanceOf<T> = pallet_treasury::PositiveImbalanceOf<T>;
pub trait Config: frame_system::Config + pallet_treasury::Config {
/// The amount held on deposit for placing a bounty proposal.
type BountyDepositBase: Get<BalanceOf<Self>>;
/// The delay period for which a bounty beneficiary need to wait before claim the payout.
type BountyDepositPayoutDelay: Get<Self::BlockNumber>;
/// Bounty duration in blocks.
type BountyUpdatePeriod: Get<Self::BlockNumber>;
/// Percentage of the curator fee that will be reserved upfront as deposit for bounty curator.
type BountyCuratorDeposit: Get<Permill>;
/// Minimum value for a bounty.
type BountyValueMinimum: Get<BalanceOf<Self>>;
/// The amount held on deposit per byte within the tip report reason or bounty description.
type DataDepositPerByte: Get<BalanceOf<Self>>;
/// The overarching event type.
type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
/// Maximum acceptable reason length.
type MaximumReasonLength: Get<u32>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
/// An index of a bounty. Just a `u32`.
pub type BountyIndex = u32;
/// A bounty proposal.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct Bounty<AccountId, Balance, BlockNumber> {
/// The account proposing it.
proposer: AccountId,
/// The (total) amount that should be paid if the bounty is rewarded.
value: Balance,
/// The curator fee. Included in value.
fee: Balance,
/// The deposit of curator.
curator_deposit: Balance,
/// The amount held on deposit (reserved) for making this proposal.
bond: Balance,
/// The status of this bounty.
status: BountyStatus<AccountId, BlockNumber>,
}
/// The status of a bounty proposal.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub enum BountyStatus<AccountId, BlockNumber> {
/// The bounty is proposed and waiting for approval.
Proposed,
/// The bounty is approved and waiting to become active at next spend period.
Approved,
/// The bounty is funded and waiting for curator assignment.
Funded,
/// A curator has been proposed by the `ApproveOrigin`. Waiting for acceptance from the curator.
CuratorProposed {
/// The assigned curator of this bounty.
curator: AccountId,
},
/// The bounty is active and waiting to be awarded.
Active {
/// The curator of this bounty.
curator: AccountId,
/// An update from the curator is due by this block, else they are considered inactive.
update_due: BlockNumber,
},
/// The bounty is awarded and waiting to released after a delay.
PendingPayout {
/// The curator of this bounty.
curator: AccountId,
/// The beneficiary of the bounty.
beneficiary: AccountId,
/// When the bounty can be claimed.
unlock_at: BlockNumber,
},
}
// Note :: For backward compatability reasons,
// pallet-bounties uses Treasury for storage.
// This is temporary solution, soon will get replaced with
// Own storage identifier.
decl_storage! {
trait Store for Module<T: Config> as Treasury {
/// Number of bounty proposals that have been made.
pub BountyCount get(fn bounty_count): BountyIndex;
/// Bounties that have been made.
pub Bounties get(fn bounties):
map hasher(twox_64_concat) BountyIndex
=> Option<Bounty<T::AccountId, BalanceOf<T>, T::BlockNumber>>;
/// The description of each bounty.
pub BountyDescriptions get(fn bounty_descriptions): map hasher(twox_64_concat) BountyIndex => Option<Vec<u8>>;
/// Bounty indices that have been approved but not yet funded.
pub BountyApprovals get(fn bounty_approvals): Vec<BountyIndex>;
}
}
decl_event!(
pub enum Event<T>
where
Balance = BalanceOf<T>,
<T as frame_system::Config>::AccountId,
{
/// New bounty proposal. \[index\]
BountyProposed(BountyIndex),
/// A bounty proposal was rejected; funds were slashed. \[index, bond\]
BountyRejected(BountyIndex, Balance),
/// A bounty proposal is funded and became active. \[index\]
BountyBecameActive(BountyIndex),
/// A bounty is awarded to a beneficiary. \[index, beneficiary\]
BountyAwarded(BountyIndex, AccountId),
/// A bounty is claimed by beneficiary. \[index, payout, beneficiary\]
BountyClaimed(BountyIndex, Balance, AccountId),
/// A bounty is cancelled. \[index\]
BountyCanceled(BountyIndex),
/// A bounty expiry is extended. \[index\]
BountyExtended(BountyIndex),
}
);
decl_error! {
/// Error for the treasury module.
pub enum Error for Module<T: Config> {
/// Proposer's balance is too low.
InsufficientProposersBalance,
/// No proposal or bounty at that index.
InvalidIndex,
/// The reason given is just too big.
ReasonTooBig,
/// The bounty status is unexpected.
UnexpectedStatus,
/// Require bounty curator.
RequireCurator,
/// Invalid bounty value.
InvalidValue,
/// Invalid bounty fee.
InvalidFee,
/// A bounty payout is pending.
/// To cancel the bounty, you must unassign and slash the curator.
PendingPayout,
/// The bounties cannot be claimed/closed because it's still in the countdown period.
Premature,
}
}
decl_module! {
pub struct Module<T: Config>
for enum Call
where origin: T::Origin
{
/// The amount held on deposit per byte within bounty description.
const DataDepositPerByte: BalanceOf<T> = T::DataDepositPerByte::get();
/// The amount held on deposit for placing a bounty proposal.
const BountyDepositBase: BalanceOf<T> = T::BountyDepositBase::get();
/// The delay period for which a bounty beneficiary need to wait before claim the payout.
const BountyDepositPayoutDelay: T::BlockNumber = T::BountyDepositPayoutDelay::get();
/// Percentage of the curator fee that will be reserved upfront as deposit for bounty curator.
const BountyCuratorDeposit: Permill = T::BountyCuratorDeposit::get();
/// Minimum value for a bounty.
const BountyValueMinimum: BalanceOf<T> = T::BountyValueMinimum::get();
/// Maximum acceptable reason length.
const MaximumReasonLength: u32 = T::MaximumReasonLength::get();
type Error = Error<T>;
fn deposit_event() = default;
/// Propose a new bounty.
///
/// The dispatch origin for this call must be _Signed_.
///
/// Payment: `TipReportDepositBase` will be reserved from the origin account, as well as
/// `DataDepositPerByte` for each byte in `reason`. It will be unreserved upon approval,
/// or slashed when rejected.
///
/// - `curator`: The curator account whom will manage this bounty.
/// - `fee`: The curator fee.
/// - `value`: The total payment amount of this bounty, curator fee included.
/// - `description`: The description of this bounty.
#[weight = <T as Config>::WeightInfo::propose_bounty(description.len() as u32)]
fn propose_bounty(
origin,
#[compact] value: BalanceOf<T>,
description: Vec<u8>,
) {
let proposer = ensure_signed(origin)?;
Self::create_bounty(proposer, description, value)?;
}
/// Approve a bounty proposal. At a later time, the bounty will be funded and become active
/// and the original deposit will be returned.
///
/// May only be called from `T::ApproveOrigin`.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::approve_bounty()]
fn approve_bounty(origin, #[compact] bounty_id: BountyIndex) {
T::ApproveOrigin::ensure_origin(origin)?;
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
let mut bounty = maybe_bounty.as_mut().ok_or(Error::<T>::InvalidIndex)?;
ensure!(bounty.status == BountyStatus::Proposed, Error::<T>::UnexpectedStatus);
bounty.status = BountyStatus::Approved;
BountyApprovals::append(bounty_id);
Ok(())
})?;
}
/// Assign a curator to a funded bounty.
///
/// May only be called from `T::ApproveOrigin`.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::propose_curator()]
fn propose_curator(
origin,
#[compact] bounty_id: BountyIndex,
curator: <T::Lookup as StaticLookup>::Source,
#[compact] fee: BalanceOf<T>,
) {
T::ApproveOrigin::ensure_origin(origin)?;
let curator = T::Lookup::lookup(curator)?;
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
let mut bounty = maybe_bounty.as_mut().ok_or(Error::<T>::InvalidIndex)?;
match bounty.status {
BountyStatus::Proposed | BountyStatus::Approved | BountyStatus::Funded => {},
_ => return Err(Error::<T>::UnexpectedStatus.into()),
};
ensure!(fee < bounty.value, Error::<T>::InvalidFee);
bounty.status = BountyStatus::CuratorProposed { curator };
bounty.fee = fee;
Ok(())
})?;
}
/// Unassign curator from a bounty.
///
/// This function can only be called by the `RejectOrigin` a signed origin.
///
/// If this function is called by the `RejectOrigin`, we assume that the curator is malicious
/// or inactive. As a result, we will slash the curator when possible.
///
/// If the origin is the curator, we take this as a sign they are unable to do their job and
/// they willingly give up. We could slash them, but for now we allow them to recover their
/// deposit and exit without issue. (We may want to change this if it is abused.)
///
/// Finally, the origin can be anyone if and only if the curator is "inactive". This allows
/// anyone in the community to call out that a curator is not doing their due diligence, and
/// we should pick a new curator. In this case the curator should also be slashed.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::unassign_curator()]
fn unassign_curator(
origin,
#[compact] bounty_id: BountyIndex,
) {
let maybe_sender = ensure_signed(origin.clone())
.map(Some)
.or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
let mut bounty = maybe_bounty.as_mut().ok_or(Error::<T>::InvalidIndex)?;
let slash_curator = |curator: &T::AccountId, curator_deposit: &mut BalanceOf<T>| {
let imbalance = T::Currency::slash_reserved(curator, *curator_deposit).0;
T::OnSlash::on_unbalanced(imbalance);
*curator_deposit = Zero::zero();
};
match bounty.status {
BountyStatus::Proposed | BountyStatus::Approved | BountyStatus::Funded => {
// No curator to unassign at this point.
return Err(Error::<T>::UnexpectedStatus.into())
}
BountyStatus::CuratorProposed { ref curator } => {
// A curator has been proposed, but not accepted yet.
// Either `RejectOrigin` or the proposed curator can unassign the curator.
ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
},
BountyStatus::Active { ref curator, ref update_due } => {
// The bounty is active.
match maybe_sender {
// If the `RejectOrigin` is calling this function, slash the curator.
None => {
slash_curator(curator, &mut bounty.curator_deposit);
// Continue to change bounty status below...
},
Some(sender) => {
// If the sender is not the curator, and the curator is inactive,
// slash the curator.
if sender != *curator {
let block_number = system::Module::<T>::block_number();
if *update_due < block_number {
slash_curator(curator, &mut bounty.curator_deposit);
// Continue to change bounty status below...
} else {
// Curator has more time to give an update.
return Err(Error::<T>::Premature.into())
}
} else {
// Else this is the curator, willingly giving up their role.
// Give back their deposit.
let _ = T::Currency::unreserve(&curator, bounty.curator_deposit);
// Continue to change bounty status below...
}
},
}
},
BountyStatus::PendingPayout { ref curator, .. } => {
// The bounty is pending payout, so only council can unassign a curator.
// By doing so, they are claiming the curator is acting maliciously, so
// we slash the curator.
ensure!(maybe_sender.is_none(), BadOrigin);
slash_curator(curator, &mut bounty.curator_deposit);
// Continue to change bounty status below...
}
};
bounty.status = BountyStatus::Funded;
Ok(())
})?;
}
/// Accept the curator role for a bounty.
/// A deposit will be reserved from curator and refund upon successful payout.
///
/// May only be called from the curator.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::accept_curator()]
fn accept_curator(origin, #[compact] bounty_id: BountyIndex) {
let signer = ensure_signed(origin)?;
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
let mut bounty = maybe_bounty.as_mut().ok_or(Error::<T>::InvalidIndex)?;
match bounty.status {
BountyStatus::CuratorProposed { ref curator } => {
ensure!(signer == *curator, Error::<T>::RequireCurator);
let deposit = T::BountyCuratorDeposit::get() * bounty.fee;
T::Currency::reserve(curator, deposit)?;
bounty.curator_deposit = deposit;
let update_due = system::Module::<T>::block_number() + T::BountyUpdatePeriod::get();
bounty.status = BountyStatus::Active { curator: curator.clone(), update_due };
Ok(())
},
_ => Err(Error::<T>::UnexpectedStatus.into()),
}
})?;
}
/// Award bounty to a beneficiary account. The beneficiary will be able to claim the funds after a delay.
///
/// The dispatch origin for this call must be the curator of this bounty.
///
/// - `bounty_id`: Bounty ID to award.
/// - `beneficiary`: The beneficiary account whom will receive the payout.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::award_bounty()]
fn award_bounty(origin, #[compact] bounty_id: BountyIndex, beneficiary: <T::Lookup as StaticLookup>::Source) {
let signer = ensure_signed(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
let mut bounty = maybe_bounty.as_mut().ok_or(Error::<T>::InvalidIndex)?;
match &bounty.status {
BountyStatus::Active {
curator,
..
} => {
ensure!(signer == *curator, Error::<T>::RequireCurator);
},
_ => return Err(Error::<T>::UnexpectedStatus.into()),
}
bounty.status = BountyStatus::PendingPayout {
curator: signer,
beneficiary: beneficiary.clone(),
unlock_at: system::Module::<T>::block_number() + T::BountyDepositPayoutDelay::get(),
};
Ok(())
})?;
Self::deposit_event(Event::<T>::BountyAwarded(bounty_id, beneficiary));
}
/// Claim the payout from an awarded bounty after payout delay.
///
/// The dispatch origin for this call must be the beneficiary of this bounty.
///
/// - `bounty_id`: Bounty ID to claim.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::claim_bounty()]
fn claim_bounty(origin, #[compact] bounty_id: BountyIndex) {
let _ = ensure_signed(origin)?; // anyone can trigger claim
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
let bounty = maybe_bounty.take().ok_or(Error::<T>::InvalidIndex)?;
if let BountyStatus::PendingPayout { curator, beneficiary, unlock_at } = bounty.status {
ensure!(system::Module::<T>::block_number() >= unlock_at, Error::<T>::Premature);
let bounty_account = Self::bounty_account_id(bounty_id);
let balance = T::Currency::free_balance(&bounty_account);
let fee = bounty.fee.min(balance); // just to be safe
let payout = balance.saturating_sub(fee);
let _ = T::Currency::unreserve(&curator, bounty.curator_deposit);
let _ = T::Currency::transfer(&bounty_account, &curator, fee, AllowDeath); // should not fail
let _ = T::Currency::transfer(&bounty_account, &beneficiary, payout, AllowDeath); // should not fail
*maybe_bounty = None;
BountyDescriptions::remove(bounty_id);
Self::deposit_event(Event::<T>::BountyClaimed(bounty_id, payout, beneficiary));
Ok(())
} else {
Err(Error::<T>::UnexpectedStatus.into())
}
})?;
}
/// Cancel a proposed or active bounty. All the funds will be sent to treasury and
/// the curator deposit will be unreserved if possible.
///
/// Only `T::RejectOrigin` is able to cancel a bounty.
///
/// - `bounty_id`: Bounty ID to cancel.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::close_bounty_proposed().max(<T as Config>::WeightInfo::close_bounty_active())]
fn close_bounty(origin, #[compact] bounty_id: BountyIndex) -> DispatchResultWithPostInfo {
T::RejectOrigin::ensure_origin(origin)?;
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResultWithPostInfo {
let bounty = maybe_bounty.as_ref().ok_or(Error::<T>::InvalidIndex)?;
match &bounty.status {
BountyStatus::Proposed => {
// The reject origin would like to cancel a proposed bounty.
BountyDescriptions::remove(bounty_id);
let value = bounty.bond;
let imbalance = T::Currency::slash_reserved(&bounty.proposer, value).0;
T::OnSlash::on_unbalanced(imbalance);
*maybe_bounty = None;
Self::deposit_event(Event::<T>::BountyRejected(bounty_id, value));
// Return early, nothing else to do.
return Ok(Some(<T as Config>::WeightInfo::close_bounty_proposed()).into())
},
BountyStatus::Approved => {
// For weight reasons, we don't allow a council to cancel in this phase.
// We ask for them to wait until it is funded before they can cancel.
return Err(Error::<T>::UnexpectedStatus.into())
},
BountyStatus::Funded |
BountyStatus::CuratorProposed { .. } => {
// Nothing extra to do besides the removal of the bounty below.
},
BountyStatus::Active { curator, .. } => {
// Cancelled by council, refund deposit of the working curator.
let _ = T::Currency::unreserve(&curator, bounty.curator_deposit);
// Then execute removal of the bounty below.
},
BountyStatus::PendingPayout { .. } => {
// Bounty is already pending payout. If council wants to cancel
// this bounty, it should mean the curator was acting maliciously.
// So the council should first unassign the curator, slashing their
// deposit.
return Err(Error::<T>::PendingPayout.into())
}
}
let bounty_account = Self::bounty_account_id(bounty_id);
BountyDescriptions::remove(bounty_id);
let balance = T::Currency::free_balance(&bounty_account);
let _ = T::Currency::transfer(&bounty_account, &Self::account_id(), balance, AllowDeath); // should not fail
*maybe_bounty = None;
Self::deposit_event(Event::<T>::BountyCanceled(bounty_id));
Ok(Some(<T as Config>::WeightInfo::close_bounty_active()).into())
})
}
/// Extend the expiry time of an active bounty.
///
/// The dispatch origin for this call must be the curator of this bounty.
///
/// - `bounty_id`: Bounty ID to extend.
/// - `remark`: additional information.
///
/// # <weight>
/// - O(1).
/// # </weight>
#[weight = <T as Config>::WeightInfo::extend_bounty_expiry()]
fn extend_bounty_expiry(origin, #[compact] bounty_id: BountyIndex, _remark: Vec<u8>) {
let signer = ensure_signed(origin)?;
Bounties::<T>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
let bounty = maybe_bounty.as_mut().ok_or(Error::<T>::InvalidIndex)?;
match bounty.status {
BountyStatus::Active { ref curator, ref mut update_due } => {
ensure!(*curator == signer, Error::<T>::RequireCurator);
*update_due = (system::Module::<T>::block_number() + T::BountyUpdatePeriod::get()).max(*update_due);
},
_ => return Err(Error::<T>::UnexpectedStatus.into()),
}
Ok(())
})?;
Self::deposit_event(Event::<T>::BountyExtended(bounty_id));
}
}
}
impl<T: Config> Module<T> {
// Add public immutables and private mutables.
/// The account ID of the treasury pot.
///
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn account_id() -> T::AccountId {
T::ModuleId::get().into_account()
}
/// The account ID of a bounty account
pub fn bounty_account_id(id: BountyIndex) -> T::AccountId {
// only use two byte prefix to support 16 byte account id (used by test)
// "modl" ++ "py/trsry" ++ "bt" is 14 bytes, and two bytes remaining for bounty index
T::ModuleId::get().into_sub_account(("bt", id))
}
fn create_bounty(
proposer: T::AccountId,
description: Vec<u8>,
value: BalanceOf<T>,
) -> DispatchResult {
ensure!(description.len() <= T::MaximumReasonLength::get() as usize, Error::<T>::ReasonTooBig);
ensure!(value >= T::BountyValueMinimum::get(), Error::<T>::InvalidValue);
let index = Self::bounty_count();
// reserve deposit for new bounty
let bond = T::BountyDepositBase::get()
+ T::DataDepositPerByte::get() * (description.len() as u32).into();
T::Currency::reserve(&proposer, bond)
.map_err(|_| Error::<T>::InsufficientProposersBalance)?;
BountyCount::put(index + 1);
let bounty = Bounty {
proposer,
value,
fee: 0u32.into(),
curator_deposit: 0u32.into(),
bond,
status: BountyStatus::Proposed,
};
Bounties::<T>::insert(index, &bounty);
BountyDescriptions::insert(index, description);
Self::deposit_event(RawEvent::BountyProposed(index));
Ok(())
}
}
impl<T: Config> pallet_treasury::SpendFunds<T> for Module<T> {
fn spend_funds(
budget_remaining: &mut BalanceOf<T>,
imbalance: &mut PositiveImbalanceOf<T>,
total_weight: &mut Weight,
missed_any: &mut bool
) {
let bounties_len = BountyApprovals::mutate(|v| {
let bounties_approval_len = v.len() as u32;
v.retain(|&index| {
Bounties::<T>::mutate(index, |bounty| {
// Should always be true, but shouldn't panic if false or we're screwed.
if let Some(bounty) = bounty {
if bounty.value <= *budget_remaining {
*budget_remaining -= bounty.value;
bounty.status = BountyStatus::Funded;
// return their deposit.
let _ = T::Currency::unreserve(&bounty.proposer, bounty.bond);
// fund the bounty account
imbalance.subsume(T::Currency::deposit_creating(&Self::bounty_account_id(index), bounty.value));
Self::deposit_event(RawEvent::BountyBecameActive(index));
false
} else {
*missed_any = true;
true
}
} else {
false
}
})
});
bounties_approval_len
});
*total_weight += <T as Config>::WeightInfo::spend_funds(bounties_len);
}
}