referrerpolicy=no-referrer-when-downgrade

pallet_psm/
benchmarking.rs

1// This file is part of Substrate.
2
3// Copyright (C) Amforc AG.
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//! Benchmarking setup for pallet-psm
19
20use super::*;
21use crate::Pallet as Psm;
22use alloc::boxed::Box;
23use frame_benchmarking::v2::*;
24use frame_support::traits::{
25	fungibles::{
26		metadata::Inspect as FungiblesMetadataInspect, Create as FungiblesCreate,
27		Inspect as FungiblesInspect, Mutate as FungiblesMutate,
28	},
29	Consideration, EnsureOriginWithArg, Get,
30};
31use frame_system::RawOrigin;
32use pallet::BalanceOf;
33use sp_runtime::{Permill, Saturating};
34
35/// Asset-ID indices passed to `BenchmarkHelper::get_asset_id`. Chosen to avoid
36/// collision with typical genesis assets.
37const INTERNAL_ASSET_INDEX: u32 = 50;
38const EXTERNAL_ASSET_OFFSET: u32 = 100;
39/// Minimum swap amount seeded for the benchmarked PSM, in internal-asset units.
40const BENCH_MIN_SWAP: u32 = 1_000;
41
42/// Ensure the benchmarked internal asset exists and a PSM record is installed for
43/// it. Returns `(internal_asset_id, internal_decimals)`.
44fn ensure_internal_setup<T: Config>() -> (T::AssetId, u8)
45where
46	T::Fungibles: FungiblesCreate<T::AccountId>,
47{
48	let admin: T::AccountId = whitelisted_caller();
49	let _ = frame_system::Pallet::<T>::inc_providers(&admin);
50	let internal_id: T::AssetId = T::BenchmarkHelper::get_asset_id(INTERNAL_ASSET_INDEX);
51	if !T::Fungibles::asset_exists(internal_id.clone()) {
52		let _ = T::Fungibles::create(internal_id.clone(), admin.clone(), true, 1u32.into());
53	}
54	let internal_decimals = T::Fungibles::decimals(internal_id.clone());
55	if !crate::Psm::<T>::contains_key(&internal_id) {
56		let origin = T::CreateOrigin::try_successful_origin(&internal_id)
57			.expect("benchmark CreateOrigin is available");
58		if let Some(depositor) = T::CreateOrigin::ensure_origin(origin.clone(), &internal_id)
59			.expect("benchmark CreateOrigin succeeds")
60		{
61			T::Consideration::ensure_successful(&depositor, Psm::<T>::psm_creation_footprint());
62		}
63		let root_origin: T::PalletsOrigin = RawOrigin::Root.into();
64		Psm::<T>::create_psm(
65			origin,
66			internal_id.clone(),
67			Box::new(root_origin.clone()),
68			Box::new(root_origin),
69			admin,
70			BalanceOf::<T>::from(u32::MAX).saturating_mul(1_000_000u32.into()),
71			BalanceOf::<T>::from(BENCH_MIN_SWAP),
72		)
73		.expect("benchmark PSM creation succeeds");
74		// So the reserve can receive any external asset, sufficient or not.
75		frame_system::Pallet::<T>::inc_providers(&Psm::<T>::psm_account(&internal_id));
76	}
77	(internal_id, internal_decimals)
78}
79
80/// Set up `n` external assets ready for PSM benchmarks. Returns
81/// `(internal_asset_id, target_external_id)`.
82///
83/// Creates the target external asset (`EXTERNAL_ASSET_OFFSET`) and the internal
84/// asset, registers `n` external assets, and configures ceiling weights so the
85/// target can absorb the full mint amount.
86///
87/// Assets beyond the target are filler, they only populate PSM storage so the
88/// iterators in `total_psm_debt()` and `max_asset_debt()` touch `n` entries
89/// during `mint()`.
90fn setup_assets<T: Config>(n: u32) -> (T::AssetId, T::AssetId)
91where
92	T::Fungibles: FungiblesCreate<T::AccountId>,
93{
94	let admin: T::AccountId = whitelisted_caller();
95	let _ = frame_system::Pallet::<T>::inc_providers(&admin);
96
97	let (internal_id, internal_decimals) = ensure_internal_setup::<T>();
98
99	// Target asset: create + set metadata via the runtime-provided benchmark
100	// helper. Setting metadata requires reserving a native deposit, which the
101	// helper handles by funding `admin` first — something the fungibles traits
102	// alone cannot express.
103	let target_id: T::AssetId = T::BenchmarkHelper::get_asset_id(EXTERNAL_ASSET_OFFSET);
104	if !T::Fungibles::asset_exists(target_id.clone()) {
105		T::BenchmarkHelper::create_asset(target_id.clone(), &admin, internal_decimals);
106	}
107
108	// Filler assets only populate PSM storage so mint()'s iterators touch `n`
109	// entries. They are never swapped against; we still seed `internal_decimals`
110	// so the storage shape matches the target row.
111	for i in 0..n {
112		let id: T::AssetId = T::BenchmarkHelper::get_asset_id(EXTERNAL_ASSET_OFFSET + i);
113		crate::ExternalAssets::<T>::insert(
114			&internal_id,
115			&id,
116			crate::ExternalAssetInfo {
117				status: CircuitBreakerLevel::AllEnabled,
118				decimals: internal_decimals,
119			},
120		);
121		crate::AssetCeilingWeight::<T>::insert(&internal_id, &id, Permill::from_percent(1));
122		crate::PsmDebt::<T>::insert(&internal_id, &id, BalanceOf::<T>::from(1u32));
123	}
124	// Target-specific: dominant weight so it can absorb the full mint amount.
125	crate::AssetCeilingWeight::<T>::insert(&internal_id, &target_id, Permill::from_percent(100));
126
127	// Keep `external_count` consistent with the rows we wrote.
128	crate::Psm::<T>::mutate(&internal_id, |maybe| {
129		if let Some(info) = maybe.as_mut() {
130			info.external_count = n.max(1);
131		}
132	});
133
134	(internal_id, target_id)
135}
136
137#[benchmarks(
138	where
139		T::Fungibles: FungiblesCreate<T::AccountId>,
140)]
141mod benchmarks {
142	use super::*;
143
144	/// Linear in `n`. The number of registered external assets, because
145	/// `total_psm_debt()` iterates `PsmDebt` and `max_asset_debt()` iterates
146	/// `AssetCeilingWeight`.
147	#[benchmark]
148	fn mint(n: Linear<1, { T::MaxExternals::get() }>) -> Result<(), BenchmarkError> {
149		let caller: T::AccountId = whitelisted_caller();
150		let (internal_id, asset_id) = setup_assets::<T>(n);
151		let mint_amount = BalanceOf::<T>::from(BENCH_MIN_SWAP).saturating_mul(10u32.into());
152
153		T::Fungibles::mint_into(asset_id.clone(), &caller, mint_amount.saturating_mul(2u32.into()))
154			.map_err(|_| BenchmarkError::Stop("Failed to fund caller"))?;
155
156		let psm_account = Psm::<T>::psm_account(&internal_id);
157		let reserve_before = T::Fungibles::balance(asset_id.clone(), &psm_account);
158
159		#[extrinsic_call]
160		_(
161			RawOrigin::Signed(caller.clone()),
162			internal_id.clone(),
163			asset_id.clone(),
164			mint_amount,
165			MintingFee::<T>::get(&internal_id, &asset_id),
166		);
167
168		assert!(T::Fungibles::balance(asset_id, &psm_account) > reserve_before);
169		Ok(())
170	}
171
172	#[benchmark]
173	fn redeem() -> Result<(), BenchmarkError> {
174		let caller: T::AccountId = whitelisted_caller();
175		let (internal_id, asset_id) = setup_assets::<T>(1);
176		let setup_amount = BalanceOf::<T>::from(BENCH_MIN_SWAP).saturating_mul(10u32.into());
177		let redeem_amount = BalanceOf::<T>::from(BENCH_MIN_SWAP);
178
179		T::Fungibles::mint_into(
180			asset_id.clone(),
181			&caller,
182			setup_amount.saturating_mul(2u32.into()),
183		)
184		.map_err(|_| BenchmarkError::Stop("Failed to fund caller"))?;
185		Psm::<T>::mint(
186			RawOrigin::Signed(caller.clone()).into(),
187			internal_id.clone(),
188			asset_id.clone(),
189			setup_amount,
190			MintingFee::<T>::get(&internal_id, &asset_id),
191		)
192		.map_err(|_| BenchmarkError::Stop("Failed to setup reserve via mint"))?;
193
194		let psm_account = Psm::<T>::psm_account(&internal_id);
195		let reserve_before = T::Fungibles::balance(asset_id.clone(), &psm_account);
196
197		#[extrinsic_call]
198		_(
199			RawOrigin::Signed(caller.clone()),
200			internal_id.clone(),
201			asset_id.clone(),
202			redeem_amount,
203			RedemptionFee::<T>::get(&internal_id, &asset_id),
204		);
205
206		assert!(T::Fungibles::balance(asset_id, &psm_account) < reserve_before);
207		Ok(())
208	}
209
210	#[benchmark]
211	fn set_minting_fee() -> Result<(), BenchmarkError> {
212		let (internal_id, asset_id) = setup_assets::<T>(1);
213		let new_fee = Permill::from_percent(2);
214
215		#[extrinsic_call]
216		_(RawOrigin::Root, internal_id.clone(), asset_id.clone(), new_fee);
217
218		assert_eq!(crate::MintingFee::<T>::get(&internal_id, &asset_id), new_fee);
219		Ok(())
220	}
221
222	#[benchmark]
223	fn set_redemption_fee() -> Result<(), BenchmarkError> {
224		let (internal_id, asset_id) = setup_assets::<T>(1);
225		let new_fee = Permill::from_percent(2);
226
227		#[extrinsic_call]
228		_(RawOrigin::Root, internal_id.clone(), asset_id.clone(), new_fee);
229
230		assert_eq!(crate::RedemptionFee::<T>::get(&internal_id, &asset_id), new_fee);
231		Ok(())
232	}
233
234	#[benchmark]
235	fn set_max_debt() -> Result<(), BenchmarkError> {
236		let (internal_id, _) = ensure_internal_setup::<T>();
237		let new_value = BalanceOf::<T>::from(123u32);
238
239		#[extrinsic_call]
240		_(RawOrigin::Root, internal_id.clone(), new_value);
241
242		assert_eq!(crate::Psm::<T>::get(&internal_id).unwrap().max_debt, new_value);
243		Ok(())
244	}
245
246	#[benchmark]
247	fn set_asset_status() -> Result<(), BenchmarkError> {
248		let (internal_id, asset_id) = setup_assets::<T>(1);
249		let new_status = CircuitBreakerLevel::MintingDisabled;
250
251		#[extrinsic_call]
252		_(RawOrigin::Root, internal_id.clone(), asset_id.clone(), new_status);
253
254		assert_eq!(
255			crate::ExternalAssets::<T>::get(&internal_id, &asset_id).map(|e| e.status),
256			Some(new_status),
257		);
258		Ok(())
259	}
260
261	#[benchmark]
262	fn set_asset_ceiling_weight() -> Result<(), BenchmarkError> {
263		let (internal_id, asset_id) = setup_assets::<T>(1);
264		let new_weight = Permill::from_percent(50);
265
266		#[extrinsic_call]
267		_(RawOrigin::Root, internal_id.clone(), asset_id.clone(), new_weight);
268
269		assert_eq!(crate::AssetCeilingWeight::<T>::get(&internal_id, &asset_id), new_weight);
270		Ok(())
271	}
272	#[benchmark]
273	fn add_external_asset() -> Result<(), BenchmarkError> {
274		// Seed PsmInfo and ensure the internal asset exists; the extrinsic
275		// reads the snapshot and compares it against live metadata.
276		let (internal_id, internal_decimals) = ensure_internal_setup::<T>();
277		let caller: T::AccountId = whitelisted_caller();
278		let new_asset_id: T::AssetId = T::BenchmarkHelper::get_asset_id(EXTERNAL_ASSET_OFFSET);
279
280		// Worst case: a non-sufficient asset without a reserve asset account, so `can_deposit`
281		// also reads the reserve's system account.
282		let _ = T::Fungibles::create(new_asset_id.clone(), caller.clone(), false, 1u32.into());
283		T::BenchmarkHelper::create_asset(new_asset_id.clone(), &caller, internal_decimals);
284
285		#[extrinsic_call]
286		_(RawOrigin::Root, internal_id.clone(), new_asset_id.clone());
287
288		assert!(crate::ExternalAssets::<T>::contains_key(&internal_id, &new_asset_id));
289		Ok(())
290	}
291
292	#[benchmark]
293	fn remove_external_asset() -> Result<(), BenchmarkError> {
294		let (internal_id, asset_id) = setup_assets::<T>(1);
295		crate::PsmDebt::<T>::remove(&internal_id, &asset_id);
296
297		#[extrinsic_call]
298		_(RawOrigin::Root, internal_id.clone(), asset_id.clone());
299
300		assert!(!crate::ExternalAssets::<T>::contains_key(&internal_id, &asset_id));
301		Ok(())
302	}
303
304	#[benchmark]
305	fn create_psm() -> Result<(), BenchmarkError> {
306		let caller: T::AccountId = whitelisted_caller();
307		let _ = frame_system::Pallet::<T>::inc_providers(&caller);
308
309		// A fresh internal asset.
310		let internal_id: T::AssetId = T::BenchmarkHelper::get_asset_id(INTERNAL_ASSET_INDEX + 1);
311		if !T::Fungibles::asset_exists(internal_id.clone()) {
312			let _ = T::Fungibles::create(internal_id.clone(), caller.clone(), true, 1u32.into());
313		}
314
315		// The origin permitted to create, and the optional account it resolves to for deposit
316		// payment.
317		let origin = T::CreateOrigin::try_successful_origin(&internal_id)
318			.map_err(|_| BenchmarkError::Weightless)?;
319		let maybe_depositor = T::CreateOrigin::ensure_origin(origin.clone(), &internal_id)
320			.map_err(|_| BenchmarkError::Stop("CreateOrigin failed"))?;
321		if let Some(who) = &maybe_depositor {
322			T::Consideration::ensure_successful(who, Psm::<T>::psm_creation_footprint());
323		}
324
325		let admin: T::PalletsOrigin =
326			maybe_depositor.map(RawOrigin::Signed).unwrap_or(RawOrigin::Root).into();
327		let max_debt = BalanceOf::<T>::from(u32::MAX);
328		let min_swap = BalanceOf::<T>::from(BENCH_MIN_SWAP);
329
330		#[extrinsic_call]
331		_(
332			origin,
333			internal_id.clone(),
334			Box::new(admin.clone()),
335			Box::new(admin),
336			caller,
337			max_debt,
338			min_swap,
339		);
340
341		assert!(crate::Psm::<T>::contains_key(&internal_id));
342		Ok(())
343	}
344
345	#[benchmark]
346	fn remove_psm() -> Result<(), BenchmarkError> {
347		// `ensure_internal_setup` installs a PSM with `Root` as full admin, no externals and
348		// no debt: exactly the preconditions for removal.
349		let (internal_id, _) = ensure_internal_setup::<T>();
350
351		#[extrinsic_call]
352		_(RawOrigin::Root, internal_id.clone());
353
354		assert!(!crate::Psm::<T>::contains_key(&internal_id));
355		Ok(())
356	}
357
358	#[benchmark]
359	fn set_full_admin() -> Result<(), BenchmarkError> {
360		let (internal_id, _) = ensure_internal_setup::<T>();
361		let new_admin: T::PalletsOrigin = RawOrigin::Signed(whitelisted_caller()).into();
362
363		#[extrinsic_call]
364		_(RawOrigin::Root, internal_id.clone(), Box::new(new_admin.clone()));
365
366		assert_eq!(crate::PsmAdmin::<T>::get(&internal_id).unwrap().full_admin, new_admin);
367		Ok(())
368	}
369
370	#[benchmark]
371	fn set_emergency_admin() -> Result<(), BenchmarkError> {
372		let (internal_id, _) = ensure_internal_setup::<T>();
373		let new_admin: T::PalletsOrigin = RawOrigin::Signed(whitelisted_caller()).into();
374
375		#[extrinsic_call]
376		_(RawOrigin::Root, internal_id.clone(), Box::new(new_admin.clone()));
377
378		assert_eq!(crate::PsmAdmin::<T>::get(&internal_id).unwrap().emergency_admin, new_admin);
379		Ok(())
380	}
381
382	impl_benchmark_test_suite!(Psm, crate::mock::new_test_ext(), crate::mock::Test);
383}