referrerpolicy=no-referrer-when-downgrade

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