referrerpolicy=no-referrer-when-downgrade

pallet_asset_conversion/
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//! Asset Conversion pallet benchmarking.
19
20use super::*;
21use crate::Pallet as AssetConversion;
22use alloc::vec;
23use core::marker::PhantomData;
24use frame_benchmarking::{v2::*, whitelisted_caller};
25use frame_support::{
26	assert_ok,
27	traits::{
28		fungible::NativeOrWithId,
29		fungibles::{Create, Inspect, Mutate, Refund},
30		EnsureOrigin,
31	},
32};
33use frame_system::RawOrigin as SystemOrigin;
34use sp_core::Get;
35use sp_runtime::Permill;
36
37/// Benchmark Helper
38pub trait BenchmarkHelper<AssetKind> {
39	/// Returns a valid assets pair for the pool creation.
40	///
41	/// When a specific asset, such as the native asset, is required in every pool, it should be
42	/// returned for each odd-numbered seed.
43	fn create_pair(seed1: u32, seed2: u32) -> (AssetKind, AssetKind);
44}
45
46impl<AssetKind> BenchmarkHelper<AssetKind> for ()
47where
48	AssetKind: From<u32>,
49{
50	fn create_pair(seed1: u32, seed2: u32) -> (AssetKind, AssetKind) {
51		(seed1.into(), seed2.into())
52	}
53}
54
55/// Factory for creating a valid asset pairs with [`NativeOrWithId::Native`] always leading in the
56/// pair.
57pub struct NativeOrWithIdFactory<AssetId>(PhantomData<AssetId>);
58impl<AssetId: From<u32> + Ord> BenchmarkHelper<NativeOrWithId<AssetId>>
59	for NativeOrWithIdFactory<AssetId>
60{
61	fn create_pair(seed1: u32, seed2: u32) -> (NativeOrWithId<AssetId>, NativeOrWithId<AssetId>) {
62		if seed1.is_multiple_of(2) {
63			(NativeOrWithId::WithId(seed2.into()), NativeOrWithId::Native)
64		} else {
65			(NativeOrWithId::Native, NativeOrWithId::WithId(seed2.into()))
66		}
67	}
68}
69
70/// Provides a pair of amounts expected to serve as sufficient initial liquidity for a pool.
71fn valid_liquidity_amount<T: Config>(ed1: T::Balance, ed2: T::Balance) -> (T::Balance, T::Balance)
72where
73	T::Assets: Inspect<T::AccountId>,
74{
75	let l =
76		ed1.max(ed2) + T::MintMinLiquidity::get() + T::MintMinLiquidity::get() + T::Balance::one();
77	(l, l)
78}
79
80/// Create the `asset` and mint the `amount` for the `caller`.
81fn create_asset<T: Config>(
82	caller: &T::AccountId,
83	asset: &T::AssetKind,
84	amount: T::Balance,
85	is_sufficient: bool,
86) where
87	T::Assets: Create<T::AccountId> + Mutate<T::AccountId>,
88{
89	if !T::Assets::asset_exists(asset.clone()) {
90		assert_ok!(T::Assets::create(
91			asset.clone(),
92			caller.clone(),
93			is_sufficient,
94			T::Balance::one()
95		));
96	}
97	assert_ok!(T::Assets::mint_into(
98		asset.clone(),
99		&caller,
100		amount + T::Assets::minimum_balance(asset.clone())
101	));
102}
103
104/// Create the designated fee asset for pool creation.
105fn create_fee_asset<T: Config>(caller: &T::AccountId)
106where
107	T::Assets: Create<T::AccountId> + Mutate<T::AccountId>,
108{
109	let fee_asset = T::PoolSetupFeeAsset::get();
110	if !T::Assets::asset_exists(fee_asset.clone()) {
111		assert_ok!(T::Assets::create(fee_asset.clone(), caller.clone(), true, T::Balance::one()));
112	}
113	assert_ok!(T::Assets::mint_into(
114		fee_asset.clone(),
115		&caller,
116		T::Assets::minimum_balance(fee_asset)
117	));
118}
119
120/// Mint the fee asset for the `caller` sufficient to cover the fee for creating a new pool.
121fn mint_setup_fee_asset<T: Config>(
122	caller: &T::AccountId,
123	asset1: &T::AssetKind,
124	asset2: &T::AssetKind,
125	lp_token: &T::PoolAssetId,
126) where
127	T::Assets: Create<T::AccountId> + Mutate<T::AccountId>,
128{
129	assert_ok!(T::Assets::mint_into(
130		T::PoolSetupFeeAsset::get(),
131		&caller,
132		T::PoolSetupFee::get() +
133			T::Assets::deposit_required(asset1.clone()) +
134			T::Assets::deposit_required(asset2.clone()) +
135			T::PoolAssets::deposit_required(lp_token.clone())
136	));
137}
138
139/// Creates a pool for a given asset pair.
140///
141/// This action mints the necessary amounts of the given assets for the `caller` to provide initial
142/// liquidity. It returns the LP token ID along with a pair of amounts sufficient for the pool's
143/// initial liquidity.
144fn create_asset_and_pool<T: Config>(
145	caller: &T::AccountId,
146	asset1: &T::AssetKind,
147	asset2: &T::AssetKind,
148) -> (T::PoolAssetId, T::Balance, T::Balance)
149where
150	T::Assets: Create<T::AccountId> + Mutate<T::AccountId>,
151{
152	let (liquidity1, liquidity2) = valid_liquidity_amount::<T>(
153		T::Assets::minimum_balance(asset1.clone()),
154		T::Assets::minimum_balance(asset2.clone()),
155	);
156	create_asset::<T>(caller, asset1, liquidity1, true);
157	create_asset::<T>(caller, asset2, liquidity2, true);
158	let lp_token = AssetConversion::<T>::get_next_pool_asset_id();
159
160	mint_setup_fee_asset::<T>(caller, asset1, asset2, &lp_token);
161
162	assert_ok!(AssetConversion::<T>::create_pool(
163		SystemOrigin::Signed(caller.clone()).into(),
164		Box::new(asset1.clone()),
165		Box::new(asset2.clone())
166	));
167
168	(lp_token, liquidity1, liquidity2)
169}
170
171fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
172	let events = frame_system::Pallet::<T>::events();
173	let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
174	// compare to the last event record
175	let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
176	assert_eq!(event, &system_event);
177}
178
179#[benchmarks(where T::Assets: Create<T::AccountId> + Mutate<T::AccountId>, T::PoolAssetId: Into<u32>,)]
180mod benchmarks {
181	use super::*;
182
183	#[benchmark]
184	fn create_pool() {
185		let caller: T::AccountId = whitelisted_caller();
186		let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1);
187		create_asset::<T>(&caller, &asset1, T::Assets::minimum_balance(asset1.clone()), true);
188		create_asset::<T>(&caller, &asset2, T::Assets::minimum_balance(asset2.clone()), true);
189
190		let lp_token = AssetConversion::<T>::get_next_pool_asset_id();
191		create_fee_asset::<T>(&caller);
192		mint_setup_fee_asset::<T>(&caller, &asset1, &asset2, &lp_token);
193
194		#[extrinsic_call]
195		_(SystemOrigin::Signed(caller.clone()), Box::new(asset1.clone()), Box::new(asset2.clone()));
196
197		let pool_id = T::PoolLocator::pool_id(&asset1, &asset2).unwrap();
198		let pool_account = T::PoolLocator::address(&pool_id).unwrap();
199		assert_last_event::<T>(
200			Event::PoolCreated { creator: caller, pool_account, pool_id, lp_token }.into(),
201		);
202	}
203
204	#[benchmark]
205	fn create_pool_with_fee() -> Result<(), BenchmarkError> {
206		let caller: T::AccountId = whitelisted_caller();
207		let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1);
208		create_asset::<T>(&caller, &asset1, T::Assets::minimum_balance(asset1.clone()), true);
209		create_asset::<T>(&caller, &asset2, T::Assets::minimum_balance(asset2.clone()), true);
210
211		let lp_token = AssetConversion::<T>::get_next_pool_asset_id();
212		create_fee_asset::<T>(&caller);
213		mint_setup_fee_asset::<T>(&caller, &asset1, &asset2, &lp_token);
214
215		let fee = Permill::from_percent(1);
216		let origin =
217			T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
218
219		#[extrinsic_call]
220		_(
221			origin as T::RuntimeOrigin,
222			caller.clone(),
223			Box::new(asset1.clone()),
224			Box::new(asset2.clone()),
225			fee,
226		);
227
228		let pool_id = T::PoolLocator::pool_id(&asset1, &asset2).unwrap();
229		assert_eq!(PoolFees::<T>::get(&pool_id), Some(fee));
230		assert_last_event::<T>(Event::PoolFeeSet { pool_id, fee }.into());
231		Ok(())
232	}
233
234	#[benchmark]
235	fn set_pool_fee() -> Result<(), BenchmarkError> {
236		let caller: T::AccountId = whitelisted_caller();
237		let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1);
238
239		create_fee_asset::<T>(&caller);
240		let _ = create_asset_and_pool::<T>(&caller, &asset1, &asset2);
241
242		let pool_id = T::PoolLocator::pool_id(&asset1, &asset2).unwrap();
243		let fee = Permill::from_percent(1);
244		let origin =
245			T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
246
247		#[extrinsic_call]
248		_(origin as T::RuntimeOrigin, pool_id.clone(), fee);
249
250		assert_eq!(PoolFees::<T>::get(&pool_id), Some(fee));
251		assert_last_event::<T>(Event::PoolFeeSet { pool_id, fee }.into());
252		Ok(())
253	}
254
255	#[benchmark]
256	fn add_liquidity() {
257		let caller: T::AccountId = whitelisted_caller();
258		let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1);
259
260		create_fee_asset::<T>(&caller);
261		let (lp_token, liquidity1, liquidity2) =
262			create_asset_and_pool::<T>(&caller, &asset1, &asset2);
263
264		#[extrinsic_call]
265		_(
266			SystemOrigin::Signed(caller.clone()),
267			Box::new(asset1.clone()),
268			Box::new(asset2.clone()),
269			liquidity1,
270			liquidity2,
271			T::Balance::one(),
272			T::Balance::zero(),
273			caller.clone(),
274		);
275
276		let pool_account = T::PoolLocator::pool_address(&asset1, &asset2).unwrap();
277		let lp_minted =
278			AssetConversion::<T>::calc_lp_amount_for_zero_supply(&liquidity1, &liquidity2).unwrap();
279		assert_eq!(T::PoolAssets::balance(lp_token, &caller), lp_minted);
280		assert_eq!(T::Assets::balance(asset1, &pool_account), liquidity1);
281		assert_eq!(T::Assets::balance(asset2, &pool_account), liquidity2);
282	}
283
284	#[benchmark]
285	fn remove_liquidity() {
286		let caller: T::AccountId = whitelisted_caller();
287		let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1);
288
289		create_fee_asset::<T>(&caller);
290		let (lp_token, liquidity1, liquidity2) =
291			create_asset_and_pool::<T>(&caller, &asset1, &asset2);
292
293		let remove_lp_amount = T::Balance::one();
294
295		assert_ok!(AssetConversion::<T>::add_liquidity(
296			SystemOrigin::Signed(caller.clone()).into(),
297			Box::new(asset1.clone()),
298			Box::new(asset2.clone()),
299			liquidity1,
300			liquidity2,
301			T::Balance::one(),
302			T::Balance::zero(),
303			caller.clone(),
304		));
305		let total_supply =
306			<T::PoolAssets as Inspect<T::AccountId>>::total_issuance(lp_token.clone());
307
308		#[extrinsic_call]
309		_(
310			SystemOrigin::Signed(caller.clone()),
311			Box::new(asset1),
312			Box::new(asset2),
313			remove_lp_amount,
314			T::Balance::zero(),
315			T::Balance::zero(),
316			caller.clone(),
317		);
318
319		let new_total_supply = <T::PoolAssets as Inspect<T::AccountId>>::total_issuance(lp_token);
320		assert_eq!(new_total_supply, total_supply - remove_lp_amount);
321	}
322
323	#[benchmark]
324	fn swap_exact_tokens_for_tokens(n: Linear<2, { T::MaxSwapPathLength::get() }>) {
325		let mut swap_amount = T::Balance::one();
326		let mut path = vec![];
327
328		let caller: T::AccountId = whitelisted_caller();
329		create_fee_asset::<T>(&caller);
330		for n in 1..n {
331			let (asset1, asset2) = T::BenchmarkHelper::create_pair(n - 1, n);
332			swap_amount = swap_amount + T::Balance::one();
333			if path.len() == 0 {
334				path = vec![Box::new(asset1.clone()), Box::new(asset2.clone())];
335			} else {
336				path.push(Box::new(asset2.clone()));
337			}
338
339			let (_, liquidity1, liquidity2) = create_asset_and_pool::<T>(&caller, &asset1, &asset2);
340
341			assert_ok!(AssetConversion::<T>::add_liquidity(
342				SystemOrigin::Signed(caller.clone()).into(),
343				Box::new(asset1.clone()),
344				Box::new(asset2.clone()),
345				liquidity1,
346				liquidity2,
347				T::Balance::one(),
348				T::Balance::zero(),
349				caller.clone(),
350			));
351		}
352
353		let asset_in = *path.first().unwrap().clone();
354		assert_ok!(T::Assets::mint_into(
355			asset_in.clone(),
356			&caller,
357			swap_amount + T::Balance::one()
358		));
359		let init_caller_balance = T::Assets::balance(asset_in.clone(), &caller);
360
361		#[extrinsic_call]
362		_(
363			SystemOrigin::Signed(caller.clone()),
364			path,
365			swap_amount,
366			T::Balance::one(),
367			caller.clone(),
368			true,
369		);
370
371		let actual_balance = T::Assets::balance(asset_in, &caller);
372		assert_eq!(actual_balance, init_caller_balance - swap_amount);
373	}
374
375	#[benchmark]
376	fn swap_tokens_for_exact_tokens(n: Linear<2, { T::MaxSwapPathLength::get() }>) {
377		let mut max_swap_amount = T::Balance::one();
378		let mut path = vec![];
379
380		let caller: T::AccountId = whitelisted_caller();
381		create_fee_asset::<T>(&caller);
382		for n in 1..n {
383			let (asset1, asset2) = T::BenchmarkHelper::create_pair(n - 1, n);
384			max_swap_amount = max_swap_amount + T::Balance::one() + T::Balance::one();
385			if path.len() == 0 {
386				path = vec![Box::new(asset1.clone()), Box::new(asset2.clone())];
387			} else {
388				path.push(Box::new(asset2.clone()));
389			}
390
391			let (_, liquidity1, liquidity2) = create_asset_and_pool::<T>(&caller, &asset1, &asset2);
392
393			assert_ok!(AssetConversion::<T>::add_liquidity(
394				SystemOrigin::Signed(caller.clone()).into(),
395				Box::new(asset1.clone()),
396				Box::new(asset2.clone()),
397				liquidity1,
398				liquidity2,
399				T::Balance::one(),
400				T::Balance::zero(),
401				caller.clone(),
402			));
403		}
404
405		let asset_in = *path.first().unwrap().clone();
406		let asset_out = *path.last().unwrap().clone();
407		assert_ok!(T::Assets::mint_into(asset_in, &caller, max_swap_amount));
408		let init_caller_balance = T::Assets::balance(asset_out.clone(), &caller);
409
410		#[extrinsic_call]
411		_(
412			SystemOrigin::Signed(caller.clone()),
413			path,
414			T::Balance::one(),
415			max_swap_amount,
416			caller.clone(),
417			true,
418		);
419
420		let actual_balance = T::Assets::balance(asset_out, &caller);
421		assert_eq!(actual_balance, init_caller_balance + T::Balance::one());
422	}
423
424	#[benchmark]
425	fn touch(n: Linear<0, 3>) {
426		let caller: T::AccountId = whitelisted_caller();
427		let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1);
428		let pool_id = T::PoolLocator::pool_id(&asset1, &asset2).unwrap();
429		let pool_account = T::PoolLocator::address(&pool_id).unwrap();
430
431		create_fee_asset::<T>(&caller);
432		create_asset::<T>(&caller, &asset1, <T as Config>::Balance::one(), false);
433		create_asset::<T>(&caller, &asset2, <T as Config>::Balance::one(), false);
434		let lp_token = AssetConversion::<T>::get_next_pool_asset_id();
435		mint_setup_fee_asset::<T>(&caller, &asset1, &asset2, &lp_token);
436
437		assert_ok!(AssetConversion::<T>::create_pool(
438			SystemOrigin::Signed(caller.clone()).into(),
439			Box::new(asset1.clone()),
440			Box::new(asset2.clone())
441		));
442
443		if n > 0 &&
444			<T as Config>::Assets::deposit_held(asset1.clone(), pool_account.clone()).is_some()
445		{
446			let _ = <T as Config>::Assets::refund(asset1.clone(), pool_account.clone());
447		}
448		if n > 1 &&
449			<T as Config>::Assets::deposit_held(asset2.clone(), pool_account.clone()).is_some()
450		{
451			let _ = <T as Config>::Assets::refund(asset2.clone(), pool_account.clone());
452		}
453		if n > 2 &&
454			<T as Config>::PoolAssets::deposit_held(lp_token.clone(), pool_account.clone())
455				.is_some()
456		{
457			let _ = <T as Config>::PoolAssets::refund(lp_token, pool_account);
458		}
459
460		#[extrinsic_call]
461		_(SystemOrigin::Signed(caller.clone()), Box::new(asset1.clone()), Box::new(asset2.clone()));
462
463		assert_last_event::<T>(Event::Touched { pool_id, who: caller }.into());
464	}
465
466	#[benchmark]
467	fn get_reserves() {
468		let caller: T::AccountId = whitelisted_caller();
469		let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1);
470
471		create_fee_asset::<T>(&caller);
472		let (_, liquidity1, liquidity2) = create_asset_and_pool::<T>(&caller, &asset1, &asset2);
473
474		assert_ok!(AssetConversion::<T>::add_liquidity(
475			SystemOrigin::Signed(caller.clone()).into(),
476			Box::new(asset1.clone()),
477			Box::new(asset2.clone()),
478			liquidity1,
479			liquidity2,
480			T::Balance::one(),
481			T::Balance::zero(),
482			caller.clone(),
483		));
484
485		#[block]
486		{
487			AssetConversion::<T>::get_reserves(asset1, asset2).expect("pool has liquidity; qed");
488		}
489	}
490
491	impl_benchmark_test_suite!(AssetConversion, crate::mock::new_test_ext(), crate::mock::Test);
492}