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