referrerpolicy=no-referrer-when-downgrade

pallet_bounties/
benchmarking.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Bounties pallet benchmarking.
19
20use super::*;
21
22use alloc::{vec, vec::Vec};
23use frame_benchmarking::v2::*;
24use frame_system::{pallet_prelude::BlockNumberFor as SystemBlockNumberFor, RawOrigin};
25use sp_runtime::traits::{BlockNumberProvider, Bounded};
26
27use crate::Pallet as Bounties;
28use pallet_treasury::Pallet as Treasury;
29
30const SEED: u32 = 0;
31
32fn set_block_number<T: Config<I>, I: 'static>(n: BlockNumberFor<T, I>) {
33	<T as pallet_treasury::Config<I>>::BlockNumberProvider::set_block_number(n);
34}
35
36fn minimum_balance<T: Config<I>, I: 'static>() -> BalanceOf<T, I> {
37	let minimum_balance = T::Currency::minimum_balance();
38
39	if minimum_balance.is_zero() {
40		1u32.into()
41	} else {
42		minimum_balance
43	}
44}
45
46// Create bounties that are approved for use in `on_initialize`.
47fn create_approved_bounties<T: Config<I>, I: 'static>(n: u32) -> Result<(), BenchmarkError> {
48	for i in 0..n {
49		let (caller, _curator, _fee, value, reason) =
50			setup_bounty::<T, I>(i, T::MaximumReasonLength::get());
51		Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
52		let bounty_id = BountyCount::<T, I>::get() - 1;
53		let approve_origin =
54			T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
55		Bounties::<T, I>::approve_bounty(approve_origin, bounty_id)?;
56	}
57	ensure!(BountyApprovals::<T, I>::get().len() == n as usize, "Not all bounty approved");
58	Ok(())
59}
60
61// Create the pre-requisite information needed to create a treasury `propose_bounty`.
62fn setup_bounty<T: Config<I>, I: 'static>(
63	u: u32,
64	d: u32,
65) -> (T::AccountId, T::AccountId, BalanceOf<T, I>, BalanceOf<T, I>, Vec<u8>) {
66	let caller = account("caller", u, SEED);
67	let value: BalanceOf<T, I> = T::BountyValueMinimum::get().saturating_mul(100u32.into());
68	let fee = value / 2u32.into();
69	let deposit = T::BountyDepositBase::get() +
70		T::DataDepositPerByte::get() * T::MaximumReasonLength::get().into();
71	let _ = T::Currency::make_free_balance_be(&caller, deposit + minimum_balance::<T, I>());
72	let curator = account("curator", u, SEED);
73	let _ =
74		T::Currency::make_free_balance_be(&curator, fee / 2u32.into() + minimum_balance::<T, I>());
75	let reason = vec![0; d as usize];
76	(caller, curator, fee, value, reason)
77}
78
79fn create_bounty<T: Config<I>, I: 'static>(
80) -> Result<(AccountIdLookupOf<T>, BountyIndex), BenchmarkError> {
81	let (caller, curator, fee, value, reason) =
82		setup_bounty::<T, I>(0, T::MaximumReasonLength::get());
83	let curator_lookup = T::Lookup::unlookup(curator.clone());
84	Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
85	let bounty_id = BountyCount::<T, I>::get() - 1;
86	let approve_origin =
87		T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
88	Bounties::<T, I>::approve_bounty(approve_origin.clone(), bounty_id)?;
89	set_block_number::<T, I>(T::SpendPeriod::get());
90	Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
91	Bounties::<T, I>::propose_curator(approve_origin, bounty_id, curator_lookup.clone(), fee)?;
92	Bounties::<T, I>::accept_curator(RawOrigin::Signed(curator).into(), bounty_id)?;
93	Ok((curator_lookup, bounty_id))
94}
95
96fn setup_pot_account<T: Config<I>, I: 'static>() {
97	let pot_account = Bounties::<T, I>::account_id();
98	let value = minimum_balance::<T, I>().saturating_mul(1_000_000_000u32.into());
99	let _ = T::Currency::make_free_balance_be(&pot_account, value);
100}
101
102fn assert_last_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>::RuntimeEvent) {
103	frame_system::Pallet::<T>::assert_last_event(generic_event.into());
104}
105
106#[instance_benchmarks]
107mod benchmarks {
108	use super::*;
109
110	#[benchmark]
111	fn propose_bounty(d: Linear<0, { T::MaximumReasonLength::get() }>) {
112		let (caller, _, _, value, description) = setup_bounty::<T, I>(0, d);
113
114		#[extrinsic_call]
115		_(RawOrigin::Signed(caller), value, description);
116	}
117
118	#[benchmark]
119	fn approve_bounty() -> Result<(), BenchmarkError> {
120		let (caller, _, _, value, reason) = setup_bounty::<T, I>(0, T::MaximumReasonLength::get());
121		Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
122		let bounty_id = BountyCount::<T, I>::get() - 1;
123		let approve_origin =
124			T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
125
126		#[extrinsic_call]
127		_(approve_origin as T::RuntimeOrigin, bounty_id);
128
129		Ok(())
130	}
131
132	#[benchmark]
133	fn propose_curator() -> Result<(), BenchmarkError> {
134		setup_pot_account::<T, I>();
135		let (caller, curator, fee, value, reason) =
136			setup_bounty::<T, I>(0, T::MaximumReasonLength::get());
137		let curator_lookup = T::Lookup::unlookup(curator);
138		Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
139		let bounty_id = BountyCount::<T, I>::get() - 1;
140		let approve_origin =
141			T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
142		Bounties::<T, I>::approve_bounty(approve_origin.clone(), bounty_id)?;
143		set_block_number::<T, I>(T::SpendPeriod::get());
144		Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
145
146		#[extrinsic_call]
147		_(approve_origin as T::RuntimeOrigin, bounty_id, curator_lookup, fee);
148
149		Ok(())
150	}
151
152	#[benchmark]
153	fn approve_bounty_with_curator() -> Result<(), BenchmarkError> {
154		setup_pot_account::<T, I>();
155		let (caller, curator, fee, value, reason) =
156			setup_bounty::<T, I>(0, T::MaximumReasonLength::get());
157		let curator_lookup = T::Lookup::unlookup(curator.clone());
158		Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
159		let bounty_id = BountyCount::<T, I>::get() - 1;
160		let approve_origin =
161			T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
162		Treasury::<T, I>::on_initialize(SystemBlockNumberFor::<T>::zero());
163
164		#[extrinsic_call]
165		_(approve_origin as T::RuntimeOrigin, bounty_id, curator_lookup, fee);
166
167		assert_last_event::<T, I>(Event::CuratorProposed { bounty_id, curator }.into());
168
169		Ok(())
170	}
171
172	// Worst case when curator is inactive and any sender unassigns the curator,
173	// or if `BountyUpdatePeriod` is large enough and `RejectOrigin` executes the call.
174	#[benchmark]
175	fn unassign_curator() -> Result<(), BenchmarkError> {
176		setup_pot_account::<T, I>();
177		let (curator_lookup, _) = create_bounty::<T, I>()?;
178		Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
179		let bounty_id = BountyCount::<T, I>::get() - 1;
180		let bounty_update_period = T::BountyUpdatePeriod::get();
181		let inactivity_timeout = T::SpendPeriod::get().saturating_add(bounty_update_period);
182		set_block_number::<T, I>(inactivity_timeout.saturating_add(2u32.into()));
183
184		// If `BountyUpdatePeriod` overflows the inactivity timeout the benchmark still executes the
185		// slash
186		let origin = if Pallet::<T, I>::treasury_block_number() <= inactivity_timeout {
187			let curator = T::Lookup::lookup(curator_lookup).map_err(<&str>::from)?;
188			T::RejectOrigin::try_successful_origin()
189				.unwrap_or_else(|_| RawOrigin::Signed(curator).into())
190		} else {
191			let caller = whitelisted_caller();
192			RawOrigin::Signed(caller).into()
193		};
194
195		#[extrinsic_call]
196		_(origin as T::RuntimeOrigin, bounty_id);
197
198		Ok(())
199	}
200
201	#[benchmark]
202	fn accept_curator() -> Result<(), BenchmarkError> {
203		setup_pot_account::<T, I>();
204		let (caller, curator, fee, value, reason) =
205			setup_bounty::<T, I>(0, T::MaximumReasonLength::get());
206		let curator_lookup = T::Lookup::unlookup(curator.clone());
207		Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
208		let bounty_id = BountyCount::<T, I>::get() - 1;
209		let approve_origin =
210			T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
211		Bounties::<T, I>::approve_bounty(approve_origin.clone(), bounty_id)?;
212		set_block_number::<T, I>(T::SpendPeriod::get());
213		Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
214		Bounties::<T, I>::propose_curator(approve_origin, bounty_id, curator_lookup, fee)?;
215
216		#[extrinsic_call]
217		_(RawOrigin::Signed(curator), bounty_id);
218
219		Ok(())
220	}
221
222	#[benchmark]
223	fn award_bounty() -> Result<(), BenchmarkError> {
224		setup_pot_account::<T, I>();
225		let (curator_lookup, _) = create_bounty::<T, I>()?;
226		Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
227
228		let bounty_id = BountyCount::<T, I>::get() - 1;
229		let curator = T::Lookup::lookup(curator_lookup).map_err(<&str>::from)?;
230
231		let beneficiary = T::Lookup::unlookup(account("beneficiary", 0, SEED));
232
233		#[extrinsic_call]
234		_(RawOrigin::Signed(curator), bounty_id, beneficiary);
235
236		Ok(())
237	}
238
239	#[benchmark]
240	fn claim_bounty() -> Result<(), BenchmarkError> {
241		setup_pot_account::<T, I>();
242		let (curator_lookup, _) = create_bounty::<T, I>()?;
243		Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
244
245		let bounty_id = BountyCount::<T, I>::get() - 1;
246		let curator = T::Lookup::lookup(curator_lookup).map_err(<&str>::from)?;
247
248		let beneficiary_account: T::AccountId = account("beneficiary", 0, SEED);
249		let beneficiary = T::Lookup::unlookup(beneficiary_account.clone());
250		Bounties::<T, I>::award_bounty(
251			RawOrigin::Signed(curator.clone()).into(),
252			bounty_id,
253			beneficiary,
254		)?;
255
256		set_block_number::<T, I>(
257			T::SpendPeriod::get() + T::BountyDepositPayoutDelay::get() + 1u32.into(),
258		);
259		assert!(
260			T::Currency::free_balance(&beneficiary_account).is_zero(),
261			"Beneficiary already has balance"
262		);
263
264		#[extrinsic_call]
265		_(RawOrigin::Signed(curator), bounty_id);
266
267		assert!(
268			!T::Currency::free_balance(&beneficiary_account).is_zero(),
269			"Beneficiary didn't get paid"
270		);
271
272		Ok(())
273	}
274
275	#[benchmark]
276	fn close_bounty_proposed() -> Result<(), BenchmarkError> {
277		setup_pot_account::<T, I>();
278		let (caller, _, _, value, reason) = setup_bounty::<T, I>(0, 0);
279		Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller).into(), value, reason)?;
280		let bounty_id = BountyCount::<T, I>::get() - 1;
281		let approve_origin =
282			T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
283
284		#[extrinsic_call]
285		close_bounty(approve_origin, bounty_id);
286
287		Ok(())
288	}
289
290	#[benchmark]
291	fn close_bounty_active() -> Result<(), BenchmarkError> {
292		setup_pot_account::<T, I>();
293		let (_, _) = create_bounty::<T, I>()?;
294		Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
295		let bounty_id = BountyCount::<T, I>::get() - 1;
296		let approve_origin =
297			T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
298
299		#[extrinsic_call]
300		close_bounty(approve_origin as T::RuntimeOrigin, bounty_id);
301
302		assert_last_event::<T, I>(Event::BountyCanceled { index: bounty_id }.into());
303
304		Ok(())
305	}
306
307	#[benchmark]
308	fn extend_bounty_expiry() -> Result<(), BenchmarkError> {
309		setup_pot_account::<T, I>();
310		let (curator_lookup, _) = create_bounty::<T, I>()?;
311		Treasury::<T, I>::on_initialize(frame_system::Pallet::<T>::block_number());
312
313		let bounty_id = BountyCount::<T, I>::get() - 1;
314		let curator = T::Lookup::lookup(curator_lookup).map_err(<&str>::from)?;
315
316		#[extrinsic_call]
317		_(RawOrigin::Signed(curator), bounty_id, Vec::new());
318
319		assert_last_event::<T, I>(Event::BountyExtended { index: bounty_id }.into());
320
321		Ok(())
322	}
323
324	#[benchmark]
325	fn spend_funds(b: Linear<0, 100>) -> Result<(), BenchmarkError> {
326		setup_pot_account::<T, I>();
327		create_approved_bounties::<T, I>(b)?;
328
329		let mut budget_remaining = BalanceOf::<T, I>::max_value();
330		let mut imbalance = PositiveImbalanceOf::<T, I>::zero();
331		let mut total_weight = Weight::zero();
332		let mut missed_any = false;
333
334		#[block]
335		{
336			<Bounties<T, I> as pallet_treasury::SpendFunds<T, I>>::spend_funds(
337				&mut budget_remaining,
338				&mut imbalance,
339				&mut total_weight,
340				&mut missed_any,
341			);
342		}
343
344		assert!(!missed_any, "Missed some");
345
346		if b > 0 {
347			assert!(budget_remaining < BalanceOf::<T, I>::max_value(), "Budget not used");
348			assert_last_event::<T, I>(Event::BountyBecameActive { index: b - 1 }.into());
349		} else {
350			assert!(budget_remaining == BalanceOf::<T, I>::max_value(), "Budget used");
351		}
352
353		Ok(())
354	}
355
356	#[benchmark]
357	fn poke_deposit() -> Result<(), BenchmarkError> {
358		// Create a bounty
359		let (caller, _, _, value, reason) = setup_bounty::<T, I>(0, 5); // 5 bytes description
360		Bounties::<T, I>::propose_bounty(RawOrigin::Signed(caller.clone()).into(), value, reason)?;
361		let bounty_id = BountyCount::<T, I>::get() - 1;
362		let old_deposit = T::Currency::reserved_balance(&caller);
363		// Modify the description to be maximum length
364		let max_description: Vec<u8> = vec![0; T::MaximumReasonLength::get() as usize];
365		let bounded_description: BoundedVec<u8, T::MaximumReasonLength> =
366			max_description.try_into().unwrap();
367		BountyDescriptions::<T, I>::insert(bounty_id, &bounded_description);
368
369		// Ensure caller has enough balance for new deposit
370		let new_deposit = Bounties::<T, I>::calculate_bounty_deposit(&bounded_description);
371		let required_balance = new_deposit.saturating_add(minimum_balance::<T, I>());
372		T::Currency::make_free_balance_be(&caller, required_balance);
373
374		#[extrinsic_call]
375		_(RawOrigin::Signed(caller.clone()), bounty_id);
376
377		let bounty = crate::Bounties::<T, I>::get(bounty_id).unwrap();
378		assert_eq!(bounty.bond, new_deposit);
379		assert_eq!(T::Currency::reserved_balance(&caller), new_deposit);
380		assert_last_event::<T, I>(
381			Event::DepositPoked { bounty_id, proposer: caller, old_deposit, new_deposit }.into(),
382		);
383
384		Ok(())
385	}
386	impl_benchmark_test_suite!(
387		Bounties,
388		crate::tests::ExtBuilder::default().build(),
389		crate::tests::Test
390	);
391}