From a0a6809b1b50eaf4d1c76609da15dd255c9d6bfc Mon Sep 17 00:00:00 2001 From: Xiliang Chen Date: Fri, 18 Sep 2020 22:19:22 +1200 Subject: [PATCH] Bounties (#5715) * add some compact annotation * implement bounties for treasury * fix test build * remove some duplicated code * fix build * add tests * fix build * fix tests * rename * merge deposit byte fee * add comments * refactor storage * support sub bounty * emit BountyBecameActive when sub bounty is created * able to contribute bounty * allow curator to cancel bounty * remove bounty contribution * implement bounty expiry * Able to extend bounty * fix build and update tests * create sub bounty test * add more tests * add benchmarks for bounties * fix build * line width * fix benchmarking test * update trait * fix typo * Update lib.rs Missing documentation on Bounties added on this change. Please check the definitions of `propose_bounty` and `create_bounty`. * update docs * add MaximumSubBountyDepth * put BountyValueMinimum into storage * rework bount depth * split on_initialize benchmarks * remove components from constant functions * Update weight integration into treasury * Update reject proposal read/writes * fix weight calculation * Ignore weights with 0 factor * Remove 0 multipliers * add some docs * allow unused for generated code * line width * allow RejectOrigin to cancel a pending payout bounty * require BountyValueMinimum > ED * make BountyValueMinimum configurable by chain spec * remove sub-bounty features * update curator * accept curator * unassign and cancel * fix tests * new tests * Update lib.rs - Include on `Assign_curator`, `accept_curator` and `unassign_curator` on Bounties Protocol Section - Include curator fee and curator deposit definitions on Terminology - Update intro. * fix test * update extend_bounty_expiry * fix benchmarking * add new benchmarking code * add docs * fix tests * Update benchmarking.rs * Make BountyValueMinimum a trait config instead of stroage value * fix runtime build * Update weights * Update default_weights.rs * update weights * update * update comments * unreserve curator fee * update tests * update benchmarks * fix curator deposit handling * trigger CI * fix benchmarking * use append instead of mutate push * additional noop tests * improve fee hanlding. update event docs * RejectOrigin to unassign * update bounty cancel logic * use Zero::zero() over 0.into() * fix tests * fix benchmarks * proposed fixes to bounties * fix tests * fix benchmarks * update weightinfo * use closure * fix compile * update weights Co-authored-by: RRTTI Co-authored-by: Shawn Tabrizi --- bin/node/runtime/src/lib.rs | 20 +- bin/node/runtime/src/weights/mod.rs | 1 + .../runtime/src/weights/pallet_treasury.rs | 140 ++++ frame/treasury/Cargo.toml | 1 + frame/treasury/src/benchmarking.rs | 185 ++++- frame/treasury/src/default_weights.rs | 139 ++++ frame/treasury/src/lib.rs | 725 ++++++++++++++++-- frame/treasury/src/tests.rs | 595 ++++++++++++-- 8 files changed, 1668 insertions(+), 138 deletions(-) create mode 100644 bin/node/runtime/src/weights/pallet_treasury.rs create mode 100644 frame/treasury/src/default_weights.rs diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 468605ddb7a09..03fe279366d50 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -602,8 +602,14 @@ parameter_types! { pub const TipCountdown: BlockNumber = 1 * DAYS; pub const TipFindersFee: Percent = Percent::from_percent(20); pub const TipReportDepositBase: Balance = 1 * DOLLARS; - pub const TipReportDepositPerByte: Balance = 1 * CENTS; + pub const DataDepositPerByte: Balance = 1 * CENTS; + pub const BountyDepositBase: Balance = 1 * DOLLARS; + pub const BountyDepositPayoutDelay: BlockNumber = 1 * DAYS; pub const TreasuryModuleId: ModuleId = ModuleId(*b"py/trsry"); + pub const BountyUpdatePeriod: BlockNumber = 14 * DAYS; + pub const MaximumReasonLength: u32 = 16384; + pub const BountyCuratorDeposit: Permill = Permill::from_percent(50); + pub const BountyValueMinimum: Balance = 5 * DOLLARS; } impl pallet_treasury::Trait for Runtime { @@ -623,15 +629,21 @@ impl pallet_treasury::Trait for Runtime { type TipCountdown = TipCountdown; type TipFindersFee = TipFindersFee; type TipReportDepositBase = TipReportDepositBase; - type TipReportDepositPerByte = TipReportDepositPerByte; + type DataDepositPerByte = DataDepositPerByte; type Event = Event; - type ProposalRejection = (); + type OnSlash = (); type ProposalBond = ProposalBond; type ProposalBondMinimum = ProposalBondMinimum; type SpendPeriod = SpendPeriod; type Burn = Burn; + type BountyDepositBase = BountyDepositBase; + type BountyDepositPayoutDelay = BountyDepositPayoutDelay; + type BountyUpdatePeriod = BountyUpdatePeriod; + type BountyCuratorDeposit = BountyCuratorDeposit; + type BountyValueMinimum = BountyValueMinimum; + type MaximumReasonLength = MaximumReasonLength; type BurnDestination = (); - type WeightInfo = (); + type WeightInfo = weights::pallet_treasury::WeightInfo; } parameter_types! { diff --git a/bin/node/runtime/src/weights/mod.rs b/bin/node/runtime/src/weights/mod.rs index a07aa2e5bc896..199e66888e65d 100644 --- a/bin/node/runtime/src/weights/mod.rs +++ b/bin/node/runtime/src/weights/mod.rs @@ -17,6 +17,7 @@ pub mod frame_system; pub mod pallet_balances; +pub mod pallet_treasury; pub mod pallet_collective; pub mod pallet_democracy; pub mod pallet_identity; diff --git a/bin/node/runtime/src/weights/pallet_treasury.rs b/bin/node/runtime/src/weights/pallet_treasury.rs new file mode 100644 index 0000000000000..fe1a3166919f0 --- /dev/null +++ b/bin/node/runtime/src/weights/pallet_treasury.rs @@ -0,0 +1,140 @@ +// 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. + +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0-rc6 + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::weights::{Weight, constants::RocksDbWeight as DbWeight}; + +pub struct WeightInfo; +impl pallet_treasury::WeightInfo for WeightInfo { + fn propose_spend() -> Weight { + (79604000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn reject_proposal() -> Weight { + (61001000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn approve_proposal() -> Weight { + (17835000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn report_awesome(r: u32, ) -> Weight { + (101602000 as Weight) + .saturating_add((2000 as Weight).saturating_mul(r as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + // WARNING! Some components were not used: ["r"] + fn retract_tip() -> Weight { + (82970000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn tip_new(r: u32, t: u32, ) -> Weight { + (63995000 as Weight) + .saturating_add((2000 as Weight).saturating_mul(r as Weight)) + .saturating_add((153000 as Weight).saturating_mul(t as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn tip(t: u32, ) -> Weight { + (46765000 as Weight) + .saturating_add((711000 as Weight).saturating_mul(t as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn close_tip(t: u32, ) -> Weight { + (160874000 as Weight) + .saturating_add((379000 as Weight).saturating_mul(t as Weight)) + .saturating_add(DbWeight::get().reads(3 as Weight)) + .saturating_add(DbWeight::get().writes(3 as Weight)) + } + fn propose_bounty(d: u32, ) -> Weight { + (86198000 as Weight) + .saturating_add((1000 as Weight).saturating_mul(d as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(4 as Weight)) + } + fn approve_bounty() -> Weight { + (23063000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn propose_curator() -> Weight { + (18890000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn unassign_curator() -> Weight { + (66768000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn accept_curator() -> Weight { + (69131000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn award_bounty() -> Weight { + (48184000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn claim_bounty() -> Weight { + (243104000 as Weight) + .saturating_add(DbWeight::get().reads(4 as Weight)) + .saturating_add(DbWeight::get().writes(5 as Weight)) + } + fn close_bounty_proposed() -> Weight { + (65917000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(3 as Weight)) + } + fn close_bounty_active() -> Weight { + (157232000 as Weight) + .saturating_add(DbWeight::get().reads(3 as Weight)) + .saturating_add(DbWeight::get().writes(4 as Weight)) + } + fn extend_bounty_expiry() -> Weight { + (46216000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn on_initialize_proposals(p: u32, ) -> Weight { + (119765000 as Weight) + .saturating_add((108368000 as Weight).saturating_mul(p as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().reads((3 as Weight).saturating_mul(p as Weight))) + .saturating_add(DbWeight::get().writes(2 as Weight)) + .saturating_add(DbWeight::get().writes((3 as Weight).saturating_mul(p as Weight))) + } + fn on_initialize_bounties(b: u32, ) -> Weight { + (112536000 as Weight) + .saturating_add((107132000 as Weight).saturating_mul(b as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().reads((3 as Weight).saturating_mul(b as Weight))) + .saturating_add(DbWeight::get().writes(2 as Weight)) + .saturating_add(DbWeight::get().writes((3 as Weight).saturating_mul(b as Weight))) + } +} diff --git a/frame/treasury/Cargo.toml b/frame/treasury/Cargo.toml index b6ef83b32eda8..674417cd73e2c 100644 --- a/frame/treasury/Cargo.toml +++ b/frame/treasury/Cargo.toml @@ -41,4 +41,5 @@ std = [ runtime-benchmarks = [ "frame-benchmarking", "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", ] diff --git a/frame/treasury/src/benchmarking.rs b/frame/treasury/src/benchmarking.rs index a972dc80bd428..5224c1446f452 100644 --- a/frame/treasury/src/benchmarking.rs +++ b/frame/treasury/src/benchmarking.rs @@ -47,7 +47,7 @@ fn setup_proposal, I: Instance>(u: u32) -> ( fn setup_awesome, I: Instance>(length: u32) -> (T::AccountId, Vec, T::AccountId) { let caller = whitelisted_caller(); let value = T::TipReportDepositBase::get() - + T::TipReportDepositPerByte::get() * length.into() + + T::DataDepositPerByte::get() * length.into() + T::Currency::minimum_balance(); let _ = T::Currency::make_free_balance_be(&caller, value); let reason = vec![0; length as usize]; @@ -109,6 +109,58 @@ fn create_approved_proposals, I: Instance>(n: u32) -> Result<(), &'s Ok(()) } +// Create bounties that are approved for use in `on_initialize`. +fn create_approved_bounties, I: Instance>(n: u32) -> Result<(), &'static str> { + for i in 0 .. n { + let (caller, _curator, _fee, value, reason) = setup_bounty::(i, MAX_BYTES); + Treasury::::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?; + let bounty_id = BountyCount::::get() - 1; + Treasury::::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, I: Instance>(u: u32, d: u32) -> ( + T::AccountId, + T::AccountId, + BalanceOf, + BalanceOf, + Vec, +) { + let caller = account("caller", u, SEED); + let value: BalanceOf = T::Currency::minimum_balance().saturating_mul(100.into()); + let fee = T::Currency::minimum_balance().saturating_mul(2.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 / 2.into()); + let reason = vec![0; d as usize]; + (caller, curator, fee, value, reason) +} + +fn create_bounty, I: Instance>() -> Result<( + ::Source, + BountyIndex, +), &'static str> { + let (caller, curator, fee, value, reason) = setup_bounty::(0, MAX_BYTES); + let curator_lookup = T::Lookup::unlookup(curator.clone()); + Treasury::::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?; + let bounty_id = BountyCount::::get() - 1; + Treasury::::approve_bounty(RawOrigin::Root.into(), bounty_id)?; + Treasury::::on_initialize(T::BlockNumber::zero()); + Treasury::::propose_curator(RawOrigin::Root.into(), bounty_id, curator_lookup.clone(), fee)?; + Treasury::::accept_curator(RawOrigin::Signed(curator).into(), bounty_id)?; + Ok((curator_lookup, bounty_id)) +} + +fn setup_pod_account, I: Instance>() { + let pot_account = Treasury::::account_id(); + let value = T::Currency::minimum_balance().saturating_mul(1_000_000_000.into()); + let _ = T::Currency::make_free_balance_be(&pot_account, value); +} + const MAX_BYTES: u32 = 16384; const MAX_TIPPERS: u32 = 100; @@ -116,16 +168,14 @@ benchmarks_instance! { _ { } propose_spend { - let u in 0 .. 1000; - let (caller, value, beneficiary_lookup) = setup_proposal::(u); + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); // Whitelist caller account from further DB operations. let caller_key = frame_system::Account::::hashed_key_for(&caller); frame_benchmarking::benchmarking::add_to_whitelist(caller_key.into()); }: _(RawOrigin::Signed(caller), value, beneficiary_lookup) reject_proposal { - let u in 0 .. 1000; - let (caller, value, beneficiary_lookup) = setup_proposal::(u); + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); Treasury::::propose_spend( RawOrigin::Signed(caller).into(), value, @@ -135,8 +185,7 @@ benchmarks_instance! { }: _(RawOrigin::Root, proposal_id) approve_proposal { - let u in 0 .. 1000; - let (caller, value, beneficiary_lookup) = setup_proposal::(u); + let (caller, value, beneficiary_lookup) = setup_proposal::(SEED); Treasury::::propose_spend( RawOrigin::Signed(caller).into(), value, @@ -202,9 +251,7 @@ benchmarks_instance! { let t in 1 .. MAX_TIPPERS; // Make sure pot is funded - let pot_account = Treasury::::account_id(); - let value = T::Currency::minimum_balance().saturating_mul(1_000_000_000.into()); - let _ = T::Currency::make_free_balance_be(&pot_account, value); + setup_pod_account::(); // Set up a new tip proposal let (member, reason, beneficiary, value) = setup_tip::(0, t)?; @@ -228,15 +275,112 @@ benchmarks_instance! { frame_benchmarking::benchmarking::add_to_whitelist(caller_key.into()); }: _(RawOrigin::Signed(caller), hash) - on_initialize { + propose_bounty { + let d in 0 .. MAX_BYTES; + + let (caller, curator, fee, value, description) = setup_bounty::(0, d); + }: _(RawOrigin::Signed(caller), value, description) + + approve_bounty { + let (caller, curator, fee, value, reason) = setup_bounty::(0, MAX_BYTES); + Treasury::::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?; + let bounty_id = BountyCount::::get() - 1; + }: _(RawOrigin::Root, bounty_id) + + propose_curator { + setup_pod_account::(); + let (caller, curator, fee, value, reason) = setup_bounty::(0, MAX_BYTES); + let curator_lookup = T::Lookup::unlookup(curator.clone()); + Treasury::::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?; + let bounty_id = BountyCount::::get() - 1; + Treasury::::approve_bounty(RawOrigin::Root.into(), bounty_id)?; + Treasury::::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::(); + let (curator_lookup, bounty_id) = create_bounty::()?; + Treasury::::on_initialize(T::BlockNumber::zero()); + let bounty_id = BountyCount::::get() - 1; + frame_system::Module::::set_block_number(T::BountyUpdatePeriod::get() + 1.into()); + let caller = whitelisted_caller(); + }: _(RawOrigin::Signed(caller), bounty_id) + + accept_curator { + setup_pod_account::(); + let (caller, curator, fee, value, reason) = setup_bounty::(0, MAX_BYTES); + let curator_lookup = T::Lookup::unlookup(curator.clone()); + Treasury::::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?; + let bounty_id = BountyCount::::get() - 1; + Treasury::::approve_bounty(RawOrigin::Root.into(), bounty_id)?; + Treasury::::on_initialize(T::BlockNumber::zero()); + Treasury::::propose_curator(RawOrigin::Root.into(), bounty_id, curator_lookup, fee)?; + }: _(RawOrigin::Signed(curator), bounty_id) + + award_bounty { + setup_pod_account::(); + let (curator_lookup, bounty_id) = create_bounty::()?; + Treasury::::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::(); + let (curator_lookup, bounty_id) = create_bounty::()?; + Treasury::::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)); + Treasury::::award_bounty(RawOrigin::Signed(curator.clone()).into(), bounty_id, beneficiary)?; + + frame_system::Module::::set_block_number(T::BountyDepositPayoutDelay::get()); + + }: _(RawOrigin::Signed(curator), bounty_id) + + close_bounty_proposed { + setup_pod_account::(); + let (caller, curator, fee, value, reason) = setup_bounty::(0, 0); + Treasury::::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::(); + let (curator_lookup, bounty_id) = create_bounty::()?; + Treasury::::on_initialize(T::BlockNumber::zero()); + let bounty_id = BountyCount::::get() - 1; + }: close_bounty(RawOrigin::Root, bounty_id) + + extend_bounty_expiry { + setup_pod_account::(); + let (curator_lookup, bounty_id) = create_bounty::()?; + Treasury::::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()) + + on_initialize_proposals { let p in 0 .. 100; - let pot_account = Treasury::::account_id(); - let value = T::Currency::minimum_balance().saturating_mul(1_000_000_000.into()); - let _ = T::Currency::make_free_balance_be(&pot_account, value); + setup_pod_account::(); create_approved_proposals::(p)?; }: { Treasury::::on_initialize(T::BlockNumber::zero()); } + + on_initialize_bounties { + let b in 0 .. 100; + setup_pod_account::(); + create_approved_bounties::(b)?; + }: { + Treasury::::on_initialize(T::BlockNumber::zero()); + } } #[cfg(test)] @@ -256,7 +400,18 @@ mod tests { assert_ok!(test_benchmark_tip_new::()); assert_ok!(test_benchmark_tip::()); assert_ok!(test_benchmark_close_tip::()); - assert_ok!(test_benchmark_on_initialize::()); + assert_ok!(test_benchmark_propose_bounty::()); + assert_ok!(test_benchmark_approve_bounty::()); + assert_ok!(test_benchmark_propose_curator::()); + assert_ok!(test_benchmark_unassign_curator::()); + assert_ok!(test_benchmark_accept_curator::()); + assert_ok!(test_benchmark_award_bounty::()); + assert_ok!(test_benchmark_claim_bounty::()); + assert_ok!(test_benchmark_close_bounty_proposed::()); + assert_ok!(test_benchmark_close_bounty_active::()); + assert_ok!(test_benchmark_extend_bounty_expiry::()); + assert_ok!(test_benchmark_on_initialize_proposals::()); + assert_ok!(test_benchmark_on_initialize_bounties::()); }); } } diff --git a/frame/treasury/src/default_weights.rs b/frame/treasury/src/default_weights.rs new file mode 100644 index 0000000000000..faf6c1a04573d --- /dev/null +++ b/frame/treasury/src/default_weights.rs @@ -0,0 +1,139 @@ +// 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. + +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0-rc6 + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::weights::{Weight, constants::RocksDbWeight as DbWeight}; + +impl crate::WeightInfo for () { + fn propose_spend() -> Weight { + (79604000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn reject_proposal() -> Weight { + (61001000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn approve_proposal() -> Weight { + (17835000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn report_awesome(r: u32, ) -> Weight { + (101602000 as Weight) + .saturating_add((2000 as Weight).saturating_mul(r as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + // WARNING! Some components were not used: ["r"] + fn retract_tip() -> Weight { + (82970000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn tip_new(r: u32, t: u32, ) -> Weight { + (63995000 as Weight) + .saturating_add((2000 as Weight).saturating_mul(r as Weight)) + .saturating_add((153000 as Weight).saturating_mul(t as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn tip(t: u32, ) -> Weight { + (46765000 as Weight) + .saturating_add((711000 as Weight).saturating_mul(t as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn close_tip(t: u32, ) -> Weight { + (160874000 as Weight) + .saturating_add((379000 as Weight).saturating_mul(t as Weight)) + .saturating_add(DbWeight::get().reads(3 as Weight)) + .saturating_add(DbWeight::get().writes(3 as Weight)) + } + fn propose_bounty(d: u32, ) -> Weight { + (86198000 as Weight) + .saturating_add((1000 as Weight).saturating_mul(d as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(4 as Weight)) + } + fn approve_bounty() -> Weight { + (23063000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn propose_curator() -> Weight { + (18890000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn unassign_curator() -> Weight { + (66768000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn accept_curator() -> Weight { + (69131000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(2 as Weight)) + } + fn award_bounty() -> Weight { + (48184000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn claim_bounty() -> Weight { + (243104000 as Weight) + .saturating_add(DbWeight::get().reads(4 as Weight)) + .saturating_add(DbWeight::get().writes(5 as Weight)) + } + fn close_bounty_proposed() -> Weight { + (65917000 as Weight) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(3 as Weight)) + } + fn close_bounty_active() -> Weight { + (157232000 as Weight) + .saturating_add(DbWeight::get().reads(3 as Weight)) + .saturating_add(DbWeight::get().writes(4 as Weight)) + } + fn extend_bounty_expiry() -> Weight { + (46216000 as Weight) + .saturating_add(DbWeight::get().reads(1 as Weight)) + .saturating_add(DbWeight::get().writes(1 as Weight)) + } + fn on_initialize_proposals(p: u32, ) -> Weight { + (119765000 as Weight) + .saturating_add((108368000 as Weight).saturating_mul(p as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().reads((3 as Weight).saturating_mul(p as Weight))) + .saturating_add(DbWeight::get().writes(2 as Weight)) + .saturating_add(DbWeight::get().writes((3 as Weight).saturating_mul(p as Weight))) + } + fn on_initialize_bounties(b: u32, ) -> Weight { + (112536000 as Weight) + .saturating_add((107132000 as Weight).saturating_mul(b as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().reads((3 as Weight).saturating_mul(b as Weight))) + .saturating_add(DbWeight::get().writes(2 as Weight)) + .saturating_add(DbWeight::get().writes((3 as Weight).saturating_mul(b as Weight))) + } +} diff --git a/frame/treasury/src/lib.rs b/frame/treasury/src/lib.rs index cedc46eb8c0dd..4d76f2f532151 100644 --- a/frame/treasury/src/lib.rs +++ b/frame/treasury/src/lib.rs @@ -44,6 +44,23 @@ //! countdown period, the median of all declared tips is paid to the reported beneficiary, along //! with any finders fee, in case of a public (and bonded) original report. //! +//! ### Bounty +//! +//! 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 //! //! - **Proposal:** A suggestion to allocate funds from the pot to a beneficiary. @@ -64,6 +81,22 @@ //! - **Finders Fee:** Some proportion of the tip amount that is paid to the reporter of the tip, //! rather than the main beneficiary. //! +//! 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 @@ -82,6 +115,19 @@ //! - `tip` - Declare or redeclare an amount to tip for a particular reason. //! - `close_tip` - Close and pay out a tip. //! +//! 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. +//! +//! //! ## GenesisConfig //! //! The Treasury module depends on the [`GenesisConfig`](./struct.GenesisConfig.html). @@ -93,12 +139,13 @@ use serde::{Serialize, Deserialize}; use sp_std::prelude::*; use frame_support::{decl_module, decl_storage, decl_event, ensure, print, decl_error, Parameter}; use frame_support::traits::{ - Currency, Get, Imbalance, OnUnbalanced, ExistenceRequirement::KeepAlive, + Currency, Get, Imbalance, OnUnbalanced, ExistenceRequirement::{KeepAlive, AllowDeath}, ReservableCurrency, WithdrawReason }; -use sp_runtime::{Permill, ModuleId, Percent, RuntimeDebug, traits::{ +use sp_runtime::{Permill, ModuleId, Percent, RuntimeDebug, DispatchResult, traits::{ Zero, StaticLookup, AccountIdConversion, Saturating, Hash, BadOrigin }}; +use frame_support::dispatch::DispatchResultWithPostInfo; use frame_support::weights::{Weight, DispatchClass}; use frame_support::traits::{Contains, ContainsLengthBound, EnsureOrigin}; use codec::{Encode, Decode}; @@ -106,6 +153,7 @@ use frame_system::{self as system, ensure_signed}; mod tests; mod benchmarking; +mod default_weights; type BalanceOf = <>::Currency as Currency<::AccountId>>::Balance; @@ -115,27 +163,26 @@ type NegativeImbalanceOf = <>::Currency as Currency<::AccountId>>::NegativeImbalance; pub trait WeightInfo { - fn propose_spend(u: u32, ) -> Weight; - fn reject_proposal(u: u32, ) -> Weight; - fn approve_proposal(u: u32, ) -> Weight; + fn propose_spend() -> Weight; + fn reject_proposal() -> Weight; + fn approve_proposal() -> Weight; fn report_awesome(r: u32, ) -> Weight; - fn retract_tip(r: u32, ) -> Weight; + fn retract_tip() -> Weight; fn tip_new(r: u32, t: u32, ) -> Weight; fn tip(t: u32, ) -> Weight; fn close_tip(t: u32, ) -> Weight; - fn on_initialize(p: u32, ) -> Weight; -} - -impl WeightInfo for () { - fn propose_spend(_u: u32, ) -> Weight { 1_000_000_000 } - fn reject_proposal(_u: u32, ) -> Weight { 1_000_000_000 } - fn approve_proposal(_u: u32, ) -> Weight { 1_000_000_000 } - fn report_awesome(_r: u32, ) -> Weight { 1_000_000_000 } - fn retract_tip(_r: u32, ) -> Weight { 1_000_000_000 } - fn tip_new(_r: u32, _t: u32, ) -> Weight { 1_000_000_000 } - fn tip(_t: u32, ) -> Weight { 1_000_000_000 } - fn close_tip(_t: u32, ) -> Weight { 1_000_000_000 } - fn on_initialize(_p: u32, ) -> Weight { 1_000_000_000 } + fn propose_bounty(r: 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 on_initialize_proposals(p: u32, ) -> Weight; + fn on_initialize_bounties(b: u32, ) -> Weight; } pub trait Trait: frame_system::Trait { @@ -165,14 +212,14 @@ pub trait Trait: frame_system::Trait { /// The amount held on deposit for placing a tip report. type TipReportDepositBase: Get>; - /// The amount held on deposit per byte within the tip report reason. - type TipReportDepositPerByte: Get>; + /// The amount held on deposit per byte within the tip report reason or bounty description. + type DataDepositPerByte: Get>; /// The overarching event type. type Event: From> + Into<::Event>; - /// Handler for the unbalanced decrease when slashing for a rejected proposal. - type ProposalRejection: OnUnbalanced>; + /// Handler for the unbalanced decrease when slashing for a rejected proposal or bounty. + type OnSlash: OnUnbalanced>; /// Fraction of a proposal's value that should be bonded in order to place the proposal. /// An accepted proposal gets these back. A rejected proposal does not. @@ -187,6 +234,24 @@ pub trait Trait: frame_system::Trait { /// Percentage of spare funds (if any) that are burnt per spend period. type Burn: Get; + /// The amount held on deposit for placing a bounty proposal. + type BountyDepositBase: Get>; + + /// The delay period for which a bounty beneficiary need to wait before claim the payout. + type BountyDepositPayoutDelay: Get; + + /// Bounty duration in blocks. + type BountyUpdatePeriod: Get; + + /// Percentage of the curator fee that will be reserved upfront as deposit for bounty curator. + type BountyCuratorDeposit: Get; + + /// Minimum value for a bounty. + type BountyValueMinimum: Get>; + + /// Maximum acceptable reason length. + type MaximumReasonLength: Get; + /// Handler for the unbalanced decrease when treasury funds are burned. type BurnDestination: OnUnbalanced>; @@ -238,6 +303,58 @@ pub struct OpenTip< finders_fee: bool, } +/// 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 { + /// 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, +} + +/// The status of a bounty proposal. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)] +pub enum BountyStatus { + /// 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, + }, +} + decl_storage! { trait Store for Module, I: Instance=DefaultInstance> as Treasury { /// Number of proposals that have been made. @@ -261,6 +378,20 @@ decl_storage! { /// Simple preimage lookup from the reason's hash to the original data. Again, has an /// insecure enumerable hash since the key is guaranteed to be the result of a secure hash. pub Reasons get(fn reasons): map hasher(identity) T::Hash => Option>; + + /// 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, T::BlockNumber>>; + + /// The description of each bounty. + pub BountyDescriptions get(fn bounty_descriptions): map hasher(twox_64_concat) BountyIndex => Option>; + + /// Bounty indices that have been approved but not yet funded. + pub BountyApprovals get(fn bounty_approvals): Vec; } add_extra_genesis { build(|_config| { @@ -303,6 +434,20 @@ decl_event!( TipClosed(Hash, AccountId, Balance), /// A tip suggestion has been retracted. \[tip_hash\] TipRetracted(Hash), + /// 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), } ); @@ -311,8 +456,8 @@ decl_error! { pub enum Error for Module, I: Instance> { /// Proposer's balance is too low. InsufficientProposersBalance, - /// No proposal at that index. - InvalidProposalIndex, + /// No proposal or bounty at that index. + InvalidIndex, /// The reason given is just too big. ReasonTooBig, /// The tip was already found/started. @@ -325,6 +470,17 @@ decl_error! { StillOpen, /// The tip cannot be claimed/closed because it's still in the countdown period. Premature, + /// 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, } } @@ -355,12 +511,26 @@ decl_module! { /// The amount held on deposit for placing a tip report. const TipReportDepositBase: BalanceOf = T::TipReportDepositBase::get(); - /// The amount held on deposit per byte within the tip report reason. - const TipReportDepositPerByte: BalanceOf = T::TipReportDepositPerByte::get(); + /// The amount held on deposit per byte within the tip report reason or bounty description. + const DataDepositPerByte: BalanceOf = T::DataDepositPerByte::get(); /// The treasury's module id, used for deriving its sovereign account ID. const ModuleId: ModuleId = T::ModuleId::get(); + /// The amount held on deposit for placing a bounty proposal. + const BountyDepositBase: BalanceOf = 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(); + + const BountyValueMinimum: BalanceOf = T::BountyValueMinimum::get(); + + /// Maximum acceptable reason length. + const MaximumReasonLength: u32 = T::MaximumReasonLength::get(); + type Error = Error; fn deposit_event() = default; @@ -374,7 +544,7 @@ decl_module! { /// - DbReads: `ProposalCount`, `origin account` /// - DbWrites: `ProposalCount`, `Proposals`, `origin account` /// # - #[weight = 120_000_000 + T::DbWeight::get().reads_writes(1, 2)] + #[weight = T::WeightInfo::propose_spend()] fn propose_spend( origin, #[compact] value: BalanceOf, @@ -403,14 +573,14 @@ decl_module! { /// - DbReads: `Proposals`, `rejected proposer account` /// - DbWrites: `Proposals`, `rejected proposer account` /// # - #[weight = (130_000_000 + T::DbWeight::get().reads_writes(2, 2), DispatchClass::Operational)] + #[weight = (T::WeightInfo::reject_proposal(), DispatchClass::Operational)] fn reject_proposal(origin, #[compact] proposal_id: ProposalIndex) { T::RejectOrigin::ensure_origin(origin)?; - let proposal = >::take(&proposal_id).ok_or(Error::::InvalidProposalIndex)?; + let proposal = >::take(&proposal_id).ok_or(Error::::InvalidIndex)?; let value = proposal.bond; let imbalance = T::Currency::slash_reserved(&proposal.proposer, value).0; - T::ProposalRejection::on_unbalanced(imbalance); + T::OnSlash::on_unbalanced(imbalance); Self::deposit_event(Event::::Rejected(proposal_id, value)); } @@ -425,12 +595,12 @@ decl_module! { /// - DbReads: `Proposals`, `Approvals` /// - DbWrite: `Approvals` /// # - #[weight = (34_000_000 + T::DbWeight::get().reads_writes(2, 1), DispatchClass::Operational)] + #[weight = (T::WeightInfo::approve_proposal(), DispatchClass::Operational)] fn approve_proposal(origin, #[compact] proposal_id: ProposalIndex) { T::ApproveOrigin::ensure_origin(origin)?; - ensure!(>::contains_key(proposal_id), Error::::InvalidProposalIndex); - >::mutate(|v| v.push(proposal_id)); + ensure!(>::contains_key(proposal_id), Error::::InvalidIndex); + Approvals::::append(proposal_id); } /// Report something `reason` that deserves a tip and claim any eventual the finder's fee. @@ -438,7 +608,7 @@ decl_module! { /// The dispatch origin for this call must be _Signed_. /// /// Payment: `TipReportDepositBase` will be reserved from the origin account, as well as - /// `TipReportDepositPerByte` for each byte in `reason`. + /// `DataDepositPerByte` for each byte in `reason`. /// /// - `reason`: The reason for, or the thing that deserves, the tip; generally this will be /// a UTF-8-encoded URL. @@ -449,15 +619,14 @@ decl_module! { /// # /// - Complexity: `O(R)` where `R` length of `reason`. /// - encoding and hashing of 'reason' - /// - DbReads: `Reasons`, `Tips`, `who account data` - /// - DbWrites: `Tips`, `who account data` + /// - DbReads: `Reasons`, `Tips` + /// - DbWrites: `Reasons`, `Tips` /// # - #[weight = 140_000_000 + 4_000 * reason.len() as Weight + T::DbWeight::get().reads_writes(3, 2)] + #[weight = T::WeightInfo::report_awesome(reason.len() as u32)] fn report_awesome(origin, reason: Vec, who: T::AccountId) { let finder = ensure_signed(origin)?; - const MAX_SENSIBLE_REASON_LENGTH: usize = 16384; - ensure!(reason.len() <= MAX_SENSIBLE_REASON_LENGTH, Error::::ReasonTooBig); + ensure!(reason.len() <= T::MaximumReasonLength::get() as usize, Error::::ReasonTooBig); let reason_hash = T::Hashing::hash(&reason[..]); ensure!(!Reasons::::contains_key(&reason_hash), Error::::AlreadyKnown); @@ -465,7 +634,7 @@ decl_module! { ensure!(!Tips::::contains_key(&hash), Error::::AlreadyKnown); let deposit = T::TipReportDepositBase::get() - + T::TipReportDepositPerByte::get() * (reason.len() as u32).into(); + + T::DataDepositPerByte::get() * (reason.len() as u32).into(); T::Currency::reserve(&finder, deposit)?; Reasons::::insert(&reason_hash, &reason); @@ -501,7 +670,7 @@ decl_module! { /// - DbReads: `Tips`, `origin account` /// - DbWrites: `Reasons`, `Tips`, `origin account` /// # - #[weight = 120_000_000 + T::DbWeight::get().reads_writes(1, 2)] + #[weight = T::WeightInfo::retract_tip()] fn retract_tip(origin, hash: T::Hash) { let who = ensure_signed(origin)?; let tip = Tips::::get(&hash).ok_or(Error::::UnknownTip)?; @@ -537,11 +706,8 @@ decl_module! { /// - DbReads: `Tippers`, `Reasons` /// - DbWrites: `Reasons`, `Tips` /// # - #[weight = 110_000_000 - + 4_000 * reason.len() as Weight - + 480_000 * T::Tippers::max_len() as Weight - + T::DbWeight::get().reads_writes(2, 2)] - fn tip_new(origin, reason: Vec, who: T::AccountId, tip_value: BalanceOf) { + #[weight = T::WeightInfo::tip_new(reason.len() as u32, T::Tippers::max_len() as u32)] + fn tip_new(origin, reason: Vec, who: T::AccountId, #[compact] tip_value: BalanceOf) { let tipper = ensure_signed(origin)?; ensure!(T::Tippers::contains(&tipper), BadOrigin); let reason_hash = T::Hashing::hash(&reason[..]); @@ -588,9 +754,8 @@ decl_module! { /// - DbReads: `Tippers`, `Tips` /// - DbWrites: `Tips` /// # - #[weight = 68_000_000 + 2_000_000 * T::Tippers::max_len() as Weight - + T::DbWeight::get().reads_writes(2, 1)] - fn tip(origin, hash: T::Hash, tip_value: BalanceOf) { + #[weight = T::WeightInfo::tip(T::Tippers::max_len() as u32)] + fn tip(origin, hash: T::Hash, #[compact] tip_value: BalanceOf) { let tipper = ensure_signed(origin)?; ensure!(T::Tippers::contains(&tipper), BadOrigin); @@ -618,8 +783,7 @@ decl_module! { /// - DbReads: `Tips`, `Tippers`, `tip finder` /// - DbWrites: `Reasons`, `Tips`, `Tippers`, `tip finder` /// # - #[weight = 220_000_000 + 1_100_000 * T::Tippers::max_len() as Weight - + T::DbWeight::get().reads_writes(3, 3)] + #[weight = T::WeightInfo::close_tip(T::Tippers::max_len() as u32)] fn close_tip(origin, hash: T::Hash) { ensure_signed(origin)?; @@ -632,6 +796,371 @@ decl_module! { Self::payout_tip(hash, tip); } + /// 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::WeightInfo::propose_bounty(description.len() as u32)] + fn propose_bounty( + origin, + #[compact] value: BalanceOf, + description: Vec, + ) { + 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`. + /// + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB change. + /// # + #[weight = T::WeightInfo::approve_bounty()] + fn approve_bounty(origin, #[compact] bounty_id: ProposalIndex) { + T::ApproveOrigin::ensure_origin(origin)?; + + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { + let mut bounty = maybe_bounty.as_mut().ok_or(Error::::InvalidIndex)?; + ensure!(bounty.status == BountyStatus::Proposed, Error::::UnexpectedStatus); + + bounty.status = BountyStatus::Approved; + + BountyApprovals::::append(bounty_id); + + Ok(()) + })?; + } + + /// Assign a curator to a funded bounty. + /// + /// May only be called from `T::ApproveOrigin`. + /// + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB change. + /// # + #[weight = T::WeightInfo::propose_curator()] + fn propose_curator( + origin, + #[compact] bounty_id: ProposalIndex, + curator: ::Source, + #[compact] fee: BalanceOf, + ) { + T::ApproveOrigin::ensure_origin(origin)?; + + let curator = T::Lookup::lookup(curator)?; + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { + let mut bounty = maybe_bounty.as_mut().ok_or(Error::::InvalidIndex)?; + match bounty.status { + BountyStatus::Funded | BountyStatus::CuratorProposed { .. } => {}, + _ => return Err(Error::::UnexpectedStatus.into()), + }; + + ensure!(fee < bounty.value, Error::::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. + /// + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB change. + /// # + #[weight = T::WeightInfo::unassign_curator()] + fn unassign_curator( + origin, + #[compact] bounty_id: ProposalIndex, + ) { + let maybe_sender = ensure_signed(origin.clone()) + .map(Some) + .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?; + + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { + let mut bounty = maybe_bounty.as_mut().ok_or(Error::::InvalidIndex)?; + + let slash_curator = |curator: &T::AccountId, curator_deposit: &mut BalanceOf| { + 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::::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::::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::::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. + /// + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB change. + /// # + #[weight = T::WeightInfo::accept_curator()] + fn accept_curator(origin, #[compact] bounty_id: ProposalIndex) { + let signer = ensure_signed(origin)?; + + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { + let mut bounty = maybe_bounty.as_mut().ok_or(Error::::InvalidIndex)?; + + match bounty.status { + BountyStatus::CuratorProposed { ref curator } => { + ensure!(signer == *curator, Error::::RequireCurator); + + let deposit = T::BountyCuratorDeposit::get() * bounty.fee; + T::Currency::reserve(curator, deposit)?; + bounty.curator_deposit = deposit; + + let update_due = system::Module::::block_number() + T::BountyUpdatePeriod::get(); + bounty.status = BountyStatus::Active { curator: curator.clone(), update_due }; + + Ok(()) + }, + _ => Err(Error::::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 = T::WeightInfo::award_bounty()] + fn award_bounty(origin, #[compact] bounty_id: ProposalIndex, beneficiary: ::Source) { + let signer = ensure_signed(origin)?; + let beneficiary = T::Lookup::lookup(beneficiary)?; + + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { + let mut bounty = maybe_bounty.as_mut().ok_or(Error::::InvalidIndex)?; + match &bounty.status { + BountyStatus::Active { + curator, + .. + } => { + ensure!(signer == *curator, Error::::RequireCurator); + }, + _ => return Err(Error::::UnexpectedStatus.into()), + } + bounty.status = BountyStatus::PendingPayout { + curator: signer, + beneficiary: beneficiary.clone(), + unlock_at: system::Module::::block_number() + T::BountyDepositPayoutDelay::get(), + }; + + Ok(()) + })?; + + Self::deposit_event(Event::::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 = T::WeightInfo::claim_bounty()] + fn claim_bounty(origin, #[compact] bounty_id: BountyIndex) { + let _ = ensure_signed(origin)?; // anyone can trigger claim + + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { + let bounty = maybe_bounty.take().ok_or(Error::::InvalidIndex)?; + if let BountyStatus::PendingPayout { curator, beneficiary, unlock_at } = bounty.status { + ensure!(system::Module::::block_number() >= unlock_at, Error::::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::::BountyClaimed(bounty_id, payout, beneficiary)); + Ok(()) + } else { + Err(Error::::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 = T::WeightInfo::close_bounty_proposed().max(T::WeightInfo::close_bounty_active())] + fn close_bounty(origin, #[compact] bounty_id: BountyIndex) -> DispatchResultWithPostInfo { + T::RejectOrigin::ensure_origin(origin)?; + + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResultWithPostInfo { + let bounty = maybe_bounty.as_ref().ok_or(Error::::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::::BountyRejected(bounty_id, value)); + // Return early, nothing else to do. + return Ok(Some(T::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::::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::::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::::BountyCanceled(bounty_id)); + Ok(Some(T::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 = T::WeightInfo::extend_bounty_expiry()] + fn extend_bounty_expiry(origin, #[compact] bounty_id: BountyIndex, _remark: Vec) { + let signer = ensure_signed(origin)?; + + Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { + let bounty = maybe_bounty.as_mut().ok_or(Error::::InvalidIndex)?; + + match bounty.status { + BountyStatus::Active { ref curator, ref mut update_due } => { + ensure!(*curator == signer, Error::::RequireCurator); + *update_due = (system::Module::::block_number() + T::BountyUpdatePeriod::get()).max(*update_due); + }, + _ => return Err(Error::::UnexpectedStatus.into()), + } + + Ok(()) + })?; + + Self::deposit_event(Event::::BountyExtended(bounty_id)); + } + /// # /// - Complexity: `O(A)` where `A` is the number of approvals /// - Db reads and writes: `Approvals`, `pot account data` @@ -642,10 +1171,7 @@ decl_module! { fn on_initialize(n: T::BlockNumber) -> Weight { // Check to see if we should spend some funds! if (n % T::SpendPeriod::get()).is_zero() { - let approvals_len = Self::spend_funds(); - - 270_000_000 * approvals_len - + T::DbWeight::get().reads_writes(2 + approvals_len * 3, 2 + approvals_len * 3) + Self::spend_funds() } else { 0 } @@ -664,6 +1190,13 @@ impl, I: Instance> Module { 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)) + } + /// The needed bond for a proposal whose spend is `value`. fn calculate_bond(value: BalanceOf) -> BalanceOf { T::ProposalBondMinimum::get().max(T::ProposalBond::get() * value) @@ -743,14 +1276,17 @@ impl, I: Instance> Module { } /// Spend some money! returns number of approvals before spend. - fn spend_funds() -> u64 { + fn spend_funds() -> Weight { + let mut total_weight: Weight = Zero::zero(); + let mut budget_remaining = Self::pot(); Self::deposit_event(RawEvent::Spending(budget_remaining)); + let account_id = Self::account_id(); let mut missed_any = false; let mut imbalance = >::zero(); - let prior_approvals_len = >::mutate(|v| { - let prior_approvals_len = v.len() as u64; + let proposals_len = Approvals::::mutate(|v| { + let proposals_approvals_len = v.len() as u32; v.retain(|&index| { // Should always be true, but shouldn't panic if false or we're screwed. if let Some(p) = Self::proposals(index) { @@ -774,9 +1310,44 @@ impl, I: Instance> Module { false } }); - prior_approvals_len + proposals_approvals_len + }); + + total_weight += T::WeightInfo::on_initialize_proposals(proposals_len); + + let bounties_len = BountyApprovals::::mutate(|v| { + let bounties_approval_len = v.len() as u32; + v.retain(|&index| { + Bounties::::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::WeightInfo::on_initialize_bounties(bounties_len); + if !missed_any { // burn some proportion of the remaining budget if we run a surplus. let burn = (T::Burn::get() * budget_remaining).min(budget_remaining); @@ -793,7 +1364,7 @@ impl, I: Instance> Module { // Thus we can't spend more than account free balance minus ED; // Thus account is kept alive; qed; if let Err(problem) = T::Currency::settle( - &Self::account_id(), + &account_id, imbalance, WithdrawReason::Transfer.into(), KeepAlive @@ -805,7 +1376,7 @@ impl, I: Instance> Module { Self::deposit_event(RawEvent::Rollover(budget_remaining)); - prior_approvals_len + total_weight } /// Return the amount of money in the pot. @@ -816,6 +1387,36 @@ impl, I: Instance> Module { .saturating_sub(T::Currency::minimum_balance()) } + fn create_bounty( + proposer: T::AccountId, + description: Vec, + value: BalanceOf, + ) -> DispatchResult { + ensure!(description.len() <= T::MaximumReasonLength::get() as usize, Error::::ReasonTooBig); + ensure!(value >= T::BountyValueMinimum::get(), Error::::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::::InsufficientProposersBalance)?; + + BountyCount::::put(index + 1); + + let bounty = Bounty { + proposer, value, fee: 0.into(), curator_deposit: 0.into(), bond, status: BountyStatus::Proposed, + }; + + Bounties::::insert(index, &bounty); + BountyDescriptions::::insert(index, description); + + Self::deposit_event(RawEvent::BountyProposed(index)); + + Ok(()) + } + pub fn migrate_retract_tip_for_tip_new() { /// An open tipping "motion". Retains all details of a tip including information on the finder /// and the members who have voted. diff --git a/frame/treasury/src/tests.rs b/frame/treasury/src/tests.rs index a4e1e3d8d77b2..a092a14f05d84 100644 --- a/frame/treasury/src/tests.rs +++ b/frame/treasury/src/tests.rs @@ -67,7 +67,7 @@ impl frame_system::Trait for Test { type Call = (); type Hash = H256; type Hashing = BlakeTwo256; - type AccountId = u64; + type AccountId = u128; // u64 is not enough to hold bytes used to generate bounty account type Lookup = IdentityLookup; type Header = Header; type Event = Event; @@ -99,17 +99,17 @@ impl pallet_balances::Trait for Test { type WeightInfo = (); } thread_local! { - static TEN_TO_FOURTEEN: RefCell> = RefCell::new(vec![10,11,12,13,14]); + static TEN_TO_FOURTEEN: RefCell> = RefCell::new(vec![10,11,12,13,14]); } pub struct TenToFourteen; -impl Contains for TenToFourteen { - fn sorted_members() -> Vec { +impl Contains for TenToFourteen { + fn sorted_members() -> Vec { TEN_TO_FOURTEEN.with(|v| { v.borrow().clone() }) } #[cfg(feature = "runtime-benchmarks")] - fn add(new: &u64) { + fn add(new: &u128) { TEN_TO_FOURTEEN.with(|v| { let mut members = v.borrow_mut(); members.push(*new); @@ -131,25 +131,37 @@ parameter_types! { pub const TipCountdown: u64 = 1; pub const TipFindersFee: Percent = Percent::from_percent(20); pub const TipReportDepositBase: u64 = 1; - pub const TipReportDepositPerByte: u64 = 1; + pub const DataDepositPerByte: u64 = 1; + pub const BountyDepositBase: u64 = 80; + pub const BountyDepositPayoutDelay: u64 = 3; pub const TreasuryModuleId: ModuleId = ModuleId(*b"py/trsry"); + pub const BountyUpdatePeriod: u32 = 20; + pub const MaximumReasonLength: u32 = 16384; + pub const BountyCuratorDeposit: Permill = Permill::from_percent(50); + pub const BountyValueMinimum: u64 = 1; } impl Trait for Test { type ModuleId = TreasuryModuleId; type Currency = pallet_balances::Module; - type ApproveOrigin = frame_system::EnsureRoot; - type RejectOrigin = frame_system::EnsureRoot; + type ApproveOrigin = frame_system::EnsureRoot; + type RejectOrigin = frame_system::EnsureRoot; type Tippers = TenToFourteen; type TipCountdown = TipCountdown; type TipFindersFee = TipFindersFee; type TipReportDepositBase = TipReportDepositBase; - type TipReportDepositPerByte = TipReportDepositPerByte; + type DataDepositPerByte = DataDepositPerByte; type Event = Event; - type ProposalRejection = (); + type OnSlash = (); type ProposalBond = ProposalBond; type ProposalBondMinimum = ProposalBondMinimum; type SpendPeriod = SpendPeriod; type Burn = Burn; + type BountyDepositBase = BountyDepositBase; + type BountyDepositPayoutDelay = BountyDepositPayoutDelay; + type BountyUpdatePeriod = BountyUpdatePeriod; + type BountyCuratorDeposit = BountyCuratorDeposit; + type BountyValueMinimum = BountyValueMinimum; + type MaximumReasonLength = MaximumReasonLength; type BurnDestination = (); // Just gets burned. type WeightInfo = (); } @@ -167,6 +179,15 @@ pub fn new_test_ext() -> sp_io::TestExternalities { t.into() } +fn last_event() -> RawEvent { + System::events().into_iter().map(|r| r.event) + .filter_map(|e| { + if let Event::treasury(inner) = e { Some(inner) } else { None } + }) + .last() + .unwrap() +} + #[test] fn genesis_config_works() { new_test_ext().execute_with(|| { @@ -176,7 +197,7 @@ fn genesis_config_works() { } fn tip_hash() -> H256 { - BlakeTwo256::hash_of(&(BlakeTwo256::hash(b"awesome.dot"), 3u64)) + BlakeTwo256::hash_of(&(BlakeTwo256::hash(b"awesome.dot"), 3u128)) } #[test] @@ -225,7 +246,7 @@ fn report_awesome_from_beneficiary_and_tip_works() { assert_ok!(Treasury::report_awesome(Origin::signed(0), b"awesome.dot".to_vec(), 0)); assert_eq!(Balances::reserved_balance(0), 12); assert_eq!(Balances::free_balance(0), 88); - let h = BlakeTwo256::hash_of(&(BlakeTwo256::hash(b"awesome.dot"), 0u64)); + let h = BlakeTwo256::hash_of(&(BlakeTwo256::hash(b"awesome.dot"), 0u128)); assert_ok!(Treasury::tip(Origin::signed(10), h.clone(), 10)); assert_ok!(Treasury::tip(Origin::signed(11), h.clone(), 10)); assert_ok!(Treasury::tip(Origin::signed(12), h.clone(), 10)); @@ -248,15 +269,7 @@ fn close_tip_works() { let h = tip_hash(); - assert_eq!( - System::events().into_iter().map(|r| r.event) - .filter_map(|e| { - if let Event::treasury(inner) = e { Some(inner) } else { None } - }) - .last() - .unwrap(), - RawEvent::NewTip(h), - ); + assert_eq!(last_event(), RawEvent::NewTip(h)); assert_ok!(Treasury::tip(Origin::signed(11), h.clone(), 10)); @@ -264,15 +277,7 @@ fn close_tip_works() { assert_ok!(Treasury::tip(Origin::signed(12), h.clone(), 10)); - assert_eq!( - System::events().into_iter().map(|r| r.event) - .filter_map(|e| { - if let Event::treasury(inner) = e { Some(inner) } else { None } - }) - .last() - .unwrap(), - RawEvent::TipClosing(h), - ); + assert_eq!(last_event(), RawEvent::TipClosing(h)); assert_noop!(Treasury::close_tip(Origin::signed(0), h.into()), Error::::Premature); @@ -281,15 +286,7 @@ fn close_tip_works() { assert_ok!(Treasury::close_tip(Origin::signed(0), h.into())); assert_eq!(Balances::free_balance(3), 10); - assert_eq!( - System::events().into_iter().map(|r| r.event) - .filter_map(|e| { - if let Event::treasury(inner) = e { Some(inner) } else { None } - }) - .last() - .unwrap(), - RawEvent::TipClosed(h, 3, 10), - ); + assert_eq!(last_event(), RawEvent::TipClosed(h, 3, 10)); assert_noop!(Treasury::close_tip(Origin::signed(100), h.into()), Error::::UnknownTip); }); @@ -441,30 +438,21 @@ fn reject_already_rejected_spend_proposal_fails() { 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::::InvalidProposalIndex, - ); + assert_noop!(Treasury::reject_proposal(Origin::root(), 0), Error::::InvalidIndex); }); } #[test] fn reject_non_existent_spend_proposal_fails() { new_test_ext().execute_with(|| { - assert_noop!( - Treasury::reject_proposal(Origin::root(), 0), - Error::::InvalidProposalIndex, - ); + assert_noop!(Treasury::reject_proposal(Origin::root(), 0), Error::::InvalidIndex); }); } #[test] fn accept_non_existent_spend_proposal_fails() { new_test_ext().execute_with(|| { - assert_noop!( - Treasury::approve_proposal(Origin::root(), 0), - Error::::InvalidProposalIndex, - ); + assert_noop!(Treasury::approve_proposal(Origin::root(), 0), Error::::InvalidIndex); }); } @@ -475,10 +463,7 @@ fn accept_already_rejected_spend_proposal_fails() { 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::::InvalidProposalIndex, - ); + assert_noop!(Treasury::approve_proposal(Origin::root(), 0), Error::::InvalidIndex); }); } @@ -574,6 +559,502 @@ fn inexistent_account_works() { }); } +#[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!(Treasury::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!(Treasury::bounties(0).unwrap(), Bounty { + proposer: 0, + fee: 0, + curator_deposit: 0, + value: 10, + bond: deposit, + status: BountyStatus::Proposed, + }); + + assert_eq!(Treasury::bounty_descriptions(0).unwrap(), b"1234567890".to_vec()); + + assert_eq!(Treasury::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!( + Treasury::propose_bounty(Origin::signed(1), 0, [0; 17_000].to_vec()), + Error::::ReasonTooBig + ); + + assert_noop!( + Treasury::propose_bounty(Origin::signed(1), 10, b"12345678901234567890".to_vec()), + Error::::InsufficientProposersBalance + ); + + assert_noop!( + Treasury::propose_bounty(Origin::signed(1), 0, b"12345678901234567890".to_vec()), + Error::::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!(Treasury::close_bounty(Origin::root(), 0), Error::::InvalidIndex); + + assert_ok!(Treasury::propose_bounty(Origin::signed(0), 10, b"12345".to_vec())); + + assert_ok!(Treasury::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!(Treasury::bounties(0), None); + assert!(!Bounties::::contains_key(0)); + assert_eq!(Treasury::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!(Treasury::approve_bounty(Origin::root(), 0), Error::::InvalidIndex); + + assert_ok!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + let deposit: u64 = 80 + 5; + + assert_eq!(Treasury::bounties(0).unwrap(), Bounty { + proposer: 0, + fee: 0, + value: 50, + curator_deposit: 0, + bond: deposit, + status: BountyStatus::Approved, + }); + assert_eq!(Treasury::bounty_approvals(), vec![0]); + + assert_noop!(Treasury::close_bounty(Origin::root(), 0), Error::::UnexpectedStatus); + + // deposit not returned yet + assert_eq!(Balances::reserved_balance(0), deposit); + assert_eq!(Balances::free_balance(0), 100 - deposit); + + >::on_initialize(2); + + // return deposit + assert_eq!(Balances::reserved_balance(0), 0); + assert_eq!(Balances::free_balance(0), 100); + + assert_eq!(Treasury::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(Treasury::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!(Treasury::propose_curator(Origin::root(), 0, 4, 4), Error::::InvalidIndex); + + assert_ok!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + System::set_block_number(2); + >::on_initialize(2); + + assert_noop!(Treasury::propose_curator(Origin::root(), 0, 4, 50), Error::::InvalidFee); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 4, 4)); + + assert_eq!(Treasury::bounties(0).unwrap(), Bounty { + proposer: 0, + fee: 4, + curator_deposit: 0, + value: 50, + bond: 85, + status: BountyStatus::CuratorProposed { + curator: 4, + }, + }); + + assert_noop!(Treasury::accept_curator(Origin::signed(1), 0), Error::::RequireCurator); + assert_noop!(Treasury::accept_curator(Origin::signed(4), 0), pallet_balances::Error::::InsufficientBalance); + + Balances::make_free_balance_be(&4, 10); + + assert_ok!(Treasury::accept_curator(Origin::signed(4), 0)); + + assert_eq!(Treasury::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!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 4, 4)); + + assert_noop!(Treasury::unassign_curator(Origin::signed(1), 0), BadOrigin); + + assert_ok!(Treasury::unassign_curator(Origin::signed(4), 0)); + + assert_eq!(Treasury::bounties(0).unwrap(), Bounty { + proposer: 0, + fee: 4, + curator_deposit: 0, + value: 50, + bond: 85, + status: BountyStatus::Funded, + }); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 4, 4)); + + Balances::make_free_balance_be(&4, 10); + + assert_ok!(Treasury::accept_curator(Origin::signed(4), 0)); + + assert_ok!(Treasury::unassign_curator(Origin::root(), 0)); + + assert_eq!(Treasury::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!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 4, 4)); + assert_ok!(Treasury::accept_curator(Origin::signed(4), 0)); + + assert_eq!(Balances::free_balance(4), 8); // inital 10 - 2 deposit + + assert_noop!(Treasury::award_bounty(Origin::signed(1), 0, 3), Error::::RequireCurator); + + assert_ok!(Treasury::award_bounty(Origin::signed(4), 0, 3)); + + assert_eq!(Treasury::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!(Treasury::claim_bounty(Origin::signed(1), 0), Error::::Premature); + + System::set_block_number(5); + >::on_initialize(5); + + assert_ok!(Balances::transfer(Origin::signed(0), Treasury::bounty_account_id(0), 10)); + + assert_ok!(Treasury::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(Treasury::bounty_account_id(0)), 0); + + assert_eq!(Treasury::bounties(0), None); + assert_eq!(Treasury::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!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 4, 49)); + assert_ok!(Treasury::accept_curator(Origin::signed(4), 0)); + + assert_ok!(Treasury::award_bounty(Origin::signed(4), 0, 3)); + + System::set_block_number(5); + >::on_initialize(5); + + // make fee > balance + let _ = Balances::slash(&Treasury::bounty_account_id(0), 10); + + assert_ok!(Treasury::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(Treasury::bounty_account_id(0)), 0); + + assert_eq!(Treasury::bounties(0), None); + assert_eq!(Treasury::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!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Balances::transfer(Origin::signed(0), Treasury::bounty_account_id(0), 10)); + + assert_eq!(Treasury::bounties(0).unwrap(), Bounty { + proposer: 0, + fee: 0, + curator_deposit: 0, + value: 50, + bond: 85, + status: BountyStatus::Funded, + }); + + assert_eq!(Balances::free_balance(Treasury::bounty_account_id(0)), 60); + + assert_noop!(Treasury::close_bounty(Origin::signed(0), 0), BadOrigin); + + assert_ok!(Treasury::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!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 0, 10)); + assert_ok!(Treasury::accept_curator(Origin::signed(0), 0)); + + assert_eq!(Balances::free_balance(0), 95); + assert_eq!(Balances::reserved_balance(0), 5); + + assert_ok!(Treasury::award_bounty(Origin::signed(0), 0, 3)); + + // Cannot close bounty directly when payout is happening... + assert_noop!(Treasury::close_bounty(Origin::root(), 0), Error::::PendingPayout); + + // Instead unassign the curator to slash them and then close. + assert_ok!(Treasury::unassign_curator(Origin::root(), 0)); + assert_ok!(Treasury::close_bounty(Origin::root(), 0)); + + assert_eq!(last_event(), RawEvent::BountyCanceled(0)); + + assert_eq!(Balances::free_balance(Treasury::bounty_account_id(0)), 0); + // Slashed. + assert_eq!(Balances::free_balance(0), 95); + assert_eq!(Balances::reserved_balance(0), 0); + + assert_eq!(Treasury::bounties(0), None); + assert_eq!(Treasury::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!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 1, 10)); + assert_ok!(Treasury::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); + >::on_initialize(22); + + assert_noop!(Treasury::unassign_curator(Origin::signed(0), 0), Error::::Premature); + + System::set_block_number(23); + >::on_initialize(23); + + assert_ok!(Treasury::unassign_curator(Origin::signed(0), 0)); + + assert_eq!(Treasury::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!(Treasury::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + + assert_ok!(Treasury::approve_bounty(Origin::root(), 0)); + + assert_noop!(Treasury::extend_bounty_expiry(Origin::signed(1), 0, Vec::new()), Error::::UnexpectedStatus); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Treasury::propose_curator(Origin::root(), 0, 4, 10)); + assert_ok!(Treasury::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); + >::on_initialize(10); + + assert_noop!(Treasury::extend_bounty_expiry(Origin::signed(0), 0, Vec::new()), Error::::RequireCurator); + assert_ok!(Treasury::extend_bounty_expiry(Origin::signed(4), 0, Vec::new())); + + assert_eq!(Treasury::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!(Treasury::extend_bounty_expiry(Origin::signed(4), 0, Vec::new())); + + assert_eq!(Treasury::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); + >::on_initialize(25); + + assert_noop!(Treasury::unassign_curator(Origin::signed(0), 0), Error::::Premature); + assert_ok!(Treasury::unassign_curator(Origin::signed(4), 0)); + + assert_eq!(Balances::free_balance(4), 10); // not slashed + assert_eq!(Balances::reserved_balance(4), 0); + }); +} + #[test] fn test_last_reward_migration() { use sp_storage::Storage; @@ -604,7 +1085,7 @@ fn test_last_reward_migration() { let reason1 = BlakeTwo256::hash(b"reason1"); let hash1 = BlakeTwo256::hash_of(&(reason1, 10u64)); - let old_tip_finder = OldOpenTip:: { + let old_tip_finder = OldOpenTip:: { reason: reason1, who: 10, finder: Some((20, 30)), @@ -615,7 +1096,7 @@ fn test_last_reward_migration() { let reason2 = BlakeTwo256::hash(b"reason2"); let hash2 = BlakeTwo256::hash_of(&(reason2, 20u64)); - let old_tip_no_finder = OldOpenTip:: { + let old_tip_no_finder = OldOpenTip:: { reason: reason2, who: 20, finder: None,