mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 07:01:03 +00:00
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:
@@ -0,0 +1,247 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 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 pallet benchmarking.
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
|
||||
use sp_runtime::traits::Bounded;
|
||||
use frame_system::{EventRecord, RawOrigin};
|
||||
use frame_benchmarking::{benchmarks, account, whitelisted_caller};
|
||||
use frame_support::traits::OnInitialize;
|
||||
|
||||
use crate::Module as Bounties;
|
||||
use pallet_treasury::Module as Treasury;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
// Create bounties that are approved for use in `on_initialize`.
|
||||
fn create_approved_bounties<T: Config>(n: u32) -> Result<(), &'static str> {
|
||||
for i in 0 .. n {
|
||||
let (caller, _curator, _fee, value, reason) = setup_bounty::<T>(i, MAX_BYTES);
|
||||
Bounties::<T>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
Bounties::<T>::approve_bounty(RawOrigin::Root.into(), bounty_id)?;
|
||||
}
|
||||
ensure!(BountyApprovals::get().len() == n as usize, "Not all bounty approved");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Create the pre-requisite information needed to create a treasury `propose_bounty`.
|
||||
fn setup_bounty<T: Config>(u: u32, d: u32) -> (
|
||||
T::AccountId,
|
||||
T::AccountId,
|
||||
BalanceOf<T>,
|
||||
BalanceOf<T>,
|
||||
Vec<u8>,
|
||||
) {
|
||||
let caller = account("caller", u, SEED);
|
||||
let value: BalanceOf<T> = T::BountyValueMinimum::get().saturating_mul(100u32.into());
|
||||
let fee = value / 2u32.into();
|
||||
let deposit = T::BountyDepositBase::get() + T::DataDepositPerByte::get() * MAX_BYTES.into();
|
||||
let _ = T::Currency::make_free_balance_be(&caller, deposit);
|
||||
let curator = account("curator", u, SEED);
|
||||
let _ = T::Currency::make_free_balance_be(&curator, fee / 2u32.into());
|
||||
let reason = vec![0; d as usize];
|
||||
(caller, curator, fee, value, reason)
|
||||
}
|
||||
|
||||
fn create_bounty<T: Config>() -> Result<(
|
||||
<T::Lookup as StaticLookup>::Source,
|
||||
BountyIndex,
|
||||
), &'static str> {
|
||||
let (caller, curator, fee, value, reason) = setup_bounty::<T>(0, MAX_BYTES);
|
||||
let curator_lookup = T::Lookup::unlookup(curator.clone());
|
||||
Bounties::<T>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
Bounties::<T>::approve_bounty(RawOrigin::Root.into(), bounty_id)?;
|
||||
Treasury::<T>::on_initialize(T::BlockNumber::zero());
|
||||
Bounties::<T>::propose_curator(RawOrigin::Root.into(), bounty_id, curator_lookup.clone(), fee)?;
|
||||
Bounties::<T>::accept_curator(RawOrigin::Signed(curator).into(), bounty_id)?;
|
||||
Ok((curator_lookup, bounty_id))
|
||||
}
|
||||
|
||||
fn setup_pod_account<T: Config>() {
|
||||
let pot_account = Bounties::<T>::account_id();
|
||||
let value = T::Currency::minimum_balance().saturating_mul(1_000_000_000u32.into());
|
||||
let _ = T::Currency::make_free_balance_be(&pot_account, value);
|
||||
}
|
||||
|
||||
fn assert_last_event<T: Config>(generic_event: <T as Config>::Event) {
|
||||
let events = frame_system::Module::<T>::events();
|
||||
let system_event: <T as frame_system::Config>::Event = generic_event.into();
|
||||
// compare to the last event record
|
||||
let EventRecord { event, .. } = &events[events.len() - 1];
|
||||
assert_eq!(event, &system_event);
|
||||
}
|
||||
|
||||
const MAX_BYTES: u32 = 16384;
|
||||
|
||||
benchmarks! {
|
||||
_ { }
|
||||
|
||||
propose_bounty {
|
||||
let d in 0 .. MAX_BYTES;
|
||||
|
||||
let (caller, curator, fee, value, description) = setup_bounty::<T>(0, d);
|
||||
}: _(RawOrigin::Signed(caller), value, description)
|
||||
|
||||
approve_bounty {
|
||||
let (caller, curator, fee, value, reason) = setup_bounty::<T>(0, MAX_BYTES);
|
||||
Bounties::<T>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
}: _(RawOrigin::Root, bounty_id)
|
||||
|
||||
propose_curator {
|
||||
setup_pod_account::<T>();
|
||||
let (caller, curator, fee, value, reason) = setup_bounty::<T>(0, MAX_BYTES);
|
||||
let curator_lookup = T::Lookup::unlookup(curator.clone());
|
||||
Bounties::<T>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
Bounties::<T>::approve_bounty(RawOrigin::Root.into(), bounty_id)?;
|
||||
Bounties::<T>::on_initialize(T::BlockNumber::zero());
|
||||
}: _(RawOrigin::Root, bounty_id, curator_lookup, fee)
|
||||
|
||||
// Worst case when curator is inactive and any sender unassigns the curator.
|
||||
unassign_curator {
|
||||
setup_pod_account::<T>();
|
||||
let (curator_lookup, bounty_id) = create_bounty::<T>()?;
|
||||
Bounties::<T>::on_initialize(T::BlockNumber::zero());
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
frame_system::Module::<T>::set_block_number(T::BountyUpdatePeriod::get() + 1u32.into());
|
||||
let caller = whitelisted_caller();
|
||||
}: _(RawOrigin::Signed(caller), bounty_id)
|
||||
|
||||
accept_curator {
|
||||
setup_pod_account::<T>();
|
||||
let (caller, curator, fee, value, reason) = setup_bounty::<T>(0, MAX_BYTES);
|
||||
let curator_lookup = T::Lookup::unlookup(curator.clone());
|
||||
Bounties::<T>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
Bounties::<T>::approve_bounty(RawOrigin::Root.into(), bounty_id)?;
|
||||
Bounties::<T>::on_initialize(T::BlockNumber::zero());
|
||||
Bounties::<T>::propose_curator(RawOrigin::Root.into(), bounty_id, curator_lookup, fee)?;
|
||||
}: _(RawOrigin::Signed(curator), bounty_id)
|
||||
|
||||
award_bounty {
|
||||
setup_pod_account::<T>();
|
||||
let (curator_lookup, bounty_id) = create_bounty::<T>()?;
|
||||
Bounties::<T>::on_initialize(T::BlockNumber::zero());
|
||||
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
let curator = T::Lookup::lookup(curator_lookup)?;
|
||||
let beneficiary = T::Lookup::unlookup(account("beneficiary", 0, SEED));
|
||||
}: _(RawOrigin::Signed(curator), bounty_id, beneficiary)
|
||||
|
||||
claim_bounty {
|
||||
setup_pod_account::<T>();
|
||||
let (curator_lookup, bounty_id) = create_bounty::<T>()?;
|
||||
Bounties::<T>::on_initialize(T::BlockNumber::zero());
|
||||
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
let curator = T::Lookup::lookup(curator_lookup)?;
|
||||
|
||||
let beneficiary_account: T::AccountId = account("beneficiary", 0, SEED);
|
||||
let beneficiary = T::Lookup::unlookup(beneficiary_account.clone());
|
||||
Bounties::<T>::award_bounty(RawOrigin::Signed(curator.clone()).into(), bounty_id, beneficiary)?;
|
||||
|
||||
frame_system::Module::<T>::set_block_number(T::BountyDepositPayoutDelay::get());
|
||||
ensure!(T::Currency::free_balance(&beneficiary_account).is_zero(), "Beneficiary already has balance");
|
||||
|
||||
}: _(RawOrigin::Signed(curator), bounty_id)
|
||||
verify {
|
||||
ensure!(!T::Currency::free_balance(&beneficiary_account).is_zero(), "Beneficiary didn't get paid");
|
||||
}
|
||||
|
||||
close_bounty_proposed {
|
||||
setup_pod_account::<T>();
|
||||
let (caller, curator, fee, value, reason) = setup_bounty::<T>(0, 0);
|
||||
Bounties::<T>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
}: close_bounty(RawOrigin::Root, bounty_id)
|
||||
|
||||
close_bounty_active {
|
||||
setup_pod_account::<T>();
|
||||
let (curator_lookup, bounty_id) = create_bounty::<T>()?;
|
||||
Bounties::<T>::on_initialize(T::BlockNumber::zero());
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
}: close_bounty(RawOrigin::Root, bounty_id)
|
||||
verify {
|
||||
assert_last_event::<T>(RawEvent::BountyCanceled(bounty_id).into())
|
||||
}
|
||||
|
||||
extend_bounty_expiry {
|
||||
setup_pod_account::<T>();
|
||||
let (curator_lookup, bounty_id) = create_bounty::<T>()?;
|
||||
Bounties::<T>::on_initialize(T::BlockNumber::zero());
|
||||
|
||||
let bounty_id = BountyCount::get() - 1;
|
||||
let curator = T::Lookup::lookup(curator_lookup)?;
|
||||
}: _(RawOrigin::Signed(curator), bounty_id, Vec::new())
|
||||
verify {
|
||||
assert_last_event::<T>(RawEvent::BountyExtended(bounty_id).into())
|
||||
}
|
||||
|
||||
spend_funds {
|
||||
let b in 1 .. 100;
|
||||
setup_pod_account::<T>();
|
||||
create_approved_bounties::<T>(b)?;
|
||||
|
||||
let mut budget_remaining = BalanceOf::<T>::max_value();
|
||||
let mut imbalance = PositiveImbalanceOf::<T>::zero();
|
||||
let mut total_weight = Weight::zero();
|
||||
let mut missed_any = false;
|
||||
}: {
|
||||
<Bounties<T> as pallet_treasury::SpendFunds<T>>::spend_funds(
|
||||
&mut budget_remaining,
|
||||
&mut imbalance,
|
||||
&mut total_weight,
|
||||
&mut missed_any,
|
||||
);
|
||||
}
|
||||
verify {
|
||||
ensure!(budget_remaining < BalanceOf::<T>::max_value(), "Budget not used");
|
||||
ensure!(missed_any == false, "Missed some");
|
||||
assert_last_event::<T>(RawEvent::BountyBecameActive(b - 1).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::{new_test_ext, Test};
|
||||
use frame_support::assert_ok;
|
||||
|
||||
#[test]
|
||||
fn test_benchmarks() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(test_benchmark_propose_bounty::<Test>());
|
||||
assert_ok!(test_benchmark_approve_bounty::<Test>());
|
||||
assert_ok!(test_benchmark_propose_curator::<Test>());
|
||||
assert_ok!(test_benchmark_unassign_curator::<Test>());
|
||||
assert_ok!(test_benchmark_accept_curator::<Test>());
|
||||
assert_ok!(test_benchmark_award_bounty::<Test>());
|
||||
assert_ok!(test_benchmark_claim_bounty::<Test>());
|
||||
assert_ok!(test_benchmark_close_bounty_proposed::<Test>());
|
||||
assert_ok!(test_benchmark_close_bounty_active::<Test>());
|
||||
assert_ok!(test_benchmark_extend_bounty_expiry::<Test>());
|
||||
assert_ok!(test_benchmark_spend_funds::<Test>());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 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 pallet tests.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use super::*;
|
||||
use std::cell::RefCell;
|
||||
|
||||
use frame_support::{
|
||||
assert_noop, assert_ok, impl_outer_origin, parameter_types, weights::Weight,
|
||||
impl_outer_event, traits::{OnInitialize}
|
||||
};
|
||||
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
Perbill, ModuleId,
|
||||
testing::Header,
|
||||
traits::{BlakeTwo256, IdentityLookup, BadOrigin},
|
||||
};
|
||||
|
||||
impl_outer_origin! {
|
||||
pub enum Origin for Test where system = frame_system {}
|
||||
}
|
||||
|
||||
mod bounties {
|
||||
// Re-export needed for `impl_outer_event!`.
|
||||
pub use crate::*;
|
||||
}
|
||||
|
||||
impl_outer_event! {
|
||||
pub enum Event for Test {
|
||||
system<T>,
|
||||
pallet_balances<T>,
|
||||
pallet_treasury<T>,
|
||||
bounties<T>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct Test;
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Call = ();
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u128; // u64 is not enough to hold bytes used to generate bounty account
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = Event;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = ();
|
||||
type AccountData = pallet_balances::AccountData<u64>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
}
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u64 = 1;
|
||||
}
|
||||
impl pallet_balances::Config for Test {
|
||||
type MaxLocks = ();
|
||||
type Balance = u64;
|
||||
type Event = Event;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
thread_local! {
|
||||
static TEN_TO_FOURTEEN: RefCell<Vec<u128>> = RefCell::new(vec![10,11,12,13,14]);
|
||||
}
|
||||
parameter_types! {
|
||||
pub const ProposalBond: Permill = Permill::from_percent(5);
|
||||
pub const ProposalBondMinimum: u64 = 1;
|
||||
pub const SpendPeriod: u64 = 2;
|
||||
pub const Burn: Permill = Permill::from_percent(50);
|
||||
pub const DataDepositPerByte: u64 = 1;
|
||||
pub const TreasuryModuleId: ModuleId = ModuleId(*b"py/trsry");
|
||||
}
|
||||
// impl pallet_treasury::Config for Test {
|
||||
impl pallet_treasury::Config for Test {
|
||||
type ModuleId = TreasuryModuleId;
|
||||
type Currency = pallet_balances::Module<Test>;
|
||||
type ApproveOrigin = frame_system::EnsureRoot<u128>;
|
||||
type RejectOrigin = frame_system::EnsureRoot<u128>;
|
||||
type Event = Event;
|
||||
type OnSlash = ();
|
||||
type ProposalBond = ProposalBond;
|
||||
type ProposalBondMinimum = ProposalBondMinimum;
|
||||
type SpendPeriod = SpendPeriod;
|
||||
type Burn = Burn;
|
||||
type BurnDestination = (); // Just gets burned.
|
||||
type WeightInfo = ();
|
||||
type SpendFunds = Bounties;
|
||||
}
|
||||
parameter_types! {
|
||||
pub const BountyDepositBase: u64 = 80;
|
||||
pub const BountyDepositPayoutDelay: u64 = 3;
|
||||
pub const BountyUpdatePeriod: u32 = 20;
|
||||
pub const BountyCuratorDeposit: Permill = Permill::from_percent(50);
|
||||
pub const BountyValueMinimum: u64 = 1;
|
||||
pub const MaximumReasonLength: u32 = 16384;
|
||||
}
|
||||
impl Config for Test {
|
||||
type Event = Event;
|
||||
type BountyDepositBase = BountyDepositBase;
|
||||
type BountyDepositPayoutDelay = BountyDepositPayoutDelay;
|
||||
type BountyUpdatePeriod = BountyUpdatePeriod;
|
||||
type BountyCuratorDeposit = BountyCuratorDeposit;
|
||||
type BountyValueMinimum = BountyValueMinimum;
|
||||
type DataDepositPerByte = DataDepositPerByte;
|
||||
type MaximumReasonLength = MaximumReasonLength;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
type System = frame_system::Module<Test>;
|
||||
type Balances = pallet_balances::Module<Test>;
|
||||
type Treasury = pallet_treasury::Module<Test>;
|
||||
type Bounties = Module<Test>;
|
||||
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test>{
|
||||
// Total issuance will be 200 with treasury account initialized at ED.
|
||||
balances: vec![(0, 100), (1, 98), (2, 1)],
|
||||
}.assimilate_storage(&mut t).unwrap();
|
||||
pallet_treasury::GenesisConfig::default().assimilate_storage::<Test, _>(&mut t).unwrap();
|
||||
t.into()
|
||||
}
|
||||
|
||||
fn last_event() -> RawEvent<u64, u128> {
|
||||
System::events().into_iter().map(|r| r.event)
|
||||
.filter_map(|e| {
|
||||
if let Event::bounties(inner) = e { Some(inner) } else { None }
|
||||
})
|
||||
.last()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_config_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_eq!(Treasury::pot(), 0);
|
||||
assert_eq!(Treasury::proposal_count(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minting_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Check that accumulate works when we have Some value in Dummy already.
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_eq!(Treasury::pot(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spend_proposal_takes_min_deposit() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 1, 3));
|
||||
assert_eq!(Balances::free_balance(0), 99);
|
||||
assert_eq!(Balances::reserved_balance(0), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spend_proposal_takes_proportional_deposit() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 100, 3));
|
||||
assert_eq!(Balances::free_balance(0), 95);
|
||||
assert_eq!(Balances::reserved_balance(0), 5);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spend_proposal_fails_when_proposer_poor() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
Treasury::propose_spend(Origin::signed(2), 100, 3),
|
||||
Error::<Test>::InsufficientProposersBalance,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepted_spend_proposal_ignored_outside_spend_period() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 100, 3));
|
||||
assert_ok!(Treasury::approve_proposal(Origin::root(), 0));
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(1);
|
||||
assert_eq!(Balances::free_balance(3), 0);
|
||||
assert_eq!(Treasury::pot(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_pot_should_diminish() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let init_total_issuance = Balances::total_issuance();
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_eq!(Balances::total_issuance(), init_total_issuance + 100);
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
assert_eq!(Treasury::pot(), 50);
|
||||
assert_eq!(Balances::total_issuance(), init_total_issuance + 50);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_spend_proposal_ignored_on_spend_period() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 100, 3));
|
||||
assert_ok!(Treasury::reject_proposal(Origin::root(), 0));
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
assert_eq!(Balances::free_balance(3), 0);
|
||||
assert_eq!(Treasury::pot(), 50);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_already_rejected_spend_proposal_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 100, 3));
|
||||
assert_ok!(Treasury::reject_proposal(Origin::root(), 0));
|
||||
assert_noop!(Treasury::reject_proposal(Origin::root(), 0), Error::<Test>::InvalidIndex);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_non_existent_spend_proposal_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(Treasury::reject_proposal(Origin::root(), 0), Error::<Test>::InvalidIndex);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_non_existent_spend_proposal_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(Treasury::approve_proposal(Origin::root(), 0), Error::<Test>::InvalidIndex);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_already_rejected_spend_proposal_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 100, 3));
|
||||
assert_ok!(Treasury::reject_proposal(Origin::root(), 0));
|
||||
assert_noop!(Treasury::approve_proposal(Origin::root(), 0), Error::<Test>::InvalidIndex);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepted_spend_proposal_enacted_on_spend_period() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_eq!(Treasury::pot(), 100);
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 100, 3));
|
||||
assert_ok!(Treasury::approve_proposal(Origin::root(), 0));
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
assert_eq!(Balances::free_balance(3), 100);
|
||||
assert_eq!(Treasury::pot(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pot_underflow_should_not_diminish() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_eq!(Treasury::pot(), 100);
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 150, 3));
|
||||
assert_ok!(Treasury::approve_proposal(Origin::root(), 0));
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
assert_eq!(Treasury::pot(), 100); // Pot hasn't changed
|
||||
|
||||
let _ = Balances::deposit_into_existing(&Treasury::account_id(), 100).unwrap();
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(4);
|
||||
assert_eq!(Balances::free_balance(3), 150); // Fund has been spent
|
||||
assert_eq!(Treasury::pot(), 25); // Pot has finally changed
|
||||
});
|
||||
}
|
||||
|
||||
// Treasury account doesn't get deleted if amount approved to spend is all its free balance.
|
||||
// i.e. pot should not include existential deposit needed for account survival.
|
||||
#[test]
|
||||
fn treasury_account_doesnt_get_deleted() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_eq!(Treasury::pot(), 100);
|
||||
let treasury_balance = Balances::free_balance(&Treasury::account_id());
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), treasury_balance, 3));
|
||||
assert_ok!(Treasury::approve_proposal(Origin::root(), 0));
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
assert_eq!(Treasury::pot(), 100); // Pot hasn't changed
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), Treasury::pot(), 3));
|
||||
assert_ok!(Treasury::approve_proposal(Origin::root(), 1));
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(4);
|
||||
assert_eq!(Treasury::pot(), 0); // Pot is emptied
|
||||
assert_eq!(Balances::free_balance(Treasury::account_id()), 1); // but the account is still there
|
||||
});
|
||||
}
|
||||
|
||||
// In case treasury account is not existing then it works fine.
|
||||
// This is useful for chain that will just update runtime.
|
||||
#[test]
|
||||
fn inexistent_account_works() {
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test>{
|
||||
balances: vec![(0, 100), (1, 99), (2, 1)],
|
||||
}.assimilate_storage(&mut t).unwrap();
|
||||
// Treasury genesis config is not build thus treasury account does not exist
|
||||
let mut t: sp_io::TestExternalities = t.into();
|
||||
|
||||
t.execute_with(|| {
|
||||
assert_eq!(Balances::free_balance(Treasury::account_id()), 0); // Account does not exist
|
||||
assert_eq!(Treasury::pot(), 0); // Pot is empty
|
||||
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 99, 3));
|
||||
assert_ok!(Treasury::approve_proposal(Origin::root(), 0));
|
||||
assert_ok!(Treasury::propose_spend(Origin::signed(0), 1, 3));
|
||||
assert_ok!(Treasury::approve_proposal(Origin::root(), 1));
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
assert_eq!(Treasury::pot(), 0); // Pot hasn't changed
|
||||
assert_eq!(Balances::free_balance(3), 0); // Balance of `3` hasn't changed
|
||||
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 100);
|
||||
assert_eq!(Treasury::pot(), 99); // Pot now contains funds
|
||||
assert_eq!(Balances::free_balance(Treasury::account_id()), 100); // Account does exist
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(4);
|
||||
|
||||
assert_eq!(Treasury::pot(), 0); // Pot has changed
|
||||
assert_eq!(Balances::free_balance(3), 99); // Balance of `3` has changed
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propose_bounty_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_eq!(Treasury::pot(), 100);
|
||||
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 10, b"1234567890".to_vec()));
|
||||
|
||||
assert_eq!(last_event(), RawEvent::BountyProposed(0));
|
||||
|
||||
let deposit: u64 = 85 + 5;
|
||||
assert_eq!(Balances::reserved_balance(0), deposit);
|
||||
assert_eq!(Balances::free_balance(0), 100 - deposit);
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 0,
|
||||
curator_deposit: 0,
|
||||
value: 10,
|
||||
bond: deposit,
|
||||
status: BountyStatus::Proposed,
|
||||
});
|
||||
|
||||
assert_eq!(Bounties::bounty_descriptions(0).unwrap(), b"1234567890".to_vec());
|
||||
|
||||
assert_eq!(Bounties::bounty_count(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propose_bounty_validation_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_eq!(Treasury::pot(), 100);
|
||||
|
||||
assert_noop!(
|
||||
Bounties::propose_bounty(Origin::signed(1), 0, [0; 17_000].to_vec()),
|
||||
Error::<Test>::ReasonTooBig
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
Bounties::propose_bounty(Origin::signed(1), 10, b"12345678901234567890".to_vec()),
|
||||
Error::<Test>::InsufficientProposersBalance
|
||||
);
|
||||
|
||||
assert_noop!(
|
||||
Bounties::propose_bounty(Origin::signed(1), 0, b"12345678901234567890".to_vec()),
|
||||
Error::<Test>::InvalidValue
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_bounty_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_noop!(Bounties::close_bounty(Origin::root(), 0), Error::<Test>::InvalidIndex);
|
||||
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 10, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::close_bounty(Origin::root(), 0));
|
||||
|
||||
let deposit: u64 = 80 + 5;
|
||||
|
||||
assert_eq!(last_event(), RawEvent::BountyRejected(0, deposit));
|
||||
|
||||
assert_eq!(Balances::reserved_balance(0), 0);
|
||||
assert_eq!(Balances::free_balance(0), 100 - deposit);
|
||||
|
||||
assert_eq!(Bounties::bounties(0), None);
|
||||
assert!(!pallet_treasury::Proposals::<Test>::contains_key(0));
|
||||
|
||||
assert_eq!(Bounties::bounty_descriptions(0), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approve_bounty_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_noop!(Bounties::approve_bounty(Origin::root(), 0), Error::<Test>::InvalidIndex);
|
||||
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
let deposit: u64 = 80 + 5;
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 0,
|
||||
value: 50,
|
||||
curator_deposit: 0,
|
||||
bond: deposit,
|
||||
status: BountyStatus::Approved,
|
||||
});
|
||||
assert_eq!(Bounties::bounty_approvals(), vec![0]);
|
||||
|
||||
assert_noop!(Bounties::close_bounty(Origin::root(), 0), Error::<Test>::UnexpectedStatus);
|
||||
|
||||
// deposit not returned yet
|
||||
assert_eq!(Balances::reserved_balance(0), deposit);
|
||||
assert_eq!(Balances::free_balance(0), 100 - deposit);
|
||||
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
// return deposit
|
||||
assert_eq!(Balances::reserved_balance(0), 0);
|
||||
assert_eq!(Balances::free_balance(0), 100);
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 0,
|
||||
curator_deposit: 0,
|
||||
value: 50,
|
||||
bond: deposit,
|
||||
status: BountyStatus::Funded,
|
||||
});
|
||||
|
||||
assert_eq!(Treasury::pot(), 100 - 50 - 25); // burn 25
|
||||
assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 50);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign_curator_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
|
||||
assert_noop!(Bounties::propose_curator(Origin::root(), 0, 4, 4), Error::<Test>::InvalidIndex);
|
||||
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_noop!(Bounties::propose_curator(Origin::root(), 0, 4, 50), Error::<Test>::InvalidFee);
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 4));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 4,
|
||||
curator_deposit: 0,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::CuratorProposed {
|
||||
curator: 4,
|
||||
},
|
||||
});
|
||||
|
||||
assert_noop!(Bounties::accept_curator(Origin::signed(1), 0), Error::<Test>::RequireCurator);
|
||||
assert_noop!(Bounties::accept_curator(Origin::signed(4), 0), pallet_balances::Error::<Test, _>::InsufficientBalance);
|
||||
|
||||
Balances::make_free_balance_be(&4, 10);
|
||||
|
||||
assert_ok!(Bounties::accept_curator(Origin::signed(4), 0));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 4,
|
||||
curator_deposit: 2,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::Active {
|
||||
curator: 4,
|
||||
update_due: 22,
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(Balances::free_balance(&4), 8);
|
||||
assert_eq!(Balances::reserved_balance(&4), 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unassign_curator_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 4));
|
||||
|
||||
assert_noop!(Bounties::unassign_curator(Origin::signed(1), 0), BadOrigin);
|
||||
|
||||
assert_ok!(Bounties::unassign_curator(Origin::signed(4), 0));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 4,
|
||||
curator_deposit: 0,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::Funded,
|
||||
});
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 4));
|
||||
|
||||
Balances::make_free_balance_be(&4, 10);
|
||||
|
||||
assert_ok!(Bounties::accept_curator(Origin::signed(4), 0));
|
||||
|
||||
assert_ok!(Bounties::unassign_curator(Origin::root(), 0));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 4,
|
||||
curator_deposit: 0,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::Funded,
|
||||
});
|
||||
|
||||
assert_eq!(Balances::free_balance(&4), 8);
|
||||
assert_eq!(Balances::reserved_balance(&4), 0); // slashed 2
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn award_and_claim_bounty_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
Balances::make_free_balance_be(&4, 10);
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 4));
|
||||
assert_ok!(Bounties::accept_curator(Origin::signed(4), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(4), 8); // inital 10 - 2 deposit
|
||||
|
||||
assert_noop!(Bounties::award_bounty(Origin::signed(1), 0, 3), Error::<Test>::RequireCurator);
|
||||
|
||||
assert_ok!(Bounties::award_bounty(Origin::signed(4), 0, 3));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 4,
|
||||
curator_deposit: 2,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::PendingPayout {
|
||||
curator: 4,
|
||||
beneficiary: 3,
|
||||
unlock_at: 5
|
||||
},
|
||||
});
|
||||
|
||||
assert_noop!(Bounties::claim_bounty(Origin::signed(1), 0), Error::<Test>::Premature);
|
||||
|
||||
System::set_block_number(5);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(5);
|
||||
|
||||
assert_ok!(Balances::transfer(Origin::signed(0), Bounties::bounty_account_id(0), 10));
|
||||
|
||||
assert_ok!(Bounties::claim_bounty(Origin::signed(1), 0));
|
||||
|
||||
assert_eq!(last_event(), RawEvent::BountyClaimed(0, 56, 3));
|
||||
|
||||
assert_eq!(Balances::free_balance(4), 14); // initial 10 + fee 4
|
||||
|
||||
assert_eq!(Balances::free_balance(3), 56);
|
||||
assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 0);
|
||||
|
||||
assert_eq!(Bounties::bounties(0), None);
|
||||
assert_eq!(Bounties::bounty_descriptions(0), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_handles_high_fee() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
Balances::make_free_balance_be(&4, 30);
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 49));
|
||||
assert_ok!(Bounties::accept_curator(Origin::signed(4), 0));
|
||||
|
||||
assert_ok!(Bounties::award_bounty(Origin::signed(4), 0, 3));
|
||||
|
||||
System::set_block_number(5);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(5);
|
||||
|
||||
// make fee > balance
|
||||
let _ = Balances::slash(&Bounties::bounty_account_id(0), 10);
|
||||
|
||||
assert_ok!(Bounties::claim_bounty(Origin::signed(1), 0));
|
||||
|
||||
assert_eq!(last_event(), RawEvent::BountyClaimed(0, 0, 3));
|
||||
|
||||
assert_eq!(Balances::free_balance(4), 70); // 30 + 50 - 10
|
||||
assert_eq!(Balances::free_balance(3), 0);
|
||||
assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 0);
|
||||
|
||||
assert_eq!(Bounties::bounties(0), None);
|
||||
assert_eq!(Bounties::bounty_descriptions(0), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_and_refund() {
|
||||
new_test_ext().execute_with(|| {
|
||||
|
||||
System::set_block_number(1);
|
||||
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_ok!(Balances::transfer(Origin::signed(0), Bounties::bounty_account_id(0), 10));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 0,
|
||||
curator_deposit: 0,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::Funded,
|
||||
});
|
||||
|
||||
assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 60);
|
||||
|
||||
assert_noop!(Bounties::close_bounty(Origin::signed(0), 0), BadOrigin);
|
||||
|
||||
assert_ok!(Bounties::close_bounty(Origin::root(), 0));
|
||||
|
||||
assert_eq!(Treasury::pot(), 85); // - 25 + 10
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn award_and_cancel() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 0, 10));
|
||||
assert_ok!(Bounties::accept_curator(Origin::signed(0), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(0), 95);
|
||||
assert_eq!(Balances::reserved_balance(0), 5);
|
||||
|
||||
assert_ok!(Bounties::award_bounty(Origin::signed(0), 0, 3));
|
||||
|
||||
// Cannot close bounty directly when payout is happening...
|
||||
assert_noop!(Bounties::close_bounty(Origin::root(), 0), Error::<Test>::PendingPayout);
|
||||
|
||||
// Instead unassign the curator to slash them and then close.
|
||||
assert_ok!(Bounties::unassign_curator(Origin::root(), 0));
|
||||
assert_ok!(Bounties::close_bounty(Origin::root(), 0));
|
||||
|
||||
assert_eq!(last_event(), RawEvent::BountyCanceled(0));
|
||||
|
||||
assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 0);
|
||||
|
||||
// Slashed.
|
||||
assert_eq!(Balances::free_balance(0), 95);
|
||||
assert_eq!(Balances::reserved_balance(0), 0);
|
||||
|
||||
assert_eq!(Bounties::bounties(0), None);
|
||||
assert_eq!(Bounties::bounty_descriptions(0), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expire_and_unassign() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 1, 10));
|
||||
assert_ok!(Bounties::accept_curator(Origin::signed(1), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 93);
|
||||
assert_eq!(Balances::reserved_balance(1), 5);
|
||||
|
||||
System::set_block_number(22);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(22);
|
||||
|
||||
assert_noop!(Bounties::unassign_curator(Origin::signed(0), 0), Error::<Test>::Premature);
|
||||
|
||||
System::set_block_number(23);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(23);
|
||||
|
||||
assert_ok!(Bounties::unassign_curator(Origin::signed(0), 0));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 10,
|
||||
curator_deposit: 0,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::Funded,
|
||||
});
|
||||
|
||||
assert_eq!(Balances::free_balance(1), 93);
|
||||
assert_eq!(Balances::reserved_balance(1), 0); // slashed
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_expiry() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
Balances::make_free_balance_be(&Treasury::account_id(), 101);
|
||||
Balances::make_free_balance_be(&4, 10);
|
||||
assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec()));
|
||||
|
||||
assert_ok!(Bounties::approve_bounty(Origin::root(), 0));
|
||||
|
||||
assert_noop!(Bounties::extend_bounty_expiry(Origin::signed(1), 0, Vec::new()), Error::<Test>::UnexpectedStatus);
|
||||
|
||||
System::set_block_number(2);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(2);
|
||||
|
||||
assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 10));
|
||||
assert_ok!(Bounties::accept_curator(Origin::signed(4), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(4), 5);
|
||||
assert_eq!(Balances::reserved_balance(4), 5);
|
||||
|
||||
System::set_block_number(10);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(10);
|
||||
|
||||
assert_noop!(Bounties::extend_bounty_expiry(Origin::signed(0), 0, Vec::new()), Error::<Test>::RequireCurator);
|
||||
assert_ok!(Bounties::extend_bounty_expiry(Origin::signed(4), 0, Vec::new()));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 10,
|
||||
curator_deposit: 5,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::Active { curator: 4, update_due: 30 },
|
||||
});
|
||||
|
||||
assert_ok!(Bounties::extend_bounty_expiry(Origin::signed(4), 0, Vec::new()));
|
||||
|
||||
assert_eq!(Bounties::bounties(0).unwrap(), Bounty {
|
||||
proposer: 0,
|
||||
fee: 10,
|
||||
curator_deposit: 5,
|
||||
value: 50,
|
||||
bond: 85,
|
||||
status: BountyStatus::Active { curator: 4, update_due: 30 }, // still the same
|
||||
});
|
||||
|
||||
System::set_block_number(25);
|
||||
<Treasury as OnInitialize<u64>>::on_initialize(25);
|
||||
|
||||
assert_noop!(Bounties::unassign_curator(Origin::signed(0), 0), Error::<Test>::Premature);
|
||||
assert_ok!(Bounties::unassign_curator(Origin::signed(4), 0));
|
||||
|
||||
assert_eq!(Balances::free_balance(4), 10); // not slashed
|
||||
assert_eq!(Balances::reserved_balance(4), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_funding_works() {
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
let initial_funding = 100;
|
||||
pallet_balances::GenesisConfig::<Test>{
|
||||
// Total issuance will be 200 with treasury account initialized with 100.
|
||||
balances: vec![(0, 100), (Treasury::account_id(), initial_funding)],
|
||||
}.assimilate_storage(&mut t).unwrap();
|
||||
pallet_treasury::GenesisConfig::default().assimilate_storage::<Test, _>(&mut t).unwrap();
|
||||
let mut t: sp_io::TestExternalities = t.into();
|
||||
|
||||
t.execute_with(|| {
|
||||
assert_eq!(Balances::free_balance(Treasury::account_id()), initial_funding);
|
||||
assert_eq!(Treasury::pot(), initial_funding - Balances::minimum_balance());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 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.
|
||||
|
||||
//! Autogenerated weights for pallet_bounties
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0
|
||||
//! DATE: 2020-12-16, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
|
||||
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/substrate
|
||||
// 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
|
||||
|
||||
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for pallet_bounties.
|
||||
pub trait WeightInfo {
|
||||
fn propose_bounty(d: u32, ) -> Weight;
|
||||
fn approve_bounty() -> Weight;
|
||||
fn propose_curator() -> Weight;
|
||||
fn unassign_curator() -> Weight;
|
||||
fn accept_curator() -> Weight;
|
||||
fn award_bounty() -> Weight;
|
||||
fn claim_bounty() -> Weight;
|
||||
fn close_bounty_proposed() -> Weight;
|
||||
fn close_bounty_active() -> Weight;
|
||||
fn extend_bounty_expiry() -> Weight;
|
||||
fn spend_funds(b: u32, ) -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for pallet_bounties using the Substrate node and recommended hardware.
|
||||
pub struct SubstrateWeight<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
fn propose_bounty(d: u32, ) -> Weight {
|
||||
(64_778_000 as Weight)
|
||||
// Standard Error: 0
|
||||
.saturating_add((1_000 as Weight).saturating_mul(d as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
fn approve_bounty() -> Weight {
|
||||
(18_293_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as Weight))
|
||||
}
|
||||
fn propose_curator() -> Weight {
|
||||
(14_248_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn unassign_curator() -> Weight {
|
||||
(52_100_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as Weight))
|
||||
}
|
||||
fn accept_curator() -> Weight {
|
||||
(52_564_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(2 as Weight))
|
||||
}
|
||||
fn award_bounty() -> Weight {
|
||||
(37_426_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn claim_bounty() -> Weight {
|
||||
(176_077_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn close_bounty_proposed() -> Weight {
|
||||
(51_162_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn close_bounty_active() -> Weight {
|
||||
(116_907_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
fn extend_bounty_expiry() -> Weight {
|
||||
(36_419_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn spend_funds(b: u32, ) -> Weight {
|
||||
(7_562_000 as Weight)
|
||||
// Standard Error: 16_000
|
||||
.saturating_add((77_328_000 as Weight).saturating_mul(b as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(b as Weight)))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(b as Weight)))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
fn propose_bounty(d: u32, ) -> Weight {
|
||||
(64_778_000 as Weight)
|
||||
// Standard Error: 0
|
||||
.saturating_add((1_000 as Weight).saturating_mul(d as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
fn approve_bounty() -> Weight {
|
||||
(18_293_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
|
||||
}
|
||||
fn propose_curator() -> Weight {
|
||||
(14_248_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn unassign_curator() -> Weight {
|
||||
(52_100_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
|
||||
}
|
||||
fn accept_curator() -> Weight {
|
||||
(52_564_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
|
||||
}
|
||||
fn award_bounty() -> Weight {
|
||||
(37_426_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn claim_bounty() -> Weight {
|
||||
(176_077_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn close_bounty_proposed() -> Weight {
|
||||
(51_162_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn close_bounty_active() -> Weight {
|
||||
(116_907_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
fn extend_bounty_expiry() -> Weight {
|
||||
(36_419_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn spend_funds(b: u32, ) -> Weight {
|
||||
(7_562_000 as Weight)
|
||||
// Standard Error: 16_000
|
||||
.saturating_add((77_328_000 as Weight).saturating_mul(b as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(b as Weight)))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(b as Weight)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user