Skip to content

Commit

Permalink
Bounty Pallet: add approve_bounty_with_curator call to bounties p…
Browse files Browse the repository at this point in the history
…allet (#5961)

Resolves issue #5928

Adds `approve_bounty_with_curator` call to the `bounties` pallet to
combine functions of `approve_bounty` and `propose_curator` into one
call. Also adds a new status `ApprovedWithCurator` required to
distinguish if bounty was approved with curator when skipping through
`Funded` status and moving to `CuratorProposed` status.

If `unassign_curator` is called after `approve_bounty_with_curator` the
process will fall back to the old flow of calling `propose_curator`
separately.

---------

Co-authored-by: DavidK <[email protected]>
Co-authored-by: command-bot <>
Co-authored-by: Ankan <[email protected]>
  • Loading branch information
3 people authored Nov 5, 2024
1 parent f1e416a commit 2ae79be
Show file tree
Hide file tree
Showing 6 changed files with 482 additions and 126 deletions.
82 changes: 47 additions & 35 deletions polkadot/runtime/rococo/src/weights/pallet_bounties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,23 @@
//! Autogenerated weights for `pallet_bounties`
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0
//! DATE: 2024-02-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! DATE: 2024-10-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `runner-bn-ce5rx-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
//! HOSTNAME: `runner-augrssgt-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("rococo-dev")`, DB CACHE: 1024

// Executed Command:
// ./target/production/polkadot
// target/production/polkadot
// benchmark
// pallet
// --chain=rococo-dev
// --steps=50
// --repeat=20
// --no-storage-info
// --no-median-slopes
// --no-min-squares
// --pallet=pallet_bounties
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --heap-pages=4096
// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json
// --pallet=pallet_bounties
// --chain=rococo-dev
// --header=./polkadot/file_header.txt
// --output=./polkadot/runtime/rococo/src/weights/

Expand Down Expand Up @@ -63,11 +61,11 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `210`
// Estimated: `3593`
// Minimum execution time: 21_772_000 picoseconds.
Weight::from_parts(22_861_341, 0)
// Minimum execution time: 26_614_000 picoseconds.
Weight::from_parts(28_274_660, 0)
.saturating_add(Weight::from_parts(0, 3593))
// Standard Error: 3
.saturating_add(Weight::from_parts(721, 0).saturating_mul(d.into()))
// Standard Error: 4
.saturating_add(Weight::from_parts(779, 0).saturating_mul(d.into()))
.saturating_add(T::DbWeight::get().reads(2))
.saturating_add(T::DbWeight::get().writes(4))
}
Expand All @@ -79,8 +77,8 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `302`
// Estimated: `3642`
// Minimum execution time: 11_218_000 picoseconds.
Weight::from_parts(11_796_000, 0)
// Minimum execution time: 14_692_000 picoseconds.
Weight::from_parts(15_070_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(2))
.saturating_add(T::DbWeight::get().writes(2))
Expand All @@ -91,22 +89,36 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `322`
// Estimated: `3642`
// Minimum execution time: 10_959_000 picoseconds.
Weight::from_parts(11_658_000, 0)
// Minimum execution time: 13_695_000 picoseconds.
Weight::from_parts(14_220_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(1))
}
/// Storage: `Bounties::Bounties` (r:1 w:1)
/// Proof: `Bounties::Bounties` (`max_values`: None, `max_size`: Some(177), added: 2652, mode: `MaxEncodedLen`)
/// Storage: `Bounties::BountyApprovals` (r:1 w:1)
/// Proof: `Bounties::BountyApprovals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`)
fn approve_bounty_with_curator() -> Weight {
// Proof Size summary in bytes:
// Measured: `322`
// Estimated: `3642`
// Minimum execution time: 18_428_000 picoseconds.
Weight::from_parts(19_145_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(2))
.saturating_add(T::DbWeight::get().writes(2))
}
/// Storage: `Bounties::Bounties` (r:1 w:1)
/// Proof: `Bounties::Bounties` (`max_values`: None, `max_size`: Some(177), added: 2652, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn unassign_curator() -> Weight {
// Proof Size summary in bytes:
// Measured: `498`
// Estimated: `3642`
// Minimum execution time: 37_419_000 picoseconds.
Weight::from_parts(38_362_000, 0)
// Minimum execution time: 44_648_000 picoseconds.
Weight::from_parts(45_860_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(2))
.saturating_add(T::DbWeight::get().writes(2))
Expand All @@ -119,8 +131,8 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `494`
// Estimated: `3642`
// Minimum execution time: 27_328_000 picoseconds.
Weight::from_parts(27_661_000, 0)
// Minimum execution time: 33_973_000 picoseconds.
Weight::from_parts(34_979_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(2))
.saturating_add(T::DbWeight::get().writes(2))
Expand All @@ -133,8 +145,8 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `400`
// Estimated: `3642`
// Minimum execution time: 16_067_000 picoseconds.
Weight::from_parts(16_865_000, 0)
// Minimum execution time: 20_932_000 picoseconds.
Weight::from_parts(21_963_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(2))
.saturating_add(T::DbWeight::get().writes(1))
Expand All @@ -151,8 +163,8 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `764`
// Estimated: `8799`
// Minimum execution time: 101_153_000 picoseconds.
Weight::from_parts(102_480_000, 0)
// Minimum execution time: 114_942_000 picoseconds.
Weight::from_parts(117_653_000, 0)
.saturating_add(Weight::from_parts(0, 8799))
.saturating_add(T::DbWeight::get().reads(5))
.saturating_add(T::DbWeight::get().writes(6))
Expand All @@ -169,8 +181,8 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `444`
// Estimated: `3642`
// Minimum execution time: 38_838_000 picoseconds.
Weight::from_parts(39_549_000, 0)
// Minimum execution time: 47_649_000 picoseconds.
Weight::from_parts(49_016_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(3))
.saturating_add(T::DbWeight::get().writes(3))
Expand All @@ -187,8 +199,8 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `680`
// Estimated: `6196`
// Minimum execution time: 68_592_000 picoseconds.
Weight::from_parts(70_727_000, 0)
// Minimum execution time: 80_298_000 picoseconds.
Weight::from_parts(82_306_000, 0)
.saturating_add(Weight::from_parts(0, 6196))
.saturating_add(T::DbWeight::get().reads(4))
.saturating_add(T::DbWeight::get().writes(4))
Expand All @@ -199,8 +211,8 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `358`
// Estimated: `3642`
// Minimum execution time: 11_272_000 picoseconds.
Weight::from_parts(11_592_000, 0)
// Minimum execution time: 14_237_000 picoseconds.
Weight::from_parts(14_969_000, 0)
.saturating_add(Weight::from_parts(0, 3642))
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(1))
Expand All @@ -216,11 +228,11 @@ impl<T: frame_system::Config> pallet_bounties::WeightInfo for WeightInfo<T> {
// Proof Size summary in bytes:
// Measured: `0 + b * (297 ±0)`
// Estimated: `1887 + b * (5206 ±0)`
// Minimum execution time: 2_844_000 picoseconds.
Weight::from_parts(2_900_000, 0)
// Minimum execution time: 3_174_000 picoseconds.
Weight::from_parts(3_336_000, 0)
.saturating_add(Weight::from_parts(0, 1887))
// Standard Error: 9_467
.saturating_add(Weight::from_parts(32_326_595, 0).saturating_mul(b.into()))
// Standard Error: 10_408
.saturating_add(Weight::from_parts(37_811_366, 0).saturating_mul(b.into()))
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(b.into())))
.saturating_add(T::DbWeight::get().writes(1))
Expand Down
15 changes: 15 additions & 0 deletions prdoc/pr_5961.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json

