diff --git a/Cargo.lock b/Cargo.lock index d67ebd7f24ddf..d5c2450fd5dba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4701,6 +4701,7 @@ dependencies = [ "pallet-bags-list", "pallet-balances", "pallet-bounties", + "pallet-child-bounties", "pallet-collective", "pallet-contracts", "pallet-contracts-primitives", @@ -5382,6 +5383,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-child-bounties" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log 0.4.14", + "pallet-balances", + "pallet-bounties", + "pallet-treasury", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-collective" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index f30b223a9b205..7bb08345c0fe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ members = [ "frame/beefy-mmr/primitives", "frame/benchmarking", "frame/bounties", + "frame/child-bounties", "frame/collective", "frame/contracts", "frame/contracts/rpc", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 4771202b90a0c..a61c2311917a8 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -59,6 +59,7 @@ pallet-babe = { version = "4.0.0-dev", default-features = false, path = "../../. pallet-bags-list = { version = "4.0.0-dev", default-features = false, path = "../../../frame/bags-list" } pallet-balances = { version = "4.0.0-dev", default-features = false, path = "../../../frame/balances" } pallet-bounties = { version = "4.0.0-dev", default-features = false, path = "../../../frame/bounties" } +pallet-child-bounties = { version = "4.0.0-dev", default-features = false, path = "../../../frame/child-bounties" } pallet-collective = { version = "4.0.0-dev", default-features = false, path = "../../../frame/collective" } pallet-contracts = { version = "4.0.0-dev", default-features = false, path = "../../../frame/contracts" } pallet-contracts-primitives = { version = "4.0.0-dev", default-features = false, path = "../../../frame/contracts/common/" } @@ -173,7 +174,8 @@ std = [ "log/std", "frame-try-runtime/std", "sp-npos-elections/std", - "sp-io/std" + "sp-io/std", + "pallet-child-bounties/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -186,6 +188,7 @@ runtime-benchmarks = [ "pallet-bags-list/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-bounties/runtime-benchmarks", + "pallet-child-bounties/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-contracts/runtime-benchmarks", "pallet-democracy/runtime-benchmarks", @@ -225,6 +228,7 @@ try-runtime = [ "pallet-babe/try-runtime", "pallet-balances/try-runtime", "pallet-bounties/try-runtime", + "pallet-child-bounties/try-runtime", "pallet-collective/try-runtime", "pallet-contracts/try-runtime", "pallet-democracy/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 2cf6548b79211..adf39a6d38f8d 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -844,10 +844,13 @@ parameter_types! { pub const BountyDepositPayoutDelay: BlockNumber = 1 * DAYS; pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const BountyUpdatePeriod: BlockNumber = 14 * DAYS; - pub const MaximumReasonLength: u32 = 16384; + pub const MaximumReasonLength: u32 = 300; pub const BountyCuratorDeposit: Permill = Permill::from_percent(50); pub const BountyValueMinimum: Balance = 5 * DOLLARS; pub const MaxApprovals: u32 = 100; + pub const MaxActiveChildBountyCount: u32 = 5; + pub const ChildBountyValueMinimum: Balance = 1 * DOLLARS; + pub const ChildBountyCuratorDepositBase: Permill = Permill::from_percent(10); } impl pallet_treasury::Config for Runtime { @@ -883,6 +886,15 @@ impl pallet_bounties::Config for Runtime { type DataDepositPerByte = DataDepositPerByte; type MaximumReasonLength = MaximumReasonLength; type WeightInfo = pallet_bounties::weights::SubstrateWeight; + type ChildBountyManager = ChildBounties; +} + +impl pallet_child_bounties::Config for Runtime { + type Event = Event; + type MaxActiveChildBountyCount = MaxActiveChildBountyCount; + type ChildBountyValueMinimum = ChildBountyValueMinimum; + type ChildBountyCuratorDepositBase = ChildBountyCuratorDepositBase; + type WeightInfo = pallet_child_bounties::weights::SubstrateWeight; } impl pallet_tips::Config for Runtime { @@ -1303,6 +1315,7 @@ construct_runtime!( Uniques: pallet_uniques, TransactionStorage: pallet_transaction_storage, BagsList: pallet_bags_list, + ChildBounties: pallet_child_bounties, } ); @@ -1642,6 +1655,7 @@ impl_runtime_apis! { list_benchmark!(list, extra, pallet_bags_list, BagsList); list_benchmark!(list, extra, pallet_balances, Balances); list_benchmark!(list, extra, pallet_bounties, Bounties); + list_benchmark!(list, extra, pallet_child_bounties, ChildBounties); list_benchmark!(list, extra, pallet_collective, Council); list_benchmark!(list, extra, pallet_contracts, Contracts); list_benchmark!(list, extra, pallet_democracy, Democracy); @@ -1719,6 +1733,7 @@ impl_runtime_apis! { add_benchmark!(params, batches, pallet_balances, Balances); add_benchmark!(params, batches, pallet_bags_list, BagsList); add_benchmark!(params, batches, pallet_bounties, Bounties); + add_benchmark!(params, batches, pallet_child_bounties, ChildBounties); add_benchmark!(params, batches, pallet_collective, Council); add_benchmark!(params, batches, pallet_contracts, Contracts); add_benchmark!(params, batches, pallet_democracy, Democracy); diff --git a/frame/bounties/src/lib.rs b/frame/bounties/src/lib.rs index 5021608b506b9..1d32b63f38f92 100644 --- a/frame/bounties/src/lib.rs +++ b/frame/bounties/src/lib.rs @@ -123,6 +123,15 @@ pub struct Bounty { status: BountyStatus, } +impl + Bounty +{ + /// Getter for bounty status, to be used for child bounties. + pub fn get_status(&self) -> BountyStatus { + self.status.clone() + } +} + /// The status of a bounty proposal. #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub enum BountyStatus { @@ -156,6 +165,15 @@ pub enum BountyStatus { }, } +/// The child-bounty manager. +pub trait ChildBountyManager { + /// Get the active child-bounties for a parent bounty. + fn child_bounties_count(bounty_id: BountyIndex) -> BountyIndex; + + /// Get total curator fees of children-bounty curators. + fn children_curator_fees(bounty_id: BountyIndex) -> Balance; +} + #[frame_support::pallet] pub mod pallet { use super::*; @@ -202,6 +220,9 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + /// The child-bounty manager. + type ChildBountyManager: ChildBountyManager>; } #[pallet::error] @@ -225,6 +246,8 @@ pub mod pallet { PendingPayout, /// The bounties cannot be claimed/closed because it's still in the countdown period. Premature, + /// The bounty cannot be closed because it has active child-bounties. + HasActiveChildBounty, } #[pallet::event] @@ -512,6 +535,13 @@ pub mod pallet { Bounties::::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult { let mut bounty = maybe_bounty.as_mut().ok_or(Error::::InvalidIndex)?; + + // Ensure no active child-bounties before processing the call. + ensure!( + T::ChildBountyManager::child_bounties_count(bounty_id) == 0, + Error::::HasActiveChildBounty + ); + match &bounty.status { BountyStatus::Active { curator, .. } => { ensure!(signer == *curator, Error::::RequireCurator); @@ -563,7 +593,15 @@ pub mod pallet { let payout = balance.saturating_sub(fee); let err_amount = T::Currency::unreserve(&curator, bounty.curator_deposit); debug_assert!(err_amount.is_zero()); - let res = T::Currency::transfer(&bounty_account, &curator, fee, AllowDeath); // should not fail + + // Get total child-bounties curator fees, and subtract it from master curator + // fee. + let children_fee = T::ChildBountyManager::children_curator_fees(bounty_id); + debug_assert!(children_fee <= fee); + + let final_fee = fee.saturating_sub(children_fee); + let res = + T::Currency::transfer(&bounty_account, &curator, final_fee, AllowDeath); // should not fail debug_assert!(res.is_ok()); let res = T::Currency::transfer(&bounty_account, &beneficiary, payout, AllowDeath); // should not fail @@ -609,6 +647,12 @@ pub mod pallet { |maybe_bounty| -> DispatchResultWithPostInfo { let bounty = maybe_bounty.as_ref().ok_or(Error::::InvalidIndex)?; + // Ensure no active child-bounties before processing the call. + ensure!( + T::ChildBountyManager::child_bounties_count(bounty_id) == 0, + Error::::HasActiveChildBounty + ); + match &bounty.status { BountyStatus::Proposed => { // The reject origin would like to cancel a proposed bounty. @@ -813,3 +857,14 @@ impl pallet_treasury::SpendFunds for Pallet { *total_weight += ::WeightInfo::spend_funds(bounties_len); } } + +// Default impl for when ChildBounties is not being used in the runtime. +impl ChildBountyManager for () { + fn child_bounties_count(_bounty_id: BountyIndex) -> BountyIndex { + Default::default() + } + + fn children_curator_fees(_bounty_id: BountyIndex) -> Balance { + Zero::zero() + } +} diff --git a/frame/bounties/src/tests.rs b/frame/bounties/src/tests.rs index 344bb6495c3bc..bbcf9d6d9e72d 100644 --- a/frame/bounties/src/tests.rs +++ b/frame/bounties/src/tests.rs @@ -146,6 +146,7 @@ impl Config for Test { type DataDepositPerByte = DataDepositPerByte; type MaximumReasonLength = MaximumReasonLength; type WeightInfo = (); + type ChildBountyManager = (); } type TreasuryError = pallet_treasury::Error; diff --git a/frame/bounties/src/weights.rs b/frame/bounties/src/weights.rs index be93636424399..1383f69bed918 100644 --- a/frame/bounties/src/weights.rs +++ b/frame/bounties/src/weights.rs @@ -18,24 +18,24 @@ //! Autogenerated weights for pallet_bounties //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2021-08-07, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2021-11-19, 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 +// ./target/release/substrate // benchmark // --chain=dev // --steps=50 // --repeat=20 -// --pallet=pallet_bounties +// --pallet +// pallet_bounties // --extrinsic=* // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --output=./frame/bounties/src/weights.rs +// --output=./frame/bounties/src/ // --template=./.maintain/frame-weight-template.hbs - #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] @@ -61,87 +61,91 @@ pub trait WeightInfo { /// Weights for pallet_bounties using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - // Storage: Treasury BountyCount (r:1 w:1) + // Storage: Bounties BountyCount (r:1 w:1) // Storage: System Account (r:1 w:1) - // Storage: Treasury BountyDescriptions (r:0 w:1) - // Storage: Treasury Bounties (r:0 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) + // Storage: Bounties Bounties (r:0 w:1) fn propose_bounty(d: u32, ) -> Weight { - (44_482_000 as Weight) + (58_161_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)) } - // Storage: Treasury Bounties (r:1 w:1) - // Storage: Treasury BountyApprovals (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: Bounties BountyApprovals (r:1 w:1) fn approve_bounty() -> Weight { - (11_955_000 as Weight) + (20_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) fn propose_curator() -> Weight { - (9_771_000 as Weight) + (14_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:1 w:1) fn unassign_curator() -> Weight { - (40_683_000 as Weight) + (62_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:1 w:1) fn accept_curator() -> Weight { - (36_390_000 as Weight) + (55_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:0) fn award_bounty() -> Weight { - (25_187_000 as Weight) - .saturating_add(T::DbWeight::get().reads(1 as Weight)) + (46_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:3 w:3) - // Storage: Treasury BountyDescriptions (r:0 w:1) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) fn claim_bounty() -> Weight { - (124_785_000 as Weight) - .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(5 as Weight)) + (185_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(6 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:0) // Storage: System Account (r:1 w:1) - // Storage: Treasury BountyDescriptions (r:0 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) fn close_bounty_proposed() -> Weight { - (39_483_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) + (82_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:0) // Storage: System Account (r:2 w:2) - // Storage: Treasury BountyDescriptions (r:0 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) fn close_bounty_active() -> Weight { - (83_453_000 as Weight) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) + (137_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) fn extend_bounty_expiry() -> Weight { - (24_151_000 as Weight) + (33_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } - // Storage: Treasury BountyApprovals (r:1 w:1) - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties BountyApprovals (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:2 w:2) fn spend_funds(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 16_000 - .saturating_add((58_004_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 191_000 + .saturating_add((81_116_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)) @@ -151,87 +155,91 @@ impl WeightInfo for SubstrateWeight { // For backwards compatibility and tests impl WeightInfo for () { - // Storage: Treasury BountyCount (r:1 w:1) + // Storage: Bounties BountyCount (r:1 w:1) // Storage: System Account (r:1 w:1) - // Storage: Treasury BountyDescriptions (r:0 w:1) - // Storage: Treasury Bounties (r:0 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) + // Storage: Bounties Bounties (r:0 w:1) fn propose_bounty(d: u32, ) -> Weight { - (44_482_000 as Weight) + (58_161_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)) } - // Storage: Treasury Bounties (r:1 w:1) - // Storage: Treasury BountyApprovals (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: Bounties BountyApprovals (r:1 w:1) fn approve_bounty() -> Weight { - (11_955_000 as Weight) + (20_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) fn propose_curator() -> Weight { - (9_771_000 as Weight) + (14_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:1 w:1) fn unassign_curator() -> Weight { - (40_683_000 as Weight) + (62_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:1 w:1) fn accept_curator() -> Weight { - (36_390_000 as Weight) + (55_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:0) fn award_bounty() -> Weight { - (25_187_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + (46_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:3 w:3) - // Storage: Treasury BountyDescriptions (r:0 w:1) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) fn claim_bounty() -> Weight { - (124_785_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + (185_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:0) // Storage: System Account (r:1 w:1) - // Storage: Treasury BountyDescriptions (r:0 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) fn close_bounty_proposed() -> Weight { - (39_483_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + (82_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:0) // Storage: System Account (r:2 w:2) - // Storage: Treasury BountyDescriptions (r:0 w:1) + // Storage: Bounties BountyDescriptions (r:0 w:1) fn close_bounty_active() -> Weight { - (83_453_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + (137_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) fn extend_bounty_expiry() -> Weight { - (24_151_000 as Weight) + (33_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } - // Storage: Treasury BountyApprovals (r:1 w:1) - // Storage: Treasury Bounties (r:1 w:1) + // Storage: Bounties BountyApprovals (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:1) // Storage: System Account (r:2 w:2) fn spend_funds(b: u32, ) -> Weight { (0 as Weight) - // Standard Error: 16_000 - .saturating_add((58_004_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 191_000 + .saturating_add((81_116_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)) diff --git a/frame/child-bounties/Cargo.toml b/frame/child-bounties/Cargo.toml new file mode 100644 index 0000000000000..040337424d399 --- /dev/null +++ b/frame/child-bounties/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "pallet-child-bounties" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet to manage child bounties" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } +sp-runtime = { version = "4.0.0-dev", default-features = false, path = "../../primitives/runtime" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +pallet-treasury = { version = "4.0.0-dev", default-features = false, path = "../treasury" } +pallet-bounties = { version = "4.0.0-dev", default-features = false, path = "../bounties" } +sp-io = { version = "4.0.0-dev", path = "../../primitives/io", default-features = false } +sp-core = { version = "4.0.0-dev", path = "../../primitives/core", default-features = false } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } +log = { version = "0.4.14", default-features = false } + +[dev-dependencies] +pallet-balances = { version = "4.0.0-dev", path = "../balances" } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-core/std", + "sp-io/std", + "scale-info/std", + "sp-std/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "pallet-treasury/std", + "pallet-bounties/std", + "log/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/child-bounties/README.md b/frame/child-bounties/README.md new file mode 100644 index 0000000000000..e07996d54957a --- /dev/null +++ b/frame/child-bounties/README.md @@ -0,0 +1,21 @@ +# Child Bounties Pallet (pallet-child-bounties) + +## Child Bounty + +> NOTE: This pallet is tightly coupled with pallet-treasury and pallet-bounties. + +With child bounties, a large bounty proposal can be divided into smaller chunks, for parallel execution, and for efficient governance and tracking of spent funds. + +A child-bounty is a smaller piece of work, extracted from a parent bounty. A curator is assigned after the child-bounty is created by the parent bounty curator, to be delegated with the responsibility of assigning a payout address once the specified set of tasks is completed. + +## Interface + +### Dispatchable Functions + +- `add_child_bounty` - Add a child-bounty for a parent-bounty to for dividing the work in smaller tasks. +- `propose_curator` - Assign an account to a child-bounty as candidate curator. +- `accept_curator` - Accept a child-bounty assignment from the parent-bounty curator, setting a curator deposit. +- `award_child_bounty` - Close and pay out the specified amount for the completed work. +- `claim_child_bounty` - Claim a specific child-bounty amount from the payout address. +- `unassign_curator` - Unassign an accepted curator from a specific child-bounty. +- `close_child_bounty` - Cancel the child-bounty for a specific treasury amount and close the bounty. diff --git a/frame/child-bounties/src/benchmarking.rs b/frame/child-bounties/src/benchmarking.rs new file mode 100644 index 0000000000000..3386245cbdca7 --- /dev/null +++ b/frame/child-bounties/src/benchmarking.rs @@ -0,0 +1,308 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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. + +//! Child-bounties pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; +use frame_system::RawOrigin; + +use crate::Pallet as ChildBounties; +use pallet_bounties::Pallet as Bounties; +use pallet_treasury::Pallet as Treasury; + +const SEED: u32 = 0; + +#[derive(Clone)] +struct BenchmarkChildBounty { + /// Bounty ID. + bounty_id: BountyIndex, + /// ChildBounty ID. + child_bounty_id: BountyIndex, + /// The account proposing it. + caller: T::AccountId, + /// The master curator account. + curator: T::AccountId, + /// The child-bounty curator account. + child_curator: T::AccountId, + /// The (total) amount that should be paid if the bounty is rewarded. + value: BalanceOf, + /// The curator fee. included in value. + fee: BalanceOf, + /// The (total) amount that should be paid if the child-bounty is rewarded. + child_bounty_value: BalanceOf, + /// The child-bounty curator fee. included in value. + child_bounty_fee: BalanceOf, + /// Bounty description. + reason: Vec, +} + +fn setup_bounty( + user: u32, + description: u32, +) -> (T::AccountId, T::AccountId, BalanceOf, BalanceOf, Vec) { + let caller = account("caller", user, SEED); + let value: BalanceOf = T::BountyValueMinimum::get().saturating_mul(100u32.into()); + let fee = value / 2u32.into(); + let deposit = T::BountyDepositBase::get() + + T::DataDepositPerByte::get() * T::MaximumReasonLength::get().into(); + let _ = T::Currency::make_free_balance_be(&caller, deposit); + let curator = account("curator", user, SEED); + let _ = T::Currency::make_free_balance_be(&curator, fee / 2u32.into()); + let reason = vec![0; description as usize]; + (caller, curator, fee, value, reason) +} + +fn setup_child_bounty(user: u32, description: u32) -> BenchmarkChildBounty { + let (caller, curator, fee, value, reason) = setup_bounty::(user, description); + let child_curator = account("child-curator", user, SEED); + let _ = T::Currency::make_free_balance_be(&child_curator, fee / 2u32.into()); + let child_bounty_value = (value - fee) / 4u32.into(); + let child_bounty_fee = child_bounty_value / 2u32.into(); + + BenchmarkChildBounty:: { + bounty_id: 0, + child_bounty_id: 0, + caller, + curator, + child_curator, + value, + fee, + child_bounty_value, + child_bounty_fee, + reason, + } +} + +fn activate_bounty( + user: u32, + description: u32, +) -> Result, &'static str> { + let mut child_bounty_setup = setup_child_bounty::(user, description); + let curator_lookup = T::Lookup::unlookup(child_bounty_setup.curator.clone()); + Bounties::::propose_bounty( + RawOrigin::Signed(child_bounty_setup.caller.clone()).into(), + child_bounty_setup.value, + child_bounty_setup.reason.clone(), + )?; + + child_bounty_setup.bounty_id = Bounties::::bounty_count() - 1; + + Bounties::::approve_bounty(RawOrigin::Root.into(), child_bounty_setup.bounty_id)?; + Treasury::::on_initialize(T::BlockNumber::zero()); + Bounties::::propose_curator( + RawOrigin::Root.into(), + child_bounty_setup.bounty_id, + curator_lookup.clone(), + child_bounty_setup.fee, + )?; + Bounties::::accept_curator( + RawOrigin::Signed(child_bounty_setup.curator.clone()).into(), + child_bounty_setup.bounty_id, + )?; + + Ok(child_bounty_setup) +} + +fn activate_child_bounty( + user: u32, + description: u32, +) -> Result, &'static str> { + let mut bounty_setup = activate_bounty::(user, description)?; + let child_curator_lookup = T::Lookup::unlookup(bounty_setup.child_curator.clone()); + + ChildBounties::::add_child_bounty( + RawOrigin::Signed(bounty_setup.curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_value, + bounty_setup.reason.clone(), + )?; + + bounty_setup.child_bounty_id = ChildBountyCount::::get() - 1; + + ChildBounties::::propose_curator( + RawOrigin::Signed(bounty_setup.curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_id, + child_curator_lookup.clone(), + bounty_setup.child_bounty_fee, + )?; + + ChildBounties::::accept_curator( + RawOrigin::Signed(bounty_setup.child_curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_id, + )?; + + Ok(bounty_setup) +} + +fn setup_pot_account() { + let pot_account = Bounties::::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(generic_event: ::Event) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +benchmarks! { + add_child_bounty { + let d in 0 .. T::MaximumReasonLength::get(); + setup_pot_account::(); + let bounty_setup = activate_bounty::(0, d)?; + }: _(RawOrigin::Signed(bounty_setup.curator), bounty_setup.bounty_id, + bounty_setup.child_bounty_value, bounty_setup.reason.clone()) + verify { + assert_last_event::(Event::Added { + index: bounty_setup.bounty_id, + child_index: bounty_setup.child_bounty_id, + }.into()) + } + + propose_curator { + setup_pot_account::(); + let bounty_setup = activate_bounty::(0, T::MaximumReasonLength::get())?; + let child_curator_lookup = T::Lookup::unlookup(bounty_setup.child_curator.clone()); + + ChildBounties::::add_child_bounty( + RawOrigin::Signed(bounty_setup.curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_value, + bounty_setup.reason.clone(), + )?; + let child_bounty_id = ChildBountyCount::::get() - 1; + + }: _(RawOrigin::Signed(bounty_setup.curator), bounty_setup.bounty_id, + child_bounty_id, child_curator_lookup, bounty_setup.child_bounty_fee) + + accept_curator { + setup_pot_account::(); + let mut bounty_setup = activate_bounty::(0, T::MaximumReasonLength::get())?; + let child_curator_lookup = T::Lookup::unlookup(bounty_setup.child_curator.clone()); + + ChildBounties::::add_child_bounty( + RawOrigin::Signed(bounty_setup.curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_value, + bounty_setup.reason.clone(), + )?; + bounty_setup.child_bounty_id = ChildBountyCount::::get() - 1; + + ChildBounties::::propose_curator( + RawOrigin::Signed(bounty_setup.curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_id, + child_curator_lookup.clone(), + bounty_setup.child_bounty_fee, + )?; + }: _(RawOrigin::Signed(bounty_setup.child_curator), bounty_setup.bounty_id, + bounty_setup.child_bounty_id) + + // Worst case when curator is inactive and any sender un-assigns the curator. + unassign_curator { + setup_pot_account::(); + let bounty_setup = activate_child_bounty::(0, T::MaximumReasonLength::get())?; + Bounties::::on_initialize(T::BlockNumber::zero()); + frame_system::Pallet::::set_block_number(T::BountyUpdatePeriod::get() + 1u32.into()); + let caller = whitelisted_caller(); + }: _(RawOrigin::Signed(caller), bounty_setup.bounty_id, + bounty_setup.child_bounty_id) + + award_child_bounty { + setup_pot_account::(); + let bounty_setup = activate_child_bounty::(0, T::MaximumReasonLength::get())?; + let beneficiary_account: T::AccountId = account("beneficiary", 0, SEED); + let beneficiary = T::Lookup::unlookup(beneficiary_account.clone()); + }: _(RawOrigin::Signed(bounty_setup.child_curator), bounty_setup.bounty_id, + bounty_setup.child_bounty_id, beneficiary) + verify { + assert_last_event::(Event::Awarded { + index: bounty_setup.bounty_id, + child_index: bounty_setup.child_bounty_id, + beneficiary: beneficiary_account + }.into()) + } + + claim_child_bounty { + setup_pot_account::(); + let bounty_setup = activate_child_bounty::(0, T::MaximumReasonLength::get())?; + let beneficiary_account: T::AccountId = account("beneficiary", 0, SEED); + let beneficiary = T::Lookup::unlookup(beneficiary_account.clone()); + + ChildBounties::::award_child_bounty( + RawOrigin::Signed(bounty_setup.child_curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_id, + beneficiary + )?; + + let beneficiary_account: T::AccountId = account("beneficiary", 0, SEED); + let beneficiary = T::Lookup::unlookup(beneficiary_account.clone()); + + frame_system::Pallet::::set_block_number(T::BountyDepositPayoutDelay::get()); + ensure!(T::Currency::free_balance(&beneficiary_account).is_zero(), + "Beneficiary already has balance."); + + }: _(RawOrigin::Signed(bounty_setup.curator), bounty_setup.bounty_id, + bounty_setup.child_bounty_id) + verify { + ensure!(!T::Currency::free_balance(&beneficiary_account).is_zero(), + "Beneficiary didn't get paid."); + } + + // Best case scenario. + close_child_bounty_added { + setup_pot_account::(); + let mut bounty_setup = activate_bounty::(0, T::MaximumReasonLength::get())?; + + ChildBounties::::add_child_bounty( + RawOrigin::Signed(bounty_setup.curator.clone()).into(), + bounty_setup.bounty_id, + bounty_setup.child_bounty_value, + bounty_setup.reason.clone(), + )?; + bounty_setup.child_bounty_id = ChildBountyCount::::get() - 1; + + }: close_child_bounty(RawOrigin::Root, bounty_setup.bounty_id, + bounty_setup.child_bounty_id) + verify { + assert_last_event::(Event::Canceled { + index: bounty_setup.bounty_id, + child_index: bounty_setup.child_bounty_id + }.into()) + } + + // Worst case scenario. + close_child_bounty_active { + setup_pot_account::(); + let bounty_setup = activate_child_bounty::(0, T::MaximumReasonLength::get())?; + Bounties::::on_initialize(T::BlockNumber::zero()); + }: close_child_bounty(RawOrigin::Root, bounty_setup.bounty_id, bounty_setup.child_bounty_id) + verify { + assert_last_event::(Event::Canceled { + index: bounty_setup.bounty_id, + child_index: bounty_setup.child_bounty_id, + }.into()) + } + + impl_benchmark_test_suite!(ChildBounties, crate::tests::new_test_ext(), crate::tests::Test) +} diff --git a/frame/child-bounties/src/lib.rs b/frame/child-bounties/src/lib.rs new file mode 100644 index 0000000000000..3aa160277c82b --- /dev/null +++ b/frame/child-bounties/src/lib.rs @@ -0,0 +1,897 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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. + +//! # Child Bounties Pallet ( pallet-child-bounties ) +//! +//! ## Child Bounty +//! +//! > NOTE: This pallet is tightly coupled with pallet-treasury and pallet-bounties. +//! +//! With child bounties, a large bounty proposal can be divided into smaller chunks, +//! for parallel execution, and for efficient governance and tracking of spent funds. +//! A child-bounty is a smaller piece of work, extracted from a parent bounty. +//! A curator is assigned after the child-bounty is created by the parent bounty curator, +//! to be delegated with the responsibility of assigning a payout address once the specified +//! set of tasks is completed. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! Child Bounty protocol: +//! - `add_child_bounty` - Add a child-bounty for a parent-bounty to for dividing the work in +//! smaller tasks. +//! - `propose_curator` - Assign an account to a child-bounty as candidate curator. +//! - `accept_curator` - Accept a child-bounty assignment from the parent-bounty curator, setting a +//! curator deposit. +//! - `award_child_bounty` - Close and pay out the specified amount for the completed work. +//! - `claim_child_bounty` - Claim a specific child-bounty amount from the payout address. +//! - `unassign_curator` - Unassign an accepted curator from a specific child-bounty. +//! - `close_child_bounty` - Cancel the child-bounty for a specific treasury amount and close the +//! bounty. + +// Most of the business logic in this pallet has been +// originally contributed by "https://github.com/shamb0", +// as part of the PR - https://github.com/paritytech/substrate/pull/7965. +// The code has been moved here and then refactored in order to +// extract child-bounties as a separate pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +mod tests; +pub mod weights; + +use sp_std::prelude::*; + +use frame_support::traits::{ + Currency, + ExistenceRequirement::{AllowDeath, KeepAlive}, + Get, OnUnbalanced, ReservableCurrency, WithdrawReasons, +}; + +use sp_runtime::{ + traits::{AccountIdConversion, BadOrigin, CheckedSub, Saturating, StaticLookup, Zero}, + DispatchResult, Permill, RuntimeDebug, +}; + +use frame_support::pallet_prelude::*; +use frame_system::pallet_prelude::*; +use pallet_bounties::BountyStatus; +use scale_info::TypeInfo; +pub use weights::WeightInfo; + +pub use pallet::*; + +type BalanceOf = pallet_treasury::BalanceOf; +type BountiesError = pallet_bounties::Error; +type BountyIndex = pallet_bounties::BountyIndex; + +/// A child bounty proposal. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ChildBounty { + /// The parent of this child-bounty. + parent_bounty: BountyIndex, + /// The (total) amount that should be paid if this child-bounty is rewarded. + value: Balance, + /// The child bounty curator fee. + fee: Balance, + /// The deposit of child-bounty curator. + curator_deposit: Balance, + /// The status of this child-bounty. + status: ChildBountyStatus, +} + +/// The status of a child-bounty. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum ChildBountyStatus { + /// The child-bounty is added and waiting for curator assignment. + Added, + /// A curator has been proposed by the parent-bounty curator. Waiting for + /// acceptance from the child-bounty curator. + CuratorProposed { + /// The assigned child-bounty curator of this bounty. + curator: AccountId, + }, + /// The child-bounty is active and waiting to be awarded. + Active { + /// The curator of this child-bounty. + curator: AccountId, + }, + /// The child-bounty is awarded and waiting to released after a delay. + PendingPayout { + /// The curator of this child-bounty. + curator: AccountId, + /// The beneficiary of the child-bounty. + beneficiary: AccountId, + /// When the child-bounty can be claimed. + unlock_at: BlockNumber, + }, +} + +#[frame_support::pallet] +pub mod pallet { + + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::generate_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_treasury::Config + pallet_bounties::Config + { + /// Maximum number of child-bounties that can be added to a parent bounty. + #[pallet::constant] + type MaxActiveChildBountyCount: Get; + + /// Minimum value for a child-bounty. + #[pallet::constant] + type ChildBountyValueMinimum: Get>; + + /// Percentage of child-bounty value to be reserved as curator deposit + /// when curator fee is zero. + #[pallet::constant] + type ChildBountyCuratorDepositBase: Get; + + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::error] + pub enum Error { + /// The parent bounty is not in active state. + ParentBountyNotActive, + /// The bounty balance is not enough to add new child-bounty. + InsufficientBountyBalance, + /// Number of child-bounties exceeds limit `MaxActiveChildBountyCount`. + TooManyChildBounties, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A child-bounty is added. + Added { index: BountyIndex, child_index: BountyIndex }, + /// A child-bounty is awarded to a beneficiary. + Awarded { index: BountyIndex, child_index: BountyIndex, beneficiary: T::AccountId }, + /// A child-bounty is claimed by beneficiary. + Claimed { + index: BountyIndex, + child_index: BountyIndex, + payout: BalanceOf, + beneficiary: T::AccountId, + }, + /// A child-bounty is cancelled. + Canceled { index: BountyIndex, child_index: BountyIndex }, + } + + /// Number of total child bounties. + #[pallet::storage] + #[pallet::getter(fn child_bounty_count)] + pub type ChildBountyCount = StorageValue<_, BountyIndex, ValueQuery>; + + /// Number of child-bounties per parent bounty. + /// Map of parent bounty index to number of child bounties. + #[pallet::storage] + #[pallet::getter(fn parent_child_bounties)] + pub type ParentChildBounties = + StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>; + + /// Child-bounties that have been added. + #[pallet::storage] + #[pallet::getter(fn child_bounties)] + pub type ChildBounties = StorageDoubleMap< + _, + Twox64Concat, + BountyIndex, + Twox64Concat, + BountyIndex, + ChildBounty, T::BlockNumber>, + >; + + /// The description of each child-bounty. + #[pallet::storage] + #[pallet::getter(fn child_bounty_descriptions)] + pub type ChildBountyDescriptions = + StorageMap<_, Twox64Concat, BountyIndex, BoundedVec>; + + /// The cumulative child-bounty curator fee for each parent bounty. + #[pallet::storage] + #[pallet::getter(fn children_curator_fees)] + pub type ChildrenCuratorFees = + StorageMap<_, Twox64Concat, BountyIndex, BalanceOf, ValueQuery>; + + #[pallet::call] + impl Pallet { + /// Add a new child-bounty. + /// + /// The dispatch origin for this call must be the curator of parent + /// bounty and the parent bounty must be in "active" state. + /// + /// Child-bounty gets added successfully & fund gets transferred from + /// parent bounty to child-bounty account, if parent bounty has enough + /// funds, else the call fails. + /// + /// Upper bound to maximum number of active child-bounties that can be + /// added are managed via runtime trait config + /// [`Config::MaxActiveChildBountyCount`]. + /// + /// If the call is success, the status of child-bounty is updated to + /// "Added". + /// + /// - `parent_bounty_id`: Index of parent bounty for which child-bounty is being added. + /// - `value`: Value for executing the proposal. + /// - `description`: Text description for the child-bounty. + #[pallet::weight(::WeightInfo::add_child_bounty(description.len() as u32))] + pub fn add_child_bounty( + origin: OriginFor, + #[pallet::compact] parent_bounty_id: BountyIndex, + #[pallet::compact] value: BalanceOf, + description: Vec, + ) -> DispatchResult { + let signer = ensure_signed(origin)?; + + // Verify the arguments. + let bounded_description = + description.try_into().map_err(|_| BountiesError::::ReasonTooBig)?; + ensure!(value >= T::ChildBountyValueMinimum::get(), BountiesError::::InvalidValue); + ensure!( + Self::parent_child_bounties(parent_bounty_id) <= + T::MaxActiveChildBountyCount::get() as u32, + Error::::TooManyChildBounties, + ); + + let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?; + ensure!(signer == curator, BountiesError::::RequireCurator); + + // Read parent bounty account info. + let parent_bounty_account = + pallet_bounties::Pallet::::bounty_account_id(parent_bounty_id); + + // Ensure parent bounty has enough balance after adding child-bounty. + let bounty_balance = T::Currency::free_balance(&parent_bounty_account); + let new_bounty_balance = bounty_balance + .checked_sub(&value) + .ok_or(Error::::InsufficientBountyBalance)?; + T::Currency::ensure_can_withdraw( + &parent_bounty_account, + value, + WithdrawReasons::TRANSFER, + new_bounty_balance, + )?; + + // Get child-bounty ID. + let child_bounty_id = Self::child_bounty_count(); + let child_bounty_account = Self::child_bounty_account_id(child_bounty_id); + + // Transfer funds from parent bounty to child-bounty. + T::Currency::transfer(&parent_bounty_account, &child_bounty_account, value, KeepAlive)?; + + // Increment the active child-bounty count. + >::mutate(parent_bounty_id, |count| count.saturating_inc()); + >::put(child_bounty_id.saturating_add(1)); + + // Create child-bounty instance. + Self::create_child_bounty( + parent_bounty_id, + child_bounty_id, + value, + bounded_description, + ); + Ok(()) + } + + /// Propose curator for funded child-bounty. + /// + /// The dispatch origin for this call must be curator of parent bounty. + /// + /// Parent bounty must be in active state, for this child-bounty call to + /// work. + /// + /// Child-bounty must be in "Added" state, for processing the call. And + /// state of child-bounty is moved to "CuratorProposed" on successful + /// call completion. + /// + /// - `parent_bounty_id`: Index of parent bounty. + /// - `child_bounty_id`: Index of child bounty. + /// - `curator`: Address of child-bounty curator. + /// - `fee`: payment fee to child-bounty curator for execution. + #[pallet::weight(::WeightInfo::propose_curator())] + pub fn propose_curator( + origin: OriginFor, + #[pallet::compact] parent_bounty_id: BountyIndex, + #[pallet::compact] child_bounty_id: BountyIndex, + curator: ::Source, + #[pallet::compact] fee: BalanceOf, + ) -> DispatchResult { + let signer = ensure_signed(origin)?; + let child_bounty_curator = T::Lookup::lookup(curator)?; + + let (curator, _) = Self::ensure_bounty_active(parent_bounty_id)?; + ensure!(signer == curator, BountiesError::::RequireCurator); + + // Mutate the child-bounty instance. + ChildBounties::::try_mutate_exists( + parent_bounty_id, + child_bounty_id, + |maybe_child_bounty| -> DispatchResult { + let mut child_bounty = + maybe_child_bounty.as_mut().ok_or(BountiesError::::InvalidIndex)?; + + // Ensure child-bounty is in expected state. + ensure!( + child_bounty.status == ChildBountyStatus::Added, + BountiesError::::UnexpectedStatus, + ); + + // Ensure child-bounty curator fee is less than child-bounty value. + ensure!(fee < child_bounty.value, BountiesError::::InvalidFee); + + // Add child-bounty curator fee to the cumulative sum. To be + // subtracted from the parent bounty curator when claiming + // bounty. + ChildrenCuratorFees::::mutate(parent_bounty_id, |value| { + *value = value.saturating_add(fee) + }); + + // Update the child-bounty curator fee. + child_bounty.fee = fee; + + // Update the child-bounty state. + child_bounty.status = + ChildBountyStatus::CuratorProposed { curator: child_bounty_curator }; + + Ok(()) + }, + ) + } + + /// Accept the curator role for the child-bounty. + /// + /// The dispatch origin for this call must be the curator of this + /// child-bounty. + /// + /// A deposit will be reserved from the curator and refund upon + /// successful payout or cancellation. + /// + /// Fee for curator is deducted from curator fee of parent bounty. + /// + /// Parent bounty must be in active state, for this child-bounty call to + /// work. + /// + /// Child-bounty must be in "CuratorProposed" state, for processing the + /// call. And state of child-bounty is moved to "Active" on successful + /// call completion. + /// + /// - `parent_bounty_id`: Index of parent bounty. + /// - `child_bounty_id`: Index of child bounty. + #[pallet::weight(::WeightInfo::accept_curator())] + pub fn accept_curator( + origin: OriginFor, + #[pallet::compact] parent_bounty_id: BountyIndex, + #[pallet::compact] child_bounty_id: BountyIndex, + ) -> DispatchResult { + let signer = ensure_signed(origin)?; + + let _ = Self::ensure_bounty_active(parent_bounty_id)?; + // Mutate child-bounty. + ChildBounties::::try_mutate_exists( + parent_bounty_id, + child_bounty_id, + |maybe_child_bounty| -> DispatchResult { + let mut child_bounty = + maybe_child_bounty.as_mut().ok_or(BountiesError::::InvalidIndex)?; + + // Ensure child-bounty is in expected state. + if let ChildBountyStatus::CuratorProposed { ref curator } = child_bounty.status + { + ensure!(signer == *curator, BountiesError::::RequireCurator); + + // Reserve child-bounty curator deposit. Curator deposit + // is reserved based on a percentage of child-bounty + // value instead of fee, to avoid no deposit in case the + // fee is set as zero. + let deposit = T::ChildBountyCuratorDepositBase::get() * child_bounty.value; + T::Currency::reserve(curator, deposit)?; + child_bounty.curator_deposit = deposit; + + child_bounty.status = + ChildBountyStatus::Active { curator: curator.clone() }; + Ok(()) + } else { + Err(BountiesError::::UnexpectedStatus.into()) + } + }, + ) + } + + /// Unassign curator from a child-bounty. + /// + /// The dispatch origin for this call can be either `RejectOrigin`, or + /// the curator of the parent bounty, or any signed origin. + /// + /// For the origin other than T::RejectOrigin and the child-bounty + /// curator, parent-bounty must be in active state, for this call to + /// work. We allow child-bounty curator and T::RejectOrigin to execute + /// this call irrespective of the parent-bounty state. + /// + /// If this function is called by the `RejectOrigin` or the + /// parent-bounty curator, we assume that the child-bounty curator is + /// malicious or inactive. As a result, child-bounty curator deposit is + /// slashed. + /// + /// If the origin is the child-bounty curator, we take this as a sign + /// that they are unable to do their job, and are willingly giving up. + /// We could slash the deposit, but for now we allow them to unreserve + /// their deposit and exit without issue. (We may want to change this if + /// it is abused.) + /// + /// Finally, the origin can be anyone iff the child-bounty curator is + /// "inactive". Expiry update due of parent bounty is used to estimate + /// inactive state of child-bounty curator. + /// + /// This allows anyone in the community to call out that a child-bounty + /// curator is not doing their due diligence, and we should pick a new + /// one. In this case the child-bounty curator deposit is slashed. + /// + /// State of child-bounty is moved to Added state on successful call + /// completion. + /// + /// - `parent_bounty_id`: Index of parent bounty. + /// - `child_bounty_id`: Index of child bounty. + #[pallet::weight(::WeightInfo::unassign_curator())] + pub fn unassign_curator( + origin: OriginFor, + #[pallet::compact] parent_bounty_id: BountyIndex, + #[pallet::compact] child_bounty_id: BountyIndex, + ) -> DispatchResult { + let maybe_sender = ensure_signed(origin.clone()) + .map(Some) + .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?; + + ChildBounties::::try_mutate_exists( + parent_bounty_id, + child_bounty_id, + |maybe_child_bounty| -> DispatchResult { + let mut child_bounty = + maybe_child_bounty.as_mut().ok_or(BountiesError::::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 child_bounty.status { + ChildBountyStatus::Added => { + // No curator to unassign at this point. + return Err(BountiesError::::UnexpectedStatus.into()) + }, + ChildBountyStatus::CuratorProposed { ref curator } => { + // A child-bounty curator has been proposed, but not accepted yet. + // Either `RejectOrigin`, parent-bounty curator or the proposed + // child-bounty curator can unassign the child-bounty curator. + ensure!( + maybe_sender.map_or(true, |sender| { + sender == *curator || + Self::ensure_bounty_active(parent_bounty_id) + .map_or(false, |(parent_curator, _)| { + sender == parent_curator + }) + }), + BadOrigin + ); + // Continue to change bounty status below. + }, + ChildBountyStatus::Active { ref curator } => { + // The child-bounty is active. + match maybe_sender { + // If the `RejectOrigin` is calling this function, slash the curator + // deposit. + None => { + slash_curator(curator, &mut child_bounty.curator_deposit); + // Continue to change child-bounty status below. + }, + Some(sender) if sender == *curator => { + // This is the child-bounty curator, willingly giving up their + // role. Give back their deposit. + T::Currency::unreserve(&curator, child_bounty.curator_deposit); + // Reset curator deposit. + child_bounty.curator_deposit = Zero::zero(); + // Continue to change bounty status below. + }, + Some(sender) => { + let (parent_curator, update_due) = + Self::ensure_bounty_active(parent_bounty_id)?; + if sender == parent_curator || + update_due < frame_system::Pallet::::block_number() + { + // Slash the child-bounty curator if + // + the call is made by the parent-bounty curator. + // + or the curator is inactive. + slash_curator(curator, &mut child_bounty.curator_deposit); + // Continue to change bounty status below. + } else { + // Curator has more time to give an update. + return Err(BountiesError::::Premature.into()) + } + }, + } + }, + ChildBountyStatus::PendingPayout { ref curator, .. } => { + let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?; + ensure!( + maybe_sender.map_or(true, |sender| parent_curator == sender), + BadOrigin, + ); + slash_curator(curator, &mut child_bounty.curator_deposit); + // Continue to change child-bounty status below. + }, + }; + // Move the child-bounty state to Added. + child_bounty.status = ChildBountyStatus::Added; + Ok(()) + }, + ) + } + + /// Award child-bounty to a beneficiary. + /// + /// The beneficiary will be able to claim the funds after a delay. + /// + /// The dispatch origin for this call must be the master curator or + /// curator of this child-bounty. + /// + /// Parent bounty must be in active state, for this child-bounty call to + /// work. + /// + /// Child-bounty must be in active state, for processing the call. And + /// state of child-bounty is moved to "PendingPayout" on successful call + /// completion. + /// + /// - `parent_bounty_id`: Index of parent bounty. + /// - `child_bounty_id`: Index of child bounty. + /// - `beneficiary`: Beneficiary account. + #[pallet::weight(::WeightInfo::award_child_bounty())] + pub fn award_child_bounty( + origin: OriginFor, + #[pallet::compact] parent_bounty_id: BountyIndex, + #[pallet::compact] child_bounty_id: BountyIndex, + beneficiary: ::Source, + ) -> DispatchResult { + let signer = ensure_signed(origin)?; + let beneficiary = T::Lookup::lookup(beneficiary)?; + + // Ensure parent bounty exists, and is active. + let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?; + + ChildBounties::::try_mutate_exists( + parent_bounty_id, + child_bounty_id, + |maybe_child_bounty| -> DispatchResult { + let mut child_bounty = + maybe_child_bounty.as_mut().ok_or(BountiesError::::InvalidIndex)?; + + // Ensure child-bounty is in active state. + if let ChildBountyStatus::Active { ref curator } = child_bounty.status { + ensure!( + signer == *curator || signer == parent_curator, + BountiesError::::RequireCurator, + ); + // Move the child-bounty state to pending payout. + child_bounty.status = ChildBountyStatus::PendingPayout { + curator: signer, + beneficiary: beneficiary.clone(), + unlock_at: frame_system::Pallet::::block_number() + + T::BountyDepositPayoutDelay::get(), + }; + Ok(()) + } else { + Err(BountiesError::::UnexpectedStatus.into()) + } + }, + )?; + + // Trigger the event Awarded. + Self::deposit_event(Event::::Awarded { + index: parent_bounty_id, + child_index: child_bounty_id, + beneficiary, + }); + + Ok(()) + } + + /// Claim the payout from an awarded child-bounty after payout delay. + /// + /// The dispatch origin for this call may be any signed origin. + /// + /// Call works independent of parent bounty state, No need for parent + /// bounty to be in active state. + /// + /// The Beneficiary is paid out with agreed bounty value. Curator fee is + /// paid & curator deposit is unreserved. + /// + /// Child-bounty must be in "PendingPayout" state, for processing the + /// call. And instance of child-bounty is removed from the state on + /// successful call completion. + /// + /// - `parent_bounty_id`: Index of parent bounty. + /// - `child_bounty_id`: Index of child bounty. + #[pallet::weight(::WeightInfo::claim_child_bounty())] + pub fn claim_child_bounty( + origin: OriginFor, + #[pallet::compact] parent_bounty_id: BountyIndex, + #[pallet::compact] child_bounty_id: BountyIndex, + ) -> DispatchResult { + let _ = ensure_signed(origin)?; + + // Ensure child-bounty is in expected state. + ChildBounties::::try_mutate_exists( + parent_bounty_id, + child_bounty_id, + |maybe_child_bounty| -> DispatchResult { + let child_bounty = + maybe_child_bounty.as_mut().ok_or(BountiesError::::InvalidIndex)?; + + if let ChildBountyStatus::PendingPayout { + ref curator, + ref beneficiary, + ref unlock_at, + } = child_bounty.status + { + // Ensure block number is elapsed for processing the + // claim. + ensure!( + frame_system::Pallet::::block_number() >= *unlock_at, + BountiesError::::Premature, + ); + + // Make curator fee payment. + let child_bounty_account = Self::child_bounty_account_id(child_bounty_id); + let balance = T::Currency::free_balance(&child_bounty_account); + let curator_fee = child_bounty.fee.min(balance); + let payout = balance.saturating_sub(curator_fee); + + // Unreserve the curator deposit. Should not fail + // because the deposit is always reserved when curator is + // assigned. + let _ = T::Currency::unreserve(&curator, child_bounty.curator_deposit); + + // Make payout to child-bounty curator. + // Should not fail because curator fee is always less than bounty value. + let fee_transfer_result = T::Currency::transfer( + &child_bounty_account, + &curator, + curator_fee, + AllowDeath, + ); + debug_assert!(fee_transfer_result.is_ok()); + + // Make payout to beneficiary. + // Should not fail. + let payout_transfer_result = T::Currency::transfer( + &child_bounty_account, + beneficiary, + payout, + AllowDeath, + ); + debug_assert!(payout_transfer_result.is_ok()); + + // Trigger the Claimed event. + Self::deposit_event(Event::::Claimed { + index: parent_bounty_id, + child_index: child_bounty_id, + payout, + beneficiary: beneficiary.clone(), + }); + + // Update the active child-bounty tracking count. + >::mutate(parent_bounty_id, |count| { + count.saturating_dec() + }); + + // Remove the child-bounty description. + >::remove(child_bounty_id); + + // Remove the child-bounty instance from the state. + *maybe_child_bounty = None; + + Ok(()) + } else { + Err(BountiesError::::UnexpectedStatus.into()) + } + }, + ) + } + + /// Cancel a proposed or active child-bounty. Child-bounty account funds + /// are transferred to parent bounty account. The child-bounty curator + /// deposit may be unreserved if possible. + /// + /// The dispatch origin for this call must be either parent curator or + /// `T::RejectOrigin`. + /// + /// If the state of child-bounty is `Active`, curator deposit is + /// unreserved. + /// + /// If the state of child-bounty is `PendingPayout`, call fails & + /// returns `PendingPayout` error. + /// + /// For the origin other than T::RejectOrigin, parent bounty must be in + /// active state, for this child-bounty call to work. For origin + /// T::RejectOrigin execution is forced. + /// + /// Instance of child-bounty is removed from the state on successful + /// call completion. + /// + /// - `parent_bounty_id`: Index of parent bounty. + /// - `child_bounty_id`: Index of child bounty. + #[pallet::weight(::WeightInfo::close_child_bounty_added() + .max(::WeightInfo::close_child_bounty_active()))] + pub fn close_child_bounty( + origin: OriginFor, + #[pallet::compact] parent_bounty_id: BountyIndex, + #[pallet::compact] child_bounty_id: BountyIndex, + ) -> DispatchResult { + let maybe_sender = ensure_signed(origin.clone()) + .map(Some) + .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?; + + // Ensure parent bounty exist, get parent curator. + let (parent_curator, _) = Self::ensure_bounty_active(parent_bounty_id)?; + + ensure!(maybe_sender.map_or(true, |sender| parent_curator == sender), BadOrigin); + + Self::impl_close_child_bounty(parent_bounty_id, child_bounty_id)?; + Ok(()) + } + } +} + +impl Pallet { + /// The account ID of a child-bounty account. + pub fn child_bounty_account_id(id: BountyIndex) -> T::AccountId { + // This function is taken from the parent (bounties) pallet, but the + // prefix is changed to have different AccountId when the index of + // parent and child is same. + T::PalletId::get().into_sub_account(("cb", id)) + } + + fn create_child_bounty( + parent_bounty_id: BountyIndex, + child_bounty_id: BountyIndex, + child_bounty_value: BalanceOf, + description: BoundedVec, + ) { + let child_bounty = ChildBounty { + parent_bounty: parent_bounty_id, + value: child_bounty_value, + fee: 0u32.into(), + curator_deposit: 0u32.into(), + status: ChildBountyStatus::Added, + }; + ChildBounties::::insert(parent_bounty_id, child_bounty_id, &child_bounty); + ChildBountyDescriptions::::insert(child_bounty_id, description); + Self::deposit_event(Event::Added { index: parent_bounty_id, child_index: child_bounty_id }); + } + + fn ensure_bounty_active( + bounty_id: BountyIndex, + ) -> Result<(T::AccountId, T::BlockNumber), DispatchError> { + let parent_bounty = pallet_bounties::Pallet::::bounties(bounty_id) + .ok_or(BountiesError::::InvalidIndex)?; + if let BountyStatus::Active { curator, update_due } = parent_bounty.get_status() { + Ok((curator, update_due)) + } else { + Err(Error::::ParentBountyNotActive.into()) + } + } + + fn impl_close_child_bounty( + parent_bounty_id: BountyIndex, + child_bounty_id: BountyIndex, + ) -> DispatchResult { + ChildBounties::::try_mutate_exists( + parent_bounty_id, + child_bounty_id, + |maybe_child_bounty| -> DispatchResult { + let child_bounty = + maybe_child_bounty.as_mut().ok_or(BountiesError::::InvalidIndex)?; + + match &child_bounty.status { + ChildBountyStatus::Added | ChildBountyStatus::CuratorProposed { .. } => { + // Nothing extra to do besides the removal of the child-bounty below. + }, + ChildBountyStatus::Active { curator } => { + // Cancelled by master curator or RejectOrigin, + // refund deposit of the working child-bounty curator. + let _ = T::Currency::unreserve(curator, child_bounty.curator_deposit); + // Then execute removal of the child-bounty below. + }, + ChildBountyStatus::PendingPayout { .. } => { + // Child-bounty is already in pending payout. If parent + // curator or RejectOrigin wants to close this + // child-bounty, it should mean the child-bounty curator + // was acting maliciously. So first unassign the + // child-bounty curator, slashing their deposit. + return Err(BountiesError::::PendingPayout.into()) + }, + } + + // Revert the curator fee back to parent-bounty curator & + // reduce the active child-bounty count. + ChildrenCuratorFees::::mutate(parent_bounty_id, |value| { + *value = value.saturating_sub(child_bounty.fee) + }); + >::mutate(parent_bounty_id, |count| { + *count = count.saturating_sub(1) + }); + + // Transfer fund from child-bounty to parent bounty. + let parent_bounty_account = + pallet_bounties::Pallet::::bounty_account_id(parent_bounty_id); + let child_bounty_account = Self::child_bounty_account_id(child_bounty_id); + let balance = T::Currency::free_balance(&child_bounty_account); + let transfer_result = T::Currency::transfer( + &child_bounty_account, + &parent_bounty_account, + balance, + AllowDeath, + ); // Should not fail; child bounty account gets this balance during creation. + debug_assert!(transfer_result.is_ok()); + + // Remove the child-bounty description. + >::remove(child_bounty_id); + + *maybe_child_bounty = None; + + Self::deposit_event(Event::::Canceled { + index: parent_bounty_id, + child_index: child_bounty_id, + }); + Ok(()) + }, + ) + } +} + +// Implement ChildBountyManager to connect with the bounties pallet. This is +// where we pass the active child-bounties and child curator fees to the parent +// bounty. +impl pallet_bounties::ChildBountyManager> for Pallet { + fn child_bounties_count( + bounty_id: pallet_bounties::BountyIndex, + ) -> pallet_bounties::BountyIndex { + Self::parent_child_bounties(bounty_id) + } + + fn children_curator_fees(bounty_id: pallet_bounties::BountyIndex) -> BalanceOf { + // This is asked for when the parent bounty is being claimed. No use of + // keeping it in state after that. Hence removing. + let children_fee_total = Self::children_curator_fees(bounty_id); + >::remove(bounty_id); + children_fee_total + } +} diff --git a/frame/child-bounties/src/tests.rs b/frame/child-bounties/src/tests.rs new file mode 100644 index 0000000000000..ada13e968c4d4 --- /dev/null +++ b/frame/child-bounties/src/tests.rs @@ -0,0 +1,1219 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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. + +//! Child-bounties pallet tests. + +#![cfg(test)] + +use super::*; +use crate as pallet_child_bounties; +use std::cell::RefCell; + +use frame_support::{ + assert_noop, assert_ok, pallet_prelude::GenesisBuild, parameter_types, traits::OnInitialize, + weights::Weight, PalletId, +}; + +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BadOrigin, BlakeTwo256, IdentityLookup}, + Perbill, Permill, +}; + +use super::Event as ChildBountiesEvent; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; +type BountiesError = pallet_bounties::Error; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Bounties: pallet_bounties::{Pallet, Call, Storage, Event}, + Treasury: pallet_treasury::{Pallet, Call, Storage, Config, Event}, + ChildBounties: pallet_child_bounties::{Pallet, Call, Storage, Event}, + } +); + +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 = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u128; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} +parameter_types! { + pub const ExistentialDeposit: u64 = 1; +} +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = u64; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); +} +thread_local! { + static TEN_TO_FOURTEEN: RefCell> = 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 TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub const MaxApprovals: u32 = 100; +} + +impl pallet_treasury::Config for Test { + type PalletId = TreasuryPalletId; + type Currency = pallet_balances::Pallet; + type ApproveOrigin = frame_system::EnsureRoot; + type RejectOrigin = frame_system::EnsureRoot; + type Event = Event; + type OnSlash = (); + type ProposalBond = ProposalBond; + type ProposalBondMinimum = ProposalBondMinimum; + type SpendPeriod = SpendPeriod; + type Burn = Burn; + type BurnDestination = (); + type WeightInfo = (); + type SpendFunds = Bounties; + type MaxApprovals = MaxApprovals; +} +parameter_types! { + pub const BountyDepositBase: u64 = 80; + pub const BountyDepositPayoutDelay: u64 = 3; + pub const BountyUpdatePeriod: u32 = 10; + pub const BountyCuratorDeposit: Permill = Permill::from_percent(50); + pub const BountyValueMinimum: u64 = 5; + pub const MaximumReasonLength: u32 = 300; +} +impl pallet_bounties::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 ChildBountyManager = ChildBounties; +} +parameter_types! { + pub const MaxActiveChildBountyCount: u32 = 2; + pub const ChildBountyValueMinimum: u64 = 1; + pub const ChildBountyCuratorDepositBase: Permill = Permill::from_percent(10); +} +impl pallet_child_bounties::Config for Test { + type Event = Event; + type MaxActiveChildBountyCount = MaxActiveChildBountyCount; + type ChildBountyValueMinimum = ChildBountyValueMinimum; + type ChildBountyCuratorDepositBase = ChildBountyCuratorDepositBase; + type WeightInfo = (); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + // Total issuance will be 200 with treasury account initialized at ED. + balances: vec![(0, 100), (1, 98), (2, 1)], + } + .assimilate_storage(&mut t) + .unwrap(); + GenesisBuild::::assimilate_storage(&pallet_treasury::GenesisConfig, &mut t).unwrap(); + t.into() +} + +fn last_event() -> ChildBountiesEvent { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let Event::ChildBounties(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 add_child_bounty() { + new_test_ext().execute_with(|| { + // TestProcedure. + // 1, Create bounty & move to active state with enough bounty fund & master-curator. + // 2, Master-curator adds child-bounty child-bounty-1, test for error like RequireCurator + // ,InsufficientProposersBalance, InsufficientBountyBalance with invalid arguments. + // 3, Master-curator adds child-bounty child-bounty-1, moves to "Approved" state & + // test for the event Added. + // 4, Test for DB state of `Bounties` & `ChildBounties`. + // 5, Observe fund transaction moment between Bounty, Child-bounty, + // Curator, child-bounty curator & beneficiary. + + // Make the parent bounty. + 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); + >::on_initialize(2); + + 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_eq!(Balances::free_balance(&4), 8); + assert_eq!(Balances::reserved_balance(&4), 2); + + // Add child-bounty. + // Acc-4 is the master curator. + // Call from invalid origin & check for error "RequireCurator". + assert_noop!( + ChildBounties::add_child_bounty(Origin::signed(0), 0, 10, b"12345-p1".to_vec()), + BountiesError::RequireCurator, + ); + + // Update the master curator balance. + Balances::make_free_balance_be(&4, 101); + + // Master curator fee is reserved on parent bounty account. + assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 50); + assert_eq!(Balances::reserved_balance(Bounties::bounty_account_id(0)), 0); + + assert_noop!( + ChildBounties::add_child_bounty(Origin::signed(4), 0, 50, b"12345-p1".to_vec()), + pallet_balances::Error::::KeepAlive, + ); + + assert_noop!( + ChildBounties::add_child_bounty(Origin::signed(4), 0, 100, b"12345-p1".to_vec()), + Error::::InsufficientBountyBalance, + ); + + // Add child-bounty with valid value, which can be funded by parent bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + // Check for the event child-bounty added. + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + assert_eq!(Balances::free_balance(4), 101); + assert_eq!(Balances::reserved_balance(4), 2); + + // DB check. + // Check the child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 0, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + + // Check the child-bounty count. + assert_eq!(ChildBounties::parent_child_bounties(0), 1); + + // Check the child-bounty description status. + assert_eq!(ChildBounties::child_bounty_descriptions(0).unwrap(), b"12345-p1".to_vec(),); + }); +} + +#[test] +fn child_bounty_assign_curator() { + new_test_ext().execute_with(|| { + // TestProcedure + // 1, Create bounty & move to active state with enough bounty fund & master-curator. + // 2, Master-curator adds child-bounty child-bounty-1, moves to "Active" state. + // 3, Test for DB state of `ChildBounties`. + + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + Balances::make_free_balance_be(&4, 101); + Balances::make_free_balance_be(&8, 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 4)); + + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Bounty account status before adding child-bounty. + assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 50); + assert_eq!(Balances::reserved_balance(Bounties::bounty_account_id(0)), 0); + + // Check the balance of master curator. + // Curator deposit is reserved for master curator on parent bounty. + assert_eq!(Balances::free_balance(4), 99); + assert_eq!(Balances::reserved_balance(4), 2); + + // Add child-bounty. + // Acc-4 is the master curator & make sure enough deposit. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + // Bounty account status after adding child-bounty. + assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 40); + assert_eq!(Balances::reserved_balance(Bounties::bounty_account_id(0)), 0); + + // Child-bounty account status. + assert_eq!(Balances::free_balance(ChildBounties::child_bounty_account_id(0)), 10); + assert_eq!(Balances::reserved_balance(ChildBounties::child_bounty_account_id(0)), 0); + + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::CuratorProposed { curator: 8 }, + } + ); + + // Check the balance of master curator. + assert_eq!(Balances::free_balance(4), 99); + assert_eq!(Balances::reserved_balance(4), 2); + + assert_noop!( + ChildBounties::accept_curator(Origin::signed(3), 0, 0), + BountiesError::RequireCurator, + ); + + assert_ok!(ChildBounties::accept_curator(Origin::signed(8), 0, 0)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::Active { curator: 8 }, + } + ); + + // Deposit for child-bounty curator is reserved. + assert_eq!(Balances::free_balance(8), 100); + assert_eq!(Balances::reserved_balance(8), 1); + + // Bounty account status at exit. + assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 40); + assert_eq!(Balances::reserved_balance(Bounties::bounty_account_id(0)), 0); + + // Child-bounty account status at exit. + assert_eq!(Balances::free_balance(ChildBounties::child_bounty_account_id(0)), 10); + assert_eq!(Balances::reserved_balance(ChildBounties::child_bounty_account_id(0)), 0); + + // Treasury account status at exit. + assert_eq!(Balances::free_balance(Treasury::account_id()), 26); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + }); +} + +#[test] +fn award_claim_child_bounty() { + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + // Propose and accept curator for child-bounty. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(8), 0, 0)); + + // Award child-bounty. + // Test for non child-bounty curator. + assert_noop!( + ChildBounties::award_child_bounty(Origin::signed(3), 0, 0, 7), + BountiesError::RequireCurator, + ); + + assert_ok!(ChildBounties::award_child_bounty(Origin::signed(8), 0, 0, 7)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::PendingPayout { + curator: 8, + beneficiary: 7, + unlock_at: 5 + }, + } + ); + + // Claim child-bounty. + // Test for Premature condition. + assert_noop!( + ChildBounties::claim_child_bounty(Origin::signed(7), 0, 0), + BountiesError::Premature + ); + + System::set_block_number(9); + + assert_ok!(ChildBounties::claim_child_bounty(Origin::signed(7), 0, 0)); + + // Ensure child-bounty curator is paid with curator fee & deposit refund. + assert_eq!(Balances::free_balance(8), 103); + assert_eq!(Balances::reserved_balance(8), 0); + + // Ensure executor is paid with beneficiary amount. + assert_eq!(Balances::free_balance(7), 8); + assert_eq!(Balances::reserved_balance(7), 0); + + // Child-bounty account status. + assert_eq!(Balances::free_balance(ChildBounties::child_bounty_account_id(0)), 0); + assert_eq!(Balances::reserved_balance(ChildBounties::child_bounty_account_id(0)), 0); + + // Check the child-bounty count. + assert_eq!(ChildBounties::parent_child_bounties(0), 0); + }); +} + +#[test] +fn close_child_bounty_added() { + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + System::set_block_number(4); + + // Close child-bounty. + // Wrong origin. + assert_noop!(ChildBounties::close_child_bounty(Origin::signed(7), 0, 0), BadOrigin); + assert_noop!(ChildBounties::close_child_bounty(Origin::signed(8), 0, 0), BadOrigin); + + // Correct origin - parent curator. + assert_ok!(ChildBounties::close_child_bounty(Origin::signed(4), 0, 0)); + + // Check the child-bounty count. + assert_eq!(ChildBounties::parent_child_bounties(0), 0); + + // Parent-bounty account status. + assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 50); + assert_eq!(Balances::reserved_balance(Bounties::bounty_account_id(0)), 0); + + // Child-bounty account status. + assert_eq!(Balances::free_balance(ChildBounties::child_bounty_account_id(0)), 0); + assert_eq!(Balances::reserved_balance(ChildBounties::child_bounty_account_id(0)), 0); + }); +} + +#[test] +fn close_child_bounty_active() { + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + // Propose and accept curator for child-bounty. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(8), 0, 0)); + + // Close child-bounty in active state. + assert_ok!(ChildBounties::close_child_bounty(Origin::signed(4), 0, 0)); + + // Check the child-bounty count. + assert_eq!(ChildBounties::parent_child_bounties(0), 0); + + // Ensure child-bounty curator balance is unreserved. + assert_eq!(Balances::free_balance(8), 101); + assert_eq!(Balances::reserved_balance(8), 0); + + // Parent-bounty account status. + assert_eq!(Balances::free_balance(Bounties::bounty_account_id(0)), 50); + assert_eq!(Balances::reserved_balance(Bounties::bounty_account_id(0)), 0); + + // Child-bounty account status. + assert_eq!(Balances::free_balance(ChildBounties::child_bounty_account_id(0)), 0); + assert_eq!(Balances::reserved_balance(ChildBounties::child_bounty_account_id(0)), 0); + }); +} + +#[test] +fn close_child_bounty_pending() { + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + // Propose and accept curator for child-bounty. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(8), 0, 0)); + + assert_ok!(ChildBounties::award_child_bounty(Origin::signed(8), 0, 0, 7)); + + // Close child-bounty in pending_payout state. + assert_noop!( + ChildBounties::close_child_bounty(Origin::signed(4), 0, 0), + BountiesError::PendingPayout + ); + + // Check the child-bounty count. + assert_eq!(ChildBounties::parent_child_bounties(0), 1); + + // Ensure no changes in child-bounty curator balance. + assert_eq!(Balances::free_balance(8), 100); + assert_eq!(Balances::reserved_balance(8), 1); + + // Child-bounty account status. + assert_eq!(Balances::free_balance(ChildBounties::child_bounty_account_id(0)), 10); + assert_eq!(Balances::reserved_balance(ChildBounties::child_bounty_account_id(0)), 0); + }); +} + +#[test] +fn child_bounty_added_unassign_curator() { + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + // Unassign curator in added state. + assert_noop!( + ChildBounties::unassign_curator(Origin::signed(4), 0, 0), + BountiesError::UnexpectedStatus + ); + }); +} + +#[test] +fn child_bounty_curator_proposed_unassign_curator() { + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + // Propose curator for child-bounty. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::CuratorProposed { curator: 8 }, + } + ); + + // Random account cannot unassign the curator when in proposed state. + assert_noop!(ChildBounties::unassign_curator(Origin::signed(99), 0, 0), BadOrigin); + + // Unassign curator. + assert_ok!(ChildBounties::unassign_curator(Origin::signed(4), 0, 0)); + + // Verify updated child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + }); +} + +#[test] +fn child_bounty_active_unassign_curator() { + // Covers all scenarios with all origin types. + // Step 1: Setup bounty, child bounty. + // Step 2: Assign, accept curator for child bounty. Unassign from reject origin. Should slash. + // Step 3: Assign, accept another curator for child bounty. Unassign from parent-bounty curator. + // Should slash. Step 4: Assign, accept another curator for child bounty. Unassign from + // child-bounty curator. Should NOT slash. Step 5: Assign, accept another curator for child + // bounty. Unassign from random account. Should slash. + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&6, 101); // Child-bounty curator 1. + Balances::make_free_balance_be(&7, 101); // Child-bounty curator 2. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator 3. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Create Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + System::set_block_number(3); + >::on_initialize(3); + + // Propose and accept curator for child-bounty. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(8), 0, 0)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::Active { curator: 8 }, + } + ); + + System::set_block_number(4); + >::on_initialize(4); + + // Unassign curator - from reject origin. + assert_ok!(ChildBounties::unassign_curator(Origin::root(), 0, 0)); + + // Verify updated child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + + // Ensure child-bounty curator was slashed. + assert_eq!(Balances::free_balance(8), 100); + assert_eq!(Balances::reserved_balance(8), 0); // slashed + + // Propose and accept curator for child-bounty again. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 7, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(7), 0, 0)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::Active { curator: 7 }, + } + ); + + System::set_block_number(5); + >::on_initialize(5); + + // Unassign curator again - from master curator. + assert_ok!(ChildBounties::unassign_curator(Origin::signed(4), 0, 0)); + + // Verify updated child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + + // Ensure child-bounty curator was slashed. + assert_eq!(Balances::free_balance(7), 100); + assert_eq!(Balances::reserved_balance(7), 0); // slashed + + // Propose and accept curator for child-bounty again. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 6, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(6), 0, 0)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::Active { curator: 6 }, + } + ); + + System::set_block_number(6); + >::on_initialize(6); + + // Unassign curator again - from child-bounty curator. + assert_ok!(ChildBounties::unassign_curator(Origin::signed(6), 0, 0)); + + // Verify updated child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + + // Ensure child-bounty curator was **not** slashed. + assert_eq!(Balances::free_balance(6), 101); // not slashed + assert_eq!(Balances::reserved_balance(6), 0); + + // Propose and accept curator for child-bounty one last time. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 6, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(6), 0, 0)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::Active { curator: 6 }, + } + ); + + System::set_block_number(7); + >::on_initialize(7); + + // Unassign curator again - from non curator; non reject origin; some random guy. + // Bounty update period is not yet complete. + assert_noop!( + ChildBounties::unassign_curator(Origin::signed(3), 0, 0), + BountiesError::Premature + ); + + System::set_block_number(20); + >::on_initialize(20); + + // Unassign child curator from random account after inactivity. + assert_ok!(ChildBounties::unassign_curator(Origin::signed(3), 0, 0)); + + // Verify updated child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + + // Ensure child-bounty curator was slashed. + assert_eq!(Balances::free_balance(6), 100); // slashed + assert_eq!(Balances::reserved_balance(6), 0); + }); +} + +#[test] +fn parent_bounty_inactive_unassign_curator_child_bounty() { + // Unassign curator when parent bounty in not in active state. + // This can happen when the curator of parent bounty has been unassigned. + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator 1. + Balances::make_free_balance_be(&5, 101); // Parent-bounty curator 2. + Balances::make_free_balance_be(&6, 101); // Child-bounty curator 1. + Balances::make_free_balance_be(&7, 101); // Child-bounty curator 2. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator 3. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Create Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + System::set_block_number(3); + >::on_initialize(3); + + // Propose and accept curator for child-bounty. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(8), 0, 0)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::Active { curator: 8 }, + } + ); + + System::set_block_number(4); + >::on_initialize(4); + + // Unassign parent bounty curator. + assert_ok!(Bounties::unassign_curator(Origin::root(), 0)); + + System::set_block_number(5); + >::on_initialize(5); + + // Try unassign child-bounty curator - from non curator; non reject + // origin; some random guy. Bounty update period is not yet complete. + assert_noop!( + ChildBounties::unassign_curator(Origin::signed(3), 0, 0), + Error::::ParentBountyNotActive + ); + + // Unassign curator - from reject origin. + assert_ok!(ChildBounties::unassign_curator(Origin::root(), 0, 0)); + + // Verify updated child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + + // Ensure child-bounty curator was slashed. + assert_eq!(Balances::free_balance(8), 100); + assert_eq!(Balances::reserved_balance(8), 0); // slashed + + System::set_block_number(6); + >::on_initialize(6); + + // Propose and accept curator for parent-bounty again. + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 5, 6)); + assert_ok!(Bounties::accept_curator(Origin::signed(5), 0)); + + System::set_block_number(7); + >::on_initialize(7); + + // Propose and accept curator for child-bounty again. + assert_ok!(ChildBounties::propose_curator(Origin::signed(5), 0, 0, 7, 2)); + assert_ok!(ChildBounties::accept_curator(Origin::signed(7), 0, 0)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::Active { curator: 7 }, + } + ); + + System::set_block_number(8); + >::on_initialize(8); + + assert_noop!( + ChildBounties::unassign_curator(Origin::signed(3), 0, 0), + BountiesError::Premature + ); + + // Unassign parent bounty curator again. + assert_ok!(Bounties::unassign_curator(Origin::signed(5), 0)); + + System::set_block_number(9); + >::on_initialize(9); + + // Unassign curator again - from master curator. + assert_ok!(ChildBounties::unassign_curator(Origin::signed(7), 0, 0)); + + // Verify updated child-bounty status. + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 0, + status: ChildBountyStatus::Added, + } + ); + + // Ensure child-bounty curator was not slashed. + assert_eq!(Balances::free_balance(7), 101); + assert_eq!(Balances::reserved_balance(7), 0); // slashed + }); +} + +#[test] +fn close_parent_with_child_bounty() { + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + assert_ok!(Bounties::propose_bounty(Origin::signed(0), 50, b"12345".to_vec())); + assert_ok!(Bounties::approve_bounty(Origin::root(), 0)); + + // Try add child-bounty. + // Should fail, parent bounty not active yet. + assert_noop!( + ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec()), + Error::::ParentBountyNotActive + ); + + System::set_block_number(2); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + System::set_block_number(4); + >::on_initialize(4); + + // Try close parent-bounty. + // Child bounty active, can't close parent. + assert_noop!( + Bounties::close_bounty(Origin::root(), 0), + BountiesError::HasActiveChildBounty + ); + + System::set_block_number(2); + + // Close child-bounty. + assert_ok!(ChildBounties::close_child_bounty(Origin::root(), 0, 0)); + + // Check the child-bounty count. + assert_eq!(ChildBounties::parent_child_bounties(0), 0); + + // Try close parent-bounty again. + // Should pass this time. + assert_ok!(Bounties::close_bounty(Origin::root(), 0)); + }); +} + +#[test] +fn children_curator_fee_calculation_test() { + // Tests the calculation of subtracting child-bounty curator fee + // from parent bounty fee when claiming bounties. + new_test_ext().execute_with(|| { + // Make the parent bounty. + System::set_block_number(1); + Balances::make_free_balance_be(&Treasury::account_id(), 101); + assert_eq!(Balances::free_balance(Treasury::account_id()), 101); + assert_eq!(Balances::reserved_balance(Treasury::account_id()), 0); + + // Bounty curator initial balance. + Balances::make_free_balance_be(&4, 101); // Parent-bounty curator. + Balances::make_free_balance_be(&8, 101); // Child-bounty curator. + + 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); + >::on_initialize(2); + + assert_ok!(Bounties::propose_curator(Origin::root(), 0, 4, 6)); + assert_ok!(Bounties::accept_curator(Origin::signed(4), 0)); + + // Child-bounty. + assert_ok!(ChildBounties::add_child_bounty(Origin::signed(4), 0, 10, b"12345-p1".to_vec())); + assert_eq!(last_event(), ChildBountiesEvent::Added { index: 0, child_index: 0 }); + + System::set_block_number(4); + >::on_initialize(4); + + // Propose curator for child-bounty. + assert_ok!(ChildBounties::propose_curator(Origin::signed(4), 0, 0, 8, 2)); + + // Check curator fee added to the sum. + assert_eq!(ChildBounties::children_curator_fees(0), 2); + + // Accept curator for child-bounty. + assert_ok!(ChildBounties::accept_curator(Origin::signed(8), 0, 0)); + + // Award child-bounty. + assert_ok!(ChildBounties::award_child_bounty(Origin::signed(8), 0, 0, 7)); + + assert_eq!( + ChildBounties::child_bounties(0, 0).unwrap(), + ChildBounty { + parent_bounty: 0, + value: 10, + fee: 2, + curator_deposit: 1, + status: ChildBountyStatus::PendingPayout { + curator: 8, + beneficiary: 7, + unlock_at: 7 + }, + } + ); + + System::set_block_number(9); + + // Claim child-bounty. + assert_ok!(ChildBounties::claim_child_bounty(Origin::signed(7), 0, 0)); + + // Check the child-bounty count. + assert_eq!(ChildBounties::parent_child_bounties(0), 0); + + // Award the parent bounty. + assert_ok!(Bounties::award_bounty(Origin::signed(4), 0, 9)); + + System::set_block_number(15); + + // Claim the parent bounty. + assert_ok!(Bounties::claim_bounty(Origin::signed(9), 0)); + + // Ensure parent-bounty curator received correctly reduced fee. + assert_eq!(Balances::free_balance(4), 105); // 101 + 6 - 2 + assert_eq!(Balances::reserved_balance(4), 0); + + // Verify parent-bounty beneficiary balance. + assert_eq!(Balances::free_balance(9), 34); + assert_eq!(Balances::reserved_balance(9), 0); + }); +} diff --git a/frame/child-bounties/src/weights.rs b/frame/child-bounties/src/weights.rs new file mode 100644 index 0000000000000..7a52495d980b5 --- /dev/null +++ b/frame/child-bounties/src/weights.rs @@ -0,0 +1,215 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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_child_bounties +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2021-12-03, 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_child_bounties +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/child-bounties/src/ +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![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_child_bounties. +pub trait WeightInfo { + fn add_child_bounty(d: u32, ) -> Weight; + fn propose_curator() -> Weight; + fn accept_curator() -> Weight; + fn unassign_curator() -> Weight; + fn award_child_bounty() -> Weight; + fn claim_child_bounty() -> Weight; + fn close_child_bounty_added() -> Weight; + fn close_child_bounty_active() -> Weight; +} + +/// Weights for pallet_child_bounties using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: ChildBounties ChildBountyCount (r:1 w:1) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + // Storage: ChildBounties ChildBounties (r:0 w:1) + fn add_child_bounty(d: u32, ) -> Weight { + (113_579_000 as Weight) + // Standard Error: 1_000 + .saturating_add((4_000 as Weight).saturating_mul(d as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(6 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + fn propose_curator() -> Weight { + (24_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn accept_curator() -> Weight { + (55_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:0) + // Storage: System Account (r:1 w:1) + fn unassign_curator() -> Weight { + (61_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + fn award_child_bounty() -> Weight { + (42_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: System Account (r:3 w:3) + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + fn claim_child_bounty() -> Weight { + (153_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(6 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: System Account (r:2 w:2) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + fn close_child_bounty_added() -> Weight { + (101_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(6 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: System Account (r:3 w:3) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + fn close_child_bounty_active() -> Weight { + (130_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(7 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:0) + // Storage: System Account (r:2 w:2) + // Storage: ChildBounties ChildBountyCount (r:1 w:1) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + // Storage: ChildBounties ChildBounties (r:0 w:1) + fn add_child_bounty(d: u32, ) -> Weight { + (113_579_000 as Weight) + // Standard Error: 1_000 + .saturating_add((4_000 as Weight).saturating_mul(d as Weight)) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + fn propose_curator() -> Weight { + (24_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn accept_curator() -> Weight { + (55_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: Bounties Bounties (r:1 w:0) + // Storage: System Account (r:1 w:1) + fn unassign_curator() -> Weight { + (61_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + fn award_child_bounty() -> Weight { + (42_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: System Account (r:3 w:3) + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + fn claim_child_bounty() -> Weight { + (153_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: System Account (r:2 w:2) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + fn close_child_bounty_added() -> Weight { + (101_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + } + // Storage: Bounties Bounties (r:1 w:0) + // Storage: ChildBounties ChildBounties (r:1 w:1) + // Storage: System Account (r:3 w:3) + // Storage: ChildBounties ChildrenCuratorFees (r:1 w:1) + // Storage: ChildBounties ParentChildBounties (r:1 w:1) + // Storage: ChildBounties ChildBountyDescriptions (r:0 w:1) + fn close_child_bounty_active() -> Weight { + (130_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(7 as Weight)) + } +}