diff --git a/Cargo.lock b/Cargo.lock index 27cb7af04d63..c66c78e97dfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15076,9 +15076,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.9.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989" +checksum = "f31999cfc7927c4e212e60fd50934ab40e8e8bfd2d493d6095d2d306bc0764d9" dependencies = [ "bytes", "rand 0.8.5", diff --git a/prdoc/pr_1747.prdoc b/prdoc/pr_1747.prdoc new file mode 100644 index 000000000000..d8031b96c95d --- /dev/null +++ b/prdoc/pr_1747.prdoc @@ -0,0 +1,13 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Implement Inactive balance tracking in Assets pallet + +doc: + - audience: Runtime Dev + description: | + The trait `fungibles::Unbalanced` provides methods to increase and decrease active issuance. + This PR addresses the lack of this feature in Assets, by introducing a new field `inactive` in the `Asset` struct and implementing the methods accordingly. + +crates: + - name: pallet-assets diff --git a/substrate/frame/assets/src/functions.rs b/substrate/frame/assets/src/functions.rs index 8791aaa736b3..592197597ebc 100644 --- a/substrate/frame/assets/src/functions.rs +++ b/substrate/frame/assets/src/functions.rs @@ -713,6 +713,7 @@ impl, I: 'static> Pallet { admin: owner.clone(), freezer: owner.clone(), supply: Zero::zero(), + inactive: Zero::zero(), deposit: Zero::zero(), min_balance, is_sufficient, diff --git a/substrate/frame/assets/src/impl_fungibles.rs b/substrate/frame/assets/src/impl_fungibles.rs index 123abeba8283..d5706270122a 100644 --- a/substrate/frame/assets/src/impl_fungibles.rs +++ b/substrate/frame/assets/src/impl_fungibles.rs @@ -164,7 +164,22 @@ impl, I: 'static> fungibles::Unbalanced for Pallet::mutate(&asset, |maybe_asset| match maybe_asset { + Some(ref mut asset) => { + // Inactive amount can't exceed supply + asset.inactive = asset.inactive.saturating_add(amount).max(asset.supply); + }, + None => log::error!("Called deactivate for nonexistent asset {:?}", asset), + }); + } + + fn reactivate(asset: Self::AssetId, amount: Self::Balance) { + Asset::::mutate(&asset, |maybe_asset| match maybe_asset { + Some(ref mut asset) => asset.inactive.saturating_reduce(amount), + None => log::error!("Called reactivate for nonexistent asset {:?}", asset), + }); + } } impl, I: 'static> fungibles::Create for Pallet { diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index 6891f04dfb51..d8592ce6b2e1 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -218,8 +218,8 @@ pub mod pallet { }; use frame_system::pallet_prelude::*; - /// The in-code storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -440,6 +440,7 @@ pub mod pallet { admin: owner.clone(), freezer: owner.clone(), supply: Zero::zero(), + inactive: Zero::zero(), deposit: Zero::zero(), min_balance: *min_balance, is_sufficient: *is_sufficient, @@ -666,6 +667,7 @@ pub mod pallet { admin: admin.clone(), freezer: admin.clone(), supply: Zero::zero(), + inactive: Zero::zero(), deposit, min_balance, is_sufficient: false, diff --git a/substrate/frame/assets/src/migration.rs b/substrate/frame/assets/src/migration.rs deleted file mode 100644 index dd7c12293e80..000000000000 --- a/substrate/frame/assets/src/migration.rs +++ /dev/null @@ -1,138 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::*; -use frame_support::traits::OnRuntimeUpgrade; -use log; - -#[cfg(feature = "try-runtime")] -use sp_runtime::TryRuntimeError; - -pub mod v1 { - use frame_support::{pallet_prelude::*, weights::Weight}; - - use super::*; - - #[derive(Decode)] - pub struct OldAssetDetails { - pub owner: AccountId, - pub issuer: AccountId, - pub admin: AccountId, - pub freezer: AccountId, - pub supply: Balance, - pub deposit: DepositBalance, - pub min_balance: Balance, - pub is_sufficient: bool, - pub accounts: u32, - pub sufficients: u32, - pub approvals: u32, - pub is_frozen: bool, - } - - impl OldAssetDetails { - fn migrate_to_v1(self) -> AssetDetails { - let status = if self.is_frozen { AssetStatus::Frozen } else { AssetStatus::Live }; - - AssetDetails { - owner: self.owner, - issuer: self.issuer, - admin: self.admin, - freezer: self.freezer, - supply: self.supply, - deposit: self.deposit, - min_balance: self.min_balance, - is_sufficient: self.is_sufficient, - accounts: self.accounts, - sufficients: self.sufficients, - approvals: self.approvals, - status, - } - } - } - - pub struct MigrateToV1(core::marker::PhantomData); - impl OnRuntimeUpgrade for MigrateToV1 { - fn on_runtime_upgrade() -> Weight { - let in_code_version = Pallet::::in_code_storage_version(); - let on_chain_version = Pallet::::on_chain_storage_version(); - if on_chain_version == 0 && in_code_version == 1 { - let mut translated = 0u64; - Asset::::translate::< - OldAssetDetails>, - _, - >(|_key, old_value| { - translated.saturating_inc(); - Some(old_value.migrate_to_v1()) - }); - in_code_version.put::>(); - log::info!( - target: LOG_TARGET, - "Upgraded {} pools, storage to version {:?}", - translated, - in_code_version - ); - T::DbWeight::get().reads_writes(translated + 1, translated + 1) - } else { - log::info!( - target: LOG_TARGET, - "Migration did not execute. This probably should be removed" - ); - T::DbWeight::get().reads(1) - } - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, TryRuntimeError> { - frame_support::ensure!( - Pallet::::on_chain_storage_version() == 0, - "must upgrade linearly" - ); - let prev_count = Asset::::iter().count(); - Ok((prev_count as u32).encode()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(prev_count: Vec) -> Result<(), TryRuntimeError> { - let prev_count: u32 = Decode::decode(&mut prev_count.as_slice()).expect( - "the state parameter should be something that was generated by pre_upgrade", - ); - let post_count = Asset::::iter().count() as u32; - ensure!( - prev_count == post_count, - "the asset count before and after the migration should be the same" - ); - - let in_code_version = Pallet::::in_code_storage_version(); - let on_chain_version = Pallet::::on_chain_storage_version(); - - frame_support::ensure!(in_code_version == 1, "must_upgrade"); - ensure!( - in_code_version == on_chain_version, - "after migration, the in_code_version and on_chain_version should be the same" - ); - - Asset::::iter().try_for_each(|(_id, asset)| -> Result<(), TryRuntimeError> { - ensure!( - asset.status == AssetStatus::Live || asset.status == AssetStatus::Frozen, - "assets should only be live or frozen. None should be in destroying status, or undefined state" - ); - Ok(()) - })?; - Ok(()) - } - } -} diff --git a/substrate/frame/assets/src/migration/mod.rs b/substrate/frame/assets/src/migration/mod.rs new file mode 100644 index 000000000000..aa13a113abf4 --- /dev/null +++ b/substrate/frame/assets/src/migration/mod.rs @@ -0,0 +1,21 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Version 0 to 1. +pub mod v1; +/// Version 1 to 2. +pub mod v2; diff --git a/substrate/frame/assets/src/migration/v1.rs b/substrate/frame/assets/src/migration/v1.rs new file mode 100644 index 000000000000..9a0c402bed9d --- /dev/null +++ b/substrate/frame/assets/src/migration/v1.rs @@ -0,0 +1,172 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{AssetStatus, Config, DepositBalanceOf, Pallet, LOG_TARGET}; +use frame_support::{ + pallet_prelude::*, sp_runtime::traits::Saturating, traits::OnRuntimeUpgrade, weights::Weight, +}; +use log; + +#[cfg(feature = "try-runtime")] +use crate::Vec; +#[cfg(feature = "try-runtime")] +use sp_runtime::TryRuntimeError; + +mod old { + use super::*; + + #[derive(Decode)] + pub struct AssetDetails { + pub owner: AccountId, + pub issuer: AccountId, + pub admin: AccountId, + pub freezer: AccountId, + pub supply: Balance, + pub deposit: DepositBalance, + pub min_balance: Balance, + pub is_sufficient: bool, + pub accounts: u32, + pub sufficients: u32, + pub approvals: u32, + pub is_frozen: bool, + } + + impl AssetDetails { + pub(super) fn migrate_to_v1( + self, + ) -> super::AssetDetails { + let status = if self.is_frozen { AssetStatus::Frozen } else { AssetStatus::Live }; + + super::AssetDetails { + owner: self.owner, + issuer: self.issuer, + admin: self.admin, + freezer: self.freezer, + supply: self.supply, + deposit: self.deposit, + min_balance: self.min_balance, + is_sufficient: self.is_sufficient, + accounts: self.accounts, + sufficients: self.sufficients, + approvals: self.approvals, + status, + } + } + } +} + +#[frame_support::storage_alias] +/// Details of an asset. +pub(crate) type Asset, I: 'static> = StorageMap< + Pallet, + Blake2_128Concat, + >::AssetId, + AssetDetails< + >::Balance, + ::AccountId, + DepositBalanceOf, + >, +>; + +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub(crate) struct AssetDetails { + pub owner: AccountId, + pub issuer: AccountId, + pub admin: AccountId, + pub freezer: AccountId, + pub supply: Balance, + pub deposit: DepositBalance, + pub min_balance: Balance, + pub is_sufficient: bool, + pub accounts: u32, + pub sufficients: u32, + pub approvals: u32, + pub status: AssetStatus, +} + +pub struct MigrateToV1(core::marker::PhantomData<(T, I)>); +impl, I: 'static> OnRuntimeUpgrade for MigrateToV1 { + fn on_runtime_upgrade() -> Weight { + let in_code_version = Pallet::::in_code_storage_version(); + let on_chain_version = Pallet::::on_chain_storage_version(); + if on_chain_version == 0 && in_code_version == 1 { + let mut translated = 0u64; + Asset::::translate::< + old::AssetDetails>, + _, + >(|_key, old_value| { + translated.saturating_inc(); + Some(old_value.migrate_to_v1()) + }); + in_code_version.put::>(); + log::info!( + target: LOG_TARGET, + "Upgraded {} pools, storage to version {:?}", + translated, + in_code_version + ); + translated.saturating_inc(); + + T::DbWeight::get().reads_writes(translated, translated) + } else { + log::info!( + target: LOG_TARGET, + "Migration did not execute. This probably should be removed" + ); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + frame_support::ensure!( + Pallet::::on_chain_storage_version() == 0, + "must upgrade linearly" + ); + let prev_count = Asset::::iter().count(); + Ok((prev_count as u32).encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(prev_count: Vec) -> Result<(), TryRuntimeError> { + let prev_count: u32 = Decode::decode(&mut prev_count.as_slice()) + .expect("the state parameter should be something that was generated by pre_upgrade"); + let post_count = Asset::::iter().count() as u32; + ensure!( + prev_count == post_count, + "the asset count before and after the migration should be the same" + ); + + let in_code_version = Pallet::::in_code_storage_version(); + let on_chain_version = Pallet::::on_chain_storage_version(); + + frame_support::ensure!(in_code_version == 1, "must_upgrade"); + frame_support::ensure!( + in_code_version == on_chain_version, + "after migration, the in_code_version and on_chain_version should be the same" + ); + + Asset::::iter().try_for_each(|(_id, asset)| -> Result<(), TryRuntimeError> { + ensure!( + asset.status == AssetStatus::Live || asset.status == AssetStatus::Frozen, + "assets should only be live or frozen. None should be in destroying status, or undefined state" + ); + Ok(()) + })?; + Ok(()) + } +} diff --git a/substrate/frame/assets/src/migration/v2.rs b/substrate/frame/assets/src/migration/v2.rs new file mode 100644 index 000000000000..3150fbbf30cb --- /dev/null +++ b/substrate/frame/assets/src/migration/v2.rs @@ -0,0 +1,190 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{Account, Asset, AssetDetails, AssetStatus, Config, DepositBalanceOf, Pallet, Vec}; +use frame_support::{ + pallet_prelude::*, + sp_runtime::traits::{Saturating, Zero}, + traits::OnRuntimeUpgrade, + weights::Weight, +}; + +#[cfg(feature = "try-runtime")] +use sp_runtime::TryRuntimeError; + +pub mod old { + use super::*; + + #[frame_support::storage_alias] + /// Details of an asset. + pub(crate) type Asset, I: 'static> = StorageMap< + Pallet, + Blake2_128Concat, + >::AssetId, + AssetDetails< + >::Balance, + ::AccountId, + DepositBalanceOf, + >, + >; + + #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] + pub(crate) struct AssetDetails { + pub owner: AccountId, + pub issuer: AccountId, + pub admin: AccountId, + pub freezer: AccountId, + pub supply: Balance, + pub deposit: DepositBalance, + pub min_balance: Balance, + pub is_sufficient: bool, + pub accounts: u32, + pub sufficients: u32, + pub approvals: u32, + pub status: AssetStatus, + } + + impl AssetDetails + where + Balance: Zero, + { + pub(super) fn migrate_to_v2( + self, + ) -> super::AssetDetails { + super::AssetDetails { + owner: self.owner, + issuer: self.issuer, + admin: self.admin, + freezer: self.freezer, + supply: self.supply, + inactive: Zero::zero(), + deposit: self.deposit, + min_balance: self.min_balance, + is_sufficient: self.is_sufficient, + accounts: self.accounts, + sufficients: self.sufficients, + approvals: self.approvals, + status: self.status, + } + } + } +} + +/// This migration moves all the state to v2 of Assets +pub struct VersionUncheckedMigrateToV2< + T: Config, + I: 'static, + A: Get>, +>(core::marker::PhantomData<(T, I, A)>); + +impl, I: 'static, A: Get>> OnRuntimeUpgrade + for VersionUncheckedMigrateToV2 +{ + fn on_runtime_upgrade() -> Weight { + let mut translated = 0u64; + + Asset::::translate::< + old::AssetDetails>, + _, + >(|_asset_id, old_value| { + translated.saturating_inc(); + Some(old_value.migrate_to_v2()) + }); + + let mut reads = 0u64; + for (asset_id, account) in A::get() { + reads.saturating_inc(); + let Some(acc) = Account::::get(&asset_id, &account) else { + log::info!( + "inactive migration: account {:?} not found for asset {:?}", + account, + asset_id + ); + continue + }; + Asset::::mutate(&asset_id, |asset| match asset { + Some(asset) => asset.inactive.saturating_accrue(acc.balance), + None => log::info!("inactive migration: asset {:?} not found", asset_id), + }); + } + + T::DbWeight::get().reads_writes(translated.saturating_add(reads), translated) + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + log::info!("pre-migration assets v2"); + Ok((old::Asset::::iter().collect::>()).encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(old_status: Vec) -> Result<(), TryRuntimeError> { + log::info!("post-migration assets v2"); + + let mut old_assets: Vec<( + T::AssetId, + old::AssetDetails>, + )> = Decode::decode(&mut old_status.as_slice()).map_err(|_| { + "the state parameter should be something that was generated by pre_upgrade" + })?; + + ensure!( + old_assets.len() == Asset::::iter().count(), + "the number of assets should be the same" + ); + + let new_assets = + old_assets.drain(..).map(|(k, v)| (k, v.migrate_to_v2())).collect::>(); + + let inactives = A::get().iter().fold( + Vec::<(T::AssetId, T::Balance)>::new(), + |mut acc, (asset_id, account)| { + let Some(details) = Account::::get(&asset_id, &account) else { return acc }; + match acc.iter().position(|(id, _)| id == asset_id) { + Some(idx) => acc[idx].1.saturating_accrue(details.balance), + None => acc.push((asset_id.clone(), details.balance)), + } + acc + }, + ); + + for (asset_id, mut asset) in new_assets { + let inactive = inactives + .iter() + .find(|(id, _)| *id == asset_id) + .map(|(_, b)| *b) + .unwrap_or_else(|| Zero::zero()); + asset.inactive = inactive; + ensure!( + Asset::::get(&asset_id) == Some(asset), + "migrated asset does not match expected inactive value" + ); + } + + Ok(()) + } +} + +/// [`VersionUncheckedMigrateToV2`] wrapped in a [`frame_support::migrations::VersionedMigration`], +/// ensuring the migration is only performed when on-chain version is 1. +pub type MigrateToV2 = frame_support::migrations::VersionedMigration< + 1, + 2, + VersionUncheckedMigrateToV2, + crate::pallet::Pallet, + ::DbWeight, +>; diff --git a/substrate/frame/assets/src/tests.rs b/substrate/frame/assets/src/tests.rs index c7021bcad531..abe2dc9bf5df 100644 --- a/substrate/frame/assets/src/tests.rs +++ b/substrate/frame/assets/src/tests.rs @@ -1777,3 +1777,79 @@ fn asset_destroy_refund_existence_deposit() { assert_eq!(Balances::reserved_balance(&admin), 0); }); } + +#[cfg(all(feature = "try-runtime", test))] +#[test] +fn migrate_to_v2_works() { + new_test_ext().execute_with(|| { + use crate::migration::v2::old as v1; + use frame_support::{pallet_prelude::StorageVersion, traits::OnRuntimeUpgrade}; + use sp_runtime::traits::Get; + StorageVersion::new(1).put::>(); + + // Create an asset with id 1 + v1::Asset::::insert( + 1, + v1::AssetDetails::< + ::Balance, + ::AccountId, + DepositBalanceOf, + > { + owner: 1, + issuer: 1, + admin: 1, + freezer: 1, + supply: 102, + deposit: 0, + min_balance: 1, + is_sufficient: false, + accounts: 0, + sufficients: 0, + approvals: 0, + status: AssetStatus::Live, + }, + ); + + // Populate balances of asset 1 + Account::::insert( + 1, + 1, + AssetAccountOf:: { + balance: 100, + status: AccountStatus::Liquid, + reason: ExistenceReason::<_, _>::Sufficient, + extra: Default::default(), + }, + ); + Account::::insert( + 1, + 2, + AssetAccountOf:: { + balance: 2, + status: AccountStatus::Liquid, + reason: ExistenceReason::<_, _>::Sufficient, + extra: Default::default(), + }, + ); + + // Define a constant Vec for migration. + // This is a workaround for the fact that we can't implement Get for Vec. + struct ConstVecForMigrationToV2; + impl Get> for ConstVecForMigrationToV2 { + fn get() -> Vec<(u32, u64)> { + // AssetId 1, AccountId 2 will be inactive. + vec![(1, 2)] + } + } + + // Run migration. + assert_ok!(crate::migration::v2::MigrateToV2::< + Test, + (), + ConstVecForMigrationToV2, + >::try_on_runtime_upgrade(true)); + + // Total inactive balance of asset 1 should be 2. + assert_eq!(Asset::::get(1).unwrap().inactive, 2); + }); +} diff --git a/substrate/frame/assets/src/types.rs b/substrate/frame/assets/src/types.rs index 11edc7d3fcb5..699c63c2add8 100644 --- a/substrate/frame/assets/src/types.rs +++ b/substrate/frame/assets/src/types.rs @@ -60,6 +60,8 @@ pub struct AssetDetails { pub(super) freezer: AccountId, /// The total supply across all accounts. pub(super) supply: Balance, + /// The total units of deactivated supply across all accounts. + pub(super) inactive: Balance, /// The balance deposited for this asset. This pays for the data stored here. pub(super) deposit: DepositBalance, /// The ED for virtual accounts.