title: "Bounties Pallet: add `approve_bounty_with_curator` call"

doc:
- audience: [Runtime Dev, Runtime User]
description: |
Adds `approve_bounty_with_curator` call to the bounties pallet to combine `approve_bounty` and `propose_curator` into one call. If `unassign_curator` is called after `approve_bounty_with_curator` the process falls back to the previous flow of calling `propose_curator` separately. Introduces a new `ApprovedWithCurator` bounty status when bounty is approved with curator.

crates:
- name: pallet-bounties
bump: major
- name: rococo-runtime
bump: minor
15 changes: 15 additions & 0 deletions substrate/frame/bounties/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ benchmarks_instance_pallet! {
Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
}: _<T::RuntimeOrigin>(approve_origin, bounty_id, curator_lookup, fee)

approve_bounty_with_curator {
setup_pot_account::<T, I>();
let (caller, curator, fee, value, reason) = setup_bounty::<T, I>(0, T::MaximumReasonLength::get());
let curator_lookup = T::Lookup::unlookup(curator.clone());
Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
let bounty_id = BountyCount::<T, I>::get() - 1;
let approve_origin = T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
Treasury::<T, I>::on_initialize(BlockNumberFor::<T>::zero());
}: _<T::RuntimeOrigin>(approve_origin, bounty_id, curator_lookup, fee)
verify {
assert_last_event::<T, I>(
Event::CuratorProposed { bounty_id, curator }.into()
);
}

