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