Skip to main content

pezpallet_asset_conversion_ops/
lib.rs

1// This file is part of Bizinikiwi.
2
3// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
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 Operations Suite.
19//!
20//! This pezpallet provides operational functionalities for the Asset Conversion pezpallet,
21//! allowing you to perform various migration and one-time-use operations. These operations
22//! are designed to facilitate updates and changes to the Asset Conversion pezpallet without
23//! breaking its API.
24//!
25//! ## Overview
26//!
27//! This suite allows you to perform the following operations:
28//! - Perform migration to update account ID derivation methods for existing pools. The migration
29//!   operation ensures that the required accounts are created, existing account deposits are
30//!   transferred, and liquidity is moved to the new accounts.
31
32#![deny(missing_docs)]
33#![cfg_attr(not(feature = "std"), no_std)]
34
35#[cfg(feature = "runtime-benchmarks")]
36mod benchmarking;
37#[cfg(test)]
38mod mock;
39#[cfg(test)]
40mod tests;
41pub mod weights;
42pub use pezpallet::*;
43pub use weights::WeightInfo;
44
45extern crate alloc;
46
47use alloc::boxed::Box;
48use pezframe_support::traits::{
49	fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate},
50	fungibles::{roles::ResetTeam, Inspect, Mutate, Refund},
51	tokens::{Fortitude, Precision, Preservation},
52	AccountTouch,
53};
54use pezpallet_asset_conversion::{PoolLocator, Pools};
55use pezsp_runtime::traits::{TryConvert, Zero};
56
57#[pezframe_support::pezpallet]
58pub mod pezpallet {
59	use super::*;
60	use pezframe_support::pezpallet_prelude::*;
61	use pezframe_system::pezpallet_prelude::*;
62
63	#[pezpallet::pezpallet]
64	pub struct Pezpallet<T>(_);
65
66	#[pezpallet::config]
67	pub trait Config:
68		pezpallet_asset_conversion::Config<
69			PoolId = (
70				<Self as pezpallet_asset_conversion::Config>::AssetKind,
71				<Self as pezpallet_asset_conversion::Config>::AssetKind,
72			),
73		> + pezframe_system::Config
74	{
75		/// Overarching event type.
76		#[allow(deprecated)]
77		type RuntimeEvent: From<Event<Self>>
78			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
79
80		/// Type previously used to derive the account ID for a pool. Indicates that the pool's
81		/// liquidity assets are located at this account before the migration.
82		type PriorAccountIdConverter: for<'a> TryConvert<
83			&'a (Self::AssetKind, Self::AssetKind),
84			Self::AccountId,
85		>;
86
87		/// Retrieves information about an existing deposit for a given account ID and asset from
88		/// the [`pezpallet_asset_conversion::Config::Assets`] registry and can initiate the refund.
89		type AssetsRefund: Refund<
90			Self::AccountId,
91			AssetId = Self::AssetKind,
92			Balance = <Self::DepositAsset as FungibleInspect<Self::AccountId>>::Balance,
93		>;
94
95		/// Retrieves information about an existing deposit for a given account ID and asset from
96		/// the [`pezpallet_asset_conversion::Config::PoolAssets`] registry and can initiate the
97		/// refund.
98		type PoolAssetsRefund: Refund<
99			Self::AccountId,
100			AssetId = Self::PoolAssetId,
101			Balance = <Self::DepositAsset as FungibleInspect<Self::AccountId>>::Balance,
102		>;
103
104		/// Means to reset the team for assets from the
105		/// [`pezpallet_asset_conversion::Config::PoolAssets`] registry.
106		type PoolAssetsTeam: ResetTeam<Self::AccountId, AssetId = Self::PoolAssetId>;
107
108		/// Registry of an asset used as an account deposit for the
109		/// [`pezpallet_asset_conversion::Config::Assets`] and
110		/// [`pezpallet_asset_conversion::Config::PoolAssets`] registries.
111		type DepositAsset: FungibleMutate<Self::AccountId>;
112
113		/// Weight information for extrinsics in this pezpallet.
114		type WeightInfo: WeightInfo;
115	}
116
117	// Pezpallet's events.
118	#[pezpallet::event]
119	#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
120	pub enum Event<T: Config> {
121		/// Indicates that a pool has been migrated to the new account ID.
122		MigratedToNewAccount {
123			/// Pool's ID.
124			pool_id: T::PoolId,
125			/// Pool's prior account ID.
126			prior_account: T::AccountId,
127			/// Pool's new account ID.
128			new_account: T::AccountId,
129		},
130	}
131
132	#[pezpallet::error]
133	pub enum Error<T> {
134		/// Provided asset pair is not supported for pool.
135		InvalidAssetPair,
136		/// The pool doesn't exist.
137		PoolNotFound,
138		/// Pool's balance cannot be zero.
139		ZeroBalance,
140		/// Indicates a partial transfer of balance to the new account during a migration.
141		PartialTransfer,
142	}
143
144	/// Pezpallet's callable functions.
145	#[pezpallet::call]
146	impl<T: Config> Pezpallet<T> {
147		/// Migrates an existing pool to a new account ID derivation method for a given asset pair.
148		/// If the migration is successful, transaction fees are refunded to the caller.
149		///
150		/// Must be signed.
151		#[pezpallet::call_index(0)]
152		#[pezpallet::weight(<T as Config>::WeightInfo::migrate_to_new_account())]
153		pub fn migrate_to_new_account(
154			origin: OriginFor<T>,
155			asset1: Box<T::AssetKind>,
156			asset2: Box<T::AssetKind>,
157		) -> DispatchResultWithPostInfo {
158			ensure_signed(origin)?;
159
160			let pool_id = T::PoolLocator::pool_id(&asset1, &asset2)
161				.map_err(|_| Error::<T>::InvalidAssetPair)?;
162			let info = Pools::<T>::get(&pool_id).ok_or(Error::<T>::PoolNotFound)?;
163
164			let (prior_account, new_account) =
165				Self::addresses(&pool_id).ok_or(Error::<T>::InvalidAssetPair)?;
166
167			let (asset1, asset2) = pool_id.clone();
168
169			// Assets that must be transferred to the new account id.
170			let balance1 = T::Assets::total_balance(asset1.clone(), &prior_account);
171			let balance2 = T::Assets::total_balance(asset2.clone(), &prior_account);
172			let lp_balance = T::PoolAssets::total_balance(info.lp_token.clone(), &prior_account);
173
174			ensure!(!balance1.is_zero(), Error::<T>::ZeroBalance);
175			ensure!(!balance2.is_zero(), Error::<T>::ZeroBalance);
176			ensure!(!lp_balance.is_zero(), Error::<T>::ZeroBalance);
177
178			// Check if a deposit needs to be placed for the new account. If so, mint the
179			// required deposit amount to the depositor's account to ensure the deposit can be
180			// provided. Once the deposit from the prior account is returned, the minted assets will
181			// be burned. Touching the new account is necessary because it's not possible to
182			// transfer assets to the new account if it's required. Additionally, the deposit cannot
183			// be refunded from the prior account until its balance is zero.
184
185			let deposit_asset_ed = T::DepositAsset::minimum_balance();
186
187			if let Some((depositor, deposit)) =
188				T::AssetsRefund::deposit_held(asset1.clone(), prior_account.clone())
189			{
190				T::DepositAsset::mint_into(&depositor, deposit + deposit_asset_ed)?;
191				T::Assets::touch(asset1.clone(), &new_account, &depositor)?;
192			}
193
194			if let Some((depositor, deposit)) =
195				T::AssetsRefund::deposit_held(asset2.clone(), prior_account.clone())
196			{
197				T::DepositAsset::mint_into(&depositor, deposit + deposit_asset_ed)?;
198				T::Assets::touch(asset2.clone(), &new_account, &depositor)?;
199			}
200
201			if let Some((depositor, deposit)) =
202				T::PoolAssetsRefund::deposit_held(info.lp_token.clone(), prior_account.clone())
203			{
204				T::DepositAsset::mint_into(&depositor, deposit + deposit_asset_ed)?;
205				T::PoolAssets::touch(info.lp_token.clone(), &new_account, &depositor)?;
206			}
207
208			// Transfer all pool related assets to the new account.
209
210			ensure!(
211				balance1
212					== T::Assets::transfer(
213						asset1.clone(),
214						&prior_account,
215						&new_account,
216						balance1,
217						Preservation::Expendable,
218					)?,
219				Error::<T>::PartialTransfer
220			);
221
222			ensure!(
223				balance2
224					== T::Assets::transfer(
225						asset2.clone(),
226						&prior_account,
227						&new_account,
228						balance2,
229						Preservation::Expendable,
230					)?,
231				Error::<T>::PartialTransfer
232			);
233
234			ensure!(
235				lp_balance
236					== T::PoolAssets::transfer(
237						info.lp_token.clone(),
238						&prior_account,
239						&new_account,
240						lp_balance,
241						Preservation::Expendable,
242					)?,
243				Error::<T>::PartialTransfer
244			);
245
246			// Refund deposits from prior accounts and burn previously minted assets.
247
248			if let Some((depositor, deposit)) =
249				T::AssetsRefund::deposit_held(asset1.clone(), prior_account.clone())
250			{
251				T::AssetsRefund::refund(asset1.clone(), prior_account.clone())?;
252				T::DepositAsset::burn_from(
253					&depositor,
254					deposit + deposit_asset_ed,
255					Preservation::Expendable,
256					Precision::Exact,
257					Fortitude::Force,
258				)?;
259			}
260
261			if let Some((depositor, deposit)) =
262				T::AssetsRefund::deposit_held(asset2.clone(), prior_account.clone())
263			{
264				T::AssetsRefund::refund(asset2.clone(), prior_account.clone())?;
265				T::DepositAsset::burn_from(
266					&depositor,
267					deposit + deposit_asset_ed,
268					Preservation::Expendable,
269					Precision::Exact,
270					Fortitude::Force,
271				)?;
272			}
273
274			if let Some((depositor, deposit)) =
275				T::PoolAssetsRefund::deposit_held(info.lp_token.clone(), prior_account.clone())
276			{
277				T::PoolAssetsRefund::refund(info.lp_token.clone(), prior_account.clone())?;
278				T::DepositAsset::burn_from(
279					&depositor,
280					deposit + deposit_asset_ed,
281					Preservation::Expendable,
282					Precision::Exact,
283					Fortitude::Force,
284				)?;
285			}
286
287			T::PoolAssetsTeam::reset_team(
288				info.lp_token,
289				new_account.clone(),
290				new_account.clone(),
291				new_account.clone(),
292				new_account.clone(),
293			)?;
294
295			Self::deposit_event(Event::MigratedToNewAccount {
296				pool_id,
297				prior_account,
298				new_account,
299			});
300
301			Ok(Pays::No.into())
302		}
303	}
304
305	impl<T: Config> Pezpallet<T> {
306		/// Returns the prior and new account IDs for a given pool ID. The prior account ID comes
307		/// first in the tuple.
308		#[cfg(not(any(test, feature = "runtime-benchmarks")))]
309		fn addresses(pool_id: &T::PoolId) -> Option<(T::AccountId, T::AccountId)> {
310			match (
311				T::PriorAccountIdConverter::try_convert(pool_id),
312				T::PoolLocator::address(pool_id),
313			) {
314				(Ok(a), Ok(b)) if a != b => Some((a, b)),
315				_ => None,
316			}
317		}
318
319		/// Returns the prior and new account IDs for a given pool ID. The prior account ID comes
320		/// first in the tuple.
321		///
322		/// This function is intended for use only in test and benchmark environments. The prior
323		/// account ID represents the new account ID from [`Config::PoolLocator`], allowing the use
324		/// of the main pezpallet's calls to set up a pool with liquidity placed in that account and
325		/// migrate it to another account, which in this case is the result of
326		/// [`Config::PriorAccountIdConverter`].
327		#[cfg(any(test, feature = "runtime-benchmarks"))]
328		pub(crate) fn addresses(pool_id: &T::PoolId) -> Option<(T::AccountId, T::AccountId)> {
329			match (
330				T::PoolLocator::address(pool_id),
331				T::PriorAccountIdConverter::try_convert(pool_id),
332			) {
333				(Ok(a), Ok(b)) if a != b => Some((a, b)),
334				_ => None,
335			}
336		}
337	}
338}