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 frame_benchmarking::v2::*;
23use frame_support::traits::{
24	fungible::{metadata::Inspect, Create as FungibleCreate, Inspect as FungibleInspect},
25	fungibles::{
26		Create as FungiblesCreate, Inspect as FungiblesInspect, Mutate as FungiblesMutate,
27	},
28	Get,
29};
30use frame_system::RawOrigin;
31use pallet::BalanceOf;
32use sp_runtime::{traits::Zero, Permill, Saturating};
33
34/// Offset for benchmark asset IDs, chosen to avoid collision with typical
35/// genesis asset IDs (e.g. internal asset ID = 1).
36const ASSET_ID_OFFSET: u32 = 100;
37
38/// Ensure the internal asset exists and its decimals snapshot is written.
39/// The snapshot is consulted by mint/redeem via `ensure_decimals_match` and by
40/// `add_external_asset`; without it those paths fail closed. Returns the live
41/// internal decimals so callers can align external-asset metadata with it.
42fn ensure_internal_setup<T: Config>() -> u8
43where
44	T::InternalAsset: FungibleCreate<T::AccountId>,
45{
46	let admin: T::AccountId = whitelisted_caller();
47	let _ = frame_system::Pallet::<T>::inc_providers(&admin);
48	if T::InternalAsset::minimum_balance().is_zero() {
49		let _ = T::InternalAsset::create(admin, true, 1u32.into());
50	}
51	let internal_decimals = T::InternalAsset::decimals();
52	if !crate::InternalDecimals::<T>::exists() {
53		crate::InternalDecimals::<T>::put(internal_decimals);
54	}
55	internal_decimals
56}
57
58/// Set up `n` external assets ready for PSM benchmarks.
59///
60/// Creates the target asset (`ASSET_ID_OFFSET`) and the internal asset,
61/// registers `n` external assets (`ASSET_ID_OFFSET..+n`), and
62/// configures ceiling weights so the target can absorb the full mint amount.
63///
64/// Assets beyond the target are filler, they only populate PSM storage so
65/// the iterators in `total_psm_debt()` and `max_asset_debt()` touch `n`
66/// entries during `mint()`.
67fn setup_assets<T: Config>(n: u32) -> T::AssetId
68where
69	T::Fungibles: FungiblesCreate<T::AccountId>,
70	T::InternalAsset: FungibleCreate<T::AccountId>,
71{
72	let admin: T::AccountId = whitelisted_caller();
73	let _ = frame_system::Pallet::<T>::inc_providers(&admin);
74
75	let internal_decimals = ensure_internal_setup::<T>();
76
77	// Target asset: create + set metadata via the runtime-provided benchmark
78	// helper. Setting metadata requires reserving a native deposit, which the
79	// helper handles by funding `admin` first — something the fungibles traits
80	// alone cannot express.
81	let target_id: T::AssetId = T::BenchmarkHelper::get_asset_id(ASSET_ID_OFFSET);
82	if !T::Fungibles::asset_exists(target_id.clone()) {
83		T::BenchmarkHelper::create_asset(target_id.clone(), &admin, internal_decimals);
84	}
85
86	crate::MaxPsmDebtOfTotal::<T>::put(Permill::from_percent(100));
87	// Filler assets only populate PSM storage so mint()'s iterators touch `n`
88	// entries. They are never swapped against, so their underlying fungibles
89	// asset does not need to exist and no ExternalDecimals snapshot is required.
90	for i in 0..n {
91		let id: T::AssetId = T::BenchmarkHelper::get_asset_id(ASSET_ID_OFFSET + i);
92		crate::ExternalAssets::<T>::insert(&id, CircuitBreakerLevel::AllEnabled);
93		crate::AssetCeilingWeight::<T>::insert(&id, Permill::from_percent(1));
94		crate::PsmDebt::<T>::insert(&id, BalanceOf::<T>::from(1u32));
95	}
96	// Target-specific: dominant weight so it can absorb the full mint amount,
97	// and a decimals snapshot so `ensure_decimals_match` passes.
98	crate::AssetCeilingWeight::<T>::insert(&target_id, Permill::from_percent(100));
99	crate::ExternalDecimals::<T>::insert(&target_id, internal_decimals);
100
101	target_id
102}
103
104#[benchmarks(
105	where
106		T::Fungibles: FungiblesCreate<T::AccountId>,
107		T::InternalAsset: FungibleCreate<T::AccountId>,
108)]
109mod benchmarks {
110	use super::*;
111
112	/// Linear in `n`. The number of registered external assets, because
113	/// `total_psm_debt()` iterates `PsmDebt` and `max_asset_debt()` iterates
114	/// `AssetCeilingWeight`.
115	#[benchmark]
116	fn mint(n: Linear<1, { T::MaxExternalAssets::get() }>) -> Result<(), BenchmarkError> {
117		let caller: T::AccountId = whitelisted_caller();
118		let asset_id = setup_assets::<T>(n);
119		let mint_amount = T::MinSwapAmount::get().saturating_mul(10u32.into());
120
121		T::Fungibles::mint_into(asset_id.clone(), &caller, mint_amount.saturating_mul(2u32.into()))
122			.map_err(|_| BenchmarkError::Stop("Failed to fund caller"))?;
123
124		let psm_account = Psm::<T>::account_id();
125		let reserve_before = T::Fungibles::balance(asset_id.clone(), &psm_account);
126
127		#[extrinsic_call]
128		_(RawOrigin::Signed(caller.clone()), asset_id.clone(), mint_amount);
129
130		assert!(T::Fungibles::balance(asset_id, &psm_account) > reserve_before);
131		Ok(())
132	}
133
134	#[benchmark]
135	fn redeem() -> Result<(), BenchmarkError> {
136		let caller: T::AccountId = whitelisted_caller();
137		let asset_id = setup_assets::<T>(1);
138		let setup_amount = T::MinSwapAmount::get().saturating_mul(10u32.into());
139		let redeem_amount = T::MinSwapAmount::get();
140
141		T::Fungibles::mint_into(
142			asset_id.clone(),
143			&caller,
144			setup_amount.saturating_mul(2u32.into()),
145		)
146		.map_err(|_| BenchmarkError::Stop("Failed to fund caller"))?;
147		Psm::<T>::mint(RawOrigin::Signed(caller.clone()).into(), asset_id.clone(), setup_amount)
148			.map_err(|_| BenchmarkError::Stop("Failed to setup reserve via mint"))?;
149
150		let psm_account = Psm::<T>::account_id();
151		let reserve_before = T::Fungibles::balance(asset_id.clone(), &psm_account);
152
153		#[extrinsic_call]
154		_(RawOrigin::Signed(caller.clone()), asset_id.clone(), redeem_amount);
155
156		assert!(T::Fungibles::balance(asset_id, &psm_account) < reserve_before);
157		Ok(())
158	}
159
160	#[benchmark]
161	fn set_minting_fee() -> Result<(), BenchmarkError> {
162		let asset_id = setup_assets::<T>(1);
163		let new_fee = Permill::from_percent(2);
164
165		#[extrinsic_call]
166		_(RawOrigin::Root, asset_id.clone(), new_fee);
167
168		assert_eq!(crate::MintingFee::<T>::get(&asset_id), new_fee);
169		Ok(())
170	}
171
172	#[benchmark]
173	fn set_redemption_fee() -> Result<(), BenchmarkError> {
174		let asset_id = setup_assets::<T>(1);
175		let new_fee = Permill::from_percent(2);
176
177		#[extrinsic_call]
178		_(RawOrigin::Root, asset_id.clone(), new_fee);
179
180		assert_eq!(crate::RedemptionFee::<T>::get(&asset_id), new_fee);
181		Ok(())
182	}
183
184	#[benchmark]
185	fn set_max_psm_debt() -> Result<(), BenchmarkError> {
186		let new_ratio = Permill::from_percent(20);
187
188		#[extrinsic_call]
189		_(RawOrigin::Root, new_ratio);
190
191		assert_eq!(crate::MaxPsmDebtOfTotal::<T>::get(), new_ratio);
192		Ok(())
193	}
194
195	#[benchmark]
196	fn set_asset_status() -> Result<(), BenchmarkError> {
197		let asset_id = setup_assets::<T>(1);
198		let new_status = CircuitBreakerLevel::MintingDisabled;
199
200		#[extrinsic_call]
201		_(RawOrigin::Root, asset_id.clone(), new_status);
202
203		assert_eq!(crate::ExternalAssets::<T>::get(&asset_id), Some(new_status));
204		Ok(())
205	}
206
207	#[benchmark]
208	fn set_asset_ceiling_weight() -> Result<(), BenchmarkError> {
209		let asset_id = setup_assets::<T>(1);
210		let new_weight = Permill::from_percent(50);
211
212		#[extrinsic_call]
213		_(RawOrigin::Root, asset_id.clone(), new_weight);
214
215		assert_eq!(crate::AssetCeilingWeight::<T>::get(&asset_id), new_weight);
216		Ok(())
217	}
218	#[benchmark]
219	fn add_external_asset() -> Result<(), BenchmarkError> {
220		// Seed InternalDecimals and ensure the internal asset exists; the extrinsic
221		// reads the snapshot and compares it against live metadata.
222		let internal_decimals = ensure_internal_setup::<T>();
223		let caller: T::AccountId = whitelisted_caller();
224		let new_asset_id: T::AssetId = T::BenchmarkHelper::get_asset_id(ASSET_ID_OFFSET);
225
226		T::BenchmarkHelper::create_asset(new_asset_id.clone(), &caller, internal_decimals);
227
228		#[extrinsic_call]
229		_(RawOrigin::Root, new_asset_id.clone());
230
231		assert!(crate::ExternalAssets::<T>::contains_key(&new_asset_id));
232		Ok(())
233	}
234
235	#[benchmark]
236	fn remove_external_asset() -> Result<(), BenchmarkError> {
237		let asset_id = setup_assets::<T>(1);
238		crate::PsmDebt::<T>::remove(&asset_id);
239
240		#[extrinsic_call]
241		_(RawOrigin::Root, asset_id.clone());
242
243		assert!(!crate::ExternalAssets::<T>::contains_key(&asset_id));
244		Ok(())
245	}
246
247	impl_benchmark_test_suite!(Psm, crate::mock::new_test_ext(), crate::mock::Test);
248}