// Worst case when curator is inactive and any sender unassigns the curator.
unassign_curator {
setup_pot_account::<T, I>();
Expand Down
72 changes: 70 additions & 2 deletions substrate/frame/bounties/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
//! - `approve_bounty` - Accept a specific treasury amount to be earmarked for a predefined body of
//! work.
//! - `propose_curator` - Assign an account to a bounty as candidate curator.
//! - `approve_bounty_with_curator` - Accept a specific treasury amount for a predefined body of
//! work with assigned candidate curator account.
//! - `accept_curator` - Accept a bounty assignment from the Council, setting a curator deposit.
//! - `extend_bounty_expiry` - Extend the expiry block number of the bounty and stay active.
//! - `award_bounty` - Close and pay out the specified amount for the completed work.
Expand Down Expand Up @@ -174,6 +176,11 @@ pub enum BountyStatus<AccountId, BlockNumber> {
/// When the bounty can be claimed.
unlock_at: BlockNumber,
},
/// The bounty is approved with curator assigned.
ApprovedWithCurator {
/// The assigned curator of this bounty.
curator: AccountId,
},
}

/// The child bounty manager.
Expand Down Expand Up @@ -471,6 +478,15 @@ pub mod pallet {
// No curator to unassign at this point.
return Err(Error::<T, I>::UnexpectedStatus.into())
},
BountyStatus::ApprovedWithCurator { ref curator } => {
// Bounty not yet funded, but bounty was approved with curator.
// `RejectOrigin` or curator himself can unassign from this bounty.
ensure!(maybe_sender.map_or(true, |sender| sender == *curator), BadOrigin);
// This state can only be while the bounty is not yet funded so we return
// bounty to the `Approved` state without curator
bounty.status = BountyStatus::Approved;
return Ok(());
},
BountyStatus::CuratorProposed { ref curator } => {
// A curator has been proposed, but not accepted yet.
// Either `RejectOrigin` or the proposed curator can unassign the curator.
Expand Down Expand Up @@ -723,7 +739,7 @@ pub mod pallet {
Some(<T as Config<I>>::WeightInfo::close_bounty_proposed()).into()
)
},
BountyStatus::Approved => {
BountyStatus::Approved | BountyStatus::ApprovedWithCurator { .. } => {
// For weight reasons, we don't allow a council to cancel in this phase.
// We ask for them to wait until it is funded before they can cancel.
return Err(Error::<T, I>::UnexpectedStatus.into())
Expand Down Expand Up @@ -804,6 +820,52 @@ pub mod pallet {
Self::deposit_event(Event::<T, I>::BountyExtended { index: bounty_id });
Ok(())
}

/// Approve bountry and propose a curator simultaneously.
/// This call is a shortcut to calling `approve_bounty` and `propose_curator` separately.
///
/// May only be called from `T::SpendOrigin`.
///
/// - `bounty_id`: Bounty ID to approve.
/// - `curator`: The curator account whom will manage this bounty.
/// - `fee`: The curator fee.
///
/// ## Complexity
/// - O(1).
#[pallet::call_index(9)]
#[pallet::weight(<T as Config<I>>::WeightInfo::approve_bounty_with_curator())]
pub fn approve_bounty_with_curator(
origin: OriginFor<T>,
#[pallet::compact] bounty_id: BountyIndex,
curator: AccountIdLookupOf<T>,
#[pallet::compact] fee: BalanceOf<T, I>,
) -> DispatchResult {
let max_amount = T::SpendOrigin::ensure_origin(origin)?;
let curator = T::Lookup::lookup(curator)?;
Bounties::<T, I>::try_mutate_exists(bounty_id, |maybe_bounty| -> DispatchResult {
// approve bounty
let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
ensure!(
bounty.value <= max_amount,
pallet_treasury::Error::<T, I>::InsufficientPermission
);
ensure!(bounty.status == BountyStatus::Proposed, Error::<T, I>::UnexpectedStatus);
ensure!(fee < bounty.value, Error::<T, I>::InvalidFee);

BountyApprovals::<T, I>::try_append(bounty_id)
.map_err(|()| Error::<T, I>::TooManyQueued)?;

bounty.status = BountyStatus::ApprovedWithCurator { curator: curator.clone() };
bounty.fee = fee;

Ok(())
})?;

Self::deposit_event(Event::<T, I>::BountyApproved { index: bounty_id });
Self::deposit_event(Event::<T, I>::CuratorProposed { bounty_id, curator });

Ok(())
}
}

#[pallet::hooks]
Expand Down Expand Up @@ -946,7 +1008,13 @@ impl<T: Config<I>, I: 'static> pallet_treasury::SpendFunds<T, I> for Pallet<T, I
if bounty.value <= *budget_remaining {
*budget_remaining -= bounty.value;

bounty.status = BountyStatus::Funded;
// jump through the funded phase if we're already approved with curator
if let BountyStatus::ApprovedWithCurator { curator } = &bounty.status {
bounty.status =
BountyStatus::CuratorProposed { curator: curator.clone() };
} else {
bounty.status = BountyStatus::Funded;
}

// return their deposit.
let err_amount = T::Currency::unreserve(&bounty.proposer, bounty.bond);
Expand Down
Loading

0 comments on commit 2ae79be

Please sign in to comment.